dsh-comfyui 0.5.4 → 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/comfyui.d.ts +14 -3
- package/lib/comfyui.js +31 -10
- package/lib/skill.js +27 -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/tools.js
CHANGED
|
@@ -1,5 +1,4 @@
|
|
|
1
1
|
import { collectMedia } from './comfyui.js';
|
|
2
|
-
import { TEMPLATES, findTemplate, cloneWorkflow, applyTemplateInputs } from './templates.js';
|
|
3
2
|
import { refreshParameterMetadata } from './params.js';
|
|
4
3
|
import { SKILL_MAIN, joinFrontmatter } from './skillpack.js';
|
|
5
4
|
/**
|
|
@@ -29,47 +28,136 @@ function hasReadSkillPack(agent, workflowId) {
|
|
|
29
28
|
return true;
|
|
30
29
|
return skillPackReads.get(agent)?.has(workflowId) === true;
|
|
31
30
|
}
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
31
|
+
function sessionJobs(jobs) {
|
|
32
|
+
return typeof jobs.readAt === 'function';
|
|
33
|
+
}
|
|
34
|
+
/** The job owner for a tool call, in the shape this host's registry resolves. */
|
|
35
|
+
function jobOwner(jobs, agent) {
|
|
36
|
+
if (agent === undefined)
|
|
37
|
+
return {};
|
|
38
|
+
if (!sessionJobs(jobs))
|
|
39
|
+
return { owner: agent };
|
|
40
|
+
const id = typeof agent === 'object' && agent !== null ? agent.id : undefined;
|
|
41
|
+
return typeof id === 'string' && id !== '' ? { owner: id } : {};
|
|
42
|
+
}
|
|
43
|
+
/** A completed job: `text` is what the model's next job_output receives. */
|
|
44
|
+
function jobCompleted(jobs, text) {
|
|
45
|
+
return sessionJobs(jobs) ? { status: 'completed', result: text } : { status: 'completed', output: text };
|
|
46
|
+
}
|
|
47
|
+
/**
|
|
48
|
+
* A failed job. 0.1.7+ shows `detail` as the status-line reason, so the
|
|
49
|
+
* message goes there; older hosts hand `output` to job_output instead.
|
|
50
|
+
*/
|
|
51
|
+
function jobFailed(jobs, message) {
|
|
52
|
+
return sessionJobs(jobs)
|
|
53
|
+
? { status: 'failed', detail: `comfyui: ${message}` }
|
|
54
|
+
: { status: 'failed', detail: 'comfyui', output: message };
|
|
35
55
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
if (
|
|
39
|
-
return
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
56
|
+
/** The job status line for one prompt: sampler progress once the WebSocket reports any. */
|
|
57
|
+
function progressLine(progress) {
|
|
58
|
+
if (progress === undefined || progress.max <= 0)
|
|
59
|
+
return 'waiting for ComfyUI';
|
|
60
|
+
const node = progress.node !== null ? ` (node ${progress.node})` : '';
|
|
61
|
+
return `step ${progress.value}/${progress.max}${node}`;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Run one queued prompt as a background job — the shared async path of
|
|
65
|
+
* `comfyui_run` and `comfyui_workflow run`.
|
|
66
|
+
* - Cancel (job_kill) aborts the wait, which cancels THIS prompt only
|
|
67
|
+
* (`cancelPrompt`: dequeue while pending, interrupt while running) and
|
|
68
|
+
* settles the job as killed. A bare `interrupt()` here used to stop
|
|
69
|
+
* whatever ComfyUI was running, i.e. someone else's job when this one was
|
|
70
|
+
* still queued.
|
|
71
|
+
* - On 0.1.7+ hosts the status line carries live sampler progress (from the
|
|
72
|
+
* ProgressTracker, pulled once per history poll — no extra timer), so
|
|
73
|
+
* job_list shows the job moving instead of inviting a status poll.
|
|
74
|
+
*/
|
|
75
|
+
function startPromptJob(jobs, runtime, client, opts) {
|
|
76
|
+
const { promptId, label, subject, waitMs } = opts;
|
|
77
|
+
const config = runtime.getConfig();
|
|
78
|
+
return jobs.start({
|
|
79
|
+
kind: 'comfyui',
|
|
80
|
+
label,
|
|
81
|
+
...jobOwner(jobs, opts.agent),
|
|
82
|
+
run: (job) => {
|
|
83
|
+
const startedAt = Date.now();
|
|
84
|
+
const controller = new AbortController();
|
|
85
|
+
let lastLine;
|
|
86
|
+
const publishProgress = () => {
|
|
87
|
+
if (job?.updateProgress === undefined)
|
|
88
|
+
return;
|
|
89
|
+
const line = progressLine(runtime.queueProgress(promptId));
|
|
90
|
+
if (line === lastLine)
|
|
91
|
+
return;
|
|
92
|
+
lastLine = line;
|
|
93
|
+
job.updateProgress(line);
|
|
94
|
+
};
|
|
95
|
+
const done = (async () => {
|
|
96
|
+
try {
|
|
97
|
+
const entry = await client.waitForCompletion({
|
|
98
|
+
promptId,
|
|
99
|
+
timeoutMs: waitMs,
|
|
100
|
+
pollIntervalMs: config.pollIntervalMs,
|
|
101
|
+
signal: controller.signal,
|
|
102
|
+
onPoll: publishProgress,
|
|
103
|
+
});
|
|
104
|
+
const items = collectMedia({ promptId, entry, maxItems: config.maxMediaItems, proxyBase: runtime.proxyBase() });
|
|
105
|
+
const result = {
|
|
106
|
+
kind: 'sync',
|
|
107
|
+
promptId,
|
|
108
|
+
status: 'completed',
|
|
109
|
+
elapsedMs: Date.now() - startedAt,
|
|
110
|
+
media: items,
|
|
111
|
+
summary: summarizeMedia(items),
|
|
112
|
+
...(opts.prompts !== undefined && opts.prompts.length > 0 ? { prompts: opts.prompts } : {}),
|
|
113
|
+
};
|
|
114
|
+
return jobCompleted(jobs, runResultText(subject, result));
|
|
115
|
+
}
|
|
116
|
+
catch (error) {
|
|
117
|
+
runtime.untrack(promptId);
|
|
118
|
+
if (controller.signal.aborted)
|
|
119
|
+
return { status: 'killed', detail: 'comfyui: cancelled' };
|
|
120
|
+
const message = error instanceof Error ? error.message : String(error);
|
|
121
|
+
return jobFailed(jobs, message);
|
|
122
|
+
}
|
|
123
|
+
})();
|
|
124
|
+
return {
|
|
125
|
+
cancel: () => controller.abort(),
|
|
126
|
+
done,
|
|
127
|
+
};
|
|
128
|
+
},
|
|
129
|
+
});
|
|
43
130
|
}
|
|
131
|
+
const TOOL_TIMEOUT_MS = 3_600_000;
|
|
132
|
+
/**
|
|
133
|
+
* The hand-written API workflow `comfyui_run` submits, with optional per-node
|
|
134
|
+
* input overrides merged in. The built-in templates are gone: they named
|
|
135
|
+
* stock checkpoints most servers do not have, and "文生图" matched `txt2img`
|
|
136
|
+
* before the agent ever looked at the library (real session, 0.5.5 round) —
|
|
137
|
+
* a leftover `template` argument gets a pointer to the right entry point.
|
|
138
|
+
*/
|
|
44
139
|
function buildWorkflow(args) {
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
const found = findTemplate(template);
|
|
48
|
-
if (found === undefined) {
|
|
49
|
-
throw new Error(`comfyui_run: unknown template "${template}" — use one of ${TEMPLATES.map((t) => t.id).join(', ')}`);
|
|
50
|
-
}
|
|
51
|
-
const workflow = cloneWorkflow(found.workflow);
|
|
52
|
-
const inputs = args.inputs;
|
|
53
|
-
if (inputs !== undefined) {
|
|
54
|
-
if (typeof inputs !== 'object' || inputs === null) {
|
|
55
|
-
throw new Error('comfyui_run: inputs must be an object keyed by node id');
|
|
56
|
-
}
|
|
57
|
-
applyTemplateInputs(workflow, inputs);
|
|
58
|
-
}
|
|
59
|
-
return { workflow, label: `comfyui ${template}` };
|
|
140
|
+
if (args.template !== undefined) {
|
|
141
|
+
throw new Error('comfyui_run: built-in templates were removed — to generate, use comfyui_workflow (action: list, then run); comfyui_run only takes a hand-written `workflow`');
|
|
60
142
|
}
|
|
61
143
|
const workflow = args.workflow;
|
|
62
|
-
if (typeof workflow !== 'object' || workflow === null) {
|
|
63
|
-
throw new Error('comfyui_run: workflow
|
|
144
|
+
if (typeof workflow !== 'object' || workflow === null || Array.isArray(workflow)) {
|
|
145
|
+
throw new Error('comfyui_run: `workflow` (a hand-written API-format object: node id → { class_type, inputs }) is required');
|
|
64
146
|
}
|
|
147
|
+
const nodes = workflow;
|
|
65
148
|
const inputs = args.inputs;
|
|
66
149
|
if (inputs !== undefined) {
|
|
67
150
|
if (typeof inputs !== 'object' || inputs === null) {
|
|
68
151
|
throw new Error('comfyui_run: inputs must be an object keyed by node id');
|
|
69
152
|
}
|
|
70
|
-
|
|
153
|
+
for (const [nodeId, partial] of Object.entries(inputs)) {
|
|
154
|
+
const node = nodes[nodeId];
|
|
155
|
+
if (node === undefined || typeof partial !== 'object' || partial === null)
|
|
156
|
+
continue;
|
|
157
|
+
node.inputs = { ...node.inputs, ...partial };
|
|
158
|
+
}
|
|
71
159
|
}
|
|
72
|
-
return { workflow:
|
|
160
|
+
return { workflow: nodes, label: 'comfyui custom workflow' };
|
|
73
161
|
}
|
|
74
162
|
function summarizeMedia(media) {
|
|
75
163
|
if (media.length === 0)
|
|
@@ -89,29 +177,106 @@ function summarizeMedia(media) {
|
|
|
89
177
|
parts.push(`${others} other file(s)`);
|
|
90
178
|
return parts.join(', ');
|
|
91
179
|
}
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
180
|
+
/** One ready-to-paste Markdown reference: images embed, other media link. */
|
|
181
|
+
function mediaMarkdown(item, n) {
|
|
182
|
+
return item.kind === 'image' ? `` : `[${item.kind} ${n}: ${item.filename}](${item.url})`;
|
|
183
|
+
}
|
|
184
|
+
/**
|
|
185
|
+
* The prompt-type parameters of a saved-workflow run as they will be
|
|
186
|
+
* submitted: auto-detected prompts (`prompt`, `prompt_2`, …, label 提示词)
|
|
187
|
+
* and any string parameter whose name or label says prompt. `passed: false`
|
|
188
|
+
* marks a value the caller left to the saved default.
|
|
189
|
+
*/
|
|
190
|
+
/**
|
|
191
|
+
* A parameter's prompt role: 'write' = the positive prompt the model must
|
|
192
|
+
* compose from the user's request every run; 'negative' = a negative prompt
|
|
193
|
+
* (its default is normally fine); undefined = not a prompt. Prompts are the
|
|
194
|
+
* auto-detected `prompt`, `prompt_2`, … (label 提示词) or any string parameter
|
|
195
|
+
* whose name or label says prompt.
|
|
196
|
+
*/
|
|
197
|
+
function promptRole(param) {
|
|
198
|
+
if (param.type !== 'string' || param.upload !== undefined)
|
|
199
|
+
return undefined;
|
|
200
|
+
const label = param.label ?? '';
|
|
201
|
+
if (!/prompt/i.test(param.name) && !/提示/.test(label))
|
|
202
|
+
return undefined;
|
|
203
|
+
return /negative|neg_|负面|反向/i.test(`${param.name} ${label}`) ? 'negative' : 'write';
|
|
204
|
+
}
|
|
205
|
+
function promptsUsed(parameters, values) {
|
|
206
|
+
const used = [];
|
|
207
|
+
for (const param of parameters) {
|
|
208
|
+
if (promptRole(param) === undefined)
|
|
209
|
+
continue;
|
|
210
|
+
const passed = Object.prototype.hasOwnProperty.call(values, param.name);
|
|
211
|
+
const value = passed ? values[param.name] : param.default;
|
|
212
|
+
if (typeof value !== 'string' || value === '')
|
|
213
|
+
continue;
|
|
214
|
+
used.push({ name: param.name, text: value, passed });
|
|
215
|
+
}
|
|
216
|
+
return used;
|
|
217
|
+
}
|
|
218
|
+
/**
|
|
219
|
+
* The model-facing account of a finished run — the sync result, the async
|
|
220
|
+
* job_output text and the `report` a run_code caller reads alike. It says the
|
|
221
|
+
* result is final (models re-check status and re-query assets for URLs they
|
|
222
|
+
* already hold), names the prompt actually submitted, and hands over the media
|
|
223
|
+
* as Markdown lines to paste: the model's reply is the one place every host
|
|
224
|
+
* mode shows (dsh 0.2 collapses finished tool rows, and run_code / PTC mode
|
|
225
|
+
* renders no tool card at all), so it is the display path, always. Headless
|
|
226
|
+
* hosts (no web server) only have bare filenames, which stay a plain list.
|
|
227
|
+
*/
|
|
228
|
+
function runResultText(subject, result) {
|
|
229
|
+
const final = result.status === 'completed' ? ' This is the final result — there is no status to check and no address to look up.' : '';
|
|
230
|
+
const lines = [`${subject} ${result.status} (prompt ${result.promptId}) in ${result.elapsedMs} ms — ${summarizeMedia(result.media)}.${final}`];
|
|
231
|
+
// The prompt as submitted — so the report names the real subject, and a run
|
|
232
|
+
// that silently used the saved default (no parameter passed) is visible.
|
|
233
|
+
for (const used of result.prompts ?? []) {
|
|
234
|
+
const text = used.text.length > 300 ? `${used.text.slice(0, 300)}…` : used.text;
|
|
235
|
+
lines.push(used.passed
|
|
236
|
+
? `Prompt used (${used.name}): "${text}"`
|
|
237
|
+
: `Prompt used (${used.name}, default — not passed): "${text}"`);
|
|
238
|
+
}
|
|
239
|
+
const linkable = result.media.filter((item) => item.url !== item.filename);
|
|
240
|
+
const theme = 'In your reply, state in one sentence what was generated (its subject, per the prompt used).';
|
|
241
|
+
if (linkable.length > 0) {
|
|
242
|
+
lines.push(`${theme} Show the results by pasting these Markdown lines into your reply as-is. The URLs are final — do not look them up again:`);
|
|
243
|
+
linkable.forEach((item, i) => lines.push(mediaMarkdown(item, i + 1)));
|
|
244
|
+
}
|
|
245
|
+
else if (result.status === 'completed') {
|
|
246
|
+
lines.push(theme);
|
|
99
247
|
}
|
|
100
|
-
const lines = [
|
|
101
|
-
`ComfyUI ${result.status} (prompt ${result.promptId}) in ${result.elapsedMs} ms — ${summarizeMedia(result.media)}`,
|
|
102
|
-
];
|
|
103
248
|
for (const item of result.media) {
|
|
104
|
-
|
|
249
|
+
if (item.url === item.filename)
|
|
250
|
+
lines.push(` ${item.kind}: ${item.filename}`);
|
|
105
251
|
}
|
|
106
|
-
return
|
|
252
|
+
return lines.join('\n');
|
|
253
|
+
}
|
|
254
|
+
/**
|
|
255
|
+
* The model-facing receipt for a started background job. Models tend to block
|
|
256
|
+
* on it (job_output wait, sleeps) or poll; where the host posts a completion
|
|
257
|
+
* notice, the right move is to end the turn — the notice wakes the agent.
|
|
258
|
+
*/
|
|
259
|
+
function backgroundText(subject, result) {
|
|
260
|
+
const head = `${subject} started in the background (job ${result.jobId}, prompt ${result.promptId}).`;
|
|
261
|
+
const collect = 'call job_output once (no wait) — its result already contains ready-to-paste Markdown for the media.';
|
|
262
|
+
return result.notifies
|
|
263
|
+
? `${head} Do not wait, poll or call job_output now: tell the user it is generating and end your turn. You will get a completion notice in this session when it finishes; then ${collect}`
|
|
264
|
+
: `${head} Do not block on it: continue with other work or end your turn. To collect it later, ${collect}`;
|
|
265
|
+
}
|
|
266
|
+
function renderRunResult(_args, value) {
|
|
267
|
+
const result = value;
|
|
268
|
+
if (result.kind === 'background')
|
|
269
|
+
return [{ type: 'text', text: backgroundText('ComfyUI generation', result) }];
|
|
270
|
+
return [{ type: 'text', text: runResultText('ComfyUI', result) }];
|
|
107
271
|
}
|
|
108
272
|
/**
|
|
109
273
|
* Wait for a queued prompt and collect its media. Untracks the prompt when
|
|
110
274
|
* the wait fails; an interrupted wait reports an interrupted result instead
|
|
111
275
|
* of throwing.
|
|
112
276
|
*/
|
|
113
|
-
async function waitSync(runtime, client, promptId, config, signal, timeoutMs) {
|
|
277
|
+
async function waitSync(runtime, client, promptId, config, signal, timeoutMs, context) {
|
|
114
278
|
const startedAt = Date.now();
|
|
279
|
+
const prompts = context.prompts !== undefined && context.prompts.length > 0 ? { prompts: context.prompts } : {};
|
|
115
280
|
try {
|
|
116
281
|
const entry = await client.waitForCompletion({
|
|
117
282
|
promptId,
|
|
@@ -120,13 +285,15 @@ async function waitSync(runtime, client, promptId, config, signal, timeoutMs) {
|
|
|
120
285
|
signal,
|
|
121
286
|
});
|
|
122
287
|
const items = collectMedia({ promptId, entry, maxItems: config.maxMediaItems, proxyBase: runtime.proxyBase() });
|
|
288
|
+
const elapsedMs = Date.now() - startedAt;
|
|
123
289
|
return {
|
|
124
290
|
kind: 'sync',
|
|
125
291
|
promptId,
|
|
126
292
|
status: 'completed',
|
|
127
|
-
elapsedMs
|
|
293
|
+
elapsedMs,
|
|
128
294
|
media: items,
|
|
129
295
|
summary: summarizeMedia(items),
|
|
296
|
+
...prompts,
|
|
130
297
|
};
|
|
131
298
|
}
|
|
132
299
|
catch (error) {
|
|
@@ -148,23 +315,20 @@ function runDefinition(runtime, ctx) {
|
|
|
148
315
|
return {
|
|
149
316
|
name: 'comfyui_run',
|
|
150
317
|
description: [
|
|
151
|
-
'
|
|
152
|
-
'
|
|
153
|
-
'
|
|
154
|
-
'
|
|
155
|
-
'Inspect available node types with comfyui_object_info before hand-writing a workflow.',
|
|
156
|
-
'`mode: sync` (default) waits and returns media URLs; `mode: async` starts a background job and returns a job id for job_output.',
|
|
318
|
+
'SECONDARY tool — not the way to generate. To generate, use comfyui_workflow (action: list, then run a saved workflow).',
|
|
319
|
+
'comfyui_run only submits a HAND-WRITTEN ComfyUI API workflow (node id → { class_type, inputs }), for a temporary test or for building a new workflow for this environment: first check the server with comfyui_object_info (node classes, inputs, available model names), then write the JSON, then run it here.',
|
|
320
|
+
'`inputs` overrides node inputs by id, e.g. {"3": {"seed": 42}}.',
|
|
321
|
+
'`mode: sync` (default) waits and returns media URLs; `mode: async` starts a background job and returns a job id at once — then end your turn instead of waiting; a completion notice brings you back, and job_output returns the media as ready-to-paste Markdown. Results are final and carry the media URLs — never re-check status or look addresses up; paste the Markdown lines the result gives into your reply to show the media.',
|
|
157
322
|
].join(' '),
|
|
158
323
|
parameters: {
|
|
159
324
|
type: 'object',
|
|
160
325
|
properties: {
|
|
161
|
-
workflow: { type: 'object', description: 'ComfyUI API-format workflow: node id → { class_type, inputs }
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
mode: { type: 'string', enum: ['sync', 'async'], default: 'sync', description: 'sync waits and returns media; async returns a background job id.' },
|
|
326
|
+
workflow: { type: 'object', description: 'Hand-written ComfyUI API-format workflow: node id → { class_type, inputs }; node references are ["<node id string>", <output slot>].' },
|
|
327
|
+
inputs: { type: 'object', description: 'Optional per-node input overrides keyed by node id, e.g. {"3": {"seed": 42, "steps": 30}}.' },
|
|
328
|
+
mode: { type: 'string', enum: ['sync', 'async'], default: 'sync', description: 'sync waits and returns media; async returns a background job id immediately (end your turn after it).' },
|
|
165
329
|
timeout_ms: { type: 'number', minimum: 5_000, maximum: 3_600_000, description: 'Generation wait budget in ms (default 180000). Video needs minutes.' },
|
|
166
330
|
},
|
|
167
|
-
required: [],
|
|
331
|
+
required: ['workflow'],
|
|
168
332
|
},
|
|
169
333
|
output: {
|
|
170
334
|
schema: { type: 'object' },
|
|
@@ -186,9 +350,6 @@ function runDefinition(runtime, ctx) {
|
|
|
186
350
|
},
|
|
187
351
|
timeoutMs: TOOL_TIMEOUT_MS,
|
|
188
352
|
async execute(args, exec) {
|
|
189
|
-
const problem = requireOneOf(args, ['workflow', 'template']);
|
|
190
|
-
if (problem !== undefined)
|
|
191
|
-
throw new Error(`comfyui_run: ${problem}`);
|
|
192
353
|
const mode = args.mode === undefined ? 'sync' : args.mode;
|
|
193
354
|
if (mode !== 'sync' && mode !== 'async')
|
|
194
355
|
throw new Error(`comfyui_run: mode must be sync or async, got ${String(mode)}`);
|
|
@@ -203,48 +364,12 @@ function runDefinition(runtime, ctx) {
|
|
|
203
364
|
if (jobs === undefined) {
|
|
204
365
|
throw new Error('comfyui_run: background jobs unavailable — load @deepseek-ai/dsh-jobs-local and @deepseek-ai/dsh-tool-jobs');
|
|
205
366
|
}
|
|
206
|
-
const jobId = jobs.
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
...(exec.agent !== undefined ? { owner: exec.agent } : {}),
|
|
210
|
-
run: () => {
|
|
211
|
-
const startedAt = Date.now();
|
|
212
|
-
const done = (async () => {
|
|
213
|
-
try {
|
|
214
|
-
const entry = await client.waitForCompletion({
|
|
215
|
-
promptId,
|
|
216
|
-
timeoutMs: waitMs,
|
|
217
|
-
pollIntervalMs: config.pollIntervalMs,
|
|
218
|
-
signal: new AbortController().signal,
|
|
219
|
-
});
|
|
220
|
-
const items = collectMedia({ promptId, entry, maxItems: config.maxMediaItems, proxyBase: runtime.proxyBase() });
|
|
221
|
-
const result = {
|
|
222
|
-
kind: 'sync',
|
|
223
|
-
promptId,
|
|
224
|
-
status: 'completed',
|
|
225
|
-
elapsedMs: Date.now() - startedAt,
|
|
226
|
-
media: items,
|
|
227
|
-
summary: summarizeMedia(items),
|
|
228
|
-
};
|
|
229
|
-
return { status: 'completed', output: JSON.stringify(result) };
|
|
230
|
-
}
|
|
231
|
-
catch (error) {
|
|
232
|
-
runtime.untrack(promptId);
|
|
233
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
234
|
-
return { status: 'failed', detail: 'comfyui', output: message };
|
|
235
|
-
}
|
|
236
|
-
})();
|
|
237
|
-
return {
|
|
238
|
-
cancel: () => { void client.interrupt().catch(() => undefined); },
|
|
239
|
-
done,
|
|
240
|
-
};
|
|
241
|
-
},
|
|
242
|
-
});
|
|
243
|
-
const result = { kind: 'background', jobId, promptId, label };
|
|
244
|
-
return result;
|
|
367
|
+
const jobId = startPromptJob(jobs, runtime, client, { promptId, label, subject: 'ComfyUI', waitMs, agent: exec.agent });
|
|
368
|
+
const result = { kind: 'background', jobId, promptId, label, notifies: sessionJobs(jobs) };
|
|
369
|
+
return { ...result, report: backgroundText('ComfyUI generation', result) };
|
|
245
370
|
}
|
|
246
|
-
const result = await waitSync(runtime, client, promptId, config, exec.signal, waitMs);
|
|
247
|
-
return result;
|
|
371
|
+
const result = await waitSync(runtime, client, promptId, config, exec.signal, waitMs, {});
|
|
372
|
+
return { ...result, report: runResultText('ComfyUI', result) };
|
|
248
373
|
},
|
|
249
374
|
};
|
|
250
375
|
}
|
|
@@ -275,7 +400,7 @@ function summarizeFields(fields, required) {
|
|
|
275
400
|
function objectInfoDefinition(runtime) {
|
|
276
401
|
return {
|
|
277
402
|
name: 'comfyui_object_info',
|
|
278
|
-
description: '
|
|
403
|
+
description: 'SECONDARY tool, for hand-writing workflows only (with comfyui_run): lists the node definitions this ComfyUI server supports — class types, required/optional inputs, and the model files each loader offers — so a temporary test or a new workflow for this environment uses real names. Not needed to generate with saved workflows (comfyui_workflow). Optional `filter` narrows by class-name substring, e.g. "KSampler", "Loader".',
|
|
279
404
|
parameters: {
|
|
280
405
|
type: 'object',
|
|
281
406
|
properties: {
|
|
@@ -334,10 +459,10 @@ function workflowDefinition(runtime, ctx) {
|
|
|
334
459
|
return {
|
|
335
460
|
name: 'comfyui_workflow',
|
|
336
461
|
description: [
|
|
337
|
-
'
|
|
462
|
+
'The way to generate images, video or audio with ComfyUI: list the saved workflows (set up for this server), pick the one that fits, run it. Lists and runs workflows from the plugin workflow library (运行主题: API-format workflows extracted from a graph or pasted directly).',
|
|
338
463
|
'`action: list` returns every runnable workflow with its id, name, description (what the workflow does), and input notes.',
|
|
339
464
|
'It also lists workflows the user saved on the ComfyUI server (衍生主题: UI graph format canvases). A graph may hold SEVERAL independent flows; each is extracted into its own runnable workflow in the panel (整体/按分量/主流程). A graph with no extracted workflow yet cannot run — tell the user to open the ComfyUI panel and 提取 it first.',
|
|
340
|
-
'`action: run` runs one saved workflow by id — pass only the id plus parameter overrides; the plugin submits the saved workflow JSON itself (never copy the JSON into your reply). It waits for media by default; add `mode: "async"` to run in the background and
|
|
465
|
+
'`action: run` runs one saved workflow by id — pass only the id plus parameter overrides keyed by the exact names from list (unknown names are rejected; parameters not passed use their defaults). Fill prompt parameters from what the user asked for, improvising from the request when unspecified. Call it directly, not from run_code, so the user sees the result card. The plugin submits the saved workflow JSON itself (never copy the JSON into your reply). It waits for media by default; add `mode: "async"` to run in the background — then end your turn instead of waiting; a completion notice brings you back, and job_output returns the media as ready-to-paste Markdown. Results are final and carry the media URLs — never re-check status or look addresses up; paste the Markdown lines the result gives into your reply to show the media.',
|
|
341
466
|
'`action: get` returns one saved workflow\'s complete API-format JSON by id for inspection/diagnostics only — it consumes many tokens and is not the run path.',
|
|
342
467
|
'`action: refresh` re-derives one saved workflow\'s parameter snapshot (options / numberKind / min/max/step) from the current node definitions and saves it back. Run it after the TTS-Audio-Suite voice library or node definitions changed: saved workflows snapshot their parameter options at save time, so a freshly added voice is not accepted by `action: run` until the snapshot catches up. It force-rescans the TTS voice library first, then updates only the fields derived from object_info — the parameter set (including user-added advanced parameters) is preserved. Returns `changed` with the parameter names that actually changed.',
|
|
343
468
|
].join(' '),
|
|
@@ -346,7 +471,7 @@ function workflowDefinition(runtime, ctx) {
|
|
|
346
471
|
properties: {
|
|
347
472
|
action: { type: 'string', enum: ['list', 'run', 'get', 'refresh'], description: 'list returns the workflow library; run executes one workflow by id (direct call to the saved JSON); get returns one workflow\'s full JSON for inspection; refresh re-derives one workflow\'s parameter snapshot from the current node definitions and saves it back.' },
|
|
348
473
|
id: { type: 'string', description: 'Workflow id (required for action: run and get).' },
|
|
349
|
-
mode: { type: 'string', enum: ['sync', 'async'], description: 'run mode (default sync); async starts a background job and returns its id
|
|
474
|
+
mode: { type: 'string', enum: ['sync', 'async'], description: 'run mode (default sync); async starts a background job and returns its id immediately (end your turn after it; collect with job_output once notified). Video/audio workflows should use async — generation takes minutes and sync may time out.' },
|
|
350
475
|
timeout_ms: { type: 'number', minimum: 5_000, maximum: 3_600_000, description: 'Generation wait budget in ms (default 900000 = 15 min). Video needs minutes; raise this for long videos.' },
|
|
351
476
|
parameters: {
|
|
352
477
|
type: 'object',
|
|
@@ -389,7 +514,7 @@ function workflowDefinition(runtime, ctx) {
|
|
|
389
514
|
return [{ type: 'text', text: `ComfyUI workflow ${data.name ?? data.id}: ${JSON.stringify(data.workflow)}` }];
|
|
390
515
|
}
|
|
391
516
|
if (data.background !== undefined) {
|
|
392
|
-
return [{ type: 'text', text:
|
|
517
|
+
return [{ type: 'text', text: backgroundText('ComfyUI workflow', data.background) }];
|
|
393
518
|
}
|
|
394
519
|
if (data.action === 'refresh') {
|
|
395
520
|
const changed = data.changed ?? [];
|
|
@@ -415,14 +540,23 @@ function workflowDefinition(runtime, ctx) {
|
|
|
415
540
|
const extra = skill.files > 1 ? `,另有 ${skill.files - 1} 篇参考文档` : '';
|
|
416
541
|
lines.push(` 技能包${skill.required ? '(运行前必读)' : ''}: ${skill.summary}${extra} — 运行前先 action: skill { id: "${workflow.id}" }`);
|
|
417
542
|
}
|
|
418
|
-
|
|
543
|
+
// Prompt parameters carry the user's request, so they are marked to
|
|
544
|
+
// be filled from it and their saved default is not printed (a
|
|
545
|
+
// default shown here gets rerun verbatim); every other parameter
|
|
546
|
+
// is listed with its default.
|
|
547
|
+
const params = workflow.parameters ?? [];
|
|
548
|
+
if (params.length > 0)
|
|
549
|
+
lines.push(' 参数(键名用括号前的英文名;未传的参数使用默认值):');
|
|
550
|
+
for (const param of params) {
|
|
551
|
+
const prompt = promptRole(param) === 'write';
|
|
419
552
|
const label = param.label ?? param.name;
|
|
420
|
-
const def = param.default === undefined ? '' : `,默认 ${typeof param.default === 'string' ? `"${param.default}"` : String(param.default)}`;
|
|
553
|
+
const def = prompt || param.default === undefined ? '' : `,默认 ${typeof param.default === 'string' ? `"${param.default}"` : String(param.default)}`;
|
|
421
554
|
const options = Array.isArray(param.options) && param.options.length > 0 ? `,可选: ${param.options.join(' / ')}` : '';
|
|
422
555
|
const upload = param.upload !== undefined
|
|
423
556
|
? `,上传类型: ${param.upload}${param.upload === 'media' ? `(${param.subfolder ?? ''}/,空值=移除该参考位)` : ''}`
|
|
424
557
|
: '';
|
|
425
|
-
|
|
558
|
+
const fill = prompt ? ' — 按用户需求填写,未明确时据需求自行发挥' : '';
|
|
559
|
+
lines.push(` ${param.name}(${label}${param.random === true ? ',随机' : ''}${def}${options}${upload})${fill}`);
|
|
426
560
|
}
|
|
427
561
|
}
|
|
428
562
|
const loadArea = data.loadArea;
|
|
@@ -449,10 +583,7 @@ function workflowDefinition(runtime, ctx) {
|
|
|
449
583
|
const result = data.result;
|
|
450
584
|
if (result === undefined)
|
|
451
585
|
return [{ type: 'text', text: 'ComfyUI workflow run returned no result.' }];
|
|
452
|
-
|
|
453
|
-
for (const item of result.media)
|
|
454
|
-
lines.push(` ${item.kind}: ${item.url}`);
|
|
455
|
-
return [{ type: 'text', text: lines.join('\n') }];
|
|
586
|
+
return [{ type: 'text', text: runResultText('ComfyUI workflow', result) }];
|
|
456
587
|
},
|
|
457
588
|
presentationMeta(_args, value) {
|
|
458
589
|
return value;
|
|
@@ -617,6 +748,15 @@ function workflowDefinition(runtime, ctx) {
|
|
|
617
748
|
const values = typeof args.parameters === 'object' && args.parameters !== null
|
|
618
749
|
? args.parameters
|
|
619
750
|
: {};
|
|
751
|
+
// Unknown names used to be ignored silently, so a misspelt prompt key
|
|
752
|
+
// ran the saved default and looked like "ComfyUI ignores my prompt".
|
|
753
|
+
// Refuse before anything is queued and name the valid keys instead.
|
|
754
|
+
const known = new Set((saved.parameters ?? []).map((param) => param.name));
|
|
755
|
+
const unknown = Object.keys(values).filter((name) => !known.has(name));
|
|
756
|
+
if (unknown.length > 0) {
|
|
757
|
+
throw new Error(`comfyui_workflow: "${saved.name}" has no parameter ${unknown.map((name) => `"${name}"`).join(', ')} — nothing was submitted. Valid parameter names: ${[...known].join(', ') || '(none)'}`);
|
|
758
|
+
}
|
|
759
|
+
const prompts = promptsUsed(saved.parameters ?? [], values);
|
|
620
760
|
const promptId = await runtime.queue(saved.workflow, {
|
|
621
761
|
workflowName: saved.name,
|
|
622
762
|
workflowId: saved.id,
|
|
@@ -634,47 +774,12 @@ function workflowDefinition(runtime, ctx) {
|
|
|
634
774
|
if (jobs === undefined) {
|
|
635
775
|
throw new Error('comfyui_workflow: background jobs unavailable — load @deepseek-ai/dsh-jobs-local and @deepseek-ai/dsh-tool-jobs');
|
|
636
776
|
}
|
|
637
|
-
const jobId = jobs.
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
...(exec.agent !== undefined ? { owner: exec.agent } : {}),
|
|
641
|
-
run: () => {
|
|
642
|
-
const startedAt = Date.now();
|
|
643
|
-
const done = (async () => {
|
|
644
|
-
try {
|
|
645
|
-
const entry = await client.waitForCompletion({
|
|
646
|
-
promptId,
|
|
647
|
-
timeoutMs: waitMs,
|
|
648
|
-
pollIntervalMs: config.pollIntervalMs,
|
|
649
|
-
signal: new AbortController().signal,
|
|
650
|
-
});
|
|
651
|
-
const items = collectMedia({ promptId, entry, maxItems: config.maxMediaItems, proxyBase: runtime.proxyBase() });
|
|
652
|
-
const result = {
|
|
653
|
-
kind: 'sync',
|
|
654
|
-
promptId,
|
|
655
|
-
status: 'completed',
|
|
656
|
-
elapsedMs: Date.now() - startedAt,
|
|
657
|
-
media: items,
|
|
658
|
-
summary: summarizeMedia(items),
|
|
659
|
-
};
|
|
660
|
-
return { status: 'completed', output: JSON.stringify(result) };
|
|
661
|
-
}
|
|
662
|
-
catch (error) {
|
|
663
|
-
runtime.untrack(promptId);
|
|
664
|
-
const message = error instanceof Error ? error.message : String(error);
|
|
665
|
-
return { status: 'failed', detail: 'comfyui', output: message };
|
|
666
|
-
}
|
|
667
|
-
})();
|
|
668
|
-
return {
|
|
669
|
-
cancel: () => { void client.interrupt().catch(() => undefined); },
|
|
670
|
-
done,
|
|
671
|
-
};
|
|
672
|
-
},
|
|
673
|
-
});
|
|
674
|
-
return { action: 'run', id, workflowName: saved.name, background: { kind: 'background', jobId, promptId, label: saved.name } };
|
|
777
|
+
const jobId = startPromptJob(jobs, runtime, client, { promptId, label: saved.name, subject: 'ComfyUI workflow', waitMs, agent: exec.agent, prompts });
|
|
778
|
+
const background = { kind: 'background', jobId, promptId, label: saved.name, notifies: sessionJobs(jobs) };
|
|
779
|
+
return { action: 'run', id, workflowName: saved.name, background, report: backgroundText('ComfyUI workflow', background) };
|
|
675
780
|
}
|
|
676
|
-
const result = await waitSync(runtime, client, promptId, config, exec.signal, waitMs);
|
|
677
|
-
return { action: 'run', id, workflowName: saved.name, result };
|
|
781
|
+
const result = await waitSync(runtime, client, promptId, config, exec.signal, waitMs, { prompts });
|
|
782
|
+
return { action: 'run', id, workflowName: saved.name, result, report: runResultText('ComfyUI workflow', result) };
|
|
678
783
|
},
|
|
679
784
|
};
|
|
680
785
|
}
|
|
@@ -864,9 +969,11 @@ function skillDefinition(runtime) {
|
|
|
864
969
|
export function registerComfyUITools(ctx, runtime) {
|
|
865
970
|
const tools = ctx.tools;
|
|
866
971
|
const disposers = [];
|
|
867
|
-
|
|
868
|
-
|
|
972
|
+
// Main entry first: generating = comfyui_workflow list → run. The
|
|
973
|
+
// hand-written-workflow pair (object_info → run) is secondary and last.
|
|
869
974
|
disposers.push(tools.register(workflowDefinition(runtime, ctx)));
|
|
870
975
|
disposers.push(tools.register(skillDefinition(runtime)));
|
|
976
|
+
disposers.push(tools.register(objectInfoDefinition(runtime)));
|
|
977
|
+
disposers.push(tools.register(runDefinition(runtime, ctx)));
|
|
871
978
|
return disposers;
|
|
872
979
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-comfyui",
|
|
3
|
-
"version": "0.5.
|
|
3
|
+
"version": "0.5.5",
|
|
4
4
|
"description": "Let the DeepSeek Harness agent smartly drive a local or remote ComfyUI to generate anything, with workflow and asset management panels, per-workflow skill packs, a companion skill and a same-origin media proxy. / 让 DeepSeek Harness 的 Agent 智能驱动本地或远程 ComfyUI 生成任何内容。附带工作流、资产管理面板与技能包管理挂载。配套 skill 与同源媒体代理。",
|
|
5
5
|
"repository": {
|
|
6
6
|
"type": "git",
|
|
@@ -33,6 +33,8 @@
|
|
|
33
33
|
"build:host": "tsc -p tsconfig.json",
|
|
34
34
|
"build:client": "tsdown -c tsdown.config.mjs",
|
|
35
35
|
"typecheck": "tsc -p tsconfig.json --noEmit && tsc -p tsconfig.client.json --noEmit",
|
|
36
|
+
"test:host-compat": "npm run build:host && node scripts/test-host-compat.mjs",
|
|
37
|
+
"prepare": "npm run build",
|
|
36
38
|
"prepack": "npm run build"
|
|
37
39
|
},
|
|
38
40
|
"keywords": [
|
|
@@ -46,7 +48,8 @@
|
|
|
46
48
|
],
|
|
47
49
|
"license": "MIT",
|
|
48
50
|
"engines": {
|
|
49
|
-
"node": ">=22.19"
|
|
51
|
+
"node": ">=22.19",
|
|
52
|
+
"dsh": ">=0.1.2-alpha.4 <0.3.0-0"
|
|
50
53
|
},
|
|
51
54
|
"dependencies": {
|
|
52
55
|
"@deepseek-ai/schemastery": "^3.18.4",
|
|
@@ -54,11 +57,11 @@
|
|
|
54
57
|
},
|
|
55
58
|
"peerDependencies": {
|
|
56
59
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
57
|
-
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.4 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1"
|
|
60
|
+
"@deepseek-ai/dsh-settings": "^0.1.2-alpha.4 || ^0.1.5-alpha.1 || ^0.1.6-alpha.1 || ^0.1.7-alpha.1 || ^0.2.0-alpha.1 || ^0.2.1-alpha.1"
|
|
58
61
|
},
|
|
59
62
|
"devDependencies": {
|
|
60
63
|
"@deepseek-ai/cordis": "^4.0.1",
|
|
61
|
-
"@deepseek-ai/dsh-settings": "^0.1.2
|
|
64
|
+
"@deepseek-ai/dsh-settings": "^0.2.1-rc.2",
|
|
62
65
|
"@types/node": "^24.0.0",
|
|
63
66
|
"@types/react": "~18.3.1",
|
|
64
67
|
"react": "^18.3.1",
|
|
@@ -73,7 +76,6 @@
|
|
|
73
76
|
"platform": "web",
|
|
74
77
|
"inject": [
|
|
75
78
|
"@deepseek-ai/dsh-client-connection",
|
|
76
|
-
"@deepseek-ai/dsh-client-runtime",
|
|
77
79
|
"@deepseek-ai/dsh-client-locale",
|
|
78
80
|
"@deepseek-ai/dsh-client-ui-settings"
|
|
79
81
|
]
|