feat: complete T-532 cmhub account prompt
This commit is contained in:
@@ -180,14 +180,35 @@ def mask_secret(secret) -> str:
|
||||
return f"{text[:4]}***{text[-4:]}"
|
||||
|
||||
|
||||
def mask_email(email) -> str:
|
||||
"""Return a display-safe email string with the local part masked."""
|
||||
|
||||
text = str(email or "").strip()
|
||||
if not text or "@" not in text:
|
||||
return text
|
||||
local, domain = text.split("@", 1)
|
||||
if not local or not domain:
|
||||
return mask_secret(text)
|
||||
if len(local) == 1:
|
||||
masked_local = "***"
|
||||
elif len(local) == 2:
|
||||
masked_local = f"{local[0]}***"
|
||||
else:
|
||||
masked_local = f"{local[0]}***{local[-1]}"
|
||||
return f"{masked_local}@{domain}"
|
||||
|
||||
|
||||
def sanitize_for_log(value):
|
||||
"""Return a copy of value with secret fields masked for logs/status/export."""
|
||||
|
||||
if isinstance(value, dict):
|
||||
sanitized = {}
|
||||
for key, child in value.items():
|
||||
lowered = str(key).lower()
|
||||
if _is_secret_field_name(key):
|
||||
sanitized[key] = mask_secret(child)
|
||||
elif lowered == "email" or lowered.endswith("_email"):
|
||||
sanitized[key] = mask_email(child)
|
||||
else:
|
||||
sanitized[key] = sanitize_for_log(child)
|
||||
return sanitized
|
||||
|
||||
@@ -1119,10 +1119,67 @@ class SettingsTab(QWidget):
|
||||
image_count = self._cmhub_alias_count("image")
|
||||
balance = payload.get("points_balance")
|
||||
balance_text = f";余额 {balance}" if balance is not None else ""
|
||||
message = f"cmhub 连接成功:生文别名 {title_count} 个,生图别名 {image_count} 个{balance_text};别名已拉取,记得点『保存设置』持久化"
|
||||
subject = self._cmhub_success_subject(payload)
|
||||
message = f"{subject}:生文别名 {title_count} 个,生图别名 {image_count} 个{balance_text};别名已拉取,记得点『保存设置』持久化"
|
||||
self.cmhub_result_label.setText(message)
|
||||
self._set_status(message)
|
||||
|
||||
def _cmhub_success_subject(self, payload):
|
||||
account_name = self._cmhub_account_display_name(payload)
|
||||
if account_name:
|
||||
return f"cmhub 账号「{account_name}」连接成功"
|
||||
return "cmhub 连接成功"
|
||||
|
||||
def _cmhub_account_display_name(self, payload):
|
||||
if not isinstance(payload, dict):
|
||||
return ""
|
||||
sources = []
|
||||
balance = payload.get("balance")
|
||||
if isinstance(balance, dict):
|
||||
account = balance.get("account")
|
||||
if isinstance(account, dict):
|
||||
sources.append(account)
|
||||
user = balance.get("user")
|
||||
if isinstance(user, dict):
|
||||
sources.append(user)
|
||||
elif user is not None:
|
||||
sources.append({"user": user})
|
||||
sources.append(balance)
|
||||
account = payload.get("account")
|
||||
if isinstance(account, dict):
|
||||
sources.append(account)
|
||||
user = payload.get("user")
|
||||
if isinstance(user, dict):
|
||||
sources.append(user)
|
||||
elif user is not None:
|
||||
sources.append({"user": user})
|
||||
sources.append(payload)
|
||||
for source in sources:
|
||||
name = self._cmhub_display_name_from_source(source)
|
||||
if name:
|
||||
return name
|
||||
return ""
|
||||
|
||||
def _cmhub_display_name_from_source(self, source):
|
||||
if not isinstance(source, dict):
|
||||
return ""
|
||||
for field in ("display_name", "name", "account_name", "username", "user", "email", "id"):
|
||||
value = source.get(field)
|
||||
if value is None:
|
||||
continue
|
||||
if isinstance(value, dict):
|
||||
nested_name = self._cmhub_display_name_from_source(value)
|
||||
if nested_name:
|
||||
return nested_name
|
||||
continue
|
||||
text = str(value).strip()
|
||||
if not text:
|
||||
continue
|
||||
if field == "email" or "@" in text:
|
||||
return appconfig.mask_email(text)
|
||||
return text
|
||||
return ""
|
||||
|
||||
def _on_cmhub_failed(self, _task_id, error):
|
||||
message = f"cmhub 连接失败:{error}"
|
||||
self.cmhub_result_label.setText(message)
|
||||
|
||||
+1
-1
@@ -131,7 +131,7 @@
|
||||
| T-529 | 默认 cmhub 网关并隐藏 AI 后端选择 | T-528, T-527 | 产品收口:后续普通用户只使用 cmhub 网关,不再在⑤设置页暴露「AI 后端」label 与 direct/cmhub 下拉框。方案:`DEFAULT_CONFIG.ai.backend` 改为 `cmhub`;⑤设置页默认直接展示 cmhub 网关配置(Base URL、API Key、生文/生图别名、刷新别名、测试连接/查余额),隐藏 direct 模型选择、模型详情、标题/图片模型角色下拉和「AI 后端」下拉;保存设置固定写 `ai.backend=cmhub`,允许先保存不完整 cmhub 配置,②生成时仍由 `app/ai.py` 对缺 Base URL/Key/别名给出「请去⑤配置 cmhub」错误。direct 代码、`config/ai_models.json` helper 与 direct 单测保留为内部兼容/手工回滚路径,但普通 UI 不提供切换入口。同步 `appconfig`、⑤设置页、②余额默认显示和 GUI/appconfig 单测;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE |
|
||||
| T-530 | cmhub Base URL 规整 + 404 明确提示 | T-526, T-527 | 现象:手动填入 Base URL 后点「刷新别名」提示 notfound(HTTP 404)。核对确认 cmshopee 请求已达标(`GET /api/v1/models` + Bearer,对齐对接文档 §4.4 与 cmhub `ModelsView` 路由,见 `docs/troubleshooting.md`「cmhub 刷新别名 notfound」与 `docs/cmhub-integration-design.md` v3.4);404 根因是 Base URL 带多余 `/api(/v1)` 路径导致双拼、或所连实例未部署 `/api/v1/models`。方案:保存/请求前规整 Base URL——去掉结尾的 `/`、`/api`、`/api/v1` 等多余路径段,只保留 scheme+host(+port)(`appconfig.cmhub_request_url()` 或保存时统一处理),并在⑤输入框旁给出「只填网关根,如 https://host」提示;`_cmhub_code_for_status` 给 404 一个明确码/中文提示(如「cmhub 接口不存在,请检查 Base URL 或该实例是否已部署 /api/v1/models」),不再透传生涩英文原文。补 `test_appconfig`(Base URL 规整:带 `/api/v1`、带尾斜杠、带路径均归一到根)与 `test_ai`(404 → 明确提示)单测;不改 AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE |
|
||||
| T-531 | ⑤ 设置未保存状态追踪 + 离开确认 | T-530, T-529, T-527 | 现象:在⑤填写 cmhub Base URL/API Key(或改任何设置)后,没有任何"未保存"提示;切换到其它 Tab、关闭窗口都不拦截,输入静默丢失。且「刷新别名/测试连接」用输入框实时文本、不落盘,成功后易误以为已保存。采用**方式 B(显式保存 + 脏状态追踪 + 离开确认)**,不改自动保存(API Key/半份 cmhub 配置不宜自动写盘)。方案:① 脏追踪——⑤ 所有可编辑控件(cmhub Base URL/API Key/别名下拉、模型详情、生成参数、路径端口、Shopee 更新安全等)接 `textChanged`/`currentIndexChanged`/`toggled`/`valueChanged` 置 `self._dirty=True`;「保存设置」成功后清 `False`;`save_app_settings()` 或等价保存入口必须返回成功/失败,失败时保留 dirty 并阻止离开。② 程序化回填保护——加载配置、保存后重载、刷新别名写回下拉、`_populate_app_settings()` / `_populate_cmhub_alias_combos()` 等必须用 `_suspend_dirty` 或 `QSignalBlocker` 避免误置脏;用户手动输入后由刷新/测试触发的 Base URL 规整仍应保持 dirty,提醒其保存。③ 离开拦截——`main_window` 切 Tab 与 `closeEvent` 若发现⑤ `_dirty` 为真,弹三选一「保存 / 放弃 / 取消」:保存则调保存入口且成功后再切/关;放弃必须重新从 `config.json` + `config/cmhub.json` 载入并回填控件,再清 dirty,不能只清标志;取消则留在⑤不切不关。`QTabWidget.currentChanged` 是切换后信号,实现时需维护上一 Tab index,并用 `_reverting_tab_change`/类似 guard 防止 `setCurrentIndex()` 递归触发。④ 视觉提示——「保存设置」旁显示「● 未保存更改」,保存或放弃回填后消失。⑤ 「刷新别名/测试连接」成功后提示语补「别名已拉取,记得点『保存设置』持久化」,**不**顺带偷偷保存 url/key(保持显式保存一致性)。只改 ⑤ 设置页、`main_window` Tab 切换/关闭守卫和 GUI 单测(脏标志置位/清除、切 Tab 保存/放弃/取消/保存失败分支、程序化回填不置脏、放弃会还原输入、currentChanged 回退不递归);不改配置 schema、AI HTTP 协议、DB schema、Excel、Shopee/CDP 流程 | DONE |
|
||||
| T-532 | ⑤ cmhub 连接成功提示显示账号名 | T-531, T-527, T-528 | 问题:⑤「测试连接/查余额」或刷新别名成功后,界面只提示 `cmhub 连接成功:...`,用户无法确认当前 API Key 实际属于哪个 cmhub 账号,尤其多账号或换 Key 时容易误判。方案:成功 payload 中若 `/api/v1/balance` 返回可识别账号信息,则在结果 label 和状态栏优先显示 `cmhub 账号「<账号名>」连接成功:生文别名 X 个,生图别名 X 个;余额 Y;别名已拉取,记得点『保存设置』持久化`;账号名来源按 `balance.user.name/account_name/username/email/id`、`balance.account.*`、顶层 `name/username/email` 等常见字段兜底提取,邮箱需要脱敏展示;没有账号字段、仅刷新别名不查余额或接口未返回用户信息时,保留现有 `cmhub 连接成功:...` 文案。不得把 API Key、token、完整邮箱或原始敏感响应写入 GUI、run log、诊断日志;不改变 cmhub HTTP 协议、配置 schema、保存逻辑、AI 生成流程或 Shopee/CDP 流程。同步 GUI 单测覆盖有账号名、只有邮箱、无账号名三种成功提示 | TODO |
|
||||
| T-532 | ⑤ cmhub 连接成功提示显示账号名 | T-531, T-527, T-528 | 问题:⑤「测试连接/查余额」或刷新别名成功后,界面只提示 `cmhub 连接成功:...`,用户无法确认当前 API Key 实际属于哪个 cmhub 账号,尤其多账号或换 Key 时容易误判。方案:成功 payload 中若 `/api/v1/balance` 返回可识别账号信息,则在结果 label 和状态栏优先显示 `cmhub 账号「<账号名>」连接成功:生文别名 X 个,生图别名 X 个;余额 Y;别名已拉取,记得点『保存设置』持久化`;当前接口结构按 `{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }` 兼容,显示名优先取 `account.display_name/name/account_name/username/email/id`,再兜底 `user` 字符串或 `user.name/account_name/username/email/id`、顶层 `display_name/name/username/email/id`;邮箱需要脱敏展示;没有账号字段、仅刷新别名不查余额或接口未返回用户信息时,保留现有 `cmhub 连接成功:...` 文案。不得把 API Key、token、完整邮箱或原始敏感响应写入 GUI、run log、诊断日志;不改变 cmhub HTTP 协议、配置 schema、保存逻辑、AI 生成流程或 Shopee/CDP 流程。同步 GUI 单测覆盖有账号名、当前接口结构、只有邮箱、无账号名四种成功提示 | DONE |
|
||||
|
||||
## Phase 8 · 工程基础设施后续(`docs/engineering-review.md`)
|
||||
|
||||
|
||||
+1
-1
@@ -383,7 +383,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负
|
||||
|
||||
- `SettingsTab` 使用居中内容区 + 适度左右留白布局,当前留白已从 T-506 初始实现缩短到约 40%;实现上使用最大内容宽度和自适应 margin,避免固定像素导致小屏挤压。各设置组默认响应式 3 列表单:短字段占 1 格,长字段(URL/API Key/路径)跨 2 格或 3 格,窄窗口降为 2 列/1 列。点击「保存设置」成功后,调用 `QMessageBox.information` 弹出“设置已保存”轻量提示框,同时保留状态栏提示。T-531 已完成:`save_app_settings()` 返回 bool,成功写 `config.json` + `config/cmhub.json` 后清 dirty,失败保留 dirty 并让调用方阻止离开。
|
||||
- `SettingsTab` 的 cmhub 网关配置:Base URL 保存/刷新前规整为网关根;API Key 单独读写 `config/cmhub.json`;别名下拉来自 `fetch_cmhub_models()`,按 `operation_type` 分生文/生图并过滤未计价别名;「测试连接/查余额」调用 cmhub models + balance helper;保存设置固定写 `ai.backend=cmhub`。`refresh_cmhub_models()` / `test_cmhub_connection()` 使用输入框实时值但不得自动保存,成功文案提醒用户保存。`is_dirty()` / `discard_unsaved_changes()` / `_suspend_dirty`(或等价机制)用于 T-531:用户编辑置脏,程序化回填不置脏,放弃时重新加载 `config.json` + `config/cmhub.json` 并回填控件。
|
||||
- T-532 要求 `SettingsTab._on_cmhub_finished()` 从 worker payload 的 `balance` / user/account 字段提取 cmhub 账号身份,成功文案优先显示 `cmhub 账号「<账号名>」连接成功:...`;账号字段缺失时保持 `cmhub 连接成功:...`。显示名必须脱敏处理邮箱,且不得把 API Key、token 或完整敏感响应写入 GUI、run log 或诊断日志。
|
||||
- T-532 要求 `SettingsTab._on_cmhub_finished()` 从 worker payload 的 `balance` / user/account 字段提取 cmhub 账号身份,成功文案优先显示 `cmhub 账号「<账号名>」连接成功:...`;当前 `/balance` 结构兼容 `{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }`,显示名优先 `account.display_name`,再兜底 `account.username` / `user` / 顶层常见字段;账号字段缺失时保持 `cmhub 连接成功:...`。显示名必须脱敏处理邮箱,且不得把 API Key、token 或完整敏感响应写入 GUI、run log 或诊断日志。
|
||||
- `MainWindow` 已负责⑤设置页离开守卫:切 Tab 与 `closeEvent` 发现 `SettingsTab.is_dirty()` 时弹保存/放弃/取消;保存成功后继续,保存失败或取消时回到⑤。由于 `QTabWidget.currentChanged` 是切换后信号,需维护上一个 index,并用 `_reverting_tab_change` 或等价 guard 防止 `setCurrentIndex()` 递归。
|
||||
- 模型详情字段按 3 个组件一组排列:启用、类别、api_type、连接超时等短字段一格;服务商名、模型 ID 视宽度占一格或两格;网址、密钥跨整行或跨 2/3 列。
|
||||
- 模型数据读写复用 `appconfig.list_ai_models(..., reveal_api_key=True)`、`add_ai_model()`、`update_ai_model()`、`delete_ai_model()`;保存时保留现有 `extra_body` 与 `timeout_seconds`。
|
||||
|
||||
@@ -121,7 +121,7 @@ cmhub 返回结构化 `{error:{code}}`。映射层**按 `code` 优先分支**(
|
||||
### 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` 提示。
|
||||
- `GET /api/v1/balance` 当前返回结构:`{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }`。该接口仅用于**手动刷新余额**或批量前可选预检(`check_balance_before_batch=true`);⑤设置页测试连接成功时优先用 `account.display_name` 显示账号名,没有时再用 `account.username` / `user` 兜底。非必需,第一版可只依赖成功响应里的余额 + 失败时的 `insufficient_points` 提示。
|
||||
|
||||
### 4.6 别名发现(`GET /api/v1/models`)
|
||||
|
||||
|
||||
File diff suppressed because one or more lines are too long
+1
-1
@@ -144,7 +144,7 @@
|
||||
|
||||
- 设置页整体布局:内容区居中,左右留白已从 T-506 初始实现缩短到约 40%;实现上用最大内容宽度 + 自适应 margin,而不是写死窗口像素。所有设置组默认响应式 3 列表单:短字段占 1 格,URL/API Key/路径等长字段跨 2 格或 3 格;窄窗口自动降为 2 列/1 列。点击「保存设置」成功后,状态栏显示“设置已保存”,并弹出轻量提示框。T-531 已完成:保存成功会清除未保存标记;保存失败时保留未保存标记并阻止离开。
|
||||
- cmhub 网关配置(T-529 后普通用户唯一 AI 入口):⑤设置页不再显示「AI 后端」label 或 direct/cmhub 下拉,直接展示 cmhub 网关 Base URL、API Key、生文别名、生图别名、连接超时、刷新别名、测试连接/查余额;Base URL 输入框旁提示“只填网关根,如 https://host”,保存/刷新前会规整掉 `/api`、`/api/v1` 或其它路径。刷新别名/测试连接使用当前输入框内容发起请求,但不自动保存 URL/API Key,成功文案需提醒“记得点保存设置持久化”。
|
||||
- T-532 需求:测试连接/查余额成功时,如果 cmhub `/balance` 返回账号名、用户名或邮箱等可识别信息,成功提示应显示 `cmhub 账号「<账号名>」连接成功:...`,让用户确认当前 API Key 属于哪个 cmhub 账号;邮箱需脱敏,没有账号信息时保留 `cmhub 连接成功:...` 兜底文案。
|
||||
- T-532 需求:测试连接/查余额成功时,如果 cmhub `/balance` 返回账号名、用户名或邮箱等可识别信息,成功提示应显示 `cmhub 账号「<账号名>」连接成功:...`,让用户确认当前 API Key 属于哪个 cmhub 账号;当前接口结构兼容 `{ "user": "cmhub_user", "points_balance": 88, "account": { "username": "cmhub_user", "display_name": "主账号" } }`,优先显示 `account.display_name`,没有时再用 `account.username` / `user` 等兜底;邮箱需脱敏,没有账号信息时保留 `cmhub 连接成功:...` 兜底文案。
|
||||
- API Key 存在 `config/cmhub.json`,本地明文保存;保存/变更时提示;UI 使用密码框打码显示,不进入日志/导出。
|
||||
- 生文/生图别名来自 `GET /api/v1/models` 动态下拉,过滤未定价别名并展示单价/需原图提示;网关临时不可达时保留已存别名。
|
||||
- 保存设置固定写 `ai.backend=cmhub`。允许先保存不完整 cmhub 配置,②真正生成时如果缺 Base URL/API Key/别名,会提示去⑤补配置,不静默回退 direct。
|
||||
|
||||
+13
@@ -1213,3 +1213,16 @@
|
||||
- 边界:只改⑤设置页成功提示和 GUI 单测;不改 cmhub HTTP 协议、配置 schema、保存逻辑、AI 生成流程或 Shopee/CDP 流程。
|
||||
- 文档:同步 `docs/06-tasks.md`、`docs/routes.md`、`docs/api.md`、`docs/current-state.md`。
|
||||
- 验证:文档-only 更新,未运行单元测试。
|
||||
## 【2026-07-06】T-532 完成 · cmhub 连接成功提示显示账号名
|
||||
|
||||
- 状态:DONE
|
||||
- 代码:`SettingsTab._on_cmhub_finished()` 成功文案会从 worker payload 的 `balance.user`、`balance.account`、顶层字段中提取 `name/account_name/username/email/id`,优先显示 `cmhub 账号「<账号名>」连接成功:...`;无账号信息时保留 `cmhub 连接成功:...`。
|
||||
- 脱敏:新增 `appconfig.mask_email()`,`sanitize_for_log()` 对 `email` / `*_email` 字段脱敏,worker 返回和 run log summary 不保存完整邮箱;设置页展示邮箱时也使用脱敏格式。
|
||||
- 边界:未改 cmhub HTTP 协议、配置 schema、保存逻辑、AI 生成流程或 Shopee/CDP 流程。
|
||||
- 测试:`python -m py_compile app\appconfig.py app\gui\tabs\settings.py tests\test_appconfig.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p test_appconfig.py` 通过(9 tests);`python -m unittest discover -s tests -p test_gui.py -k cmhub` 通过(9 tests)。
|
||||
|
||||
## 【2026-07-06】T-532 补充 · 兼容 cmhub 当前账号返回结构
|
||||
|
||||
- 文档:`docs/06-tasks.md`、`docs/api.md`、`docs/routes.md`、`docs/cmhub-integration-design.md`、`docs/current-state.md` 已记录当前 `/api/v1/balance` 返回结构 `{user, points_balance, account:{username, display_name}}`。
|
||||
- 代码:`SettingsTab` 账号显示名提取优先读取 `account.display_name`,再兜底 `account.username`、`user`、name/account_name/email/id 等字段;邮箱仍脱敏展示。
|
||||
- 测试:补充 GUI worker 与设置页成功提示单测,确认当前结构显示 `主账号` 而不是 `cmhub_user`;未改 cmhub HTTP 协议、配置 schema、AI 生成或 Shopee/CDP 流程。
|
||||
@@ -284,8 +284,10 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
|
||||
def test_sanitize_for_log_masks_secret_fields(self):
|
||||
payload = {
|
||||
"name": "demo",
|
||||
"email": "owner@example.com",
|
||||
"api_key": "sk-1234567890",
|
||||
"nested": {
|
||||
"support_email": "helpdesk@example.com",
|
||||
"password": "account-secret",
|
||||
"items": [
|
||||
{"provider_token": "token-secret"},
|
||||
@@ -298,6 +300,8 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
|
||||
sanitized = appconfig.sanitize_for_log(payload)
|
||||
|
||||
self.assertEqual("demo", sanitized["name"])
|
||||
self.assertEqual("o***r@example.com", sanitized["email"])
|
||||
self.assertEqual("h***k@example.com", sanitized["nested"]["support_email"])
|
||||
self.assertEqual("sk-1***7890", sanitized["api_key"])
|
||||
self.assertEqual("acco***cret", sanitized["nested"]["password"])
|
||||
self.assertEqual("toke***cret", sanitized["nested"]["items"][0]["provider_token"])
|
||||
|
||||
+76
-1
@@ -758,7 +758,11 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
"pricing_status": "priced",
|
||||
}
|
||||
]
|
||||
balance = {"user": {"id": "u1"}, "points_balance": 88}
|
||||
balance = {
|
||||
"user": "cmhub_user",
|
||||
"points_balance": 88,
|
||||
"account": {"username": "cmhub_user", "display_name": "主账号"},
|
||||
}
|
||||
|
||||
with mock.patch("app.gui.ai.fetch_cmhub_models", return_value=models) as fetch_models, \
|
||||
mock.patch("app.gui.ai.fetch_cmhub_balance", return_value=balance) as fetch_balance:
|
||||
@@ -777,6 +781,9 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
self.assertTrue(result["ok"])
|
||||
self.assertEqual(models, result["models"])
|
||||
self.assertEqual(88, result["points_balance"])
|
||||
self.assertEqual("cmhub_user", result["balance"]["user"])
|
||||
self.assertEqual("主账号", result["balance"]["account"]["display_name"])
|
||||
self.assertEqual("cmhub_user", result["balance"]["account"]["username"])
|
||||
|
||||
def test_cmhub_settings_worker_redacts_key_on_failure(self):
|
||||
worker = CMHubSettingsWorker(
|
||||
@@ -974,6 +981,74 @@ class GuiTests(TempDirMixin, unittest.TestCase):
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
|
||||
def test_settings_tab_cmhub_success_message_shows_account_name(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
cfg["ai"] = appconfig.default_config()["ai"]
|
||||
cfg["ai"]["backend"] = "cmhub"
|
||||
cfg["ai"]["cmhub"] = {
|
||||
"base_url": "https://cmhub.example.com",
|
||||
"title_alias": "",
|
||||
"image_alias": "",
|
||||
"connect_timeout": 10,
|
||||
"check_balance_before_batch": False,
|
||||
}
|
||||
statuses = []
|
||||
tab = SettingsTab(
|
||||
config=cfg,
|
||||
config_path=cfg["config_path"],
|
||||
ai_models_path=cfg["ai_models_path"],
|
||||
status_callback=statuses.append,
|
||||
)
|
||||
self.addCleanup(tab.close)
|
||||
models = [
|
||||
{"alias": "title-priced", "operation_type": "title", "pricing_status": "priced"},
|
||||
{"alias": "image-priced", "operation_type": "image", "pricing_status": "priced"},
|
||||
]
|
||||
|
||||
tab._on_cmhub_finished(
|
||||
{
|
||||
"ok": True,
|
||||
"models": models,
|
||||
"balance": {
|
||||
"user": "cmhub_user",
|
||||
"points_balance": 66,
|
||||
"account": {"username": "cmhub_user", "display_name": "主账号"},
|
||||
},
|
||||
"points_balance": 66,
|
||||
}
|
||||
)
|
||||
self.assertIn("cmhub 账号「主账号」连接成功", tab.cmhub_result_label.text())
|
||||
self.assertNotIn("cmhub 账号「cmhub_user」", tab.cmhub_result_label.text())
|
||||
self.assertIn("余额 66", tab.cmhub_result_label.text())
|
||||
self.assertEqual(tab.cmhub_result_label.text(), statuses[-1])
|
||||
|
||||
tab._on_cmhub_finished(
|
||||
{
|
||||
"ok": True,
|
||||
"models": models,
|
||||
"balance": {"user": {"name": "备用账号"}, "points_balance": 67},
|
||||
"points_balance": 67,
|
||||
}
|
||||
)
|
||||
self.assertIn("cmhub 账号「备用账号」连接成功", tab.cmhub_result_label.text())
|
||||
|
||||
tab._on_cmhub_finished(
|
||||
{
|
||||
"ok": True,
|
||||
"models": models,
|
||||
"balance": {"account": {"email": "owner@example.com"}, "points_balance": 77},
|
||||
"points_balance": 77,
|
||||
}
|
||||
)
|
||||
self.assertIn("cmhub 账号「o***r@example.com」连接成功", tab.cmhub_result_label.text())
|
||||
self.assertNotIn("owner@example.com", tab.cmhub_result_label.text())
|
||||
|
||||
tab._on_cmhub_finished({"ok": True, "models": models, "points_balance": 88})
|
||||
self.assertIn("cmhub 连接成功", tab.cmhub_result_label.text())
|
||||
self.assertNotIn("cmhub 账号", tab.cmhub_result_label.text())
|
||||
|
||||
self.assert_removed(temp_dir)
|
||||
def test_settings_tab_tracks_dirty_state_and_programmatic_cmhub_refresh_is_clean(self):
|
||||
with self.make_temp_dir() as temp_dir:
|
||||
cfg = self.make_config(temp_dir)
|
||||
|
||||
Reference in New Issue
Block a user