Files
cmshoppe/docs/tasks/T-564.md
T
chengmaandClaude Opus 4.8 5f5bf73949 docs(tasks): add T-564 async cmhub image task integration (submit + poll)
对接 cmhub 已实现的异步生图接口(POST /generate/image/tasks + 轮询),
替代当前 900s 同步长等待:submit 拿 task_id、轮询到 succeeded 再下载、
task_id 持久化支撑重启续查、per-attempt 幂等键防重复扣点。含三处结构
改动、五类小改、单测点与 v3.6 文档修订项。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-08 23:18:35 +08:00

72 lines
6.3 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
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/<batch_id>/<slug>/` 路径、封面开关。
- 不改 CDP 选择器 / Shopee 交互 / Excel / 生文链路。
- 不引入 cancel 接口(cmhub 本期未实现)。
- 不做 DB 版本化迁移重构(沿用 ad-hoc ALTER;版本化仍在 backlog)。
## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)