@metabrain-labs/comfyui-mcp-server 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (88) hide show
  1. package/.env +179 -0
  2. package/.github/workflows/ci.yml +63 -0
  3. package/.husky/pre-commit +11 -0
  4. package/LICENSE +21 -0
  5. package/README-en.md +635 -0
  6. package/README.md +656 -0
  7. package/__tests__/services/task/execution.test.ts +272 -0
  8. package/__tests__/services/task/wait.test.ts +220 -0
  9. package/__tests__/types/result.test.ts +227 -0
  10. package/__tests__/utils/mcp-helpers.test.ts +137 -0
  11. package/__tests__/utils/special-node-handler.test.ts +186 -0
  12. package/comfyui-mcp-server-skill/SKILL.md +43 -0
  13. package/comfyui-mcp-server-skill/references/api-json.md +323 -0
  14. package/comfyui-mcp-server-skill/references/catalog.md +251 -0
  15. package/comfyui-mcp-server-skill/rules/api-json.md +94 -0
  16. package/comfyui-mcp-server-skill/rules/catalog.md +93 -0
  17. package/comfyui-mcp-server-skill/rules/comfyui.md +158 -0
  18. package/configs/inspector/dev.json +11 -0
  19. package/configs/inspector/prod.json +9 -0
  20. package/docs/en/content/public/inspector-example.png +0 -0
  21. package/docs/en/content/public/workflow_name_example.png +0 -0
  22. package/docs/en/content/public/workflow_parameter_example.png +0 -0
  23. package/docs/en/md/Project-Advantages.md +75 -0
  24. package/docs/zh-CN/content/public/inspector-example.png +0 -0
  25. package/docs/zh-CN/content/public/workflow_name_example.png +0 -0
  26. package/docs/zh-CN/content/public/workflow_parameter_example.png +0 -0
  27. package/docs/zh-CN/md/why-us.md +51 -0
  28. package/example.json +26 -0
  29. package/jest.config.js +45 -0
  30. package/locales/en.json +150 -0
  31. package/locales/zh.json +150 -0
  32. package/package.json +68 -0
  33. package/src/api/api.ts +123 -0
  34. package/src/api/http.ts +70 -0
  35. package/src/constants/common.ts +38 -0
  36. package/src/constants/index.ts +1 -0
  37. package/src/hooks/websocket.ts +93 -0
  38. package/src/i18n.ts +40 -0
  39. package/src/index.ts +268 -0
  40. package/src/scripts/comfy-ui/list-history.ts +31 -0
  41. package/src/scripts/comfy-ui/run-ws.ts +23 -0
  42. package/src/scripts/ws/send-feature-flags.ts +23 -0
  43. package/src/server-stdio.ts +23 -0
  44. package/src/services/business.ts +194 -0
  45. package/src/services/dynamic-tool.ts +303 -0
  46. package/src/services/index.ts +19 -0
  47. package/src/services/storage/asset-storage.ts +48 -0
  48. package/src/services/storage/index.ts +2 -0
  49. package/src/services/storage/workflow-storage.ts +192 -0
  50. package/src/services/task/execution.ts +69 -0
  51. package/src/services/task/fetch.ts +131 -0
  52. package/src/services/task/index.ts +3 -0
  53. package/src/services/task/wait.ts +294 -0
  54. package/src/services/workflow/executor.ts +134 -0
  55. package/src/services/workflow/index.ts +1 -0
  56. package/src/tools/handlers/core-manual.ts +28 -0
  57. package/src/tools/handlers/index.ts +22 -0
  58. package/src/tools/handlers/interrupt-prompt.ts +35 -0
  59. package/src/tools/handlers/list-models.ts +51 -0
  60. package/src/tools/handlers/mount-workflow.ts +88 -0
  61. package/src/tools/handlers/prompt-result.ts +35 -0
  62. package/src/tools/handlers/prompts.ts +61 -0
  63. package/src/tools/handlers/queue-custom-prompt.ts +164 -0
  64. package/src/tools/handlers/queue-prompt.ts +170 -0
  65. package/src/tools/handlers/save-custom-workflow.ts +67 -0
  66. package/src/tools/handlers/save-task-assets.ts +82 -0
  67. package/src/tools/handlers/system-status.ts +30 -0
  68. package/src/tools/handlers/task-detail.ts +35 -0
  69. package/src/tools/handlers/tool-status-map.ts +33 -0
  70. package/src/tools/handlers/upload-assets.ts +82 -0
  71. package/src/tools/handlers/workflow-api.ts +46 -0
  72. package/src/tools/handlers/workflows-catalog.ts +79 -0
  73. package/src/tools/index.ts +101 -0
  74. package/src/types/common.ts +74 -0
  75. package/src/types/dynamic-tool.ts +85 -0
  76. package/src/types/enums/result.ts +29 -0
  77. package/src/types/execute.ts +24 -0
  78. package/src/types/object-info.ts +43 -0
  79. package/src/types/result.ts +126 -0
  80. package/src/types/task.ts +118 -0
  81. package/src/types/workflow.ts +111 -0
  82. package/src/types/ws.ts +80 -0
  83. package/src/utils/format.ts +134 -0
  84. package/src/utils/mcp-helpers.ts +110 -0
  85. package/src/utils/special-node-handler.ts +36 -0
  86. package/src/utils/workflow-converter.ts +140 -0
  87. package/src/utils/ws.ts +219 -0
  88. package/tsconfig.json +18 -0
@@ -0,0 +1,192 @@
1
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
2
+ import { mkdir, readFile, writeFile } from "fs/promises";
3
+ import path from "path";
4
+ import { COMMON } from "../../constants";
5
+ import { CollectFormatTaskWorkflow, sourcePriority } from "../../types/common";
6
+
7
+ export interface SaveWorkflowOptions {
8
+ dir?: string;
9
+ fileName?: string;
10
+ append?: boolean;
11
+ }
12
+
13
+ /**
14
+ * 保存工作流任务信息到本地
15
+ */
16
+ export async function saveWorkflow(
17
+ data: CollectFormatTaskWorkflow[],
18
+ options: SaveWorkflowOptions = {},
19
+ ): Promise<{ filePath: string; itemsCollected: number }> {
20
+ try {
21
+ const {
22
+ dir = COMMON.WORKFLOW_DIR,
23
+ fileName = COMMON.WORKFLOW_FILE,
24
+ append = true,
25
+ } = options;
26
+
27
+ const itemsCollected = Object.keys(data).length;
28
+ await mkdir(dir, { recursive: true });
29
+
30
+ const filePath = COMMON.WORKFLOW_PATH;
31
+ let finalData: CollectFormatTaskWorkflow[] = [];
32
+
33
+ if (append) {
34
+ try {
35
+ const existingContent = await readFile(filePath, "utf-8");
36
+ const existingData = JSON.parse(existingContent);
37
+ if (Array.isArray(existingData)) {
38
+ finalData = existingData.flat(Infinity);
39
+ } else {
40
+ console.error("现有文件格式不是数组,将被覆盖");
41
+ }
42
+ } catch (error: any) {
43
+ if (error.code !== "ENOENT") {
44
+ throw new McpError(
45
+ ErrorCode.InternalError,
46
+ `读取现有文件失败: ${error.message}`,
47
+ );
48
+ }
49
+ }
50
+ }
51
+
52
+ const newData = data.flat(Infinity);
53
+ finalData = [...finalData, ...newData];
54
+ const uniqueData = deduplicateWorkflows(finalData);
55
+
56
+ const filteredData = uniqueData.filter((item) => {
57
+ if (typeof item === "object" && item !== null && !Array.isArray(item)) {
58
+ return Object.keys(item).length > 0;
59
+ }
60
+ return true;
61
+ });
62
+
63
+ await writeFile(filePath, JSON.stringify(filteredData, null, 2), "utf-8");
64
+ return { filePath, itemsCollected };
65
+ } catch (error) {
66
+ if (error instanceof McpError) {
67
+ throw error;
68
+ }
69
+ throw new McpError(
70
+ ErrorCode.InternalError,
71
+ `保存工作流失败: ${error instanceof Error ? error.message : String(error)}`,
72
+ );
73
+ }
74
+ }
75
+
76
+ /**
77
+ * 保存自定义工作流到本地
78
+ */
79
+ export async function saveCustomWorkflow(
80
+ data: CollectFormatTaskWorkflow[],
81
+ options: SaveWorkflowOptions = {},
82
+ ): Promise<string> {
83
+ try {
84
+ const dir = COMMON.WORKFLOW_DIR;
85
+ const fileName = options.fileName!;
86
+
87
+ await mkdir(dir, { recursive: true });
88
+
89
+ const filePath = path.join(dir, fileName);
90
+
91
+ await writeFile(filePath, JSON.stringify(data, null, 2), "utf-8");
92
+ return filePath;
93
+ } catch (error) {
94
+ if (error instanceof McpError) {
95
+ throw error;
96
+ }
97
+ throw new McpError(
98
+ ErrorCode.InternalError,
99
+ `保存自定义工作流失败: ${error instanceof Error ? error.message : String(error)}`,
100
+ );
101
+ }
102
+ }
103
+
104
+ /**
105
+ * 根据 name 去重,保留最新的项
106
+ * 规则:
107
+ * 1. External 与 非External 重名时,优先保留 非External
108
+ * 2. 同类型时,保留 last_updated 最新的
109
+ * 3. InitialInspection 与 CompleteInspection 重名时,根据 userdata_modified 判断
110
+ */
111
+ function deduplicateWorkflows(
112
+ workflows: CollectFormatTaskWorkflow[],
113
+ ): CollectFormatTaskWorkflow[] {
114
+ const map = new Map<string, CollectFormatTaskWorkflow>();
115
+
116
+ for (const workflow of workflows) {
117
+ const existing = map.get(workflow.name);
118
+ if (!existing) {
119
+ map.set(workflow.name, workflow);
120
+ continue;
121
+ }
122
+ if (shouldReplaceWorkflow(existing, workflow)) {
123
+ map.set(workflow.name, workflow);
124
+ }
125
+ }
126
+
127
+ return Array.from(map.values());
128
+ }
129
+
130
+ /**
131
+ * 判断是否应该替换工作流
132
+ * 优先级规则:
133
+ * 1. External 与 非External 重名时,优先保留 非External(External 被抛弃)
134
+ * 2. InitialInspection 与 CompleteInspection 重名时,根据 userdata_modified 判断
135
+ * 3. 同类型时,保留 last_updated 最新的
136
+ */
137
+ function shouldReplaceWorkflow(
138
+ existing: CollectFormatTaskWorkflow,
139
+ candidate: CollectFormatTaskWorkflow,
140
+ ): boolean {
141
+ // 规则1: External 与 非External 重名时,优先保留 非External
142
+ const existingIsExternal = existing.inspection_status === "External";
143
+ const candidateIsExternal = candidate.inspection_status === "External";
144
+
145
+ if (existingIsExternal && !candidateIsExternal) {
146
+ // existing 是 External,candidate 不是,用 candidate 替换 existing
147
+ return true;
148
+ }
149
+
150
+ if (!existingIsExternal && candidateIsExternal) {
151
+ // existing 不是 External,candidate 是,保留 existing
152
+ return false;
153
+ }
154
+
155
+ // 规则2: 同类型时,保留 last_updated 最新的
156
+ if (existing.inspection_status === candidate.inspection_status) {
157
+ return candidate.last_updated > existing.last_updated;
158
+ }
159
+
160
+ // 规则3: InitialInspection 与 CompleteInspection 重名时,根据 userdata_modified 判断
161
+ const isInitialVsComplete =
162
+ (existing.inspection_status === "InitialInspection" &&
163
+ candidate.inspection_status === "CompleteInspection") ||
164
+ (existing.inspection_status === "CompleteInspection" &&
165
+ candidate.inspection_status === "InitialInspection");
166
+
167
+ if (isInitialVsComplete) {
168
+ const initial =
169
+ existing.inspection_status === "InitialInspection" ? existing : candidate;
170
+ const complete =
171
+ existing.inspection_status === "CompleteInspection"
172
+ ? existing
173
+ : candidate;
174
+
175
+ const initialModifiedTime = initial.userdata_modified ?? 0;
176
+ if (initialModifiedTime > complete.last_updated) {
177
+ return candidate === initial;
178
+ } else {
179
+ return candidate === complete;
180
+ }
181
+ }
182
+
183
+ // 其他情况(理论上不会走到这里,因为目前只有三种类型),按优先级处理
184
+ const existingPriority = sourcePriority[existing.inspection_status] ?? -1;
185
+ const candidatePriority = sourcePriority[candidate.inspection_status] ?? -1;
186
+
187
+ if (candidatePriority !== existingPriority) {
188
+ return candidatePriority > existingPriority;
189
+ }
190
+
191
+ return candidate.last_updated > existing.last_updated;
192
+ }
@@ -0,0 +1,69 @@
1
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
2
+ import { api } from "../../api/api";
3
+ import {
4
+ ExecutePromptRequest,
5
+ ExecutePromptResult,
6
+ } from "../../types/execute";
7
+ import { ComfyPromptConfig } from "../../types/task";
8
+
9
+ export interface ExecuteTaskOptions {
10
+ prompts: ComfyPromptConfig;
11
+ clientId: string;
12
+ }
13
+
14
+ /** 执行进度信息 */
15
+ export interface ExecutionProgress {
16
+ stage: "starting" | "executing" | "progress" | "completed" | "error";
17
+ message: string;
18
+ percent?: number;
19
+ nodeId?: string;
20
+ current?: number;
21
+ max?: number;
22
+ elapsedTime?: number;
23
+ }
24
+
25
+ /**
26
+ * 执行工作流任务
27
+ */
28
+ export async function executeWorkflowTaskByPrompts(
29
+ options: ExecuteTaskOptions,
30
+ ): Promise<ExecutePromptResult> {
31
+ const { prompts, clientId } = options;
32
+
33
+ if (!clientId) {
34
+ throw new McpError(
35
+ ErrorCode.InternalError,
36
+ `不存在WS客户端ID,请检查是否正确连接WS服务器`,
37
+ );
38
+ }
39
+
40
+ const data: ExecutePromptRequest = {
41
+ client_id: clientId,
42
+ prompt: prompts,
43
+ };
44
+
45
+ return await api.prompt(data);
46
+ }
47
+
48
+ /**
49
+ * 执行自定义工作流任务
50
+ */
51
+ export async function executeCustomWorkflowTaskByPrompts(
52
+ options: ExecuteTaskOptions,
53
+ ): Promise<ExecutePromptResult> {
54
+ const { prompts, clientId } = options;
55
+
56
+ if (!clientId) {
57
+ throw new McpError(
58
+ ErrorCode.InternalError,
59
+ `不存在WS客户端ID,请检查是否正确连接WS服务器`,
60
+ );
61
+ }
62
+
63
+ const data: ExecutePromptRequest = {
64
+ client_id: clientId,
65
+ prompt: prompts,
66
+ };
67
+
68
+ return await api.prompt(data);
69
+ }
@@ -0,0 +1,131 @@
1
+ import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
2
+ import { api } from "../../api/api";
3
+ import {
4
+ ComfyNodeOutput,
5
+ ComfyPromptConfig,
6
+ ComfyTaskItem,
7
+ ComfyTaskResponse,
8
+ } from "../../types/task";
9
+
10
+ export interface FetchTasksOptions {
11
+ maxItems?: number;
12
+ offset?: number;
13
+ }
14
+
15
+ export interface FetchTaskOptions {
16
+ promptId: string;
17
+ }
18
+
19
+ /**
20
+ * 获取历史任务(支持分页)
21
+ */
22
+ export async function fetchHistoryTasks(
23
+ options: FetchTasksOptions,
24
+ ): Promise<{ successTasks: ComfyTaskResponse; total: number; fail: number }> {
25
+ const { maxItems, offset } = options;
26
+ const res = await api.pageHistoryTasks(maxItems, offset);
27
+ const total = Object.entries(res).length;
28
+
29
+ const successTasks = Object.fromEntries(
30
+ Object.entries(res).filter(
31
+ ([uuid, item]) => item.status.status_str === "success",
32
+ ),
33
+ ) as ComfyTaskResponse;
34
+
35
+ const fail = total - Object.entries(successTasks).length;
36
+ return { successTasks, total, fail };
37
+ }
38
+
39
+ /**
40
+ * 获取指定promptIds对应的历史任务信息
41
+ */
42
+ export async function fetchUserWorkflow(
43
+ availableWorkflow: string[],
44
+ ): Promise<ComfyTaskResponse> {
45
+ let historyTasks: [string, ComfyTaskItem][] = [];
46
+
47
+ for (const promptId of availableWorkflow) {
48
+ const res = await api.getDetailHistoryTasks(promptId);
49
+ historyTasks = historyTasks.concat(Object.entries(res));
50
+ }
51
+
52
+ const successTasks = Object.fromEntries(historyTasks) as ComfyTaskResponse;
53
+
54
+ if (successTasks === null) {
55
+ throw new McpError(
56
+ ErrorCode.InternalError,
57
+ "Invalid detail tasks response",
58
+ );
59
+ }
60
+
61
+ return successTasks;
62
+ }
63
+
64
+ /**
65
+ * 根据 promptId 获取任务详情
66
+ */
67
+ export async function fetchTaskByPromptId(
68
+ options: FetchTaskOptions,
69
+ ): Promise<ComfyPromptConfig> {
70
+ const { promptId } = options;
71
+ const res = await api.getDetailHistoryTasks(promptId);
72
+
73
+ const successTasks = Object.fromEntries(
74
+ Object.entries(res).filter(([uuid, item]) => {
75
+ if (item.status.status_str === "success") {
76
+ return true;
77
+ } else {
78
+ const isExecutionInterrupted = item.status.messages.filter((item) => {
79
+ return item[0] === "execution_interrupted";
80
+ });
81
+ return isExecutionInterrupted.length > 0;
82
+ }
83
+ }),
84
+ ) as ComfyTaskResponse;
85
+
86
+ if (successTasks === null) {
87
+ throw new McpError(
88
+ ErrorCode.InternalError,
89
+ "Invalid detail tasks response",
90
+ );
91
+ }
92
+
93
+ return Object.values(successTasks)[0].prompt[2];
94
+ }
95
+
96
+ /**
97
+ * 获取成功历史任务的资产信息
98
+ */
99
+ export async function fetchAssetsByPromptId(
100
+ promptId: string,
101
+ ): Promise<ComfyNodeOutput[]> {
102
+ const res = await api.getDetailHistoryTasks(promptId);
103
+
104
+ const successTasks = Object.fromEntries(
105
+ Object.entries(res).filter(([uuid, item]) => {
106
+ if (item.status.status_str === "success") {
107
+ return true;
108
+ } else {
109
+ const isExecutionInterrupted = item.status.messages.filter((item) => {
110
+ return item[0] === "execution_interrupted";
111
+ });
112
+ return isExecutionInterrupted.length > 0;
113
+ }
114
+ }),
115
+ ) as ComfyTaskResponse;
116
+
117
+ if (successTasks === null) {
118
+ throw new McpError(
119
+ ErrorCode.InternalError,
120
+ "Invalid detail tasks response",
121
+ );
122
+ }
123
+
124
+ const outputs = Object.values(successTasks)[0].outputs;
125
+
126
+ if (Object.keys(outputs).length === 0) {
127
+ throw new McpError(ErrorCode.InternalError, "Invalid outputs response");
128
+ }
129
+
130
+ return Object.values(outputs);
131
+ }
@@ -0,0 +1,3 @@
1
+ export * from "./fetch";
2
+ export * from "./execution";
3
+ export * from "./wait";
@@ -0,0 +1,294 @@
1
+ import { ExecutionResult } from "../../types/execute";
2
+ import { ComfyClient } from "../../utils/ws";
3
+ import { ExecutionProgress } from "./execution";
4
+
5
+ export interface WaitForExecutionOptions {
6
+ client: ComfyClient;
7
+ promptId: string;
8
+ timeout?: number;
9
+ onProgress?: (progress: ExecutionProgress) => void;
10
+ }
11
+
12
+ /**
13
+ * 等待工作流执行完成
14
+ */
15
+ export async function waitForExecutionCompletion(
16
+ options: WaitForExecutionOptions,
17
+ ): Promise<ExecutionResult> {
18
+ const { client, promptId, timeout = 10 * 60 * 1000, onProgress } = options;
19
+ const startTime = Date.now();
20
+
21
+ console.error(`[等待执行] 开始监听 Prompt ID: ${promptId}`);
22
+
23
+ const reportProgress = (progress: ExecutionProgress) => {
24
+ if (onProgress) {
25
+ onProgress({
26
+ ...progress,
27
+ elapsedTime: Date.now() - startTime,
28
+ });
29
+ }
30
+ };
31
+
32
+ return new Promise((resolve, reject) => {
33
+ let isCompleted = false;
34
+ const outputs: Record<string, any> = {};
35
+
36
+ const timeoutTimer = setTimeout(() => {
37
+ if (!isCompleted) {
38
+ isCompleted = true;
39
+ cleanup();
40
+ resolve({
41
+ success: false,
42
+ promptId,
43
+ error: `工作流执行超时(${timeout / 1000}秒)`,
44
+ executionTime: Date.now() - startTime,
45
+ });
46
+ }
47
+ }, timeout);
48
+
49
+ const originalHooks = {
50
+ onExecutionStart: client.hook.onExecutionStart.bind(client.hook),
51
+ onNodeExecuting: client.hook.onNodeExecuting.bind(client.hook),
52
+ onProgress: client.hook.onProgress.bind(client.hook),
53
+ onExecuted: client.hook.onExecuted.bind(client.hook),
54
+ onExecutionSuccess: client.hook.onExecutionSuccess.bind(client.hook),
55
+ onExecutionError: client.hook.onExecutionError.bind(client.hook),
56
+ onExecutionInterrupted: client.hook.onExecutionInterrupted.bind(
57
+ client.hook,
58
+ ),
59
+ };
60
+
61
+ const cleanup = () => {
62
+ clearTimeout(timeoutTimer);
63
+ client.hook.onExecutionStart = originalHooks.onExecutionStart;
64
+ client.hook.onNodeExecuting = originalHooks.onNodeExecuting;
65
+ client.hook.onProgress = originalHooks.onProgress;
66
+ client.hook.onExecuted = originalHooks.onExecuted;
67
+ client.hook.onExecutionSuccess = originalHooks.onExecutionSuccess;
68
+ client.hook.onExecutionError = originalHooks.onExecutionError;
69
+ client.hook.onExecutionInterrupted = originalHooks.onExecutionInterrupted;
70
+ };
71
+
72
+ client.hook.onExecutionStart = (data) => {
73
+ console.error(`[onExecutionStart] 收到消息:`, data);
74
+ originalHooks.onExecutionStart(data);
75
+ if (data.prompt_id === promptId) {
76
+ console.error(`[执行开始] Prompt ID: ${promptId}`);
77
+ reportProgress({
78
+ stage: "starting",
79
+ message: "工作流开始执行",
80
+ percent: 0,
81
+ });
82
+ }
83
+ };
84
+
85
+ client.hook.onNodeExecuting = (data) => {
86
+ console.error(`[onNodeExecuting] 收到消息:`, data);
87
+ originalHooks.onNodeExecuting(data);
88
+ if (data.prompt_id === promptId && data.node) {
89
+ console.error(`[执行节点] Node: ${data.node}`);
90
+ reportProgress({
91
+ stage: "executing",
92
+ message: `正在执行节点: ${data.node}`,
93
+ nodeId: data.node,
94
+ });
95
+ }
96
+ };
97
+
98
+ client.hook.onProgress = (data) => {
99
+ originalHooks.onProgress(data);
100
+ if (data.prompt_id === promptId) {
101
+ const percent = Math.round((data.value / data.max) * 100);
102
+ console.error(`[进度更新] ${data.value}/${data.max} (${percent}%)`);
103
+ reportProgress({
104
+ stage: "progress",
105
+ message: `节点 ${data.node} 执行中: ${data.value}/${data.max}`,
106
+ percent,
107
+ nodeId: data.node,
108
+ current: data.value,
109
+ max: data.max,
110
+ });
111
+ }
112
+ };
113
+
114
+ client.hook.onExecuted = (data) => {
115
+ console.error(`[onExecuted] 收到消息:`, data);
116
+ originalHooks.onExecuted(data);
117
+ if (data.prompt_id === promptId) {
118
+ console.error(`[节点完成] Node: ${data.node}`);
119
+ if (data.output) {
120
+ outputs[data.node] = data.output;
121
+ }
122
+ }
123
+ };
124
+
125
+ client.hook.onExecutionSuccess = (data) => {
126
+ console.error(`[onExecutionSuccess] 收到消息:`, data);
127
+ originalHooks.onExecutionSuccess(data);
128
+ if (data.prompt_id === promptId && !isCompleted) {
129
+ isCompleted = true;
130
+ cleanup();
131
+ console.error(`[执行成功] Prompt ID: ${promptId}`);
132
+ reportProgress({
133
+ stage: "completed",
134
+ message: "工作流执行完成",
135
+ percent: 100,
136
+ });
137
+ resolve({
138
+ success: true,
139
+ promptId,
140
+ outputs,
141
+ executionTime: Date.now() - startTime,
142
+ });
143
+ }
144
+ };
145
+
146
+ client.hook.onExecutionError = (data) => {
147
+ console.error(`[onExecutionError] 收到消息:`, data);
148
+ originalHooks.onExecutionError(data);
149
+ if (data.prompt_id === promptId && !isCompleted) {
150
+ isCompleted = true;
151
+ cleanup();
152
+ console.error(`[执行错误] Prompt ID: ${promptId}`, data);
153
+ const errorMsg =
154
+ data.exception_message || data.error || JSON.stringify(data);
155
+ reportProgress({
156
+ stage: "error",
157
+ message: `执行出错: ${errorMsg}`,
158
+ });
159
+ resolve({
160
+ success: false,
161
+ promptId,
162
+ error: errorMsg,
163
+ executionTime: Date.now() - startTime,
164
+ });
165
+ }
166
+ };
167
+
168
+ client.hook.onExecutionInterrupted = (data) => {
169
+ console.error(`[onExecutionInterrupted] 收到消息:`, data);
170
+ originalHooks.onExecutionInterrupted(data);
171
+ if (data.prompt_id === promptId && !isCompleted) {
172
+ isCompleted = true;
173
+ cleanup();
174
+ console.error(`[执行中断] Prompt ID: ${promptId}`);
175
+ resolve({
176
+ success: false,
177
+ promptId,
178
+ error: "工作流执行被中断",
179
+ executionTime: Date.now() - startTime,
180
+ });
181
+ }
182
+ };
183
+ });
184
+ }
185
+
186
+ /**
187
+ * 等待工作流执行中断
188
+ */
189
+ export async function waitForExecutionInterrupt(
190
+ options: WaitForExecutionOptions,
191
+ ): Promise<ExecutionResult> {
192
+ const { client, promptId, timeout = 20 * 1000 } = options;
193
+ const startTime = Date.now();
194
+
195
+ console.error(`[等待中断信号] 开始监听 Prompt ID: ${promptId}`);
196
+
197
+ return new Promise((resolve, reject) => {
198
+ let isCompleted = false;
199
+
200
+ const timeoutTimer = setTimeout(() => {
201
+ if (!isCompleted) {
202
+ isCompleted = true;
203
+ cleanup();
204
+ resolve({
205
+ success: false,
206
+ promptId,
207
+ error: `工作流等待中断信号超时(${timeout / 1000}秒)`,
208
+ executionTime: Date.now() - startTime,
209
+ });
210
+ }
211
+ }, timeout);
212
+
213
+ const originalHooks = {
214
+ onExecutionInterrupted: client.hook.onExecutionInterrupted.bind(
215
+ client.hook,
216
+ ),
217
+ };
218
+
219
+ const cleanup = () => {
220
+ clearTimeout(timeoutTimer);
221
+ client.hook.onExecutionInterrupted = originalHooks.onExecutionInterrupted;
222
+ };
223
+
224
+ client.hook.onExecutionInterrupted = (data) => {
225
+ console.error(`[onExecutionInterrupted] 收到消息:`, data);
226
+ originalHooks.onExecutionInterrupted(data);
227
+ if (data.prompt_id === promptId && !isCompleted) {
228
+ isCompleted = true;
229
+ cleanup();
230
+ console.error(`[执行中断] Prompt ID: ${promptId}`);
231
+ resolve({
232
+ success: true,
233
+ promptId,
234
+ error: "工作流已收到中断信号",
235
+ executionTime: Date.now() - startTime,
236
+ });
237
+ }
238
+ };
239
+ });
240
+ }
241
+
242
+ /**
243
+ * 等待工作流开始执行
244
+ */
245
+ export async function waitForExecutionStart(
246
+ options: WaitForExecutionOptions,
247
+ ): Promise<ExecutionResult> {
248
+ const { client, promptId, timeout = 20 * 1000 } = options;
249
+ const startTime = Date.now();
250
+
251
+ console.error(`[等待开始信号] 开始监听 Prompt ID: ${promptId}`);
252
+
253
+ return new Promise((resolve, reject) => {
254
+ let isCompleted = false;
255
+
256
+ const timeoutTimer = setTimeout(() => {
257
+ if (!isCompleted) {
258
+ isCompleted = true;
259
+ cleanup();
260
+ resolve({
261
+ success: false,
262
+ promptId,
263
+ error: `工作流等待开始信号超时(${timeout / 1000}秒)`,
264
+ executionTime: Date.now() - startTime,
265
+ });
266
+ }
267
+ }, timeout);
268
+
269
+ const originalHooks = {
270
+ onExecutionStart: client.hook.onExecutionStart.bind(client.hook),
271
+ };
272
+
273
+ const cleanup = () => {
274
+ clearTimeout(timeoutTimer);
275
+ client.hook.onExecutionStart = originalHooks.onExecutionStart;
276
+ };
277
+
278
+ client.hook.onExecutionStart = (data) => {
279
+ console.error(`[onExecutionStart] 收到消息:`, data);
280
+ originalHooks.onExecutionStart(data);
281
+ if (data.prompt_id === promptId && !isCompleted) {
282
+ isCompleted = true;
283
+ cleanup();
284
+ console.error(`[执行开始] Prompt ID: ${promptId}`);
285
+ resolve({
286
+ success: true,
287
+ promptId,
288
+ error: "工作流已收到开始信号",
289
+ executionTime: Date.now() - startTime,
290
+ });
291
+ }
292
+ };
293
+ });
294
+ }