dsh-web
zhu1090093659/dsh-webDeepSeek Harness(DSH)Web 插件聚合生态包 · 一切皆插件,创意工坊分发(dsh-market.com)
面向 DeepSeek Harness 的本地图像生成插件(优化版):经本机 ComfyUI 调用 Qwen-Image 2.1 生成图像,全部流量限于 127.0.0.1。本版增强推理性能、NVIDIA/Intel/AMD 显卡适配与 Windows/Linux 兼容。原作者 @奇迹与你,优化版 @yanfei0725。
面向 DeepSeek Harness 的本地图像生成插件。经由本机 ComfyUI 调用 Qwen-Image 2.1 模型,
在对话中直接完成文生图与图生图;生成的图像落盘保存,并在会话界面内直接呈现。
网络边界(请以本节为准):插件的网络出口有两类,边界不同。
① 出图数据流(提示词、参考图)—— 只发往 comfyUrl(ComfyUI)与 hipfireUrl(hipfire serve)。
默认仅本机回环(127.0.0.1 / localhost / ::1),且这一约束由代码层强制(lib/net-guard.js):
任一地址被配成本机以外时,插件会拒绝启动而不是照发请求。所有出站请求统一走 guardedFetch,
强制 redirect: 'error'(不跟随任何 HTTP 重定向)。因此出图全程不依赖外部网络,无需任何 API 密钥,亦不产生调用费用。
② 加速包下载(可选,默认开启)—— accelAutoInstall(默认 true)时,插件会从 pip 源与 GitHub
下载厂商专用加速包(pip install --upgrade / git clone)。这会访问外部地址,
但不发送任何提示词或图片。设为 accelAutoInstall: false 即可完全关闭,出图功能不受影响。
详见下文「权限与已知风险」。
⚠ 为如实披露能力:插件提供了
allowRemoteComfy开关(默认false)。用户显式设为true后, 才可把comfyUrl/hipfireUrl指向局域网(如192.168.1.100)或远程主机——此时插件会向你指定的地址发送提示词与图片数据。 这是用户主动选择的行为,不是默认状态。
原作者:@奇迹与你(https://github.com/wonderandyou/dsh-qwen-paint)
优化版作者:@yanfei0725(https://github.com/yanfei0725/dsh-qwen-paint) | 许可协议:MIT | 当前版本:0.17.13
VAEDecodeTiled 节点。/api/qwen-paint/health、/capabilities、/presets、/memory/recent、/memory/preference,供本地工具集成;端点仅接受本机请求。.qwen-paint-memory.jsonl,dsh-auto-memory 可从 recent 端点读取;偏好 GET/POST 目前只持久化,不自动改变生图参数。pyproject.toml 以供 Registry 索引发现。此仓库仍是 DSH 插件而非 ComfyUI Python custom node;元数据不代表已发布或已获 Registry 收录。serialBatch 设为 auto(默认)/ on / off 手动控制。
upscaleAfter(如 2,即 1024² → 2048)后,生图完成会自动接着做分块放大。放大失败不影响已生成的原图(原图已落盘,只记录警告)。
upscale_image 工具共用同一套节点图,不存在两套实现。本版没有改动出图逻辑 —— 全是"让别人能正确收录我们"的元数据与文档整改。
files=write / network=默认仅 127.0.0.1(代码强制,另有 allowRemoteComfy opt-in) / commands=restricted / credentials=none,
以及 6 条已知风险 —— 不再让用户靠猜。.comfyignore:把开发脚本、构建产物、内部报告排除出发布归档。.github/workflows/publish_action.yml:推送到 main 且改动涉及 pyproject.toml 时自动发版。pyproject.toml 按 ComfyUI Registry 官方规范重写:
PublisherId / DisplayName 字段名大小写、[project.urls].Repository、移除非规范字段。
⚠ 记录一个踩过的坑:Publisher ID 不等于 GitHub 用户名,
写错时报的是 Failed to validate token(完全没提 publisher),极具误导性。⚠ 关于 DSH STORE 的如实说明:本插件必然具备文件 / 网络 / 命令能力, 因此不满足其"自动准入"要求(
automaticApproval.permissionSignals必须全为false), 只能走user-reviewed人工通道。这是功能决定的,不是缺陷,也不打算为此阉割功能。
这一版没有新功能,是把 0.17.12 刚上的圈选浮层修干净。三条都是只看代码不跑就可能漏掉的隐患,
新增回归套件 _inpaint_ui_test.mjs(19 项断言,已并入 npm test)逐条锁住。
改之前的流程:面板圈选 → host 把 base64 蒙版存成 outputDir/.qwen-paint-mask-<时间戳>.png
→ 重绘 → 这个文件就留在那儿了。每圈一次堆一个,还是 dotfile —— 用户自己在目录里
根本看不见,日子久了就是一排隐形垃圾。
现在:try / finally 里用完即删(ComfyUI 用的是上传时复制过去的副本,删本地这份不影响重绘)。
删除失败也吞掉——它只是个临时文件,绝不能反过来把重绘搞失败。
顺手修掉一个并发隐患:文件名原来只拼 Date.now(),同一毫秒两次圈选会算出同一个名字,
后写的把先写的覆盖 ⇒ 用户看到"我涂的第二张用了第一张的蒙版"。现在带 6 位随机后缀
(uiMaskPath(),纯函数,回归直接断言"连续两次调用不重名")。
内核的 /api/file?path=… 只服务"已登记"的文件(deliverables)。draw_image 的产物走
tools/result 钩子登记,所以一向能显示;而圈选重绘是 host 直接调
editToolRef.execute(),没有任何登记 ⇒ 弹窗里用 api/file 看结果,大概率是裂图。
现在插件自己开了一条 /api/qwen-paint/file:直接吐 outputDir 内文件的字节,边界有三道:
isLocalRequest 只放行本机请求outputDir 之内(.. 穿越挡掉)GET / HEAD,其它方法 405弹窗内的图片(原图 + 结果图)全部改走这条路由,预览不再依赖"文件是否登记过"。
append 一个新 <img> ⇒ 连改几次就在弹窗里叠一排,
越叠越长还把上面的顶出视野。现在固定一个容器,replaceChildren 只留最新一张。这一版加的是用户看得见的功能,不是合规整改。全部默认关闭 ⇒ 升级后行为与 0.17.11 完全一致。
以前 inpaint_image 只能吃你自己在外部做好的蒙版图,面板里没有任何圈选能力。
现在:点结果图 → 弹出一个浮层 → 用画笔把要改的地方涂出来 → 写一句"这块要改成什么" → 开始重绘。
POST /api/qwen-paint/inpaint,执行的是同一个 inpaint_image 工具
(校验、节点探测、报错文案全部复用,不存在两套逻辑)为什么是独立浮层而不是"在消息里的图上圈":那张预览图在 DSH 的消息树里,而那棵树由 React 管理 —— 插自己的 DOM 会被下一次渲染重排/清空(这个坑本文件里记着三次失败,最后一次 把界面搞成整片空白)。浮层是自己的 DOM,与消息树完全隔离。
两个实现细节(都踩过):
fetch 取 blob、再用 blob: URL 喂 <img>:直接画进 canvas 再 toDataURL
会因跨协议被标脏、抛 SecurityError。
(取 blob 的 URL 在 0.17.13 里又从内核的 api/file?… 换成了插件自己的
/api/qwen-paint/file——原因见下一节第 2 条。)canvas.width / rect.width 折算。⚠ 这是 0.17.12 里的一处方案替换:inpaint_image 原本要靠
Qwen-Image-InstantX-ControlNet-Inpainting.safetensors,但 InstantX 公开的 Qwen ControlNet
是给 Qwen-Image 1.0 的(latent 16 通道),本机底模是 2.1(64 通道)⇒ 装上也不兼容;
2.1 唯一对得上的官方权重是阿里 Qwen-Image-2.1-Fun-Controlnet-Union(7.55 GB,单文件,
8 种控制 + 修复),但它走的是 ModelPatchLoader 路径、且官方要求 Comfy-Org 原版底模,
本机是 int8+convrot 量化版,叠加稳定性未验证。两者都不划算 ⇒ 改为不装任何东西。
现在走 ComfyUI 自带的潜空间重绘,原理(comfy/samplers.py:634-642):
VAEEncode 成 latent,蒙版挂成它的 noise_mask(SetLatentNoiseMask)GrowMask 外扩(默认 8px)再 FeatherMask 羽化(默认 16px),用于缓解接缝新增两个可选参数,都填 0 即退回硬边蒙版:
feather 羽化像素 0–64,默认 16(接缝明显就加大)grow 外扩像素 0–64,默认 8(避免羽化把重绘区吃小)denoise 默认由 1.0 改为 0.7:蒙版外始终严格保持原样,它只决定蒙版内重画得多彻底 ——
想彻底替换这块用 1.0,轻微改动用 0.4–0.6。
本机实测(1024×1024,steps=4,denoise=0.7,21 秒出图):
| 区域 | 平均绝对差 | 像素变化率 |
|---|---|---|
| 蒙版外(保留区) | 0.48 | 0.00% |
| 蒙版中心(重绘区) | 12.26 | 98.90% |
| 羽化边缘带 | 7.27 | 72.88% |
⇒ 保留区逐像素几乎零变化(0.48 的残差是 VAE 编解码往返造成),重绘区 99% 被改掉, 羽化带平滑过渡。"只改这一块、其余原样"这个承诺是真的。
aiPickBest)开启后,一次出多张时工具描述会要求模型:
read_image 逐张查看,挑最好的一张放在最前面推荐并说明理由(构图 / 手部 / 文字 / 贴合度)read_image 不可用,就如实说"这是按参数挑的、没有实际看图",绝不假装看过为什么不做"拼图再挑":4 张缩成 1 张 2×2 会丢细节(手部畸形、文字糊掉正好是缩图后最看不清的), 而省下的约 3000 token 在 1M 上下文里只占 0.4%。用细节换这点 token 不划算。
userPickBest)开启后,一次出多张时并排全部显示(2 列网格 + 序号),每张都可点开放大/圈选。 关着时仍是"只显示第一张"的旧行为。
以前
presentationMeta只带path(第一张),所以出 4 张时界面上只看得见 1 张 —— 这一版把files也带上,才有得选。
菜单元新增一组「抽卡挑图(重载后生效)」,各两档(开 / 关)。
选择写进 qwen-paint-ui.json,重启记得住。
⚠ 为什么要重载才生效:aiPickBest 影响的是工具描述,而描述是插件加载时按配置拼的静态文本。
这一点在菜单标题里写明了,免得用户以为选了个坏的。
无。 两个开关默认 off,行为与 0.17.11 一致;新端点只接受 POST 且过与状态端点同一套守卫
(Host / Sec-Fetch-Site / Origin 围栏 + application/json + 有界 body + 蒙版限 24 MB)。
这一版没有任何出图行为变更。改的是"启动宽容度"和"工程护栏"。
真机实测(AMD RX 7900 XT / ROCm)冷启动 ComfyUI 经常要 60~90 秒 (要加载 20 GB 显存 + 一堆自定义节点)。原来的超时偏短,会出现 进程明明还活着、日志里也没有报错,界面却先报"启动超时" —— 用户只能干等或反复重试。
LAUNCH_TIMEOUT_MS,唯一准绳就是这个常量)。LAUNCH_TIMEOUT_MS ——
历史上因为"注释和实现不一致"被审核点过两次,这次从根上避免。npm test 一条命令跑完全部以前 16 套检查散在 scripts/ 里,靠人一条条敲。结果就是有 15 项断言红了三天没人发现
(它们不在任何清单里,等于没有测试)。现在:
npm test # 全部 16 套,约 4 秒
node scripts/test-all.mjs --only=net # 只跑名字含 net 的
判定口径刻意分成三档,"跑不了"不算"通过":
| 结果 | 含义 |
|---|---|
| 通过 | 退出码 0 且输出没有失败行 |
| 失败 | 退出码非 0,或退出码 0 但输出里有 ✗(抓"忘了 exit(1)"的套件) |
| 跳过 | 环境不具备(如沙箱禁止派生进程),如实标注,不冒充通过 |
0.17.8 的 AUTH001 整改把状态端点的写操作从「GET + 查询参数」改成了
「POST + JSON body」,而 selftest.mjs 里还在用老写法调用,被新守卫 405 挡住,
于是"改档位没生效"看起来像 bug。
git diff 里被删的 check( 行数 = 0)。无。 已安装的用户升级后行为不变;唯一感知得到的差别是启动判定更宽容了。
//xxx → 404)这一版不是合规整改,是真机上实测出来的功能性 bug,而且很严重。
在本机把 DSH 更新到 0.2.1-rc.2 之后做兼容性验证,插件加载完全正常
(5 个工具注册成功、3 条状态端点注册成功、ComfyUI 也被成功拉起),
但状态日志里从第一条起就是 online=false —— 界面一直停在"未就绪 / 启动超时"。
apply() 里做回环校验时写的是:
cfg.comfyUrl = assertLocalUrl(cfg.comfyUrl, { … }).toString()
而 new URL('http://127.0.0.1:8188').toString() 会把空路径补成 /,结果是
http://127.0.0.1:8188/。代码里又有 15 处 ${cfg.comfyUrl}/xxx 拼接,于是实际请求变成:
http://127.0.0.1:8188//system_stats
实测(本机 ComfyUI 正在运行):
| 请求 | 结果 |
|---|---|
GET http://127.0.0.1:8188/system_stats | 200 |
GET http://127.0.0.1:8188//system_stats | 404 |
GET http://127.0.0.1:8188//prompt | 404 |
ComfyUI 用的 aiohttp 不合并重复斜杠。后果是整条 ComfyUI 链路全部 404:
探活(/system_stats)、提交(/prompt)、取结果(/history)、取图(/view)、
上传参考图(/upload/image)。界面上却只表现为"没反应 / 启动超时",极难定位。
0.17.2 起一直存在(git show v0.17.2:lib/index.js 里已有这行规范化)。
也就是说从 0.17.2 到 0.17.9,ComfyUI 后端实际是不可用的。
顺带说明为什么之前没被发现:
lib/hipfire-img.js里早就自己做了String(cfg.hipfireUrl).replace(/\/+$/u, '')—— 说明这个坑在 hipfire 链路上踩过并修了, 但comfyUrl这一侧漏了。而且这条链路的测试一直只测"函数被调用", 没有任何一条断言去检查"拼出来的 URL 长什么样"。
在 apply() 里统一去掉末尾的斜杠(只去尾部,带路径前缀的地址如
http://127.0.0.1:8188/comfy 不受影响):
const trimTrailingSlashes = (u) => u.replace(/\/+$/, '')
cfg.comfyUrl = trimTrailingSlashes(assertLocalUrl(…).toString())
cfg.hipfireUrl = trimTrailingSlashes(assertLocalUrl(…).toString())
scripts/test-net-guard.mjs 新增 [10] 组 8 项,既钉住"坑本身"也钉住"修法":
toString() 确实会补尾斜杠(把坑的事实写进测试)//system_statshttp://127.0.0.1:8188/system_statscfg.comfyUrl / cfg.hipfireUrl 的赋值必须走 trimTrailingSlashes,
且不许残留"直接 toString() 赋给 comfyUrl"的写法net-guard 自测因此从 62 项增至 70 项。
审核上一轮另提出 COMPAT001:称官方最新三版为 0.21-alpha.3 / alpha.4 / alpha.5,
而本插件 manifest 只声明了 0.2.0-rc.2,要求「在实际验证后补当前窗口至少一版的准确声明
及证据,其余保留 unknown」。这一条已按原要求做完 —— 先实测,再声明。
审核笔下的 0.21-alpha.x 是 @deepseek-ai/dsh(dsh 命令本体)的 0.2.1-alpha.x。
该包 npm dist-tags 实测为:latest 0.2.0-rc.2 / next 0.2.1-rc.2 / alpha 0.2.1-alpha.5
(dsh.compatibility.dsh 声明的就是它)。
另注:Electron 桌面外壳 dsh-tui 是另一套版本号(本机 0.13.0,npm 最新 0.15.0),
与这里的 dsh 版本不是同一个东西 —— 0.17.8 的说明里曾把两者混为一谈,此处更正。
用一次性目录装被测版本,并用 $DSH_HOME 指向临时目录做 profile 隔离
(DSH 的 profile / 配置 / 日志全部在 $DSH_HOME 下,因此不会碰 ~/.dsh):
mkdir /tmp/dsh-a5 && cd /tmp/dsh-a5 && npm i --prefix . @deepseek-ai/dsh@0.2.1-alpha.5
DSH_TEST_INSTALL=/tmp/dsh-a5 node scripts/test-dsh-compat.mjs
| 被测版本 | 来源 | 结果 |
|---|---|---|
0.2.1-alpha.5 | 一次性目录安装(审核点名的当前窗口最新 alpha) | 7 / 7 通过 |
0.2.1-rc.1 | 本机实际安装 | 7 / 7 通过 |
0.2.1-rc.2 | 一次性目录安装(next 通道) | 7 / 7 通过 |
7 项分别是:模块能被该版本 import、name / inject / apply 契约成立、
该版本真实的 tools 服务可用、apply() 无异常跑完、确实注册出工具
(draw_image、list_available_models、check_acceleration、upscale_image、inpaint_image)。
另外在 0.2.1-alpha.5 下还跑通了两条"装载链路"证据:
dsh plugin --profile <name> add <本包> 安装成功(plugin-manager 把本包登记为 bundle),
dsh --profile <name> --dump-config 退出码 0 且合成树里确实出现 qwen-paint 条目及其 config。
(该版本 profile 的 app 启动后无输出并常驻;用不含本插件的对照 profile 做同样启动,行为一致
⇒ 该现象是基线行为,与本插件无关。)
systemPrompt:完整 dsh app 才提供它,而该脚本刻意不启动整个 app
(那会进入交互式 TUI 并需要登录),只挂载 tools 这一层;tools 本身是被测版本的真实实现。0.2.1-alpha.3 / alpha.4 及其它未实测版本保持 unknown,不因为"同系列"就顺手写成兼容。dsh.compatibility.dshReleases 现为 0.2.0-rc.2 + 上述三版,并在 note 里写明验证范围与日期。
该结论可由 scripts/test-dsh-compat.mjs 一键复跑(环境不具备时它明确 SKIP、退出 0,
不会把"没测"伪装成"通过")。
做上面这些事的时候顺手核了一遍体积,发现 lib/index.js 已经涨到 263,997 B,
超过了 DSH Store 的单文件上限 262,144 B(超限文件会被判"扫描面不完整",
等于审不到主文件——对一个正在接受安全复核的插件,这比"体积大"本身严重得多)。
这不是新问题:v0.17.5 是 253,729 B(达标)、v0.17.7 已到 260,375 B(勉强达标),
v0.17.8 就超了。而仓库里本来就有这道护栏(scripts/selftest.mjs 的
「发布体积护栏」,逐文件比对 262,144 B),只是它一直没被跑到
(selftest.mjs 卡在需要真机 ComfyUI 的端到端段,跑不到护栏那一段)。
处理方式与之前两次一致(index.js → config.js → describe.js 都这么来的):
把加速包探测与安装整块搬到 lib/comfy-accel.js,逻辑一行未改(只给 function 加了
export,lib/index.js 改为 import)。搬完 lib/index.js 降到 250,350 B,
新文件 15,346 B,两者都远低于上限。
这一块的挑选不是随手挑的:它自洽(依赖全部来自 ./config.js 与 node 内置模块)、
且没有任何测试按名字引用它,所以搬迁面最小。搬完后 14 套离线回归全部通过
(含用到被搬函数的 _gpu_vendor_test / _mac_env_test / _opt_status_test)。
⚠ 如实说一句:
selftest.mjs在这台机器上仍然跑不完整(卡在需要真机 ComfyUI 的 端到端段,那里会一直等)。所以这道体积护栏目前是靠人工核算 + 上面那次手工核查 保证的,不是靠自动跑。要让护栏真正生效,得把selftest拆成"离线部分 / 需真机部分" 两段——这件事列进后续待办,不在本版范围。
审核复核 0.17.7 后确认 Host 缺失拒绝、cross-site / Origin 围栏、客户端改用 POST 均已通过
(并明确"不要求所有请求必须带 Origin"),AUTH001 缩小到写入口的三条边界。三条都属实:
Content-Type原先只校验方法是 POST,没校验媒体类型 —— text/plain 的"简单请求"(跨站表单能发、
且不触发 CORS 预检)也能把 JSON 塞进来。现在:非 application/json ⇒ 415。
repair 端点不限方法、不限媒体类型审核原话:"任意方法的 JSON confirm:true 都会进入 runRepair" —— 确实如此,
原先连 GET 带 body 都能触发修复。现在:非 POST ⇒ 405;非 JSON ⇒ 415;
正文统一走有界解析(与状态端点、memory/preference 同一套)。
审核原话:"recoverFromSageCrash 会把 cfg.nvidiaSageAttention 改为 false。
请把配置恢复放到后台生命周期或显式写操作,GET 只展示状态。"
这也是一处有副作用的 GET,与刚改掉的"GET 写配置"同性质。现在:
launch.sageSuspect),绝不改 cfg;launchComfy 的失败分支),
那条路径不依赖任何 HTTP 请求、本来就会生效。新增 guardWriteRequest(req, res):任何动作之前先判方法与媒体类型,
不通过就 405 / 415 并直接返回 —— 不读 body、不碰 cfg。
状态 POST、repair、memory/preference 三处全部接入。
回归测试扩到 48 项(新增 14 项覆盖 405/415 的各类方法与非 JSON 媒体类型), 三套合计 184 项全部通过。
审核另提出:当前官方最新三版是 0.21-alpha.3 / alpha.4 / alpha.5,
而本插件 manifest 只声明了 0.2.0-rc.2,要求"在实际验证后补当前窗口至少一版的准确声明及证据,
其余保留 unknown"。
本版(0.17.8)当时的处理:按"实际验证后才声明"的要求,没有写未经验证的兼容声明,
dshReleases 仍只列已确认的 0.2.0-rc.2,其余保持 unknown。
⚠ 这一段的包名称写错了,在此更正:本机实际安装的
dsh命令是@deepseek-ai/dsh0.2.1-rc.1;上文提到的dsh-tui 0.13.0是 Electron 桌面外壳的 版本号,两者不是同一套版本。✅ 已在 0.17.9 完成:搭了一次性隔离环境实测
0.2.1-alpha.5 / rc.1 / rc.2(各 7 项全过), 声明与证据见上方 0.17.9 新增说明。
审核在 Issue #1390 复核 0.17.6 后,确认此前各项整改已通过
(含 Tool-view 归属"已审清,不再作为待审理由"),并给出唯一剩余项 AUTH001:
lib/index.js的isLocalRequest只看 Host 和 socket 的本机地址;缺失值也放行, 不检查 Origin、Sec-Fetch-Site 或宿主调用方身份。状态 GET 处理器修改运行配置… 让状态 GET 保持只读,写配置移到显式 POST JSON。
两条都属实。 这是典型的 confused-deputy 面:来自其它网页、但目标地址为本机的请求, Host 和 socket 仍然是本机 —— 单纯"只允许本机连接"并不证明发起请求的网页可信 (CORS 也挡不住有副作用的 GET)。
api-request-trust.ts 等价isLocalRequest → isTrustedRequest(旧名保留为兼容别名),判定顺序逐条照官方实现:
原版这里是
host === '' → 放行,即审核点名的"缺失值也放行",已改为缺失即拒。
Sec-Fetch-Site: cross-site ⇒ 拒绝(不论 Origin)。Origin: null(沙箱 iframe / file:)
是不透明源 ⇒ 拒绝;没带 Origin 则放行(官方原文:"Absent Origin is fine —
the Host fence above already bound the request.")。另外保留 socket 检查(有值时必须是回环)作为附加一层。不做"必须有 Origin"的硬性要求 —— 官方实现同样不要求,把它变成硬性条件会误伤非浏览器客户端。
?idleMinutes= / ?flowStyle= / ?drawTrigger= / ?preset= / ?start=1(启动 ComfyUI)/
各类开关与档位 / ?outputDir= 全部是 GET 带 query 就能改配置。queryLc 这张参数表只从 POST 的 JSON body 填充;GET 时它是空的 ⇒
下面所有写分支自然全部失效(无需逐个改,也就不会漏)。ping)⇒ 405 并明确提示改用 POST,不静默忽略。lib/client.js 同步:新增统一入口 postStatus(params),9 处写调用全部改走它。`${API}/status.json`,
而 API 这个标识符在 client.js 里从未定义 ⇒ 拼出来是 undefined/status.json,
请求必然失败(此前被 window.prompt 被拦后提前 return 掩盖了)。scripts/test-request-trust.mjs,34 项)覆盖审核的验收条件:本机 Host + Origin: https://untrusted.example / Sec-Fetch-Site: cross-site
必须被拒;同源 Origin 与"无 Origin"正常放行;Origin: null / file: / 伪装域名
(localhost.evil.com)/ Host 缺失 一律拒绝;跨站标记压过同源 Origin;
以及源码级回归锁(服务端 GET 只读 + 客户端无 GET 写调用 + 不再引用未定义的 ${API})。
三套测试合计 170 项(net-guard 62 + workflow-contract 74 + request-trust 34),全部通过。
审核在 Issue #1390 复核 0.17.5 后,确认基础预检与有界文本读取完整、
lib/net-guard.js 的回环检查与 redirect: 'error' 有源码依据,
并确认 ACCEL_SOURCES / runInstallSource 的装包行为会访问外部供应链
(因此不能概括成"全部网络只有回环" —— 这一点已在 0.17.3 起如实披露)。
本版只改一处:lib/config.js 里 check_acceleration 工具上方的注释原写
"默认 accelAutoInstall: false,让'装不装'由人决定",而实际默认值是 true
(插件加载后会自动检测并安装白名单内的加速包)—— 属"注释与实现相反"。
已改为与实现一致,并写明"要恢复'只报告、不自动下载'请显式设 accelAutoInstall: false"。
同时做了一次同类问题自查:扫描 DEFAULTS 里全部 25 个布尔配置项,
比对"注释里说的默认值"与"实际默认值",未发现其它矛盾。
⚠ 能力本身保留不变(审核也说明"保留该能力可以走受审路径"): 自动触发、白名单包与来源、子进程参数与环境、用户关闭路径、Tool-view key 作用域, 均已在 README「权限与已知风险」与「0.17.3 / 0.17.5」章节中逐项披露。
来源:插件市场的真实用户反馈(A 卡用户),不是审核意见。逐条回代码 + 回 ComfyUI 源码核实后, 3 个致命 bug 全部属实 —— 这也是本项目一直标注的「真机端到端未验证」所付出的实际代价。
| 症状 | 根因 | 位置 |
|---|---|---|
放大(upscale_image)必崩 | LoadImage 的输入名写成了 upload,正确是 image | lib/index.js 3 处 |
局部重绘(inpaint_image)必崩 | ① 同上 upload;② LoadImageMask 漏了必需的 channel;③ ControlNetInpaintingAliMamaApply 漏了 strength / start_percent / end_percent | lib/index.js |
判定依据(不是推测,是 ComfyUI 源码):
nodes.py 的 LoadImage.INPUT_TYPES → {"required": {"image": ...}}nodes.py 的 LoadImageMask.INPUT_TYPES → 在父类基础上追加 "channel": (["alpha","red","green","blue"],);
取值 'red' 与官方 blueprint「Image Inpainting (Qwen-image).json」里 ImageToMask 的 channel 一致comfy_extras/nodes_controlnet.py 的 define_schema() → 三个 Float.Input;
取值 1.0 / 0.0 / 1.0 与官方 blueprint 里该节点的 widget [1, 0, 1] 一致⚠ 为什么只有这两条链崩、主链路却是好的:同一份工作流代码在
config.js与lib/index.js各写了一遍,config.js那处用的是正确的image。这是典型的"复制粘贴时只改了一处"。
window.prompt / window.alert 做交互与回执,
而 DSH 桌面端会拦这两个;更糟的是 window.prompt 写在 try 外面,一抛错连 catch 都进不去。
⇒ 改为面板内联输入(点一下就地长出输入框,回车/点确定提交,回执显示在同一行),全程不依赖原生弹窗。inpaint_image 的工具描述现在在动手之前就写明这一点;
并新增 readSafetensorsConvInChannels() 读权重 header 做保守提示 ——
只在"只见到 16、没见到 64"时才警告,其余一律不吭声(宁可漏报,绝不误报)。steps 在 turbo8 链是"假控制"(用户实测):forceTurbo8Steps 默认为 true,
此时用户传的 steps 会被丢弃、强制 8 步。这是设计如此(turbo8 是 8 步蒸馏 LoRA),
但 schema 原先没说明 ⇒ 现在 steps 的参数描述里写明该条件下无效及如何解除。三个 bug 之所以能溜到用户手里,是因为当时所有测试都只验"结构"、从不验"输入名"。
scripts/extract_comfy_schema.py —— 用 Python ast 解析本机 ComfyUI 的
nodes.py / comfy_extras/*.py,提取每个节点 INPUT_TYPES() 的 required / optional 键
(新式 io.ComfyNode.define_schema() 也支持),并正确处理继承 ——
仅当子类调用了 super().INPUT_TYPES() 才合并基类键(LoadImageMask 是这种),
完整重写的(如 LoraLoaderModelOnly)不合并,否则会污染契约。
本机实测:扫描 150 个 .py、提取 631 个节点。scripts/comfy-node-contract.json —— 上述解析结果,固化成文件。scripts/test-workflow-contract.mjs —— 74 项断言,读契约比对 lib/ 里构建的所有工作流:
① 必需输入必须齐全;② 不得出现 ComfyUI 不认识的输入名;③ 回归锁(三个坑必须保持修好)。
纯 JS、不需要 ComfyUI 在跑、不需要联网、不派生任何子进程。DSH STORE 自动审核在 Issue #1390 的第二轮反馈中确认:0.17.2 的整改已被读到并采信 ——
"ComfyUI 的启动校验、三处 HTTP helper 的 redirect: 'error' 和子进程环境过滤均已读到,
旧的'完全没有拦截 comfyUrl'结论已撤换"。
但它同时指出两处我上一轮漏掉的,且都属实。本版逐条修复:
hipfireUrl —— 独立于 comfyUrl 的第二个外部端点(已接入守卫)审核原文:
lib/hipfire-img.js的probeHipfireHttp/generateViaHttp直接使用可配置cfg.hipfireUrl, 没有调用回环校验,也不受allowRemoteComfy控制;guardedFetch目前只有定义、没有调用。
完全属实。 上一轮我只收口了 comfyUrl,而 hipfire 出图后端有自己的一条 HTTP 链路。
本版:
guardedFetch 从"只有定义"变成"所有出站的唯一出口":config.js 的 fetchWithTimeout、
hipfire-img.js 的 fetchJson、index.js 的参考图上传,全部改走它 ——
回环校验与禁重定向收口在一处,不会再有"某条链路只做了一半"。apply() 启动校验扩成两个端点:comfyUrl 与 hipfireUrl 都过 assertLocalUrl。
imageBackend: 'hipfire' 时 hipfireUrl 非回环 → 拒绝启动;warn 告知"切到 hipfire 就会被拦"。generateWithHipfire 在进入 HTTP 路径前显式校验并返回结构化可读错误 ——
否则错误会被 probeHipfireHttp 的 catch 吞成 online=false,用户看到的是
"hipfire HTTP 服务不在线,请先启动它",报错指错方向(会去反复重启一个根本不该连的地址)。setAllowRemote():allowRemoteComfy 是启动时确定的全局配置,
而 fetchWithTimeout 在 config.js 内有 8 处调用 —— 逐处传参漏一处就是一个未受控出口,
故由 apply() 设置一次、所有守卫自动继承(显式传参仍可覆盖,收紧优先)。审核原文:
README 权限表仍写"不装包",但
lib/index.js的runInstallSource会经runShell执行pip install --upgrade;lib/config.js使用 PowerShell /sh -c。请对齐网络、装包与 Shell 能力的披露和实际确认边界,无需删掉正常功能或把真实信号标为 false。
也属实,而且比"措辞不准"更严重:accelAutoInstall 默认为 true,
插件加载后会自动 pip install --upgrade / git clone 从 pip 源与 GitHub 下载厂商加速包。
这意味着 README 里"默认仅本机回环"的说法本身也不完整 —— 装包是一条真实的外部网络出口。
本版按"如实披露 + 明确边界"处理(不删功能):
runShell 走 PowerShell 或 sh -c、且默认会 pip install / git clone。accelAutoInstall: true(默认)会装包、会出网、
不发送提示词或图片,并给出关闭方法(accelAutoInstall: false,关掉后出图不受影响)。scripts/test-net-guard.mjs 从 48 项扩到 62 项,新增覆盖:
全局开关语义(含"显式传参收紧优先")、hipfireUrl 作为第二端点被拒/放行、
guardedFetch 在非回环时底层 fetch 调用次数为 0(证明确实没发出去)。
本版修复的是"文档声明与代码行为不符" —— 这是 DSH STORE 自动审核在 Issue #1390 给出的
本轮唯一拒绝理由(原文:"当前 README 仍将网络能力声明为'仅本机回环、不访问外部地址',
但 lib/index.js 直接合并 rawConfig,随后使用可配置的 comfyUrl 发起请求,
未证明回环约束及重定向边界")。
审核员同时明确:不要删除功能,也不要把真实能力声明为 false;请正面解决 —— 要么落地限制并处理重定向,要么如实披露可配置的外部端点。本版两条都做了。
lib/net-guard.js(网络边界守卫,代码层强制):
isLoopbackUrl(url) / assertLocalUrl(url):只认字面量 127.0.0.0/8、localhost、::1、0.0.0.0;
不接受任何域名(含 localhost.evil.com 这类伪装),从根上消除 DNS rebinding 面。lib/index.js 的 apply(),合并 rawConfig 之后第一件事):
comfyUrl 非回环 → 抛出错误、拒绝启动(fail-fast,不静默回退 —— 静默回退会让用户
以为远端生效了、实际连的是本机,那更糟)。redirect: 'error':不跟随任何 302/301。回环被重定向到公网是
最典型的越界手法,直接抛错比"跟过去再检查"可靠(那时数据已经发出去了)。
覆盖 config.js 的 fetchWithTimeout、index.js 的参考图上传、hipfire-img.js 的 HTTP 调用。allowRemoteComfy(默认 false)。
用户显式开启后才允许 comfyUrl 指向局域网/远程地址,且放行时记 warn 日志。
这是"如实披露",不是"撒谎说不支持"。net-guard.js 的 filteredEnv):
hipfire-img.js 的 spawn 与 config.js 的 runShell 不再直接继承宿主环境,
剔除 HTTP_PROXY / HTTPS_PROXY / ALL_PROXY 等代理变量(避免子进程网络去向与声明不一致)
与 *TOKEN* / *KEY* / *SECRET* / *PASSWORD* / *CREDENTIAL* 类变量;
保留 PATH / SystemRoot / HOME / TEMP 等运行必需项。
审核员原话:"spawn 未显式设置 env 会继承宿主环境 —— 这不构成拒绝理由"。 我们仍然做了,是为了防患于未然。