docs: record async image task tradeoffs

This commit is contained in:
QiuSW
2026-07-08 23:11:46 +08:00
parent d5656b4f56
commit d79bd6fae9
6 changed files with 28 additions and 4 deletions
+4 -2
View File
@@ -41,7 +41,7 @@ T-301 已实现 `ApiKeyAuthentication` 与 `ExternalApiView`:外部 API 使用
T-302 已实现 `/api/v1/generate/title` 与 `/api/v1/generate/image`:API 层只做鉴权、参数校验和编排;别名解析、Provider 选择、计费计算、预扣、成功确认、失败退点分别调用 `apps.ai` / `apps.billing` 既有模块。T-613 已把生成链路抽为 `apps.api.generation` 的核心阶段:`prepare_generation()` 负责审核、图片输入、别名、Provider 与计费准备;`precharge_generation()` 只调用 billing 预扣;`execute_precharged_generation()` 复用已预扣 `CallRecord` 调上游并成功确认或失败退点,供旧同步接口和后续异步 worker 共用。图片结果 MVP 先用本地 `default_storage` 保存到 `MEDIA_ROOT/generated/images/...` 并返回 `image_url`;核心阶段通过 URL 构建器生成外部 URL,不依赖 DRF `Request`;`CallRecord` 只写 URL / 摘要,不保存 provider `raw` 或 base64。
T-614 已实现 `/api/v1/generate/image/tasks` 与 `/api/v1/generate/image/tasks/{task_id}`:提交接口同步审核 prompt、解析图片输入和预扣点,创建 `ImageGenerationTask(status=queued)` 后立即返回公开 UUID `task_id`;后台 worker 通过 `select_for_update(skip_locked)` 抢任务,复用 T-613 `execute_precharged_generation()` 对已预扣 `CallRecord` 调上游、保存结果、成功确认或失败退点。任务表记录 worker 租约、心跳和尝试次数;reaper 识别僵尸 `running` 任务后默认判失败并幂等退点,不默认重排队。异步 worker 没有 DRF request,结果 URL 由 `MEDIA_PUBLIC_BASE_URL` / `PUBLIC_BASE_URL` 生成。
T-614 已实现 `/api/v1/generate/image/tasks` 与 `/api/v1/generate/image/tasks/{task_id}`:提交接口同步审核 prompt、解析图片输入和预扣点,创建 `ImageGenerationTask(status=queued)` 后立即返回公开 UUID `task_id`;后台 worker 通过 `select_for_update(skip_locked)` 抢任务,复用 T-613 `execute_precharged_generation()` 对已预扣 `CallRecord` 调上游、保存结果、成功确认或失败退点。任务表记录 worker 租约、心跳和尝试次数;reaper 识别僵尸 `running` 任务后默认判失败并幂等退点,不默认重排队。异步 worker 没有 DRF request,结果 URL 由 `MEDIA_PUBLIC_BASE_URL` / `PUBLIC_BASE_URL` 生成。当前实现中 worker 会基于任务快照再次运行 `prepare_generation()`,因此会复审 prompt、重解析别名 / Provider / 定价;账务仍使用已预扣 `CallRecord.points_cost`,不会重复扣点。这个取舍偏安全(排队期间敏感词库更新后仍能拦截并退款),但如果未来队列积压明显,应单独实现“提交时模型配置快照”,避免执行时别名映射变化导致按旧价预扣、按新模型执行。
T-615 已给旧同步生图接口和新异步提交接口接入 `cmhub.api.generation_usage` 结构化日志,事件名为 `generation_route_usage`。日志字段只包含 `route_type(sync/async)`、`api_key_id`、`api_key_prefix`、`user_id`、`client_version`、`alias`、`status`、`latency_ms`、`error_code`、`http_status` 等白名单信息;不得记录 API Key 明文、prompt 全文、`image_base64`、provider raw 或上游密钥。第一版用日志查询完成用量观察,不新增报表表结构;如后续要在 admin 做统计报表,需单独评估数据量、索引和保留周期。
@@ -391,7 +391,7 @@ CREATE TABLE image_generation_task (
```text
1. API Key 鉴权 + serializer 参数校验。
2. prompt 内容安全审核;命中 content_blocked → 400,不建任务、不扣点、不调上游。
3. 图片输入校验 / 解码:image_base64 解码后保存为输入文件引用,任务快照不保存 base64 原文;image_url 执行公网/协议/大小校验。
3. 图片输入校验 / 解码:image_base64 解码后保存为输入文件引用,任务快照不保存 base64 原文;image_url 在 submit 阶段执行公网/协议/大小校验并下载为输入文件引用。
4. 别名、Provider 能力、PricingRule 校验;缺规则或能力不符 → 不扣点。
5. 事务:锁钱包预扣点,写 CallRecord(pending) + PointsLedger(consume),创建 ImageGenerationTask(queued, call_record=已预扣调用)。
6. 立即返回 202 + task_id。
@@ -408,6 +408,7 @@ CREATE TABLE image_generation_task (
- 提交即预扣,避免余额只够 1 张却排入大量任务;余额不足仍返回 `402 insufficient_points`。
- `Idempotency-Key` 按当前 API Key 去重,同 key 同 payload 返回同一任务且不重复扣点;同 key 不同 payload 返回 `409 idempotency_conflict`。
- 桌面端主链路推荐传 `image_base64`,submit 阶段只做解码和落盘,通常是毫秒级;`image_url` 输入会在 submit 阶段下载并可能阻塞,属于边缘路径,换来的是任务自包含和 worker 不再访问调用方外部 URL。
- worker 是至少一次执行模型,但账务终态和结果终态必须 exactly-once:不得重复扣/退,不得覆盖已成功结果,也不得把 reaper 已失败退款的任务改回成功。
- 查询接口只按 `task_id` + 当前 API Key 所属用户查任务,防止 IDOR;成功任务重复查询返回同一 `result_url`。
@@ -469,6 +470,7 @@ CREATE TABLE image_generation_task (
| 回调伪造 | 不验签会被刷点 | 强制验签,验签失败不入账并留痕 |
| 图片生成耗时长 | 同步等待几十秒到分钟级;后台可换上慢模型拖垮 worker | T-612 后生图 Provider 读取超时取 `min(AiModel.timeout_seconds 或分辨率默认值, AI_IMAGE_UPSTREAM_DEADLINE_SECONDS)`,撞硬截止走失败退点;T-614 已提供异步提交 / 轮询路径,新客户端优先使用异步任务,旧同步接口保留兼容 |
| 异步任务僵死 / 迟到 worker | worker 抢到任务后进程崩溃会留下 running;迟到 worker 可能在 reaper 退款后返回成功 | `ImageGenerationTask` 记录租约和心跳;reaper 按 `lease_expires_at` / `heartbeat_at` 幂等失败退款;成功/失败终态写入前重新锁任务,禁止覆盖已成功或已退款失败的任务 |
| 异步执行时配置漂移 | worker 当前会复跑审核、别名解析和定价;账务用已预扣点数,不重复扣费,但排队期间后台改别名可能导致按提交时价格、执行时模型运行 | 当前接受该取舍并视为短队列安全优先;队列积压或多模型价差扩大后,单独实现提交时 `ai_model_id` / `model_used` 快照,worker 只复审 prompt、不重选模型 |
| 上游错误分类 | 区分参数错误与上游故障 | AI 层抛分类异常;上游故障退点 |
| 凭证安全 | 上游 api_key、支付密钥 | 应用层加密存储,admin 脱敏不回显;其余密钥走环境变量,不入代码与样例 |
| 抽象泄漏 / 参数越权 | 各供应商入参出参不一致;若调用方 `parameters` 可覆盖 `model`/`n`/`size` 会击穿别名计费 | Provider 适配器 + capabilities 声明 + `parameters` 白名单过滤;核心/计费字段服务端固定,不取交集、不允许覆盖 |
+1
View File
@@ -114,6 +114,7 @@
- ~~异步生成(任务队列 + 轮询/回调)~~ → 已立项 **T-612~T-615**(生图异步任务化:先硬截止 + 长请求池校准止血,再抽共享 core,最后新增 `/tasks` 提交+轮询接口与旧同步接口共存、旧路遥测后弃用)。
- 按账号授权可用别名(`account_alias_permission`),防止调用未授权/昂贵模型。
- 别名按比例分流到多个模型(灰度 / A/B / 故障转移);供应商 A 故障自动切 B。
- 异步生图提交时模型配置快照:当前 worker 会复跑 `prepare_generation()`,重新审核 prompt、解析别名 / Provider / 定价,但账务使用已预扣 `CallRecord.points_cost`。短队列下可接受;若后续队列积压或后台频繁切换别名,应在 submit 阶段快照 `ai_model_id` / `model_used` / 必要 Provider 配置,worker 使用提交时模型执行,同时保留 prompt 复审。
- 复杂活动赠点、邀请奖励、历史用户批量补发(需单独审批、风控、批处理与流水留痕)。
- 用量统计报表。
- 退款对账自动化、API Key 轮换、Provider key 轮换(MultiFernet / 双 key 迁移)、限流细化。
+3 -1
View File
@@ -253,7 +253,9 @@ Content-Type: application/json
- 提交阶段预扣点数;余额不足返回 `402 insufficient_points`,不建任务。
- `Idempotency-Key` 按当前 API Key 去重。同 key + 同 payload 返回同一 `task_id`,不重复扣点;同 key + 不同 payload 返回 `409 idempotency_conflict`。
- 不保存 provider `raw`、上游密钥或 `image_base64` 原文;base64 会解码后作为输入文件引用保存,任务请求快照只保存必要字段和文件引用。
- `image_url` 仍按同步接口的 SSRF 与大小规则处理,失败不扣点。
- 桌面端主链路推荐传 `image_base64`。该路径在 submit 阶段只做解码和输入文件落盘,不发生外部网络请求,提交请求应保持短耗时。
- `image_url` 仍按同步接口的 SSRF 与大小规则处理,并在 submit 阶段下载成输入文件引用;失败不扣点。这个路径可能因远程图片下载变慢而让 submit 阻塞,适合作为边缘兼容能力,不建议桌面端批量生图主流程使用。
- worker 执行时会复审 prompt 并重解析当前别名 / Provider 配置,但账务使用提交阶段已预扣的 `CallRecord.points_cost`,不会重复扣点。若排队时间较长且后台切换别名,可能出现按提交时价格预扣、按执行时模型运行;未来如需强一致模型选择,应单独实现提交时模型配置快照。
### `GET /api/v1/generate/image/tasks/{task_id}`
File diff suppressed because one or more lines are too long
+4
View File
@@ -281,6 +281,10 @@ python3.12 manage.py run_image_tasks \
建议单独托管为 `cmhub-image-worker.service`。该 worker 从数据库 `image_generation_task` 表抢 `queued` 任务,使用 MySQL `select_for_update(skip_locked)` 标记 `running`,执行成功后写 `succeeded` 和稳定 `result_url`;失败或上游超时会调用计费层退点并写 `failed`。worker 循环会按 `IMAGE_TASK_REAPER_INTERVAL_SECONDS` 扫描租约或心跳过期的 `running` 任务,默认判失败并幂等退点,不默认重排队。
当前 worker 执行前会基于任务快照复跑一次生成准备逻辑,包括 prompt 复审、别名 / Provider 解析和定价检查;账务仍使用 submit 阶段已预扣的 `CallRecord.points_cost`,不会重复扣点。这是短队列下偏安全的取舍:敏感词库变更后,排队任务仍可在执行前被拦截并退款。若生产出现明显排队或频繁切换别名 / 模型,应单独开发“提交时模型配置快照”,让 worker 使用提交时确认的模型执行。
输入方式对 submit 耗时有直接影响:桌面端批量生图应优先传 `image_base64`,submit 阶段只解码并写入输入文件;`image_url` 会在 submit 阶段完成 SSRF 校验、远程下载和大小限制,再保存为输入文件引用,因此可能阻塞提交请求。`image_url` 的好处是任务进入队列后自包含,worker 不再访问调用方外部 URL;生产排查 submit 慢时,应先确认是否有客户端批量使用 `image_url`。
systemd 单元示例:
```ini
+14
View File
@@ -1760,3 +1760,17 @@
- T-615 第一版只做日志遥测,不新增 admin 报表或统计表;如后续需要报表,单独评估数据量、索引和保留周期。
- 旧同步接口下线前必须继续保持成功响应兼容;待新版客户端默认异步、旧路调用归零或低于运营阈值一段时间后,再宣布 deprecated 并单独立下线任务。
- 下一步:部署后观察 `generation_route_usage` 日志;继续处理真实支付回调到账闭环、客户端下载包发布等业务事项。
## 2026-07-08 文档补充:T-614 异步生图实现取舍记录(非任务)
- 状态:DONE。
- 背景:复核 T-612~T-615 审核建议后确认两点实现行为需要文档化:worker 会复跑 `prepare_generation()`;`image_url` 输入会在 submit 阶段下载。
- 变更:
- `docs/04-architecture.md`:补充 worker 复审 prompt、重解析别名 / Provider / 定价的当前取舍;明确账务仍使用已预扣 `CallRecord.points_cost`,不重复扣点;队列积压时再做提交时模型配置快照。
- `docs/api.md`:在异步生图提交接口说明桌面端主链路推荐 `image_base64`,`image_url` 会在 submit 阶段下载并可能阻塞。
- `docs/deployment.md`:补充生产排查口径,submit 慢时先检查是否批量使用 `image_url`;worker 重解析配置是短队列下偏安全取舍。
- `docs/06-tasks.md`:Backlog 新增“异步生图提交时模型配置快照”。
- `docs/current-state.md`:同步当前实现边界。
- 验证:仅文档更新;使用 `git diff --check` 检查格式。
- 决策:当前不改代码。`image_base64` 是桌面端主路径,submit 仍为短请求;`image_url` 阻塞属于边缘兼容路径。模型配置快照不作为热修,待队列积压或多模型价差扩大后单独立任务。
- 下一步:无需立即编码;继续按生产优先级处理支付回调闭环、客户端发布和线上遥测观察。