在 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>
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 平台类型),
适配器从站点配置读取,而非散落的环境变量/配置文件。
已确认的关键决策
- 采用 Approach A:站点管理只作「配置载体 + 前端入口」(存 base_url + X-API-Key),
执行逻辑由独立的 Comfy Cloud 适配器承担;不把 Comfy Cloud 塞进
/v1/*token 路由 (它是工作流执行 API,不是聊天协议,塞进 token-router 跑不通)。 - 适配器对外说 ComfyUI 原生协议,Pixelle/comfykit 零改动,只改
comfyui_url配置。 - 鉴权翻译:入口端点不强制校验(本机互信)/出口注入
X-API-Key。 - 分支
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": [...] } } } }。
适配器:
GET {base_url}/api/job/{prompt_id}/status拿{ "status": "completed|in_progress|..." }。- 若未完成(pending/in_progress)→ 回
{}(comfykit 据此继续轮询)。 - 若 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}]} */ } } } - 若 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 视角,全程无感)
- comfykit
POST {comfyui_url}/prompt→ 适配器 → Comfy Cloud → 返回 prompt_id。 - comfykit 轮询
GET {comfyui_url}/history/{id}→ 适配器翻译 job 状态 → 未完成回{}, 完成回 ComfyUI history。 - 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 detailJSON → 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 风格。