dsh-comfyui 0.5.3 → 0.5.5

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/lib/analyze.js CHANGED
@@ -5,7 +5,7 @@
5
5
  * graph links (with bypassed and dangling nodes excluded). The analysis feeds
6
6
  * the extract (拆分) choices in the panel and the agent-facing skill.
7
7
  */
8
- import { normalizeLinks } from './graph.js';
8
+ import { isVirtualNodeType, normalizeLinks, resolveVirtualLinks } from './graph.js';
9
9
  function isObject(value) {
10
10
  return typeof value === 'object' && value !== null && !Array.isArray(value);
11
11
  }
@@ -28,11 +28,15 @@ export function analyzeGraph(graph) {
28
28
  return { ok: false, error: '无法解析图文件(缺少 nodes/links)' };
29
29
  }
30
30
  const nodes = graph.nodes;
31
- const links = normalizeLinks(graph.links);
31
+ // Virtual nodes (Set/Get wireless links, rgthree mode relays) are rewired
32
+ // out first, exactly as conversion does, so components follow the real data
33
+ // flow and the virtual nodes themselves never show up as components or as
34
+ // dangling nodes (Issue #8).
35
+ const links = resolveVirtualLinks(graph.nodes, normalizeLinks(graph.links));
32
36
  const groups = Array.isArray(graph.groups) ? graph.groups : [];
33
- const active = nodes.filter((node) => node.mode !== 4);
37
+ const active = nodes.filter((node) => node.mode !== 4 && !isVirtualNodeType(node.type));
34
38
  const activeById = new Map(active.map((node) => [node.id, node]));
35
- const bypassedCount = nodes.length - active.length;
39
+ const bypassedCount = nodes.filter((node) => node.mode === 4).length;
36
40
  // Dangling nodes: active but touching no link at all.
37
41
  const linkedIds = new Set();
38
42
  for (const link of links) {
package/lib/comfyui.d.ts CHANGED
@@ -171,6 +171,16 @@ export declare class ComfyUIClient {
171
171
  deleteQueueItems(promptIds: string[]): Promise<void>;
172
172
  /** Clear the entire pending queue (running job is unaffected). */
173
173
  clearQueue(): Promise<void>;
174
+ /**
175
+ * Stop one prompt without touching anyone else's work: dequeue it while
176
+ * pending, interrupt it only while it is the running one, no-op once it has
177
+ * left the queue. A bare `interrupt()` stops whatever is running — another
178
+ * session's job or the user's own ComfyUI run — and ComfyUI versions before
179
+ * prompt-scoped interrupts ignore the `prompt_id` body, hence the queue
180
+ * check. (Older servers keep a tiny race: the prompt could finish between
181
+ * the check and the interrupt.)
182
+ */
183
+ cancelPrompt(promptId: string): Promise<void>;
174
184
  /** Interrupt the running prompt; without an id, interrupt globally. */
175
185
  interruptPrompt(promptId?: string): Promise<void>;
176
186
  /** Cancel one job regardless of state (running → interrupt, pending → dequeue). */
@@ -202,8 +212,6 @@ export declare class ComfyUIClient {
202
212
  comfyui_version?: string;
203
213
  };
204
214
  }>;
205
- /** Ask ComfyUI to interrupt the running prompt. */
206
- interrupt(): Promise<void>;
207
215
  /** Download one generated media file through GET /view. */
208
216
  fetchView(ref: ComfyUIMediaRef): Promise<{
209
217
  bytes: Uint8Array;
@@ -221,13 +229,16 @@ export declare class ComfyUIClient {
221
229
  fetchViewStreamed(ref: ComfyUIMediaRef, rangeHeader?: string, method?: 'GET' | 'HEAD'): Promise<ComfyViewStream>;
222
230
  /**
223
231
  * Poll history until the prompt completes, fails, or the budget/signal ends.
224
- * Interrupts the server when the signal aborts before throwing.
232
+ * When the signal aborts — mid-sleep included — cancels this prompt only
233
+ * (`cancelPrompt`) and throws an "interrupted" error. `onPoll` runs once per
234
+ * poll, for callers that publish progress.
225
235
  */
226
236
  waitForCompletion(opts: {
227
237
  promptId: string;
228
238
  timeoutMs: number;
229
239
  pollIntervalMs: number;
230
240
  signal: AbortSignal;
241
+ onPoll?: () => void;
231
242
  }): Promise<ComfyUIHistoryEntry>;
232
243
  }
233
244
  export declare function hasMedia(entry: ComfyUIHistoryEntry): boolean;
package/lib/comfyui.js CHANGED
@@ -268,6 +268,25 @@ export class ComfyUIClient {
268
268
  body: JSON.stringify({ clear: true }),
269
269
  });
270
270
  }
271
+ /**
272
+ * Stop one prompt without touching anyone else's work: dequeue it while
273
+ * pending, interrupt it only while it is the running one, no-op once it has
274
+ * left the queue. A bare `interrupt()` stops whatever is running — another
275
+ * session's job or the user's own ComfyUI run — and ComfyUI versions before
276
+ * prompt-scoped interrupts ignore the `prompt_id` body, hence the queue
277
+ * check. (Older servers keep a tiny race: the prompt could finish between
278
+ * the check and the interrupt.)
279
+ */
280
+ async cancelPrompt(promptId) {
281
+ const queue = await this.getQueue();
282
+ if (queue.queue_pending.some((item) => item.prompt_id === promptId)) {
283
+ await this.deleteQueueItems([promptId]);
284
+ return;
285
+ }
286
+ if (queue.queue_running.some((item) => item.prompt_id === promptId)) {
287
+ await this.interruptPrompt(promptId);
288
+ }
289
+ }
271
290
  /** Interrupt the running prompt; without an id, interrupt globally. */
272
291
  async interruptPrompt(promptId) {
273
292
  await this.request('/interrupt', {
@@ -361,10 +380,6 @@ export class ComfyUIClient {
361
380
  async systemStats() {
362
381
  return this.request('/system_stats');
363
382
  }
364
- /** Ask ComfyUI to interrupt the running prompt. */
365
- async interrupt() {
366
- await this.request('/interrupt', { method: 'POST' });
367
- }
368
383
  /** Download one generated media file through GET /view. */
369
384
  async fetchView(ref) {
370
385
  const params = new URLSearchParams({ filename: ref.filename, subfolder: ref.subfolder, type: ref.type });
@@ -409,12 +424,14 @@ export class ComfyUIClient {
409
424
  headers['Range'] = rangeHeader;
410
425
  const response = await this.fetchRoute(`/view?${params.toString()}`, { headers, method, signal: controller.signal });
411
426
  if (response.status !== 206 && response.status !== 416 && !response.ok) {
412
- throw new ComfyUIError(`ComfyUI /view failed: HTTP ${response.status}`);
427
+ await response.body?.cancel().catch(() => undefined);
428
+ throw new ComfyUIError(`ComfyUI /view failed: HTTP ${response.status}`, response.status);
413
429
  }
414
430
  if (response.status === 200) {
415
431
  const contentLength = response.headers.get('content-length');
416
432
  if (contentLength !== null && Number(contentLength) > this.maxMediaBytes) {
417
- throw new ComfyUIError(`ComfyUI media too large: ${contentLength} bytes exceeds maxMediaBytes ${this.maxMediaBytes}`);
433
+ await response.body?.cancel().catch(() => undefined);
434
+ throw new ComfyUIError(`ComfyUI media too large: ${contentLength} bytes exceeds maxMediaBytes ${this.maxMediaBytes}`, 413);
418
435
  }
419
436
  }
420
437
  return { status: response.status, headers: response.headers, body: response.body };
@@ -425,16 +442,19 @@ export class ComfyUIClient {
425
442
  }
426
443
  /**
427
444
  * Poll history until the prompt completes, fails, or the budget/signal ends.
428
- * Interrupts the server when the signal aborts before throwing.
445
+ * When the signal aborts — mid-sleep included — cancels this prompt only
446
+ * (`cancelPrompt`) and throws an "interrupted" error. `onPoll` runs once per
447
+ * poll, for callers that publish progress.
429
448
  */
430
449
  async waitForCompletion(opts) {
431
- const { promptId, timeoutMs, pollIntervalMs, signal } = opts;
450
+ const { promptId, timeoutMs, pollIntervalMs, signal, onPoll } = opts;
432
451
  const deadline = Date.now() + timeoutMs;
433
452
  for (;;) {
434
453
  if (signal.aborted) {
435
- await this.interrupt().catch(() => undefined);
454
+ await this.cancelPrompt(promptId).catch(() => undefined);
436
455
  throw new ComfyUIError(`ComfyUI generation interrupted (prompt ${promptId})`);
437
456
  }
457
+ onPoll?.();
438
458
  const entry = await this.getHistory(promptId);
439
459
  if (entry !== undefined) {
440
460
  const status = entry.status;
@@ -448,7 +468,8 @@ export class ComfyUIClient {
448
468
  if (Date.now() >= deadline) {
449
469
  throw new ComfyUIError(`ComfyUI generation timed out after ${timeoutMs} ms (prompt ${promptId})`);
450
470
  }
451
- await sleep(pollIntervalMs, signal);
471
+ // An abort ends the sleep early; the loop top then cancels and throws.
472
+ await sleep(pollIntervalMs, signal).catch(() => undefined);
452
473
  }
453
474
  }
454
475
  }
package/lib/convert.js CHANGED
@@ -7,7 +7,7 @@
7
7
  * Reroute / bypassed (mode 4) nodes are skipped with their links rewired.
8
8
  * Nodes the conversion cannot represent fail loudly with the offending type.
9
9
  */
10
- import { normalizeLinks } from './graph.js';
10
+ import { isVirtualNodeType, normalizeLinks, resolveVirtualLinks } from './graph.js';
11
11
  /** Node types that exist only in the UI and carry no data flow. */
12
12
  const UI_ONLY = new Set(['Note', 'StickyNote', 'Reroute', 'Fast Groups Bypasser (rgthree)']);
13
13
  function isObject(value) {
@@ -253,7 +253,9 @@ export function convertGraphToApi(graph, objectInfo, options) {
253
253
  // normalizeLinks accepts both the legacy positional rows and the v0.4
254
254
  // frontend's object entries; anything unreadable is skipped, exactly like
255
255
  // the old array-only guard did.
256
- for (const link of normalizeLinks(rawLinks))
256
+ // Set/Get wireless links and rgthree mode relays are rewired out before
257
+ // anything resolves a link (Issue #8), shared with analyzeGraph.
258
+ for (const link of resolveVirtualLinks(rawNodes, normalizeLinks(rawLinks)))
257
259
  links.set(link[0], link);
258
260
  const nodesById = new Map(nodes.map((node) => [node.id, node]));
259
261
  const included = options?.includeNodeIds;
@@ -272,7 +274,7 @@ export function convertGraphToApi(graph, objectInfo, options) {
272
274
  const brokenInputs = new Map();
273
275
  const dropReasons = new Map();
274
276
  for (const node of candidates) {
275
- if (node.type === '' || UI_ONLY.has(node.type))
277
+ if (node.type === '' || UI_ONLY.has(node.type) || isVirtualNodeType(node.type))
276
278
  continue;
277
279
  if (node.mode === 4)
278
280
  continue;
package/lib/graph.d.ts CHANGED
@@ -13,3 +13,32 @@ export type GraphLink = [number, number, number, number, number, string];
13
13
  export declare function normalizeLink(raw: unknown): GraphLink | undefined;
14
14
  /** Normalize a whole `links` array, skipping entries that are not readable. */
15
15
  export declare function normalizeLinks(raw: unknown): GraphLink[];
16
+ /**
17
+ * Frontend-only nodes whose links are rewired or dropped before analysis and
18
+ * conversion (Issue #8). None of them exists on the server.
19
+ *
20
+ * - KJNodes `SetNode` / `GetNode`: "wireless" links paired by name. A GetNode's
21
+ * output is the value wired into the SetNode of the same name (and a
22
+ * SetNode's own passthrough output is its input).
23
+ * - rgthree `Mute / Bypass Relay` / `Repeater`: their `OPT_CONNECTION` links
24
+ * only propagate mute/bypass mode between nodes and carry no data. The mode
25
+ * they set is already saved on each target node, so the links are dropped.
26
+ */
27
+ export declare const SET_NODE = "SetNode";
28
+ export declare const GET_NODE = "GetNode";
29
+ /** Whether a node type is one of the virtual nodes {@link resolveVirtualLinks} removes. */
30
+ export declare function isVirtualNodeType(type: string): boolean;
31
+ /**
32
+ * Rewire the virtual nodes out of a graph's links, keeping link ids stable so
33
+ * node `inputs[].link` references stay valid.
34
+ *
35
+ * A GetNode resolves to the SetNode of the same name with the greatest `order`
36
+ * below its own (the scope rule the frontend applies), falling back to any
37
+ * SetNode of that name. A GetNode without a matching, wired SetNode (e.g. an
38
+ * empty optional slot) resolves to nothing, so its consumer stays unconnected.
39
+ * @param rawNodes - the graph's `nodes` array as saved.
40
+ * @param links - normalized links.
41
+ * @returns links with virtual-node hops replaced by their real origins, and
42
+ * every link touching a virtual node otherwise removed.
43
+ */
44
+ export declare function resolveVirtualLinks(rawNodes: unknown, links: GraphLink[]): GraphLink[];
package/lib/graph.js CHANGED
@@ -46,3 +46,102 @@ export function normalizeLinks(raw) {
46
46
  }
47
47
  return links;
48
48
  }
49
+ /**
50
+ * Frontend-only nodes whose links are rewired or dropped before analysis and
51
+ * conversion (Issue #8). None of them exists on the server.
52
+ *
53
+ * - KJNodes `SetNode` / `GetNode`: "wireless" links paired by name. A GetNode's
54
+ * output is the value wired into the SetNode of the same name (and a
55
+ * SetNode's own passthrough output is its input).
56
+ * - rgthree `Mute / Bypass Relay` / `Repeater`: their `OPT_CONNECTION` links
57
+ * only propagate mute/bypass mode between nodes and carry no data. The mode
58
+ * they set is already saved on each target node, so the links are dropped.
59
+ */
60
+ export const SET_NODE = 'SetNode';
61
+ export const GET_NODE = 'GetNode';
62
+ const MODE_LINK_NODES = new Set(['Mute / Bypass Relay (rgthree)', 'Mute / Bypass Repeater (rgthree)']);
63
+ /** Whether a node type is one of the virtual nodes {@link resolveVirtualLinks} removes. */
64
+ export function isVirtualNodeType(type) {
65
+ return type === SET_NODE || type === GET_NODE || MODE_LINK_NODES.has(type);
66
+ }
67
+ function virtualNodeFacts(raw) {
68
+ if (!isObject(raw))
69
+ return undefined;
70
+ const id = typeof raw.id === 'number' ? raw.id : Number(raw.id);
71
+ if (!Number.isFinite(id))
72
+ return undefined;
73
+ const values = Array.isArray(raw.widgets_values) ? raw.widgets_values : [];
74
+ const name = typeof values[0] === 'string' && values[0].trim() !== '' ? values[0] : undefined;
75
+ const inputs = Array.isArray(raw.inputs) ? raw.inputs : [];
76
+ const wired = inputs.find((entry) => isObject(entry) && typeof entry.link === 'number');
77
+ return {
78
+ id,
79
+ type: typeof raw.type === 'string' ? raw.type : '',
80
+ order: typeof raw.order === 'number' ? raw.order : Number.MAX_SAFE_INTEGER,
81
+ name,
82
+ firstInputLink: wired?.link,
83
+ };
84
+ }
85
+ /**
86
+ * Rewire the virtual nodes out of a graph's links, keeping link ids stable so
87
+ * node `inputs[].link` references stay valid.
88
+ *
89
+ * A GetNode resolves to the SetNode of the same name with the greatest `order`
90
+ * below its own (the scope rule the frontend applies), falling back to any
91
+ * SetNode of that name. A GetNode without a matching, wired SetNode (e.g. an
92
+ * empty optional slot) resolves to nothing, so its consumer stays unconnected.
93
+ * @param rawNodes - the graph's `nodes` array as saved.
94
+ * @param links - normalized links.
95
+ * @returns links with virtual-node hops replaced by their real origins, and
96
+ * every link touching a virtual node otherwise removed.
97
+ */
98
+ export function resolveVirtualLinks(rawNodes, links) {
99
+ const facts = new Map();
100
+ for (const raw of Array.isArray(rawNodes) ? rawNodes : []) {
101
+ const node = virtualNodeFacts(raw);
102
+ if (node !== undefined && isVirtualNodeType(node.type))
103
+ facts.set(node.id, node);
104
+ }
105
+ if (facts.size === 0)
106
+ return links;
107
+ const linkById = new Map(links.map((link) => [link[0], link]));
108
+ const setters = new Map();
109
+ for (const node of facts.values()) {
110
+ if (node.type !== SET_NODE || node.name === undefined)
111
+ continue;
112
+ const list = setters.get(node.name) ?? [];
113
+ list.push(node);
114
+ setters.set(node.name, list);
115
+ }
116
+ const setterFor = (getter) => {
117
+ const candidates = getter.name === undefined ? [] : setters.get(getter.name) ?? [];
118
+ const before = candidates.filter((node) => node.order < getter.order);
119
+ const pool = before.length > 0 ? before : candidates;
120
+ return pool.reduce((best, node) => (best === undefined || node.order > best.order ? node : best), undefined);
121
+ };
122
+ /** The real [originId, originSlot] behind an origin, or undefined when it resolves to nothing. */
123
+ const realOrigin = (originId, originSlot, seen) => {
124
+ const node = facts.get(originId);
125
+ if (node === undefined)
126
+ return [originId, originSlot];
127
+ if (seen.has(originId))
128
+ return undefined;
129
+ seen.add(originId);
130
+ if (MODE_LINK_NODES.has(node.type))
131
+ return undefined;
132
+ const source = node.type === GET_NODE ? setterFor(node) : node;
133
+ const upstream = source?.firstInputLink === undefined ? undefined : linkById.get(source.firstInputLink);
134
+ return upstream === undefined ? undefined : realOrigin(upstream[1], upstream[2], seen);
135
+ };
136
+ const out = [];
137
+ for (const link of links) {
138
+ // Links INTO a virtual node only feed the resolution above.
139
+ if (facts.has(link[3]))
140
+ continue;
141
+ const origin = realOrigin(link[1], link[2], new Set());
142
+ if (origin === undefined)
143
+ continue;
144
+ out.push(origin[0] === link[1] && origin[1] === link[2] ? link : [link[0], origin[0], origin[1], link[3], link[4], link[5]]);
145
+ }
146
+ return out;
147
+ }
package/lib/skill.js CHANGED
@@ -6,10 +6,31 @@
6
6
  export const COMFYUI_SKILL = {
7
7
  name: 'dsh-comfyui-workflows',
8
8
  source: 'runtime',
9
- description: 'ComfyUI 工作流管理:区分“图工作流”(ComfyUI 端保存的画布,衍生主题)与“API 工作流”(可执行,运行主题),分析画布中的多个独立流程(连通分量),以及图→API 提取的规则与面板操作引导。也包含本机环境(用户 ComfyUI 目录)与 TTS-Audio-Suite 音色库的快速查询/快照刷新方案。处理 comfyui_workflow、需要定位用户 ComfyUI 文件/音色库,或用户要求运行 ComfyUI 端保存的工作流时加载。',
10
- whenToUse: '用户要求运行/管理 ComfyUI 中保存的工作流,或 comfyui_workflow list 显示未提取的图工作流时;需要判断一个画布是否包含多个独立流程、或解释为何某个工作流无法直接运行时;需要查询 TTS-Audio-Suite 音色库、刷新已存工作流的音色参数快照、或定位用户本机 ComfyUI 目录时;或 comfyui_workflow list 里某个工作流带“技能包”标记、需要在运行前读取它时。',
9
+ description: '用 ComfyUI 生成图片/视频/音频(文生图、图生图、文生视频、语音等)的入口:第一步用 comfyui_workflow list 挑库里的工作流再 run。也负责工作流管理:区分“图工作流”(ComfyUI 端保存的画布,衍生主题)与“API 工作流”(可执行,运行主题),分析画布中的多个独立流程(连通分量),以及图→API 提取的规则与面板操作引导。也包含本机环境(用户 ComfyUI 目录)与 TTS-Audio-Suite 音色库的快速查询/快照刷新方案。处理 comfyui_workflow、需要定位用户 ComfyUI 文件/音色库,或用户要求运行 ComfyUI 端保存的工作流时加载。',
10
+ whenToUse: '用户要用 ComfyUI 生成任何内容时(先 list 再 run);用户要求运行/管理 ComfyUI 中保存的工作流,或 comfyui_workflow list 显示未提取的图工作流时;需要判断一个画布是否包含多个独立流程、或解释为何某个工作流无法直接运行时;需要查询 TTS-Audio-Suite 音色库、刷新已存工作流的音色参数快照、或定位用户本机 ComfyUI 目录时;或 comfyui_workflow list 里某个工作流带“技能包”标记、需要在运行前读取它时。',
11
11
  content: `# dsh-comfyui 工作流管理
12
12
 
13
+ ## 生成内容:第一步永远是 comfyui_workflow list
14
+
15
+ 用户要生成任何内容(图片、视频、音频,文生图、图生图、TTS 等)时:
16
+
17
+ 1. **先调用 \`comfyui_workflow action: list\`**,按名称、描述、标签挑出合适的工作流。库里的工作流是用户为这台服务器配好的,模型和节点都可用。
18
+ 2. 有技能包的先 \`action: skill { id }\` 读一遍,然后 \`comfyui_workflow run { id, parameters }\`(细节见「运行工作流的正确流程」)。
19
+ 3. \`comfyui_object_info\` 和 \`comfyui_run\` **不是生成入口**,只用于手写工作流(见下一节)。库里有合适的工作流就不要用它们。
20
+ 4. 能直接调用工具就直接调用(对话里才会显示生成卡片);当前环境只能在 run_code 里调用时也可以,照返回值里的 \`report\` 字段做。
21
+
22
+ ## 手写工作流(次要:临时测试,或按本机环境新建工作流)
23
+
24
+ 库里没有合适的工作流、需要临时测试,或要为本机环境新建一个工作流时才用,步骤固定:
25
+
26
+ 1. **查环境**:\`comfyui_object_info { filter }\` 查节点类名、输入项和加载器里实际可选的模型文件名(如 \`filter: "Loader"\`)。
27
+ 2. **手写 API JSON**:节点 id → \`{ class_type, inputs }\`,连线写成 \`["节点id字符串", 输出槽位]\`,模型名只用第 1 步查到的。
28
+ 3. **运行测试**:\`comfyui_run { workflow }\`,报错就按错误信息改 JSON 再跑。
29
+
30
+ 测通后如果要长期用,告诉用户在面板里「新建工作流」粘贴这份 JSON 存进库,之后走 list → run。
31
+
32
+ ## 两个工作流主题
33
+
13
34
  本插件把 ComfyUI 工作流分成两个主题:
14
35
 
15
36
  - **图工作流(衍生主题)**:用户在 ComfyUI 画布上保存的 UI 图(nodes/links/widgets),是"源"。不能直接运行。一个图可能包含**多个互相独立的流程**。
@@ -66,6 +87,9 @@ TTS-Audio-Suite(\`{comfyuiDir}/custom_nodes/tts_audio_suite\`)的"🎭 Chara
66
87
  - **\`control_after_generate\` 占位**:名为 \`seed\`/\`noise_seed\` 的 INT(或 object_info 显式声明 \`control_after_generate\`)前面端会渲染生成后控制下拉,其值占据 widgets_values 一个槽位但无 API 输入——提取时消费该值但不写入工作流,否则后续 widget 全部错位(如 TTS-Audio-Suite 的 seed 后跟 "fixed")。
67
88
  - **对象型 widgets_values**(如 VHS_VideoCombine)按名取值,跳过内部状态(\`videopreview\` 等带 \`hidden\` 的对象)。
68
89
  - **Reroute 与 bypass(mode 4)节点直通**:输出跟随其第一条有连线的输入。
90
+ - **虚拟节点先改线再转换**(分析与转换共用同一规则):KJNodes \`SetNode\`/\`GetNode\` 按名称配对——\`GetNode\` 的输出接到同名 \`SetNode\` 的上游(同名多个时取 \`order\` 小于它的最大者),找不到对应 \`SetNode\` 的消费端保持未连接;rgthree \`Mute / Bypass Relay\`/\`Repeater\` 的连线只传播静音/绕过状态、不传数据,直接丢弃(状态已保存在各节点的 \`mode\` 里)。
91
+ - **DynamicCombo V3 保持扁平**:主输入的值是所选 option key 字符串,子控件以 \`主名.子名\` 平铺(如 \`codec: "auto"\`),不要包成 \`{ key, inputs }\`——服务端会自己重组,包起来反而匹配不上,节点在 execute 时报缺参数。
92
+ - **子图(subgraph)实例不支持**:遇到请让用户在 ComfyUI 里把子图转换回普通节点后再提取。
69
93
  - **未注册的 UI-only 节点**:若是 \`Primitive*\` 内联其第一个 widget 值;有输出被使用且非 Primitive → 报错。
70
94
  - **输出槽位越界**(保存图里 SaveImage 声称 2 个输出但服务端只有 1 个)→ 断开该引用并给警告。
71
95
  - **必需输入缺失**(required 里没有、且非 lazy/template 类型)→ 明确报错(源图本身断线),不产出"能转但跑不起来"的工作流。\`COMFY_AUTOGROW_V3\`/带 \`template\`/带 \`lazy\` 的输入豁免。
@@ -139,7 +163,10 @@ TTS-Audio-Suite(\`{comfyuiDir}/custom_nodes/tts_audio_suite\`)的"🎭 Chara
139
163
  2. **有技能包就先读**:list 里某个工作流带 \`技能包:\` 那一行,说明用户为它写了专属说明书(适用场景、参数怎么填、哪些环节会翻车)。选中它之后、运行之前,先 \`comfyui_workflow action: skill { id }\` 把正文读进来并照着做。标注「运行前必读」的,不读会被 run 直接拒绝。没有这行的工作流跳过本步。
140
164
  3. **参数决策**:只依据清单里的参数(键名用英文名),选择/覆盖适合的值——**不需要查看工作流内部结构**。
141
165
  4. **run 直接调用**:\`comfyui_workflow run { id, parameters: {...} }\`。插件直接从库里取该工作流的 JSON 提交给 ComfyUI,你**只传参数覆盖值,绝不自己输出或复制完整工作流 JSON**(那会浪费大量 token)。
142
- - **视频/音频工作流一律 \`mode: "async"\`**:生成要几分钟,sync 会等待超时中断(报 generation aborted)。async 立即返回 job id,**启动后不要阻塞等待**(不要对返回的 job 用 wait: true 卡住),继续做其他事,后台任务完成时系统会通知你,到时再用 job_output(不 wait)收集结果和媒体回显。
166
+ - **参数**:键名与 list 完全一致(写错会被拒绝);未传的参数使用默认值。提示词类参数按用户需求填写,未明确时据需求自行发挥。
167
+ - **视频/音频工作流一律 \`mode: "async"\`**:生成要几分钟,sync 会等待超时中断(报 generation aborted)。async 立即返回 job id,**启动后不要阻塞等待**(不要对返回的 job 用 wait: true 卡住,也不要 sleep/轮询):告诉用户正在生成,**直接结束本轮回复**;有其他独立的事可以先做完再结束。后台任务完成时系统会发通知唤醒你,到时调用一次 job_output(不 wait)收集结果。
168
+ - **结果就是最终结果,地址已经带好**:不要再查状态,也不要去查资产、history 或拼接 URL。
169
+ - **生成完毕后的统一做法**(sync、async 的 job_output、run_code 都一样):用一句话告诉用户生成了什么(依据结果里的「Prompt used」),然后把结果给出的 Markdown 行**原样贴进回复**(图片是 \`![image 1](url)\`,视频/音频是链接)——这是用户看到结果的地方。
143
170
  - 图片生成可 sync(通常 <1 分钟);若不确定输出类型,也用 async。
144
171
  - \`action: get\` 只在**诊断/检查**工作流内容时使用(它会输出完整 JSON、占用大量 token),它**不是运行路径**。
145
172
 
package/lib/tools.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Model-facing tools for dsh-comfyui, registered into the host `tools`
3
- * registry. `comfyui_run` submits a workflow and returns media results
4
- * (synchronously or as a background job); `comfyui_object_info` exposes the
5
- * server's node definitions; `comfyui_workflow` lists and runs saved
6
- * workflows from the panel-managed workflow library.
3
+ * registry. The main entry is `comfyui_workflow`: list and run saved
4
+ * workflows from the panel-managed library (synchronously or as a background
5
+ * job); `comfyui_skill` maintains their skill packs. Secondary, for
6
+ * hand-written workflows only (temporary tests, building a workflow for this
7
+ * environment): `comfyui_object_info` exposes the server's node definitions
8
+ * and `comfyui_run` submits a hand-written API workflow.
7
9
  */
8
10
  import type { Context } from '@deepseek-ai/cordis';
9
11
  import type { Config } from './config.js';
@@ -155,6 +157,17 @@ export interface RunResult {
155
157
  elapsedMs: number;
156
158
  media: RunMediaItem[];
157
159
  summary: string;
160
+ /** Saved-workflow runs: the prompt-type parameters as actually submitted. */
161
+ prompts?: PromptUsed[];
162
+ /** The model-facing text (`runResultText`), carried in the value for code-dispatched calls. */
163
+ report?: string;
164
+ }
165
+ /** One prompt-type parameter of a saved-workflow run, as actually submitted. */
166
+ export interface PromptUsed {
167
+ name: string;
168
+ text: string;
169
+ /** false = the caller did not pass it, so the saved default was used. */
170
+ passed: boolean;
158
171
  }
159
172
  /** Background mode result: collect later with job_output. */
160
173
  export interface BackgroundResult {
@@ -162,6 +175,10 @@ export interface BackgroundResult {
162
175
  jobId: string;
163
176
  promptId: string;
164
177
  label: string;
178
+ /** The host posts a completion notice (and wakes an idle agent) — dsh 0.1.7+ jobs. */
179
+ notifies: boolean;
180
+ /** The model-facing receipt (`backgroundText`), carried in the value for code-dispatched calls. */
181
+ report?: string;
165
182
  }
166
183
  /** Register the plugin tools; returns disposers. */
167
184
  export declare function registerComfyUITools(ctx: Context, runtime: ComfyUIRuntime): Array<() => void>;