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/lib/comfyui.d.ts CHANGED
@@ -171,6 +171,16 @@ export declare class ComfyUIClient {
171
171
  deleteQueueItems(promptIds: string[]): Promise<void>;
172
172
  /** Clear the entire pending queue (running job is unaffected). */
173
173
  clearQueue(): Promise<void>;
174
+ /**
175
+ * Stop one prompt without touching anyone else's work: dequeue it while
176
+ * pending, interrupt it only while it is the running one, no-op once it has
177
+ * left the queue. A bare `interrupt()` stops whatever is running — another
178
+ * session's job or the user's own ComfyUI run — and ComfyUI versions before
179
+ * prompt-scoped interrupts ignore the `prompt_id` body, hence the queue
180
+ * check. (Older servers keep a tiny race: the prompt could finish between
181
+ * the check and the interrupt.)
182
+ */
183
+ cancelPrompt(promptId: string): Promise<void>;
174
184
  /** Interrupt the running prompt; without an id, interrupt globally. */
175
185
  interruptPrompt(promptId?: string): Promise<void>;
176
186
  /** Cancel one job regardless of state (running → interrupt, pending → dequeue). */
@@ -202,8 +212,6 @@ export declare class ComfyUIClient {
202
212
  comfyui_version?: string;
203
213
  };
204
214
  }>;
205
- /** Ask ComfyUI to interrupt the running prompt. */
206
- interrupt(): Promise<void>;
207
215
  /** Download one generated media file through GET /view. */
208
216
  fetchView(ref: ComfyUIMediaRef): Promise<{
209
217
  bytes: Uint8Array;
@@ -221,13 +229,16 @@ export declare class ComfyUIClient {
221
229
  fetchViewStreamed(ref: ComfyUIMediaRef, rangeHeader?: string, method?: 'GET' | 'HEAD'): Promise<ComfyViewStream>;
222
230
  /**
223
231
  * Poll history until the prompt completes, fails, or the budget/signal ends.
224
- * Interrupts the server when the signal aborts before throwing.
232
+ * When the signal aborts — mid-sleep included — cancels this prompt only
233
+ * (`cancelPrompt`) and throws an "interrupted" error. `onPoll` runs once per
234
+ * poll, for callers that publish progress.
225
235
  */
226
236
  waitForCompletion(opts: {
227
237
  promptId: string;
228
238
  timeoutMs: number;
229
239
  pollIntervalMs: number;
230
240
  signal: AbortSignal;
241
+ onPoll?: () => void;
231
242
  }): Promise<ComfyUIHistoryEntry>;
232
243
  }
233
244
  export declare function hasMedia(entry: ComfyUIHistoryEntry): boolean;
package/lib/comfyui.js CHANGED
@@ -268,6 +268,25 @@ export class ComfyUIClient {
268
268
  body: JSON.stringify({ clear: true }),
269
269
  });
270
270
  }
271
+ /**
272
+ * Stop one prompt without touching anyone else's work: dequeue it while
273
+ * pending, interrupt it only while it is the running one, no-op once it has
274
+ * left the queue. A bare `interrupt()` stops whatever is running — another
275
+ * session's job or the user's own ComfyUI run — and ComfyUI versions before
276
+ * prompt-scoped interrupts ignore the `prompt_id` body, hence the queue
277
+ * check. (Older servers keep a tiny race: the prompt could finish between
278
+ * the check and the interrupt.)
279
+ */
280
+ async cancelPrompt(promptId) {
281
+ const queue = await this.getQueue();
282
+ if (queue.queue_pending.some((item) => item.prompt_id === promptId)) {
283
+ await this.deleteQueueItems([promptId]);
284
+ return;
285
+ }
286
+ if (queue.queue_running.some((item) => item.prompt_id === promptId)) {
287
+ await this.interruptPrompt(promptId);
288
+ }
289
+ }
271
290
  /** Interrupt the running prompt; without an id, interrupt globally. */
272
291
  async interruptPrompt(promptId) {
273
292
  await this.request('/interrupt', {
@@ -361,10 +380,6 @@ export class ComfyUIClient {
361
380
  async systemStats() {
362
381
  return this.request('/system_stats');
363
382
  }
364
- /** Ask ComfyUI to interrupt the running prompt. */
365
- async interrupt() {
366
- await this.request('/interrupt', { method: 'POST' });
367
- }
368
383
  /** Download one generated media file through GET /view. */
369
384
  async fetchView(ref) {
370
385
  const params = new URLSearchParams({ filename: ref.filename, subfolder: ref.subfolder, type: ref.type });
@@ -409,12 +424,14 @@ export class ComfyUIClient {
409
424
  headers['Range'] = rangeHeader;
410
425
  const response = await this.fetchRoute(`/view?${params.toString()}`, { headers, method, signal: controller.signal });
411
426
  if (response.status !== 206 && response.status !== 416 && !response.ok) {
412
- throw new ComfyUIError(`ComfyUI /view failed: HTTP ${response.status}`);
427
+ await response.body?.cancel().catch(() => undefined);
428
+ throw new ComfyUIError(`ComfyUI /view failed: HTTP ${response.status}`, response.status);
413
429
  }
414
430
  if (response.status === 200) {
415
431
  const contentLength = response.headers.get('content-length');
416
432
  if (contentLength !== null && Number(contentLength) > this.maxMediaBytes) {
417
- throw new ComfyUIError(`ComfyUI media too large: ${contentLength} bytes exceeds maxMediaBytes ${this.maxMediaBytes}`);
433
+ await response.body?.cancel().catch(() => undefined);
434
+ throw new ComfyUIError(`ComfyUI media too large: ${contentLength} bytes exceeds maxMediaBytes ${this.maxMediaBytes}`, 413);
418
435
  }
419
436
  }
420
437
  return { status: response.status, headers: response.headers, body: response.body };
@@ -425,16 +442,19 @@ export class ComfyUIClient {
425
442
  }
426
443
  /**
427
444
  * Poll history until the prompt completes, fails, or the budget/signal ends.
428
- * Interrupts the server when the signal aborts before throwing.
445
+ * When the signal aborts — mid-sleep included — cancels this prompt only
446
+ * (`cancelPrompt`) and throws an "interrupted" error. `onPoll` runs once per
447
+ * poll, for callers that publish progress.
429
448
  */
430
449
  async waitForCompletion(opts) {
431
- const { promptId, timeoutMs, pollIntervalMs, signal } = opts;
450
+ const { promptId, timeoutMs, pollIntervalMs, signal, onPoll } = opts;
432
451
  const deadline = Date.now() + timeoutMs;
433
452
  for (;;) {
434
453
  if (signal.aborted) {
435
- await this.interrupt().catch(() => undefined);
454
+ await this.cancelPrompt(promptId).catch(() => undefined);
436
455
  throw new ComfyUIError(`ComfyUI generation interrupted (prompt ${promptId})`);
437
456
  }
457
+ onPoll?.();
438
458
  const entry = await this.getHistory(promptId);
439
459
  if (entry !== undefined) {
440
460
  const status = entry.status;
@@ -448,7 +468,8 @@ export class ComfyUIClient {
448
468
  if (Date.now() >= deadline) {
449
469
  throw new ComfyUIError(`ComfyUI generation timed out after ${timeoutMs} ms (prompt ${promptId})`);
450
470
  }
451
- await sleep(pollIntervalMs, signal);
471
+ // An abort ends the sleep early; the loop top then cancels and throws.
472
+ await sleep(pollIntervalMs, signal).catch(() => undefined);
452
473
  }
453
474
  }
454
475
  }
package/lib/skill.js CHANGED
@@ -6,10 +6,31 @@
6
6
  export const COMFYUI_SKILL = {
7
7
  name: 'dsh-comfyui-workflows',
8
8
  source: 'runtime',
9
- description: 'ComfyUI 工作流管理:区分“图工作流”(ComfyUI 端保存的画布,衍生主题)与“API 工作流”(可执行,运行主题),分析画布中的多个独立流程(连通分量),以及图→API 提取的规则与面板操作引导。也包含本机环境(用户 ComfyUI 目录)与 TTS-Audio-Suite 音色库的快速查询/快照刷新方案。处理 comfyui_workflow、需要定位用户 ComfyUI 文件/音色库,或用户要求运行 ComfyUI 端保存的工作流时加载。',
10
- whenToUse: '用户要求运行/管理 ComfyUI 中保存的工作流,或 comfyui_workflow list 显示未提取的图工作流时;需要判断一个画布是否包含多个独立流程、或解释为何某个工作流无法直接运行时;需要查询 TTS-Audio-Suite 音色库、刷新已存工作流的音色参数快照、或定位用户本机 ComfyUI 目录时;或 comfyui_workflow list 里某个工作流带“技能包”标记、需要在运行前读取它时。',
9
+ description: '用 ComfyUI 生成图片/视频/音频(文生图、图生图、文生视频、语音等)的入口:第一步用 comfyui_workflow list 挑库里的工作流再 run。也负责工作流管理:区分“图工作流”(ComfyUI 端保存的画布,衍生主题)与“API 工作流”(可执行,运行主题),分析画布中的多个独立流程(连通分量),以及图→API 提取的规则与面板操作引导。也包含本机环境(用户 ComfyUI 目录)与 TTS-Audio-Suite 音色库的快速查询/快照刷新方案。处理 comfyui_workflow、需要定位用户 ComfyUI 文件/音色库,或用户要求运行 ComfyUI 端保存的工作流时加载。',
10
+ whenToUse: '用户要用 ComfyUI 生成任何内容时(先 list 再 run);用户要求运行/管理 ComfyUI 中保存的工作流,或 comfyui_workflow list 显示未提取的图工作流时;需要判断一个画布是否包含多个独立流程、或解释为何某个工作流无法直接运行时;需要查询 TTS-Audio-Suite 音色库、刷新已存工作流的音色参数快照、或定位用户本机 ComfyUI 目录时;或 comfyui_workflow list 里某个工作流带“技能包”标记、需要在运行前读取它时。',
11
11
  content: `# dsh-comfyui 工作流管理
12
12
 
13
+ ## 生成内容:第一步永远是 comfyui_workflow list
14
+
15
+ 用户要生成任何内容(图片、视频、音频,文生图、图生图、TTS 等)时:
16
+
17
+ 1. **先调用 \`comfyui_workflow action: list\`**,按名称、描述、标签挑出合适的工作流。库里的工作流是用户为这台服务器配好的,模型和节点都可用。
18
+ 2. 有技能包的先 \`action: skill { id }\` 读一遍,然后 \`comfyui_workflow run { id, parameters }\`(细节见「运行工作流的正确流程」)。
19
+ 3. \`comfyui_object_info\` 和 \`comfyui_run\` **不是生成入口**,只用于手写工作流(见下一节)。库里有合适的工作流就不要用它们。
20
+ 4. 能直接调用工具就直接调用(对话里才会显示生成卡片);当前环境只能在 run_code 里调用时也可以,照返回值里的 \`report\` 字段做。
21
+
22
+ ## 手写工作流(次要:临时测试,或按本机环境新建工作流)
23
+
24
+ 库里没有合适的工作流、需要临时测试,或要为本机环境新建一个工作流时才用,步骤固定:
25
+
26
+ 1. **查环境**:\`comfyui_object_info { filter }\` 查节点类名、输入项和加载器里实际可选的模型文件名(如 \`filter: "Loader"\`)。
27
+ 2. **手写 API JSON**:节点 id → \`{ class_type, inputs }\`,连线写成 \`["节点id字符串", 输出槽位]\`,模型名只用第 1 步查到的。
28
+ 3. **运行测试**:\`comfyui_run { workflow }\`,报错就按错误信息改 JSON 再跑。
29
+
30
+ 测通后如果要长期用,告诉用户在面板里「新建工作流」粘贴这份 JSON 存进库,之后走 list → run。
31
+
32
+ ## 两个工作流主题
33
+
13
34
  本插件把 ComfyUI 工作流分成两个主题:
14
35
 
15
36
  - **图工作流(衍生主题)**:用户在 ComfyUI 画布上保存的 UI 图(nodes/links/widgets),是"源"。不能直接运行。一个图可能包含**多个互相独立的流程**。
@@ -142,7 +163,10 @@ TTS-Audio-Suite(\`{comfyuiDir}/custom_nodes/tts_audio_suite\`)的"🎭 Chara
142
163
  2. **有技能包就先读**:list 里某个工作流带 \`技能包:\` 那一行,说明用户为它写了专属说明书(适用场景、参数怎么填、哪些环节会翻车)。选中它之后、运行之前,先 \`comfyui_workflow action: skill { id }\` 把正文读进来并照着做。标注「运行前必读」的,不读会被 run 直接拒绝。没有这行的工作流跳过本步。
143
164
  3. **参数决策**:只依据清单里的参数(键名用英文名),选择/覆盖适合的值——**不需要查看工作流内部结构**。
144
165
  4. **run 直接调用**:\`comfyui_workflow run { id, parameters: {...} }\`。插件直接从库里取该工作流的 JSON 提交给 ComfyUI,你**只传参数覆盖值,绝不自己输出或复制完整工作流 JSON**(那会浪费大量 token)。
145
- - **视频/音频工作流一律 \`mode: "async"\`**:生成要几分钟,sync 会等待超时中断(报 generation aborted)。async 立即返回 job id,**启动后不要阻塞等待**(不要对返回的 job 用 wait: true 卡住),继续做其他事,后台任务完成时系统会通知你,到时再用 job_output(不 wait)收集结果和媒体回显。
166
+ - **参数**:键名与 list 完全一致(写错会被拒绝);未传的参数使用默认值。提示词类参数按用户需求填写,未明确时据需求自行发挥。
167
+ - **视频/音频工作流一律 \`mode: "async"\`**:生成要几分钟,sync 会等待超时中断(报 generation aborted)。async 立即返回 job id,**启动后不要阻塞等待**(不要对返回的 job 用 wait: true 卡住,也不要 sleep/轮询):告诉用户正在生成,**直接结束本轮回复**;有其他独立的事可以先做完再结束。后台任务完成时系统会发通知唤醒你,到时调用一次 job_output(不 wait)收集结果。
168
+ - **结果就是最终结果,地址已经带好**:不要再查状态,也不要去查资产、history 或拼接 URL。
169
+ - **生成完毕后的统一做法**(sync、async 的 job_output、run_code 都一样):用一句话告诉用户生成了什么(依据结果里的「Prompt used」),然后把结果给出的 Markdown 行**原样贴进回复**(图片是 \`![image 1](url)\`,视频/音频是链接)——这是用户看到结果的地方。
146
170
  - 图片生成可 sync(通常 <1 分钟);若不确定输出类型,也用 async。
147
171
  - \`action: get\` 只在**诊断/检查**工作流内容时使用(它会输出完整 JSON、占用大量 token),它**不是运行路径**。
148
172
 
package/lib/tools.d.ts CHANGED
@@ -1,9 +1,11 @@
1
1
  /**
2
2
  * Model-facing tools for dsh-comfyui, registered into the host `tools`
3
- * registry. `comfyui_run` submits a workflow and returns media results
4
- * (synchronously or as a background job); `comfyui_object_info` exposes the
5
- * server's node definitions; `comfyui_workflow` lists and runs saved
6
- * workflows from the panel-managed workflow library.
3
+ * registry. The main entry is `comfyui_workflow`: list and run saved
4
+ * workflows from the panel-managed library (synchronously or as a background
5
+ * job); `comfyui_skill` maintains their skill packs. Secondary, for
6
+ * hand-written workflows only (temporary tests, building a workflow for this
7
+ * environment): `comfyui_object_info` exposes the server's node definitions
8
+ * and `comfyui_run` submits a hand-written API workflow.
7
9
  */
8
10
  import type { Context } from '@deepseek-ai/cordis';
9
11
  import type { Config } from './config.js';
@@ -155,6 +157,17 @@ export interface RunResult {
155
157
  elapsedMs: number;
156
158
  media: RunMediaItem[];
157
159
  summary: string;
160
+ /** Saved-workflow runs: the prompt-type parameters as actually submitted. */
161
+ prompts?: PromptUsed[];
162
+ /** The model-facing text (`runResultText`), carried in the value for code-dispatched calls. */
163
+ report?: string;
164
+ }
165
+ /** One prompt-type parameter of a saved-workflow run, as actually submitted. */
166
+ export interface PromptUsed {
167
+ name: string;
168
+ text: string;
169
+ /** false = the caller did not pass it, so the saved default was used. */
170
+ passed: boolean;
158
171
  }
159
172
  /** Background mode result: collect later with job_output. */
160
173
  export interface BackgroundResult {
@@ -162,6 +175,10 @@ export interface BackgroundResult {
162
175
  jobId: string;
163
176
  promptId: string;
164
177
  label: string;
178
+ /** The host posts a completion notice (and wakes an idle agent) — dsh 0.1.7+ jobs. */
179
+ notifies: boolean;
180
+ /** The model-facing receipt (`backgroundText`), carried in the value for code-dispatched calls. */
181
+ report?: string;
165
182
  }
166
183
  /** Register the plugin tools; returns disposers. */
167
184
  export declare function registerComfyUITools(ctx: Context, runtime: ComfyUIRuntime): Array<() => void>;