T-565 handle relative cmhub image urls

This commit is contained in:
chengma
2026-07-09 08:57:46 +08:00
parent c435456977
commit a57254da9e
5 changed files with 141 additions and 10 deletions
+1
View File
@@ -320,6 +320,7 @@ fetch_cmhub_models(base_url, api_key, connect_timeout=10, read_timeout=30) -> li
- direct 连接超时参考模型 `connect_timeout_seconds`;**返回超时 = 模型 `timeout_seconds` 或 `appconfig.response_timeout()`(随分辨率:512/1k/2k/4k → 180/240/360/600)**。cmhub 使用 `requests timeout=(connect, read)`,connect 来自 `ai.cmhub.connect_timeout`;cmhub 生文读取等待固定 600s。②批量生图异步 submit 读取等待 30s、poll 单次读取等待 15s、本地总预算 900s,图片下载读取等待 900s;`gen_cover()` 旧同步兼容路径仍用 900s 读取等待。
- 并发数/重试/分辨率/jpg 质量来自 `appconfig.ai_config()`;标题/图片并发会被夹到 1..5,失败重试次数会被夹到 0..10,兼容旧配置中的超限值;Key 本地明文存储,但不入日志、不导出。cmhub 响应的 `points_cost/points_balance/call_id` 不改变返回值,通过 `on_event` metadata 事件上报,GUI 余额/计费展示留给 T-528。
- 标题快、图片慢:分两段、各用各自并发数;失败按 `retry` 重试,仍失败记 error 不阻塞其余。cmhub 批量生图使用内部实际并发 `min(image_concurrency, 5)` 约束 submit+poll 在途数;已有 `tasks.image_task_id` 时直接 GET 续查,不二次 submit;submit 前先持久化 `image_task_key`,submit 成功立即持久化 `image_task_id`;poll `failed/expired` 会清空二者,poll 超时/用户停止/程序退出则保留二者供下次续查。拿到 `image_url` 后交给独立下载/保存线程池,下载线程数与实际生图并发一致且最大 5;T-548 后图片下载后端由 `ai.cmhub.download_with_curl` 控制,默认 `auto`:Windows 且检测到系统 curl 时优先用 curl 下载,否则回退 requests;curl 失败也会自动回退 requests。下载前仍执行公网 URL 校验,curl 使用 `-K` 临时配置文件传入 URL,不把带 token 的 `image_url` 放进命令行参数;`use_system_proxy=false` 时 curl 加 `--noproxy "*"`。下载失败最多安全重试 3 次,只复用同一个 `image_url`,不会重新调用 cmhub 生图接口;下载总耗时超过 20 秒时写“图片下载较慢”警告;direct 兼容路径暂保持原 `image_concurrency` 语义。
- cmhub 图片地址兼容:优先递归读取 `image_url` / `image_url.url` / `images[]` / `image_urls[]` 等字段;若返回 `/generated/...` 或 `generated/...` 相对路径,先按 cmhub Base URL 补全为完整 `http(s)` URL,再进入原有公网 URL 安全校验和下载。
- 调试 cmhub 图片 URL:默认不在日志或 DB 中记录完整 `image_url`。若本机调试需要查看,启动程序前设置环境变量 `CMSHOPEE_DEBUG_CMHUB_IMAGE_URL=1`,②本轮可见运行日志会显示脱敏后的“cmhub 图片 URL”,但该调试行不写入 SQLite `run_log_events`。
- 调用有成本与失败可能:超时、限流、内容安全拒绝都要返回明确错误。
- 生成结果**直接进入 ③ 更新候选**;③ 点击「开始更新」后弹窗批量确认,确认后提交线上。本地留档 + 回写 Excel 供追溯。
+1 -1
View File
@@ -99,7 +99,7 @@
- **②批量响应(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` 返回值。
- **下载**:拿 `image_url` 后下载该图字节(cmhub 自家对象存储公网 URL)→ 交给现有 `_save_jpeg(image_bytes, out_path, resolution, quality)` 落盘。cmshopee 兼容 `image_url` / `image_url.url` / `images[]` / `image_urls[]` 等返回结构;若 cmhub 返回相对路径(如 `/generated/...`),先按 cmhub Base URL 补全为完整 `http(s)` URL。下载 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 "*"`。
+55
View File
@@ -0,0 +1,55 @@
---
id: T-565
title: 兼容 cmhub 异步生图返回相对图片地址
phase: 7
deps: [T-564]
status: DONE
created: 2026-07-09
---
## 问题 / 背景
T-564 切到 cmhub 异步生图任务接口后,②AI生成在 poll 返回 `succeeded` 后进入下载阶段,但真实运行出现:
```text
图片生成失败:下载新封面失败:cmhub 图片地址只允许 http/https
```
从日志看,submit 和 poll 都成功,失败发生在本地下载前的 URL 安全校验。根因是当前 `app/ai.py` 的异步 poll 成功分支只读取 `result.image_url` / 顶层 `image_url`,并直接传给下载器;如果 cmhub 返回的是相对路径(如 `/generated/images/...png`)、协议相对 URL(`//host/path`)、`image_url` 对象或图片列表字段,本地会把非完整 URL 字符串交给 `_assert_public_http_url()`,从而报“只允许 http/https”。
## 方案
- `app/ai.py`
- 新增 cmhub 异步任务图片地址提取/规整 helper。
- poll `succeeded` 后复用现有 `_find_image_ref()` 递归提取图片地址,兼容 `result.image_url`、`result.image_url.url`、`result.images[]`、`result.image_urls[]`、顶层同类字段。
- 对相对路径按当前 cmhub Base URL 规整为完整 `http(s)://host/...` 后再进入原有公网 URL 安全校验和下载流程。
- 如果仍无法提取地址,报清晰中文错误“cmhub 生图任务成功但没有图片地址”;如果字段存在但格式不可用,报“cmhub 生图任务返回的图片地址格式错误”。
- `tests/test_ai.py`
- 覆盖异步 poll 返回相对路径时能拼成完整 URL 下载。
- 覆盖异步 poll 返回 `image_urls` 列表时能提取第一张图片。
## 验收要点
- cmhub poll 返回 `result.image_url="/generated/xxx.png"` 时,下载 URL 应为 `<base_url>/generated/xxx.png`。
- cmhub poll 返回 `result.image_urls=["https://..."]` 时可正常下载。
- 原有绝对 `https://...` 图片地址路径不回归。
- 验证命令:
- `py -3.10 -m unittest tests.test_ai`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `git diff --check`
## 边界(不改什么)
- 不改 DB schema、`tasks.image_task_id/image_task_key` 生命周期。
- 不改 cmhub submit/poll 状态机和重试策略。
- 不改 curl/requests 下载实现和公网 URL 安全校验。
- 不改 CDP/Shopee、Excel、GUI 布局。
## 执行记录
- 2026-07-09:完成 T-565。
- `app/ai.py`:新增 `_extract_cmhub_image_url()` / `_normalize_cmhub_image_url()`,异步 poll 和旧同步兼容路径统一通过 helper 提取图片地址;兼容 `image_url`、`image_url.url`、`images[]`、`image_urls[]`、协议相对 URL 与相对路径,并在下载前按 cmhub Base URL 补全。
- `tests/test_ai.py`:新增/调整 cmhub 异步生图测试,覆盖 poll 返回相对路径时拼接完整 URL 下载,以及 `image_urls` 列表提取。
- 文档:同步 `docs/api.md`、`docs/cmhub-integration-design.md` 的图片地址兼容口径。
- 验证通过:`py -3.10 -m unittest tests.test_ai`、`python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`py -3.10 -m unittest discover -s tests`、`git diff --check`。