# 画图与视频(ComfyUI,v0.22.0) > 状态:**已实现(画图 v0.21.0 / 视频 v0.22.0,2026-10-20)**。真实验证: > ComfyUI 0.39.0 / RTX 4070(`192.168.1.10:8188`)。出图 / 视频均走同一个远端实例: > - 出图:isolated DSH 启动后 `status.comfy.reachable = true`、`skills` 里 `comfy-image.loaded = true`; > `POST /api/comfy/test` 用错误地址返回 `ok=true` + `probe.reachable=false`。 > - 视频:`h3-t2v` 4 步 / 0.2MP / 3s 服务端 **29.1s**(608×352、73 帧、3.04s、带音轨); > `h3-r2v` 同规格 **30.4s**。 > client 产物 `lib/client.js` 已含「画图 / 视频」页文案。 > 相关:[config.md](./config.md)(`comfy` 配置段)、[client.md](./client.md)(「画图 / 视频」二级页)、 > [media.md](./media.md)(产物经 `qq_send` 回发)、[architecture.md](./architecture.md)(skills 服务注入)。 > > 一句话:插件探测配置的 ComfyUI,连上后把**两个自带的动态 skill**(`comfy-image` 出图 / > `comfy-video` 视频)注册给 DSH;群里说「画一张…」或「生成一段…视频」时 agent 按 skill 用 > HTTP API 生成,产物落在群项目里,再用 `qq_send` 的 `images` / `videos` 发回群。 ## 1. 能力概览 - **开关式**:`comfy.enabled` 默认 **false**;关闭时既不探测,也不注册 skill,开箱行为与旧版一致。 - **服务探测**:host 每 60 秒(以及状态轮询时)GET ComfyUI 的 `/system_stats` 与 `/queue`, 拿到版本、GPU 名称与队列长度。 - **两个动态 skill**(首次连上后一起注册,**共用** `lib/skills/comfy-image/` 目录与运行器): | skill | 模板 | 用途 | 产物 | |---|---|---|---| | `comfy-image` | `SKILL.md` | 出图 / 改图:Qwen-Image-2.1(`qwen-t2i` / `qwen-i2i` / `qwen-edit`)+ MOSS-Image(`moss-t2i`) | png / jpg → `assets/comfy/output/` | | `comfy-video` | `SKILL-video.md` | 短视频(MiniMax H3,带原生音频):`h3-t2v` / `h3-i2v` / `h3-r2v` | mp4 → `assets/comfy/output/`(按远端 subfolder) | - 两个 skill 都注册成 `source: "custom"` / `provider: "dsh-qq-relay"` 的 **runtime skill**, **所有 cwd 的 DSH 会话都能看到**(QQ 群会话与 GUI 会话);注册不需要重启 GUI。 一个模板坏了不影响另一个(各自记录加载状态与错误)。 - **产物回群**:图片用 `qq_send` 的 `images`(≤4 张)发回,视频用 `videos`(≤1 个,内联视频消息) 发回(详见 [media.md](./media.md) §5.1 / §5.3)。 - **配置页**:「画图 / 视频」二级页可改开关 / 服务地址 / 探测超时 / skill 开关,查看连通性与 **逐 skill** 的加载状态,并有「测试连接」按钮(只探测、不保存)。 ## 2. 架构(配置 → 探测 → skills.register → agent 目录) ~~~text profile patch / settings.json └─ comfy: { enabled, baseUrl, timeoutMs, skillEnabled } ↓ normalizeComfyConfig(非法值拒绝保存 / 回落默认) lib/index.js apply() ├─ createComfyController({ getConfig }) # controller/comfy.js ├─ comfyController.start() + refresh() # 启动探测一次,之后 60s 周期补探测 └─ ctx.inject(['skills'], ...) # skills 服务就绪后 attachSkills(ctx.skills) ↓ probeComfy:GET /system_stats + /queue ├─ 可达 && enabled && skillEnabled → 对 COMFY_SKILLS 逐个: │ comfy-image → 渲染 SKILL.md → skills.register(...) │ comfy-video → 渲染 SKILL-video.md → skills.register(...) │ (单个失败只记该 skill 的 error,另一个照常注册) └─ 不可达 → 只更新 status,不动已注册的 skill(避免目录抖动) ↓ DSH skill 目录 / 会话可见 → agent 触发画图 / 视频 → scripts/comfy_run.py → ComfyUI POST /prompt ↓ <群项目>/assets/comfy/output[/video]/* → qq_send({ images } / { videos }) → 群聊 ~~~ - **与 preset 注册的区别**(见 [architecture.md](./architecture.md) §3): preset 走 `ctx.agentPresets.register()`,影响**新建会话**的系统提示词与组合表; skill 走 `ctx.skills.register()`,影响**所有会话**的可用技能。两者都是运行时动态注册, 都不需要重启 GUI,但互不替代。 - **服务缺失**:宿主没有 `skills` 服务(精简宿主 / 旧版本)时探测照常, `status.comfy.supported = false`、两个 skill 都不加载,页面显示原因。 - **零外部依赖**:探测只用全局 `fetch`(可注入桩给单测),模板渲染只用 node: 内置模块。 ## 3. 配置示例与字段 profile 的 `cordis.patch.yml`(也可以只写默认值,然后在配置页「画图 / 视频」里改): ~~~yaml - id: dsh-qq-relay config: # …连接 / 项目 / 闸门等已有配置… comfy: enabled: false # 先关着;需要时在配置页打开 baseUrl: http://192.168.1.10:8188 # ComfyUI HTTP 地址(无鉴权) timeoutMs: 30000 # 探测 / 请求超时,3s..120s skillEnabled: true # 连上后是否注册 comfy-image / comfy-video ~~~ | 字段 | 默认 | 取值 | 作用 | |---|---|---|---| | `comfy.enabled` | `false` | 布尔 | 画图 / 视频总开关;关闭时不探测、不注册 skill | | `comfy.baseUrl` | `http://192.168.1.10:8188` | 非空 `http://` / `https://` URL | ComfyUI 地址;尾部斜杠会被去掉 | | `comfy.timeoutMs` | `30000` | 整数 `3000..120000`(毫秒) | 探测与连接测试的超时 | | `comfy.skillEnabled` | `true` | 布尔 | 连接成功后是否注册两个 skill | - **层序**:与其它配置项一致——profile patch → 配置页保存的 `~/.dsh/storages/dsh-qq-relay/settings.json` 覆盖 → `normalizeConfig` 校验。 非法值(URL 不合法 / 超时越界 / 类型不对)保存失败(400),不写盘。 - **可覆盖子项**:`EDITABLE_COMFY_KEYS = ['enabled', 'baseUrl', 'timeoutMs', 'skillEnabled']`, 四个字段全部能在「画图 / 视频」页改;合并时只覆盖这四个子项(见 `model/settings.js`)。 - **旧 host 兜底**:host 没返回 `config.comfy` 时页面按「关闭」渲染并提示重启 (`config.comfySupported = false`),不会白屏。 - **保存后的行为**:先按新配置**立即探测**,只有 `enabled && skillEnabled` 且连得上才注册; 仅 `comfy` 变化时**不重建 relay 连接**(比较配置时忽略 `comfy`),也不重启 GUI。 - 探测超时的含义是**单个 HTTP 请求**的超时,不是整个生成任务的超时(视频长任务在 skill 里跑)。 ## 4. HTTP 路由 路由都在配置页同一 HTTP 面(同源、无独立鉴权),统一 JSON 信封 `{ ok: true, value }` / `{ ok: false, error: { code, message } }`。 | 方法 | 路径 | 说明 | |---|---|---| | GET / HEAD | `/dsh-qq-relay/api/status` | 状态快照;画图 / 视频信息在顶层 `comfy` 与 `config.comfy` | | POST | `/dsh-qq-relay/api/comfy/test` | 探测 ComfyUI,body 可选 `{ baseUrl?, timeoutMs? }` | `POST /comfy/test`: ~~~json // 请求(body 可省略;省略时用当前配置) { "baseUrl": "http://192.168.1.10:8188", "timeoutMs": 5000 } // 成功 { "ok": true, "value": { "probe": { "target": "http://192.168.1.10:8188", "reachable": true, "version": "0.39.0", "device": "NVIDIA GeForce RTX 4070", "queue": { "running": 0, "pending": 0 }, "error": "", "checkedAt": 1790000000000, "latencyMs": 12 }, "status": { "comfy": { } } } } ~~~ - **只探测、不保存**:不写 settings.json、不改配置、不改 skill 注册,也不更新后台探测的 `checkedAt`(页面上的「最近检查」仍来自后台轮询)。`probe` 里带 `latencyMs` 供页面显示。 - **探测失败也是成功响应**:地址连不上时仍返回 HTTP 200 + `ok=true`,由 `probe.reachable=false` / `probe.error` 表达(实测:错误地址 → `ok=true` + `reachable=false`);只有参数非法(400) 或处理器异常(500)才是错误响应。 - 错误码:参数非法 → 400 `COMFY_INVALID`(含具体 problems);探测异常 → 500 `COMFY_TEST_FAILED`;当前实例没注入测试处理器 → 501 `CONFIG_UNSUPPORTED`; 非 POST → 405 `CONFIG_METHOD_NOT_ALLOWED`。body 上限沿用配置页的 64 KiB。 - **状态快照 `comfy` 字段**(页面轮询的数据源;v0.22.0 起 `skill` 单数改为 `skills` 数组): ~~~json { "enabled": true, "baseUrl": "http://…", "timeoutMs": 30000, "skillEnabled": true, "supported": true, "reachable": true, "checking": false, "checkedAt": 1790000000000, "error": "", "version": "0.39.0", "device": "NVIDIA GeForce RTX 4070", "queue": { "running": 0, "pending": 0 }, "skills": [ { "name": "comfy-image", "loaded": true, "error": "" }, { "name": "comfy-video", "loaded": true, "error": "" } ] } ~~~ - `supported`:宿主是否已挂上 `skills` 服务(false 时两个 skill 一定不加载)。 - `checking`:是否有探测在跑;`checkedAt` / `error` 是最近一次探测结果。 - `skills[].error`:该 skill 的注册失败原因(模板缺失 / 残留占位符 / 没有 skills 服务等), 一个 skill 失败不影响另一个。 - 旧 host(v0.21.x)只返回单数 `skill`;client 端的兜底会把它读成一个元素的数组, 页面仍可用但只显示一个 skill。 ## 5. skill 生命周期与渲染 ### 5.1 注册策略(两个 skill 各自独立) | 事件 | 动作 | |---|---| | 首次探测成功、`enabled && skillEnabled` | 逐个读模板 → 渲染 → `skills.register(...)`,各自保持注册 | | 某一个模板读取 / 渲染失败 | **只跳过该 skill**,错误写进它的 `skills[].error`;另一个照常注册 | | 之后探测失败 | **不注销**(ComfyUI 会抖动;skill 留在 DSH 目录里,生成会失败但不反复进出目录) | | 关闭 `comfy.enabled` | 下一轮同步注销**两个** skill(dispose) | | 关闭 `comfy.skillEnabled` | 下一轮同步注销两个 skill | | 宿主 skills 服务卸载 / 插件卸载 | `detachSkills` / `dispose` 注销全部 | - **触发时机**:插件启动、配置保存(`comfy` 字段被提交)、`skills` 服务就绪、每 60 秒周期探测、 状态轮询发现超过 30 秒没探测过。注册/探测串行执行,并发调用不会重复注册。 - 注册清单(`lib/model/comfy.js` 的 `COMFY_SKILLS`,`lib/controller/comfy.js` 遍历注册): ~~~js COMFY_SKILLS = [ { name: 'comfy-image', template: 'SKILL.md', description, whenToUse }, { name: 'comfy-video', template: 'SKILL-video.md', description, whenToUse }, ] // 每个都注册成: skills.register({ name, description, whenToUse, content, // 对应模板正文(已渲染) source: 'custom', provider: 'dsh-qq-relay', path: '/