T-564 async cmhub image tasks
This commit is contained in:
@@ -12,12 +12,13 @@
|
||||
> **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` 并给出中文排障提示。
|
||||
> **v3.5 修订(2026-07-08,T-553 待实现)**:cmhub 生图稳定性口径调整为连接超时默认 66 秒、生图请求和 `image_url` 下载读取等待统一 900 秒,与线上 Nginx/Gunicorn 的长等待窗口对齐;生图读超时仍不自动重发,避免重复扣点。
|
||||
> **v3.6 修订(2026-07-08,T-564)**:cmhub 已新增异步生图任务接口,②批量生图改为 `POST /api/v1/generate/image/tasks` submit + `GET /api/v1/generate/image/tasks/{task_id}` poll;cmshopee 持久化 `tasks.image_task_id/image_task_key`,支持停止/超时/重启后续查,避免 900 秒同步长连接和读超时重复扣点。旧同步 `POST /api/v1/generate/image` 仅保留给单独 `gen_cover()` 兼容/回滚路径。
|
||||
|
||||
## 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 的生成、余额和模型发现接口:`POST /api/v1/generate/title`、②批量生图 `POST /api/v1/generate/image/tasks` + `GET /api/v1/generate/image/tasks/{task_id}`、兼容生图 `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 适配代码;计费/额度统一。
|
||||
|
||||
@@ -29,9 +30,9 @@
|
||||
| 生文请求 | 自拼 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, ...}` → 需再下载 |
|
||||
| 生图响应 | 直接返回 image bytes | ②批量:submit 返回 `{task_id, ...}`,poll 成功返回 `{result:{image_url}}`;兼容同步:`{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` | 生图同步且慢;T-553 目标为 cmshopee 生图请求和图片下载读取等待统一固定 900s,不再按分辨率变化;生图读超时仍不自动重发 |
|
||||
| 超时 | 按 `resolution_timeouts` | ②批量生图 submit 短读超时、poll 总预算 900s、下载 900s;旧同步兼容路径仍 900s |
|
||||
| 幂等 | 直连一次成功一次 | **非幂等、无幂等键**:客户端超时 ≠ 未扣点,读超时后不可无脑重发 |
|
||||
|
||||
关键差异(决定改造点):
|
||||
@@ -89,16 +90,18 @@
|
||||
|
||||
返回值不变(仍返回已存 JPEG 路径),现有调用保持兼容;允许新增可选事件回调参数承载计费元数据。内层:
|
||||
|
||||
- **请求**:`POST {base_url}/api/v1/generate/image`,体:
|
||||
- **②批量请求(T-564)**:`POST {base_url}/api/v1/generate/image/tasks`,头带 `Idempotency-Key` 与 `X-Client-Version`,体与旧生图一致:
|
||||
```jsonc
|
||||
{ "prompt": <cover_prompt>, "model": <image_alias>,
|
||||
"image_base64": <旧封面转 data URL>, "resolution": <resolution>, "aspect_ratio": <可选> }
|
||||
```
|
||||
旧封面必传(改图类),复用现有 `_image_data_url(old_cover_path)` 生成 base64。
|
||||
- **②批量响应(T-564)**:submit 返回 `202 {task_id,status:"queued",call_id,points_cost,points_balance,expires_at}`;cmshopee 立即写 `tasks.image_task_id`,随后 `GET /api/v1/generate/image/tasks/{task_id}` 轮询。`queued/running` 继续等待;`succeeded` 取 `result.image_url`;`failed/expired` 视为终态失败且 cmhub 已退点。
|
||||
- **单独 `gen_cover()` 兼容**:公开函数没有本地 `task.id/db_path` 上下文,第一版继续调用旧同步 `POST /api/v1/generate/image`,保持返回值和旧调用方兼容;②批量生成才使用完整持久化、幂等键和重启续查。
|
||||
- `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` 返回值。
|
||||
- **超时(关键)**:生图同步且慢。T-553 目标为 cmshopee 的 cmhub 连接超时默认 66 秒,生图请求和随后 `image_url` 下载读取等待统一固定 900 秒,不再按分辨率变化,绝不用 30s/60s 调生图——否则客户端超时但服务端仍在算并扣点(见 §4.4 幂等)。
|
||||
- **并发(T-545 已实现)**:最近实测 `/media/generated/images/*.png` 下载链路在 10 并发下明显慢且有连接失败。cmhub 模式下采用内置保护:实际生图请求并发 = `min(ai.image_concurrency, 5)`;下载/保存使用独立线程池,线程数与实际生图请求并发一致,同样最大 5;不新增用户可见配置项。运行日志必须同时显示用户设置和实际并发,避免用户误解设置 10 就会对 cmhub 打 10 并发。下载失败记为该任务失败,不得重新调用生图接口导致重复扣点;读超时仍按 §4.4 的非幂等规则处理。
|
||||
- **下载**:拿 `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` 返回值。
|
||||
- **超时(关键)**:②批量生图 submit 读取等待约 30 秒,poll 单次读取等待约 15 秒,本地总预算 900 秒;撞预算、用户停止或程序退出都保留 `image_task_id/image_task_key`,下次直接续查,不重新 submit。下载读取等待 900 秒;旧同步兼容路径也使用 900 秒。绝不用 30s/60s 同步死等旧生图,否则客户端超时但服务端仍在算并扣点。
|
||||
- **并发(T-545/T-564 已实现)**:最近实测 `/media/generated/images/*.png` 下载链路在 10 并发下明显慢且有连接失败。cmhub 模式下采用内置保护:实际 submit+poll 在途并发 = `min(ai.image_concurrency, 5)`;下载/保存使用独立线程池,线程数与实际生图并发一致,同样最大 5;不新增用户可见配置项。运行日志必须同时显示用户设置和实际并发,避免用户误解设置 10 就会对 cmhub 打 10 并发。下载失败记为该任务失败,不得重新调用生图接口导致重复扣点。
|
||||
- **下载后端(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 错误映射与重试策略
|
||||
@@ -112,14 +115,19 @@ cmhub 返回结构化 `{error:{code}}`。映射层**按 `code` 优先分支**(
|
||||
| `account_disabled` | 403 | 提示 Key 被吊销/账号禁用,去网页端重生成 | 否 |
|
||||
| `bad_request` / `model_not_allowed` / `no_pricing_rule` / `content_blocked` | 400 | 记录具体 code,判为配置/参数/内容错 | 否 |
|
||||
| `upstream_error` | 502 | 上游失败(cmhub **已自动退点**),可提示稍后重试 | 是(安全)|
|
||||
| `upstream_timeout` / `task_timeout` | 200/5xx | 上游或任务超时(cmhub **已自动退点**),可提示稍后重试 | 是(安全)|
|
||||
| `idempotency_conflict` | 409/400 | 本地幂等键被不同 payload 复用,清空本地 key 后失败提示 | 否 |
|
||||
| `rate_limited` | 429 | 退避重试,读 `Retry-After`(默认限流 60次/分)| 是 |
|
||||
| **未列出的 code** | 任意 | 展示 `message`,当不可重试错误(前向兼容)| 否 |
|
||||
|
||||
**幂等与超时——生图重试必须特别处理(会亏钱)**:cmhub 生成接口**非幂等、无幂等键**,客户端超时 ≠ 未扣点。
|
||||
**幂等与超时——②批量生图以 task_id 续查为准**:旧同步生图接口非幂等,客户端超时 ≠ 未扣点;T-564 后②批量生图必须使用任务化接口和本地持久化规避这个问题。
|
||||
|
||||
- **生图(`gen_cover`)**:**读超时后绝不自动重发**——服务端可能已算完并扣点,重发 = 重复扣点。首选办法是把读超时设够大(§4.3,T-553 目标固定 900s)从源头避免歧义;只对**连接超时**(请求根本没送达服务端)安全重试。
|
||||
- submit 前先生成并写入 `tasks.image_task_key`,同一次网络抖动重发复用同一个 `Idempotency-Key`;submit 成功后立即写入 `tasks.image_task_id`。
|
||||
- 只要本地已有 `image_task_id`,下一轮直接 GET 续查,不重新 submit;`succeeded` 下载保存并 `set_generated()`;本地总预算超时、用户停止、程序退出都保留 `image_task_id/image_task_key`。
|
||||
- poll 返回 `failed/expired`(已退点)时清空 `image_task_id/image_task_key`,后续重试会生成新的 key 并重新 submit;submit 阶段 `content_blocked`、`insufficient_points`、`bad_request`、`idempotency_conflict` 等没有有效任务时也清空 key。
|
||||
- **单独 `gen_cover()` 兼容路径**:没有 DB 上下文,仍走旧同步接口;读超时后仍不自动重发,避免重复扣点。
|
||||
- **生文(`gen_title`)**:秒级返回、点数低,读超时重试风险小,但仍建议同样区分连接超时/读超时;重试次数可小。
|
||||
- 只对 `502`/`429`/**连接**超时重试;`402/401/403/400`/读超时立即失败。现状 `_call_with_retry` 是**一刀切重试**,cmhub 模式必须替换为这套区分策略。
|
||||
- 只对 `502`/`429`/**连接**超时以及已退点的 `upstream_timeout/task_timeout` 重试;`402/401/403/400`/读超时立即失败或进入续查。现状 `_call_with_retry` 是**一刀切重试**,cmhub 模式必须替换为这套区分策略。
|
||||
- `insufficient_points` 是新的用户可见态:② 生成页应弹明确提示并引导去网页端充值,不当普通失败淹没在计数里。
|
||||
|
||||
### 4.5 余额展示与额度预检
|
||||
@@ -140,13 +148,14 @@ cmhub 返回结构化 `{error:{code}}`。映射层**按 `code` 优先分支**(
|
||||
|
||||
| 模块 | 改动 | 量 |
|
||||
| --- | --- | --- |
|
||||
| `app/ai.py` | `gen_title`/`gen_cover` 加 cmhub 分支(请求体+解析+生图下载);抽 backend 选择;错误映射 + 区分重试。`direct` 分支保留现有代码 | M |
|
||||
| `app/ai.py` | `gen_title`/`gen_cover` 加 cmhub 分支(请求体+解析+生图下载);T-564 后②批量生图走 submit+poll+续查,单独 `gen_cover()` 保留旧同步兼容;抽 backend 选择;错误映射 + 区分重试。`direct` 分支保留现有代码 | M |
|
||||
| `app/db.py` | T-564 新增 `tasks.image_task_id/image_task_key`,提供 `ensure_image_task_key()`、`set_image_task_submitted()`、`clear_image_task()`;重置封面时清空异步生图状态,只重置标题时保留 | S |
|
||||
| `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` 的设置与用户提示层。
|
||||
**明确不动(T-564)**:不改 CDP/Shopee/Excel/账号流程,不改图片本地路径方案和 T-520 封面开关;不引入 cancel 接口;不做 DB 版本化迁移重构。`generate_batch` 仍保持“先标题、后封面、失败不阻塞其余”的骨架,只替换 cmhub 批量封面请求内核。
|
||||
|
||||
## 6. 安全与合规
|
||||
|
||||
@@ -158,8 +167,8 @@ cmhub 返回结构化 `{error:{code}}`。映射层**按 `code` 优先分支**(
|
||||
|
||||
- **单元(mock cmhub,不连真实网关)**:
|
||||
- 生文:`titles` 多条取首条;空 `titles`/空串 → `AIError`。
|
||||
- 生图:`image_url` → mock 下载字节 → `_save_jpeg` 落盘校验分辨率/质量;覆盖 scheme、内网/回环字符串地址、域名解析到内网 IP 的拒绝路径。
|
||||
- 错误码矩阵:402/401/403/400 不重试且原因正确;502/429/连接超时按 attempts 重试;生图读超时不重发;未知 code 不重试。
|
||||
- 生图:②批量 mock submit 202 → `task_id` 落库 → poll succeeded → `image_url` → mock 下载字节 → `_save_jpeg` 落盘校验分辨率/质量;覆盖已有 `image_task_id` 续查不 POST、failed/expired 清 key、取消/超时保留 task_id。单独 `gen_cover()` 继续覆盖旧同步 `image_url` 下载路径。
|
||||
- 错误码矩阵:402/401/403/400 不重试且原因正确;502/429/连接超时按 attempts 重试;`upstream_timeout/task_timeout` 可重试且清 key;未知 code 不重试。
|
||||
- 配置:`backend=cmhub` 走 cmhub 分支、显式 `direct` 走旧分支;T-529 后缺 `backend` 的配置按 `DEFAULT_CONFIG` 补为 cmhub;缺 `base_url`/Key/别名时明确报错。
|
||||
- **GUI**:⑤ cmhub 面板读写、测试连接 worker、别名下拉;② 生成在 `insufficient_points` 时的提示路径。
|
||||
- **回归**:`direct` 模式现有 `test_ai.py` 用例保持绿。
|
||||
@@ -178,7 +187,7 @@ cmhub 返回结构化 `{error:{code}}`。映射层**按 `code` 优先分支**(
|
||||
3. ~~`resolution` 取值~~ **已解决**:`512/1K/2K/4K`(大写 K),默认 `1K`;cmshopee 小写值发请求前归一。
|
||||
4. ~~`aspect_ratio`~~ **已解决**:默认 `1:1`,Shopee 封面用 `1:1`。
|
||||
5. **`image_url` 有效期(按最坏处理)**:对象存储 URL 可能过期——本设计已是**生成后立即下载落盘**,无需长期持有。
|
||||
6. ~~超时上限~~ **已解决**:T-553 目标为 cmshopee 生图请求和图片下载读取等待统一固定 900s,连接超时默认 66s。
|
||||
6. ~~超时上限~~ **已解决**:T-553/T-564 后②批量生图 submit 短等待、poll 总预算 900s,图片下载读取等待 900s,连接超时默认 66s。
|
||||
7. **Base URL / API Key 形态**:域名待部署方提供;Key 形如 `sk_cmhub_xxx`,仅网页端生成时显示一次——⑤设置需提示用户从网页端复制粘贴,本地保存。
|
||||
|
||||
## 10. 落地拆分与任务顺序
|
||||
@@ -188,6 +197,7 @@ cmhub 返回结构化 `{error:{code}}`。映射层**按 `code` 优先分支**(
|
||||
- **第三步**:② 计费错误提示(`insufficient_points` 引导充值)+ 可选余额展示。
|
||||
- **第四步(T-529)**:产品默认 cmhub,⑤去掉 AI 后端选择,保存固定 `backend=cmhub`。
|
||||
- **第五步(T-530)**:Base URL 规整到网关根,404 给出明确中文提示。
|
||||
- **第六步(T-564)**:②批量生图切到 cmhub 异步任务接口,持久化 `image_task_id/image_task_key` 支持续查;旧同步生图只保留给单独 `gen_cover()` 兼容。
|
||||
- 文档随每步同步。
|
||||
|
||||
> 当前已在 `docs/06-tasks.md` 落成 T-526~T-535,且 T-525 工程基础设施已完成;下一步按看板进入 T-539 设置简化,仍遵守 `docs/05-coding-rules.md` 验证清单。
|
||||
> T-526~T-535、T-545、T-548、T-549 与 T-564 已按当前任务文件体系落地;后续新任务继续写入 `docs/tasks/T-<编号>.md`,不再新增到冻结的 `docs/06-tasks.md`。
|
||||
|
||||
Reference in New Issue
Block a user