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/README.en.md +4 -6
- package/README.md +4 -6
- package/client/client.js +329 -174
- package/client/client.js.map +1 -1
- package/lib/analyze.js +8 -4
- package/lib/comfyui.d.ts +14 -3
- package/lib/comfyui.js +31 -10
- package/lib/convert.js +5 -3
- package/lib/graph.d.ts +29 -0
- package/lib/graph.js +99 -0
- package/lib/skill.js +30 -3
- package/lib/tools.d.ts +21 -4
- package/lib/tools.js +260 -153
- package/package.json +7 -5
- package/lib/templates.d.ts +0 -36
- package/lib/templates.js +0 -84
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
|
-
|
|
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.
|
|
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
|
-
*
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
*
|
|
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.
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
10
|
-
whenToUse: '
|
|
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
|
-
-
|
|
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 行**原样贴进回复**(图片是 \`\`,视频/音频是链接)——这是用户看到结果的地方。
|
|
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.
|
|
4
|
-
* (synchronously or as a background
|
|
5
|
-
*
|
|
6
|
-
* workflows
|
|
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>;
|