This commit is contained in:
LAOGOU-666
2025-03-20 02:51:06 +08:00
parent 358ffc830c
commit 2f344e9e1a
7 changed files with 2633 additions and 0 deletions
+28
View File
@@ -0,0 +1,28 @@
from .py.lgutils import *
from .py.trans import *
WEB_DIRECTORY = "web"
NODE_CLASS_MAPPINGS = {
"GroupExecutorSingle": GroupExecutorSingle,
"GroupExecutorSender": GroupExecutorSender,
"GroupExecutorRepeater": GroupExecutorRepeater,
"MemoryCleanup": MemoryCleanup,
"LG_ImageSender": LG_ImageSender,
"LG_ImageReceiver": LG_ImageReceiver,
"ImageListSplitter": ImageListSplitter,
"MaskListSplitter": MaskListSplitter,
"ImageListRepeater": ImageListRepeater,
"MaskListRepeater": MaskListRepeater,
}
NODE_DISPLAY_NAME_MAPPINGS = {
"GroupExecutorSingle": "🎈GroupExecutorSingle",
"GroupExecutorSender": "🎈GroupExecutorSender",
"GroupExecutorRepeater": "🎈GroupExecutorRepeater",
"MemoryCleanup": "🎈Memory-Cleanup",
"LG_ImageSender": "🎈LG_ImageSender",
"LG_ImageReceiver": "🎈LG_ImageReceiver",
"ImageListSplitter": "🎈List-Image-Splitter",
"MaskListSplitter": "🎈List-Mask-Splitter",
"ImageListRepeater": "🎈List-Image-Repeater",
}
+140
View File
@@ -0,0 +1,140 @@
from server import PromptServer
class GroupExecutorSingle:
"""单组执行节点"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"group_name": ("STRING", {"default": "", "multiline": False}),
"repeat_count": ("INT", {"default": 1, "min": 1, "max": 100, "step": 1}),
"delay_seconds": ("FLOAT", {"default": 0.0, "min": 0.0, "max": 60.0, "step": 0.1}),
},
"optional": {
"signal": ("SIGNAL",),
},
"hidden": {
"unique_id": "UNIQUE_ID"
}
}
RETURN_TYPES = ("SIGNAL",)
FUNCTION = "execute_group"
CATEGORY = "🎈LAOGOU"
def execute_group(self, group_name, repeat_count, delay_seconds, unique_id, signal=None):
try:
current_execution = {
"group_name": group_name,
"repeat_count": repeat_count,
"delay_seconds": delay_seconds
}
# 如果有信号输入
if signal is not None:
if isinstance(signal, list):
signal.append(current_execution)
return (signal,)
else:
return ([signal, current_execution],)
return (current_execution,)
except Exception as e:
return ({"error": str(e)},)
class GroupExecutorSender:
"""执行信号发送节点"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"signal": ("SIGNAL",),
},
"hidden": {
"unique_id": "UNIQUE_ID"
}
}
RETURN_TYPES = ()
FUNCTION = "execute"
CATEGORY = "🎈LAOGOU"
OUTPUT_NODE = True
def execute(self, signal, unique_id):
try:
if not signal:
raise ValueError("没有收到执行信号")
execution_list = signal if isinstance(signal, list) else [signal]
PromptServer.instance.send_sync(
"execute_group_list", {
"node_id": unique_id,
"execution_list": execution_list
}
)
return ()
except Exception as e:
print(f"执行错误: {str(e)}")
return ()
class GroupExecutorRepeater:
"""执行列表重复处理节点"""
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"signal": ("SIGNAL",),
"repeat_count": ("INT", {
"default": 1,
"min": 1,
"max": 100,
"step": 1
}),
"group_delay": ("FLOAT", {
"default": 0.0,
"min": 0.0,
"max": 300.0,
"step": 0.1
}),
},
}
RETURN_TYPES = ("SIGNAL",)
FUNCTION = "repeat"
CATEGORY = "🎈LAOGOU"
def repeat(self, signal, repeat_count, group_delay):
try:
if not signal:
raise ValueError("没有收到执行信号")
execution_list = signal if isinstance(signal, list) else [signal]
repeated_list = []
for i in range(repeat_count):
repeated_list.extend(execution_list)
if i < repeat_count - 1:
repeated_list.append({
"group_name": "__delay__",
"repeat_count": 1,
"delay_seconds": group_delay
})
return (repeated_list,)
except Exception as e:
print(f"重复处理错误: {str(e)}")
return ([],)
+498
View File
@@ -0,0 +1,498 @@
from server import PromptServer
import os
import sys
import time
import torch
import numpy as np
from PIL import Image
import folder_paths
class AnyType(str):
"""用于表示任意类型的特殊类,在类型比较时总是返回相等"""
def __eq__(self, _) -> bool:
return True
def __ne__(self, __value: object) -> bool:
return False
any = AnyType("*")
class MemoryCleanup:
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"anything": (any, {}),
"offload_model": ("BOOLEAN", {"default": True}),
"offload_cache": ("BOOLEAN", {"default": True}),
},
"optional": {},
"hidden": {
"unique_id": "UNIQUE_ID",
"extra_pnginfo": "EXTRA_PNGINFO",
}
}
RETURN_TYPES = (any,)
RETURN_NAMES = ("output",)
OUTPUT_NODE = True
FUNCTION = "empty_cache"
CATEGORY = "Memory Management"
def empty_cache(self, anything, offload_model, offload_cache, unique_id=None, extra_pnginfo=None):
try:
# 发送信号到前端
PromptServer.instance.send_sync("memory_cleanup", {
"type": "cleanup_request",
"data": {
"unload_models": offload_model,
"free_memory": offload_cache
}
})
print("已发送内存清理信号")
except Exception as e:
print(f"发送内存清理信号出错: {str(e)}")
return (anything,)
class LG_ImageSender:
def __init__(self):
self.output_dir = folder_paths.get_temp_directory()
self.type = "temp"
self.compress_level = 1
@classmethod
def INPUT_TYPES(s):
return {
"required": {
"images": ("IMAGE", {"tooltip": "要发送的图像"}),
"filename_prefix": ("STRING", {"default": "lg_send"}),
"link_id": ("INT", {"default": 1, "min": 0, "max": sys.maxsize, "step": 1, "tooltip": "发送端连接ID"}),
"trigger_always": ("BOOLEAN", {"default": False, "tooltip": "开启后每次都会触发"})
},
"optional": {
"masks": ("MASK", {"tooltip": "要发送的遮罩"})
},
"hidden": {"prompt": "PROMPT", "extra_pnginfo": "EXTRA_PNGINFO"},
}
RETURN_TYPES = ()
OUTPUT_NODE = True
FUNCTION = "save_images"
CATEGORY = "🎈LAOGOU"
INPUT_IS_LIST = True
@classmethod
def IS_CHANGED(s, images, filename_prefix, link_id, trigger_always, masks=None, prompt=None, extra_pnginfo=None):
if isinstance(trigger_always, list):
trigger_always = trigger_always[0]
if trigger_always:
return float("NaN")
# 同时考虑图像和遮罩的变化
hash_value = hash(str(images) + str(masks))
return hash_value
def save_images(self, images, filename_prefix, link_id, trigger_always, masks=None, prompt=None, extra_pnginfo=None):
timestamp = int(time.time() * 1000)
results = list()
# 获取实际的值
filename_prefix = filename_prefix[0] if isinstance(filename_prefix, list) else filename_prefix
link_id = link_id[0] if isinstance(link_id, list) else link_id
trigger_always = trigger_always[0] if isinstance(trigger_always, list) else trigger_always
for idx, image_batch in enumerate(images):
try:
image = image_batch.squeeze()
# 转换为PIL图像
rgb_image = Image.fromarray(np.clip(255. * image.cpu().numpy(), 0, 255).astype(np.uint8))
# 获取对应的遮罩或创建空遮罩
if masks is not None and idx < len(masks):
mask = masks[idx].squeeze()
mask_img = Image.fromarray(np.clip(255. * (1 - mask.cpu().numpy()), 0, 255).astype(np.uint8))
else:
mask_img = Image.new('L', rgb_image.size, 255)
# 合并RGB和遮罩为RGBA
r, g, b = rgb_image.convert('RGB').split()
rgba_image = Image.merge('RGBA', (r, g, b, mask_img))
# 保存RGBA图像
filename = f"{filename_prefix}_{link_id}_{timestamp}_{idx}.png"
file_path = os.path.join(self.output_dir, filename)
print(f"[ImageSender] 发送图像: {filename}")
rgba_image.save(file_path, compress_level=self.compress_level)
results.append({
"filename": filename,
"subfolder": "",
"type": self.type
})
except Exception as e:
print(f"[ImageSender] 处理图像 {idx+1} 时出错: {str(e)}")
import traceback
traceback.print_exc()
continue
if results:
PromptServer.instance.send_sync("img-send", {
"link_id": link_id,
"images": results
})
return { "ui": { "images": results } }
class LG_ImageReceiver:
@classmethod
def INPUT_TYPES(s):
temp_dir = folder_paths.get_temp_directory()
files = [f for f in os.listdir(temp_dir) if os.path.isfile(os.path.join(temp_dir, f))]
return {
"required": {
"image": ("STRING", {"default": "", "multiline": False, "tooltip": "多个文件名用逗号分隔"}),
"link_id": ("INT", {"default": 1, "min": 0, "max": sys.maxsize, "step": 1, "tooltip": "发送端连接ID"}),
}
}
CATEGORY = "🎈LAOGOU"
RETURN_TYPES = ("IMAGE", "MASK")
RETURN_NAMES = ("images", "masks")
OUTPUT_IS_LIST = (True, True)
FUNCTION = "load_image"
def load_image(self, image, link_id):
image_files = [x.strip() for x in image.split(',') if x.strip()]
print(f"[ImageReceiver] 加载图像: {image_files}")
output_images = []
output_masks = []
if not image_files:
empty_image = torch.zeros((1, 64, 64, 3), dtype=torch.float32)
empty_mask = torch.zeros((1, 64, 64), dtype=torch.float32)
return ([empty_image], [empty_mask])
temp_dir = folder_paths.get_temp_directory()
for img_file in image_files:
try:
img_path = os.path.join(temp_dir, img_file)
if not os.path.exists(img_path):
continue
# 加载RGBA图像
img = Image.open(img_path)
if img.mode == 'RGBA':
# 分离RGB和Alpha通道
r, g, b, a = img.split()
# 合并RGB通道
rgb_image = Image.merge('RGB', (r, g, b))
# 转换为tensor
image = np.array(rgb_image).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
# 转换alpha为mask,并确保维度是 (B, H, W)
mask = np.array(a).astype(np.float32) / 255.0
mask = torch.from_numpy(mask)[None,] # 添加batch维度
# 反转遮罩值
mask = 1.0 - mask
else:
# 如果不是RGBA,按原来的方式处理
image = np.array(img.convert('RGB')).astype(np.float32) / 255.0
image = torch.from_numpy(image)[None,]
mask = torch.zeros((1, image.shape[1], image.shape[2]), dtype=torch.float32, device="cpu")
output_images.append(image)
output_masks.append(mask)
except Exception as e:
print(f"[ImageReceiver] 处理文件 {img_file} 时出错: {str(e)}")
import traceback
traceback.print_exc()
continue
return (output_images, output_masks)
class ImageListSplitter:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"indices": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "输入要提取的图片索引,用逗号分隔,如:0,1,3,4"
}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "split_images"
CATEGORY = "🎈LAOGOU"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,) # (images,)
def split_images(self, images, indices):
try:
# 解析索引字符串
try:
if isinstance(indices, list):
indices = indices[0] if indices else ""
indices = [int(idx.strip()) for idx in indices.split(',') if idx.strip()]
except ValueError:
print("[ImageSplitter] 索引格式错误,请使用逗号分隔的数字")
return ([],)
# 确保images是列表
if not isinstance(images, list):
images = [images]
# 处理批量图片的情况
if len(images) == 1 and len(images[0].shape) == 4: # [B, H, W, C]
batch_images = images[0]
total_images = batch_images.shape[0]
print(f"[ImageSplitter] 检测到批量图片,总数: {total_images}")
selected_images = []
for idx in indices:
if 0 <= idx < total_images:
# 保持批次维度,使用unsqueeze确保维度为 [1, H, W, C]
img = batch_images[idx].unsqueeze(0)
selected_images.append(img)
print(f"[ImageSplitter] 从批量中选择第 {idx} 张图片")
else:
print(f"[ImageSplitter] 索引 {idx} 超出批量范围 0-{total_images-1}")
if not selected_images:
return ([],)
return (selected_images,)
# 处理图片列表的情况
total_images = len(images)
print(f"[ImageSplitter] 检测到图片列表,总数: {total_images}")
if total_images == 0:
print("[ImageSplitter] 没有输入图片")
return ([],)
selected_images = []
for idx in indices:
if 0 <= idx < total_images:
selected_image = images[idx]
# 确保输出维度为 [1, H, W, C]
if len(selected_image.shape) == 3: # [H, W, C]
selected_image = selected_image.unsqueeze(0)
selected_images.append(selected_image)
print(f"[ImageSplitter] 从列表中选择第 {idx} 张图片")
else:
print(f"[ImageSplitter] 索引 {idx} 超出列表范围 0-{total_images-1}")
if not selected_images:
return ([],)
return (selected_images,)
except Exception as e:
print(f"[ImageSplitter] 处理出错: {str(e)}")
return ([],)
class MaskListSplitter:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"masks": ("MASK",),
"indices": ("STRING", {
"default": "",
"multiline": False,
"tooltip": "输入要提取的遮罩索引,用逗号分隔,如:0,1,3,4"
}),
},
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("masks",)
FUNCTION = "split_masks"
CATEGORY = "🎈LAOGOU"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,) # (masks,)
def split_masks(self, masks, indices):
try:
# 解析索引字符串
try:
if isinstance(indices, list):
indices = indices[0] if indices else ""
indices = [int(idx.strip()) for idx in indices.split(',') if idx.strip()]
except ValueError:
print("[MaskSplitter] 索引格式错误,请使用逗号分隔的数字")
return ([],)
# 确保masks是列表
if not isinstance(masks, list):
masks = [masks]
# 处理批量遮罩的情况
if len(masks) == 1 and len(masks[0].shape) == 3: # [B, H, W]
batch_masks = masks[0]
total_masks = batch_masks.shape[0]
print(f"[MaskSplitter] 检测到批量遮罩,总数: {total_masks}")
selected_masks = []
for idx in indices:
if 0 <= idx < total_masks:
selected_masks.append(batch_masks[idx].unsqueeze(0))
print(f"[MaskSplitter] 从批量中选择第 {idx} 个遮罩")
else:
print(f"[MaskSplitter] 索引 {idx} 超出批量范围 0-{total_masks-1}")
if not selected_masks:
return ([],)
return (selected_masks,)
# 处理遮罩列表的情况
total_masks = len(masks)
print(f"[MaskSplitter] 检测到遮罩列表,总数: {total_masks}")
if total_masks == 0:
print("[MaskSplitter] 没有输入遮罩")
return ([],)
selected_masks = []
for idx in indices:
if 0 <= idx < total_masks:
selected_mask = masks[idx]
if len(selected_mask.shape) == 2: # [H, W]
selected_mask = selected_mask.unsqueeze(0)
elif len(selected_mask.shape) != 3: # 不是 [B, H, W]
print(f"[MaskSplitter] 不支持的遮罩维度: {selected_mask.shape}")
continue
selected_masks.append(selected_mask)
print(f"[MaskSplitter] 从列表中选择第 {idx} 个遮罩")
else:
print(f"[MaskSplitter] 索引 {idx} 超出列表范围 0-{total_masks-1}")
if not selected_masks:
return ([],)
return (selected_masks,)
except Exception as e:
print(f"[MaskSplitter] 处理出错: {str(e)}")
return ([],)
class ImageListRepeater:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"images": ("IMAGE",),
"repeat_times": ("INT", {
"default": 1,
"min": 1,
"max": 100,
"step": 1,
"tooltip": "每张图片重复的次数"
}),
},
}
RETURN_TYPES = ("IMAGE",)
RETURN_NAMES = ("images",)
FUNCTION = "repeat_images"
CATEGORY = "🎈LAOGOU"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def repeat_images(self, images, repeat_times):
try:
# 处理 repeat_times 参数
if isinstance(repeat_times, list):
repeat_times = repeat_times[0] if repeat_times else 1
# 确保images是列表
if not isinstance(images, list):
images = [images]
if len(images) == 0:
print("[ImageRepeater] 没有输入图片")
return ([],)
# 创建重复后的图片列表
repeated_images = []
for idx, img in enumerate(images):
for _ in range(int(repeat_times)): # 确保 repeat_times 是整数
repeated_images.append(img)
print(f"[ImageRepeater] 图片 {idx} 重复 {repeat_times} 次")
print(f"[ImageRepeater] 输入 {len(images)} 张图片,输出 {len(repeated_images)} 张图片")
return (repeated_images,)
except Exception as e:
print(f"[ImageRepeater] 处理出错: {str(e)}")
return ([],)
class MaskListRepeater:
@classmethod
def INPUT_TYPES(cls):
return {
"required": {
"masks": ("MASK",),
"repeat_times": ("INT", {
"default": 1,
"min": 1,
"max": 100,
"step": 1,
"tooltip": "每张遮罩重复的次数"
}),
},
}
RETURN_TYPES = ("MASK",)
RETURN_NAMES = ("masks",)
FUNCTION = "repeat_masks"
CATEGORY = "🎈LAOGOU"
INPUT_IS_LIST = True
OUTPUT_IS_LIST = (True,)
def repeat_masks(self, masks, repeat_times):
try:
# 处理 repeat_times 参数
if isinstance(repeat_times, list):
repeat_times = repeat_times[0] if repeat_times else 1
# 确保masks是列表
if not isinstance(masks, list):
masks = [masks]
if len(masks) == 0:
print("[MaskRepeater] 没有输入遮罩")
return ([],)
# 创建重复后的遮罩列表
repeated_masks = []
for idx, mask in enumerate(masks):
for _ in range(int(repeat_times)): # 确保 repeat_times 是整数
repeated_masks.append(mask)
print(f"[MaskRepeater] 遮罩 {idx} 重复 {repeat_times} 次")
print(f"[MaskRepeater] 输入 {len(masks)} 个遮罩,输出 {len(repeated_masks)} 个遮罩")
return (repeated_masks,)
except Exception as e:
print(f"[MaskRepeater] 处理出错: {str(e)}")
return ([],)
+527
View File
@@ -0,0 +1,527 @@
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";
constructor(title, comfyClass) {
super(title);
this.isVirtualNode = false;
this.configuring = false;
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);
for (const w of this.widgets || []) {
w.last_y = w.last_y || 0;
}
this.configuring = false;
}
static setUp() {
if (!this.type) {
throw new Error(`Missing type for ${this.name}: ${this.title}`);
}
LiteGraph.registerNodeType(this.type, this);
if (this._category) {
this.category = this._category;
}
}
}
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"); // 新增:延迟时间属性
// 组数量组件
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,
step: 1,
default: 1,
label: "Repeat Count",
tooltip: "执行重复次数"
}], app);
// 新增:延迟时间组件
const delayWidget = ComfyWidgets["FLOAT"](this, "delaySeconds", ["FLOAT", {
min: 0,
max: 300, // 最大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) => {
const widgetIndex = this.widgets.indexOf(widget);
if (widgetIndex !== -1) {
const w = this.widgets.splice(widgetIndex, 1)[0];
this.widgets.splice(1 + index, 0, w);
}
});
}
// 监听值变化
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;
return function() {
self.updateGroupList();
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;
if (LiteGraph.overlapBounding(group._bounding, node.getBounding())) {
groupNodes.push(node);
}
}
group._nodes = groupNodes;
// 获取输出节点
return this.getOutputNodes(group._nodes);
}
// 获取输出节点
getOutputNodes(nodes) {
return nodes.filter((n) => {
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) =>
currentGroups[i] || ""
);
// 1. 首先移除所有组选择下拉框
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(
"combo",
`Group #${i + 1}`,
this.properties.groups[i] || "",
(v) => {
this.properties.groups[i] = v;
},
{
values: groupNames
}
);
}
// 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 => {
if (w.type === "combo") {
w.options.values = groups;
}
});
}
// 添加延迟函数
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;
}
console.warn(`[GroupExecutorSender] rgthree执行失败,使用默认方式:`, queueError);
for (const n of outputNodes) {
if (this.properties.isCancelling) {
return;
}
if (n.triggerQueue) {
await n.triggerQueue();
await this.waitForQueue();
}
}
}
if (i < this.properties.groupCount - 1) {
// 延迟前检查是否取消
if (this.properties.isCancelling) {
return;
}
this.updateStatus(
`等待 ${this.properties.delaySeconds}s...`
);
await this.delay(this.properties.delaySeconds);
}
} catch (error) {
console.error(`[GroupExecutor] 执行组 ${groupName} 时发生错误:`, error);
throw error;
}
}
}
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);
}
} catch (error) {
console.error('[GroupExecutor] 执行错误:', error);
this.updateStatus(`错误: ${error.message}`);
app.ui.dialog.show(`执行错误: ${error.message}`);
} finally {
this.properties.isExecuting = false;
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,
runningCount: 0,
pendingCount: 0,
rawRunning: [],
rawPending: []
};
}
}
// 等待队列完成
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 包含重复次数组件和取消按钮
return [width, height];
}
static setUp() {
LiteGraph.registerNodeType(this.type, this);
this.category = this._category;
}
// 序列化节点数据
serialize() {
const data = super.serialize();
data.properties = {
...data.properties,
groupCount: parseInt(this.properties.groupCount) || 1,
groups: [...this.properties.groups],
isExecuting: this.properties.isExecuting,
repeatCount: parseInt(this.properties.repeatCount) || 1,
delaySeconds: parseFloat(this.properties.delaySeconds) || 0
};
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] : [];
this.properties.isExecuting = info.properties.isExecuting ?? false;
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;
} else if (w.name === "repeatCount") {
w.value = this.properties.repeatCount;
} else if (w.name === "delaySeconds") {
w.value = this.properties.delaySeconds;
}
});
if (!this.configuring) {
this.updateGroupWidgets();
}
}
}
app.registerExtension({
name: "GroupExecutor",
registerCustomNodes() {
GroupExecutorNode.setUp();
}
});
+969
View File
@@ -0,0 +1,969 @@
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;
this.isCancelling = false;
this.groups = [];
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 = `
<div class="ge-header">
<span class="ge-title">组执行管理器</span>
<div class="ge-controls">
<button class="ge-dock-btn" title="停靠位置">📌</button>
<button class="ge-minimize-btn" title="最小化">-</button>
<button class="ge-close-btn" title="关闭">×</button>
</div>
</div>
<div class="ge-content">
<div class="ge-row ge-config-row">
<select class="ge-config-select">
<option value="">选择配置</option>
</select>
<button class="ge-save-config" title="保存配置">💾</button>
<button class="ge-delete-config" title="删除配置">🗑️</button>
</div>
<div class="ge-row">
<label>组数量:</label>
<input type="number" class="ge-group-count" min="1" max="10" value="1">
</div>
<div class="ge-groups-container"></div>
<div class="ge-row">
<label>重复次数:</label>
<input type="number" class="ge-repeat-count" min="1" max="100" value="1">
</div>
<div class="ge-row">
<label>延迟(秒):</label>
<input type="number" class="ge-delay" min="0" max="300" step="0.1" value="0">
</div>
<div class="ge-status"></div>
<div class="ge-buttons">
<button class="ge-execute-btn">执行</button>
<button class="ge-cancel-btn" disabled>取消</button>
</div>
</div>
`;
// 添加样式
const style = document.createElement('style');
style.textContent = `
.group-executor-ui {
position: fixed;
top: 20px;
right: 20px;
width: 300px !important; /* 强制固定宽度 */
min-width: 300px;
max-width: 300px;
background: #2a2a2a;
border: 1px solid #444;
border-radius: 8px;
box-shadow: 0 4px 12px rgba(0,0,0,0.2);
z-index: 1000;
font-family: Arial, sans-serif;
color: #fff;
user-select: none; /* 禁止文字选择 */
}
.ge-header {
display: flex;
justify-content: space-between;
align-items: center;
padding: 8px 12px;
background: #333;
border-radius: 8px 8px 0 0;
cursor: move;
width: 100%;
box-sizing: border-box;
}
.ge-controls button {
background: none;
border: none;
color: #fff;
margin-left: 8px;
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;
background: #333;
border: 1px solid #444;
color: #fff;
border-radius: 4px;
}
.ge-groups-container {
margin-bottom: 12px;
}
.ge-group-select {
width: 100%;
margin-bottom: 8px;
padding: 4px 8px;
background: #333;
border: 1px solid #444;
color: #fff;
border-radius: 4px;
}
.ge-buttons {
display: flex;
gap: 8px;
}
.ge-buttons button {
flex: 1;
padding: 8px;
border: none;
border-radius: 4px;
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;
background: #333;
border-radius: 4px;
min-height: 20px;
text-align: center;
position: relative;
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); /* 绿色半透明背景 */
transition: width 0.3s ease;
z-index: 0;
}
.ge-status span {
position: relative;
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
opacity: 0;
transition: opacity 0.2s;
}
.ge-dock-menu.visible {
visibility: visible;
opacity: 1;
}
.ge-dock-menu button {
display: block;
width: 100%;
padding: 4px 12px;
background: none;
border: none;
color: #fff;
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; /* 文字溢出时显示省略号 */
}
.ge-config-row {
display: flex;
gap: 8px;
margin-bottom: 12px;
}
.ge-config-select {
flex: 1;
padding: 4px 8px;
background: #333;
border: 1px solid #444;
color: #fff;
border-radius: 4px;
}
.ge-save-config,
.ge-delete-config {
background: #333;
border: 1px solid #444;
color: #fff;
padding: 4px 8px;
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();
this.dragOffset = {
x: e.clientX - rect.left,
y: e.clientY - rect.top
};
}
});
document.addEventListener('mousemove', (e) => {
if (this.isDragging) {
const x = e.clientX - this.dragOffset.x;
const y = e.clientY - this.dragOffset.y;
this.container.style.left = `${x}px`;
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 = `
<button data-position="top-left">左上角</button>
<button data-position="top-right">右上角</button>
<button data-position="bottom-left">左下角</button>
<button data-position="bottom-right">右下角</button>
`;
// 将菜单添加到容器内部而不是 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); // 等待过渡动画完成后移除
}
});
// 点击其他地方关闭菜单
const closeMenu = (e) => {
if (!menu.contains(e.target) && e.target !== button) {
menu.classList.remove('visible');
setTimeout(() => menu.remove(), 200);
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`;
style.left = `${marginX}px`;
style.right = 'auto';
style.bottom = 'auto';
break;
case 'top-right':
style.top = `${marginY}px`;
style.right = `${marginX}px`;
style.left = 'auto';
style.bottom = 'auto';
break;
case 'bottom-left':
style.bottom = `${marginY}px`;
style.left = `${marginX}px`;
style.right = 'auto';
style.top = 'auto';
break;
case 'bottom-right':
style.bottom = `${marginY}px`;
style.right = `${marginX}px`;
style.left = 'auto';
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';
select.innerHTML = `
<option value="">选择组 #${i + 1}</option>
${groupNames.map(name => `<option value="${name}">${name}</option>`).join('')}
`;
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 = `<span>${text}</span>`; // 将文本包装在 span 中
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++) {
if (this.isCancelling) {
console.log('[GroupExecutorUI] 执行被用户取消');
await fetch('/interrupt', { method: 'POST' });
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);
}
} catch (error) {
throw new Error(`执行组 "${groupName}" 失败: ${error.message}`);
}
}
if (repeat < repeatCount - 1 && !this.isCancelling) {
await this.delay(delaySeconds);
}
}
if (!this.isCancelling) {
this.updateStatus("完成");
}
} catch (error) {
console.error('[GroupExecutorUI] 执行错误:', error);
this.updateStatus(`错误: ${error.message}`);
app.ui.dialog.show(`执行错误: ${error.message}`);
} finally {
this.isExecuting = false;
this.isCancelling = false;
executeBtn.disabled = false;
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;
if (LiteGraph.overlapBounding(group._bounding, node.getBounding())) {
if (node.mode !== LiteGraph.NEVER && node.constructor.nodeData?.output_node === true) {
outputNodes.push(node);
}
}
}
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 不可用');
}
await rgthree.queueOutputNodes(nodeIds);
await this.waitForQueue();
} catch (queueError) {
console.warn(`[GroupExecutorUI] rgthree执行失败,使用默认方式:`, queueError);
for (const n of outputNodes) {
if (this.isCancelling) return;
if (n.triggerQueue) {
await n.triggerQueue();
await this.waitForQueue();
}
}
}
}
async cancelExecution() {
if (!this.isExecuting) {
console.warn('[GroupExecutorUI] 没有正在执行的任务');
return;
}
try {
this.isCancelling = true;
this.updateStatus("已取消", 0); // 重置进度条为0
await fetch('/interrupt', { method: 'POST' });
} catch (error) {
console.error('[GroupExecutorUI] 取消执行时出错:', error);
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,
runningCount: 0,
pendingCount: 0,
rawRunning: [],
rawPending: []
};
}
}
// 等待队列完成
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`;
}
if (this.container.style.left !== 'auto') {
this.container.style.left = `${this.DOCK_MARGIN_X}px`;
}
if (this.container.style.top !== 'auto') {
this.container.style.top = `${this.DOCK_MARGIN_Y}px`;
}
if (this.container.style.bottom !== 'auto') {
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 = `
<option value="">选择配置</option>
${result.configs.map(config => `<option value="${config.name}">${config.name}</option>`).join('')}
`;
} catch (error) {
console.error('[GroupExecutor] 加载配置失败:', error);
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')]
.map(select => select.value)
.filter(Boolean),
repeatCount: parseInt(this.container.querySelector('.ge-repeat-count').value),
delay: parseFloat(this.container.querySelector('.ge-delay').value)
};
try {
const response = await api.fetchApi('/group_executor/configs', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
body: JSON.stringify(config)
});
const result = await response.json();
if (result.status === "error") {
throw new Error(result.message);
}
await this.loadConfigs();
app.ui.dialog.show('配置保存成功');
} catch (error) {
console.error('[GroupExecutor] 保存配置失败:', error);
app.ui.dialog.show('保存配置失败: ' + error.message);
}
}
async loadConfig(configName) {
try {
const response = await api.fetchApi(`/group_executor/configs/${configName}`, {
method: 'GET',
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) {
console.error('加载配置失败:', error);
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) {
console.error('[GroupExecutor] 删除配置失败:', error);
app.ui.dialog.show('删除配置失败: ' + error.message);
}
}
}
// 修改扩展注册部分
app.registerExtension({
name: "GroupExecutorUI",
async setup() {
// 等待 UI 设置初始化完成
await app.ui.settings.setup;
// 添加设置选项
app.ui.settings.addSetting({
id: "GroupExecutor.enabled",
name: "显示组执行器按钮",
type: "boolean",
defaultValue: true,
onChange: (value) => {
const btn = document.querySelector('.group-executor-btn');
if (btn) {
btn.style.display = value ? 'block' : 'none';
}
}
});
app.ui.settings.addSetting({
id: "GroupExecutor.marginX",
name: "组执行器水平边距",
type: "number",
defaultValue: 0,
min: 0,
max: 100,
step: 1,
onChange: (value) => {
GroupExecutorUI.DOCK_MARGIN_X = value;
document.querySelectorAll('.group-executor-ui').forEach(el => {
const instance = el.instance;
if (instance) {
instance.DOCK_MARGIN_X = value;
instance.ensureInViewport();
}
});
}
});
app.ui.settings.addSetting({
id: "GroupExecutor.marginY",
name: "组执行器垂直边距",
type: "number",
defaultValue: 20,
min: 0,
max: 100,
step: 1,
onChange: (value) => {
GroupExecutorUI.DOCK_MARGIN_Y = value;
document.querySelectorAll('.group-executor-ui').forEach(el => {
const instance = el.instance;
if (instance) {
instance.DOCK_MARGIN_Y = value;
instance.ensureInViewport();
}
});
}
});
try {
// 尝试使用新版UI
const btn = new (await import("../../scripts/ui/components/button.js")).ComfyButton({
icon: "layers-outline",
action: () => {
new GroupExecutorUI();
},
tooltip: "组执行器",
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 {
menu.appendChild(groupExecutorButton);
}
}
}
});
+187
View File
@@ -0,0 +1,187 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "GroupExecutorQueueManager",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
// 检查是否已经重写过 fetchApi
if (api.fetchApi._isGroupExecutorQueueManager) {
return;
}
// 保存原始的 api.fetchApi
const originalFetchApi = api.fetchApi;
// 递归收集相关节点
function collectRelatedNodes(prompt, nodeId, relevantNodes) {
if (!prompt[nodeId] || relevantNodes.has(nodeId)) return;
relevantNodes.add(nodeId);
// 收集输入节点
const node = prompt[nodeId];
if (node.inputs) {
Object.values(node.inputs).forEach(input => {
if (input && input.length > 0) {
collectRelatedNodes(prompt, input[0], relevantNodes);
}
});
}
}
// 重写 api.fetchApi
const newFetchApi = async function(url, options = {}) {
// 只拦截 POST /prompt 请求
if (url === '/prompt' && options.method === 'POST') {
const requestData = JSON.parse(options.body);
// 如果是 GroupExecutorSender 的内部请求,直接放行
if (requestData.extra_data?.isGroupExecutorRequest) {
return originalFetchApi.call(api, url, options);
}
const prompt = requestData.prompt;
// 检查是否存在 GroupExecutorSender 节点
const hasGroupExecutor = Object.values(prompt).some(node =>
node.class_type === "GroupExecutorSender"
);
if (hasGroupExecutor) {
// 找出所有 GroupExecutorSender 相关的节点
const relevantNodes = new Set();
for (const [nodeId, node] of Object.entries(prompt)) {
if (node.class_type === "GroupExecutorSender") {
collectRelatedNodes(prompt, nodeId, relevantNodes);
}
}
// 创建过滤后的 prompt
const filteredPrompt = {};
for (const nodeId of relevantNodes) {
if (prompt[nodeId]) {
filteredPrompt[nodeId] = prompt[nodeId];
}
}
// 修改请求数据,添加标记表示这是内部请求
const modifiedOptions = {
...options,
body: JSON.stringify({
...requestData,
prompt: filteredPrompt,
extra_data: {
...requestData.extra_data,
isGroupExecutorRequest: true
}
})
};
return originalFetchApi.call(api, url, modifiedOptions);
}
}
return originalFetchApi.call(api, url, options);
};
// 标记新的 fetchApi 函数
newFetchApi._isGroupExecutorQueueManager = true;
// 替换 api.fetchApi
api.fetchApi = newFetchApi;
}
});
api.addEventListener("img-send", async ({ detail }) => {
if (detail.images.length === 0) return;
// 构建所有图像的文件名列表,用逗号连接
const filenames = detail.images.map(data => data.filename).join(', ');
// 查找所有LG_ImageReceiver节点
for (const node of app.graph._nodes) {
if (node.type === "LG_ImageReceiver") {
let isLinked = false;
// 检查link_id是否匹配
const linkWidget = node.widgets.find(w => w.name === "link_id");
if (linkWidget.value === detail.link_id) {
isLinked = true;
}
if (isLinked) {
// 更新image widget的值
if (node.widgets[0]) {
node.widgets[0].value = filenames;
if (node.widgets[0].callback) {
node.widgets[0].callback(filenames);
}
}
// 加载所有预览图像
Promise.all(detail.images.map(imageData => {
return new Promise((resolve) => {
const img = new Image();
img.onload = () => resolve(img);
img.src = `/view?filename=${encodeURIComponent(imageData.filename)}&type=${imageData.type}${app.getPreviewFormatParam()}`;
});
})).then(loadedImages => {
node.imgs = loadedImages;
node.size[1] = Math.max(200, 100 + loadedImages.length * 100);
app.canvas.setDirty(true);
});
}
}
}
});
// 注册节点扩展
app.registerExtension({
name: "Comfy.LG_Image",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "LG_ImageReceiver") {
const onExecuted = nodeType.prototype.onExecuted;
nodeType.prototype.onExecuted = function (message) {
onExecuted?.apply(this, arguments);
};
}
},
});
app.registerExtension({
name: "memory.cleanup",
init() {
// 监听来自后端的内存清理信号
api.addEventListener("memory_cleanup", ({ detail }) => {
if (detail.type === "cleanup_request") {
console.log("收到内存清理请求");
// 发送清理请求到 /free 接口
fetch("/free", {
method: "POST",
headers: {
"Content-Type": "application/json",
},
body: JSON.stringify(detail.data)
})
.then(response => {
if (response.ok) {
console.log("内存清理请求已发送");
} else {
console.error("内存清理请求失败");
}
})
.catch(error => {
console.error("发送内存清理请求出错:", error);
});
}
});
}
});
+284
View File
@@ -0,0 +1,284 @@
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
app.registerExtension({
name: "GroupExecutorSender",
async beforeRegisterNodeDef(nodeType, nodeData, app) {
if (nodeData.name === "GroupExecutorSender") {
// 添加状态显示相关的属性
nodeType.prototype.onNodeCreated = function() {
this.properties = {
...this.properties,
isExecuting: false,
statusText: "",
showStatus: false
};
this.size = this.computeSize();
};
// 重写绘制方法
const onDrawForeground = nodeType.prototype.onDrawForeground;
nodeType.prototype.onDrawForeground = function(ctx) {
const r = onDrawForeground?.apply?.(this, arguments);
if (!this.flags.collapsed && this.properties.showStatus) {
const text = this.properties.statusText;
if (text) {
ctx.save();
// 设置字体
ctx.font = "bold 30px sans-serif";
ctx.textAlign = "center";
ctx.textBaseline = "middle";
// 设置颜色
ctx.fillStyle = this.properties.isExecuting ? "dodgerblue" : "limegreen";
// 计算中心位置
const centerX = this.size[0] / 2;
const centerY = this.size[1] / 2 + 10; // 稍微向下偏移以避开输入端口
// 绘制文本
ctx.fillText(text, centerX, centerY);
ctx.restore();
}
}
return r;
};
// 更新节点大小计算
nodeType.prototype.computeSize = function() {
return [400, 100]; // 固定宽度和高度
};
// 更新状态显示方法
nodeType.prototype.updateStatus = function(text) {
this.properties.statusText = text;
this.properties.showStatus = true;
this.setDirtyCanvas(true, true);
};
// 重置状态显示方法
nodeType.prototype.resetStatus = function() {
this.properties.statusText = "";
this.properties.showStatus = false;
this.setDirtyCanvas(true, true);
};
// 获取指定组的输出节点
nodeType.prototype.getGroupOutputNodes = function(groupName) {
// 找到指定名称的组
const group = app.graph._groups.find(g => g.title === groupName);
if (!group) {
console.warn(`[GroupExecutorSender] 未找到名为 "${groupName}" 的组`);
return [];
}
// 获取组内的所有节点
const groupNodes = [];
for (const node of app.graph._nodes) {
if (!node || !node.pos) continue;
if (LiteGraph.overlapBounding(group._bounding, node.getBounding())) {
groupNodes.push(node);
}
}
group._nodes = groupNodes;
// 获取输出节点
return this.getOutputNodes(group._nodes);
};
// 获取输出节点
nodeType.prototype.getOutputNodes = function(nodes) {
return nodes.filter((n) => {
return n.mode !== LiteGraph.NEVER &&
n.constructor.nodeData?.output_node === true;
});
};
// 获取队列状态
nodeType.prototype.getQueueStatus = async function() {
try {
const response = await api.fetchApi('/queue');
if (!response.ok) {
throw new Error(`HTTP error! status: ${response.status}`);
}
const data = await response.json();
// 确保数据包含必要的字段
const queueRunning = data.queue_running || [];
const queuePending = data.queue_pending || [];
return {
isRunning: queueRunning.length > 0,
isPending: queuePending.length > 0,
runningCount: queueRunning.length,
pendingCount: queuePending.length,
rawRunning: queueRunning,
rawPending: queuePending
};
} catch (error) {
console.error('[GroupExecutorSender] 获取队列状态失败:', error);
// 返回默认状态而不是 null
return {
isRunning: false,
isPending: false,
runningCount: 0,
pendingCount: 0,
rawRunning: [],
rawPending: []
};
}
};
// 等待队列完成
nodeType.prototype.waitForQueue = async function() {
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(`[GroupExecutorSender] 检查队列状态失败:`, error);
// 发生错误时继续检查,而不是中断
setTimeout(checkQueue, 500);
}
};
// 开始检查队列
checkQueue();
});
};
// 监听执行列表请求
api.addEventListener("execute_group_list", async ({ detail }) => {
if (!detail || !detail.node_id || !Array.isArray(detail.execution_list)) {
console.error('[GroupExecutorSender] 收到无效的执行数据:', detail);
return;
}
const node = app.graph._nodes_by_id[detail.node_id];
if (!node) {
console.error(`[GroupExecutorSender] 未找到节点: ${detail.node_id}`);
return;
}
try {
const executionList = detail.execution_list;
console.log(`[GroupExecutorSender] 收到执行列表:`, executionList);
if (node.properties.isExecuting) {
console.warn('[GroupExecutorSender] 已有执行任务在进行中');
return;
}
node.properties.isExecuting = true;
// 计算实际任务数(排除延迟组)
let totalTasks = executionList.filter(item => item.group_name !== "__delay__").length;
let currentTask = 0;
try {
for (const execution of executionList) {
const group_name = execution.group_name || '';
const repeat_count = parseInt(execution.repeat_count) || 1;
const delay_seconds = parseFloat(execution.delay_seconds) || 0;
if (!group_name) {
console.warn('[GroupExecutorSender] 跳过无效的组名称:', execution);
continue;
}
// 处理延迟组
if (group_name === "__delay__") {
if (delay_seconds > 0) {
node.updateStatus(
`等待下一组 ${delay_seconds}s...`
);
await new Promise(resolve => setTimeout(resolve, delay_seconds * 1000));
}
continue;
}
// 更新当前任务计数
currentTask++;
const progress = (currentTask / totalTasks) * 100;
node.updateStatus(
`执行组: ${group_name} (${currentTask}/${totalTasks})`,
progress
);
try {
const outputNodes = node.getGroupOutputNodes(group_name);
if (!outputNodes || !outputNodes.length) {
throw new Error(`组 "${group_name}" 中没有找到输出节点`);
}
const nodeIds = outputNodes.map(n => n.id);
if (rgthree?.queueOutputNodes) {
try {
await rgthree.queueOutputNodes(nodeIds);
await node.waitForQueue();
} catch (queueError) {
console.warn(`[GroupExecutorSender] rgthree执行失败,使用默认方式:`, queueError);
for (const n of outputNodes) {
if (n.triggerQueue) {
await n.triggerQueue();
await node.waitForQueue();
}
}
}
} else {
for (const n of outputNodes) {
if (n.triggerQueue) {
await n.triggerQueue();
await node.waitForQueue();
}
}
}
if (delay_seconds > 0 && currentTask < totalTasks) {
node.updateStatus(
`执行组: ${group_name} (${currentTask}/${totalTasks}) - 等待 ${delay_seconds}s`,
progress
);
await new Promise(resolve => setTimeout(resolve, delay_seconds * 1000));
}
} catch (error) {
throw new Error(`执行组 "${group_name}" 失败: ${error.message}`);
}
}
// 执行完成后保留最后的状态显示
node.updateStatus(`执行完成 (${totalTasks}/${totalTasks})`, 100);
} catch (error) {
console.error('[GroupExecutorSender] 执行错误:', error);
node.updateStatus(`错误: ${error.message}`);
app.ui.dialog.show(`执行错误: ${error.message}`);
} finally {
node.properties.isExecuting = false;
}
} catch (error) {
console.error(`[GroupExecutorSender] 执行失败:`, error);
app.ui.dialog.show(`执行错误: ${error.message}`);
node.updateStatus(`错误: ${error.message}`);
node.properties.isExecuting = false;
}
});
}
}
});