# Comfy Cloud 工作流执行适配器 — 设计 日期:2026-06-17 分支:`feat/comfy-cloud-adapter` 状态:已通过 brainstorming,待落实现计划 ## 背景 数字人/图片工作流走 `selfhost` 生成视频时报 `Submit workflow failed: [401] AuthenticationRequired`。 根因经多层验证已锁定: - Pixelle 的 comfykit 0.1.12 `HttpExecutor` 说的是 **ComfyUI 原生协议**: `POST {base_url}/prompt`、`GET {base_url}/history/{id}`、鉴权用 `Authorization: Bearer`。 - 配置里 `comfyui_url = https://cloud.comfy.org`,但 **Comfy Cloud 的 API 契约完全不同** (官方文档 https://docs.comfy.org/development/cloud/overview): - 鉴权:`X-API-Key` 头(非 Bearer)。 - 提交:`POST /api/prompt`(带 `/api/` 前缀)。 - 轮询:`GET /api/job/{prompt_id}/status` → 状态 pending/in_progress/completed/failed/cancelled。 - 详情/产出:`GET /api/jobs/{prompt_id}`(产出按 node_id 聚合 images/video/audio)。 - 下载:`GET /api/view?filename=&subfolder=&type=output`(302 跳带签名 URL)。 - 接受标准 ComfyUI API-format 工作流。 - 已用官方方式验证 Comfy Cloud key 有效:`GET /api/user` 返回 200 `{"status":"active"}`。 所以这不是 key 失效,而是 comfykit 与 Comfy Cloud 协议不兼容(路径前缀 + 鉴权头 + 轮询端点三处全不同)。 ## 目标 在 **BoosAPI** 里实现一个 **ComfyUI 原生协议 ↔ Comfy Cloud API 翻译适配器**:对外暴露 ComfyUI 原生端点(`/prompt`、`/history/{id}`、`/view`),内部翻译成 Comfy Cloud 的 `/api/*` 契约 + `X-API-Key` 鉴权。Pixelle/comfykit **一行代码都不改**,只把 `comfyui_url` 指向 BoosAPI 的适配端点即可。 Comfy Cloud 的 base URL + API Key 通过**站点管理**录入(新增 `comfy_cloud` 平台类型), 适配器从站点配置读取,而非散落的环境变量/配置文件。 ## 已确认的关键决策 1. 采用 **Approach A**:站点管理只作「配置载体 + 前端入口」(存 base_url + X-API-Key), 执行逻辑由独立的 Comfy Cloud 适配器承担;**不**把 Comfy Cloud 塞进 `/v1/*` token 路由 (它是工作流执行 API,不是聊天协议,塞进 token-router 跑不通)。 2. 适配器对外说 ComfyUI 原生协议,Pixelle/comfykit 零改动,只改 `comfyui_url` 配置。 3. 鉴权翻译:入口端点不强制校验(本机互信)/出口注入 `X-API-Key`。 4. 分支 `feat/comfy-cloud-adapter`,与 NIT 工作无关,不混入。 ## 非目标 - 不改 Pixelle / comfykit 源码。 - 不做 WebSocket 实时进度(comfykit HTTP 模式只轮询 `/history/{id}`,够用)。 - 不把 Comfy Cloud 接入 LLM token 路由/failover。 - 不做 Comfy Cloud 的计费集成(已有 `fetch_comfy_cloud_balance` 另说)。 ## 架构 ``` Pixelle(comfykit 0.1.12, 不改) comfyui_url = http:///comfy-cloud 说 ComfyUI 原生协议: POST /prompt · GET /history/{id} · GET /view · Bearer(忽略) │ ▼ BoosAPI 适配端点(新增 src/routes/comfy_cloud_adapter.rs) · POST /comfy-cloud/prompt → 翻译 → POST cloud.comfy.org/api/prompt · GET /comfy-cloud/history/{id} → 翻译 → GET /api/job/{id}/status (+ /api/jobs/{id}) 拼回 ComfyUI history 格式 · GET /comfy-cloud/view → 翻译 → GET /api/view (跟随 302) 鉴权: 出口注入 X-API-Key(从站点配置读) │ ▼ Comfy Cloud (https://cloud.comfy.org/api/*) ``` ## 站点管理集成(配置载体) - 前端 `SITE_PLATFORM_OPTIONS`(`src/web/pages/Sites.tsx`)新增一项 `{ value: 'comfy_cloud', label: 'Comfy Cloud', description: '工作流执行 API' }`。 - 站点记录复用现有字段:`url` 存 `https://cloud.comfy.org`,API Key 存入现有的 站点凭证字段(沿用站点已有的 key/header 存储方式;实现时确认 sites 表里承载 key 的列)。 - 适配器通过一个 helper `resolve_comfy_cloud_site(state) -> (base_url, api_key)`: 查 `platform = 'comfy_cloud'` 且 enabled 的站点,取其 url + key。无配置 → 明确报错。 ## 适配端点详细行为 ### POST /comfy-cloud/prompt - 入参:ComfyUI 原生 body `{ "prompt": {...}, "client_id": "..." }`。 - 出口:`POST {base_url}/api/prompt`,头 `X-API-Key: {key}` + `Content-Type: application/json`, body 原样转发(Comfy Cloud 接受相同的 `{prompt: ...}` 包裹)。 - 回包:Comfy Cloud 返回 `{ "prompt_id": "..." }`,原样回给 comfykit(字段名一致,无需改)。 ### GET /comfy-cloud/history/{prompt_id} comfykit 轮询此端点,期待 ComfyUI history 格式: `{ "{prompt_id}": { "status": {...}, "outputs": { "{node_id}": { "images": [...] } } } }`。 适配器: 1. `GET {base_url}/api/job/{prompt_id}/status` 拿 `{ "status": "completed|in_progress|..." }`。 2. 若未完成(pending/in_progress)→ 回 `{}`(comfykit 据此继续轮询)。 3. 若 completed → `GET {base_url}/api/jobs/{prompt_id}` 拿产出,翻译成 ComfyUI history 形状: ```json { "{prompt_id}": { "status": { "completed": true, "status_str": "success" }, "outputs": { /* node_id → {images|video|audio: [{filename,subfolder,type}]} */ } } } ``` 4. 若 failed/cancelled → 回带 `status_str: "error"` 的 history,让 comfykit 终止轮询并报错。 ### GET /comfy-cloud/view - 透传 query(`filename`/`subfolder`/`type`)→ `GET {base_url}/api/view?...`,头注入 X-API-Key。 - Comfy Cloud 回 302 签名 URL → 适配器跟随重定向取字节流回传(reqwest 默认跟随, 或手动取 Location 再 GET),Content-Type 透传。 ## 数据流(comfykit 视角,全程无感) 1. comfykit `POST {comfyui_url}/prompt` → 适配器 → Comfy Cloud → 返回 prompt_id。 2. comfykit 轮询 `GET {comfyui_url}/history/{id}` → 适配器翻译 job 状态 → 未完成回 `{}`, 完成回 ComfyUI history。 3. comfykit 从 outputs 取 filename → `GET {comfyui_url}/view?filename=...` → 适配器取回字节。 ## 错误处理 - 站点未配置 `comfy_cloud` → 适配端点返回 503 + 中文提示「未配置 Comfy Cloud 站点」。 - Comfy Cloud 返回非 2xx → 适配器透传状态码 + 上游错误体(便于排查,不吞错)。 - job failed/cancelled → 在 history 里标 error,comfykit 会抛 `Submit workflow failed` 类错误 (但这次是真实的执行失败,不是鉴权问题)。 ## 路由挂载 `src/routes/mod.rs` 的 `build_router` 加 `.merge(comfy_cloud_adapter::routes())`。 端点前缀 `/comfy-cloud/*`,不需要 admin/user 鉴权(本机内 Pixelle → BoosAPI 互信; 如需收紧可后续加内部 token,本期不做)。 ## 测试策略(TDD) - 单测翻译函数:Comfy Cloud `job status + jobs detail` JSON → ComfyUI history JSON 的纯函数 (`comfy_cloud_job_to_history`),覆盖 completed / in_progress / failed 三种。 - 单测 `resolve_comfy_cloud_site`:有配置返回 (url,key);无配置报错。 - 用 wiremock 起一个假 Comfy Cloud,集成测 `POST /comfy-cloud/prompt` 透传 + X-API-Key 注入、 `GET /comfy-cloud/history/{id}` 轮询翻译(沿用项目现有 wiremock 测试范式)。 ## 验证 - `cargo build -j 2`、`cargo test -p boosapi --lib comfy_cloud`。 - 真实联调:把 Pixelle `comfyui_url` 指向 `http:///comfy-cloud`,跑一次 selfhost 视频, 确认不再 401 且能取回产出。 ## 提交约定 分支 `feat/comfy-cloud-adapter`,提交按逻辑拆分(站点平台类型 / 适配器 service + 端点 / 路由挂载 / 前端平台选项),沿用项目中文 commit 风格。