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` 白名单过滤;核心/计费字段服务端固定,不取交集、不允许覆盖 |