@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.
- package/.env +179 -0
- package/.github/workflows/ci.yml +63 -0
- package/.husky/pre-commit +11 -0
- package/LICENSE +21 -0
- package/README-en.md +635 -0
- package/README.md +656 -0
- package/__tests__/services/task/execution.test.ts +272 -0
- package/__tests__/services/task/wait.test.ts +220 -0
- package/__tests__/types/result.test.ts +227 -0
- package/__tests__/utils/mcp-helpers.test.ts +137 -0
- package/__tests__/utils/special-node-handler.test.ts +186 -0
- package/comfyui-mcp-server-skill/SKILL.md +43 -0
- package/comfyui-mcp-server-skill/references/api-json.md +323 -0
- package/comfyui-mcp-server-skill/references/catalog.md +251 -0
- package/comfyui-mcp-server-skill/rules/api-json.md +94 -0
- package/comfyui-mcp-server-skill/rules/catalog.md +93 -0
- package/comfyui-mcp-server-skill/rules/comfyui.md +158 -0
- package/configs/inspector/dev.json +11 -0
- package/configs/inspector/prod.json +9 -0
- package/docs/en/content/public/inspector-example.png +0 -0
- package/docs/en/content/public/workflow_name_example.png +0 -0
- package/docs/en/content/public/workflow_parameter_example.png +0 -0
- package/docs/en/md/Project-Advantages.md +75 -0
- package/docs/zh-CN/content/public/inspector-example.png +0 -0
- package/docs/zh-CN/content/public/workflow_name_example.png +0 -0
- package/docs/zh-CN/content/public/workflow_parameter_example.png +0 -0
- package/docs/zh-CN/md/why-us.md +51 -0
- package/example.json +26 -0
- package/jest.config.js +45 -0
- package/locales/en.json +150 -0
- package/locales/zh.json +150 -0
- package/package.json +68 -0
- package/src/api/api.ts +123 -0
- package/src/api/http.ts +70 -0
- package/src/constants/common.ts +38 -0
- package/src/constants/index.ts +1 -0
- package/src/hooks/websocket.ts +93 -0
- package/src/i18n.ts +40 -0
- package/src/index.ts +268 -0
- package/src/scripts/comfy-ui/list-history.ts +31 -0
- package/src/scripts/comfy-ui/run-ws.ts +23 -0
- package/src/scripts/ws/send-feature-flags.ts +23 -0
- package/src/server-stdio.ts +23 -0
- package/src/services/business.ts +194 -0
- package/src/services/dynamic-tool.ts +303 -0
- package/src/services/index.ts +19 -0
- package/src/services/storage/asset-storage.ts +48 -0
- package/src/services/storage/index.ts +2 -0
- package/src/services/storage/workflow-storage.ts +192 -0
- package/src/services/task/execution.ts +69 -0
- package/src/services/task/fetch.ts +131 -0
- package/src/services/task/index.ts +3 -0
- package/src/services/task/wait.ts +294 -0
- package/src/services/workflow/executor.ts +134 -0
- package/src/services/workflow/index.ts +1 -0
- package/src/tools/handlers/core-manual.ts +28 -0
- package/src/tools/handlers/index.ts +22 -0
- package/src/tools/handlers/interrupt-prompt.ts +35 -0
- package/src/tools/handlers/list-models.ts +51 -0
- package/src/tools/handlers/mount-workflow.ts +88 -0
- package/src/tools/handlers/prompt-result.ts +35 -0
- package/src/tools/handlers/prompts.ts +61 -0
- package/src/tools/handlers/queue-custom-prompt.ts +164 -0
- package/src/tools/handlers/queue-prompt.ts +170 -0
- package/src/tools/handlers/save-custom-workflow.ts +67 -0
- package/src/tools/handlers/save-task-assets.ts +82 -0
- package/src/tools/handlers/system-status.ts +30 -0
- package/src/tools/handlers/task-detail.ts +35 -0
- package/src/tools/handlers/tool-status-map.ts +33 -0
- package/src/tools/handlers/upload-assets.ts +82 -0
- package/src/tools/handlers/workflow-api.ts +46 -0
- package/src/tools/handlers/workflows-catalog.ts +79 -0
- package/src/tools/index.ts +101 -0
- package/src/types/common.ts +74 -0
- package/src/types/dynamic-tool.ts +85 -0
- package/src/types/enums/result.ts +29 -0
- package/src/types/execute.ts +24 -0
- package/src/types/object-info.ts +43 -0
- package/src/types/result.ts +126 -0
- package/src/types/task.ts +118 -0
- package/src/types/workflow.ts +111 -0
- package/src/types/ws.ts +80 -0
- package/src/utils/format.ts +134 -0
- package/src/utils/mcp-helpers.ts +110 -0
- package/src/utils/special-node-handler.ts +36 -0
- package/src/utils/workflow-converter.ts +140 -0
- package/src/utils/ws.ts +219 -0
- package/tsconfig.json +18 -0
package/src/types/ws.ts
ADDED
|
@@ -0,0 +1,80 @@
|
|
|
1
|
+
export interface ComfyStatusData {
|
|
2
|
+
status: {
|
|
3
|
+
exec_info: {
|
|
4
|
+
queue_remaining: number;
|
|
5
|
+
};
|
|
6
|
+
};
|
|
7
|
+
sid: string;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
export interface ComfyFeatureFlagsData {
|
|
11
|
+
supports_preview_metadata: boolean;
|
|
12
|
+
max_upload_size: number;
|
|
13
|
+
extension: {
|
|
14
|
+
manager?: {
|
|
15
|
+
supports_v4: boolean;
|
|
16
|
+
};
|
|
17
|
+
[key: string]: any;
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
export interface ComfyExecutionStartData {
|
|
22
|
+
prompt_id: string;
|
|
23
|
+
timestamp: number;
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
export interface ComfyExecutionCachedData {
|
|
27
|
+
nodes: string[];
|
|
28
|
+
prompt_id: string;
|
|
29
|
+
timestamp: number;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
export interface ComfyExecutionSuccessData {
|
|
33
|
+
prompt_id: string;
|
|
34
|
+
timestamp: number;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface ComfyProgressStateData {
|
|
38
|
+
prompt_id: string;
|
|
39
|
+
nodes: Record<string, ComfyNodeProgressState>;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
export interface ComfyProgressData {
|
|
43
|
+
prompt_id: string;
|
|
44
|
+
value: number;
|
|
45
|
+
max: number;
|
|
46
|
+
node: string;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
export interface ComfyExecutingData {
|
|
50
|
+
node: string | null;
|
|
51
|
+
display_node: string;
|
|
52
|
+
prompt_id: string;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
export interface ComfyExecutedData {
|
|
56
|
+
node: string;
|
|
57
|
+
display_node: string;
|
|
58
|
+
output: ComfyNodeOutputData;
|
|
59
|
+
prompt_id: string;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/** 节点进度详情 */
|
|
63
|
+
export interface ComfyNodeProgressState {
|
|
64
|
+
value: number; // 当前进度 (0.0 - 1.0 或具体步数)
|
|
65
|
+
max: number; // 最大进度/总步数
|
|
66
|
+
state: "running" | "finished" | "pending" | string; // 节点状态
|
|
67
|
+
node_id: string; // 当前节点 ID
|
|
68
|
+
prompt_id: string; // 关联的任务 ID
|
|
69
|
+
display_node_id: string; // UI 显示的节点 ID
|
|
70
|
+
parent_node_id: string | null;
|
|
71
|
+
real_node_id: string; // 实际执行的节点 ID
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
import { ComfyImage } from "./task";
|
|
75
|
+
|
|
76
|
+
/** 节点执行结果输出 (通常包含 images, 但也可能有 tags, text 等) */
|
|
77
|
+
export interface ComfyNodeOutputData {
|
|
78
|
+
images?: ComfyImage[];
|
|
79
|
+
[key: string]: any; // 允许其他类型的输出(如 text, gifs 等)
|
|
80
|
+
}
|
|
@@ -0,0 +1,134 @@
|
|
|
1
|
+
import { CollectFormatTaskResult, SourceType } from "../types/common";
|
|
2
|
+
import { ComfyPromptConfig, ComfyTaskResponse } from "../types/task";
|
|
3
|
+
import "dotenv/config";
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @METHOD
|
|
7
|
+
* @description 格式化工作流任务数据
|
|
8
|
+
*/
|
|
9
|
+
export const formatTask = (
|
|
10
|
+
data: ComfyTaskResponse,
|
|
11
|
+
sourceType: SourceType,
|
|
12
|
+
modifiedWorkflow?: Map<string, number>,
|
|
13
|
+
workflowNames?: Map<string, string>,
|
|
14
|
+
): CollectFormatTaskResult => {
|
|
15
|
+
const workflowNameRegexString =
|
|
16
|
+
process.env.WORKFLOW_NAME_REGEX || "==(.+?)==";
|
|
17
|
+
const workflowParamRegexString = process.env.WORKFLOW_PARAM_REGEX || "^=>";
|
|
18
|
+
|
|
19
|
+
const workflowNameRegex = new RegExp(workflowNameRegexString);
|
|
20
|
+
const workflowParamRegex = new RegExp(workflowParamRegexString);
|
|
21
|
+
|
|
22
|
+
const result: CollectFormatTaskResult = {
|
|
23
|
+
last_updated: Date.now(),
|
|
24
|
+
workflows: [],
|
|
25
|
+
};
|
|
26
|
+
|
|
27
|
+
for (const [uuid, item] of Object.entries(data)) {
|
|
28
|
+
const promptConfig: ComfyPromptConfig = item.prompt[2];
|
|
29
|
+
const promptId: string = item.prompt[1];
|
|
30
|
+
const parameters: string[] = [];
|
|
31
|
+
let modified: number | undefined;
|
|
32
|
+
let workflowName: string | undefined;
|
|
33
|
+
let description: string | null = null;
|
|
34
|
+
let name: string | null = null;
|
|
35
|
+
|
|
36
|
+
for (const [nodeId, nodeConfig] of Object.entries(promptConfig)) {
|
|
37
|
+
const isDesc = nodeConfig._meta?.title?.match(workflowNameRegex);
|
|
38
|
+
if (isDesc) {
|
|
39
|
+
description =
|
|
40
|
+
(nodeConfig.inputs["value"] as string) || "无工作流描述内容";
|
|
41
|
+
name = isDesc[1] || "无工作流名称";
|
|
42
|
+
}
|
|
43
|
+
const isRequired = nodeConfig._meta?.title?.match(workflowParamRegex);
|
|
44
|
+
if (isRequired) {
|
|
45
|
+
parameters.push(nodeConfig.class_type + nodeConfig._meta?.title);
|
|
46
|
+
}
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// 如果name为null或者description为null的时候则不纳入列表
|
|
50
|
+
if (!name || !description) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const timestamp = item.status.messages.find(
|
|
55
|
+
([type]) => type === "execution_start",
|
|
56
|
+
)?.[1].timestamp;
|
|
57
|
+
|
|
58
|
+
if (!timestamp) {
|
|
59
|
+
continue;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
if (
|
|
63
|
+
sourceType === "InitialInspection" &&
|
|
64
|
+
modifiedWorkflow &&
|
|
65
|
+
workflowNames
|
|
66
|
+
) {
|
|
67
|
+
modified = modifiedWorkflow.get(promptId);
|
|
68
|
+
workflowName = workflowNames.get(promptId);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
result.workflows.push({
|
|
72
|
+
name: name,
|
|
73
|
+
id: uuid,
|
|
74
|
+
description: description,
|
|
75
|
+
parameters: parameters,
|
|
76
|
+
last_updated: timestamp,
|
|
77
|
+
inspection_status: sourceType,
|
|
78
|
+
userdata_modified: modified,
|
|
79
|
+
workflowPath: workflowName,
|
|
80
|
+
});
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
return result;
|
|
84
|
+
};
|
|
85
|
+
|
|
86
|
+
export const formatTaskFromApiJson = (
|
|
87
|
+
apiJson: Record<string, any>,
|
|
88
|
+
promptId: string,
|
|
89
|
+
): CollectFormatTaskResult => {
|
|
90
|
+
const workflowNameRegexString =
|
|
91
|
+
process.env.WORKFLOW_NAME_REGEX || "==(.+?)==";
|
|
92
|
+
const workflowParamRegexString = process.env.WORKFLOW_PARAM_REGEX || "^=>";
|
|
93
|
+
|
|
94
|
+
const workflowNameRegex = new RegExp(workflowNameRegexString);
|
|
95
|
+
const workflowParamRegex = new RegExp(workflowParamRegexString);
|
|
96
|
+
|
|
97
|
+
const result: CollectFormatTaskResult = {
|
|
98
|
+
last_updated: Date.now(),
|
|
99
|
+
workflows: [],
|
|
100
|
+
};
|
|
101
|
+
|
|
102
|
+
const parameters: string[] = [];
|
|
103
|
+
let description: string | null = null;
|
|
104
|
+
let name: string | null = null;
|
|
105
|
+
|
|
106
|
+
for (const [nodeId, nodeConfig] of Object.entries(apiJson)) {
|
|
107
|
+
const isDesc = nodeConfig._meta?.title?.match(workflowNameRegex);
|
|
108
|
+
if (isDesc) {
|
|
109
|
+
description =
|
|
110
|
+
(nodeConfig.inputs["value"] as string) || "无工作流描述内容";
|
|
111
|
+
name = isDesc[1] || "无工作流名称";
|
|
112
|
+
}
|
|
113
|
+
const isRequired = workflowParamRegex.test(nodeConfig._meta?.title);
|
|
114
|
+
if (isRequired) {
|
|
115
|
+
parameters.push(nodeConfig.class_type + nodeConfig._meta?.title);
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// 如果name为null或者description为null的时候则不纳入列表
|
|
120
|
+
if (!name || !description) {
|
|
121
|
+
throw new Error("Invalid workflow name or description");
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
result.workflows.push({
|
|
125
|
+
name: name,
|
|
126
|
+
id: promptId,
|
|
127
|
+
description: description,
|
|
128
|
+
parameters: parameters,
|
|
129
|
+
last_updated: Date.now(),
|
|
130
|
+
inspection_status: "External",
|
|
131
|
+
});
|
|
132
|
+
|
|
133
|
+
return result;
|
|
134
|
+
};
|
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
import type { CallToolResult } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
3
|
+
import { ExecutionResult } from "../types/execute";
|
|
4
|
+
import { Result } from "../types/result";
|
|
5
|
+
import "dotenv/config";
|
|
6
|
+
|
|
7
|
+
const BASE_URL = process.env.COMFY_UI_SERVER_IP ?? "http://127.0.0.1:8188";
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* @METHOD
|
|
11
|
+
* @description 包装 MCP 工具处理函数,自动处理错误
|
|
12
|
+
*/
|
|
13
|
+
export function withMcpErrorHandling<T extends any[], R>(
|
|
14
|
+
handler: (...args: T) => Promise<R>,
|
|
15
|
+
) {
|
|
16
|
+
return async (...args: T): Promise<CallToolResult> => {
|
|
17
|
+
try {
|
|
18
|
+
const result = await handler(...args);
|
|
19
|
+
|
|
20
|
+
if (result && typeof result === "object" && "content" in result) {
|
|
21
|
+
return result as CallToolResult;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
// 否则包装成 CallToolResult
|
|
25
|
+
return {
|
|
26
|
+
content: [
|
|
27
|
+
{
|
|
28
|
+
type: "text",
|
|
29
|
+
text: JSON.stringify(result, null, 2),
|
|
30
|
+
},
|
|
31
|
+
],
|
|
32
|
+
};
|
|
33
|
+
} catch (error) {
|
|
34
|
+
if (error instanceof McpError) {
|
|
35
|
+
throw error;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
throw new McpError(
|
|
39
|
+
ErrorCode.InternalError,
|
|
40
|
+
error instanceof Error ? error.message : String(error),
|
|
41
|
+
);
|
|
42
|
+
}
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
/**
|
|
47
|
+
* @METHOD
|
|
48
|
+
* @description 将结构化结果转换为 MCP 响应格式
|
|
49
|
+
*/
|
|
50
|
+
export function ResultToMcpResponse(result: Result): CallToolResult {
|
|
51
|
+
if (result.success) {
|
|
52
|
+
return {
|
|
53
|
+
content: [
|
|
54
|
+
{
|
|
55
|
+
type: "text",
|
|
56
|
+
text: JSON.stringify(result, null, 2),
|
|
57
|
+
},
|
|
58
|
+
],
|
|
59
|
+
};
|
|
60
|
+
} else {
|
|
61
|
+
return {
|
|
62
|
+
content: [
|
|
63
|
+
{
|
|
64
|
+
type: "text",
|
|
65
|
+
text: JSON.stringify(result, null, 2),
|
|
66
|
+
},
|
|
67
|
+
],
|
|
68
|
+
isError: true,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* @METHOD
|
|
75
|
+
* @description 将文本结果转换为 MCP 响应格式
|
|
76
|
+
*/
|
|
77
|
+
export function ResultToMcpStringResponse(result: string): CallToolResult {
|
|
78
|
+
return {
|
|
79
|
+
content: [
|
|
80
|
+
{
|
|
81
|
+
type: "text",
|
|
82
|
+
text: JSON.stringify(result, null, 2),
|
|
83
|
+
},
|
|
84
|
+
],
|
|
85
|
+
};
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @METHOD
|
|
90
|
+
* @description 拼接生成资源路径
|
|
91
|
+
*/
|
|
92
|
+
export function buildComfyViewUrls(result: ExecutionResult): string[] {
|
|
93
|
+
if (!result.outputs) return [];
|
|
94
|
+
|
|
95
|
+
const urls: string[] = [];
|
|
96
|
+
|
|
97
|
+
for (const nodeOutput of Object.values(result.outputs)) {
|
|
98
|
+
for (const img of nodeOutput.images ?? []) {
|
|
99
|
+
const params = new URLSearchParams({
|
|
100
|
+
filename: img.filename,
|
|
101
|
+
type: img.type,
|
|
102
|
+
subfolder: img.subfolder ?? "",
|
|
103
|
+
});
|
|
104
|
+
|
|
105
|
+
urls.push(`${BASE_URL}/view?${params.toString()}`);
|
|
106
|
+
}
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
return urls;
|
|
110
|
+
}
|
|
@@ -0,0 +1,36 @@
|
|
|
1
|
+
// 当前需要特殊处理的结点参数
|
|
2
|
+
const supportedKeys = new Set<string>(["seed"]);
|
|
3
|
+
|
|
4
|
+
/**
|
|
5
|
+
* @METHOD
|
|
6
|
+
* @description 是否是受控 key
|
|
7
|
+
*/
|
|
8
|
+
export function isSupportedKey(key: string): boolean {
|
|
9
|
+
return supportedKeys.has(key.toLowerCase());
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @METHOD
|
|
14
|
+
* @description 处理 key 对应生成随机数逻辑
|
|
15
|
+
*/
|
|
16
|
+
export function handleKey(key: string): number | string {
|
|
17
|
+
const lowerKey = key.toLowerCase();
|
|
18
|
+
|
|
19
|
+
switch (lowerKey) {
|
|
20
|
+
case "seed":
|
|
21
|
+
return generateSeed32();
|
|
22
|
+
|
|
23
|
+
default:
|
|
24
|
+
return generateSeed32();
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* @METHOD
|
|
30
|
+
* @description 生成32位随机数
|
|
31
|
+
*/
|
|
32
|
+
export function generateSeed32(): number {
|
|
33
|
+
const array = new Uint32Array(1);
|
|
34
|
+
crypto.getRandomValues(array);
|
|
35
|
+
return array[0];
|
|
36
|
+
}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
import { ErrorCode, McpError } from "@modelcontextprotocol/sdk/types.js";
|
|
2
|
+
import { ObjectInfoResponse } from "../types/object-info";
|
|
3
|
+
import { ComfyInputValue, ComfyPromptConfig } from "../types/task";
|
|
4
|
+
import {
|
|
5
|
+
ComfyLink,
|
|
6
|
+
ComfyNode,
|
|
7
|
+
ComfyNodeMode,
|
|
8
|
+
ComfyUIWorkflow,
|
|
9
|
+
} from "../types/workflow";
|
|
10
|
+
import { api } from "../api/api";
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* @METHOD
|
|
14
|
+
* @description 根据 ComfyUI 规则,将从原始工作流中获取的信息(ComfyUIWorkflow)格式化为执行工作流需要用到的信息(ComfyPromptConfig)
|
|
15
|
+
*/
|
|
16
|
+
export class WorkflowConverter {
|
|
17
|
+
private objectInfo: ObjectInfoResponse = {};
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* @METHOD
|
|
21
|
+
* @description 初始化,加载图的节点对象定义
|
|
22
|
+
*/
|
|
23
|
+
async init(): Promise<void> {
|
|
24
|
+
this.objectInfo = await api.getNodeDefs();
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* @METHOD
|
|
29
|
+
* @description 格式化核心方法 —— 将 ComfyUIWorkflow 转换为 ComfyPromptConfig
|
|
30
|
+
* @param workflow ComfyUIWorkflow 数据
|
|
31
|
+
* @returns ComfyPromptConfig 对象,可直接用于 /prompt 接口
|
|
32
|
+
*/
|
|
33
|
+
convert(workflow: ComfyUIWorkflow): ComfyPromptConfig {
|
|
34
|
+
const output: ComfyPromptConfig = {};
|
|
35
|
+
const workflowId = workflow?.id || "";
|
|
36
|
+
|
|
37
|
+
// 1. 创建所有节点的映射,过滤掉不需要的节点
|
|
38
|
+
const validNodes = workflow.nodes.filter((node: ComfyNode) => {
|
|
39
|
+
return (
|
|
40
|
+
!node.mode ||
|
|
41
|
+
(node.mode !== ComfyNodeMode.Bypass &&
|
|
42
|
+
node.mode !== ComfyNodeMode.Never)
|
|
43
|
+
);
|
|
44
|
+
});
|
|
45
|
+
|
|
46
|
+
// 2. 构建节点ID到节点的映射
|
|
47
|
+
const nodeMap = new Map<string, ComfyNode>();
|
|
48
|
+
validNodes.forEach((node: ComfyNode) => {
|
|
49
|
+
nodeMap.set(String(node.id), node);
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// 3. 构建连接关系映射 links 结构: [link_id, origin_id, origin_slot, target_id, target_slot, link_type]
|
|
53
|
+
const linksMap = new Map<string, Array<string | number>>();
|
|
54
|
+
workflow.links.forEach((link: ComfyLink) => {
|
|
55
|
+
const linkId = link[0];
|
|
56
|
+
const originId = String(link[1]);
|
|
57
|
+
const originSlot = link[2];
|
|
58
|
+
const linkType = link[5];
|
|
59
|
+
|
|
60
|
+
const linkKey = `${linkId}:${linkType}`;
|
|
61
|
+
if (!linksMap.has(linkKey)) {
|
|
62
|
+
linksMap.set(linkKey, []);
|
|
63
|
+
}
|
|
64
|
+
linksMap.get(linkKey)!.push(originId, originSlot);
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
// 4. 处理每个节点
|
|
68
|
+
validNodes.forEach((node: ComfyNode) => {
|
|
69
|
+
const nodeId = node.id;
|
|
70
|
+
const inputs: Record<string, ComfyInputValue> = {};
|
|
71
|
+
const inputNames: string[] = [];
|
|
72
|
+
let noLink = 0;
|
|
73
|
+
|
|
74
|
+
const object = Object.entries(this.objectInfo).find(([key, value]) => {
|
|
75
|
+
return key === node.type;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (!object) {
|
|
79
|
+
throw new McpError(
|
|
80
|
+
ErrorCode.InternalError,
|
|
81
|
+
"MCP_OBJECT_INFO_NOT_FOUND",
|
|
82
|
+
);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
Object.entries(object[1].input.required).forEach(([inputName, input]) => {
|
|
86
|
+
inputNames.push(inputName);
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
if (object[1].input.optional) {
|
|
90
|
+
Object.entries(object[1].input.optional).forEach(
|
|
91
|
+
([inputName, input]) => {
|
|
92
|
+
inputNames.push(inputName);
|
|
93
|
+
},
|
|
94
|
+
);
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
if (object[1].input.hidden) {
|
|
98
|
+
Object.entries(object[1].input.hidden).forEach(([inputName, input]) => {
|
|
99
|
+
inputNames.push(inputName);
|
|
100
|
+
});
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
inputNames.forEach((inputName: string) => {
|
|
104
|
+
for (let i = 0; i < node.inputs.length; i++) {
|
|
105
|
+
if (inputName === node.inputs[i].name) {
|
|
106
|
+
if (node.inputs[i].link !== null && !node.inputs[i].widget) {
|
|
107
|
+
inputs[inputName] =
|
|
108
|
+
linksMap.get(`${node.inputs[i].link}:${node.inputs[i].type}`) ||
|
|
109
|
+
[];
|
|
110
|
+
} else {
|
|
111
|
+
if (Array.isArray(node.widgets_values)) {
|
|
112
|
+
if (node.widgets_values[noLink] === "randomize") {
|
|
113
|
+
noLink++;
|
|
114
|
+
}
|
|
115
|
+
inputs[inputName] = node.widgets_values[noLink++];
|
|
116
|
+
} else if (
|
|
117
|
+
typeof node.widgets_values === "object" &&
|
|
118
|
+
node.widgets_values !== null
|
|
119
|
+
) {
|
|
120
|
+
inputs[inputName] = node.widgets_values[inputName];
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
break;
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
// 构建节点对象
|
|
129
|
+
output[nodeId] = {
|
|
130
|
+
inputs,
|
|
131
|
+
class_type: node.type,
|
|
132
|
+
_meta: {
|
|
133
|
+
title: node.title || node.type,
|
|
134
|
+
},
|
|
135
|
+
};
|
|
136
|
+
});
|
|
137
|
+
|
|
138
|
+
return output;
|
|
139
|
+
}
|
|
140
|
+
}
|