dsh-comfyui-canvas 0.1.2 → 0.1.3

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 CHANGED
@@ -3,7 +3,7 @@
3
3
  > [中文](README.zh.md) · English
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
- [![Version](https://img.shields.io/badge/version-0.1.2-brightgreen.svg)](https://github.com/wbin0001/dsh-comfyui-canvas/releases)
6
+ [![Version](https://img.shields.io/badge/version-0.1.3-brightgreen.svg)](https://github.com/wbin0001/dsh-comfyui-canvas/releases)
7
7
  [![GitHub Stars](https://img.shields.io/github/stars/wbin0001/dsh-comfyui-canvas.svg?style=social)](https://github.com/wbin0001/dsh-comfyui-canvas)
8
8
  [![DSH](https://img.shields.io/badge/DeepSeek_Harness-compatible-blueviolet.svg)](https://github.com/DeepSeek-Harness/DSH)
9
9
  [![ComfyUI](https://img.shields.io/badge/ComfyUI-0.34+-orange.svg)](https://github.com/comfyanonymous/ComfyUI)
@@ -28,11 +28,13 @@ This package is the DSH-side plugin, and it ships the ComfyUI-side bridge node t
28
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). |
29
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`. |
30
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. |
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. |
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
+ | **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 — with optional `outputStem` auto-incrementing names (`stem.01.png`, never overwrites); `comfyui_attach_file` uploads any local file (image/audio/video/3D/text) 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
+ | **Projects & traceability** | Downloads default to the project directory (Settings → 项目目录, default `<workspace>/projects`); every downloaded run appends `runs.json` (promptId / overrides / timestamp / files) so any output can be traced back to its parameters. Failed runs return a structured `executionError` (node id / node type / exception / message) instead of a raw JSON wall. |
33
+ | **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), `comfyui-video-audio-ops` (video + voiceover/audio track), and `comfyui-dev-ops` (develop/debug custom nodes). Install the plugin and the skills ship with it — no extra setup. |
34
+ | **Upkeep tool** | `comfyui_upgrade` one-click updates the ComfyUI core and every git-backed custom node (concurrent, dirty-safe); `comfyui_config` reports the active connection, canvas focus, project directory, and a bridge-auth handshake check (`bridgeAuthEffective`). |
35
+ | **Node dev tools** | `comfyui_read_source` / `comfyui_edit_source` / `comfyui_reload` — read and edit custom-node source under custom_nodes/ and restart ComfyUI from the conversation, then verify on the canvas. |
34
36
  | **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. |
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
+ | **Settings page** | ComfyUI base URL / port / network mode / bridge token / launch command / project directory / rail width. Changes apply live. Customized nav icon with ComfyUI logo. |
36
38
  | **Rail polish** | Image previews inside the input box, a `+` button to attach local images (DSH's official attachment path), approval popup over the canvas (split layout), send button pinned to the panel corner. |
37
39
 
38
40
  ## Install
@@ -186,7 +188,8 @@ The **bridge** is the only ComfyUI-side dependency. It exposes `/dsh-bridge/work
186
188
 
187
189
  ## Known issues
188
190
 
189
- _(None pending. The former "node previews missing after a run" is fixed in v0.1.1: removed the iframe's `referrerpolicy="no-referrer"` to match the native tab environment, and `bridge.js` now listens to ComfyUI's `executed` event to force a canvas redraw.)_
191
+ - `comfyui_reload` currently supports **Windows only** (uses `netstat`/`taskkill`); on macOS/Linux it fails loud instead of pretending, restart ComfyUI manually there.
192
+ - The former "node previews missing after a run" is fixed in v0.1.1: removed the iframe's `referrerpolicy="no-referrer"` to match the native tab environment, and `bridge.js` now listens to ComfyUI's `executed` event to force a canvas redraw.
190
193
 
191
194
  ## License
192
195
 
package/README.zh.md CHANGED
@@ -3,7 +3,7 @@
3
3
  > 中文 · [English](README.md)
4
4
 
5
5
  [![License: MIT](https://img.shields.io/badge/license-MIT-blue.svg)](LICENSE)
6
- [![Version](https://img.shields.io/badge/version-0.1.2-brightgreen.svg)](https://github.com/wbin0001/dsh-comfyui-canvas/releases)
6
+ [![Version](https://img.shields.io/badge/version-0.1.3-brightgreen.svg)](https://github.com/wbin0001/dsh-comfyui-canvas/releases)
7
7
  [![GitHub Stars](https://img.shields.io/github/stars/wbin0001/dsh-comfyui-canvas.svg?style=social)](https://github.com/wbin0001/dsh-comfyui-canvas)
8
8
  [![DSH](https://img.shields.io/badge/DeepSeek_Harness-compatible-blueviolet.svg)](https://github.com/DeepSeek-Harness/DSH)
9
9
  [![ComfyUI](https://img.shields.io/badge/ComfyUI-0.34+-orange.svg)](https://github.com/comfyanonymous/ComfyUI)
@@ -28,12 +28,14 @@
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` 把产物直接带回对话——图像、视频、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`(视频+配音/音轨)。装插件即自带技能,无需额外配置。 |
33
- | **维护工具** | `comfyui_upgrade` 一键升级 ComfyUI 核心与全部 git 自定义节点;`comfyui_config` 报告当前连接与画布专注状态。 |
31
+ | **生产工具** | `comfyui_batch_run` 一次扫参数矩阵(seed / prompt / 强度);`comfyui_get_outputs` 把产物直接带回对话——图像、视频、GIF、音频都支持,可传 `outputStem` 自动编号(`stem.01.png`,永不覆盖);`comfyui_attach_file` 把本机任意文件(图片/音频/视频/3D/**文本**)上传进 ComfyUI `input/` 供对应 Load 节点使用;`comfyui_export_api` 把当前画布导出为 API 格式工作流,供 comfy-cli 无人值守批量。 |
32
+ | **项目目录与溯源** | 下载默认落「项目目录」(设置页可改,默认 `<工作区>/projects`);每次下载把 `runs.json`(promptId / overrides / 时间戳 / 文件)追加进项目目录,任意一张图都能追溯回它的生成参数。运行失败返回结构化 `executionError`(节点 id / 节点类型 / 异常类型 / 信息),不再是一堵 JSON 墙。 |
33
+ | **技能包(SOP)** | 内置技能教 agent 按正确顺序操作:`comfyui-canvas-ops`(读→确认→改→跑→取回→自检)、`comfyui-admin-ops`(配置/启动/升级/节点管理)、`comfyui-video-audio-ops`(视频+配音/音轨)、`comfyui-dev-ops`(开发/调试自定义节点)。装插件即自带技能,无需额外配置。 |
34
+ | **维护工具** | `comfyui_upgrade` 一键升级 ComfyUI 核心与全部 git 自定义节点(并发、跳过本地改过的仓库);`comfyui_config` 报告当前连接、画布专注状态、项目目录,以及**桥接鉴权握手检查**(`bridgeAuthEffective`)。 |
35
+ | **节点开发工具** | `comfyui_read_source` / `comfyui_edit_source` / `comfyui_reload`——对话里直接读写 `custom_nodes/` 下的节点源码并重启 ComfyUI,再到画布上验证。 |
34
36
  | **画布专注模式(会话隔离)** | agent 通过 `comfyui_config` 感知当前会话是否在画布标签,只在画布场景专注画布操作,且**按会话隔离**——多个会话互不干扰。 |
35
37
  | **ComfyUI 报错处理** | `debug` 校验工作流并高亮报错节点(纯校验,不触发执行),agent 帮你定位/修复画布错误。 |
36
- | **设置页** | ComfyUI 地址 / 端口 / 网络模式 / 桥接 Token / 启动命令 / 右侧面板宽度,实时生效。导航栏已自定义为 ComfyUI logo 图标。 |
38
+ | **设置页** | ComfyUI 地址 / 端口 / 网络模式 / 桥接 Token / 启动命令 / 项目目录 / 右侧面板宽度,实时生效。导航栏已自定义为 ComfyUI logo 图标。 |
37
39
  | **对话栏增强** | 图片预览并入输入框、`+` 号上传本地图片(走 DSH 官方附件通道)、画布上的授权弹窗、发送按钮贴右下角。 |
38
40
 
39
41
  ---
@@ -208,7 +210,8 @@ dsh-comfyui-canvas/
208
210
 
209
211
  ## 已知问题
210
212
 
211
- _(当前无已知未决问题。此前"画布运行后节点预览不显示"已在 v0.1.1 修复:移除 iframe 的 `referrerpolicy="no-referrer"` 消除环境差异,并在 `bridge.js` 监听 ComfyUI `executed` 事件强制重绘画布。)_
213
+ - `comfyui_reload` 目前**仅支持 Windows**(依赖 `netstat`/`taskkill`);macOS/Linux 上会明确报错而非假装成功,需手动重启 ComfyUI。
214
+ - 此前"画布运行后节点预览不显示"已在 v0.1.1 修复:移除 iframe 的 `referrerpolicy="no-referrer"` 消除环境差异,并在 `bridge.js` 监听 ComfyUI `executed` 事件强制重绘画布。
212
215
 
213
216
  ---
214
217
 
@@ -0,0 +1,101 @@
1
+ # dsh-comfyui-canvas v0.1.3 方案
2
+
3
+ > 目标:从「画布遥控器」进化为「可复现的创作工作台」——补齐运行溯源、输出不覆盖、项目目录三大能力(P0),加画布文件上传通道(A3,v0.1.2 搁置项)与失败结构化诊断(P1)。
4
+ > 依据:第三方评估报告(2026-09-02)的 6 项优化建议 + v0.1.2 收尾时搁置的 A3。
5
+
6
+ ## 1. P0-1:运行溯源 `.runs.json`(~30 行)
7
+
8
+ **问题**:`comfyui_run` 返回 promptId 后,批量跑多个 seed/prompt 变体,无法追溯「哪张图对应什么参数」。
9
+
10
+ **方案**:host 端在每次 run/batch_run 时记录 `{ promptId, overrides, timestamp, status }` 到内存;`get_outputs` 下载时把记录 append 到 `downloadDir/<workflow_name>.runs.json`(无则新建)。
11
+
12
+ ```json
13
+ [
14
+ { "run": 1, "promptId": "abc123", "overrides": [{"nodeId":6,"key":"seed","value":42}],
15
+ "timestamp": "2026-09-02T22:00:00Z", "status": "success", "files": ["char_ref.01.png"] }
16
+ ]
17
+ ```
18
+
19
+ **价值**:可复现是创作的命脉。agent 读 `.runs.json` 对比历史运行,用户追溯任意一张图的来源参数。
20
+
21
+ ## 2. P0-2:输出不覆盖命名(~20 行)
22
+
23
+ **问题**:ComfyUI `output/` 用自带命名规则,批量跑后文件混乱、可能互相覆盖。
24
+
25
+ **方案**:`get_outputs` 下载时新增 `outputStem` 参数(可选)。扫描 `downloadDir` 已有 `<stem>.NN.<ext>` 文件,NN 自增不覆盖。
26
+
27
+ **价值**:批量跑 20 个变体后文件名直接反映顺序,永不覆盖。
28
+
29
+ ## 3. P0-3:项目目录概念(~15 行)
30
+
31
+ **问题**:导出的 API JSON、下载的图片、溯源文件散落各处。
32
+
33
+ **方案**:ConfigSchema 新增 `projectDir` 字段。`get_outputs` 下载、`export_api` 导出、`.runs.json` 默认落在 `projectDir/<workflow_name>/`;未设时回退现有行为(不破坏兼容)。
34
+
35
+ **价值**:一个创作项目的所有资产(工作流/输出/溯源)聚在一起,agent 用文件工具管理整个项目。
36
+
37
+ ## 4. A3:画布会话框「+ 文件」通道 —— 已砍(2026-09-03 决策)
38
+
39
+ **原内容**:画布会话框加「+ 文件」按钮,直接传本地文件进 ComfyUI `input/`。
40
+
41
+ **硬前置**:浏览器 `<input type="file">` 给的是 File 对象、不是绝对路径,而 `attach_file` 收 `path`——需先确认 client → host 传参通道(v0.1.2 方案开放问题 #1)。
42
+
43
+ **砍的原因**:A1(host 侧 `attach_file`)已让 agent 能传任意文件(含文本/提示词),A3 只是「手动点按钮直传」的锦上添花,浏览器路径传递是硬前置、收益边际低。**留 v0.2+ 或不做**。若有需要,降级方案:agent 引导用户给路径,无需 UI。
44
+
45
+ ## 5. P1:运行失败结构化诊断(~40 行)
46
+
47
+ **问题**:`run` 失败只返回 HTTP 错误或 `node_errors`,无法定位具体节点异常。
48
+
49
+ **方案**:`run` 的 sendCommand 返回后,若 nodeErrors 非空,自动 GET `/history/{promptId}` 提取 `execution_error`,结构化返回:
50
+
51
+ ```json
52
+ {
53
+ "promptId": "abc123", "success": false,
54
+ "nodeErrors": {"7": ["missing required input 'model'"]},
55
+ "executionError": { "nodeId": 7, "nodeType": "CheckpointLoaderSimple",
56
+ "exception": "FileNotFoundError", "message": "Checkpoint file not found: sd_xl_base.safetensors" }
57
+ }
58
+ ```
59
+
60
+ **价值**:agent 直接定位「节点 7 缺 checkpoint」并修复,不用看一堆 JSON。
61
+
62
+ ## 6. 本版顺手修复
63
+
64
+ - ✅ `get_outputs` 无限超时(已修):history fetch 15s、文件下载 fetch 30s 超时(`AbortSignal.any([exec.signal, timeout])`)
65
+ - 桥接鉴权告警(设置页 + 画布激活,host 代理探测)——已实现(v0.1.2 后补,随 v0.1.3 发布)
66
+ - ✅ `attach_file` 文本/提示词上传(`d58d302`):mediaType 映射新增 txt/md/json/csv/yaml/yml/srt——提示词/批量提示词 JSON 等文本可直接进 ComfyUI `input/` 供文本类 Load 节点消费
67
+
68
+ ### 文件上传支持矩阵(`attach_file`,2026-09-03)
69
+
70
+ | 类别 | 扩展名 | mediaType |
71
+ |---|---|---|
72
+ | 图片 | png / jpg / jpeg / webp / gif | image/* |
73
+ | 音频(含音乐) | wav / mp3 / ogg / flac / m4a / aac | audio/* |
74
+ | 视频 | mp4 / webm / mov / mkv / avi | video/* |
75
+ | 3D | glb / gltf / obj / fbx / stl | model/* |
76
+ | **文本/提示词** | txt / md / json / csv / yaml / yml / srt | text/plain、application/json、text/markdown、text/csv、application/yaml、application/x-subrip |
77
+ | 其他 | 任意 | application/octet-stream 透传 |
78
+
79
+ ## 不做清单(范围控制)
80
+
81
+ - ❌ WebSocket 推送替代轮询(P2,改动大,收益中等,留 v0.2)
82
+ - ❌ 工作流链式组合(P1,单链封装 set_param+run,可用性待验证)
83
+ - ❌ src/ + 构建管线、CI、DOM 选择器集中管理(工程化,独立排期)
84
+ - ❌ `/dsh-bridge/report` 鉴权(localhost-only 已可接受,远期选项)
85
+
86
+ ## 实施步骤
87
+
88
+ ```
89
+ step 1 P0-1 `.runs.json`:host 记录 run/batch_run 元数据 → get_outputs 下载时 flush
90
+ step 2 P0-2 输出不覆盖命名:get_outputs 加 outputStem,NN 自增
91
+ step 3 P0-3 项目目录:ConfigSchema 加 projectDir → get_outputs/export_api 默认路径
92
+ step 4 P1 失败诊断:run 失败自动拉 /history/{pid} execution_error
93
+ step 5 A3 画布上传通道:探 client→host 传参通道;不可行则降级/砍
94
+ step 6 README 更新(P0 三项 + 新工具/参数说明)+ 版本号 0.1.2 → 0.1.3
95
+ step 7 npm run check → 同步三处 → 提交推送 → npm publish → GitHub Release
96
+ ```
97
+
98
+ ## 参考
99
+
100
+ - 第三方评估报告(2026-09-02):6 项优化建议,P0 三项 ~65 行
101
+ - v0.1.2 方案 `docs/PLAN-v0.1.2.md` §4.5「剩余」:A3 移入 v0.1.3
package/lib/client.js CHANGED
@@ -40,7 +40,6 @@ function setSessionView(sc, sessionId, mode) {
40
40
  if (keys.length > MAX_SESSION_VIEWS) {
41
41
  // Drop oldest keys (object string-key order = insertion order), always
42
42
  // keeping the current session.
43
- var drop = keys.length - MAX_SESSION_VIEWS;
44
43
  var keep = {};
45
44
  var currentKey = String(sessionId);
46
45
  var kept = 0;
@@ -657,6 +656,34 @@ function ComfyUICanvasView(props) {
657
656
  });
658
657
  };
659
658
 
659
+ // 桥接鉴权握手(画布激活时按需触发,非启动路径):浏览器跨域读不到
660
+ // ComfyUI 状态码(CORS),所以向 host 请求一次代理探测——写
661
+ // probeRequested=true,host watcher 后台探测并把结果写回 bridgeProbe。
662
+ // 这里只负责发起 + 订阅展示,全程异步,不影响任何启动流程。
663
+ var probeState = React.useState(null); // {bridgeAuthEffective, bridgeAuthProbe, checkedAt}
664
+ var bridgeProbe = probeState[0];
665
+ var setBridgeProbe = probeState[1];
666
+ var tokenSetState = React.useState(false);
667
+ var tokenSet = tokenSetState[0];
668
+ var setTokenSet = tokenSetState[1];
669
+ React.useEffect(function () {
670
+ var sc = props.scope;
671
+ if (!sc) return;
672
+ if (typeof sc.set === "function") {
673
+ sc.set("probeRequested", true).catch(function () {});
674
+ }
675
+ if (typeof sc.subscribe !== "function" || typeof sc.getSnapshot !== "function") return;
676
+ var sync = function () {
677
+ var snap = sc.getSnapshot();
678
+ var v = snap && snap.value ? snap.value : {};
679
+ setBridgeProbe(v.bridgeProbe || null);
680
+ setTokenSet(!!(v.bridgeToken));
681
+ };
682
+ sync();
683
+ var unsub = sc.subscribe(sync);
684
+ return function () { if (typeof unsub === "function") unsub(); };
685
+ }, [sessionId]);
686
+
660
687
  // 启动超时:host 用 launchCommand 拉起 ComfyUI 通常几十秒内完成;超过
661
688
  // 45 秒仍未探测到在线就复位"正在启动…",允许用户重试,避免永久卡死。
662
689
  React.useEffect(function () {
@@ -677,6 +704,20 @@ function ComfyUICanvasView(props) {
677
704
  // 授权弹窗渲染在 host 内部(与控制条同级),absolute 定位锚定 host 的
678
705
  // relative 边界 → 只盖画布视图区,不会挡住顶栏/右侧 rail。
679
706
  var hostChildren = [control];
707
+ // 桥接鉴权告警(非启动路径,画布激活时按需展示):只有当 DSH 侧配了
708
+ // token、但桥接握手显示"错误 token 也能通过"时才亮红字——这是真正的
709
+ // 配置错位(ComfyUI 端没设 DSH_BRIDGE_TOKEN)。正常/未配 token 不打扰。
710
+ if (bridgeProbe && bridgeProbe.bridgeAuthEffective === false && tokenSet) {
711
+ hostChildren.push(React.createElement("div", {
712
+ style: {
713
+ position: "absolute", top: "8px", left: "50%", transform: "translateX(-50%)",
714
+ zIndex: 45, maxWidth: "86%",
715
+ background: "rgba(248,81,73,.12)", color: "#f85149",
716
+ border: "1px solid rgba(248,81,73,.4)", borderRadius: "6px",
717
+ padding: "6px 12px", fontSize: "12px", lineHeight: "1.5", textAlign: "center",
718
+ },
719
+ }, "⚠️ 桥接鉴权未生效:ComfyUI 端可能没设 DSH_BRIDGE_TOKEN。打开设置页 → ComfyUI 画布 查看详情。"));
720
+ }
680
721
  if (approval) {
681
722
  hostChildren.push(React.createElement(ComfyUIApprovalOverlay, { wait: approval }));
682
723
  }
@@ -702,6 +743,7 @@ function ComfyUISettingsRow(props) {
702
743
  var launchCommand = typeof value.launchCommand === "string" ? value.launchCommand : "";
703
744
  var comfyuiDir = typeof value.comfyuiDir === "string" ? value.comfyuiDir : "";
704
745
  var railWidth = value.railWidth != null ? Number(value.railWidth) : DEFAULT_RAIL;
746
+ var projectDir = typeof value.projectDir === "string" ? value.projectDir : "";
705
747
  var writable = !snapshot || snapshot.writable !== false;
706
748
 
707
749
  var inputStyle = {
@@ -719,6 +761,33 @@ function ComfyUISettingsRow(props) {
719
761
  scope.set(field, next).catch(function () { /* recover handled by scope */ });
720
762
  }
721
763
 
764
+ // 桥接鉴权握手(非启动路径):设置页打开 / token 变化时向 host 请求一次
765
+ // 探测(probeRequested=true → host 后台探测 → 结果写回 bridgeProbe)。
766
+ // host 代理是因为浏览器跨域读不到 ComfyUI 的状态码(CORS),而 host 端
767
+ // fetch 没有此限制。这里只负责"发起请求 + 展示结果",不阻塞任何启动流程。
768
+ // 输入 token 时防抖 600ms,避免每个字符触发一次探测。
769
+ React.useEffect(function () {
770
+ if (!bridgeToken) return;
771
+ var timer = setTimeout(function () {
772
+ if (scope && typeof scope.set === "function") {
773
+ scope.set("probeRequested", true).catch(function () {});
774
+ }
775
+ }, 600);
776
+ return function () { clearTimeout(timer); };
777
+ }, [bridgeToken]);
778
+ var probe = value.bridgeProbe || {};
779
+ var probeText = "";
780
+ var probeColor = "#9a9aa2";
781
+ if (probe.bridgeAuthEffective === true) {
782
+ probeText = "✅ 桥接鉴权已生效(错误 Token 会被拒绝)";
783
+ probeColor = "#3fb950";
784
+ } else if (probe.bridgeAuthEffective === false) {
785
+ probeText = bridgeToken
786
+ ? "⚠️ 桥接鉴权未生效:ComfyUI 端可能没设 DSH_BRIDGE_TOKEN(检查启动器)"
787
+ : "ℹ️ 未配置桥接 Token —— 桥接不鉴权(ComfyUI 默认信任模型)";
788
+ probeColor = bridgeToken ? "#f85149" : "#d29922";
789
+ }
790
+
722
791
  return React.createElement("div", { style: rowStyle },
723
792
  React.createElement("div", { style: titleStyle }, "ComfyUI 画布"),
724
793
  React.createElement("label", { style: labelStyle }, "ComfyUI 地址"),
@@ -762,12 +831,21 @@ function ComfyUISettingsRow(props) {
762
831
  spellCheck: false, placeholder: "例如 C:\\path\\to\\ComfyUI",
763
832
  onChange: function (e) { setField("comfyuiDir", e.target.value); },
764
833
  }),
834
+ React.createElement("label", { style: labelStyle }, "项目目录(输出/导出/溯源默认落此;留空用 <工作区>/projects)"),
835
+ React.createElement("input", {
836
+ style: inputStyle, value: projectDir, disabled: !writable,
837
+ spellCheck: false, placeholder: "例如 D:\\AI-Projects(留空 = 工作区 projects/)",
838
+ onChange: function (e) { setField("projectDir", e.target.value); },
839
+ }),
765
840
  React.createElement("label", { style: labelStyle }, "右侧面板宽度 (px)(画布宽度 = 总宽 − 右侧面板)"),
766
841
  React.createElement("input", {
767
842
  style: inputStyle, value: String(railWidth), disabled: !writable,
768
843
  type: "number", min: 120, max: 1200, step: 10,
769
844
  onChange: function (e) { setField("railWidth", Number(e.target.value)); },
770
845
  }),
846
+ probeText ? React.createElement("div", {
847
+ style: { fontSize: "12px", color: probeColor, marginTop: "10px", lineHeight: "1.5" },
848
+ }, probeText) : null,
771
849
  );
772
850
  }
773
851
 
package/lib/index.js CHANGED
@@ -15,6 +15,7 @@
15
15
  import { defineTool } from '@deepseek-ai/dsh-tools'
16
16
  import z from '@deepseek-ai/schemastery'
17
17
  import { settingsNamespace } from '@deepseek-ai/dsh-settings'
18
+ import path from 'node:path'
18
19
 
19
20
  export const name = 'dsh-comfyui-canvas'
20
21
  export const inject = ['tools', 'settings', 'shell', 'skills']
@@ -44,6 +45,12 @@ const ConfigSchema = z.object({
44
45
  canvasScale: z.number().default(80),
45
46
  /** Last active view across sessions (legacy echo of activeViewBySession). */
46
47
  activeView: z.string().default('chat'),
48
+ /**
49
+ * Project directory for outputs/export/traceability. When empty, downloads
50
+ * and exports default to `<DSH workspace>/projects/<workflow_name>/`.
51
+ * Users with their own organization may point it anywhere (e.g. D:\AI-Projects).
52
+ */
53
+ projectDir: z.string().default(''),
47
54
  /**
48
55
  * Optional shared secret for the ComfyUI bridge. When set, every agent
49
56
  * request carries `Authorization: Bearer <token>`, and the bridge only
@@ -72,6 +79,20 @@ const ConfigSchema = z.object({
72
79
  * current session's value so the agent knows whether to focus on canvas work.
73
80
  */
74
81
  activeViewBySession: z.dict(z.union([z.const('canvas'), z.const('chat')])).default({}),
82
+ /**
83
+ * One-shot probe request: the client settings page / canvas tab sets this
84
+ * true when it wants a fresh bridge-auth handshake; the host watcher runs
85
+ * the probe (host side has no CORS problem) and writes the result into
86
+ * `bridgeProbe`, then clears this flag. Kept out of the startup path —
87
+ * probing is on-demand, async, and never blocks DSH boot.
88
+ */
89
+ probeRequested: z.boolean().default(false),
90
+ /** Last bridge-auth probe result, written by the host watcher. */
91
+ bridgeProbe: z.object({
92
+ bridgeAuthEffective: z.union([z.boolean(), z.const(null)]),
93
+ bridgeAuthProbe: z.string(),
94
+ checkedAt: z.number(),
95
+ }).default({ bridgeAuthEffective: null, bridgeAuthProbe: 'not-probed', checkedAt: 0 }),
75
96
  })
76
97
 
77
98
  function renderJSON(_args, value) {
@@ -88,6 +109,9 @@ const MEDIA_TYPE_BY_EXT = {
88
109
  mp4: 'video/mp4', webm: 'video/webm', mov: 'video/quicktime', mkv: 'video/x-matroska', avi: 'video/x-msvideo',
89
110
  // 3D
90
111
  glb: 'model/gltf-binary', gltf: 'model/gltf+json', obj: 'model/obj', fbx: 'model/fbx', stl: 'model/stl',
112
+ // text / prompts / structured inputs
113
+ txt: 'text/plain', md: 'text/markdown', srt: 'application/x-subrip',
114
+ json: 'application/json', csv: 'text/csv', yaml: 'application/yaml', yml: 'application/yaml',
91
115
  }
92
116
  function mediaTypeOf(ext) {
93
117
  return MEDIA_TYPE_BY_EXT[ext] ?? null
@@ -100,6 +124,86 @@ function bridgeHeaders(token) {
100
124
  : { 'Content-Type': 'application/json' }
101
125
  }
102
126
 
127
+ /**
128
+ * P0 handshake: does the BRIDGE actually enforce the token? The DSH-side
129
+ * setting alone proves nothing — if the ComfyUI side never got
130
+ * DSH_BRIDGE_TOKEN, _authorized() always passes and the bridge is open.
131
+ * Probe with the correct token and a deliberately wrong one: both succeeding
132
+ * means auth is NOT effective on the wire. Shared by comfyui_config and the
133
+ * settings watcher (the client cannot probe cross-origin, so the host probes
134
+ * on the client's behalf).
135
+ */
136
+ async function probeBridgeAuth(base, token) {
137
+ const result = {
138
+ bridgeAuthEffective: null,
139
+ bridgeAuthProbe: 'not-probed',
140
+ checkedAt: Date.now(),
141
+ }
142
+ try {
143
+ const good = await fetch(`${base}/dsh-bridge/workflow`, {
144
+ headers: token ? { Authorization: `Bearer ${token}` } : {},
145
+ signal: AbortSignal.timeout(4000),
146
+ })
147
+ const bad = token
148
+ ? await fetch(`${base}/dsh-bridge/workflow`, {
149
+ headers: { Authorization: 'Bearer dsh-bt-__wrong-probe__' },
150
+ signal: AbortSignal.timeout(4000),
151
+ })
152
+ : good
153
+ if (!good.ok) {
154
+ result.bridgeAuthProbe = `unreachable-or-error: HTTP ${good.status}`
155
+ } else if (!token) {
156
+ // No token configured on either side — open by design but flag it.
157
+ result.bridgeAuthEffective = false
158
+ result.bridgeAuthProbe = 'no token configured on DSH side'
159
+ } else {
160
+ result.bridgeAuthEffective = bad.status === 401
161
+ result.bridgeAuthProbe = bad.status === 401
162
+ ? 'bridge rejects bad token (auth effective)'
163
+ : 'bridge accepted a WRONG token — auth NOT effective on the ComfyUI side'
164
+ }
165
+ } catch (e) {
166
+ result.bridgeAuthProbe = `probe failed: ${e instanceof Error ? e.message : String(e)}`
167
+ }
168
+ return result
169
+ }
170
+
171
+ /**
172
+ * P1: extract a structured execution error from a ComfyUI history item, so a
173
+ * failed run reports "node 7 CheckpointLoaderSimple → FileNotFoundError" instead
174
+ * of a raw JSON wall. ComfyUI stores it under status.execution_error (newer) or
175
+ * status.messages[] entries of type "execution_error" (classic). Returns null
176
+ * when the run had no execution error.
177
+ */
178
+ function extractExecutionError(item) {
179
+ const status = item?.status
180
+ if (!status) return null
181
+ if (status.execution_error && typeof status.execution_error === 'object') {
182
+ const e = status.execution_error
183
+ return {
184
+ nodeId: e.node_id ?? null,
185
+ nodeType: e.node_type ?? null,
186
+ exceptionType: e.exception_type ?? null,
187
+ message: e.exception_message ?? null,
188
+ traceback: Array.isArray(e.traceback) ? e.traceback.join('\n').slice(0, 2000) : (e.traceback ?? null),
189
+ }
190
+ }
191
+ if (Array.isArray(status.messages)) {
192
+ for (const [type, payload] of status.messages) {
193
+ if (type === 'execution_error' && payload && typeof payload === 'object') {
194
+ return {
195
+ nodeId: payload.node_id ?? null,
196
+ nodeType: payload.node_type ?? null,
197
+ exceptionType: payload.exception_type ?? null,
198
+ message: payload.exception_message ?? null,
199
+ traceback: Array.isArray(payload.traceback) ? payload.traceback.join('\n').slice(0, 2000) : (payload.traceback ?? null),
200
+ }
201
+ }
202
+ }
203
+ }
204
+ return null
205
+ }
206
+
103
207
  /** POST a write command to the bridge and poll for the frontend result. */
104
208
  async function sendCommand(baseUrl, token, cmd, payload, signal, { pollMs = 400, timeoutMs = 8000 } = {}) {
105
209
  const started = Date.now()
@@ -134,6 +238,36 @@ export function apply(ctx) {
134
238
  // activeView read); undefined when the settings service is unavailable.
135
239
  let settingsScope = undefined
136
240
 
241
+ // v0.1.3: in-memory run ledger for `.runs.json` traceability — keyed by
242
+ // prompt_id, recording the overrides each run was started with so downloads
243
+ // can answer "which parameters produced this image". Bounded: DSH runs are
244
+ // long-lived and every run adds an entry, so cap it and drop the oldest
245
+ // (Map preserves insertion order).
246
+ const runLedger = new Map()
247
+ const RUN_LEDGER_MAX = 500
248
+ const recordRun = (promptId, overrides, status) => {
249
+ runLedger.set(String(promptId), {
250
+ overrides: Array.isArray(overrides) ? overrides : [],
251
+ timestamp: new Date().toISOString(),
252
+ status,
253
+ })
254
+ if (runLedger.size > RUN_LEDGER_MAX) {
255
+ const oldest = runLedger.keys().next().value
256
+ if (oldest !== undefined) runLedger.delete(oldest)
257
+ }
258
+ }
259
+
260
+ // v0.1.3: resolve the project directory — explicit setting wins, otherwise
261
+ // default to `<DSH workspace>/projects`. This is where get_outputs downloads
262
+ // land and `.runs.json` is written, so one project's assets stay together
263
+ // (outputs, export JSON, trace) without touching ComfyUI's own output/.
264
+ const resolveProjectDir = () => {
265
+ const value = settingsScope?.get?.()
266
+ const explicit = (value?.projectDir || '').trim()
267
+ if (explicit) return explicit
268
+ return path.join(process.cwd(), 'projects')
269
+ }
270
+
137
271
  // M3: prefer the settings namespace when the settings service is present.
138
272
  // Registration is optional so missing settings never blocks startup.
139
273
  const settings = ctx.settings ?? ctx.get?.('settings')
@@ -205,6 +339,30 @@ export function apply(ctx) {
205
339
  }
206
340
  })()
207
341
  })
342
+
343
+ // Bridge-auth probe watcher: the client settings page / canvas tab sets
344
+ // probeRequested=true when it wants a fresh handshake; the host probes
345
+ // (host-side fetch has no CORS issue — the browser cannot read ComfyUI's
346
+ // status cross-origin, which is why the client asks us to do it) and
347
+ // writes the verdict back into `bridgeProbe`, then clears the flag.
348
+ let probing = false
349
+ scope.watch(() => {
350
+ const value = scope.get()
351
+ if (!value?.probeRequested || probing) return
352
+ probing = true
353
+ void (async () => {
354
+ try {
355
+ const token = (value?.bridgeToken || '').trim()
356
+ const base = resolveBase()
357
+ const result = await probeBridgeAuth(base, token)
358
+ try {
359
+ scope.update({ bridgeProbe: result, probeRequested: false })
360
+ } catch { /* best effort */ }
361
+ } finally {
362
+ probing = false
363
+ }
364
+ })()
365
+ })
208
366
  } catch {
209
367
  // Duplicate registration or unavailable provider — keep defaults.
210
368
  }
@@ -370,7 +528,28 @@ export function apply(ctx) {
370
528
  },
371
529
  output: { schema: { type: 'object', additionalProperties: true }, render: renderJSON },
372
530
  async execute(args, exec) {
373
- return sendCommand(baseUrl(), bridgeToken(), 'run', { overrides: args.overrides }, exec.signal, { timeoutMs: 20000 })
531
+ const result = await sendCommand(baseUrl(), bridgeToken(), 'run', { overrides: args.overrides }, exec.signal, { timeoutMs: 20000 })
532
+ // v0.1.3: record the run for .runs.json traceability.
533
+ if (result && result.promptId) {
534
+ recordRun(result.promptId, args.overrides, result.nodeErrors ? 'error' : 'queued')
535
+ }
536
+ // P1: a submission failure (nodeErrors) often hides a concrete execution
537
+ // error. Best-effort fetch /history right away to surface it structurally.
538
+ const hasNodeErrors = result && result.nodeErrors && Object.keys(result.nodeErrors).length > 0
539
+ if (result && result.promptId && hasNodeErrors) {
540
+ try {
541
+ const pid = encodeURIComponent(result.promptId)
542
+ const hr = await fetch(`${baseUrl()}/history/${pid}`, {
543
+ signal: AbortSignal.any([exec.signal, AbortSignal.timeout(8000)]),
544
+ })
545
+ if (hr.ok) {
546
+ const history = await hr.json()
547
+ const executionError = extractExecutionError(history?.[result.promptId])
548
+ if (executionError) result.executionError = executionError
549
+ }
550
+ } catch { /* best effort — nodeErrors alone is still actionable */ }
551
+ }
552
+ return result
374
553
  },
375
554
  }))
376
555
 
@@ -391,7 +570,18 @@ export function apply(ctx) {
391
570
  },
392
571
  output: { schema: { type: 'object', additionalProperties: true }, render: renderJSON },
393
572
  async execute(args, exec) {
394
- return sendCommand(baseUrl(), bridgeToken(), 'batch_run', { runs: args.runs }, exec.signal, { timeoutMs: 30000 })
573
+ const result = await sendCommand(baseUrl(), bridgeToken(), 'batch_run', { runs: args.runs }, exec.signal, { timeoutMs: 30000 })
574
+ // v0.1.3: record every queued run for .runs.json traceability.
575
+ if (result && Array.isArray(result.results)) {
576
+ for (let i = 0; i < result.results.length; i++) {
577
+ const r = result.results[i]
578
+ const overrides = Array.isArray(args.runs?.[i]?.overrides) ? args.runs[i].overrides : []
579
+ if (r && r.promptId) {
580
+ recordRun(r.promptId, overrides, r.nodeErrors ? 'error' : 'queued')
581
+ }
582
+ }
583
+ }
584
+ return result
395
585
  },
396
586
  }))
397
587
 
@@ -404,17 +594,25 @@ export function apply(ctx) {
404
594
  name: 'comfyui_get_outputs',
405
595
  description: 'Fetch the output files produced by a completed ComfyUI run (comfyui_run returns prompt_id): ' +
406
596
  '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. ' +
597
+ 'files (kind/filename/type/subfolder/URL), and downloads them into downloadDir — or the project directory ' +
598
+ '(Settings → 项目目录, default <workspace>/projects) when downloadDir is omitted. With outputStem, files are ' +
599
+ 'saved as <stem>.<NN>.<ext> with an auto-incrementing number so sweep runs never overwrite each other; ' +
600
+ 'each download also appends the run trace (promptId/overrides/timestamp/files) to runs.json. ' +
408
601
  'Returns ready:false while the run is still executing.',
409
602
  parameters: {
410
603
  promptId: { type: 'string', required: true, description: 'The prompt_id returned by comfyui_run.' },
411
- downloadDir: { type: 'string', description: 'Optional absolute local directory to save the output files into. Omit to return URLs only.' },
604
+ downloadDir: { type: 'string', description: 'Optional absolute local directory to save the output files into. Defaults to the project directory when omitted.' },
605
+ outputStem: { type: 'string', description: 'Optional filename stem; files are saved as <stem>.<NN>.<ext> (NN auto-increments, never overwrites).' },
412
606
  },
413
607
  output: { schema: { type: 'object', additionalProperties: true }, render: renderJSON },
414
608
  async execute(args, exec) {
415
609
  const base = baseUrl()
416
610
  const pid = encodeURIComponent(args.promptId)
417
- const res = await fetch(`${base}/history/${pid}`, { signal: exec.signal })
611
+ // Timeout the history read so a hung ComfyUI endpoint can't strand the
612
+ // agent forever; keep exec.signal (agent cancel) in the same signal.
613
+ const res = await fetch(`${base}/history/${pid}`, {
614
+ signal: AbortSignal.any([exec.signal, AbortSignal.timeout(15000)]),
615
+ })
418
616
  if (!res.ok) throw new Error(`history HTTP ${res.status}`)
419
617
  const history = await res.json()
420
618
  const item = history?.[args.promptId]
@@ -446,19 +644,80 @@ export function apply(ctx) {
446
644
  }
447
645
  }
448
646
  }
647
+ // v0.1.3: downloads land in downloadDir, or the project directory
648
+ // (explicit setting, defaulting to <workspace>/projects). outputStem
649
+ // renames to <stem>.<NN>.<ext> so sweep runs never overwrite; the run
650
+ // ledger (promptId/overrides/timestamp from comfyui_run/batch_run) is
651
+ // appended to runs.json for traceability.
449
652
  let downloaded = []
450
- if (args.downloadDir && files.length > 0) {
653
+ let runsFile = null
654
+ const targetDir = args.downloadDir || resolveProjectDir()
655
+ if (targetDir && files.length > 0) {
451
656
  const fs = await import('node:fs')
452
657
  const path = await import('node:path')
453
- fs.mkdirSync(args.downloadDir, { recursive: true })
658
+ fs.mkdirSync(targetDir, { recursive: true })
659
+ // next NN for outputStem: scan existing <stem>.<NN>.<ext> in targetDir
660
+ const stem = (args.outputStem || '').trim()
661
+ let nextNum = 1
662
+ if (stem) {
663
+ const stemPrefix = stem + '.'
664
+ const nums = []
665
+ try {
666
+ for (const name of fs.readdirSync(targetDir)) {
667
+ if (!name.startsWith(stemPrefix)) continue
668
+ const rest = name.slice(stemPrefix.length)
669
+ const dot = rest.indexOf('.')
670
+ if (dot <= 0) continue
671
+ const numStr = rest.slice(0, dot)
672
+ if (/^\d+$/.test(numStr)) nums.push(Number(numStr))
673
+ }
674
+ } catch { /* dir may not exist yet */ }
675
+ if (nums.length > 0) nextNum = Math.max(...nums) + 1
676
+ }
677
+ const savedNames = []
454
678
  for (const f of files) {
455
- const resp = await fetch(f.url, { signal: exec.signal })
679
+ const resp = await fetch(f.url, {
680
+ signal: AbortSignal.any([exec.signal, AbortSignal.timeout(30000)]),
681
+ })
456
682
  if (!resp.ok) throw new Error(`download ${f.filename} HTTP ${resp.status}`)
457
683
  const buf = Buffer.from(await resp.arrayBuffer())
458
- const local = path.join(args.downloadDir, f.filename)
684
+ const ext = path.extname(f.filename) || '.bin'
685
+ const localName = stem
686
+ ? `${stem}.${String(nextNum).padStart(2, '0')}${ext}`
687
+ : f.filename
688
+ if (stem) nextNum++
689
+ const local = path.join(targetDir, localName)
459
690
  fs.writeFileSync(local, buf)
460
- downloaded.push({ ...f, local })
691
+ downloaded.push({ ...f, local, localName })
692
+ savedNames.push(localName)
461
693
  }
694
+ // append this run's trace to runs.json (create if missing). Dedup by
695
+ // promptId: re-downloading the same run updates its entry instead of
696
+ // appending a duplicate; `run` numbers continue from the max seen.
697
+ const ledger = runLedger.get(String(args.promptId)) || {}
698
+ const runsPath = path.join(targetDir, 'runs.json')
699
+ let runs = []
700
+ try {
701
+ if (fs.existsSync(runsPath)) runs = JSON.parse(fs.readFileSync(runsPath, 'utf8'))
702
+ if (!Array.isArray(runs)) runs = []
703
+ } catch { runs = [] }
704
+ const existing = runs.findIndex((r) => r && r.promptId === args.promptId)
705
+ // Prefer the run's FINAL history status ('success'/'error') over the
706
+ // ledger's provisional 'queued' — the ledger is written at submit time,
707
+ // history reflects what actually happened.
708
+ const finalStatus = item.status?.status_str || ledger.status || 'success'
709
+ const entry = {
710
+ run: existing >= 0 ? runs[existing].run : (runs.reduce((m, r) => Math.max(m, r?.run || 0), 0) + 1),
711
+ promptId: args.promptId,
712
+ overrides: Array.isArray(ledger.overrides) ? ledger.overrides : [],
713
+ timestamp: ledger.timestamp || new Date().toISOString(),
714
+ status: finalStatus,
715
+ files: savedNames,
716
+ }
717
+ if (existing >= 0) runs[existing] = entry
718
+ else runs.push(entry)
719
+ fs.writeFileSync(runsPath, JSON.stringify(runs, null, 2))
720
+ runsFile = runsPath
462
721
  }
463
722
  return {
464
723
  promptId: args.promptId,
@@ -468,6 +727,10 @@ export function apply(ctx) {
468
727
  nodeCount: Object.keys(outputs).length,
469
728
  outputs: files,
470
729
  downloaded,
730
+ runsFile,
731
+ // P1: structured execution error when the run failed on a node, so the
732
+ // agent can name the failing node/exception instead of reading raw JSON.
733
+ executionError: extractExecutionError(item),
471
734
  }
472
735
  },
473
736
  }))
@@ -543,47 +806,21 @@ export function apply(ctx) {
543
806
  // P0 handshake: does the BRIDGE actually enforce the token? The DSH side
544
807
  // setting alone proves nothing — if the ComfyUI side never got
545
808
  // 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.
809
+ // Shared probe (also used by the settings watcher) answers it.
548
810
  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
- }
811
+ const probe = await probeBridgeAuth(baseUrl(), token)
577
812
  return {
578
813
  baseUrl: baseUrl(),
579
814
  activeView,
580
815
  activeViewKnown,
581
816
  sessionSource,
582
817
  bridgeTokenSet: Boolean(token),
583
- bridgeAuthEffective,
584
- bridgeAuthProbe,
818
+ bridgeAuthEffective: probe.bridgeAuthEffective,
819
+ bridgeAuthProbe: probe.bridgeAuthProbe,
820
+ checkedAt: probe.checkedAt,
585
821
  comfyuiDir: value?.comfyuiDir || '',
586
822
  launchCommand: value?.launchCommand || '',
823
+ projectDir: resolveProjectDir(),
587
824
  }
588
825
  },
589
826
  }))
@@ -692,10 +929,11 @@ export function apply(ctx) {
692
929
  // Load node (LoadImage→image, LoadAudio→audio, LoadVideo→video, 3D→mesh…).
693
930
  ctx.tools.register(defineTool({
694
931
  name: 'comfyui_attach_file',
695
- description: 'Upload a local file (image/audio/video/3D) into ComfyUI\'s input/ directory and optionally ' +
932
+ description: 'Upload a local file (image/audio/video/3D/text) into ComfyUI\'s input/ directory and optionally ' +
696
933
  'point a Load node at it. Uses ComfyUI\'s native upload endpoint (not the bridge); the source file is read ' +
697
- 'from the agent\'s own machine. mediaType is inferred from the extension when omitted. After upload, pass ' +
698
- 'nodeId (and widgetKey for non-image loads) to update the Load node so it takes effect immediately.',
934
+ 'from the agent\'s own machine. mediaType is inferred from the extension when omitted (incl. text/prompts: ' +
935
+ 'txt/md/json/csv/yaml/srt). After upload, pass nodeId (and widgetKey for non-image loads) to update the ' +
936
+ 'Load node so it takes effect immediately.',
699
937
  parameters: {
700
938
  path: { type: 'string', required: true, description: 'Absolute local path to the file.' },
701
939
  mediaType: { type: 'string', description: 'Optional explicit media type (e.g. video/mp4, audio/wav, model/gltf-binary); defaults to a per-extension guess.' },
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "dsh-comfyui-canvas",
3
- "version": "0.1.2",
3
+ "version": "0.1.3",
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",