Files
cmshoppe/docs/tasks/T-677.md
T
2026-07-20 15:23:21 +08:00

144 lines
16 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
id: T-677
title: ⑤生成网关来源切换(默认网关/自定义网关)
phase: 7
deps: [T-517, T-529]
status: TODO
created: 2026-07-20
---
# T-677 ⑤生成网关来源切换(默认网关/自定义网关)
## 问题 / 背景
T-529 把普通用户的 AI 后端收口为 cmhub:⑤ 隐藏「AI 后端」下拉,保存固定写 `ai.backend=cmhub`。当前生文、生图、图片理解全部只走 cmhub 托管并扣点。
现在需要给用户一条自定义(OpenAI 兼容)网关的逃生口,用于 cmhub 托管上游不稳或用户自备算力的场景。相关代码事实:
- `direct` 路径并未删除,当前按既有 OpenAI 兼容请求形式工作:`app/ai.py` 生文走 `chat/completions`,生图支持 `api_type` = `chat` / `images_edits` / `auto`(`app/appconfig.py:41`),`appconfig` 会自动补 endpoint;配置 schema 与校验在 `app/appconfig.py`(`list_ai_models` / 模型规范化)。
- ⑤ 的自定义模型 UI(模型列表、模型详情、direct 角色选择)也都还在,只是被 `_on_backend_changed()` 无条件 `setVisible(False)` 硬藏(`app/gui/tabs/settings.py:580-584`),`backend_combo` 同样常隐。
- 因此本任务的主要工作是**恢复并规范双面板切换**,不是新建自定义配置体系。
覆盖面不对称是本任务最大的风险点:默认网关有生文/生图/图片理解三个角色,外加商品套图;`direct` 只有生文、生图两条路径。当前正式第六 Tab 是「商品套图」,旧 `ImageStudioTab` 仅作内部兼容,不能只写“AI工场入口拦截”而遗漏真实可见入口。
- `app/ai.py` 的 `analyze_product_images()` 在非 cmhub 时直接抛错(⑥商品套图AI帮写);
- `app/image_studio_generation.py` 全文只有 cmhub 分支,其 `_runtime()` 直接取 `ai._cmhub_runtime()`,**完全不看 `ai.backend`**。
若只改 UI 不补联动,用户切到「自定义网关」后商品套图仍会用默认网关 Key 继续跑、继续扣点——用户以为切走了而费用照扣,属于资金口径错误,必须在本任务同版本内处理。
## 方案
### 1. ⑤ 网关分区改为来源选择
- `app/gui/tabs/settings.py` 第一段 `_section_title("cmhub 网关", "settingsAiModelSectionTitle")` 文案改为「生成网关」,**保留该 section title**(⑤ 是长页面,分区标题与「角色与生成参数」「蝦皮更新执行」「基础设施」并列承担导航作用,不可直接删掉换控件)。
- 在该分区标题所在位置增加**互斥选中态**的来源选择器:「默认网关」/「自定义网关」。
- 用两个 `setCheckable(True)` 的按钮加入同一 `QButtonGroup`(`setExclusive(True)`)实现分段选择器外观,或等价的两个 `QRadioButton`;
- **不得使用普通 `QPushButton`**:这是状态选择不是动作,普通按钮无法表达"当前选中项",也无法表达"点击即生效还是保存生效";
- 两个控件各自设置 `objectName`(如 `gatewayDefaultButton` / `gatewayCustomButton`)供 GUI 测试选择。
- 选择器只改 pending 状态并切换面板显隐,**落盘发生在「保存设置」时**;选择器需接入 `_connect_dirty_signals()`,切换触发未保存提示,与⑤其他控件行为一致。载入配置时在 `_dirty_tracking_suspended()` 内设置选中态。
### 2. 恢复双面板显隐
- `_on_backend_changed()`(`app/gui/tabs/settings.py:579-587`)去掉硬编码隐藏,改为按当前来源双向显隐:
- 默认网关 → 显示 `cmhub_panel`,隐藏 `model_picker_panel`、`model_detail_section_title`、`model_detail_panel`、`direct_role_panel`;
- 自定义网关 → 相反。
- 自定义侧复用现有面板与控件,不新建:模型列表、模型详情(名称/接口地址/模型ID/API Key/api_type/超时/启用)、`direct_role_panel` 的默认文本模型与默认图像模型选择、以及现有模型测试按钮(`AIModelTestWorker`)。
- 两套配置的**测试按钮与结果标签必须分开**,自定义侧不得复用 `cmhub_result_label`。
- 别名下拉(`cmhub_title_alias_combo` / `cmhub_image_alias_combo` / `cmhub_vision_alias_combo`)属于 `cmhub_panel`,随面板整体显隐即可。
### 3. 配置读写与迁移
- 沿用现有 `ai.backend` 字段与取值 `cmhub` / `direct`(`appconfig.AI_BACKENDS`、`app/ai.py` 的 `_ai_backend()` 校验保持不变),**不新增第三种取值、不新增并行的来源字段**。
- `_collect_settings_values()` 中 `backend = "cmhub"` 的硬编码(`app/gui/tabs/settings.py:751`)改为读选择器当前值。
- **两套配置并存、切换不清空**:`data/config/cmhub.json` 与 `data/config/ai_models.json` 互不影响,来回切换后原配置仍在。
- 存量迁移天然成立:老配置 `backend=direct` 载入后选中「自定义网关」,`backend=cmhub` 或缺失按现状选中「默认网关」。
- 沿用 v3.1 约定:**允许先保存不完整配置**,②生成时再提示"去⑤补齐",切换来源时不弹阻断式校验。
- 保存语义必须明确且不混淆:来源选择器仅修改待保存状态,只有点击「保存设置」才切换运行来源;既有「新增 / 保存 / 删除模型」仍立即写入 `ai_models.json`,不会等待「保存设置」。用户点击放弃设置时,只恢复来源和普通设置字段,不回滚已经单独保存的模型 CRUD。
- `api_type=auto` 不是自动探测接口:当前 direct 实现只有 `images_edits` 走图片编辑请求,`chat` 与 `auto` 都走对话请求。本任务不新增 `images/generations`、provider 适配或接口探测;自定义网关 UI 应将用户可见字段改为「接口类型」,并在帮助文案中按现有三种行为说明,不得泛称所有 OpenAI 兼容服务均可用。
### 3a. 运行配置冻结与保存竞争
- ② `GenerateWorker`、⑥商品套图生成与 AI 帮写开始时必须冻结本轮 AI 配置快照;运行中打开⑤并保存来源切换,不得让同一批任务前后混用默认网关和自定义网关。
- ⑤保存来源成功后,主窗口需通知⑥刷新来源能力状态;⑥再次显示时也应重新检查当前来源。可以用 `SettingsTab` 的保存成功 signal/callback 与 `MainWindow` 转发实现,但不得依赖用户重启程序。
- 即使 UI 刷新遗漏,worker/service 层仍必须按创建时的冻结来源执行,不能读取被后续保存原地修改的共享 `config`。
### 4. 必须同版本处理的六个联动
1. **⑥商品套图入口拦截**:`ai.backend != "cmhub"` 时,正式可见的 `ProductSuiteTab`「生成套图」、图片重试和「AI帮写」入口置灰并说明「商品套图仅支持默认网关」;保存来源切换后立即刷新,切入⑥时也再次刷新。旧 `ImageStudioTab` 不在主 Tab 栏(`app/gui/widgets.py:57` 的 `TAB_TITLES` 不含它,`tests/test_gui.py` 显式断言其不存在),由第 2 条的服务层防线覆盖即可,**不单独为它改 UI**。
2. **⑥防御性服务拦截,但必须区分"新建"与"取回"**:UI 置灰不足以作为费用边界,同时**不得把已经扣过点的任务困死**。
- **拒绝**:新建与新提交——`ProductSuiteGenerateWorker` 在创建 job 前拒绝非默认网关;`image_studio_generation` 侧拦在 `create_generation_jobs()` 与 `_submit_or_resume_job()` 中"尚无 `image_task_id`"的提交分支。
- **放行**:已提交任务的轮询、下载与保存——`resume_image_jobs()` 同样走 `run_jobs()`(`app/image_studio_generation.py:142-163`),**不能在 `run_jobs()` 入口一刀切拒绝**,否则用户切到自定义网关后,此前已提交到默认网关、已预扣点数、正等待轮询取回的生图任务将永久拿不回来(钱已花、图丢失)。已有 `image_task_id` 的 job 必须继续用默认网关凭据完成轮询与下载。
- 口径依据:Obsidian《卡密设备绑定与套餐授权方案》第五节 §5「已被服务端接受的异步任务轮询、下载:允许,避免已预扣点数却拿不到结果」。本任务与该原则保持一致,实现时不要另立边界。
- `ProductSuiteAiWriteWorker` 与 `analyze_product_images()` 保持非默认网关不可用(图片理解无异步任务、不存在已扣点待取回的情形,可直接拒绝);UI 必须在读取默认网关模型目录、展示扣点确认、提交图片理解前拦截,而不是等待运行后报错。
3. **⑥商品套图说明**:⑤自定义面板增加说明行,明确「图片理解与商品套图仅默认网关支持」;拦截提示使用同一中文口径,不显示技术实现或 cmhub Key 信息。
4. **②自定义网关预检**:允许保存不完整配置,但点击②开始生成时、创建 `GenerateWorker` 前按本轮“只生成标题 / 只生成封面 / 生成图文”检查所需 direct 模型是否已启用、类别匹配、模型地址、模型 ID、API Key、接口类型齐全。失败时中文提示去⑤补齐,且不创建 worker、不发请求、不逐条改任务状态。不要等 `_role_model()` 在批量运行中逐条失败。
5. **「批量生成前检查余额」**(`cmhub_check_balance_checkbox`):自定义网关下隐藏——点数余额是默认网关概念。
6. **点数展示文案**:②自定义网关每轮开始时在运行日志和状态栏各明确一次显示「自定义网关(不计点数,费用由服务商收取)」,并清空/隐藏上轮默认网关余额;不得显示 `0 点`、空白余额或沿用旧余额。默认网关下 `points_cost` / `points_balance` 展示保持不变。
### 5. 用户可见文案收敛
⑤ 面板内以及⑤触发的状态栏用户可见文案中的 `cmhub` 字样收敛为「默认网关」口径,包括分区标题、Base URL/API Key placeholder 与帮助、刷新/测试按钮、连接中/成功/失败结果、明文保存警告(`PLAINTEXT_CMHUB_API_KEY_WARNING`,`app/gui/tabs/settings.py:17`)。
**②的余额展示一并纳入**:`cmhub_balance_label`(`app/gui/tabs/generate.py:1092`,默认文案「cmhub余额:未获取」)虽不在⑤内,但与⑤同屏出现在用户视野中。若只改⑤,会出现⑤显示「默认网关」而②显示「cmhub余额」的两套叫法,用户第一眼即困惑。该 label 的可见文案改为「默认网关余额」口径,与第 4 节第 6 条的自定义网关文案配套。
内部类名、`objectName`(含 `cmhub_balance_label` 本身的标识)、配置键、日志字段和 `app/ai.py` 的中文错误消息(如"连接 cmhub 超时")不改。
### 6. 效果图
按 `AGENTS.md` 约定出 `docs/ui/tab5-settings-T677.svg`(⑤ 是改版,已有 `tab5-settings.svg`),在 `docs/ui/README.md` 登记,随本任务文档一起提交。
## 验收要点
- [ ] ⑤ 生成网关分区显示互斥选中态的「默认网关」/「自定义网关」,当前选中项视觉可辨;两者均有 `objectName`。
- [ ] 切换来源立即切面板显隐并标记未保存;未点「保存设置」时不落盘,取消/重新载入后恢复原选择。
- [ ] 选默认网关时只显示默认网关配置(Base URL / API Key / 三个别名 / 连接超时 / 检查余额 / 测试连接·查余额);选自定义网关时只显示自定义模型列表、模型详情与默认文本/图像模型选择。
- [ ] 保存后 `config.json` 的 `ai.backend` 正确写入 `cmhub` / `direct`;`cmhub.json` 与 `ai_models.json` 在来回切换保存后内容均不丢失。
- [ ] 载入 `backend=direct` 的存量配置时选中「自定义网关」并正确回填自定义模型;缺失或 `cmhub` 时选中「默认网关」。
- [ ] 自定义网关下②生文、②生图可用;⑥商品套图的生成、重试和 AI 帮写入口被拦截并提示仅默认网关支持;worker/service 防线证明非默认网关不会创建 job、读取默认网关模型目录、提交新请求或扣点。
- [ ] **切到自定义网关后,此前已提交(已有 `image_task_id`)的默认网关生图任务仍可通过 `resume_image_jobs()` 完成轮询、下载与保存,不因来源切换被拒绝或丢失**;未提交的 job 则被拒绝创建。
- [ ] ②/⑥运行开始后保存来源切换,不影响已启动任务的来源快照;保存完成后⑥按钮状态立即刷新,重新进入⑥时也正确刷新。
- [ ] 自定义网关下「批量生成前检查余额」不可见;②日志/状态栏显示"不计点数"口径而非 `0 点`,且清除旧余额;默认网关下点数与余额展示不回归。
- [ ] 自定义配置不完整时可保存;②按本轮生成内容在创建 worker 前给出"去⑤补齐"的可读提示,不发请求、不改任务状态、不崩溃。
- [ ] 自定义模型 CRUD 的立即保存与来源选择的待保存语义均有明确中文反馈;放弃设置不回滚已保存模型,但恢复未保存来源选择。
- [ ] `auto` 接口类型的实际请求行为有测试覆盖,不宣称自动探测或未实现的 `images/generations` 支持(若 `tests/test_ai.py` 现有用例已覆盖该分支,确认后即可,不必重复补)。
- [ ] ⑤ 面板、⑤触发的状态栏、以及②余额 label 的可见文案不再出现 `cmhub` 字样;内部标识(含 `objectName`、配置键、日志字段)和 `app/ai.py` 错误消息不在本次范围。
- [ ] `tests/test_gui.py` 覆盖:选择器互斥与默认选中、面板显隐、dirty 与保存生效、存量 `backend=direct` 载入、模型立即保存与来源待保存语义、②预检、⑥入口/worker拦截、余额控件显隐、点数文案分支、运行配置快照;`tests/test_ai.py` 覆盖 direct 文本/图片、`auto` 的既有请求路径与缺配置报错;现有 backend 相关断言同步更新不回归。
- [ ] `docs/ui/tab5-settings-T677.svg` 已产出并在 `docs/ui/README.md` 登记。
## 测试与文档
- 更新 `docs/cmhub-integration-design.md`:补一条修订说明,记录 T-529 的"普通 UI 不提供后端切换"在本任务被有限放开,及自定义网关的定位与覆盖边界。
- 更新 `docs/routes.md` / `docs/api.md` 中⑤设置与生成来源的描述。
- 运行:
```bash
py -3.10 -m unittest discover -s tests -p "test_gui.py"
py -3.10 -m unittest discover -s tests
py -3.10 -m ruff check app tests main.py
py -3.10 -m compileall app main.py
git diff --check
```
## 边界(不改什么)
- 不给商品套图(`app/image_studio_generation.py`)和图片理解(`analyze_product_images()`)新增自定义网关路径;本任务只做入口拦截与提示。
- 不实现方案中经 cmhub 代理透传的正式 BYOK(T-596 / T-599~T-602 仍按其门禁推进);本任务的自定义网关是**客户端直连的过渡形态**,正式商业版需迁移到网关代理并重新收口 direct 入口。
- 不修改 `ai_models.json` 的 schema 与 `API_TYPES` 取值,不新增 provider 适配。
- 不改 `app/ai.py` 的 cmhub 请求协议、重试与超时策略、异步 submit/poll 状态机、幂等键。
- 不改 `app/ai.py` 中文错误消息里的 `cmhub` 字样。
- 不新增 `images/generations`、接口自动探测或 provider 专用适配;自定义网关仅覆盖现有 direct 文本/图片请求能力。
- 不改 SQLite schema、Chrome/CDP、Excel 回写、蝦皮更新流程。
- 不涉及卡密、设备绑定、订阅授权(见 Obsidian《卡密设备绑定与套餐授权方案》,另行拆任务)。
## 关联
- T-529:默认 cmhub 网关并隐藏 AI 后端选择(本任务有限放开其 UI 收口)。
- T-517:⑤设置分区(分区标题与布局约定)。
- T-596:AI工场自定义模型(BYOK)总设计与启动门禁(`status: BLOCKED`,正式形态,本任务不解除其门禁)。
- Obsidian《卡密设备绑定与套餐授权方案》第五节 §9、第六节 §5:正式发行版需移除普通用户可通过配置开启的 direct 直连路径。
## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)