docs: add cmhub AI gateway integration design and sync docs

- 新增 docs/cmhub-integration-design.md(v3.2):对接 cmhub 生文/生图/余额/别名发现接口的设计、迁移策略、错误与重试、待确认项
- docs/06-tasks.md:新增 Phase 7(T-526~T-528)AI 网关对接任务,补充别名动态下拉与 fetch_cmhub_models helper;相关文档索引与工程/打包任务同步
- 同步 README/03-tech-stack/current-state/packaging/troubleshooting/ux-review 文档

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
chengma
2026-07-04 10:56:08 +08:00
co-authored by Claude Opus 4.8
parent 6d4f60fcdb
commit c8a5e9ada8
8 changed files with 228 additions and 24 deletions
+16 -1
View File
@@ -117,7 +117,22 @@
| T-523 | 拆分 `app/gui.py` 为 `app/gui/` 包 | T-511, T-514, T-515, T-516, T-517 | 依据 `docs/engineering-review.md` P0。把 6317 行 God-file 拆为包:`models.py`(3 个 TableModel)、`tabs/`(①~⑤各一文件)、`workers.py`(Generate/Apply/Collect/WriteBack/AccountLoginCheck/AIModelTest 从 gui 挪出,与 `app/workers.py` 基类归拢)、`widgets.py`(色板常量、空状态卡、批次总览等 helper)、`main_window.py`。纯结构重构、对外行为与公开符号不变(保留 `from app import gui` 及 `MainWindow`/各 Tab/Worker 的导入路径或提供兼容再导出),保留 PySide6 缺失优雅降级;68 个 GUI 测试全绿、不删减断言 | DONE | | T-523 | 拆分 `app/gui.py` 为 `app/gui/` 包 | T-511, T-514, T-515, T-516, T-517 | 依据 `docs/engineering-review.md` P0。把 6317 行 God-file 拆为包:`models.py`(3 个 TableModel)、`tabs/`(①~⑤各一文件)、`workers.py`(Generate/Apply/Collect/WriteBack/AccountLoginCheck/AIModelTest 从 gui 挪出,与 `app/workers.py` 基类归拢)、`widgets.py`(色板常量、空状态卡、批次总览等 helper)、`main_window.py`。纯结构重构、对外行为与公开符号不变(保留 `from app import gui` 及 `MainWindow`/各 Tab/Worker 的导入路径或提供兼容再导出),保留 PySide6 缺失优雅降级;68 个 GUI 测试全绿、不删减断言 | DONE |
| T-523a | ②/③ 新一轮运行前清空界面日志显示 | T-519, T-505, T-523 | 问题:②AI生成和③更新shopee进入模块或启动新一轮任务时仍显示上一次运行日志,用户容易把旧失败、旧进度误认为本轮状态。方案:点击②「开始生成」、③「检查本轮更新」或③「开始更新」时,先清空对应界面的可见日志显示区,并写入本轮开始摘要;运行中只追加本轮日志,完成后保留本轮日志。只清空界面文本,不删除 SQLite `run_logs/run_log_events` 或本地 `logs/` 诊断日志;进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志查看作为后续低优先级入口,不自动混入当前运行界面。同步 GUI 测试覆盖新一轮运行不会混入旧日志 | DONE | | T-523a | ②/③ 新一轮运行前清空界面日志显示 | T-519, T-505, T-523 | 问题:②AI生成和③更新shopee进入模块或启动新一轮任务时仍显示上一次运行日志,用户容易把旧失败、旧进度误认为本轮状态。方案:点击②「开始生成」、③「检查本轮更新」或③「开始更新」时,先清空对应界面的可见日志显示区,并写入本轮开始摘要;运行中只追加本轮日志,完成后保留本轮日志。只清空界面文本,不删除 SQLite `run_logs/run_log_events` 或本地 `logs/` 诊断日志;进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志查看作为后续低优先级入口,不自动混入当前运行界面。同步 GUI 测试覆盖新一轮运行不会混入旧日志 | DONE |
| T-524 | PyInstaller 打包为免安装 exe | T-521 | 依据 `docs/engineering-review.md` P1。新增 PyInstaller spec/脚本,产出 Windows onedir 免安装 `.exe`;打包排除并绝不内置 `config.json`/`config/ai_models.json`/`cmshopee.db*`/`chrome_user_data_dir/`/`images/`/`logs/`/`prompts/`/`title_prompt.txt` 等本地数据与密钥;打包版工作目录切到 exe 所在目录,首次运行按现有默认值在本地生成配置;`scripts/build_exe.ps1` 可构建并校验发布目录,`docs/packaging.md` 说明分发和用户手动覆盖更新步骤 | DONE | | T-524 | PyInstaller 打包为免安装 exe | T-521 | 依据 `docs/engineering-review.md` P1。新增 PyInstaller spec/脚本,产出 Windows onedir 免安装 `.exe`;打包排除并绝不内置 `config.json`/`config/ai_models.json`/`cmshopee.db*`/`chrome_user_data_dir/`/`images/`/`logs/`/`prompts/`/`title_prompt.txt` 等本地数据与密钥;打包版工作目录切到 exe 所在目录,首次运行按现有默认值在本地生成配置;`scripts/build_exe.ps1` 可构建并校验发布目录,`docs/packaging.md` 说明分发和用户手动覆盖更新步骤 | DONE |
| T-525 | 引入 ruff(lint + format)+ 可选 pre-commit | T-006 | 依据 `docs/engineering-review.md` P1。加 `ruff` 配置(lint + format),先以现状为基线不做大规模风格重排,只开启安全规则(未用 import/变量、明显错误);可选 `.pre-commit-config.yaml`;不改业务逻辑;CI(T-522)可串入 ruff 检查。数据模型渐进上 mypy 作为后续可选 | TODO |
## Phase 7 · AI 网关对接(`docs/cmhub-integration-design.md`)
> 业务优先级调整:先完成 cmhub 生文/生图网关对接;T-525(ruff)顺延到 Phase 8,不作为 T-526 的阻塞项。
| ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- |
| T-526 | `app/ai.py` + `appconfig` 接入 cmhub backend | T-301, T-303, T-520 | 依据 `docs/cmhub-integration-design.md` v3.2。`config.json` 的 `ai` 段加 `backend`(`cmhub`/`direct`)+ `cmhub` 子段(`base_url`/`title_alias`/`image_alias`/`connect_timeout` 等);为保护既有用户,全新配置默认 `backend=direct`、加载既有配置缺 `backend` 时也补 `direct`,`cmhub` 一律由用户在⑤显式 opt-in;`backend=cmhub` 但 `base_url`/Key 缺失时须抛清晰「请去⑤配置 cmhub」错误(`CMHubError`/`AIError`),不崩溃、不静默直连。cmhub API Key 固定存 `config/cmhub.json`(schema `{ "api_key": "..." }`),新增读写/打码 helper 并把该文件加入 `.gitignore`;日志脱敏。`gen_title`/`gen_cover` **返回值不变**,内层按 backend 分流并保留 `direct`;计费元数据不塞进返回值,允许给二者**新增一个可选事件回调参数**(如 `on_meta`/`on_event`)承载,属向后兼容加参,`generate_batch` 显式传回调不受影响。cmhub 分支:生文 `POST /api/v1/generate/title` 体 `{prompt,model:别名,resolution?}`、取 `titles[0]`、空则 `AIError`;生图 `POST /api/v1/generate/image` 体 `{prompt,model:别名,image_base64:<旧封面>,resolution,aspect_ratio:"1:1"}`、拿 `image_url` 后**立即下载**再走 `_save_jpeg`;`resolution` 归一大写 `512/1K/2K/4K`。新增 `CMHubError(AIError)`,带 `code/status/retryable/retry_after`,错误按 `code` 优先分支(`insufficient_points`/`unauthorized`/`account_disabled`/`bad_request`/`model_not_allowed`/`no_pricing_rule`/`content_blocked`/`upstream_error`/`rate_limited`,未知 code 当不可重试);cmhub HTTP helper 需区分 connect/read timeout(优先用 `requests timeout=(connect, read)`),只对 502/429/连接超时重试,生图读超时绝不自动重发,读超时按分辨率封顶 600s。`points_cost`/`points_balance`/`call_id` 不改返回值,通过 `on_step`/事件回调上报;T-526 只保证 metadata 事件完整传出,T-528 再由 GUI worker 脱敏写 run_logs 和余额展示。`image_url` 下载必须限制 http/https、拒绝内网/回环地址、校验域名解析后的 IP 仍不是内网/回环/本机地址,并设置超时和大小上限。新增 `fetch_cmhub_models(base_url, api_key)` helper 调 `GET /api/v1/models` 返回别名清单(`alias/operation_type/requires_image/pricing_status/prices`)供 T-527 渲染下拉,错误脱敏。不碰 editor/cdp/chrome/accounts/excel/db,也不改 ①③④流程。`tests/test_ai.py` 加 cmhub mock(titles 列表、image_url 下载、安全下载、错误码与重试、读超时不重发、metadata 事件)、`tests/test_appconfig.py` 加 schema 和 key 文件 helper,direct 用例保持绿 | TODO |
| T-527 | ⑤设置 cmhub 网关面板 | T-526, T-517 | 依据 `docs/cmhub-integration-design.md` v3.2。⑤ AI 设置按 `backend` 切换:cmhub 模式显示「网关 Base URL + API Key(打码,提示从网页端复制、仅显示一次)+ 生文别名 + 生图别名 + 测试连接/查余额」;**别名从 `GET /api/v1/models`(T-526 的 `fetch_cmhub_models`)动态拉取渲染下拉**,按 `operation_type` 分生文/生图,过滤 `pricing_status="unpriced"` 的别名,可展示单价与 `requires_image` 提示,选中值持久化到 `ai.cmhub.title_alias/image_alias`(网关临时不可达时回退已存值);不写死别名。direct 模式保留现有 AI 模型 master-detail。保存写 `config.json` 的 `ai` 段与 `config/cmhub.json`;切换 backend 时不删除 legacy `config/ai_models.json`。测试连接/查余额经后台 worker 调 cmhub(复用 `AIModelTestWorker` 思路或新增 worker),错误必须脱敏并给用户可读提示。同步 GUI 设置测试;不改 Shopee/CDP 流程 | TODO |
| T-528 | ② 计费错误提示 + 余额展示 | T-526, T-527, T-303 | 依据 `docs/cmhub-integration-design.md` v3.2。② AI生成页把 cmhub 计费失败态显式化:通过 `CMHubError.code` 识别 `insufficient_points`,弹明确提示「点数不足,请先充值」并引导去网页端充值,本轮未开始任务可提前中止,不靠中文字符串匹配、不淹没在失败计数里;用 T-526 成功响应事件里的 `points_balance` 刷新②页剩余点数显示,`/balance` 仅作手动刷新/可选批量前预检;`points_cost`/`call_id` 记入脱敏 run_logs。只改② UI、`GenerateWorker` 事件处理/文案及 GUI 单测;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | TODO |
## Phase 8 · 工程基础设施后续(`docs/engineering-review.md`)
| ID | 任务 | 依赖 | 验收要点 | 状态 |
| --- | --- | --- | --- | --- |
| T-525 | 引入 ruff(lint + format)+ 可选 pre-commit | T-006 | 依据 `docs/engineering-review.md` P1。加 `ruff` 配置(lint + format),先以现状为基线不做大规模风格重排,只开启安全规则(未用 import/变量、明显错误);可选 `.pre-commit-config.yaml`;不改业务逻辑;CI(T-522)可串入 ruff 检查。数据模型渐进上 mypy 作为后续可选。当前因业务优先级调整顺延到 T-526~T-528 之后 | TODO |
## 里程碑 ## 里程碑
+1
View File
@@ -26,6 +26,7 @@ cmshopee 是一个给**电商运营**使用的 Windows PySide6 桌面自动化
- [界面配色设计](ui-color-design.md):语义色板与组件配色映射规范,指导给状态 / 按钮 / 校验 / 登录状态上色。 - [界面配色设计](ui-color-design.md):语义色板与组件配色映射规范,指导给状态 / 按钮 / 校验 / 登录状态上色。
- [工程评估](engineering-review.md):全栈视角评估工程基础设施与可维护性(依赖清单 / CI / 打包 / gui.py 拆分 / lint),含 P0-P2 与优先级。 - [工程评估](engineering-review.md):全栈视角评估工程基础设施与可维护性(依赖清单 / CI / 打包 / gui.py 拆分 / lint),含 P0-P2 与优先级。
- [打包与分发](packaging.md):PyInstaller 免安装 exe 打包命令、排除本地数据规则和用户手动更新方式。 - [打包与分发](packaging.md):PyInstaller 免安装 exe 打包命令、排除本地数据规则和用户手动更新方式。
- [对接 cmhub AI 网关设计](cmhub-integration-design.md):把生文/生图从本地直连改为对接 cmhub 计费网关的设计方案、改动边界与待确认问题。
## 运行环境安装 ## 运行环境安装
+187
View File
@@ -0,0 +1,187 @@
# 对接 cmhub AI 网关 · 设计文档
> 目标:把 cmshopee 的 AI 生成(生文/生图)从"本地直连多个第三方 provider"改为"对接 `cmhub` 计费型 AI 网关"。
> 性质:设计与评审文档,当前已拆分为 `docs/06-tasks.md` 的 T-526~T-528;本文件用于约束实现边界、迁移策略和验收重点。
> 参考:cmhub `docs/api.md` 对外接口契约;对接注意事项以 Obsidian 笔记《cmhub-接口对接文档-桌面端生文生图》为准(含幂等/超时/退点等运营级约定,本文已吸收);cmshopee `app/ai.py`(`gen_title`/`gen_cover`/`generate_batch`)、`app/appconfig.py`(`config.json` + `config/ai_models.json`)、`docs/04-architecture.md` §5.1b/§6.2。
>
> **v2 修订(2026-07-04,吸收对接文档)**:修正生图重试策略(非幂等,读超时不重发,避免重复扣点);超时按分辨率细化到 600s 上限;余额直接用响应 `points_balance`;错误处理改为按 `code` 优先分支 + 未知 code 当不可重试,补 `content_blocked`;确认 `resolution`=`512/1K/2K/4K`(大写)、`aspect_ratio` 默认 `1:1`。
> **v3 修订(2026-07-04,工程评审修正)**:补充既有配置迁移策略(缺 `backend` 先按 `direct` 处理,避免未配置 cmhub 时破坏现有生成);固定 cmhub Key 文件为 `config/cmhub.json`;新增 `CMHubError` 结构化错误要求;明确 `points_balance/points_cost/call_id` 通过回调事件传播而不是改变 `gen_title`/`gen_cover` 返回值;要求 cmhub 分支用可区分 connect/read timeout 的 HTTP 调用;把 `image_url` 下载安全校验纳入 T-526 验收;T-525 顺延,Phase 7 成为当前业务优先任务。
> **v3.1 修订(2026-07-04,实现前澄清)**:① 全新配置默认 backend 统一为 `direct`(cmhub 显式 opt-in),且 `backend=cmhub` 但未配置时须抛清晰"去⑤配置"错误而非崩溃(见 §4.1);② 明确"返回值不变"≠"回调不变"——计费元数据须给 `gen_title`/`gen_cover` **新增可选事件回调参数**承载,是向后兼容加参(见 §4.2)。两点已同步 T-526 验收。
> **v3.2 修订(2026-07-04,同步对接文档新增)**:cmhub 新增第 4 个接口 `GET /api/v1/models`(别名自助发现,见 §4.6),⑤设置别名由手填改为**动态下拉**(按 `operation_type` 分生文/生图、过滤 `unpriced`、展示单价、`requires_image` 提示),关闭原待确认 #1;仅 Base URL 仍待部署方提供。已同步 T-526(加 `fetch_cmhub_models` helper)与 T-527(别名下拉)。
## 1. 背景与目标
**现状**:`app/ai.py` 直连多个上游 provider——每个模型在 `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 Key);换上游模型对 cmshopee 零改动(cmhub 用能力别名);可删除大量 provider 适配代码;计费/额度统一。
## 2. 契约对照
| 维度 | cmshopee 现状 | cmhub 接口 |
| --- | --- | --- |
| 鉴权 | 每模型一把 `Bearer api_key` | 一把 `Bearer <API_KEY>` |
| 生文请求 | 自拼 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, ...}` → 需再下载 |
| 错误 | 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` | 生图同步且慢,读超时按分辨率 512≈180s/1K≈240s/2K≈360s/4K≈600s,客户端取上限 600s |
| 幂等 | 直连一次成功一次 | **非幂等、无幂等键**:客户端超时 ≠ 未扣点,读超时后不可无脑重发 |
关键差异(决定改造点):
- 生文 **`model` 传能力别名**(如 `title-standard`),不是具体模型名;生文返回**列表** `titles`。
- 生图返回 **`image_url`**(对象存储),cmshopee 要**多一步下载**再本地转 JPEG。
- 计费错误 `insufficient_points`(点数不足)是**新的用户可见失败态**。
## 3. 设计原则与边界
- **接缝最小化**:保持 `gen_title(...)` / `gen_cover(...)` 的**返回值与现有调用兼容**(`gen_title`→标题字符串、`gen_cover`→已存 JPEG 路径),允许向后兼容新增可选事件回调参数承载计费元数据,只改内层实现。这样 `generate_batch` 编排、并发、重试、DB 写入、诊断日志、JPEG 落盘、`images/<batch_id>/<slug>/...` 路径、T-520 封面开关**全部复用、零改动**;GUI `run_logs` 记录和余额展示放到 T-528。
- **不碰高风险层**:`editor.py`/`cdp.py`/`chrome.py`/`accounts.py`/`excel.py`/`db.py`/`workers.py`,以及 ①采集/③更新/④账号全流程**完全不动**。这是本改动最重要的安全边界。
- **适配器而非硬替换**:内层抽 `backend`(`cmhub` / `direct`),由配置选择。为保护既有用户,旧 `config.json` 缺 `ai.backend` 时先按 `direct` 迁移;用户在⑤设置明确切到 `cmhub` 并填好 Base URL / Key / 别名后才走网关。老 `ai_models.json` 不作废、测试易写、可灰度。
## 4. 方案
### 4.1 配置 schema
在 `config.json` 的 `ai` 段新增 cmhub 子段(`app/appconfig.py` 默认值 + 校验):
```jsonc
"ai": {
"backend": "direct", // "cmhub" | "direct";全新配置默认 direct,cmhub 为显式 opt-in
"cmhub": {
"base_url": "https://<cmhub>", // 网关根地址,请求时拼 /api/v1/...
"title_alias": "title-standard", // 生文能力别名
"image_alias": "image-hd", // 生图能力别名
"connect_timeout": 10,
"check_balance_before_batch": false
},
// 现有字段保留:resolution / jpg_quality / *_concurrency / retry /
// resolution_timeouts / generate_cover(T-520)等,backend 无关,继续用
}
```
- **cmhub API Key 不进 `config.json`**(避免与其它设置混放、避免误提交)。固定存到 `config/cmhub.json`,文件 schema 第一版为 `{ "api_key": "sk_cmhub_xxx" }`;新增 `CMHUB_CONFIG_PATH`、`load_cmhub_config()`、`save_cmhub_config()`、`get_cmhub_api_key(masked=False)` 等 helper;`config/cmhub.json` 必须加入 `.gitignore`。沿用现有"本地明文保存但 gitignore + UI 打码 + 日志脱敏"纪律(T-503)。
- **`ai_models.json` 去留**:`direct` 模式继续用;`cmhub` 模式不读它。文件保留但标记 legacy。
- **默认 backend 的两条路径要分清(避免首启破坏)**:`appconfig` 里两条路径都必须落到"不破坏开箱"这一条统一规则——`backend` 默认 `direct`:
- **加载既有 `config.json`** 缺 `ai.backend` 时,补 `direct`(保护老用户,其 `ai_models.json` 仍有效)。
- **`DEFAULT_CONFIG` / `default_config()` 造全新配置** 同样写 `direct`,不写 `cmhub`——否则全新安装 `base_url`/Key 为空时首次生成直接报错。`cmhub` 一律由用户在 ⑤ 设置显式切换并填好 Base URL / Key / 别名后才生效。
- 兜底:即使 `backend=cmhub` 但 `base_url`/Key 缺失,也必须抛**清晰的"请去⑤配置 cmhub"错误**(`CMHubError`/`AIError`),不得崩溃或静默直连。
### 4.2 `gen_title` 改造(cmhub 分支)
返回值不变,现有调用保持兼容;允许新增可选事件回调参数承载计费元数据。内层按 backend 分流:
> **返回值不变不等于回调不变(关键实现说明)**:现在 `gen_title`/`gen_cover` 的 `on_step` 回调只传一个**步骤字符串**(`_notify_step(cb, step)`)。要把 `points_cost`/`points_balance`/`call_id` "通过回调上报且不改返回值",就**必须给回调接口扩容**——推荐给 `gen_title`/`gen_cover` **新增一个可选事件回调参数**(如 `on_meta` / `on_event`)传结构化计费元数据,或把 `on_step` 的 payload 从 str 升级为结构化对象。**"返回值不变"成立,但回调契约会变大**;这是**向后兼容的加参**,`generate_batch` 显式传回调、不受影响。实现时不要为了"少改参数"把计费数据塞进返回值或用中文串旁路传递。
- **请求**:`POST {base_url}/api/v1/generate/title`,头 `Authorization: Bearer <cmhub_key>`,体:
```jsonc
{ "prompt": <title_prompt + 旧标题合成为单串>, "model": <title_alias>, "resolution": <可选> }
```
现在 `gen_title` 用 system+user 两条 message,cmhub 只收单个 `prompt`,需把「提示词 + 旧标题 + 只返回新标题」折叠为一个 `prompt` 字符串。
- **响应**:解析 `titles`,取 `titles[0]`(当前一条任务要一个新标题);空列表/空串按现有语义抛 `AIError("AI 返回为空标题")`。
- 不复用现有 `_call_with_retry` 的一刀切重试逻辑;cmhub 分支新增专用 HTTP helper,优先使用项目已依赖的 `requests`,传 `timeout=(connect_timeout, read_timeout)` 以区分连接超时和读超时。`model_used`/`points_cost`/`points_balance`/`call_id` 不改变返回值,通过 `on_step`/事件回调上报给 `generate_batch` 与 GUI worker;T-526 只保证 metadata 事件完整传出,不直接要求写 GUI `run_logs`,T-528 再由 GUI worker 脱敏写 `run_logs` 和展示余额;不入 Excel。
### 4.3 `gen_cover` 改造(cmhub 分支)
返回值不变(仍返回已存 JPEG 路径),现有调用保持兼容;允许新增可选事件回调参数承载计费元数据。内层:
- **请求**:`POST {base_url}/api/v1/generate/image`,体:
```jsonc
{ "prompt": <cover_prompt>, "model": <image_alias>,
"image_base64": <旧封面转 data URL>, "resolution": <resolution>, "aspect_ratio": <可选> }
```
旧封面必传(改图类),复用现有 `_image_data_url(old_cover_path)` 生成 base64。
- `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` 返回值。
- **超时(关键)**:生图同步且慢,cmhub 内部读超时 512≈180s/1K≈240s/2K≈360s/4K≈600s。cmshopee 的**读超时按分辨率取对应上限、统一封顶并默认 600s**,绝不用 30s/60s 调生图——否则客户端超时但服务端仍在算并扣点(见 §4.4 幂等)。
### 4.4 错误映射与重试策略
cmhub 返回结构化 `{error:{code}}`。映射层**按 `code` 优先分支**(不要只看 HTTP 状态),并决定是否重试。实现上新增 `CMHubError(AIError)`,至少带 `code`、`message`、`status`、`retryable`、`retry_after` 字段;GUI 和 worker 不靠中文字符串判断错误类型:
| cmhub code | HTTP | 处理 | 是否重试 |
| --- | --- | --- | --- |
| `insufficient_points` | 402 | 明确提示「点数不足,请先充值」,整批可提前中止 | 否(禁止循环重试)|
| `unauthorized` | 401 | 提示 cmhub API Key 无效,去⑤设置重填 | 否 |
| `account_disabled` | 403 | 提示 Key 被吊销/账号禁用,去网页端重生成 | 否 |
| `bad_request` / `model_not_allowed` / `no_pricing_rule` / `content_blocked` | 400 | 记录具体 code,判为配置/参数/内容错 | 否 |
| `upstream_error` | 502 | 上游失败(cmhub **已自动退点**),可提示稍后重试 | 是(安全)|
| `rate_limited` | 429 | 退避重试,读 `Retry-After`(默认限流 60次/分)| 是 |
| **未列出的 code** | 任意 | 展示 `message`,当不可重试错误(前向兼容)| 否 |
**幂等与超时——生图重试必须特别处理(会亏钱)**:cmhub 生成接口**非幂等、无幂等键**,客户端超时 ≠ 未扣点。
- **生图(`gen_cover`)**:**读超时后绝不自动重发**——服务端可能已算完并扣点,重发 = 重复扣点。首选办法是把读超时设够大(§4.3,默认 600s)从源头避免歧义;只对**连接超时**(请求根本没送达服务端)安全重试。
- **生文(`gen_title`)**:秒级返回、点数低,读超时重试风险小,但仍建议同样区分连接超时/读超时;重试次数可小。
- 只对 `502`/`429`/**连接**超时重试;`402/401/403/400`/读超时立即失败。现状 `_call_with_retry` 是**一刀切重试**,cmhub 模式必须替换为这套区分策略。
- `insufficient_points` 是新的用户可见态:② 生成页应弹明确提示并引导去网页端充值,不当普通失败淹没在计数里。
### 4.5 余额展示与额度预检
- **每次成功响应都带 `points_balance`**——直接用它刷新 ② 页的剩余点数显示,**不必每次再调 `/balance`**。由于 `gen_title`/`gen_cover` 返回值保持不变,生成结果的 `points_cost`/`points_balance`/`call_id` 通过 `on_step`/`on_event` 结构化事件上报;T-526 只保证事件传递,T-528 再记入 `run_logs`(脱敏)并刷新余额,便于对账与报运营排障。
- `GET /api/v1/balance` → `{user, points_balance}`:仅用于**手动刷新余额**或批量前可选预检(`check_balance_before_batch=true`)。非必需,第一版可只依赖成功响应里的余额 + 失败时的 `insufficient_points` 提示。
### 4.6 别名发现(`GET /api/v1/models`)
对接文档已把别名发现从"未来提供"落地为**真实接口**(第 4 个接口,鉴权同为 Bearer Key):
- 响应 `{models:[{alias, operation_type:"title"|"image", capabilities[], requires_image:bool, pricing_status:"priced"|"unpriced", prices:[{resolution, points_cost}]}]}`——只含别名侧信息,不含具体模型名/URL/密钥。
- **⑤设置的别名由手填改为动态下拉**:新增 `app/appconfig.py`(或 `app/ai.py`)helper `fetch_cmhub_models(base_url, api_key)`,⑤按 `operation_type` 拆成「生文别名 / 生图别名」两个下拉;`requires_image` 供 UI 提示(cmshopee 生图恒传旧封面 base64,天然满足);`pricing_status="unpriced"` 的别名**不放入下拉**(直接调用会 `no_pricing_rule`);单价用 `prices` 展示。
- 拉取时机:⑤ 打开或用户点「测试连接/刷新别名」时拉一次,选中的别名仍持久化到 `config.json` 的 `ai.cmhub.title_alias/image_alias`(网关临时不可达时用已存值)。
- 这**关闭了原待确认 #1(别名清单)**——不再需部署方单独提供;仅 Base URL 仍待部署方给。
## 5. 各模块改动点
| 模块 | 改动 | 量 |
| --- | --- | --- |
| `app/ai.py` | `gen_title`/`gen_cover` 加 cmhub 分支(请求体+解析+生图下载);抽 backend 选择;错误映射 + 区分重试。`direct` 分支保留现有代码 | M |
| `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 与 `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` 的设置与用户提示层。
## 6. 安全与合规
- cmhub API Key **本地明文保存但 gitignore**,UI 打码,日志/导出脱敏(T-503 纪律);不写进代码/文档/`config.json`。
- cmhub 返回的 `image_url` 是外部地址,cmshopee 下载时设**超时 + 大小上限**,只允许 `http/https`;不下载内网/回环地址,并校验域名解析后的 IP 仍不是内网/回环/本机地址(cmhub 侧已做 SSRF,但本地下载再校验一层更稳)。
- `run_logs`/诊断日志可记 `alias/model_used/points_cost/points_balance/call_id` 便于排障,但**不得记** API Key、完整请求体、base64 图片、超长 prompt(沿用 `diagnostics` 脱敏)。
## 7. 测试计划
- **单元(mock cmhub,不连真实网关)**:
- 生文:`titles` 多条取首条;空 `titles`/空串 → `AIError`。
- 生图:`image_url` → mock 下载字节 → `_save_jpeg` 落盘校验分辨率/质量;覆盖 scheme、内网/回环字符串地址、域名解析到内网 IP 的拒绝路径。
- 错误码矩阵:402/401/403/400 不重试且原因正确;502/429/连接超时按 attempts 重试;生图读超时不重发;未知 code 不重试。
- 配置:`backend=cmhub` 走 cmhub 分支、`direct` 走旧分支;旧配置缺 `backend` 时保持 direct;缺 `base_url`/Key 时明确报错。
- **GUI**:⑤ cmhub 面板读写、测试连接 worker、别名下拉;② 生成在 `insufficient_points` 时的提示路径。
- **回归**:`direct` 模式现有 `test_ai.py` 用例保持绿。
- 验证命令沿用 `python -m unittest discover -s tests`。
## 8. 迁移与回退
- 既有配置缺 `backend` 时迁移为 `direct`,避免升级后未配置 cmhub 就让②生成失败;用户在⑤设置选择 `cmhub` 并配好 `base_url + Key + 两个别名`后启用网关。
- 回退:`backend=direct` 立即切回本地直连,`ai_models.json` 仍有效。
- 灰度:可先在 ② 单条生成上验证 cmhub 联通与计费,再放批量。
## 9. 待 cmhub 侧确认的问题
1. ~~别名清单~~ **已解决**:`GET /api/v1/models`(§4.6)实时自查可用别名/单价/是否需原图,⑤动态渲染下拉,不再需部署方单独提供别名清单。
2. **生文看图(可先不传)**:接口支持 `image_url`/`image_base64`,但当前 cmshopee 生文只用旧标题文本,第一版不传图。
3. ~~`resolution` 取值~~ **已解决**:`512/1K/2K/4K`(大写 K),默认 `1K`;cmshopee 小写值发请求前归一。
4. ~~`aspect_ratio`~~ **已解决**:默认 `1:1`,Shopee 封面用 `1:1`。
5. **`image_url` 有效期(按最坏处理)**:对象存储 URL 可能过期——本设计已是**生成后立即下载落盘**,无需长期持有。
6. ~~超时上限~~ **已解决**:512≈180s/1K≈240s/2K≈360s/4K≈600s,客户端封顶 600s。
7. **Base URL / API Key 形态**:域名待部署方提供;Key 形如 `sk_cmhub_xxx`,仅网页端生成时显示一次——⑤设置需提示用户从网页端复制粘贴,本地保存。
## 10. 落地拆分与任务顺序
- **第一步**:`app/ai.py` + `app/appconfig.py` 接入 cmhub backend(mock 联调),保留 direct。
- **第二步**:⑤设置 UI cmhub 面板 + 测试连接/查余额。
- **第三步**:② 计费错误提示(`insufficient_points` 引导充值)+ 可选余额展示。
- 文档随每步同步。
> 当前已在 `docs/06-tasks.md` 落成 T-526~T-528;下一步按 T-526 开始代码实现,仍遵守 `docs/05-coding-rules.md` 验证清单。
+2 -1
View File
@@ -69,7 +69,7 @@
任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。 任务状态以 [`06-tasks.md`](06-tasks.md) 为准,历史记录见 [`../progress.md`](../progress.md)。
- 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-103b(登录检测补充识别 Shopee accounts 登录页)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)、T-204b(采集完成自动回写旧字段)、T-205(首次未配账号 / Chrome 未启动 / 未登录引导保护)、T-205b(采集后关闭自动新建商品页 tab)、T-206(Tab① 删除指定批次软删除)、T-301(AI 生成接口)、T-302(Tab② 左右布局与任务列表)、T-302p(提示词管理)、T-303(Tab② 开始生成 + 停止 + 进度)、T-303b(②/③ 商品ID筛选)、T-401(Tab③ 更新列表筛选 + 开始更新确认弹窗)、T-402(Tab③ 确认后串行更新)、T-403(Tab③ 结果回写与结束汇总)、T-501(Tab⑤ AI 模型管理 UI)、T-501b(Tab⑤ 角色与生成参数)、T-501c(Tab⑤ Shopee 更新安全开关)、T-502(换封面删第一张再上传)、T-503(敏感信息本地明文保存提示与日志脱敏)、T-504(多账号并行 / dry-run / 运行日志)、T-207(①采集诊断日志)、② AI生成图片失败诊断日志补丁、T-404a(②/③ 选中记录重置)、T-404(真实 Shopee 单条更新冒烟验收)、T-506(正式使用批量更新体验)、T-507(正式批量更新:移除普通流程测试商品 ID 限制)、T-508(③ 更新shopee生产化操作区)、T-509(② 新标题人工微调)、T-510(③ 检查本轮更新文案统一)、T-505(全流程诊断日志扩展)、T-511(语义色板 + ①②③任务状态列上色)、T-512(③高风险按钮上色 + ①导入校验数字标红)、T-513(登录点 / ③Tab危险标识 / 破坏性按钮上色)、T-514(①②③ 首次空状态引导卡片)、T-515(批次阶段进度总览)、T-516(①筛选对齐②③)、T-517(⑤设置分区 + 清理兼容字段)、T-518(②左栏提示词区组件密度优化)、T-519(②AI生成长任务进度条 + 用户可读滚动日志)、T-520(②AI生成封面可选生成开关)、T-521(依赖清单:锁版本 requirements)、T-522(CI:自动跑语法 + 单元 / GUI 测试)、T-523(拆分 `app/gui.py` 为 `app/gui/` 包)、T-523a(②/③ 新一轮运行前清空界面日志显示)、T-404b(商品详情页加载失败 toast 自动捕获)、T-524(PyInstaller 打包为免安装 exe)。 - 已完成:T-000(正式代码包结构)、T-001(`app/editor.py` 模块化)、T-002(`app/appconfig.py` + `config.json`)、T-003(SQLite 建表)、T-004(本地数据 gitignore)、T-005(AI 模型清单后端)、T-006(单元测试基座)、T-101(账号 slug/user-data-dir)、T-102(Chrome 启动器)、T-103(登录保活与检测)、T-103b(登录检测补充识别 Shopee accounts 登录页)、T-104(PySide6 五 Tab 主窗口骨架)、T-104b(PySide6 worker 基座)、T-105(Tab④ 账号管理)、T-106(账号快捷方式)、T-201(Excel 导入:解析多文件输入列入库)、T-202(Tab① 任务列表 + 导入按钮 + 别名匹配标记)、T-202b(Tab① 导入汇总栏)、T-203(采集旧标题+旧封面)、T-204(回写旧字段到原 Excel)、T-204b(采集完成自动回写旧字段)、T-205(首次未配账号 / Chrome 未启动 / 未登录引导保护)、T-205b(采集后关闭自动新建商品页 tab)、T-206(Tab① 删除指定批次软删除)、T-301(AI 生成接口)、T-302(Tab② 左右布局与任务列表)、T-302p(提示词管理)、T-303(Tab② 开始生成 + 停止 + 进度)、T-303b(②/③ 商品ID筛选)、T-401(Tab③ 更新列表筛选 + 开始更新确认弹窗)、T-402(Tab③ 确认后串行更新)、T-403(Tab③ 结果回写与结束汇总)、T-501(Tab⑤ AI 模型管理 UI)、T-501b(Tab⑤ 角色与生成参数)、T-501c(Tab⑤ Shopee 更新安全开关)、T-502(换封面删第一张再上传)、T-503(敏感信息本地明文保存提示与日志脱敏)、T-504(多账号并行 / dry-run / 运行日志)、T-207(①采集诊断日志)、② AI生成图片失败诊断日志补丁、T-404a(②/③ 选中记录重置)、T-404(真实 Shopee 单条更新冒烟验收)、T-506(正式使用批量更新体验)、T-507(正式批量更新:移除普通流程测试商品 ID 限制)、T-508(③ 更新shopee生产化操作区)、T-509(② 新标题人工微调)、T-510(③ 检查本轮更新文案统一)、T-505(全流程诊断日志扩展)、T-511(语义色板 + ①②③任务状态列上色)、T-512(③高风险按钮上色 + ①导入校验数字标红)、T-513(登录点 / ③Tab危险标识 / 破坏性按钮上色)、T-514(①②③ 首次空状态引导卡片)、T-515(批次阶段进度总览)、T-516(①筛选对齐②③)、T-517(⑤设置分区 + 清理兼容字段)、T-518(②左栏提示词区组件密度优化)、T-519(②AI生成长任务进度条 + 用户可读滚动日志)、T-520(②AI生成封面可选生成开关)、T-521(依赖清单:锁版本 requirements)、T-522(CI:自动跑语法 + 单元 / GUI 测试)、T-523(拆分 `app/gui.py` 为 `app/gui/` 包)、T-523a(②/③ 新一轮运行前清空界面日志显示)、T-404b(商品详情页加载失败 toast 自动捕获)、T-524(PyInstaller 打包为免安装 exe)。
- 下一个可领取任务:T-525(引入 ruff lint + format + 可选 pre-commit)。 - 下一个可领取任务:T-526(`app/ai.py` + `appconfig` 接入 cmhub backend);T-525 ruff 顺延到 cmhub 对接后。
## 当前已知限制 ## 当前已知限制
@@ -96,6 +96,7 @@
- T-523a 已完成:②点击「开始生成」、③点击「检查本轮更新」或「开始更新」时,先清空对应界面的旧日志显示并写入本轮开始摘要;运行中只追加本轮日志。历史 `run_logs/run_log_events` 和本地 `logs/` 不删除、不自动混入当前运行界面。 - T-523a 已完成:②点击「开始生成」、③点击「检查本轮更新」或「开始更新」时,先清空对应界面的旧日志显示并写入本轮开始摘要;运行中只追加本轮日志。历史 `run_logs/run_log_events` 和本地 `logs/` 不删除、不自动混入当前运行界面。
- T-404b 已完成:`open_product()` 在导航商品详情页前后安装 toast 捕获,等待详情页关键元素超时时会读取最近 `.eds-toasts`/toast/message 节点及页面缓存,把明确商品失效/商品不存在/无权限类 toast 上浮为 `商品失效:<原始toast>`;如果失败发生在 `open_product()` 内部,本轮自动新建的失败 tab 会关闭,复用用户已有 tab 不关闭;①列表“阶段”列只在这类明确失效错误时显示“商品失效”,底层仍保持 `stage=imported/status=failed`,其他打开失败仍显示“失败”。 - T-404b 已完成:`open_product()` 在导航商品详情页前后安装 toast 捕获,等待详情页关键元素超时时会读取最近 `.eds-toasts`/toast/message 节点及页面缓存,把明确商品失效/商品不存在/无权限类 toast 上浮为 `商品失效:<原始toast>`;如果失败发生在 `open_product()` 内部,本轮自动新建的失败 tab 会关闭,复用用户已有 tab 不关闭;①列表“阶段”列只在这类明确失效错误时显示“商品失效”,底层仍保持 `stage=imported/status=failed`,其他打开失败仍显示“失败”。
- T-524 已完成:新增 PyInstaller onedir 打包配置、构建脚本和发布目录校验;打包版启动时工作目录切到 `cmshopee.exe` 所在目录,首次运行在 exe 同级生成/使用本地配置、DB、图片、日志、登录态和提示词;发布包不内置这些本地数据,用户后续更新采用关闭程序后覆盖程序文件、保留本地数据的方式。 - T-524 已完成:新增 PyInstaller onedir 打包配置、构建脚本和发布目录校验;打包版启动时工作目录切到 `cmshopee.exe` 所在目录,首次运行在 exe 同级生成/使用本地配置、DB、图片、日志、登录态和提示词;发布包不内置这些本地数据,用户后续更新采用关闭程序后覆盖程序文件、保留本地数据的方式。
- Phase 7 cmhub 网关对接已补设计与任务定义:T-526 先接 `app/ai.py`/`appconfig` backend,T-527 做⑤设置面板,T-528 做②计费错误提示与余额展示;T-525 ruff 因业务优先级顺延。
- T-301/T-303 已完成通用 HTTP AI 接口、批量生成编排和 GUI 接入 mock 单测;真实 AI 生成还需要在 `config/ai_models.json` 填入可用 url/model/api_key 后做一次成本可控的小样本实测;URL 可填完整 endpoint 或 OpenAI-compatible base URL。 - T-301/T-303 已完成通用 HTTP AI 接口、批量生成编排和 GUI 接入 mock 单测;真实 AI 生成还需要在 `config/ai_models.json` 填入可用 url/model/api_key 后做一次成本可控的小样本实测;URL 可填完整 endpoint 或 OpenAI-compatible base URL。
- 本地 `config/ai_models.json` 若由旧版本或手工维护,可能缺少 `category`;启动报 “AI 模型 category 必须是 text 或 image” 时,按 [`troubleshooting.md`](troubleshooting.md) 只补 `category` / `enabled` 等非密钥字段,保留 API Key,且不要提交该文件。 - 本地 `config/ai_models.json` 若由旧版本或手工维护,可能缺少 `category`;启动报 “AI 模型 category 必须是 text 或 image” 时,按 [`troubleshooting.md`](troubleshooting.md) 只补 `category` / `enabled` 等非密钥字段,保留 API Key,且不要提交该文件。