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/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
- const TOOL_TIMEOUT_MS = 3_600_000;
33
- function missing(args, name) {
34
- return args[name] === undefined || args[name] === null;
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
- function requireOneOf(args, names) {
37
- const present = names.filter((name) => !missing(args, name));
38
- if (present.length === 0)
39
- return `exactly one of ${names.join(', ')} is required`;
40
- if (present.length > 1)
41
- return `only one of ${names.join(', ')} may be given`;
42
- return undefined;
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
- const template = args.template;
46
- if (typeof template === 'string') {
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 must be an object');
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
- applyTemplateInputs(workflow, inputs);
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: workflow, label: 'comfyui custom 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
- function renderRunResult(_args, value) {
93
- const result = value;
94
- if (result.kind === 'background') {
95
- return [{
96
- type: 'text',
97
- text: `ComfyUI generation started in the background (job ${result.jobId}, prompt ${result.promptId}). Collect the result with job_output.`,
98
- }];
180
+ /** One ready-to-paste Markdown reference: images embed, other media link. */
181
+ function mediaMarkdown(item, n) {
182
+ return item.kind === 'image' ? `![image ${n}](${item.url})` : `[${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
- lines.push(` ${item.kind}: ${item.url}`);
249
+ if (item.url === item.filename)
250
+ lines.push(` ${item.kind}: ${item.filename}`);
105
251
  }
106
- return [{ type: 'text', text: lines.join('\n') }];
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: Date.now() - startedAt,
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
- 'Submit a workflow to the configured ComfyUI server and return the generated media (images/videos).',
152
- 'Provide exactly one of `workflow` (ComfyUI API-format object: node id → { class_type, inputs }) or `template` (built-in: txt2img | img2img | video).',
153
- 'Use `inputs` to override node inputs by id, e.g. {"6": {"text": "a red cat"}} for the positive prompt in the templates.',
154
- 'Templates: txt2img — 4 checkpoint, 5 EmptyLatentImage (width/height), 6 positive text, 7 negative text, 3 KSampler (seed/steps/cfg/denoise), 9 SaveImage. img2img — 10 LoadImage (image), 11 VAEEncode, 6 text, 3 KSampler (denoise). video — Wan 2.1, needs ComfyUI-WanVideoWrapper custom nodes (10 UNETLoader, 13 WanTextEncode, 14 WanImageToVideo, 15 KSampler, 17 SaveVideo).',
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 }. Alternative to `template`.' },
162
- template: { type: 'string', enum: ['txt2img', 'img2img', 'video'], description: 'Built-in workflow template id. Alternative to `workflow`.' },
163
- inputs: { type: 'object', description: 'Per-node input overrides keyed by node id, e.g. {"3": {"seed": 42, "steps": 30}, "6": {"text": "prompt"}}.' },
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.start({
207
- kind: 'comfyui',
208
- label,
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: 'List the node definitions the configured ComfyUI server supports (class types, required and optional inputs). Use it to build valid API-format workflows for comfyui_run. Optional `filter` narrows by class-name substring, e.g. "KSampler", "VAE", "LoadImage".',
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
- 'List and run saved ComfyUI workflows from the plugin workflow library (运行主题: API-format workflows extracted from a graph or pasted directly).',
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 collect the result with job_output.',
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 for job_output. Video/audio workflows should use async — generation takes minutes and sync may time out.' },
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: `ComfyUI workflow started in the background (job ${data.background.jobId}, prompt ${data.background.promptId}). Collect the result with job_output.` }];
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
- for (const param of workflow.parameters ?? []) {
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
- lines.push(` ${param.name}(${label}${param.random === true ? ',随机' : ''}${def}${options}${upload})`);
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
- const lines = [`ComfyUI workflow ${result.status} (prompt ${result.promptId}) in ${result.elapsedMs} ms — ${summarizeMedia(result.media)}`];
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.start({
638
- kind: 'comfyui',
639
- label: saved.name,
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
- disposers.push(tools.register(runDefinition(runtime, ctx)));
868
- disposers.push(tools.register(objectInfoDefinition(runtime)));
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",
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-alpha.4",
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
  ]