diff --git a/pyproject.toml b/pyproject.toml index fa82c83..c7ca451 100644 --- a/pyproject.toml +++ b/pyproject.toml @@ -1,7 +1,7 @@ [project] name = "comfyui_lg_groupexecutor" description = "A ComfyUI extension for controlling and managing node group execution flow. Features include single/multiple group execution, delay control, signal chaining, and execution list repetition processing." -version = "1.1.0" +version = "1.1.1" dependencies = [""] [project.urls] diff --git a/web/groupexecutor.js b/web/groupexecutor.js index e7f48bc..b2625e7 100644 --- a/web/groupexecutor.js +++ b/web/groupexecutor.js @@ -1,9 +1,8 @@ import { app } from "../../scripts/app.js"; import { ComfyWidgets } from "../../scripts/widgets.js"; import { api } from "../../scripts/api.js"; - class BaseNode extends LGraphNode { - static defaultComfyClass = "BaseNode"; + static defaultComfyClass = "BaseNode"; constructor(title, comfyClass) { super(title); this.isVirtualNode = false; @@ -11,27 +10,23 @@ class BaseNode extends LGraphNode { this.__constructed__ = false; this.widgets = this.widgets || []; this.properties = this.properties || {}; - this.comfyClass = comfyClass || this.constructor.comfyClass || BaseNode.defaultComfyClass; setTimeout(() => { this.checkAndRunOnConstructed(); }); } - checkAndRunOnConstructed() { if (!this.__constructed__) { this.onConstructed(); } return this.__constructed__; } - onConstructed() { if (this.__constructed__) return false; this.type = this.type ?? undefined; this.__constructed__ = true; return this.__constructed__; } - configure(info) { this.configuring = true; super.configure(info); @@ -50,32 +45,25 @@ class BaseNode extends LGraphNode { } } } - class GroupExecutorNode extends BaseNode { static type = "GroupExecutor"; static title = "Group Executor"; static category = "🎈LAOGOU"; static _category = "🎈LAOGOU"; - constructor(title = GroupExecutorNode.title) { super(title, null); - this.isVirtualNode = true; this.addProperty("groupCount", 1, "int"); this.addProperty("groups", [], "array"); this.addProperty("isExecuting", false, "boolean"); this.addProperty("repeatCount", 1, "int"); - this.addProperty("delaySeconds", 0, "number"); // 新增:延迟时间属性 - - // 组数量组件 + this.addProperty("delaySeconds", 0, "number"); const groupCountWidget = ComfyWidgets["INT"](this, "groupCount", ["INT", { min: 1, max: 10, step: 1, default: 1 }], app); - - // 重复次数组件 const repeatCountWidget = ComfyWidgets["INT"](this, "repeatCount", ["INT", { min: 1, max: 100, @@ -84,18 +72,14 @@ class GroupExecutorNode extends BaseNode { label: "Repeat Count", tooltip: "执行重复次数" }], app); - - // 新增:延迟时间组件 const delayWidget = ComfyWidgets["FLOAT"](this, "delaySeconds", ["FLOAT", { min: 0, - max: 300, // 最大300秒 + max: 300, step: 0.1, default: 0, label: "Delay (s)", tooltip: "队列之间的延迟时间(秒)" }], app); - - // 调整组件顺序 if (repeatCountWidget.widget && delayWidget.widget) { const widgets = [repeatCountWidget.widget, delayWidget.widget]; widgets.forEach((widget, index) => { @@ -106,38 +90,24 @@ class GroupExecutorNode extends BaseNode { } }); } - - // 监听值变化 groupCountWidget.widget.callback = (v) => { this.properties.groupCount = Math.max(1, Math.min(10, parseInt(v) || 1)); this.updateGroupWidgets(); }; - repeatCountWidget.widget.callback = (v) => { this.properties.repeatCount = Math.max(1, Math.min(100, parseInt(v) || 1)); }; - delayWidget.widget.callback = (v) => { this.properties.delaySeconds = Math.max(0, Math.min(300, parseFloat(v) || 0)); }; - - // 添加执行按钮 this.addWidget("button", "Execute Groups", "Execute", () => { this.executeGroups(); }); - - // 添加取消按钮(放在执行按钮后面) this.addWidget("button", "Cancel", "Cancel", () => { this.cancelExecution(); }); - - // 添加取消状态属性 this.addProperty("isCancelling", false, "boolean"); - - // 初始化组选择下拉框 this.updateGroupWidgets(); - - // 监听画布变化以更新组列表 const self = this; app.canvas.onDrawBackground = (() => { const original = app.canvas.onDrawBackground; @@ -146,26 +116,17 @@ class GroupExecutorNode extends BaseNode { return original?.apply(this, arguments); }; })(); - - // 保存原始标题 this.originalTitle = title; } - - // 获取所有组名称 getGroupNames() { return [...app.graph._groups].map(g => g.title).sort(); } - - // 获取指定组的输出节点 getGroupOutputNodes(groupName) { - // 找到指定名称的组 const group = app.graph._groups.find(g => g.title === groupName); if (!group) { console.warn(`[GroupExecutor] 未找到名为 "${groupName}" 的组`); return []; } - - // 获取组内的所有节点 const groupNodes = []; for (const node of app.graph._nodes) { if (!node || !node.pos) continue; @@ -174,49 +135,34 @@ class GroupExecutorNode extends BaseNode { } } group._nodes = groupNodes; - - // 获取输出节点 return this.getOutputNodes(group._nodes); } - - // 获取输出节点 getOutputNodes(nodes) { return nodes.filter((n) => { - return n.mode !== LiteGraph.NEVER && + return n.mode !== LiteGraph.NEVER && n.constructor.nodeData?.output_node === true; }); } - - // 更新组选择下拉框 updateGroupWidgets() { - // 保留已有的组选择,只初始化新增的 const currentGroups = [...this.properties.groups]; - this.properties.groups = new Array(this.properties.groupCount).fill("").map((_, i) => + this.properties.groups = new Array(this.properties.groupCount).fill("").map((_, i) => currentGroups[i] || "" ); - - // 1. 首先移除所有组选择下拉框 - this.widgets = this.widgets.filter(w => - w.name === "groupCount" || - w.name === "repeatCount" || + this.widgets = this.widgets.filter(w => + w.name === "groupCount" || + w.name === "repeatCount" || w.name === "delaySeconds" || w.name === "Execute Groups" || w.name === "Cancel" ); - - // 2. 获取执行按钮和取消按钮的引用 const executeButton = this.widgets.find(w => w.name === "Execute Groups"); const cancelButton = this.widgets.find(w => w.name === "Cancel"); - - // 3. 如果存在这些按钮,先从widgets中移除 if (executeButton) { this.widgets = this.widgets.filter(w => w.name !== "Execute Groups"); } if (cancelButton) { this.widgets = this.widgets.filter(w => w.name !== "Cancel"); } - - // 4. 添加组选择下拉框 const groupNames = this.getGroupNames(); for (let i = 0; i < this.properties.groupCount; i++) { const widget = this.addWidget( @@ -231,20 +177,14 @@ class GroupExecutorNode extends BaseNode { } ); } - - // 5. 最后重新添加执行按钮和取消按钮 if (executeButton) { this.widgets.push(executeButton); } if (cancelButton) { this.widgets.push(cancelButton); } - - // 6. 更新节点大小 this.size = this.computeSize(); } - - // 更新组列表 updateGroupList() { const groups = this.getGroupNames(); this.widgets.forEach(w => { @@ -253,99 +193,71 @@ class GroupExecutorNode extends BaseNode { } }); } - - // 添加延迟函数 async delay(seconds) { if (seconds <= 0) return; return new Promise(resolve => setTimeout(resolve, seconds * 1000)); } - - // 更新状态显示 updateStatus(text) { - // 更新节点标题来显示状态 this.title = `${this.originalTitle} - ${text}`; this.setDirtyCanvas(true, true); } - - // 重置状态显示 resetStatus() { this.title = this.originalTitle; this.setDirtyCanvas(true, true); } - - // 修改取消方法 async cancelExecution() { if (!this.properties.isExecuting) { console.warn('[GroupExecutor] 没有正在执行的任务'); return; } - try { this.properties.isCancelling = true; this.updateStatus("已取消"); - - // 中断当前执行的队列 await fetch('/interrupt', { method: 'POST' }); - - // 不需要清理待执行队列,因为通过 isCancelling 标志会阻止新队列的添加 - } catch (error) { console.error('[GroupExecutor] 取消执行时出错:', error); this.updateStatus(`取消失败: ${error.message}`); } } - - // 修改执行方法,添加取消检查 async executeGroups() { if (this.properties.isExecuting) { console.warn('[GroupExecutor] 已有执行任务在进行中'); return; } - this.properties.isExecuting = true; this.properties.isCancelling = false; const totalSteps = this.properties.repeatCount * this.properties.groupCount; let currentStep = 0; - try { for (let repeat = 0; repeat < this.properties.repeatCount; repeat++) { for (let i = 0; i < this.properties.groupCount; i++) { - // 检查是否已取消 if (this.properties.isCancelling) { console.log('[GroupExecutor] 执行被用户取消'); - // 中断当前执行的队列 await fetch('/interrupt', { method: 'POST' }); this.updateStatus("已取消"); setTimeout(() => this.resetStatus(), 2000); return; } - const groupName = this.properties.groups[i]; if (!groupName) continue; - currentStep++; this.updateStatus( `${currentStep}/${totalSteps} - ${groupName}` ); - const outputNodes = this.getGroupOutputNodes(groupName); if (outputNodes && outputNodes.length > 0) { try { if (!rgthree || !rgthree.queueOutputNodes) { throw new Error('rgthree.queueOutputNodes 不可用'); } - const nodeIds = outputNodes.map(n => n.id); - try { - // 在每个队列执行前检查取消状态 if (this.properties.isCancelling) { return; } await rgthree.queueOutputNodes(nodeIds); await this.waitForQueue(); } catch (queueError) { - // 在回退执行前检查取消状态 if (this.properties.isCancelling) { return; } @@ -360,9 +272,7 @@ class GroupExecutorNode extends BaseNode { } } } - if (i < this.properties.groupCount - 1) { - // 延迟前检查是否取消 if (this.properties.isCancelling) { return; } @@ -377,16 +287,13 @@ class GroupExecutorNode extends BaseNode { } } } - if (repeat < this.properties.repeatCount - 1) { - // 重复前检查是否取消 if (this.properties.isCancelling) { return; } await this.delay(this.properties.delaySeconds); } } - if (!this.properties.isCancelling) { this.updateStatus("完成"); setTimeout(() => this.resetStatus(), 2000); @@ -400,25 +307,20 @@ class GroupExecutorNode extends BaseNode { this.properties.isCancelling = false; } } - - // 获取队列状态 async getQueueStatus() { try { const response = await fetch('/queue'); const data = await response.json(); - return { isRunning: data.queue_running.length > 0, isPending: data.queue_pending.length > 0, runningCount: data.queue_running.length, pendingCount: data.queue_pending.length, - // 保存原始数据用于调试 rawRunning: data.queue_running, rawPending: data.queue_pending }; } catch (error) { console.error('[GroupExecutor] 获取队列状态失败:', error); - // 返回默认状态而不是 null,避免空值判断 return { isRunning: false, isPending: false, @@ -429,50 +331,35 @@ class GroupExecutorNode extends BaseNode { }; } } - - // 等待队列完成 async waitForQueue() { return new Promise((resolve, reject) => { const checkQueue = async () => { try { const status = await this.getQueueStatus(); - - // 只有当运行队列和等待队列都为空时才算完成 if (!status.isRunning && !status.isPending) { - // 额外等待100ms确保状态完全更新 setTimeout(resolve, 100); return; } - - // 继续检查队列状态,使用较短的间隔以提高响应性 setTimeout(checkQueue, 500); } catch (error) { console.warn(`[GroupExecutor] 检查队列状态失败:`, error); - // 发生错误时继续检查,而不是中断 setTimeout(checkQueue, 500); } }; - - // 开始检查队列 checkQueue(); }); } - - // 修改计算节点大小方法以适应新按钮 computeSize() { const widgetHeight = 28; const padding = 4; const width = 200; - const height = (this.properties.groupCount + 4) * widgetHeight + padding * 2; // +4 包含重复次数组件和取消按钮 + const height = (this.properties.groupCount + 4) * widgetHeight + padding * 2; return [width, height]; } - static setUp() { LiteGraph.registerNodeType(this.type, this); this.category = this._category; } - - // 序列化节点数据 serialize() { const data = super.serialize(); data.properties = { @@ -485,11 +372,8 @@ class GroupExecutorNode extends BaseNode { }; return data; } - - // 反序列化节点数据 configure(info) { super.configure(info); - if (info.properties) { this.properties.groupCount = parseInt(info.properties.groupCount) || 1; this.properties.groups = info.properties.groups ? [...info.properties.groups] : []; @@ -497,7 +381,6 @@ class GroupExecutorNode extends BaseNode { this.properties.repeatCount = parseInt(info.properties.repeatCount) || 1; this.properties.delaySeconds = parseFloat(info.properties.delaySeconds) || 0; } - this.widgets.forEach(w => { if (w.name === "groupCount") { w.value = this.properties.groupCount; @@ -507,21 +390,14 @@ class GroupExecutorNode extends BaseNode { w.value = this.properties.delaySeconds; } }); - if (!this.configuring) { this.updateGroupWidgets(); } } } - - app.registerExtension({ name: "GroupExecutor", registerCustomNodes() { GroupExecutorNode.setUp(); } -}); - - - - +}); \ No newline at end of file diff --git a/web/groupexecutorUI.js b/web/groupexecutorUI.js index 7194060..641109e 100644 --- a/web/groupexecutorUI.js +++ b/web/groupexecutorUI.js @@ -1,12 +1,8 @@ import { app } from "../../scripts/app.js"; import { api } from "../../scripts/api.js"; - - class GroupExecutorUI { - // 使用默认值初始化静态属性 static DOCK_MARGIN_X = 0; static DOCK_MARGIN_Y = 40; - constructor() { this.container = null; this.isExecuting = false; @@ -15,26 +11,17 @@ class GroupExecutorUI { this.position = { x: 0, y: 0 }; this.isDragging = false; this.dragOffset = { x: 0, y: 0 }; - // 从静态属性获取边距值 this.DOCK_MARGIN_X = GroupExecutorUI.DOCK_MARGIN_X; this.DOCK_MARGIN_Y = GroupExecutorUI.DOCK_MARGIN_Y; - this.createUI(); this.attachEvents(); - - // 保存实例引用 this.container.instance = this; } - createUI() { - // 创建主容器 this.container = document.createElement('div'); this.container.className = 'group-executor-ui'; - - // 设置初始位置,使用当前的边距值 this.container.style.top = `${this.DOCK_MARGIN_Y}px`; this.container.style.right = `${this.DOCK_MARGIN_X}px`; - this.container.innerHTML = `
组执行管理器 @@ -72,15 +59,13 @@ class GroupExecutorUI {
`; - - // 添加样式 const style = document.createElement('style'); style.textContent = ` .group-executor-ui { position: fixed; top: 20px; right: 20px; - width: 300px !important; /* 强制固定宽度 */ + width: 300px !important; min-width: 300px; max-width: 300px; background: #2a2a2a; @@ -90,9 +75,8 @@ class GroupExecutorUI { z-index: 1000; font-family: Arial, sans-serif; color: #fff; - user-select: none; /* 禁止文字选择 */ + user-select: none; } - .ge-header { display: flex; justify-content: space-between; @@ -104,7 +88,6 @@ class GroupExecutorUI { width: 100%; box-sizing: border-box; } - .ge-controls button { background: none; border: none; @@ -113,22 +96,18 @@ class GroupExecutorUI { cursor: pointer; font-size: 16px; } - .ge-content { padding: 12px; } - .ge-row { display: flex; align-items: center; margin-bottom: 12px; } - .ge-row label { flex: 1; margin-right: 12px; } - .ge-row input { width: 100px; padding: 4px 8px; @@ -137,11 +116,9 @@ class GroupExecutorUI { color: #fff; border-radius: 4px; } - .ge-groups-container { margin-bottom: 12px; } - .ge-group-select { width: 100%; margin-bottom: 8px; @@ -151,12 +128,10 @@ class GroupExecutorUI { color: #fff; border-radius: 4px; } - .ge-buttons { display: flex; gap: 8px; } - .ge-buttons button { flex: 1; padding: 8px; @@ -165,27 +140,22 @@ class GroupExecutorUI { cursor: pointer; font-weight: bold; } - .ge-execute-btn { background: #4CAF50; color: white; } - .ge-execute-btn:disabled { background: #2a5a2d; cursor: not-allowed; } - .ge-cancel-btn { background: #f44336; color: white; } - .ge-cancel-btn:disabled { background: #7a2520; cursor: not-allowed; } - .ge-status { margin: 12px 0; padding: 8px; @@ -194,52 +164,45 @@ class GroupExecutorUI { min-height: 20px; text-align: center; position: relative; - overflow: hidden; /* 确保进度条不会溢出 */ + overflow: hidden; } - .ge-status::before { content: ''; position: absolute; left: 0; top: 0; height: 100%; - width: var(--progress, 0%); /* 使用CSS变量控制宽度 */ - background: rgba(36, 145, 235, 0.8); /* 绿色半透明背景 */ + width: var(--progress, 0%); + background: rgba(36, 145, 235, 0.8); transition: width 0.3s ease; z-index: 0; } - .ge-status span { position: relative; - z-index: 1; /* 确保文字在进度条上层 */ + z-index: 1; } - .ge-minimized { width: auto !important; min-width: auto; } - .ge-minimized .ge-content { display: none; } - .ge-dock-menu { position: absolute; background: #333; border: 1px solid #444; border-radius: 4px; padding: 4px 0; - z-index: 1001; // 确保菜单在最上层 - visibility: hidden; // 改用 visibility 代替 display + z-index: 1001; + visibility: hidden; opacity: 0; transition: opacity 0.2s; } - .ge-dock-menu.visible { visibility: visible; opacity: 1; } - .ge-dock-menu button { display: block; width: 100%; @@ -250,25 +213,21 @@ class GroupExecutorUI { text-align: left; cursor: pointer; } - .ge-dock-menu button:hover { background: #444; } - .ge-title { flex: 1; - pointer-events: none; /* 让鼠标事件穿透到父元素 */ - white-space: nowrap; /* 防止文字换行 */ - overflow: hidden; /* 防止文字溢出 */ - text-overflow: ellipsis; /* 文字溢出时显示省略号 */ + pointer-events: none; + white-space: nowrap; + overflow: hidden; + text-overflow: ellipsis; } - .ge-config-row { display: flex; gap: 8px; margin-bottom: 12px; } - .ge-config-select { flex: 1; padding: 4px 8px; @@ -277,7 +236,6 @@ class GroupExecutorUI { color: #fff; border-radius: 4px; } - .ge-save-config, .ge-delete-config { background: #333; @@ -287,27 +245,21 @@ class GroupExecutorUI { border-radius: 4px; cursor: pointer; } - .ge-save-config:hover, .ge-delete-config:hover { background: #444; } - .ge-delete-config:disabled { opacity: 0.5; cursor: not-allowed; } `; - document.head.appendChild(style); document.body.appendChild(this.container); } - attachEvents() { - // 拖拽功能 const header = this.container.querySelector('.ge-header'); header.addEventListener('mousedown', (e) => { - // 检查点击的不是控制按钮 if (!e.target.matches('.ge-controls button')) { this.isDragging = true; const rect = this.container.getBoundingClientRect(); @@ -317,7 +269,6 @@ class GroupExecutorUI { }; } }); - document.addEventListener('mousemove', (e) => { if (this.isDragging) { const x = e.clientX - this.dragOffset.x; @@ -326,102 +277,68 @@ class GroupExecutorUI { this.container.style.top = `${y}px`; } }); - document.addEventListener('mouseup', () => { this.isDragging = false; }); - - // 停靠按钮 const dockBtn = this.container.querySelector('.ge-dock-btn'); dockBtn.addEventListener('click', () => { this.showDockMenu(dockBtn); }); - - // 最小化按钮 const minimizeBtn = this.container.querySelector('.ge-minimize-btn'); minimizeBtn.addEventListener('click', () => { this.container.classList.toggle('ge-minimized'); minimizeBtn.textContent = this.container.classList.contains('ge-minimized') ? '+' : '-'; }); - - // 关闭按钮 const closeBtn = this.container.querySelector('.ge-close-btn'); closeBtn.addEventListener('click', () => { this.container.remove(); }); - - // 组数量变化 const groupCountInput = this.container.querySelector('.ge-group-count'); groupCountInput.addEventListener('change', () => { this.updateGroupSelects(parseInt(groupCountInput.value)); }); - - // 执行按钮 const executeBtn = this.container.querySelector('.ge-execute-btn'); executeBtn.addEventListener('click', () => { this.executeGroups(); }); - - // 取消按钮 const cancelBtn = this.container.querySelector('.ge-cancel-btn'); cancelBtn.addEventListener('click', () => { this.cancelExecution(); }); - - // 初始化组选择 this.updateGroupSelects(1); - - // 添加窗口大小变化的监听 window.addEventListener('resize', () => { this.ensureInViewport(); }); - - // 配置相关事件 const deleteConfigBtn = this.container.querySelector('.ge-delete-config'); const saveConfigBtn = this.container.querySelector('.ge-save-config'); const configSelect = this.container.querySelector('.ge-config-select'); - - // 更新删除按钮状态 const updateDeleteButton = () => { deleteConfigBtn.disabled = !configSelect.value; }; - - // 配置选择事件 configSelect.addEventListener('change', () => { updateDeleteButton(); if (configSelect.value) { this.loadConfig(configSelect.value); } }); - - // 保存配置事件 saveConfigBtn.addEventListener('click', () => { this.saveCurrentConfig(); }); - - // 删除配置事件 deleteConfigBtn.addEventListener('click', () => { const configName = configSelect.value; if (configName) { this.deleteConfig(configName); } }); - - // 初始化删除按钮状态 updateDeleteButton(); - - // 初始化加载配置列表 this.loadConfigs(); } - showDockMenu(button) { - // 移除已存在的菜单 const existingMenu = document.querySelector('.ge-dock-menu'); if (existingMenu) { existingMenu.remove(); return; } - const menu = document.createElement('div'); menu.className = 'ge-dock-menu'; menu.innerHTML = ` @@ -430,32 +347,22 @@ class GroupExecutorUI { `; - - // 将菜单添加到容器内部而不是 body this.container.appendChild(menu); - - // 计算菜单位置 const buttonRect = button.getBoundingClientRect(); const containerRect = this.container.getBoundingClientRect(); - menu.style.left = `${buttonRect.left - containerRect.left}px`; menu.style.top = `${buttonRect.bottom - containerRect.top + 5}px`; - - // 使用 requestAnimationFrame 确保过渡动画正常工作 requestAnimationFrame(() => { menu.classList.add('visible'); }); - menu.addEventListener('click', (e) => { const position = e.target.dataset.position; if (position) { this.dockTo(position); menu.classList.remove('visible'); - setTimeout(() => menu.remove(), 200); // 等待过渡动画完成后移除 + setTimeout(() => menu.remove(), 200); } }); - - // 点击其他地方关闭菜单 const closeMenu = (e) => { if (!menu.contains(e.target) && e.target !== button) { menu.classList.remove('visible'); @@ -463,21 +370,15 @@ class GroupExecutorUI { document.removeEventListener('click', closeMenu); } }; - - // 延迟添加点击监听,避免立即触发 setTimeout(() => { document.addEventListener('click', closeMenu); }, 0); } - dockTo(position) { const style = this.container.style; style.transition = 'all 0.3s ease'; - - // 使用实例的边距值 const marginX = this.DOCK_MARGIN_X; const marginY = this.DOCK_MARGIN_Y; - switch (position) { case 'top-left': style.top = `${marginY}px`; @@ -504,18 +405,14 @@ class GroupExecutorUI { style.top = 'auto'; break; } - setTimeout(() => { style.transition = ''; }, 300); } - updateGroupSelects(count) { const container = this.container.querySelector('.ge-groups-container'); container.innerHTML = ''; - const groupNames = this.getGroupNames(); - for (let i = 0; i < count; i++) { const select = document.createElement('select'); select.className = 'ge-group-select'; @@ -526,41 +423,33 @@ class GroupExecutorUI { container.appendChild(select); } } - getGroupNames() { return [...app.graph._groups].map(g => g.title).sort(); } - updateStatus(text, progress = null) { const status = this.container.querySelector('.ge-status'); - status.innerHTML = `${text}`; // 将文本包装在 span 中 - + status.innerHTML = `${text}`; if (progress !== null) { status.style.setProperty('--progress', `${progress}%`); } } - async executeGroups() { if (this.isExecuting) { console.warn('[GroupExecutorUI] 已有执行任务在进行中'); return; } - const executeBtn = this.container.querySelector('.ge-execute-btn'); const cancelBtn = this.container.querySelector('.ge-cancel-btn'); const groupSelects = [...this.container.querySelectorAll('.ge-group-select')]; const repeatCount = parseInt(this.container.querySelector('.ge-repeat-count').value); const delaySeconds = parseFloat(this.container.querySelector('.ge-delay').value); - this.isExecuting = true; this.isCancelling = false; executeBtn.disabled = true; cancelBtn.disabled = false; - const selectedGroups = groupSelects.map(select => select.value).filter(Boolean); const totalSteps = repeatCount * selectedGroups.length; let currentStep = 0; - try { for (let repeat = 0; repeat < repeatCount; repeat++) { for (let i = 0; i < selectedGroups.length; i++) { @@ -570,15 +459,12 @@ class GroupExecutorUI { this.updateStatus("已取消"); break; } - const groupName = selectedGroups[i]; currentStep++; const progress = (currentStep / totalSteps) * 100; this.updateStatus(`${currentStep}/${totalSteps} - ${groupName}`, progress); - try { await this.executeGroup(groupName); - if (i < selectedGroups.length - 1 && delaySeconds > 0) { this.updateStatus(`等待 ${delaySeconds}s...`); await this.delay(delaySeconds); @@ -587,12 +473,10 @@ class GroupExecutorUI { throw new Error(`执行组 "${groupName}" 失败: ${error.message}`); } } - if (repeat < repeatCount - 1 && !this.isCancelling) { await this.delay(delaySeconds); } } - if (!this.isCancelling) { this.updateStatus("完成"); } @@ -607,13 +491,11 @@ class GroupExecutorUI { cancelBtn.disabled = true; } } - async executeGroup(groupName) { const group = app.graph._groups.find(g => g.title === groupName); if (!group) { throw new Error(`未找到名为 "${groupName}" 的组`); } - const outputNodes = []; for (const node of app.graph._nodes) { if (!node || !node.pos) continue; @@ -623,13 +505,10 @@ class GroupExecutorUI { } } } - if (outputNodes.length === 0) { throw new Error(`组 "${groupName}" 中没有找到输出节点`); } - const nodeIds = outputNodes.map(n => n.id); - try { if (!rgthree || !rgthree.queueOutputNodes) { throw new Error('rgthree.queueOutputNodes 不可用'); @@ -647,40 +526,34 @@ class GroupExecutorUI { } } } - async cancelExecution() { if (!this.isExecuting) { console.warn('[GroupExecutorUI] 没有正在执行的任务'); return; } - try { this.isCancelling = true; - this.updateStatus("已取消", 0); // 重置进度条为0 + this.updateStatus("已取消", 0); await fetch('/interrupt', { method: 'POST' }); } catch (error) { console.error('[GroupExecutorUI] 取消执行时出错:', error); - this.updateStatus(`取消失败: ${error.message}`, 0); // 错误时也重置进度条 + this.updateStatus(`取消失败: ${error.message}`, 0); } } - async getQueueStatus() { try { const response = await fetch('/queue'); const data = await response.json(); - return { isRunning: data.queue_running.length > 0, isPending: data.queue_pending.length > 0, runningCount: data.queue_running.length, pendingCount: data.queue_pending.length, - // 保存原始数据用于调试 rawRunning: data.queue_running, rawPending: data.queue_pending }; } catch (error) { console.error('[GroupExecutor] 获取队列状态失败:', error); - // 返回默认状态而不是 null,避免空值判断 return { isRunning: false, isPending: false, @@ -691,47 +564,32 @@ class GroupExecutorUI { }; } } - - // 等待队列完成 async waitForQueue() { return new Promise((resolve, reject) => { const checkQueue = async () => { try { const status = await this.getQueueStatus(); - - // 只有当运行队列和等待队列都为空时才算完成 if (!status.isRunning && !status.isPending) { - // 额外等待100ms确保状态完全更新 setTimeout(resolve, 100); return; } - - // 继续检查队列状态,使用较短的间隔以提高响应性 setTimeout(checkQueue, 500); } catch (error) { console.warn(`[GroupExecutor] 检查队列状态失败:`, error); - // 发生错误时继续检查,而不是中断 setTimeout(checkQueue, 500); } }; - - // 开始检查队列 checkQueue(); }); } - async delay(seconds) { if (seconds <= 0) return; return new Promise(resolve => setTimeout(resolve, seconds * 1000)); } - - // 添加一个方法来确保UI不会超出视窗 ensureInViewport() { const rect = this.container.getBoundingClientRect(); const windowWidth = window.innerWidth; const windowHeight = window.innerHeight; - - // 应用边距限制 if (this.container.style.right !== 'auto') { this.container.style.right = `${this.DOCK_MARGIN_X}px`; } @@ -745,19 +603,15 @@ class GroupExecutorUI { this.container.style.bottom = `${this.DOCK_MARGIN_Y}px`; } } - - // 添加配置相关方法 async loadConfigs() { try { const response = await api.fetchApi('/group_executor/configs', { method: 'GET' }); - const result = await response.json(); if (result.status === "error") { throw new Error(result.message); } - const select = this.container.querySelector('.ge-config-select'); select.innerHTML = ` @@ -768,11 +622,9 @@ class GroupExecutorUI { app.ui.dialog.show('加载配置失败: ' + error.message); } } - async saveCurrentConfig() { const configName = prompt('请输入配置名称:', '新配置'); if (!configName) return; - const config = { name: configName, groups: [...this.container.querySelectorAll('.ge-group-select')] @@ -781,12 +633,9 @@ class GroupExecutorUI { repeatCount: parseInt(this.container.querySelector('.ge-repeat-count').value), delay: parseFloat(this.container.querySelector('.ge-delay').value) }; - try { - // 添加JSON验证 const jsonString = JSON.stringify(config); - JSON.parse(jsonString); // 验证JSON是否有效 - + JSON.parse(jsonString); const response = await api.fetchApi('/group_executor/configs', { method: 'POST', headers: { @@ -794,12 +643,10 @@ class GroupExecutorUI { }, body: jsonString }); - const result = await response.json(); if (result.status === "error") { throw new Error(result.message); } - await this.loadConfigs(); app.ui.dialog.show('配置保存成功'); } catch (error) { @@ -807,30 +654,23 @@ class GroupExecutorUI { app.ui.dialog.show('保存配置失败: ' + error.message); } } - async loadConfig(configName) { try { const response = await api.fetchApi(`/group_executor/configs/${configName}`, { method: 'GET', - cache: 'no-store' // 禁用缓存 + cache: 'no-store' }); if (!response.ok) { throw new Error(`HTTP error! status: ${response.status}`); } const config = await response.json(); - - // 更新组数量 const groupCountInput = this.container.querySelector('.ge-group-count'); groupCountInput.value = config.groups.length; await this.updateGroupSelects(config.groups.length); - - // 设置组选择 const selects = this.container.querySelectorAll('.ge-group-select'); config.groups.forEach((group, index) => { if (selects[index]) selects[index].value = group; }); - - // 设置其他参数 this.container.querySelector('.ge-repeat-count').value = config.repeatCount; this.container.querySelector('.ge-delay').value = config.delay; } catch (error) { @@ -838,25 +678,19 @@ class GroupExecutorUI { app.ui.dialog.show('加载配置失败: ' + error.message); } } - - // 添加删除配置方法 async deleteConfig(configName) { if (!configName) return; - if (!confirm(`确定要删除配置 "${configName}" 吗?`)) { return; } - try { const response = await api.fetchApi(`/group_executor/configs/${configName}`, { method: 'DELETE' }); - const result = await response.json(); if (result.status === "error") { throw new Error(result.message); } - await this.loadConfigs(); app.ui.dialog.show('配置已删除'); } catch (error) { @@ -865,15 +699,10 @@ class GroupExecutorUI { } } } - -// 修改扩展注册部分 app.registerExtension({ name: "GroupExecutorUI", async setup() { - // 等待 UI 设置初始化完成 await app.ui.settings.setup; - - // 添加设置选项 app.ui.settings.addSetting({ id: "GroupExecutor.enabled", name: "显示组执行器按钮", @@ -886,7 +715,6 @@ app.registerExtension({ } } }); - app.ui.settings.addSetting({ id: "GroupExecutor.marginX", name: "组执行器水平边距", @@ -906,7 +734,6 @@ app.registerExtension({ }); } }); - app.ui.settings.addSetting({ id: "GroupExecutor.marginY", name: "组执行器垂直边距", @@ -926,9 +753,7 @@ app.registerExtension({ }); } }); - try { - // 尝试使用新版UI const btn = new (await import("../../scripts/ui/components/button.js")).ComfyButton({ icon: "layers-outline", action: () => { @@ -938,29 +763,20 @@ app.registerExtension({ content: "组执行器", classList: "comfyui-button comfyui-menu-mobile-collapse group-executor-btn" }).element; - - // 根据设置决定是否显示按钮 const enabled = app.ui.settings.getSettingValue("GroupExecutor.enabled", true); btn.style.display = enabled ? 'block' : 'none'; - app.menu?.actionsGroup.element.after(btn); } catch { - // 传统UI方式 const menu = document.querySelector(".comfy-menu"); const clearButton = document.getElementById("comfy-clear-button"); - const groupExecutorButton = document.createElement("button"); groupExecutorButton.textContent = "组执行器"; groupExecutorButton.classList.add("group-executor-btn"); - - // 根据设置决定是否显示按钮 const enabled = app.ui.settings.getSettingValue("GroupExecutor.enabled", true); groupExecutorButton.style.display = enabled ? 'block' : 'none'; - groupExecutorButton.addEventListener("click", () => { new GroupExecutorUI(); }); - if (clearButton) { menu.insertBefore(groupExecutorButton, clearButton); } else { @@ -968,6 +784,4 @@ app.registerExtension({ } } } -}); - - +}); \ No newline at end of file