Files
BoosAPI/docs/superpowers/specs/2026-06-17-comfy-cloud-adapter-design.md
T
boos e1d0ade434 docs: Comfy Cloud 工作流执行适配器设计 spec
在 BoosAPI 内做 ComfyUI 原生协议↔Comfy Cloud API 翻译适配器:对外暴露
/comfy-cloud/{prompt,history,view},内部翻译成 cloud.comfy.org/api/* + X-API-Key。
Comfy Cloud base_url+key 经站点管理(新增 comfy_cloud 平台类型)录入。
Pixelle/comfykit 零改动,只改 comfyui_url 指向适配端点。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-06-17 22:56:55 +08:00

7.3 KiB

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://<boosapi-host>/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 形状:
    { "{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://<boosapi>/comfy-cloud,跑一次 selfhost 视频, 确认不再 401 且能取回产出。

提交约定

分支 feat/comfy-cloud-adapter,提交按逻辑拆分(站点平台类型 / 适配器 service + 端点 / 路由挂载 / 前端平台选项),沿用项目中文 commit 风格。