Files
cmshoppe/docs/cmhub-integration-design.md
T

24 KiB
Raw Blame History

对接 cmhub AI 网关 · 设计文档

目标:把 cmshopee 的 AI 生成(生文/生图)从"本地直连多个第三方 provider"改为"对接 cmhub 计费型 AI 网关"。 性质:设计与评审文档,当前已拆分为 docs/06-tasks.md 的 T-526~T-528;本文件用于约束实现边界、迁移策略和验收重点。 参考:cmhub docs/api.md 对外接口契约;对接注意事项以 Obsidian 笔记《cmhub-接口对接文档-桌面端生文生图》为准(含幂等/超时/退点等运营级约定,本文已吸收);cmshopee app/ai.py(gen_title/gen_cover/generate_batch)、app/appconfig.py(config.json + config/ai_models.json)、docs/04-architecture.md §5.1b/§6.2。

v2 修订(2026-07-04,吸收对接文档):修正生图重试策略(非幂等,读超时不重发,避免重复扣点);超时按分辨率细化到 600s 上限;余额直接用响应 points_balance;错误处理改为按 code 优先分支 + 未知 code 当不可重试,补 content_blocked;确认 resolution=512/1K/2K/4K(大写)、aspect_ratio 默认 1:1。 v3 修订(2026-07-04,工程评审修正):补充既有配置迁移策略(缺 backend 先按 direct 处理,避免未配置 cmhub 时破坏现有生成);固定 cmhub Key 文件为 config/cmhub.json;新增 CMHubError 结构化错误要求;明确 points_balance/points_cost/call_id 通过回调事件传播而不是改变 gen_title/gen_cover 返回值;要求 cmhub 分支用可区分 connect/read timeout 的 HTTP 调用;把 image_url 下载安全校验纳入 T-526 验收;T-525 顺延,Phase 7 成为当前业务优先任务。 v3.1 修订(2026-07-04,实现前澄清):当时为保护既有直连流程,要求全新配置默认 backend 为 direct(cmhub 显式 opt-in),且 backend=cmhub 但未配置时须抛清晰"去⑤配置"错误而非崩溃(见 §4.1);② 明确"返回值不变"≠"回调不变"——计费元数据须给 gen_title/gen_cover 新增可选事件回调参数承载,是向后兼容加参(见 §4.2)。默认策略已被 v3.3/T-529 覆盖为普通产品默认 cmhub。 v3.2 修订(2026-07-04,同步对接文档新增):cmhub 新增第 4 个接口 GET /api/v1/models(别名自助发现,见 §4.6),⑤设置别名由手填改为动态下拉(按 operation_type 分生文/生图、过滤 unpriced、展示单价、requires_image 提示),关闭原待确认 #1;仅 Base URL 仍待部署方提供。 v3.3 修订(2026-07-04,产品收口):普通用户后续默认使用 cmhub 网关;⑤设置页去掉「AI 后端」label 和 direct/cmhub 下拉,直接展示 cmhub 网关配置。direct 代码和 config/ai_models.json 保留为内部兼容/手工回滚路径,但普通 UI 不提供切换入口;保存设置固定写 ai.backend=cmhub,允许先保存不完整 cmhub 配置,②生成时再提示去⑤补齐 Base URL/API Key/别名。已同步 T-529。 v3.4 修订(2026-07-07,T-538 路径收敛):打包版和源码运行的用户数据统一落在 data/ 下;本文早期提到的 config.json、config/ai_models.json、config/cmhub.json、images/,当前默认路径分别为 data/config.json、data/config/ai_models.json、data/config/cmhub.json、data/images/。文件名和 schema 不变,旧布局由启动迁移逻辑处理。 v3.4 核对(2026-07-06,刷新别名 notfound):核对对接文档 §4.4 与 cmhub ModelsView 路由,确认 cmshopee GET /api/v1/models + Bearer 请求已达标;「notfound」为 404,根因是 Base URL 带多余 /api(/v1) 路径(双拼)或所连实例未部署 /api/v1/models(见 docs/troubleshooting.md)。T-530 已落地:保存/请求前规整 Base URL 到网关根,HTTP 404 映射为 not_found 并给出中文排障提示。

1. 背景与目标

现状:app/ai.py 支持 cmhub 网关和 direct 兼容路径。direct 模式下每个模型在 data/config/ai_models.json 配 url/model/api_key/api_type,gen_title 自拼 chat messages 解析文本,gen_cover 走 images_edits multipart 或 vision chat 直接拿图片字节本地存。

目标:改为对接 cmhub 的生成、余额和模型发现接口:POST /api/v1/generate/title、POST /api/v1/generate/image、GET /api/v1/balance、GET /api/v1/models,一把 Authorization: Bearer <API_KEY> 即可调用。上游 provider、密钥、计费、SSRF 防护、分辨率映射、对象存储与别名发现由 cmhub 承担。

收益:密钥收敛(本地只留一把 cmhub Key);换上游模型对 cmshopee 零改动(cmhub 用能力别名);可删除大量 provider 适配代码;计费/额度统一。

2. 契约对照

维度 cmshopee 现状 cmhub 接口
鉴权 每模型一把 Bearer api_key 一把 Bearer <API_KEY>
生文请求 自拼 chat messages(system+user) {prompt, model:别名, image_url?/image_base64?, resolution?, parameters?}
生文响应 chat completion → 取单条文本 {titles:[...], alias, model_used, points_cost, points_balance, call_id}
生图请求 images_edits multipart 或 vision chat {prompt, model:别名, image_base64?/image_url?, resolution?, aspect_ratio?, parameters?}
生图响应 直接返回 image bytes {image_url, ...} → 需再下载
错误 HTTP error 文本 {error:{code,message}}:insufficient_points(402)/upstream_error(502)/rate_limited(429)/unauthorized(401)/account_disabled(403)/bad_request(400)/model_not_allowed/no_pricing_rule
超时 按 resolution_timeouts 生图同步且慢;当前 cmshopee 生图请求和图片下载读取等待统一固定 650s,不再按分辨率变化;生图读超时仍不自动重发
幂等 直连一次成功一次 非幂等、无幂等键:客户端超时 ≠ 未扣点,读超时后不可无脑重发

关键差异(决定改造点):

  • 生文 model 传能力别名(如 title-standard),不是具体模型名;生文返回列表 titles。
  • 生图返回 image_url(对象存储),cmshopee 要多一步下载再本地转 JPEG。
  • 计费错误 insufficient_points(点数不足)是新的用户可见失败态。

3. 设计原则与边界

  • 接缝最小化:保持 gen_title(...) / gen_cover(...) 的返回值与现有调用兼容(gen_title→标题字符串、gen_cover→已存 JPEG 路径),允许向后兼容新增可选事件回调参数承载计费元数据,只改内层实现。这样 generate_batch 编排、并发、重试、DB 写入、诊断日志、JPEG 落盘、data/images/<batch_id>/<slug>/... 路径、T-520 封面开关全部复用、零改动;GUI run_logs 记录和余额展示放到 T-528。
  • 不碰高风险层:editor.py/cdp.py/chrome.py/accounts.py/excel.py/db.py/workers.py,以及 ①采集/③更新/④账号全流程完全不动。这是本改动最重要的安全边界。
  • 产品默认 cmhub,direct 保留兼容:内层仍抽 backend(cmhub / direct),但普通产品默认走 cmhub,⑤设置页不再展示后端切换。老 ai_models.json 和 direct 分支不删除,作为内部兼容/手工回滚路径;普通用户只配置 cmhub Base URL / Key / 别名。

4. 方案

4.1 配置 schema

在 config.json 的 ai 段新增 cmhub 子段(app/appconfig.py 默认值 + 校验):

"ai": {
  "backend": "cmhub",                // 普通产品默认 cmhub;direct 仅内部兼容/手工回滚
  "cmhub": {
    "base_url": "https://<cmhub>",    // 网关根地址,请求时拼 /api/v1/...
    "title_alias": "title-standard",  // 生文能力别名
    "image_alias": "image-hd",        // 生图能力别名
    "connect_timeout": 10,
    "check_balance_before_batch": false
  },
  // 现有字段保留:resolution / jpg_quality / *_concurrency / retry /
  // resolution_timeouts / generate_cover(T-520)等,backend 无关,继续用
}
  • cmhub API Key 不进 data/config.json(避免与其它设置混放、避免误提交)。固定存到 data/config/cmhub.json,文件 schema 第一版为 { "api_key": "sk_cmhub_xxx" };新增 CMHUB_CONFIG_PATH、load_cmhub_config()、save_cmhub_config()、get_cmhub_api_key(masked=False) 等 helper;data/config/cmhub.json 必须加入 .gitignore。沿用现有"本地明文保存但 gitignore + UI 打码 + 日志脱敏"纪律(T-503)。
  • ai_models.json 去留:direct 模式继续用;cmhub 模式不读它。文件保留但标记 legacy。
  • 默认 backend 与配置不完整处理(T-529 后):DEFAULT_CONFIG / default_config() 造全新配置时写 backend=cmhub;⑤设置页隐藏 direct/cmhub 下拉并固定保存 backend=cmhub。允许先保存不完整 cmhub 配置,便于用户先保存其它路径/安全设置;②生成真正调用时如果 base_url/Key/别名缺失,必须抛清晰的"请去⑤配置 cmhub"错误(CMHubError/AIError),不得崩溃或静默回退 direct。显式手工配置 backend=direct 仍作为内部回滚路径保留,但普通 UI 不提供入口。
  • Base URL 必须是网关根:只填 https://<cmhub-域名>,不带 /api、/api/v1 或任何路径。cmhub_request_url() 会自行拼 /api/v1/...;若 Base URL 已含 /api/v1,会双拼成 .../api/v1/api/v1/... → 404(正是「刷新别名 notfound」现象,见 docs/troubleshooting.md)。当前请求本身已达标(GET /api/v1/models + Authorization: Bearer <key>,对齐对接文档 §4.4 与 cmhub ModelsView 路由);404 的根因是 Base URL 带多余路径或所连实例未部署 /api/v1/models。T-530 已在保存/请求前规整 Base URL,会去掉多余路径、查询串和片段,只保留 scheme+host(+port);404 会给出明确中文提示。

4.2 gen_title 改造(cmhub 分支)

返回值不变,现有调用保持兼容;允许新增可选事件回调参数承载计费元数据。内层按 backend 分流:

返回值不变不等于回调不变(关键实现说明):现在 gen_title/gen_cover 的 on_step 回调只传一个步骤字符串(_notify_step(cb, step))。要把 points_cost/points_balance/call_id "通过回调上报且不改返回值",就必须给回调接口扩容——推荐给 gen_title/gen_cover 新增一个可选事件回调参数(如 on_meta / on_event)传结构化计费元数据,或把 on_step 的 payload 从 str 升级为结构化对象。"返回值不变"成立,但回调契约会变大;这是向后兼容的加参,generate_batch 显式传回调、不受影响。实现时不要为了"少改参数"把计费数据塞进返回值或用中文串旁路传递。

  • 请求:POST {base_url}/api/v1/generate/title,头 Authorization: Bearer <cmhub_key>,体:
    { "prompt": <title_prompt + 旧标题合成为单串>, "model": <title_alias>, "resolution": <可选> }
    
    T-549 后 direct 与 cmhub 共用标题 prompt 组装:若标题提示词包含 {旧标题},先替换该占位符且不重复追加旧标题块;若不包含,则保持旧行为自动追加「旧标题:...」。两种情况都把「提示词 + 旧标题/占位符结果 + 只返回新标题」折叠为一个最终 prompt 字符串。
  • 响应:解析 titles,取 titles[0](当前一条任务要一个新标题);空列表/空串按现有语义抛 AIError("AI 返回为空标题")。
  • 不复用现有 _call_with_retry 的一刀切重试逻辑;cmhub 分支新增专用 HTTP helper,优先使用项目已依赖的 requests,传 timeout=(connect_timeout, read_timeout) 以区分连接超时和读超时。model_used/points_cost/points_balance/call_id 不改变返回值,通过 on_step/事件回调上报给 generate_batch 与 GUI worker;T-526 只保证 metadata 事件完整传出,不直接要求写 GUI run_logs,T-528 再由 GUI worker 脱敏写 run_logs 和展示余额;不入 Excel。

4.3 gen_cover 改造(cmhub 分支)

返回值不变(仍返回已存 JPEG 路径),现有调用保持兼容;允许新增可选事件回调参数承载计费元数据。内层:

  • 请求:POST {base_url}/api/v1/generate/image,体:
    { "prompt": <cover_prompt>, "model": <image_alias>,
      "image_base64": <旧封面转 data URL>, "resolution": <resolution>, "aspect_ratio": <可选> }
    
    旧封面必传(改图类),复用现有 _image_data_url(old_cover_path) 生成 base64。
  • resolution 归一为大写 512/1K/2K/4K(cmshopee 内部用小写 1k,发请求前转 1K);aspect_ratio 默认 1:1(Shopee 封面)。
  • 响应:拿 image_url → 新增一步下载该图字节(cmhub 自家对象存储公网 URL)→ 交给现有 _save_jpeg(image_bytes, out_path, resolution, quality) 落盘。下载 helper 必须校验 URL scheme 只允许 http/https,拒绝内网/回环/本机地址,并校验域名解析后的 IP 仍不属于内网/回环/本机地址,设置超时和大小上限;生成后立即下载(对象存储 URL 可能有有效期)。points_cost/points_balance/call_id 同样通过事件回调传播,不改变 gen_cover 返回值。
  • 超时(关键):生图同步且慢。当前 cmshopee 的 cmhub 生图请求和随后 image_url 下载读取等待统一固定 650 秒,不再按分辨率变化,绝不用 30s/60s 调生图——否则客户端超时但服务端仍在算并扣点(见 §4.4 幂等)。
  • 并发(T-545 已实现):最近实测 /media/generated/images/*.png 下载链路在 10 并发下明显慢且有连接失败。cmhub 模式下采用内置保护:实际生图请求并发 = min(ai.image_concurrency, 5);下载/保存使用独立线程池,线程数与实际生图请求并发一致,同样最大 5;不新增用户可见配置项。运行日志必须同时显示用户设置和实际并发,避免用户误解设置 10 就会对 cmhub 打 10 并发。下载失败记为该任务失败,不得重新调用生图接口导致重复扣点;读超时仍按 §4.4 的非幂等规则处理。
  • 下载后端(T-548 已实现):cmhub 生成/models/balance 仍走共享 requests Session;仅 image_url 图片下载可按 ai.cmhub.download_with_curl 选择系统 curl。默认 auto 在 Windows 且检测到系统 curl 时优先 curl,非 Windows、无 curl 或 curl 失败自动回退 requests。curl 调用前仍做公网 URL 校验,用 -K 临时配置文件传 URL,避免 token 出现在进程命令行;use_system_proxy=false 时加 --noproxy "*"。

4.4 错误映射与重试策略

cmhub 返回结构化 {error:{code}}。映射层按 code 优先分支(不要只看 HTTP 状态),并决定是否重试。实现上新增 CMHubError(AIError),至少带 code、message、status、retryable、retry_after 字段;GUI 和 worker 不靠中文字符串判断错误类型:

cmhub code HTTP 处理 是否重试
insufficient_points 402 明确提示「点数不足,请先充值」,整批可提前中止 否(禁止循环重试)
unauthorized 401 提示 cmhub API Key 无效,去⑤设置重填 否
account_disabled 403 提示 Key 被吊销/账号禁用,去网页端重生成 否
bad_request / model_not_allowed / no_pricing_rule / content_blocked 400 记录具体 code,判为配置/参数/内容错 否
upstream_error 502 上游失败(cmhub 已自动退点),可提示稍后重试 是(安全)
rate_limited 429 退避重试,读 Retry-After(默认限流 60次/分) 是
未列出的 code 任意 展示 message,当不可重试错误(前向兼容) 否

幂等与超时——生图重试必须特别处理(会亏钱):cmhub 生成接口非幂等、无幂等键,客户端超时 ≠ 未扣点。

  • 生图(gen_cover):读超时后绝不自动重发——服务端可能已算完并扣点,重发 = 重复扣点。首选办法是把读超时设够大(§4.3,当前固定 650s)从源头避免歧义;只对连接超时(请求根本没送达服务端)安全重试。
  • 生文(gen_title):秒级返回、点数低,读超时重试风险小,但仍建议同样区分连接超时/读超时;重试次数可小。
  • 只对 502/429/连接超时重试;402/401/403/400/读超时立即失败。现状 _call_with_retry 是一刀切重试,cmhub 模式必须替换为这套区分策略。
  • insufficient_points 是新的用户可见态:② 生成页应弹明确提示并引导去网页端充值,不当普通失败淹没在计数里。

4.5 余额展示与额度预检

  • 每次成功响应都带 points_balance——直接用它刷新 ② 页的剩余点数显示,不必每次再调 /balance。由于 gen_title/gen_cover 返回值保持不变,生成结果的 points_cost/points_balance/call_id 通过 on_step/on_event 结构化事件上报;T-526 只保证事件传递,T-528 再记入 run_logs(脱敏)并刷新余额,便于对账与报运营排障。
  • GET /api/v1/balance 当前返回结构:{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }。该接口仅用于手动刷新余额或批量前可选预检(check_balance_before_batch=true);⑤设置页测试连接成功时优先用 account.display_name 显示账号名,没有时再用 account.username / user 兜底。非必需,第一版可只依赖成功响应里的余额 + 失败时的 insufficient_points 提示。

4.6 别名发现(GET /api/v1/models)

对接文档已把别名发现从"未来提供"落地为真实接口(第 4 个接口,鉴权同为 Bearer Key):

  • 响应 {models:[{alias, operation_type:"title"|"image", capabilities[], requires_image:bool, pricing_status:"priced"|"unpriced", prices:[{resolution, points_cost}]}]}——只含别名侧信息,不含具体模型名/URL/密钥。
  • ⑤设置的别名由手填改为动态下拉:新增 app/appconfig.py(或 app/ai.py)helper fetch_cmhub_models(base_url, api_key),⑤按 operation_type 拆成「生文别名 / 生图别名」两个下拉;requires_image 供 UI 提示(cmshopee 生图恒传旧封面 base64,天然满足);pricing_status="unpriced" 的别名不放入下拉(直接调用会 no_pricing_rule);单价用 prices 展示。
  • 拉取时机:⑤ 打开或用户点「测试连接/刷新别名」时拉一次,选中的别名仍持久化到 config.json 的 ai.cmhub.title_alias/image_alias(网关临时不可达时用已存值)。
  • 这关闭了原待确认 #1(别名清单)——不再需部署方单独提供;仅 Base URL 仍待部署方给。

5. 各模块改动点

模块 改动 量
app/ai.py gen_title/gen_cover 加 cmhub 分支(请求体+解析+生图下载);抽 backend 选择;错误映射 + 区分重试。direct 分支保留现有代码 M
app/appconfig.py ai 段加 backend/cmhub 子段默认值与校验;cmhub Key 的读写与打码(复用脱敏工具);新增 cmhub_request_url() 类 helper S
app/gui/tabs/settings.py / app/gui/workers.py ⑤ AI 设置按 backend 切换:cmhub 模式显示「网关地址 + API Key + 生文/生图别名 + 测试连接/查余额」;direct 模式保留现有 master-detail;测试连接/查余额走后台 worker。最大 UI 触点 M
测试 tests/test_ai.py 增 cmhub mock(titles 列表、image_url 下载、安全下载、各错误码与重试、连接/读超时差异);test_appconfig 加 schema 与 data/config/cmhub.json helper;⑤ gui 设置测试跟随 M
文档 docs/04-architecture.md §5.1b/§6.2、docs/api.md、docs/03-tech-stack.md、current-state.md 同步 S

明确不动(T-526):editor.py/cdp.py/chrome.py/accounts.py/excel.py/db.py,以及 ①采集/③更新/④账号全流程;generate_batch 主编排、图片本地路径方案和 T-520 封面开关保持原语义。T-527/T-528 可按任务边界修改 app/gui/tabs/settings.py、app/gui/tabs/generate.py、app/gui/workers.py 的设置与用户提示层。

6. 安全与合规

  • cmhub API Key 本地明文保存但 gitignore,UI 打码,日志/导出脱敏(T-503 纪律);不写进代码/文档/config.json。
  • cmhub 返回的 image_url 是外部地址,cmshopee 下载时设超时 + 大小上限,只允许 http/https;不下载内网/回环地址,并校验域名解析后的 IP 仍不是内网/回环/本机地址(cmhub 侧已做 SSRF,但本地下载再校验一层更稳)。
  • run_logs/诊断日志可记 alias/model_used/points_cost/points_balance/call_id 便于排障,但不得记 API Key、完整请求体、base64 图片、超长 prompt(沿用 diagnostics 脱敏)。

7. 测试计划

  • 单元(mock cmhub,不连真实网关):
    • 生文:titles 多条取首条;空 titles/空串 → AIError。
    • 生图:image_url → mock 下载字节 → _save_jpeg 落盘校验分辨率/质量;覆盖 scheme、内网/回环字符串地址、域名解析到内网 IP 的拒绝路径。
    • 错误码矩阵:402/401/403/400 不重试且原因正确;502/429/连接超时按 attempts 重试;生图读超时不重发;未知 code 不重试。
    • 配置:backend=cmhub 走 cmhub 分支、显式 direct 走旧分支;T-529 后缺 backend 的配置按 DEFAULT_CONFIG 补为 cmhub;缺 base_url/Key/别名时明确报错。
  • GUI:⑤ cmhub 面板读写、测试连接 worker、别名下拉;② 生成在 insufficient_points 时的提示路径。
  • 回归:direct 模式现有 test_ai.py 用例保持绿。
  • 验证命令沿用 python -m unittest discover -s tests。

8. 迁移与回退

  • T-529 后普通配置默认 backend=cmhub;显式手工配置 backend=direct 仍可作为内部回滚路径。⑤普通 UI 不再提供后端切换,用户只需配好 base_url + Key + 两个别名。
  • 回退:backend=direct 立即切回本地直连,ai_models.json 仍有效。
  • 灰度:可先在 ② 单条生成上验证 cmhub 联通与计费,再放批量。

9. 待 cmhub 侧确认的问题

  1. 别名清单 已解决:GET /api/v1/models(§4.6)实时自查可用别名/单价/是否需原图,⑤动态渲染下拉,不再需部署方单独提供别名清单。
  2. 生文看图(可先不传):接口支持 image_url/image_base64,但当前 cmshopee 生文只用旧标题文本,第一版不传图。
  3. resolution 取值 已解决:512/1K/2K/4K(大写 K),默认 1K;cmshopee 小写值发请求前归一。
  4. aspect_ratio 已解决:默认 1:1,Shopee 封面用 1:1。
  5. image_url 有效期(按最坏处理):对象存储 URL 可能过期——本设计已是生成后立即下载落盘,无需长期持有。
  6. 超时上限 已解决:当前 cmshopee 生图请求和图片下载读取等待统一固定 650s。
  7. Base URL / API Key 形态:域名待部署方提供;Key 形如 sk_cmhub_xxx,仅网页端生成时显示一次——⑤设置需提示用户从网页端复制粘贴,本地保存。

10. 落地拆分与任务顺序

  • 第一步:app/ai.py + app/appconfig.py 接入 cmhub backend(mock 联调),保留 direct。
  • 第二步:⑤设置 UI cmhub 面板 + 测试连接/查余额。
  • 第三步:② 计费错误提示(insufficient_points 引导充值)+ 可选余额展示。
  • 第四步(T-529):产品默认 cmhub,⑤去掉 AI 后端选择,保存固定 backend=cmhub。
  • 第五步(T-530):Base URL 规整到网关根,404 给出明确中文提示。
  • 文档随每步同步。

当前已在 docs/06-tasks.md 落成 T-526~T-535,且 T-525 工程基础设施已完成;下一步按看板进入 T-539 设置简化,仍遵守 docs/05-coding-rules.md 验证清单。