Files
2025-09-12 21:11:31 +08:00

978 lines
37 KiB
JavaScript
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import { app } from "../../scripts/app.js";
import { api } from "../../scripts/api.js";
import { queueManager } from "./queue_utils.js";
class GroupExecutorUI {
static DOCK_MARGIN_X = 0;
static DOCK_MARGIN_Y = 60;
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-mode-switch">
<button class="ge-mode-btn active" data-mode="multi">多组执行</button>
<button class="ge-mode-btn" data-mode="single">单组执行</button>
</div>
<div class="ge-multi-mode">
<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="50" 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>
<div class="ge-single-mode" style="display: none;">
<div class="ge-search-container">
<input type="text" class="ge-search-input" placeholder="搜索组名称...">
<button class="ge-search-clear" title="清除搜索">×</button>
</div>
<div class="ge-groups-list"></div>
</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;
display: flex;
flex-direction: column;
max-height: calc(100vh - 100px);
}
.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,
.ge-groups-list {
max-height: calc(50vh - 180px);
overflow-y: auto;
margin-bottom: 12px;
padding-right: 8px;
}
.ge-groups-container::-webkit-scrollbar,
.ge-groups-list::-webkit-scrollbar {
width: 6px;
}
.ge-groups-container::-webkit-scrollbar-track,
.ge-groups-list::-webkit-scrollbar-track {
background: #2a2a2a;
border-radius: 3px;
}
.ge-groups-container::-webkit-scrollbar-thumb,
.ge-groups-list::-webkit-scrollbar-thumb {
background: #555;
border-radius: 3px;
}
.ge-groups-container::-webkit-scrollbar-thumb:hover,
.ge-groups-list::-webkit-scrollbar-thumb:hover {
background: #666;
}
.ge-group-select {
width: 100%;
margin-bottom: 8px;
padding: 4px 8px;
background: #333;
border: 1px solid #444;
color: #fff;
border-radius: 4px;
}
.ge-group-select:last-child {
margin-bottom: 0;
}
.ge-group-item {
display: flex;
align-items: center;
justify-content: space-between;
padding: 10px;
margin-bottom: 8px;
background: #333;
border-radius: 4px;
}
.ge-group-item:last-child {
margin-bottom: 0;
}
.ge-group-name {
flex: 1;
margin-right: 8px;
}
.ge-group-controls {
display: flex;
gap: 10px;
margin-left: auto;
}
.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%);
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;
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;
}
.ge-mode-switch {
display: flex;
margin-bottom: 12px;
gap: 8px;
}
.ge-mode-btn {
flex: 1;
padding: 8px;
background: #333;
border: 1px solid #444;
color: #fff;
border-radius: 4px;
cursor: pointer;
}
.ge-mode-btn.active {
background: #4CAF50;
border-color: #4CAF50;
}
.ge-execute-single-btn,
.ge-cancel-single-btn {
padding: 6px 12px;
font-size: 14px;
min-width: 60px;
border: none;
border-radius: 4px;
cursor: pointer;
font-weight: bold;
}
.ge-execute-single-btn {
background: #4CAF50;
color: white;
}
.ge-cancel-single-btn {
background: #f44336;
color: white;
display: none;
}
.ge-execute-single-btn:disabled,
.ge-cancel-single-btn:disabled {
opacity: 0.5;
cursor: not-allowed;
}
.ge-execute-single-btn:hover:not(:disabled) {
background: #45a049;
}
.ge-cancel-single-btn:hover:not(:disabled) {
background: #d32f2f;
}
.ge-search-container {
display: flex;
align-items: center;
margin-bottom: 12px;
gap: 8px;
}
.ge-search-input {
flex: 1;
padding: 8px 12px;
background: #333;
border: 1px solid #444;
color: #fff;
border-radius: 4px;
font-size: 14px;
}
.ge-search-input:focus {
outline: none;
border-color: #666;
}
.ge-search-clear {
background: #444;
border: none;
color: #fff;
padding: 6px 10px;
border-radius: 4px;
cursor: pointer;
font-size: 16px;
display: none;
}
.ge-search-clear:hover {
background: #555;
}
`;
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();
const modeBtns = this.container.querySelectorAll('.ge-mode-btn');
modeBtns.forEach(btn => {
btn.addEventListener('click', () => {
const mode = btn.dataset.mode;
this.switchMode(mode);
});
});
this.updateSingleModeList();
const searchInput = this.container.querySelector('.ge-search-input');
const clearButton = this.container.querySelector('.ge-search-clear');
searchInput.addEventListener('input', () => {
clearButton.style.display = searchInput.value ? 'block' : 'none';
});
}
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>
`;
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(() => {
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>`;
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 api.interrupt();
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 {
await queueManager.queueOutputNodes(nodeIds);
await this.waitForQueue();
} catch (queueError) {
console.warn(`[GroupExecutorUI] 队列执行失败,使用默认方式:`, 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);
await api.interrupt();
} 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);
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) {
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));
}
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 jsonString = JSON.stringify(config);
JSON.parse(jsonString);
const response = await api.fetchApi('/group_executor/configs', {
method: 'POST',
headers: {
'Content-Type': 'application/json'
},
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) {
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);
}
}
switchMode(mode) {
const multiMode = this.container.querySelector('.ge-multi-mode');
const singleMode = this.container.querySelector('.ge-single-mode');
const modeBtns = this.container.querySelectorAll('.ge-mode-btn');
modeBtns.forEach(btn => {
btn.classList.toggle('active', btn.dataset.mode === mode);
});
if (mode === 'multi') {
multiMode.style.display = '';
singleMode.style.display = 'none';
} else {
multiMode.style.display = 'none';
singleMode.style.display = '';
this.updateSingleModeList();
}
}
updateSingleModeList() {
const container = this.container.querySelector('.ge-groups-list');
const searchInput = this.container.querySelector('.ge-search-input');
const clearButton = this.container.querySelector('.ge-search-clear');
const groupNames = this.getGroupNames();
const filterGroups = (searchText) => {
const normalizedSearch = searchText.toLowerCase();
return groupNames.filter(name =>
name.toLowerCase().includes(normalizedSearch)
);
};
const renderGroups = (filteredGroups) => {
container.innerHTML = filteredGroups.map(name => `
<div class="ge-group-item" data-group="${name}">
<span class="ge-group-name">${name}</span>
<div class="ge-group-controls">
<button class="ge-execute-single-btn">执行</button>
<button class="ge-cancel-single-btn" disabled>取消</button>
</div>
</div>
`).join('');
container.querySelectorAll('.ge-group-item').forEach(item => {
const groupName = item.dataset.group;
const executeBtn = item.querySelector('.ge-execute-single-btn');
const cancelBtn = item.querySelector('.ge-cancel-single-btn');
executeBtn.addEventListener('click', async () => {
executeBtn.disabled = true;
cancelBtn.disabled = false;
cancelBtn.style.display = 'block';
this.isExecuting = true;
this.isCancelling = false;
try {
await this.executeGroup(groupName);
this.updateStatus(`组 "${groupName}" 执行完成`);
} catch (error) {
this.updateStatus(`执行失败: ${error.message}`);
console.error(error);
} finally {
this.isExecuting = false;
this.isCancelling = false;
executeBtn.disabled = false;
cancelBtn.disabled = true;
cancelBtn.style.display = 'none';
}
});
cancelBtn.addEventListener('click', async () => {
if (!this.isExecuting) return;
try {
this.isCancelling = true;
this.updateStatus("正在取消...", 0);
await api.interrupt();
this.updateStatus("已取消", 0);
} catch (error) {
console.error('[GroupExecutorUI] 取消执行时出错:', error);
this.updateStatus(`取消失败: ${error.message}`, 0);
}
});
});
};
renderGroups(groupNames);
searchInput.addEventListener('input', (e) => {
const searchText = e.target.value;
clearButton.style.display = searchText ? 'block' : 'none';
const filteredGroups = filterGroups(searchText);
renderGroups(filteredGroups);
});
clearButton.addEventListener('click', () => {
searchInput.value = '';
clearButton.style.display = 'none';
renderGroups(groupNames);
});
}
}
app.registerExtension({
name: "GroupExecutorUI",
async setup() {
await app.ui.settings.setup;
// 添加右键菜单选项
const origMenu = LGraphCanvas.prototype.getCanvasMenuOptions;
LGraphCanvas.prototype.getCanvasMenuOptions = function() {
const options = origMenu.call(this);
// 在菜单顶部添加组执行器选项(在第一个选项之后)
options.splice(1, 0, null); // 在第一个选项后添加分隔线
options.splice(2, 0, {
content: "⚡ 打开组执行器",
callback: () => {
new GroupExecutorUI();
}
});
return options;
}
}
});