dsh-comfyui-canvas 0.1.1 → 0.1.2
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.md +14 -5
- package/README.zh.md +10 -3
- package/docs/PLAN-v0.1.2.md +23 -4
- package/lib/client.js +42 -10
- package/lib/index.js +384 -29
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> [中文](README.zh.md) · English
|
|
4
4
|
|
|
5
5
|
[](LICENSE)
|
|
6
|
-
[](https://github.com/wbin0001/dsh-comfyui-canvas/releases)
|
|
7
7
|
[](https://github.com/wbin0001/dsh-comfyui-canvas)
|
|
8
8
|
[](https://github.com/DeepSeek-Harness/DSH)
|
|
9
9
|
[](https://github.com/comfyanonymous/ComfyUI)
|
|
@@ -19,6 +19,8 @@ Embed your **ComfyUI** (local or cloud) as a split-screen canvas tab inside [Dee
|
|
|
19
19
|
|
|
20
20
|
This package is the DSH-side plugin, and it ships the ComfyUI-side bridge node too. For headless/scale workloads it can be paired with the official ComfyUI MCP server — see [Canvas vs MCP](#canvas-vs-mcp--two-ways-to-drive-comfyui).
|
|
21
21
|
|
|
22
|
+
> **Where this plugin fits**: use it while **building / tuning a workflow on the live canvas** (the "IDE" role). For **unattended / batch / production runs**, hand the exported workflow to **Comfy CLI** (`comfy-cli run_workflow`) — it runs headlessly without a browser, which this canvas plugin deliberately does not do (the agent drives the canvas you are looking at; a closed browser means no runner). Export a workflow once with `comfyui_export_api`, then script it with the CLI at scale.
|
|
23
|
+
|
|
22
24
|
## What you get
|
|
23
25
|
|
|
24
26
|
| Surface | Description |
|
|
@@ -26,7 +28,8 @@ This package is the DSH-side plugin, and it ships the ComfyUI-side bridge node t
|
|
|
26
28
|
| **ComfyUI canvas tab** | A `ComfyUI` conversation view that embeds the ComfyUI frontend (local or cloud) side by side with the Chat rail. The iframe stays alive across tab switches (no reload). |
|
|
27
29
|
| **Visual canvas copilot** | The agent operates **the canvas you are looking at** — nodes appear, links wire, widgets change and runs trigger live on screen, so you watch every step instead of trusting an opaque JSON edit. Output images come back into the chat via `comfyui_get_outputs`. |
|
|
28
30
|
| **Canvas ops tools** | `comfyui_read_workflow`, `add_node`, `connect`, `set_param`, `remove_node`, `inject_text`, `load_workflow`, `run`, `debug` — build and fix workflows on the live canvas; `inject_text` writes conversation text straight into a node or a new wirable source. |
|
|
29
|
-
| **Production tools** | `comfyui_batch_run` sweeps a parameter matrix (seeds/prompts/strengths) in one go; `comfyui_get_outputs` pulls the resulting
|
|
31
|
+
| **Production tools** | `comfyui_batch_run` sweeps a parameter matrix (seeds/prompts/strengths) in one go; `comfyui_get_outputs` pulls the resulting files back into the chat — images, videos, gifs, and audio; `comfyui_attach_file` uploads any local file (image/audio/video/3D) into ComfyUI's input/ for the matching Load node; `comfyui_export_api` exports the live canvas as API-format workflow JSON for comfy-cli headless batch runs. |
|
|
32
|
+
| **Skills (SOPs)** | Built-in skills teach the agent the right order of operations: `comfyui-canvas-ops` (read → confirm → edit → run → fetch → self-check), `comfyui-admin-ops` (configure/launch/upgrade/node management), and `comfyui-video-audio-ops` (video + voiceover/audio track). Install the plugin and the skills ship with it — no extra setup. |
|
|
30
33
|
| **Upkeep tool** | `comfyui_upgrade` one-click updates the ComfyUI core and every git-backed custom node; `comfyui_config` reports the active connection and canvas focus. |
|
|
31
34
|
| **Canvas focus mode** | The agent can tell (via `comfyui_config`) whether the browser is on the canvas tab for the current session, and focus on canvas work only then. Session-isolated. |
|
|
32
35
|
| **Settings page** | ComfyUI base URL / port / network mode / bridge token / launch command / rail width. Changes apply live. Customized nav icon with ComfyUI logo. |
|
|
@@ -37,10 +40,16 @@ This package is the DSH-side plugin, and it ships the ComfyUI-side bridge node t
|
|
|
37
40
|
### 1. Install the DSH plugin
|
|
38
41
|
|
|
39
42
|
```bash
|
|
40
|
-
dsh plugin add
|
|
43
|
+
dsh plugin add dsh-comfyui-canvas
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
Or from a GitHub checkout:
|
|
47
|
+
|
|
48
|
+
```bash
|
|
49
|
+
dsh plugin add github:wbin0001/dsh-comfyui-canvas
|
|
41
50
|
```
|
|
42
51
|
|
|
43
|
-
|
|
52
|
+
Or from a local checkout:
|
|
44
53
|
|
|
45
54
|
```bash
|
|
46
55
|
# in your DSH profile
|
|
@@ -166,7 +175,7 @@ dsh-comfyui-canvas/
|
|
|
166
175
|
│ ├── __init__.py # /dsh-bridge/* HTTP routes on the ComfyUI server
|
|
167
176
|
│ └── entry/bridge.js # injected frontend: reports graph + runs commands
|
|
168
177
|
├── lib/
|
|
169
|
-
│ ├── index.js # DSH host:
|
|
178
|
+
│ ├── index.js # DSH host: 19 canvas tools + 4 built-in skills
|
|
170
179
|
│ └── client.js # DSH web: canvas tab / settings / rail polish
|
|
171
180
|
├── LICENSE
|
|
172
181
|
├── README.md
|
package/README.zh.md
CHANGED
|
@@ -3,7 +3,7 @@
|
|
|
3
3
|
> 中文 · [English](README.md)
|
|
4
4
|
|
|
5
5
|
[](LICENSE)
|
|
6
|
-
[](https://github.com/wbin0001/dsh-comfyui-canvas/releases)
|
|
7
7
|
[](https://github.com/wbin0001/dsh-comfyui-canvas)
|
|
8
8
|
[](https://github.com/DeepSeek-Harness/DSH)
|
|
9
9
|
[](https://github.com/comfyanonymous/ComfyUI)
|
|
@@ -28,7 +28,8 @@
|
|
|
28
28
|
| **画布标签页** | 对话里新增 `ComfyUI` 标签,左边画布、右边对话 rail 分屏。iframe 常驻不重载,切标签秒回。 |
|
|
29
29
|
| **可视化画布副驾** | agent 操作**你正在看的画布**——节点出现、连线接上、参数变化、运行触发,全部实时显示在屏幕上,每一步都看得见,而不是黑盒改 JSON。出图经 `comfyui_get_outputs` 直接带回对话。 |
|
|
30
30
|
| **画布操作工具** | `comfyui_read_workflow` / `add_node` / `connect` / `set_param` / `remove_node` / `inject_text` / `load_workflow` / `run` / `debug`——在活画布上搭建与修复工作流;`inject_text` 把对话文本一步注入为可连线节点。 |
|
|
31
|
-
| **生产工具** | `comfyui_batch_run` 一次扫参数矩阵(seed / prompt / 强度);`comfyui_get_outputs`
|
|
31
|
+
| **生产工具** | `comfyui_batch_run` 一次扫参数矩阵(seed / prompt / 强度);`comfyui_get_outputs` 把产物直接带回对话——图像、视频、GIF、音频都支持;`comfyui_attach_file` 把本机任意文件(图片/音频/视频/3D)上传进 ComfyUI `input/` 供对应 Load 节点使用;`comfyui_export_api` 把当前画布导出为 API 格式工作流,供 comfy-cli 无人值守批量。 |
|
|
32
|
+
| **技能包(SOP)** | 内置技能教 agent 按正确顺序操作:`comfyui-canvas-ops`(读→确认→改→跑→取回→自检)、`comfyui-admin-ops`(配置/启动/升级/节点管理)、`comfyui-video-audio-ops`(视频+配音/音轨)。装插件即自带技能,无需额外配置。 |
|
|
32
33
|
| **维护工具** | `comfyui_upgrade` 一键升级 ComfyUI 核心与全部 git 自定义节点;`comfyui_config` 报告当前连接与画布专注状态。 |
|
|
33
34
|
| **画布专注模式(会话隔离)** | agent 通过 `comfyui_config` 感知当前会话是否在画布标签,只在画布场景专注画布操作,且**按会话隔离**——多个会话互不干扰。 |
|
|
34
35
|
| **ComfyUI 报错处理** | `debug` 校验工作流并高亮报错节点(纯校验,不触发执行),agent 帮你定位/修复画布错误。 |
|
|
@@ -42,7 +43,13 @@
|
|
|
42
43
|
### 1. 安装 DSH 插件
|
|
43
44
|
|
|
44
45
|
```bash
|
|
45
|
-
dsh plugin add
|
|
46
|
+
dsh plugin add dsh-comfyui-canvas
|
|
47
|
+
```
|
|
48
|
+
|
|
49
|
+
或从 GitHub 安装:
|
|
50
|
+
|
|
51
|
+
```bash
|
|
52
|
+
dsh plugin add github:wbin0001/dsh-comfyui-canvas
|
|
46
53
|
```
|
|
47
54
|
|
|
48
55
|
或本地安装:
|
package/docs/PLAN-v0.1.2.md
CHANGED
|
@@ -249,10 +249,29 @@ step 12 发布(npm publish + awesome-dsh-plugin 收录信息若有变更)
|
|
|
249
249
|
### 设置导航图标(核心改动 §5.5)已落地
|
|
250
250
|
- 见 §5.5 完整记录;核心已还原、非侵入式图标生效、已推送
|
|
251
251
|
|
|
252
|
-
### npm publish
|
|
253
|
-
-
|
|
254
|
-
-
|
|
255
|
-
-
|
|
252
|
+
### npm publish 已完成(2026-09-02)
|
|
253
|
+
- ✅ `dsh-comfyui-canvas@0.1.1` 已发布到官方 registry(https://www.npmjs.com/package/dsh-comfyui-canvas)
|
|
254
|
+
- 包名未被占用;`files` 覆盖 LICENSE/README/lib/comfyui-bridge/screenshots.json/docs
|
|
255
|
+
- 安装命令已更新为 `dsh plugin add dsh-comfyui-canvas`(npm 方式)
|
|
256
|
+
- 发布 SOP:改版本号 → `npm config set registry https://registry.npmjs.org` → `npm publish`
|
|
257
|
+
- ⚠️ 注意:账户已开 2FA(安全密钥/Passkey);发布用 granular token(bypass 2FA,90 天有效,约 2026-12 到期)
|
|
258
|
+
|
|
259
|
+
### 通用文件传输 A1 + A2 已实现(2026-09-02,`a73ffd4`)
|
|
260
|
+
- ✅ `comfyui_attach_file`(host `lib/index.js`):任意文件(image/audio/video/3D)上传进 ComfyUI `input/`,`mediaTypeOf` 按扩展名推断,可选 `nodeId`+`widgetKey` 指向 Load 节点
|
|
261
|
+
- ✅ `comfyui_get_outputs` 泛化:扫描 `images/videos/gifs/audio` 四种 kind,每条带 `kind` + `/view` URL
|
|
262
|
+
- 已同步 node_modules 副本、已推送 GitHub
|
|
263
|
+
|
|
264
|
+
### 技能包 B 已实现(2026-09-02,`d1c3d07`)
|
|
265
|
+
- ✅ `ctx.skills.register` 内嵌三个 runtime 技能(随插件分发、零步骤):
|
|
266
|
+
- `comfyui-canvas-ops`(画布操作 SOP:读→确认→改→跑→取回→自检)
|
|
267
|
+
- `comfyui-admin-ops`(环境管理 SOP:配置/启动/升级/节点管理)
|
|
268
|
+
- `comfyui-video-audio-ops`(音画创作 SOP:视频+配音/音轨)
|
|
269
|
+
- `inject` 加 `skills`;技能内容中英双语、含安全约定
|
|
270
|
+
- 已同步 node_modules 副本、已推送 GitHub
|
|
271
|
+
|
|
272
|
+
### 剩余(见 §4 实施步骤)
|
|
273
|
+
- A3 画布会话框「+ 文件」通道(client 侧,硬前置:浏览器路径传递)
|
|
274
|
+
- C 节点开发调试(`read_source` / `edit_source` / `reload` + `comfyui-dev-ops` 技能)
|
|
256
275
|
|
|
257
276
|
---
|
|
258
277
|
|
package/lib/client.js
CHANGED
|
@@ -24,6 +24,9 @@ function canvasState() {
|
|
|
24
24
|
|
|
25
25
|
// 按会话更新 activeViewBySession 映射(读-改-写整张 map,settings 写入串行安全)。
|
|
26
26
|
// 只改自己 sessionId 的键,其它会话不受影响 → 画布模式感知是会话隔离的。
|
|
27
|
+
// 上限 MAX_SESSION_VIEWS:会话销毁时不会自动清理,若不封顶 map 会无限增长
|
|
28
|
+
// (WKB review 实测已 7 条)。写时裁剪到「当前会话 + 最近的 N-1 条」。
|
|
29
|
+
var MAX_SESSION_VIEWS = 50;
|
|
27
30
|
function setSessionView(sc, sessionId, mode) {
|
|
28
31
|
var snap = sc.getSnapshot ? sc.getSnapshot() : null;
|
|
29
32
|
var value = snap && snap.value ? snap.value : {};
|
|
@@ -33,6 +36,23 @@ function setSessionView(sc, sessionId, mode) {
|
|
|
33
36
|
for (var k in current) map[k] = current[k];
|
|
34
37
|
}
|
|
35
38
|
map[String(sessionId)] = mode;
|
|
39
|
+
var keys = Object.keys(map);
|
|
40
|
+
if (keys.length > MAX_SESSION_VIEWS) {
|
|
41
|
+
// Drop oldest keys (object string-key order = insertion order), always
|
|
42
|
+
// keeping the current session.
|
|
43
|
+
var drop = keys.length - MAX_SESSION_VIEWS;
|
|
44
|
+
var keep = {};
|
|
45
|
+
var currentKey = String(sessionId);
|
|
46
|
+
var kept = 0;
|
|
47
|
+
for (var i = 0; i < keys.length; i++) {
|
|
48
|
+
var k = keys[i];
|
|
49
|
+
if (k === currentKey || kept < MAX_SESSION_VIEWS - 1) {
|
|
50
|
+
keep[k] = map[k];
|
|
51
|
+
kept++;
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
map = keep;
|
|
55
|
+
}
|
|
36
56
|
sc.set("activeViewBySession", map).catch(function () {});
|
|
37
57
|
}
|
|
38
58
|
|
|
@@ -562,9 +582,10 @@ function ComfyUICanvasView(props) {
|
|
|
562
582
|
};
|
|
563
583
|
}, [sessionId]);
|
|
564
584
|
|
|
565
|
-
// ComfyUI
|
|
566
|
-
//
|
|
567
|
-
//
|
|
585
|
+
// ComfyUI 可达性检测:每隔几秒探一下。优先用正常 CORS 请求并检查 res.ok
|
|
586
|
+
// (能区分"在线"和"500 报错");若 DSH 页面 (127.0.0.1:3080) 跨域探测
|
|
587
|
+
// ComfyUI (127.0.0.1:8188) 被 CORS 拦截(抛 TypeError),回退到 mode:'no-cors'
|
|
588
|
+
// 的 opaque 可达性探测——服务可达即 resolve,不可达才 reject。
|
|
568
589
|
var onlineState = React.useState(null); // null=检测中, true=在线, false=离线
|
|
569
590
|
var online = onlineState[0];
|
|
570
591
|
var setOnline = onlineState[1];
|
|
@@ -574,17 +595,28 @@ function ComfyUICanvasView(props) {
|
|
|
574
595
|
// 同步顶栏状态圆点(初始检测中)。
|
|
575
596
|
updateCanvasStatus(null);
|
|
576
597
|
var probe = function () {
|
|
577
|
-
fetch(activeBase + "/system_stats", { method: "GET",
|
|
578
|
-
.then(function () {
|
|
598
|
+
fetch(activeBase + "/system_stats", { method: "GET", signal: AbortSignal.timeout(2500) })
|
|
599
|
+
.then(function (res) {
|
|
579
600
|
if (stopped) return;
|
|
580
|
-
setOnline(
|
|
581
|
-
setStarting(false);
|
|
582
|
-
updateCanvasStatus(
|
|
601
|
+
setOnline(res.ok); // 200-299 → 在线;4xx/5xx → 离线(服务在但报错)
|
|
602
|
+
setStarting(false);
|
|
603
|
+
updateCanvasStatus(res.ok);
|
|
583
604
|
})
|
|
584
605
|
.catch(function () {
|
|
585
606
|
if (stopped) return;
|
|
586
|
-
|
|
587
|
-
|
|
607
|
+
// CORS 拦截(TypeError)或网络错误 → 用 no-cors 回退判断端口是否可达
|
|
608
|
+
fetch(activeBase + "/system_stats", { method: "GET", mode: "no-cors", signal: AbortSignal.timeout(2500) })
|
|
609
|
+
.then(function () {
|
|
610
|
+
if (stopped) return;
|
|
611
|
+
setOnline(true);
|
|
612
|
+
setStarting(false);
|
|
613
|
+
updateCanvasStatus(true);
|
|
614
|
+
})
|
|
615
|
+
.catch(function () {
|
|
616
|
+
if (stopped) return;
|
|
617
|
+
setOnline(false);
|
|
618
|
+
updateCanvasStatus(false);
|
|
619
|
+
});
|
|
588
620
|
});
|
|
589
621
|
};
|
|
590
622
|
probe();
|
package/lib/index.js
CHANGED
|
@@ -17,7 +17,7 @@ import z from '@deepseek-ai/schemastery'
|
|
|
17
17
|
import { settingsNamespace } from '@deepseek-ai/dsh-settings'
|
|
18
18
|
|
|
19
19
|
export const name = 'dsh-comfyui-canvas'
|
|
20
|
-
export const inject = ['tools', 'settings', 'shell']
|
|
20
|
+
export const inject = ['tools', 'settings', 'shell', 'skills']
|
|
21
21
|
|
|
22
22
|
const NAMESPACE = settingsNamespace('dsh-comfyui-canvas')
|
|
23
23
|
const DEFAULT_BASE = 'http://127.0.0.1:8188'
|
|
@@ -40,6 +40,10 @@ const ConfigSchema = z.object({
|
|
|
40
40
|
comfyuiDir: z.string().default(''),
|
|
41
41
|
/** Split-rail width in px. */
|
|
42
42
|
railWidth: z.number().default(360),
|
|
43
|
+
/** Canvas scale percent (legacy UI field; kept in schema so settings don't drop it). */
|
|
44
|
+
canvasScale: z.number().default(80),
|
|
45
|
+
/** Last active view across sessions (legacy echo of activeViewBySession). */
|
|
46
|
+
activeView: z.string().default('chat'),
|
|
43
47
|
/**
|
|
44
48
|
* Optional shared secret for the ComfyUI bridge. When set, every agent
|
|
45
49
|
* request carries `Authorization: Bearer <token>`, and the bridge only
|
|
@@ -236,7 +240,38 @@ export function apply(ctx) {
|
|
|
236
240
|
updated_at: null,
|
|
237
241
|
}
|
|
238
242
|
}
|
|
239
|
-
|
|
243
|
+
// Normalize node/link ids to numbers. The frontend reports ids as
|
|
244
|
+
// strings; every write tool (connect srcId/dstId, set_param nodeId, …)
|
|
245
|
+
// declares `number`, so returning strings forces the model to convert.
|
|
246
|
+
// Coerce only numeric-looking ids; leave exotic ones untouched.
|
|
247
|
+
const normalized = {
|
|
248
|
+
...data,
|
|
249
|
+
nodes: Array.isArray(data.nodes) ? data.nodes.map((n) => {
|
|
250
|
+
const id = Number(n?.id)
|
|
251
|
+
return { ...n, id: Number.isFinite(id) ? id : n?.id }
|
|
252
|
+
}) : data.nodes,
|
|
253
|
+
workflow: data.workflow && Array.isArray(data.workflow.nodes)
|
|
254
|
+
? {
|
|
255
|
+
...data.workflow,
|
|
256
|
+
nodes: data.workflow.nodes.map((n) => {
|
|
257
|
+
const id = Number(n?.id)
|
|
258
|
+
return { ...n, id: Number.isFinite(id) ? id : n?.id }
|
|
259
|
+
}),
|
|
260
|
+
links: Array.isArray(data.workflow.links) ? data.workflow.links.map((l) => {
|
|
261
|
+
const src = Number(l?.[1])
|
|
262
|
+
const dst = Number(l?.[3])
|
|
263
|
+
return [
|
|
264
|
+
l?.[0],
|
|
265
|
+
Number.isFinite(src) ? src : l?.[1],
|
|
266
|
+
l?.[2],
|
|
267
|
+
Number.isFinite(dst) ? dst : l?.[3],
|
|
268
|
+
...(l?.slice ? l.slice(4) : []),
|
|
269
|
+
]
|
|
270
|
+
}) : data.workflow.links,
|
|
271
|
+
}
|
|
272
|
+
: data.workflow,
|
|
273
|
+
}
|
|
274
|
+
return { ready: true, ...normalized }
|
|
240
275
|
},
|
|
241
276
|
}))
|
|
242
277
|
|
|
@@ -360,19 +395,20 @@ export function apply(ctx) {
|
|
|
360
395
|
},
|
|
361
396
|
}))
|
|
362
397
|
|
|
363
|
-
// M2: fetch the output
|
|
364
|
-
// /history/{promptId} (native endpoint, same
|
|
365
|
-
// lists every output node's
|
|
366
|
-
//
|
|
367
|
-
// the id is not in history yet
|
|
398
|
+
// M2: fetch the output files of a completed run (images, videos, gifs,
|
|
399
|
+
// audio). Reads ComfyUI's own /history/{promptId} (native endpoint, same
|
|
400
|
+
// trust model as /prompt) and lists every output node's files (kind /
|
|
401
|
+
// filename / subfolder / type / view URL). With downloadDir, the files are
|
|
402
|
+
// also saved locally. `ready:false` means the id is not in history yet.
|
|
368
403
|
ctx.tools.register(defineTool({
|
|
369
404
|
name: 'comfyui_get_outputs',
|
|
370
|
-
description: 'Fetch the output
|
|
371
|
-
'Reads ComfyUI /history/{promptId}, lists every output node with its
|
|
372
|
-
'and optionally downloads them into a local directory.
|
|
405
|
+
description: 'Fetch the output files produced by a completed ComfyUI run (comfyui_run returns prompt_id): ' +
|
|
406
|
+
'images, videos, gifs, and audio. Reads ComfyUI /history/{promptId}, lists every output node with its ' +
|
|
407
|
+
'files (kind/filename/type/subfolder/URL), and optionally downloads them into a local directory. ' +
|
|
408
|
+
'Returns ready:false while the run is still executing.',
|
|
373
409
|
parameters: {
|
|
374
410
|
promptId: { type: 'string', required: true, description: 'The prompt_id returned by comfyui_run.' },
|
|
375
|
-
downloadDir: { type: 'string', description: 'Optional absolute local directory to save the
|
|
411
|
+
downloadDir: { type: 'string', description: 'Optional absolute local directory to save the output files into. Omit to return URLs only.' },
|
|
376
412
|
},
|
|
377
413
|
output: { schema: { type: 'object', additionalProperties: true }, render: renderJSON },
|
|
378
414
|
async execute(args, exec) {
|
|
@@ -387,17 +423,27 @@ export function apply(ctx) {
|
|
|
387
423
|
}
|
|
388
424
|
const outputs = item.outputs ?? {}
|
|
389
425
|
const files = []
|
|
426
|
+
// ComfyUI history outputs carry file arrays under per-kind keys:
|
|
427
|
+
// images / videos / gifs / audio (depends on the node). Scan them all so
|
|
428
|
+
// a run's video/audio/3D outputs come back like images do — each entry
|
|
429
|
+
// keeps its kind + a /view URL (the view endpoint serves any stored file).
|
|
430
|
+
const FILE_KINDS = ['images', 'videos', 'gifs', 'audio']
|
|
431
|
+
// Singular label per history key (images→image, videos→video, gifs→gif,
|
|
432
|
+
// audio→audio). A naive slice(0,-1) would corrupt "audio" into "audi".
|
|
433
|
+
const KIND_LABEL = { images: 'image', videos: 'video', gifs: 'gif', audio: 'audio' }
|
|
390
434
|
for (const [nodeId, out] of Object.entries(outputs)) {
|
|
391
|
-
const
|
|
392
|
-
|
|
393
|
-
const
|
|
394
|
-
|
|
395
|
-
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
|
|
399
|
-
|
|
400
|
-
|
|
435
|
+
for (const kind of FILE_KINDS) {
|
|
436
|
+
const list = Array.isArray(out?.[kind]) ? out[kind] : []
|
|
437
|
+
for (const file of list) {
|
|
438
|
+
const filename = file?.filename
|
|
439
|
+
if (!filename) continue
|
|
440
|
+
const subfolder = file?.subfolder ?? ''
|
|
441
|
+
const type = file?.type ?? 'output'
|
|
442
|
+
const qs = `filename=${encodeURIComponent(filename)}` +
|
|
443
|
+
(subfolder ? `&subfolder=${encodeURIComponent(subfolder)}` : '') +
|
|
444
|
+
`&type=${encodeURIComponent(type)}`
|
|
445
|
+
files.push({ nodeId, kind: KIND_LABEL[kind] ?? kind, filename, subfolder, type, url: `${base}/view?${qs}` })
|
|
446
|
+
}
|
|
401
447
|
}
|
|
402
448
|
}
|
|
403
449
|
let downloaded = []
|
|
@@ -494,12 +540,48 @@ export function apply(ctx) {
|
|
|
494
540
|
activeView = 'chat'
|
|
495
541
|
activeViewKnown = true
|
|
496
542
|
}
|
|
543
|
+
// P0 handshake: does the BRIDGE actually enforce the token? The DSH side
|
|
544
|
+
// setting alone proves nothing — if the ComfyUI side never got
|
|
545
|
+
// DSH_BRIDGE_TOKEN, _authorized() always passes and the bridge is open.
|
|
546
|
+
// Probe with the correct token and a deliberately wrong one: both
|
|
547
|
+
// succeeding means auth is NOT effective on the wire.
|
|
548
|
+
const token = (value?.bridgeToken || '').trim()
|
|
549
|
+
let bridgeAuthEffective = null
|
|
550
|
+
let bridgeAuthProbe = 'not-probed'
|
|
551
|
+
try {
|
|
552
|
+
const good = await fetch(`${baseUrl()}/dsh-bridge/workflow`, {
|
|
553
|
+
headers: token ? { Authorization: `Bearer ${token}` } : {},
|
|
554
|
+
signal: AbortSignal.timeout(4000),
|
|
555
|
+
})
|
|
556
|
+
const bad = token
|
|
557
|
+
? await fetch(`${baseUrl()}/dsh-bridge/workflow`, {
|
|
558
|
+
headers: { Authorization: 'Bearer dsh-bt-__wrong-probe__' },
|
|
559
|
+
signal: AbortSignal.timeout(4000),
|
|
560
|
+
})
|
|
561
|
+
: good
|
|
562
|
+
if (!good.ok) {
|
|
563
|
+
bridgeAuthProbe = `unreachable-or-error: HTTP ${good.status}`
|
|
564
|
+
} else if (!token) {
|
|
565
|
+
// No token configured on either side — open by design but flag it.
|
|
566
|
+
bridgeAuthEffective = false
|
|
567
|
+
bridgeAuthProbe = 'no token configured on DSH side'
|
|
568
|
+
} else {
|
|
569
|
+
bridgeAuthEffective = bad.status === 401
|
|
570
|
+
bridgeAuthProbe = bad.status === 401
|
|
571
|
+
? 'bridge rejects bad token (auth effective)'
|
|
572
|
+
: 'bridge accepted a WRONG token — auth NOT effective on the ComfyUI side'
|
|
573
|
+
}
|
|
574
|
+
} catch (e) {
|
|
575
|
+
bridgeAuthProbe = `probe failed: ${e instanceof Error ? e.message : String(e)}`
|
|
576
|
+
}
|
|
497
577
|
return {
|
|
498
578
|
baseUrl: baseUrl(),
|
|
499
579
|
activeView,
|
|
500
580
|
activeViewKnown,
|
|
501
581
|
sessionSource,
|
|
502
|
-
bridgeTokenSet: Boolean(
|
|
582
|
+
bridgeTokenSet: Boolean(token),
|
|
583
|
+
bridgeAuthEffective,
|
|
584
|
+
bridgeAuthProbe,
|
|
503
585
|
comfyuiDir: value?.comfyuiDir || '',
|
|
504
586
|
launchCommand: value?.launchCommand || '',
|
|
505
587
|
}
|
|
@@ -529,6 +611,12 @@ export function apply(ctx) {
|
|
|
529
611
|
}
|
|
530
612
|
const fs = await import('node:fs')
|
|
531
613
|
const path = await import('node:path')
|
|
614
|
+
// WKB review: 69 sequential git pulls can take ~2.3h and hang the agent.
|
|
615
|
+
// Run concurrently (4 lanes), bound the whole batch, and skip dirty
|
|
616
|
+
// checkouts (locally-modified repos are exactly this plugin's own users)
|
|
617
|
+
// instead of hard-pulling into conflicts.
|
|
618
|
+
const CONCURRENCY = 4
|
|
619
|
+
const BATCH_TIMEOUT_MS = 300000 // 5 min total
|
|
532
620
|
const run = async (label, command, workdir) => {
|
|
533
621
|
try {
|
|
534
622
|
const spec = shell.resolve({ command, workdir, timeoutMs: 120000 })
|
|
@@ -542,9 +630,10 @@ export function apply(ctx) {
|
|
|
542
630
|
}
|
|
543
631
|
const results = []
|
|
544
632
|
const scopeArg = args?.scope ?? 'all'
|
|
633
|
+
const jobs = []
|
|
545
634
|
// core: the ComfyUI root is itself a git checkout
|
|
546
635
|
if (scopeArg !== 'nodes' && fs.existsSync(path.join(dir, '.git'))) {
|
|
547
|
-
|
|
636
|
+
jobs.push({ label: 'core', workdir: dir })
|
|
548
637
|
}
|
|
549
638
|
// nodes: every git-backed custom node
|
|
550
639
|
if (scopeArg !== 'core') {
|
|
@@ -557,12 +646,39 @@ export function apply(ctx) {
|
|
|
557
646
|
for (const name of entries) {
|
|
558
647
|
const nodeDir = path.join(nodesDir, name)
|
|
559
648
|
if (fs.existsSync(path.join(nodeDir, '.git'))) {
|
|
560
|
-
|
|
649
|
+
jobs.push({ label: `node:${name}`, workdir: nodeDir })
|
|
561
650
|
}
|
|
562
651
|
}
|
|
563
652
|
}
|
|
564
653
|
}
|
|
565
|
-
|
|
654
|
+
// dirty probe: empty output = clean (pull safe); non-empty = skip + report
|
|
655
|
+
const skippedDirty = []
|
|
656
|
+
const pullJobs = []
|
|
657
|
+
for (const job of jobs) {
|
|
658
|
+
const probe = await run(job.label, 'git status --porcelain', job.workdir)
|
|
659
|
+
if (probe.ok && probe.out.trim().length > 0) {
|
|
660
|
+
skippedDirty.push({ label: job.label, changedFiles: probe.out.trim().split('\n').length })
|
|
661
|
+
} else {
|
|
662
|
+
pullJobs.push(job)
|
|
663
|
+
}
|
|
664
|
+
}
|
|
665
|
+
// bounded concurrency over the pull queue
|
|
666
|
+
const started = Date.now()
|
|
667
|
+
let cursor = 0
|
|
668
|
+
const worker = async () => {
|
|
669
|
+
while (cursor < pullJobs.length) {
|
|
670
|
+
if (Date.now() - started > BATCH_TIMEOUT_MS) {
|
|
671
|
+
results.push({ label: pullJobs[cursor].label, ok: false, error: 'batch timeout exceeded' })
|
|
672
|
+
cursor++
|
|
673
|
+
continue
|
|
674
|
+
}
|
|
675
|
+
const job = pullJobs[cursor++]
|
|
676
|
+
results.push(await run(job.label, 'git pull', job.workdir))
|
|
677
|
+
}
|
|
678
|
+
}
|
|
679
|
+
await Promise.all(Array.from({ length: Math.min(CONCURRENCY, pullJobs.length) }, worker))
|
|
680
|
+
results.sort((a, b) => a.label.localeCompare(b.label))
|
|
681
|
+
return { upgraded: results.length, skippedDirty, results }
|
|
566
682
|
},
|
|
567
683
|
}))
|
|
568
684
|
|
|
@@ -627,12 +743,14 @@ export function apply(ctx) {
|
|
|
627
743
|
// the source file is on the agent's own machine, so the host reads and
|
|
628
744
|
// uploads it — same trust model as get_outputs' /history + /view. The bridge
|
|
629
745
|
// token does not apply (native endpoint, not /dsh-bridge/*).
|
|
746
|
+
// DEPRECATED (v0.1.2): superseded by comfyui_attach_file (image is a subset
|
|
747
|
+
// of its media types). Kept for back-compat; removed in a future version.
|
|
630
748
|
ctx.tools.register(defineTool({
|
|
631
749
|
name: 'comfyui_attach_image',
|
|
632
|
-
description: 'Upload a local image into ComfyUI\'s input/
|
|
633
|
-
'node at it. Uses ComfyUI\'s native /upload/image (not the bridge),
|
|
634
|
-
'the agent\'s own machine. After upload, pass nodeId to update a LoadImage
|
|
635
|
-
'the new filename so it takes effect immediately.',
|
|
750
|
+
description: '[DEPRECATED — use comfyui_attach_file instead] Upload a local image into ComfyUI\'s input/ ' +
|
|
751
|
+
'directory and optionally point a LoadImage node at it. Uses ComfyUI\'s native /upload/image (not the bridge), ' +
|
|
752
|
+
'so the source file is read from the agent\'s own machine. After upload, pass nodeId to update a LoadImage ' +
|
|
753
|
+
'node\'s image widget to the new filename so it takes effect immediately.',
|
|
636
754
|
parameters: {
|
|
637
755
|
image: { type: 'string', required: true, description: 'Absolute local path to the image file (png/jpeg/webp/gif).' },
|
|
638
756
|
nodeId: { type: 'number', description: 'Optional LoadImage node id to point at the uploaded file.' },
|
|
@@ -723,4 +841,241 @@ export function apply(ctx) {
|
|
|
723
841
|
return sendCommand(baseUrl(), bridgeToken(), 'export_api', {}, exec.signal, { timeoutMs: 10000 })
|
|
724
842
|
},
|
|
725
843
|
}))
|
|
844
|
+
|
|
845
|
+
// v0.1.2 C: node development/debug tools — read / edit custom-node source and
|
|
846
|
+
// reload ComfyUI so the change takes effect, all from the conversation. The
|
|
847
|
+
// canvas doubles as the observation window: edit → reload → run a minimal
|
|
848
|
+
// workflow on the canvas → see the result, without leaving the chat.
|
|
849
|
+
const requireComfyUIDir = async () => {
|
|
850
|
+
const value = settingsScope?.get?.()
|
|
851
|
+
const dir = (value?.comfyuiDir || process.env.COMFYUI_DIR || '').trim()
|
|
852
|
+
if (!dir) throw new Error('ComfyUI install directory not set. Set comfyuiDir in Settings → ComfyUI 画布, or env COMFYUI_DIR.')
|
|
853
|
+
return dir
|
|
854
|
+
}
|
|
855
|
+
const resolveNodeDir = async (root, nodeName) => {
|
|
856
|
+
const { join, resolve, sep } = await import('node:path')
|
|
857
|
+
const nodeDir = join(root, 'custom_nodes', nodeName)
|
|
858
|
+
// Anchor to the custom_nodes directory itself, not the ComfyUI root:
|
|
859
|
+
// a nodeName like "../x" would otherwise resolve inside root but outside
|
|
860
|
+
// custom_nodes and slip past a root-anchored prefix check.
|
|
861
|
+
const nodesReal = resolve(join(root, 'custom_nodes'))
|
|
862
|
+
const nodeReal = resolve(nodeDir)
|
|
863
|
+
if (!nodeReal.startsWith(nodesReal + sep)) throw new Error(`invalid node path: ${nodeName}`)
|
|
864
|
+
return nodeDir
|
|
865
|
+
}
|
|
866
|
+
// Shared helper: list source files under a node dir (top two levels, skipping
|
|
867
|
+
// .git / node_modules / __pycache__ / .venv), or read one file's content.
|
|
868
|
+
const inspectNodeSource = async (nodeDir, relPath) => {
|
|
869
|
+
const fs = await import('node:fs')
|
|
870
|
+
const path = await import('node:path')
|
|
871
|
+
if (relPath) {
|
|
872
|
+
const target = path.resolve(nodeDir, relPath)
|
|
873
|
+
if (!target.startsWith(path.resolve(nodeDir) + path.sep)) throw new Error('path escapes node dir')
|
|
874
|
+
if (!fs.existsSync(target) || !fs.statSync(target).isFile()) throw new Error(`file not found: ${relPath}`)
|
|
875
|
+
return { path: relPath, content: fs.readFileSync(target, 'utf8') }
|
|
876
|
+
}
|
|
877
|
+
const SKIP = new Set(['.git', 'node_modules', '__pycache__', '.venv', 'venv', 'dist', 'build'])
|
|
878
|
+
const tree = []
|
|
879
|
+
const walk = (dir, depth) => {
|
|
880
|
+
if (depth > 2) return
|
|
881
|
+
for (const entry of fs.readdirSync(dir, { withFileTypes: true })) {
|
|
882
|
+
if (SKIP.has(entry.name)) continue
|
|
883
|
+
const full = path.join(dir, entry.name)
|
|
884
|
+
const rel = path.relative(nodeDir, full).replace(/\\/g, '/')
|
|
885
|
+
tree.push({ path: rel, type: entry.isDirectory() ? 'dir' : 'file', size: entry.isFile() ? fs.statSync(full).size : null })
|
|
886
|
+
if (entry.isDirectory()) walk(full, depth + 1)
|
|
887
|
+
}
|
|
888
|
+
}
|
|
889
|
+
walk(nodeDir, 0)
|
|
890
|
+
return { node: path.basename(nodeDir), tree }
|
|
891
|
+
}
|
|
892
|
+
ctx.tools.register(defineTool({
|
|
893
|
+
name: 'comfyui_read_source',
|
|
894
|
+
description: 'Read a ComfyUI custom node\'s source under custom_nodes/<name>/. Without path: list its source tree ' +
|
|
895
|
+
'(top two levels, skipping .git/node_modules/caches). With path: return that file\'s full content. ' +
|
|
896
|
+
'Use during node development to inspect or debug a custom node (including this plugin\'s own bridge node).',
|
|
897
|
+
parameters: {
|
|
898
|
+
node: { type: 'string', required: true, description: 'Custom node directory name under ComfyUI custom_nodes/ (e.g. ComfyUI-DSH-Canvas).' },
|
|
899
|
+
path: { type: 'string', description: 'Optional relative file path inside the node dir to read (omit to list the tree).' },
|
|
900
|
+
},
|
|
901
|
+
output: { schema: { type: 'object', additionalProperties: true }, render: renderJSON },
|
|
902
|
+
async execute(args) {
|
|
903
|
+
const fs = await import('node:fs')
|
|
904
|
+
const dir = await requireComfyUIDir()
|
|
905
|
+
const nodeDir = await resolveNodeDir(dir, args.node)
|
|
906
|
+
if (!fs.existsSync(nodeDir)) throw new Error(`custom node not found: ${args.node}`)
|
|
907
|
+
return inspectNodeSource(nodeDir, args.path)
|
|
908
|
+
},
|
|
909
|
+
}))
|
|
910
|
+
ctx.tools.register(defineTool({
|
|
911
|
+
name: 'comfyui_edit_source',
|
|
912
|
+
description: 'Edit a file inside a ComfyUI custom node\'s source (custom_nodes/<name>/). Writes new content to the ' +
|
|
913
|
+
'given relative path; path is confined to the node dir. Pair with comfyui_reload to apply the change: ' +
|
|
914
|
+
'edit → reload → run a minimal workflow on the canvas to verify. This is how you develop/debug a custom node ' +
|
|
915
|
+
'from the conversation without touching the ComfyUI UI.',
|
|
916
|
+
parameters: {
|
|
917
|
+
node: { type: 'string', required: true, description: 'Custom node directory name under ComfyUI custom_nodes/.' },
|
|
918
|
+
path: { type: 'string', required: true, description: 'Relative file path inside the node dir to write (e.g. __init__.py or entry/bridge.js).' },
|
|
919
|
+
content: { type: 'string', required: true, description: 'Full new file content (replaces the file).' },
|
|
920
|
+
},
|
|
921
|
+
output: { schema: { type: 'object', additionalProperties: true }, render: renderJSON },
|
|
922
|
+
async execute(args) {
|
|
923
|
+
const fs = await import('node:fs')
|
|
924
|
+
const path = await import('node:path')
|
|
925
|
+
const dir = await requireComfyUIDir()
|
|
926
|
+
const nodeDir = await resolveNodeDir(dir, args.node)
|
|
927
|
+
const target = path.resolve(nodeDir, args.path)
|
|
928
|
+
if (!target.startsWith(path.resolve(nodeDir) + path.sep)) throw new Error('path escapes node dir')
|
|
929
|
+
fs.mkdirSync(path.dirname(target), { recursive: true })
|
|
930
|
+
fs.writeFileSync(target, args.content, 'utf8')
|
|
931
|
+
return { node: args.node, path: args.path, bytes: Buffer.byteLength(args.content, 'utf8'), ok: true }
|
|
932
|
+
},
|
|
933
|
+
}))
|
|
934
|
+
ctx.tools.register(defineTool({
|
|
935
|
+
name: 'comfyui_reload',
|
|
936
|
+
description: 'Restart the local ComfyUI so custom-node source changes take effect: find the process listening on ' +
|
|
937
|
+
'the ComfyUI port, kill it, then relaunch via the configured launch command. Use after comfyui_edit_source, ' +
|
|
938
|
+
'then run a minimal workflow on the canvas to verify the change. Warns about running generations; cancels ' +
|
|
939
|
+
'pending work by restarting.',
|
|
940
|
+
parameters: {},
|
|
941
|
+
output: { schema: { type: 'object', additionalProperties: true }, render: renderJSON },
|
|
942
|
+
async execute(_args) {
|
|
943
|
+
const value = settingsScope?.get?.()
|
|
944
|
+
const base = baseUrl()
|
|
945
|
+
const port = (() => { try { return new URL(base).port || '8188' } catch { return '8188' } })()
|
|
946
|
+
const cmd = (value?.launchCommand || '').trim()
|
|
947
|
+
const workdir = (value?.comfyuiDir || process.env.COMFYUI_DIR || '').trim() || undefined
|
|
948
|
+
const shell = ctx.get?.('shell')
|
|
949
|
+
if (!shell || typeof shell.run !== 'function') throw new Error('shell service unavailable — cannot reload ComfyUI')
|
|
950
|
+
if (!cmd) throw new Error('launchCommand is empty — set it in Settings → ComfyUI 画布 to allow reload')
|
|
951
|
+
// netstat -ano / findstr / taskkill are Windows-specific; on other
|
|
952
|
+
// platforms the PID hunt and kill would silently no-op and the tool
|
|
953
|
+
// would report a false success. Fail loud instead of pretending.
|
|
954
|
+
if (process.platform !== 'win32') {
|
|
955
|
+
throw new Error('comfyui_reload currently supports Windows only (netstat/taskkill); on macOS/Linux restart ComfyUI manually')
|
|
956
|
+
}
|
|
957
|
+
// 1. find the PID on the ComfyUI port
|
|
958
|
+
const netstat = await shell.run(shell.resolve({ command: `netstat -ano | findstr :${port} | findstr LISTENING`, timeoutMs: 15000 }))
|
|
959
|
+
const out = (netstat.stdout?.text || netstat.stdout || '').toString()
|
|
960
|
+
const pids = [...new Set([...out.matchAll(/\s(\d+)\s*$/gm)].map(m => m[1]))]
|
|
961
|
+
// 2. kill those PIDs (best-effort; ignore failures so relaunch still runs).
|
|
962
|
+
// taskkill is a host-level operation outside the session workspace, so
|
|
963
|
+
// run it unconfined like the launch below — the default workspace-write
|
|
964
|
+
// sandbox would deny it and the old process would survive, blocking the
|
|
965
|
+
// port and making the reload look like a no-op.
|
|
966
|
+
const killed = []
|
|
967
|
+
for (const pid of pids) {
|
|
968
|
+
try {
|
|
969
|
+
await shell.run(shell.resolve({
|
|
970
|
+
command: `taskkill /PID ${pid} /F`,
|
|
971
|
+
timeoutMs: 10000,
|
|
972
|
+
sandboxPolicy: { mode: 'danger-full-access', workspaceRoot: process.cwd() },
|
|
973
|
+
}))
|
|
974
|
+
killed.push(pid)
|
|
975
|
+
} catch { /* already gone or denied — proceed */ }
|
|
976
|
+
}
|
|
977
|
+
// 3. relaunch via the same unconfined path the Start button uses
|
|
978
|
+
shell.start(shell.resolve({
|
|
979
|
+
command: cmd,
|
|
980
|
+
...(workdir ? { workdir } : {}),
|
|
981
|
+
sandboxPolicy: { mode: 'danger-full-access', workspaceRoot: process.cwd() },
|
|
982
|
+
}))
|
|
983
|
+
// 4. wait for /system_stats to come back
|
|
984
|
+
const deadline = Date.now() + 45000
|
|
985
|
+
let up = false
|
|
986
|
+
let lastErr = ''
|
|
987
|
+
while (Date.now() < deadline) {
|
|
988
|
+
try {
|
|
989
|
+
const res = await fetch(`${base}/system_stats`, { signal: AbortSignal.timeout(3000) })
|
|
990
|
+
if (res.ok) { up = true; break }
|
|
991
|
+
} catch (e) { lastErr = e instanceof Error ? e.message : String(e) }
|
|
992
|
+
await new Promise((done) => setTimeout(done, 1500))
|
|
993
|
+
}
|
|
994
|
+
return { reloaded: up, killed, port, launched: cmd, error: up ? null : (lastErr || 'timed out waiting for ComfyUI to come back') }
|
|
995
|
+
},
|
|
996
|
+
}))
|
|
997
|
+
|
|
998
|
+
// v0.1.2: register the skills (SOPs) that teach the agent HOW to use the
|
|
999
|
+
// canvas/admin tools in the right order. Runtime skills ship inside the
|
|
1000
|
+
// plugin, so `dsh plugin add` installs them with zero extra steps.
|
|
1001
|
+
const skills = ctx.skills ?? ctx.get?.('skills')
|
|
1002
|
+
if (skills && typeof skills.register === 'function') {
|
|
1003
|
+
skills.register({
|
|
1004
|
+
name: 'comfyui-canvas-ops',
|
|
1005
|
+
source: 'runtime',
|
|
1006
|
+
description: 'Operate the live ComfyUI canvas with the agent: read/edit/run workflows, fetch outputs.',
|
|
1007
|
+
whenToUse: 'Use when the user asks to make an image, edit a workflow, adjust a node, run the canvas, or fetch its outputs.',
|
|
1008
|
+
content: [
|
|
1009
|
+
'# ComfyUI 画布操作 SOP',
|
|
1010
|
+
'',
|
|
1011
|
+
'当用户要求用画布作图 / 修改工作流 / 出图时,按此顺序执行:',
|
|
1012
|
+
'',
|
|
1013
|
+
'1. comfyui_config —— 确认画布在前台(activeView=canvas)、连接正常;不在画布视图时先提示用户切到 ComfyUI 标签',
|
|
1014
|
+
'2. comfyui_read_workflow —— 读取当前图,向用户复述关键节点并确认要改什么',
|
|
1015
|
+
'3. 按需修改:comfyui_set_param(改参数)、comfyui_connect(连线)、comfyui_add_node / comfyui_remove_node(增删节点)、comfyui_inject_text(注入文本)',
|
|
1016
|
+
'4. 修改前说明改了哪些;运行前用 comfyui_debug 校验(不触发执行)',
|
|
1017
|
+
'5. comfyui_run 运行(可传 overrides 临时覆盖参数,跑完还原画布)',
|
|
1018
|
+
'6. comfyui_get_outputs 取回出图(支持 images/videos/gifs/audio)',
|
|
1019
|
+
'7. 用 vision 工具自检出图质量 → 不达标回 step 3 迭代;达标再交付',
|
|
1020
|
+
'',
|
|
1021
|
+
'安全约定:读取永远只读;写操作(改节点/运行/替换图)先向用户确认;批量用 comfyui_batch_run。',
|
|
1022
|
+
].join('\n'),
|
|
1023
|
+
})
|
|
1024
|
+
skills.register({
|
|
1025
|
+
name: 'comfyui-admin-ops',
|
|
1026
|
+
source: 'runtime',
|
|
1027
|
+
description: 'Manage the ComfyUI environment: configure, launch, upgrade, and list/install custom nodes.',
|
|
1028
|
+
whenToUse: 'Use when the user asks to configure ComfyUI, start it, update it, upgrade its nodes, or manage installed custom nodes.',
|
|
1029
|
+
content: [
|
|
1030
|
+
'# ComfyUI 环境管理 SOP',
|
|
1031
|
+
'',
|
|
1032
|
+
'当用户要求配置 / 启动 / 更新 ComfyUI、管理已装节点时,按此顺序执行:',
|
|
1033
|
+
'',
|
|
1034
|
+
'1. 配置管理 —— comfyui_config 读/改连接(baseUrl / port / networkMode / 桥接 token / 启动命令 / rail 宽度)',
|
|
1035
|
+
'2. 启动 / 状态 —— 用设置里的启动命令拉起 ComfyUI(launchRequested 机制),轮询 /system_stats 确认在线;读 launchError 排查启动失败',
|
|
1036
|
+
'3. 更新 —— comfyui_upgrade:git pull 核心 + 全部节点(scope=core/nodes/all),失败不中断其余',
|
|
1037
|
+
'4. 节点管理 —— 用文件系统工具列出 custom_nodes/、识别 git 托管与手工目录;安装/更新节点用 shell 走 git 或 pnpm',
|
|
1038
|
+
'',
|
|
1039
|
+
'安全约定:更新 / 重启 / 装节点是重操作 —— 先确认、讲清影响(如「更新 60+ 节点」「重启中断生成」);更新前检查是否有运行中任务。',
|
|
1040
|
+
].join('\n'),
|
|
1041
|
+
})
|
|
1042
|
+
skills.register({
|
|
1043
|
+
name: 'comfyui-video-audio-ops',
|
|
1044
|
+
source: 'runtime',
|
|
1045
|
+
description: 'Create video with audio/voiceover on ComfyUI: generate video, attach audio, fetch the merged file.',
|
|
1046
|
+
whenToUse: 'Use when the user wants a video with sound / voiceover / music track produced through the ComfyUI canvas.',
|
|
1047
|
+
content: [
|
|
1048
|
+
'# ComfyUI 音画创作 SOP',
|
|
1049
|
+
'',
|
|
1050
|
+
'当用户要求「视频 + 配音 / 音轨 / 音乐」时,按此顺序执行:',
|
|
1051
|
+
'',
|
|
1052
|
+
'1. 生成视频 —— 读画布上的视频工作流(如 Wan / Kling / LTX),comfyui_run 运行',
|
|
1053
|
+
'2. 生成音频 —— 用 comfyui_attach_file 把 TTS 生成的 wav / 音轨文件上传进 ComfyUI input/(供 LoadAudio 节点)',
|
|
1054
|
+
'3. 音画合成 —— 走 VideoAddAudio 或对应音画工作流,comfyui_run 合并',
|
|
1055
|
+
'4. comfyui_get_outputs 取回含音轨的视频文件(URL/路径)',
|
|
1056
|
+
'5. 交付时给出文件路径/URL;DSH 不播放,用户本地打开',
|
|
1057
|
+
'',
|
|
1058
|
+
'提示:TTS 音频只是「传输的一类文件」,上传即用;播放能力由 DSH 生态负责,本插件只做传输。',
|
|
1059
|
+
].join('\n'),
|
|
1060
|
+
})
|
|
1061
|
+
skills.register({
|
|
1062
|
+
name: 'comfyui-dev-ops',
|
|
1063
|
+
source: 'runtime',
|
|
1064
|
+
description: 'Develop or debug a ComfyUI custom node from the conversation: read its source, edit it, reload ComfyUI, and verify on the canvas.',
|
|
1065
|
+
whenToUse: 'Use when the user wants to develop, modify, or debug a ComfyUI custom node (including this plugin\'s bridge node) — read/edit its source, restart ComfyUI, or verify a change on the canvas.',
|
|
1066
|
+
content: [
|
|
1067
|
+
'# ComfyUI 节点开发调试 SOP',
|
|
1068
|
+
'',
|
|
1069
|
+
'当用户要求开发 / 修改 / 调试 ComfyUI 自定义节点(含本插件的桥接节点)时,按此循环执行:',
|
|
1070
|
+
'',
|
|
1071
|
+
'1. comfyui_read_source —— 读目标节点源码(列出目录树或读单个文件,如 __init__.py / entry/bridge.js),先弄清结构与注册方式',
|
|
1072
|
+
'2. comfyui_edit_source —— 修改源码(受控写文件,仅限该节点目录内)',
|
|
1073
|
+
'3. comfyui_reload —— 重启 ComfyUI 使改动生效(会中断运行中任务,先确认);等 /system_stats 恢复在线',
|
|
1074
|
+
'4. 画布验证 —— 在画布上搭一个用到该节点的最小工作流,comfyui_run 运行',
|
|
1075
|
+
'5. comfyui_get_outputs 看结果 → 不达标回 step 2 迭代',
|
|
1076
|
+
'',
|
|
1077
|
+
'安全约定:改的是 custom_nodes/ 下的本机源码 —— 写操作先确认、讲清改哪个文件;重启前检查是否有运行中任务;改坏了节点可能让 ComfyUI 起不来(保留备份 / 可回退)。',
|
|
1078
|
+
].join('\n'),
|
|
1079
|
+
})
|
|
1080
|
+
}
|
|
726
1081
|
}
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-comfyui-canvas",
|
|
3
|
-
"version": "0.1.
|
|
3
|
+
"version": "0.1.2",
|
|
4
4
|
"description": "Give DeepSeek Harness agents live control of your ComfyUI canvas (local or cloud): embed it as a split-screen tab, then read/edit/run workflows, debug errors, fetch output images, and batch-sweep parameters straight from the chat. Ships the ComfyUI-side bridge node.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"main": "lib/index.js",
|