From 5f5bf73949be8141609fc6e76d34b7d07ef97f98 Mon Sep 17 00:00:00 2001 From: chengma Date: Wed, 8 Jul 2026 23:18:35 +0800 Subject: [PATCH] docs(tasks): add T-564 async cmhub image task integration (submit + poll) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 对接 cmhub 已实现的异步生图接口(POST /generate/image/tasks + 轮询), 替代当前 900s 同步长等待:submit 拿 task_id、轮询到 succeeded 再下载、 task_id 持久化支撑重启续查、per-attempt 幂等键防重复扣点。含三处结构 改动、五类小改、单测点与 v3.6 文档修订项。 Co-Authored-By: Claude Opus 4.8 --- docs/tasks/T-564.md | 71 +++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 71 insertions(+) create mode 100644 docs/tasks/T-564.md diff --git a/docs/tasks/T-564.md b/docs/tasks/T-564.md new file mode 100644 index 0000000..89bc2d1 --- /dev/null +++ b/docs/tasks/T-564.md @@ -0,0 +1,71 @@ +--- +id: T-564 +title: 生图对接 cmhub 异步任务接口(submit + 轮询 + task_id 续查),替代 900s 同步长等待 +phase: 7 +deps: [T-526, T-529, T-546] +status: TODO +created: 2026-07-08 +--- + +## 问题 / 背景 + +当前 cmshopee 生封面(`app/ai.py`)是两段串行同步阻塞:`POST /api/v1/generate/image` 读超时 900s 死等,拿到 `image_url` 后再下载最长 900s。上游卡死时 cmshopee QThread 与 cmhub Gunicorn worker 双双被占满 15 分钟,且第 899 秒抖动会「扣了点却拿不到图、无法找回」(这步 `retryable=False` 防重复扣点)。 + +cmhub 侧已实现并测试通过异步任务化接口(新增 `/generate/image/tasks` 子资源,T-612~T-615),旧同步接口保持不变仍可用。本任务把 cmshopee 生图链路切到新异步路:submit 拿 `task_id` → 轮询到 `succeeded` → 下载保存;`task_id` 持久化支撑重启续查、幂等键防重复扣点。 + +设计与逐点落地评估见 Obsidian「生图接口异步任务化-提交轮询方案」§「对接契约 v3」+「cmshopee 侧落地评估」。**对接字段/错误码以该文 v3 节与 cmhub `docs/api.md` 为准。** + +## 方案(改哪个文件、改成什么) + +### 接口(cmhub 已实现,本任务对接) +- submit:`POST /api/v1/generate/image/tasks` → `202 {task_id, status:queued, call_id, points_cost, points_balance, expires_at}`(**提交即预扣**)。请求体与旧生图一致;头带 `Idempotency-Key`、`X-Client-Version`。 +- poll:`GET /api/v1/generate/image/tasks/{task_id}` → 除 404 外恒 `200`,按 `status` 分支:`queued/running` / `succeeded`(带 `result.image_url`) / `failed`|`expired`(带 `error.code`,**均已退点**)。 +- 无 cancel 接口。 + +### `app/ai.py` +1. `_request_cmhub_cover_image`(`ai.py:900`)改为两段: + - **submit**:POST `/generate/image/tasks`,带 `Idempotency-Key`、`X-Client-Version`;连接超时保留 66s、读超时降 ~30s。拿 `task_id` 后**立即写库 `image_task_id`**(见结构改动 ①)。 + - **poll**:循环 GET,递增退避 `3s→5s→8s→10s` 封顶带抖动,撞本地总预算(默认 900s,可配)才放弃;`succeeded` → 返回 `result.image_url` 交给原 `_download_and_save_cmhub_cover`;`failed`/`expired` → 抛结构化 `CMHubError`(已退点,按可重试);撞预算 → 超时错误但**保留 `task_id`**。 +2. **续查前置**:进 submit 前先看 task 有无 `image_task_id`,有则直接 GET 续查,命中 `succeeded` 取图,**不重新 submit**。 +3. `_cmhub_call_once`(`ai.py:1184`)加请求头 `Idempotency-Key`、`X-Client-Version`。 +4. 错误码表 / `_cmhub_retryable`(`ai.py:1252-1265`)补 `upstream_timeout`、`task_timeout`(可重试、已退点)。 +5. 超时分段:不再用 `_cmhub_read_timeout` 恒 900(`ai.py:1074`),submit/poll 各自超时。 +6. 余额/计费事件回调(`_emit_cmhub_metadata`)在 submit 202 触发一次(`points_cost/points_balance/call_id`)。 +7. 重试语义分层:失败重试是「换新 Idempotency-Key 重新 submit」的上层重跑,不走 `_cmhub_call_with_retry`(`ai.py:1149`)复用同 payload 的内层重试;内层对 submit 关闭或强制带同一幂等键。 + +### 结构改动 +- **① request 段写库落 `task_id`**:`_request_cmhub_cover_image` 现无状态、不碰 DB。需把 `task.id` + 持久化回调(或 `db_path`)传进去,在 submit 成功点写 `image_task_id`。调用点在 `run_cmhub_cover_tasks`(`ai.py:687`)。 +- **② per-attempt 幂等键**:键用 `f"{task.id}:{generate_attempts}"`(复用 `app/db.py:239` 的 `generate_attempts`),保证「手动重新生成封面」不撞回旧任务,而「同一次 submit 网络抖动重发」仍幂等返回同一 `task_id`。 +- **③ 轮询占 request-pool 线程**:submit+poll 同步塞进 `request_executor` future 即可(本地线程、廉价,非稀缺资源);`cmhub_image_concurrency_plan`(`ai.py:241`)现约束「在途轮询数」,第一版保持池形、可放宽上限,不重构编排。 + +### `app/db.py` +- 加列 `tasks.image_task_id TEXT`(沿用现有 `ALTER TABLE ADD COLUMN` ad-hoc 迁移模式,参考 `db.py:363`)。 +- 提供读写 `image_task_id` 的辅助(或并入现有 task 更新路径)。 + +### 文档 +- `docs/cmhub-integration-design.md` 出 v3.6:不推翻 v3.5 全量,标注「旧同步接口沿用 v3.5 口径 + 新增任务化接口口径」,记弃用计划。 +- 同步 `docs/api.md`、`docs/current-state.md` 里生图链路的描述。 + +## 验收要点 + +- submit 返回 202 → `image_task_id` **已落库**、进入轮询、不阻塞主线程;单测断言写库发生在轮询前。 +- 轮询 `running`→`succeeded` → 走下载保存、`gen_cover` 仍返回已存 JPEG 路径;`failed`/`expired`(已退点) → 抛结构化错误、按可重试、**不重复扣点**。 +- **重启续查**:已有 `image_task_id` 的 task 直接 GET,命中 `succeeded` 取图,**无二次 submit、无二次扣点**(断言未调 POST /tasks)。 +- **幂等键 per-attempt 隔离**:同 task 同 `generate_attempts` 重发 → 同 `task_id`;`generate_attempts` 递增后 → 新 `task_id`。 +- 撞本地总预算未出结果 → 超时错误但 `image_task_id` 已持久化、可续查。 +- 轮询期 `should_cancel()` → 停止轮询、不再下载(服务端继续跑完,按结果结算)。 +- 错误码 `upstream_timeout`/`task_timeout`/`content_blocked`/`insufficient_points`/`idempotency_conflict` 各有对应处理与中文提示。 +- 未配置 cmhub / Base URL 双拼等既有错误口径不回归。 +- 验证命令:`py -3.10 -m pytest tests/test_ai.py -q`(新增异步路用例,mock `_cmhub_session()` 的 request/get 按 status 分支返回)。 + +## 边界(不改什么) + +- 不改旧同步 `POST /api/v1/generate/image` 的对接(保留为兼容/回滚路径)。 +- 不改 `gen_cover(...)` 对外返回值(仍返回已存 JPEG 路径)、`generate_batch` 编排骨架、`_download_and_save_cmhub_cover` 下载/保存逻辑、`data/images///` 路径、封面开关。 +- 不改 CDP 选择器 / Shopee 交互 / Excel / 生文链路。 +- 不引入 cancel 接口(cmhub 本期未实现)。 +- 不做 DB 版本化迁移重构(沿用 ad-hoc ALTER;版本化仍在 backlog)。 + +## 执行记录 + +(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)