From a9ae3bed0d954e7eae8c083dd481be1ffe5d461b Mon Sep 17 00:00:00 2001
From: LAOGOU-666 <441059767@qq.com>
Date: Sun, 23 Mar 2025 02:56:46 +0800
Subject: [PATCH] update
---
pyproject.toml | 2 +-
web/groupexecutor.js | 144 ++------------------------
web/groupexecutorUI.js | 224 ++++-------------------------------------
3 files changed, 30 insertions(+), 340 deletions(-)
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 = `
`;
-
- // 添加样式
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