From 0dad4151efe58bda0263b44313521df3d2f895a4 Mon Sep 17 00:00:00 2001 From: chengma Date: Mon, 6 Jul 2026 15:01:50 +0800 Subject: [PATCH] feat: complete T-105b account login reuse --- app/accounts.py | 89 +++++++++++++++++++++++++++++++++++++++- app/cdp.py | 14 +++++++ app/gui/tabs/accounts.py | 28 ++++++++++--- docs/04-architecture.md | 4 +- docs/06-tasks.md | 1 + docs/api.md | 8 ++-- docs/current-state.md | 13 +++--- docs/routes.md | 6 +-- docs/troubleshooting.md | 10 +++++ progress.md | 18 ++++++++ tests/test_accounts.py | 67 ++++++++++++++++++++++++++++-- tests/test_gui.py | 48 +++++++++++++++++++--- 12 files changed, 276 insertions(+), 30 deletions(-) diff --git a/app/accounts.py b/app/accounts.py index 1d196ed..63b3ac0 100644 --- a/app/accounts.py +++ b/app/accounts.py @@ -7,7 +7,7 @@ from datetime import datetime from typing import Optional from urllib.parse import urlparse -from . import appconfig, chrome, db, editor +from . import appconfig, cdp, chrome, db, editor from . import config as account_config @@ -42,6 +42,12 @@ def _required_text(value, field_name: str) -> str: return text +def _account_value(account, field, default=None): + if isinstance(account, dict): + return account.get(field, default) + return getattr(account, field, default) + + def normalize_region_host(value=None) -> str: text = _trim(value) or DEFAULT_REGION_HOST if "://" in text: @@ -208,9 +214,88 @@ def resolve_account(account_or_alias, path=None, config=None): return account_or_alias +def _debug_host(account) -> str: + return f"{chrome.CDP_HOST}:{normalize_debug_port(_account_value(account, 'debug_port'))}" + + +def _seller_portal_url(account) -> str: + return f"https://{normalize_region_host(_account_value(account, 'region_host'))}/portal/" + + +def _find_login_tab(account, host): + region_host = normalize_region_host(_account_value(account, 'region_host')).lower() + seller_tabs = [] + login_tabs = [] + for tab in cdp.http_get("/json", host=host): + if tab.get("type") != "page": + continue + url = str(tab.get("url") or "") + lower_url = url.lower() + if region_host in lower_url: + seller_tabs.append(tab) + elif "accounts.shopee." in lower_url and "/seller/login" in lower_url: + login_tabs.append(tab) + return (seller_tabs or login_tabs or [None])[0] + + +def _open_or_activate_login_tab(account): + host = _debug_host(account) + portal_url = _seller_portal_url(account) + tab = _find_login_tab(account, host) + if tab: + target_id = tab.get("id") + if target_id: + cdp.activate_tab(target_id, host=host) + return { + "target_id": target_id, + "url": tab.get("url") or portal_url, + "created_tab": False, + } + + tab = cdp.create_tab_info(portal_url, host=host) + target_id = tab.get("id") + if target_id: + cdp.activate_tab(target_id, host=host) + return { + "target_id": target_id, + "url": tab.get("url") or portal_url, + "created_tab": True, + } + + def launch_for_login(account_or_alias, path=None, config=None): account = resolve_account(account_or_alias, path=path, config=config) - return chrome.launch_chrome(account, config=config) + port = normalize_debug_port(_account_value(account, "debug_port")) + alias = _account_value(account, "alias") + + if chrome.is_running(port): + tab = _open_or_activate_login_tab(account) + return { + "ok": True, + "action": "reused", + "reused": True, + "launched": False, + "alias": alias, + "debug_port": port, + "pid": None, + **tab, + } + + process = chrome.launch_chrome(account, config=config) + timeout = appconfig.cdp_ready_timeout(config) + if not chrome.wait_debug_ready(port, timeout=timeout): + raise AccountError(f"Chrome 已启动,但 CDP 端口 {port} 未在 {timeout} 秒内就绪") + tab = _open_or_activate_login_tab(account) + return { + "ok": True, + "action": "launched", + "reused": False, + "launched": True, + "alias": alias, + "debug_port": port, + "pid": getattr(process, "pid", None), + **tab, + } def create_shortcut(account_or_alias, shortcut_path=None, desktop_dir=None, path=None, config=None) -> str: diff --git a/app/cdp.py b/app/cdp.py index 8160753..ca8e43f 100644 --- a/app/cdp.py +++ b/app/cdp.py @@ -43,6 +43,20 @@ def close_tab(target_id, host=None): return response.ok +def activate_tab(target_id, host=None): + """Bring an existing Chrome target/page to the foreground.""" + + if not target_id: + return False + ver = http_get("/json/version", host=host) + b = CDP(ver["webSocketDebuggerUrl"]) + try: + b.send("Target.activateTarget", {"targetId": target_id}) + finally: + b.close() + return True + + class CDP: """单个 target 的 CDP 客户端:命令同步、事件回调异步。""" diff --git a/app/gui/tabs/accounts.py b/app/gui/tabs/accounts.py index c8b7b76..50bd919 100644 --- a/app/gui/tabs/accounts.py +++ b/app/gui/tabs/accounts.py @@ -312,7 +312,7 @@ class AccountsTab(QWidget): account=account, ) try: - process = accounts.launch_for_login(account, config=self.config) + launch_result = accounts.launch_for_login(account, config=self.config) except Exception as exc: elapsed_ms = _elapsed_ms(started) safe_error = diagnostics.redact_log_text(str(exc) or exc.__class__.__name__) @@ -344,10 +344,17 @@ class AccountsTab(QWidget): self._show_error(safe_error) return elapsed_ms = _elapsed_ms(started) - pid = getattr(process, "pid", None) + action = (launch_result or {}).get("action") or "launched" + pid = (launch_result or {}).get("pid") + target_id = (launch_result or {}).get("target_id") or "" + url = (launch_result or {}).get("url") or "" + event_result = "reused" if action == "reused" else "launched" _safe_add_run_log_event( run_id, - f"step=launch_chrome result=success detail=账号 {account.alias} pid={pid or ''} elapsed_ms={elapsed_ms}", + ( + f"step=launch_chrome result={event_result} detail=账号 {account.alias} " + f"pid={pid or ''} target_id={target_id} url={url} elapsed_ms={elapsed_ms}" + ), db_path=self.db_path, account=account, ) @@ -358,11 +365,22 @@ class AccountsTab(QWidget): done=1, success_count=1, failed_count=0, - summary_json={"ok": True, "alias": account.alias, "debug_port": account.debug_port, "pid": pid}, + summary_json={ + "ok": True, + "alias": account.alias, + "debug_port": account.debug_port, + "action": action, + "pid": pid, + "target_id": target_id, + "url": url, + }, ) self.login_statuses[account.alias] = "已启动" self.refresh_accounts() - self._set_status("Chrome 已启动,请人工登录") + if action == "reused": + self._set_status("该账号 Chrome 已打开,已复用现有窗口") + else: + self._set_status("Chrome 已启动,请人工登录") def check_login(self, checked=False): account = self._selected_account() diff --git a/docs/04-architecture.md b/docs/04-architecture.md index 70f4e37..2e9b5d7 100644 --- a/docs/04-architecture.md +++ b/docs/04-architecture.md @@ -87,6 +87,8 @@ imported → collected → generated → applied 采用**每账号独立 user-data-dir**(非 Chrome profile)。`--remote-debugging-port` 绑定在 user-data-dir/进程上,profile 方案无法每账号独立 CDP、串号风险高。启动主路径用程序 `subprocess` 直启(`--remote-debugging-port` + `--remote-allow-origins=*` + `--user-data-dir`);可选生成 `.lnk` 快捷方式(PowerShell `WScript.Shell`,参数写在「目标」字段)。 +同一账号重复点击④「启动登录」不得重复执行 `subprocess.Popen`。正确流程是先探测该账号 `debug_port` 的 `/json/version`:端口未响应才按上述启动参数新开 Chrome;端口已响应则复用该账号现有 Chrome/CDP,打开或激活卖家中心登录 tab 供人工登录,并记录为复用,不创建第二个账号窗口。 + ## 五、数据模型 ### 5.1 应用配置 `config.json` @@ -435,7 +437,7 @@ images///__new. # AI 生成的新封 ### 6.4 登录检测 - 无 Shopee tab 时打开卖家中心根地址 `https:///`(默认 `https://seller.shopee.tw/`),重定向到登录页或缺会话 Cookie(`SPC_ST`/`SPC_U`)→ 未登录;不自动登录,提示人工登录。 -- ④「启动登录」只负责打开该账号独立 user-data-dir + CDP 端口的 Chrome,供用户人工登录;「检测登录」只验证当前 Chrome/CDP/会话 Cookie 是否可用。①/③ 的预检只检测,不自动启动缺失浏览器。若商品详情页或卖家中心重定向到 `accounts.shopee.tw/seller/login`,必须按登录页处理,返回 `LOGIN_PAGE` 并在 GUI 显示未登录。 +- ④「启动登录」只负责准备该账号独立 user-data-dir + CDP 端口的 Chrome,供用户人工登录;该入口必须幂等:若端口已响应,复用现有账号 Chrome 并打开/激活卖家中心登录 tab,不再新开 Chrome;若端口未响应,才启动 Chrome。「检测登录」只验证当前 Chrome/CDP/会话 Cookie 是否可用。①/③ 的预检只检测,不自动启动缺失浏览器。若商品详情页或卖家中心重定向到 `accounts.shopee.tw/seller/login`,必须按登录页处理,返回 `LOGIN_PAGE` 并在 GUI 显示未登录。 ## 七、CDP 已验证事实(务必遵守) diff --git a/docs/06-tasks.md b/docs/06-tasks.md index b3634ef..2cf5d4f 100644 --- a/docs/06-tasks.md +++ b/docs/06-tasks.md @@ -42,6 +42,7 @@ | T-104 | PySide6 五 Tab 主窗口骨架(`QMainWindow` + `QTabWidget`,5 Tab 空壳) | T-002 | 五个 Tab 按顺序可切换;启动不阻塞;基础状态栏可用 | DONE | | T-104b | PySide6 worker 基类与线程启动工具(`BaseWorker` + `QThread` 包装) | T-104 | signals: progress/log/row_updated/failed/finished/cancelled;取消标记可用;worker 不直接操作 QWidget | DONE | | T-105 | Tab④ 账号增删改(账号名/别名/端口/密码本地明文仅参考)+ 启动登录 + 检测登录 + 顶部 Tab 栏防误点样式 | T-104, T-101, T-103 | 增删改入库、建目录;密码字段 UI 打码;状态列刷新;不自动登录/填密码;5 个 Tab 有足够宽度/间距与当前态高亮,不易误点 | DONE | +| T-105b | ④「启动登录」复用已打开 Chrome,防止同账号重复开窗口 | T-105, T-102, T-523 | Bug:同一账号已打开 Chrome 后再次点击「启动登录」,当前链路仍从 `AccountsTab.launch_login()` → `accounts.launch_for_login()` → `chrome.launch_chrome()` → `subprocess.Popen()`,没有先检查该账号 `debug_port` 是否已有 CDP 响应;`login_statuses[alias]="已启动"` 只是界面显示,不是幂等保护。方案:把④启动入口改为幂等流程,先调用 `chrome.is_running(account.debug_port)`;若端口已响应,不再 `Popen`,通过现有 CDP 能力复用该账号 Chrome,优先激活/打开 `https:///portal/` 或卖家中心登录 tab,并写 `run_type=chrome_launch` 事件 `result=reused`,状态栏提示“该账号 Chrome 已打开,已复用现有窗口”;若端口未响应,才调用 `chrome.launch_chrome()` 并等待 CDP 就绪,事件 `result=launched`。不得自动填写密码、不得绕过登录/验证码、不得影响①/③账号预检“不自动启动 Chrome”的规则。测试覆盖:端口已运行时不调用 `subprocess.Popen`,端口未运行时仍启动;GUI 重复点击同账号不会新增 Chrome 进程,run log/status 文案区分复用与新启动 | DONE | | T-106 | 可选:为账号生成桌面快捷方式 | T-102 | `.lnk` 目标含该账号参数;双击进对应账号 | DONE | ## Phase 2 · 导入采集(①) diff --git a/docs/api.md b/docs/api.md index f2ae7b0..b7c86c3 100644 --- a/docs/api.md +++ b/docs/api.md @@ -201,7 +201,7 @@ create_account(account_name, alias, region_host=None, debug_port=None, update_account(original_alias, account_name, alias, region_host=None, debug_port=None, password=None, note=None, path=None, config=None) -> db.Account delete_account(alias, path=None, config=None) -> None -launch_for_login(account_or_alias, path=None, config=None) -> subprocess.Popen +launch_for_login(account_or_alias, path=None, config=None) -> dict # 幂等启动/复用入口 create_shortcut(account_or_alias, shortcut_path=None, desktop_dir=None, path=None, config=None) -> str detect_login(account_or_alias, timeout=8, path=None, config=None) -> dict @@ -213,8 +213,8 @@ login_status_text(status) -> str - `create_account()` / `update_account()` 负责生成 slug、创建 `chrome_user_data_dir/` 并写 DB;编辑别名会生成新 slug/目录,但不删除旧 user-data-dir。 - `debug_port` 在账号服务层按账号唯一校验;默认端口取 `debug_port_range` 中第一个未占用端口。 - `delete_account()` 只删除 DB 账号记录,不删除本地 user-data-dir,避免误删登录态。 -- `launch_for_login()` 只启动带该账号 user-data-dir 和 CDP 端口的 Chrome;不会读取、填写或提交密码。 -- ③ 更新 shopee 的账号预检不会自动调用 `launch_for_login()`;Chrome 未启动/端口不可达时只返回阻断原因,由 GUI 提示用户去④手动打开账号浏览器并登录。 +- `launch_for_login()` 是幂等入口。先检查 `chrome.is_running(account.debug_port)`;若该账号 CDP 端口已响应,不再调用 `subprocess.Popen`,而是复用已打开的账号 Chrome,优先激活/打开 `https:///portal/` 或卖家中心登录 tab,并返回 `reused=true`;若端口未响应,才启动带该账号 user-data-dir 和 CDP 端口的 Chrome,等待 CDP 就绪并返回 `launched=true`。不会读取、填写或提交密码,不绕过验证码。 +- ③ 更新 shopee 的账号预检不会自动调用 `launch_for_login()`;Chrome 未启动/端口不可达时只返回阻断原因,由 GUI 提示用户去④手动打开账号浏览器并登录。T-105b 的复用逻辑只作用于④用户主动点击「启动登录」这一入口。 - `create_shortcut()` 生成 `.lnk`,目标/参数复用 `chrome.build_launch_args()`,不包含密码。 - `detect_login()` 复用 `editor.login_status()`;检测为已登录时更新 `last_login_at`。 @@ -377,7 +377,7 @@ T-523 后 GUI 已从旧 `app/gui.py` 拆为 `app/gui/` 包:`__init__.py` 负 - 表格列:账号名、别名、地区、端口、登录状态、备注;不展示密码。 - 弹窗字段:账号名、别名、地区、调试端口、密码、备注、slug、数据目录;密码框使用打码显示;首次保存/变更非空密码前弹窗提示“本地明文保存”。 -- 「启动登录」只启动 Chrome,人工登录;「检测登录」用 worker 跑 `accounts.detect_login()` 并刷新状态列;若当前 URL 跳到 `accounts.shopee.tw/seller/login`,状态必须显示未登录;「快捷方式」调用 `accounts.create_shortcut()` 生成默认桌面 `.lnk`。 +- 「启动登录」必须幂等:若该账号 Chrome/CDP 已打开,则复用现有窗口并打开/激活卖家中心登录 tab,不重复启动 Chrome;未打开时才新启动 Chrome,仍由用户人工登录。「检测登录」用 worker 跑 `accounts.detect_login()` 并刷新状态列;若当前 URL 跳到 `accounts.shopee.tw/seller/login`,状态必须显示未登录;「快捷方式」调用 `accounts.create_shortcut()` 生成默认桌面 `.lnk`。 ⑤ 设置当前要点(T-501): diff --git a/docs/current-state.md b/docs/current-state.md index 4334c64..9cbfbe4 100644 --- a/docs/current-state.md +++ b/docs/current-state.md @@ -6,10 +6,10 @@ ## 当前快照 - 日期:2026-07-06 -- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-103b 登录检测补充识别 Shopee accounts 登录页、T-104 PySide6 主窗口骨架、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② AI 生成布局与任务列表、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-526 `app/ai.py` + `appconfig` 接入 cmhub backend、T-527 ⑤设置 cmhub 网关面板、T-528 ②计费错误提示与余额展示、T-529 默认 cmhub 网关并隐藏 AI 后端选择、T-530 cmhub Base URL 规整与 404 明确提示、T-531 ⑤设置未保存状态追踪与离开确认。 +- 阶段:V0 单账号 CDP 流程已验证;V1 已完成 T-000 正式代码包结构、T-001 `app/editor.py` 模块化、T-002 `app/appconfig.py` 应用配置、T-003 SQLite 持久化地基、T-004 本地数据忽略规则、T-005 AI 模型清单后端、T-006 单元测试基座、T-101 账号 user-data-dir 工具、T-102 Chrome 启动器、T-103 登录保活与检测、T-103b 登录检测补充识别 Shopee accounts 登录页、T-104 PySide6 主窗口骨架、T-104b PySide6 worker 基座、T-105 Tab④ 账号管理、T-105b ④启动登录复用已打开 Chrome、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② AI 生成布局与任务列表、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-526 `app/ai.py` + `appconfig` 接入 cmhub backend、T-527 ⑤设置 cmhub 网关面板、T-528 ②计费错误提示与余额展示、T-529 默认 cmhub 网关并隐藏 AI 后端选择、T-530 cmhub Base URL 规整与 404 明确提示、T-531 ⑤设置未保存状态追踪与离开确认。 - 技术栈:Python 3.10+,根目录 `requirements.txt` 锁定运行依赖,GitHub Actions 使用 Windows + Python 3.11 自动跑语法和单元/GUI 测试,PyInstaller onedir 打包为 Windows 免安装 exe;自研 CDP(websocket-client + requests),SQLite(sqlite3)+ `config.json` + openpyxl + AI(默认 cmhub: `config.json` 网关配置 + `config/cmhub.json` Key;direct: `config/ai_models.json` 通用 HTTP 内部兼容),GUI PySide6 5 Tab(已定)。 -- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座,已区分 `CDP.close()` 断开 WebSocket 与 `close_tab()` 关闭浏览器 target;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力,打开商品详情页时会安装 toast 捕获并在详情页加载失败时上浮商品失效/无权限等错误原因,`open_product()` 内部失败会关闭本轮自动新建的失败 tab,复用用户已有 tab 不关闭,并在采集结束后只关闭本轮自动新建的商品编辑页 tab、保留用户已有 tab,更新提交成功且设置开启时可关闭本轮自动新建商品页,确认后跳回商品列表页时关闭前等待 2 秒;`click_update()` 已补 Shopee 站点侧确认框处理,页面主「更新」后若出现 `確定您要更新商品嗎?` modal(`.eds-modal__content` / `.eds-modal__box`),只点击弹窗主按钮「更新」并避开「立即優化」,且记录确认后是否跳回商品列表页;`replace_cover()` 已实现更新封面统一先校验本地旧封面备份,再点第一张删除、可见确认框、等待图片管理器稳定和上传 input 恢复,随后先点击上传块模拟人工入口、短暂等待、重新获取 input、注入文件上传新图并等待 Shopee CDN 后拖到第一位的代码路径;已修正 2026-06-30 稳定等待回归:删除第一张前不要求上传 input 可用,只等图片列表稳定;删除后再等上传入口恢复;`重複/重复/duplicate` 上传 toast 会立即判为新封面重复错误;`app/image_paths.py` 已统一新采集/新生成图片路径为 `images///__old/new.jpg`,历史 DB 路径继续按原路径读取;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数、端口读取、Shopee 更新安全与执行模式默认值,`ai.backend` 默认 `cmhub`,`config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接与 OpenAI-compatible base URL 自动补 endpoint,`config/cmhub.json` cmhub Key 读写/打码 helper、cmhub Base URL 规整与请求 URL 拼接 helper,以及 `mask_secret()`、`sanitize_for_log()`、`redact_secrets()` 敏感信息脱敏工具;`app/ai.py` 已实现 `gen_title()`/`gen_cover()`/`generate_batch()`,支持 direct 通用 HTTP 和 cmhub 网关 backend,direct 按默认文本/图片模型调用,cmhub 按 Base URL + 生文/生图别名调用 title/image/models/balance 接口,生图 `image_url` 安全下载后转本地 JPEG,计费 metadata 通过事件回调传出,HTTP 404 映射为 `not_found` 并给出中文排障提示,且保留重试、错误脱敏、图片 URL/base64 解析、resolution resize、jpg_quality 保存、按 `ai.generate_cover` 选择只生成标题或先并发标题再并发封面、逐条 `set_generated`、失败 `mark_failed`、步骤级事件/错误回调与停止取消未开始项;`app/prompts.py` 已实现标题提示词读写、封面模板 CRUD 与变量替换;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数、T-206 批次软删除标记与默认业务查询过滤、T-404a 本地生成结果/更新状态重置函数、T-509 `update_generated_title()` 本地新标题微调函数,以及 `run_logs/run_log_events` 运行日志函数;`app/diagnostics.py` 已实现 gitignore 本地诊断日志、滚动写入、结构化 payload 和自由文本脱敏;T-505 已把 Excel 导入/回写、③更新shopee、④Chrome 启动/登录检测、⑤AI模型测试连接接入 `run_logs/run_log_events` 与本地脱敏诊断日志;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel、更新结果回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui/` 已由 T-523 拆分为 PySide6 GUI 包,包入口 `__init__.py` 兼容旧导入,`main_window.py` 放 `MainWindow`,`models.py` 放 3 个 TableModel,`widgets.py` 放色板/空状态/批次总览/helper,`workers.py` 放具体 GUI worker,`tabs/` 放 ①~⑤ Tab;整体仍实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、统一语义色板、①②③任务状态列前景色、③「开始更新」warning 描边/文字色和①导入校验数字标红、④登录状态点上色、③更新shopee Tab warning 小圆点和删除类按钮 danger 样式、①②③首次空状态引导卡片、①②③批次阶段进度总览、① 导入采集的 Excel 导入按钮/导入汇总栏/批次筛选与删除批次软删除入口/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/采集前账号就绪预检与④引导/采集完成自动回写/旧数据回写重试按钮与 worker/采集运行日志视图、② AI生成左右布局/标题与封面提示词管理/批次/店铺/商品ID/状态筛选/任务列表/新标题列本地微调/变量预览/生成封面图片成本开关/开始生成/停止/标题与图片双进度条/双击新旧封面预览/用户可读自动滚动AI生成运行日志/cmhub余额显示/点数不足弹窗/计费日志/重置生成结果与 `GenerateWorker`、③ 更新shopee批次/店铺/商品ID/状态筛选栏/任务列表/开始更新主按钮/重置更新状态右键菜单/更新安全开关拦截与「前往设置」跳转/「检查本轮更新」按钮/开始更新确认弹窗/确认后 `ApplyWorker` 按每批最大更新条数分批执行当前筛选全部可更新记录/账号就绪和端口冲突预检/按账号并行可选/逐条 `set_applied`/运行日志/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、密码明文保存提示、⑤ 设置页 T-529 后默认展示 cmhub 网关 Base URL、API Key、动态生文/生图别名下拉、Base URL 网关根提示、刷新别名与测试连接/查余额,不再显示「AI 后端」label/dropdown、direct 模型选择、模型详情或标题/图片模型角色下拉;保存设置固定写 `ai.backend=cmhub`,保存/刷新前会把 Base URL 规整为网关根,允许先保存不完整 cmhub 配置,生成时再提示补齐;direct 模型配置、`AIModelTestWorker` 和 `config/ai_models.json` 仅作内部兼容/手工回滚;通用生成参数、路径/端口、Shopee 更新安全与多账号并行设置继续持久化 `config.json`,cmhub Key 单独写入 `config/cmhub.json`,保存成功后弹轻量提示框;⑤ 设置页已将「Shopee 更新安全 / 执行模式」前置、将「基础设施(路径与端口)」后置,`test_item_id` 与 `dry_run` 不再有用户可操作控件,保存时保留 `test_item_id` 兼容值并固定 `dry_run=false`;③ 普通正式更新不再用 `test_item_id` 阻断非测试商品,确认弹窗不再显示测试商品 ID;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。 -- 测试:`tests/` 已建立;T-522 已新增 GitHub Actions 在 push / pull_request 自动运行语法检查与全量 unittest;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts/chrome 启动与快捷方式/editor 登录检测(含 Shopee accounts 登录页)与商品 tab 生命周期、商品详情页失效/隐藏 toast 捕获、更新成功后关闭本轮新开 tab、更新封面备份缺失阻断/8张与9张先删第一张/删除确认/上传入口初始不可用/重复图片 toast/物流错误不阻断上传/删图后稳定等待/上传状态诊断/拖首位 mock 路径/Shopee 更新确认框、成功跳回商品列表和残留错误 toast 不覆盖成功 mock 路径/excel 导入/旧字段与更新结果回写/ai 标题与封面 HTTP 解析/`generate_batch` 正常、失败与停止/prompts 读写与渲染/gui ① 导入采集、删除批次软删除与采集诊断日志/gui ② AI生成布局与批次/店铺/商品ID/状态筛选、提示词管理、生成 worker、图片失败诊断日志、双击预览、重置生成结果与新标题本地编辑/gui ③ 更新shopee批次/店铺/商品ID/状态筛选列表、确认弹窗、开始更新主按钮、重置更新状态右键菜单、Shopee 更新安全拦截与前往设置、`ApplyWorker` 串行/检查/分批/按账号并行/端口冲突预检、运行日志、结果回写与汇总/gui ④ 账号管理、密码打码与明文保存提示/gui ⑤ AI 模型管理、API Key 打码与明文保存提示、角色/生成参数设置、Shopee 更新安全设置、检查按钮/每批最大更新条数/多账号并行设置、测试商品 ID 限制移除/worker signal 与线程包装、T-505 全流程诊断日志(import/write_back/apply/chrome_launch/login_check/ai_model_test)与本地日志脱敏、gui T-511 语义色板和任务状态列前景色、T-512 高风险按钮与导入校验数字样式、T-513 登录状态/Tab 标识/删除按钮样式、T-514 首次空状态引导卡片、T-515 批次阶段进度总览、T-516 ①筛选对齐②③、T-517 ⑤设置分区与兼容字段清理、T-518 ②左栏提示词区组件密度优化、T-519 ②AI生成长任务进度条与用户可读滚动日志、T-520 ②AI生成封面可选生成开关、T-524 PyInstaller 打包入口/构建脚本/发布目录校验、T-526 cmhub backend mock、T-527 设置页 cmhub backend 切换/别名下拉/worker mock、T-528 ②cmhub余额显示/计费日志/点数不足中止提示、T-529 默认 cmhub/隐藏 AI 后端选择/保存固定 cmhub/direct 兼容、T-530 cmhub Base URL 规整/404 not_found 明确提示、T-531 设置页未保存状态/离开确认/放弃还原;2026-07-01 已完成 5 个真实商品的 T-404 更新验收,后续 CDP/Shopee 改动仍需测试商品手动验证。 +- 生产代码:已建立 `app/` 包 + 根入口 `main.py`;`app/cdp.py` 为已验证 CDP 底座,已区分 `CDP.close()` 断开 WebSocket 与 `close_tab()` 关闭浏览器 target;`app/editor.py` 已封装登录状态检测、标题/封面/采集/更新按钮能力,打开商品详情页时会安装 toast 捕获并在详情页加载失败时上浮商品失效/无权限等错误原因,`open_product()` 内部失败会关闭本轮自动新建的失败 tab,复用用户已有 tab 不关闭,并在采集结束后只关闭本轮自动新建的商品编辑页 tab、保留用户已有 tab,更新提交成功且设置开启时可关闭本轮自动新建商品页,确认后跳回商品列表页时关闭前等待 2 秒;`click_update()` 已补 Shopee 站点侧确认框处理,页面主「更新」后若出现 `確定您要更新商品嗎?` modal(`.eds-modal__content` / `.eds-modal__box`),只点击弹窗主按钮「更新」并避开「立即優化」,且记录确认后是否跳回商品列表页;`replace_cover()` 已实现更新封面统一先校验本地旧封面备份,再点第一张删除、可见确认框、等待图片管理器稳定和上传 input 恢复,随后先点击上传块模拟人工入口、短暂等待、重新获取 input、注入文件上传新图并等待 Shopee CDN 后拖到第一位的代码路径;已修正 2026-06-30 稳定等待回归:删除第一张前不要求上传 input 可用,只等图片列表稳定;删除后再等上传入口恢复;`重複/重复/duplicate` 上传 toast 会立即判为新封面重复错误;`app/image_paths.py` 已统一新采集/新生成图片路径为 `images///__old/new.jpg`,历史 DB 路径继续按原路径读取;`app/appconfig.py` 已实现 `config.json` 默认值/读写/更新、AI 参数、端口读取、Shopee 更新安全与执行模式默认值,`ai.backend` 默认 `cmhub`,`config/ai_models.json` 模型清单 CRUD/过滤/打码/测试连接与 OpenAI-compatible base URL 自动补 endpoint,`config/cmhub.json` cmhub Key 读写/打码 helper、cmhub Base URL 规整与请求 URL 拼接 helper,以及 `mask_secret()`、`sanitize_for_log()`、`redact_secrets()` 敏感信息脱敏工具;`app/ai.py` 已实现 `gen_title()`/`gen_cover()`/`generate_batch()`,支持 direct 通用 HTTP 和 cmhub 网关 backend,direct 按默认文本/图片模型调用,cmhub 按 Base URL + 生文/生图别名调用 title/image/models/balance 接口,生图 `image_url` 安全下载后转本地 JPEG,计费 metadata 通过事件回调传出,HTTP 404 映射为 `not_found` 并给出中文排障提示,且保留重试、错误脱敏、图片 URL/base64 解析、resolution resize、jpg_quality 保存、按 `ai.generate_cover` 选择只生成标题或先并发标题再并发封面、逐条 `set_generated`、失败 `mark_failed`、步骤级事件/错误回调与停止取消未开始项;`app/prompts.py` 已实现标题提示词读写、封面模板 CRUD 与变量替换;`app/db.py` 已实现 SQLite schema、连接 PRAGMA、批次/账号/任务与阶段写库函数、T-206 批次软删除标记与默认业务查询过滤、T-404a 本地生成结果/更新状态重置函数、T-509 `update_generated_title()` 本地新标题微调函数,以及 `run_logs/run_log_events` 运行日志函数;`app/diagnostics.py` 已实现 gitignore 本地诊断日志、滚动写入、结构化 payload 和自由文本脱敏;T-505 已把 Excel 导入/回写、③更新shopee、④Chrome 启动/登录检测、⑤AI模型测试连接接入 `run_logs/run_log_events` 与本地脱敏诊断日志;`app/excel.py` 已实现多 Excel 输入列解析、整文件列校验、脏行统计跳过、导入批次与任务入库、别名匹配统计、旧标题/旧封面路径回写原 Excel、更新结果回写原 Excel 与另存副本;`app/config.py` 已实现账号 slug 与 user-data-dir 创建;`app/accounts.py` 已实现账号 CRUD 服务、端口默认分配、启动登录幂等复用已打开 Chrome、检测登录、生成快捷方式;`app/chrome.py` 已实现 Chrome 参数拼装、启动、CDP 端口探测、PowerShell `.lnk` 快捷方式生成;`app/gui/` 已由 T-523 拆分为 PySide6 GUI 包,包入口 `__init__.py` 兼容旧导入,`main_window.py` 放 `MainWindow`,`models.py` 放 3 个 TableModel,`widgets.py` 放色板/空状态/批次总览/helper,`workers.py` 放具体 GUI worker,`tabs/` 放 ①~⑤ Tab;整体仍实现 PySide6 `MainWindow`、五 Tab、顶部 Tab 栏防误点样式、统一语义色板、①②③任务状态列前景色、③「开始更新」warning 描边/文字色和①导入校验数字标红、④登录状态点上色、③更新shopee Tab warning 小圆点和删除类按钮 danger 样式、①②③首次空状态引导卡片、①②③批次阶段进度总览、① 导入采集的 Excel 导入按钮/导入汇总栏/批次筛选与删除批次软删除入口/QTableView 任务列表/未匹配筛选与略过标记/采集旧标题旧封面 worker/采集前账号就绪预检与④引导/采集完成自动回写/旧数据回写重试按钮与 worker/采集运行日志视图、② AI生成左右布局/标题与封面提示词管理/批次/店铺/商品ID/状态筛选/任务列表/新标题列本地微调/变量预览/生成封面图片成本开关/开始生成/停止/标题与图片双进度条/双击新旧封面预览/用户可读自动滚动AI生成运行日志/cmhub余额显示/点数不足弹窗/计费日志/重置生成结果与 `GenerateWorker`、③ 更新shopee批次/店铺/商品ID/状态筛选栏/任务列表/开始更新主按钮/重置更新状态右键菜单/更新安全开关拦截与「前往设置」跳转/「检查本轮更新」按钮/开始更新确认弹窗/确认后 `ApplyWorker` 按每批最大更新条数分批执行当前筛选全部可更新记录/账号就绪和端口冲突预检/按账号并行可选/逐条 `set_applied`/运行日志/自动回写结果到 Excel/结束汇总弹窗/手动回写重试按钮、④ 账号管理表格/弹窗/按钮/快捷方式与状态栏、密码明文保存提示、⑤ 设置页 T-529 后默认展示 cmhub 网关 Base URL、API Key、动态生文/生图别名下拉、Base URL 网关根提示、刷新别名与测试连接/查余额,不再显示「AI 后端」label/dropdown、direct 模型选择、模型详情或标题/图片模型角色下拉;保存设置固定写 `ai.backend=cmhub`,保存/刷新前会把 Base URL 规整为网关根,允许先保存不完整 cmhub 配置,生成时再提示补齐;direct 模型配置、`AIModelTestWorker` 和 `config/ai_models.json` 仅作内部兼容/手工回滚;通用生成参数、路径/端口、Shopee 更新安全与多账号并行设置继续持久化 `config.json`,cmhub Key 单独写入 `config/cmhub.json`,保存成功后弹轻量提示框;⑤ 设置页已将「Shopee 更新安全 / 执行模式」前置、将「基础设施(路径与端口)」后置,`test_item_id` 与 `dry_run` 不再有用户可操作控件,保存时保留 `test_item_id` 兼容值并固定 `dry_run=false`;③ 普通正式更新不再用 `test_item_id` 阻断非测试商品,确认弹窗不再显示测试商品 ID;`app/workers.py` 已实现 `BaseWorker`、通用 signals、取消标记和 `QThread` 启动包装。 +- 测试:`tests/` 已建立;T-522 已新增 GitHub Actions 在 push / pull_request 自动运行语法检查与全量 unittest;T-006 后纯逻辑改动必须运行 `python -m unittest discover -s tests`,当前覆盖 appconfig/db/config/accounts(含 T-105b 启动登录复用已运行 Chrome)/chrome 启动与快捷方式/editor 登录检测(含 Shopee accounts 登录页)与商品 tab 生命周期、商品详情页失效/隐藏 toast 捕获、更新成功后关闭本轮新开 tab、更新封面备份缺失阻断/8张与9张先删第一张/删除确认/上传入口初始不可用/重复图片 toast/物流错误不阻断上传/删图后稳定等待/上传状态诊断/拖首位 mock 路径/Shopee 更新确认框、成功跳回商品列表和残留错误 toast 不覆盖成功 mock 路径/excel 导入/旧字段与更新结果回写/ai 标题与封面 HTTP 解析/`generate_batch` 正常、失败与停止/prompts 读写与渲染/gui ① 导入采集、删除批次软删除与采集诊断日志/gui ② AI生成布局与批次/店铺/商品ID/状态筛选、提示词管理、生成 worker、图片失败诊断日志、双击预览、重置生成结果与新标题本地编辑/gui ③ 更新shopee批次/店铺/商品ID/状态筛选列表、确认弹窗、开始更新主按钮、重置更新状态右键菜单、Shopee 更新安全拦截与前往设置、`ApplyWorker` 串行/检查/分批/按账号并行/端口冲突预检、运行日志、结果回写与汇总/gui ④ 账号管理、密码打码与明文保存提示/gui ⑤ AI 模型管理、API Key 打码与明文保存提示、角色/生成参数设置、Shopee 更新安全设置、检查按钮/每批最大更新条数/多账号并行设置、测试商品 ID 限制移除/worker signal 与线程包装、T-505 全流程诊断日志(import/write_back/apply/chrome_launch/login_check/ai_model_test)与本地日志脱敏、gui T-511 语义色板和任务状态列前景色、T-512 高风险按钮与导入校验数字样式、T-513 登录状态/Tab 标识/删除按钮样式、T-514 首次空状态引导卡片、T-515 批次阶段进度总览、T-516 ①筛选对齐②③、T-517 ⑤设置分区与兼容字段清理、T-518 ②左栏提示词区组件密度优化、T-519 ②AI生成长任务进度条与用户可读滚动日志、T-520 ②AI生成封面可选生成开关、T-524 PyInstaller 打包入口/构建脚本/发布目录校验、T-526 cmhub backend mock、T-527 设置页 cmhub backend 切换/别名下拉/worker mock、T-528 ②cmhub余额显示/计费日志/点数不足中止提示、T-529 默认 cmhub/隐藏 AI 后端选择/保存固定 cmhub/direct 兼容、T-530 cmhub Base URL 规整/404 not_found 明确提示、T-531 设置页未保存状态/离开确认/放弃还原;2026-07-01 已完成 5 个真实商品的 T-404 更新验收,后续 CDP/Shopee 改动仍需测试商品手动验证。 - 数据:`config.json`、`config/ai_models.json`、`config/cmhub.json`、`cmshopee.db`、`cmshopee.db-*`、`chrome_user_data_dir/`、`images/`、`logs/`、`prompts/`、`title_prompt.txt`、`build/`、`dist/` 已由 `.gitignore` 排除;密码与 API Key 本地明文保存但保存/变更时提示,UI 打码,日志/导出必须脱敏;运营填写后的 Excel 业务文件默认忽略,标准空模板 `shopee待处理任务模板.xlsx` 可提交;`app/appconfig.py` 首次读取缺失的 `config.json` 时会在本地写默认配置,`app/db.py` 调用 `init_db()` 时会在本地创建 SQLite DB。 ## 既定设计要点(文档已定) @@ -22,7 +22,7 @@ - 账号↔任务绑定:以 Excel“别名”列为权威;未匹配略过,结束弹窗汇总。 - 执行:默认多账号串行、单条失败继续;③ 提供「检查本轮更新」按钮,检查只写运行日志和汇总,不打开 Shopee、不提交线上、不改任务状态;⑤ 可开启多账号并行(不同账号并行、同账号内串行)并设置每批最大更新条数。③ 点击「开始更新」后先按真实更新安全设置弹窗确认当前筛选范围、任务数量、每批大小和预计批次;确认后做本轮账号 Chrome/CDP/登录态预检,未启动或未登录则弹窗列出账号并中止、不自动打开 Chrome;账号就绪后按批逐条点「更新」提交线上,点击停止后不再开始下一条或下一批。 - AI:普通产品默认 `backend=cmhub`,由 `config.json` 的 Base URL/别名和 `config/cmhub.json` Key 配置,Base URL 会规整为网关根;`app/ai.py` 仍支持 `backend=direct` 作为内部兼容/手工回滚路径,direct 由 `config/ai_models.json` 配置服务商/模型/Key。生成返回值保持不变,计费 metadata 通过事件传出。生成内容直接用于更新,本地留档+回写 Excel 供追溯。 -- 登录:人工登录 + 程序检测,不自动登录;④「启动登录」只是打开对应账号浏览器供人工登录,④「检测登录」和①/③预检只验证登录态;无 Shopee tab 时检测入口为 `https:///`(默认 `https://seller.shopee.tw/`);页面跳到 `accounts.shopee.tw/seller/login` 时已明确判为未登录;首次未配账号、对应账号 Chrome 未启动或未登录时,① ③ 应禁用或执行前预检提示,并引导去④,③ 不静默启动缺失账号 Chrome。 +- 登录:人工登录 + 程序检测,不自动登录;④「启动登录」会打开或复用对应账号浏览器供人工登录,④「检测登录」和①/③预检只验证登录态;无 Shopee tab 时检测入口为 `https:///`(默认 `https://seller.shopee.tw/`);页面跳到 `accounts.shopee.tw/seller/login` 时已明确判为未登录;首次未配账号、对应账号 Chrome 未启动或未登录时,① ③ 应禁用或执行前预检提示,并引导去④,③ 不静默启动缺失账号 Chrome。 ## 当前目录要点 @@ -69,12 +69,13 @@ 任务状态以 [`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-526(`app/ai.py` + `appconfig` 接入 cmhub backend)、T-527(⑤设置 cmhub 网关面板)、T-528(②计费错误提示 + 余额展示)、T-529(默认 cmhub 网关并隐藏 AI 后端选择)、T-530(cmhub Base URL 规整 + 404 明确提示)、T-531(⑤设置未保存状态追踪 + 离开确认)。 +- 已完成: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-105b(④启动登录复用已打开 Chrome)、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-526(`app/ai.py` + `appconfig` 接入 cmhub backend)、T-527(⑤设置 cmhub 网关面板)、T-528(②计费错误提示 + 余额展示)、T-529(默认 cmhub 网关并隐藏 AI 后端选择)、T-530(cmhub Base URL 规整 + 404 明确提示)、T-531(⑤设置未保存状态追踪 + 离开确认)。 - 下一个可领取任务:T-525(引入 ruff lint + format)。 ## 当前已知限制 - T-505 已完成:①采集、②AI生成、Excel 导入/回写、③更新shopee、④Chrome 启动/登录检测、⑤AI模型测试连接均已接入业务可读 `run_logs/run_log_events`;异常路径写本地 `logs/cmshopee.log` 脱敏 traceback,业务日志不记录 Cookie、密码、API Key、token。 +- T-105b 已完成:④账号管理「启动登录」改为幂等入口。同一账号再次点击时会先检查 `debug_port` 是否已有 CDP 响应;已运行则复用现有账号 Chrome 并打开/激活卖家中心登录 tab,状态栏提示“该账号 Chrome 已打开,已复用现有窗口”,不会再次 `subprocess.Popen()`;未运行才新启动 Chrome 并等待 CDP 就绪。不自动登录、不填密码、不改变①/③预检不自动启动 Chrome 的规则。 - ① 采集和③真实更新都依赖对应账号 Chrome 已用专属 user-data-dir 和 CDP 端口启动并登录;T-205/T-402 已在执行前拦截未配置账号、Chrome 未启动、CDP 端口不可访问、未登录,并引导去④账号管理。③ 点击「开始更新」后若本轮需要账号未就绪,必须中止本轮更新,不自动打开账号 Chrome、不提交任何商品。 - T-501/T-501b/T-501c 已完成 `config/ai_models.json` 模型清单 UI,以及 `config.json` 里的标题/图片默认模型角色、并发、重试、分辨率、jpg 质量、路径/端口设置和 Shopee 更新安全开关。 @@ -101,7 +102,7 @@ - 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-524 已完成:新增 PyInstaller onedir 打包配置、构建脚本和发布目录校验;打包版启动时工作目录切到 `cmshopee.exe` 所在目录,首次运行在 exe 同级生成/使用本地配置、DB、图片、日志、登录态和提示词;发布包不内置这些本地数据,用户后续更新采用关闭程序后覆盖程序文件、保留本地数据的方式。 -- Phase 7 cmhub 网关对接已完成并已做 T-529/T-530/T-531 收口:T-526 已接 `app/ai.py`/`appconfig` backend,T-527 已完成⑤设置页 cmhub 网关配置,T-528 已完成②cmhub余额展示、计费日志和点数不足中止提示,T-529 已把普通默认改为 cmhub 并隐藏后端选择,T-530 已完成 Base URL 规整和 404 中文提示,T-531 已完成设置页未保存状态与离开确认;下一步回到 T-525 ruff。 +- Phase 7 cmhub 网关对接已完成并已做 T-529/T-530/T-531 收口:T-526 已接 `app/ai.py`/`appconfig` backend,T-527 已完成⑤设置页 cmhub 网关配置,T-528 已完成②cmhub余额展示、计费日志和点数不足中止提示,T-529 已把普通默认改为 cmhub 并隐藏后端选择,T-530 已完成 Base URL 规整和 404 中文提示,T-531 已完成设置页未保存状态与离开确认;T-105b 已完成④启动登录幂等复用;下一步回到 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。 - 本地 `config/ai_models.json` 若由旧版本或手工维护,可能缺少 `category`;启动报 “AI 模型 category 必须是 text 或 image” 时,按 [`troubleshooting.md`](troubleshooting.md) 只补 `category` / `enabled` 等非密钥字段,保留 API Key,且不要提交该文件。 diff --git a/docs/routes.md b/docs/routes.md index 46e3284..68679c6 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -13,7 +13,7 @@ | ① 导入采集 | 导入多个 Excel;任务列表;**采集**商品当前的旧标题/旧封面(只读),封面图下载本地;回写 Excel 旧字段 | 只读,低 | | ② AI生成 | 左侧标题/封面**提示词**;右侧按批次/店铺/商品ID/状态筛选任务列表;AI 生成新标题,并按本轮开关可选生成新封面;已生成任务可本地微调新标题;双击看新旧封面 | 不触线上,中 | | ③ 更新shopee | 对**已生成**任务点击「开始更新」后弹窗确认;确认后打开编辑页换标题+封面并逐条点「更新」提交;结果回写 Excel | **写线上,高** | -| ④ 账号管理 | Shopee 账号(账号名/别名/数据目录/端口/密码本地明文仅参考/登录状态);启动登录、检测登录、生成快捷方式;检测登录遇到 `accounts.shopee.tw/seller/login` 必须显示未登录 | 中 | +| ④ 账号管理 | Shopee 账号(账号名/别名/数据目录/端口/密码本地明文仅参考/登录状态);启动登录、检测登录、生成快捷方式;启动登录必须复用已打开的同账号 Chrome,避免重复开窗口;检测登录遇到 `accounts.shopee.tw/seller/login` 必须显示未登录 | 中 | | ⑤ 设置 | cmhub 网关/API Key、生文/生图别名、生成参数、本地图片目录、Chrome 路径、默认端口、Shopee 更新安全开关等 | — | 任务的**阶段状态**贯穿各 Tab:`imported → collected → generated → applied`(或 `failed/skipped`)。② 不设逐条人工确认阶段;③ 无常驻提交开关,但点击「开始更新」后必须先通过 ⑤ 的 Shopee 更新安全开关,再弹窗确认当前筛选范围和任务数量。各 Tab 聚焦各自阶段的列与按钮,但操作同一批任务(同一 batch)。 @@ -32,7 +32,7 @@ - ① 导入采集 与 ③ 更新shopee 都依赖**账号已配置且已登录**(在 ④ 账号管理)。 - 当无账号 / 对应账号 Chrome 未启动 / 账号未登录时:相关执行按钮**禁用或在执行前汇总拦截**,并提示「请先到『账号管理』配置账号并登录」。 - ③ 点击「开始更新」后必须检查当前筛选结果涉及的账号;只要有账号 Chrome 未启动、CDP 端口不可访问或 Shopee 未登录,就弹窗列出账号并中止本轮更新,不创建真实更新 worker,不提交任何商品。 -- 可以提供「打开账号管理」或「启动登录」入口辅助用户处理当前账号;不要无提示批量启动所有账号 Chrome,避免开错账号或启动过多浏览器进程。 +- 可以提供「打开账号管理」或「启动登录」入口辅助用户处理当前账号;不要无提示批量启动所有账号 Chrome,避免开错账号或启动过多浏览器进程。用户主动点击④「启动登录」时也必须先检查该账号 CDP 端口,已打开则复用现有 Chrome 并打开/激活卖家中心 tab,不重复 `Popen` 新窗口。 - 老用户账号已就绪则无感。 ## ① 导入采集 @@ -162,7 +162,7 @@ ## 流程导航 ```text -④ 账号管理:配账号 + 启动登录(首次必做) +④ 账号管理:配账号 + 启动登录(首次必做;重复点击应复用已打开 Chrome) │ ① 导入采集:导入 Excel → 采集旧标题/旧封面 → 自动回写旧字段(失败可手动重试) │ diff --git a/docs/troubleshooting.md b/docs/troubleshooting.md index 1c38a43..7ceff10 100644 --- a/docs/troubleshooting.md +++ b/docs/troubleshooting.md @@ -217,3 +217,13 @@ T-530 已实现:保存和请求前都会把 Base URL 规整为网关根,去 - 本地 `logs/cmshopee.log` 保存脱敏后的 traceback、任务 id、alias、item_id、phase 和 step,用于判断卡在模型配置、封面请求、图片解析、保存文件还是写库。 排查顺序:先看 ② 页面运行日志里的 `phase` / `step` / `detail`;如果只看到简短错误,再查看本地 `logs/cmshopee.log`。不要把 `config/ai_models.json` 或 API Key 发到聊天、文档或提交里。 + +## ④启动登录重复打开 Chrome + +现象:在④账号管理中选中同一个账号,第一次点击「启动登录」会打开该账号 Chrome;不关闭该 Chrome 的情况下再次点击「启动登录」,旧版本会再打开一个 Chrome 窗口,而不是复用已打开的账号窗口创建/激活 tab。 + +旧原因:原代码路径是 `AccountsTab.launch_login()` → `accounts.launch_for_login()` → `chrome.launch_chrome()` → `subprocess.Popen()`,每次点击都会直接启动进程;已有的 `chrome.is_running(debug_port)` 没有接入启动入口,`login_statuses[alias]="已启动"` 也只是界面状态,不会阻止下一次点击。 + +已修复:T-105b 已把④「启动登录」改成幂等流程。先检查该账号 `debug_port` 是否已有 CDP 响应;已响应时不再 `Popen`,而是复用现有 Chrome/CDP 并打开或激活 `https:///portal/` 登录/卖家中心 tab;未响应时才新启动 Chrome。该修复不自动登录、不填密码、不绕过验证码,也不改变①/③预检“不自动启动 Chrome”的规则。 + +验证方式:同一账号第一次点击「启动登录」应显示“Chrome 已启动,请人工登录”;保持该账号 Chrome 不关闭,再点一次应显示“该账号 Chrome 已打开,已复用现有窗口”,且不会新增 Chrome 进程。 diff --git a/progress.md b/progress.md index 7b972e1..d737948 100644 --- a/progress.md +++ b/progress.md @@ -1176,9 +1176,27 @@ - 文档:同步 `AGENTS.md`、`docs/05-coding-rules.md`、`docs/current-state.md`。 - 代码:同步修正 T-531 设置未保存离开确认框,改用自定义中文按钮“保存 / 放弃 / 取消”,避免系统默认按钮显示英文。 - 验证:`python -m py_compile app\gui\main_window.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p test_gui.py` 通过(86 tests);`python -m compileall app main.py` 通过;`python -m unittest discover -s tests` 通过(195 tests);`git diff --check` 通过,仅有本机 LF/CRLF 提示。 + ## 【2026-07-06】运行修正 · ②AI生成失败状态分阶段提示 - 现象:在②AI生成选择一个批次,列表都是“失败”状态时点击「开始生成」,状态栏提示“当前筛选结果没有可生成任务”。 - 排查:本地最新未删除批次 `20260702_171000_75bd2557` 的 5 条任务均为 `stage=imported/status=failed/collect_attempts=3/generate_attempts=0/apply_attempts=0`,属于①采集失败,不是 AI 生成失败;②没有旧标题/旧封面,不能直接生成。 - 修正:新增 `ai.is_generatable_task()` 统一 GUI/Worker/底层批处理口径;②状态列把失败细分显示为“采集失败 / 生成失败 / 更新失败”;「开始生成」无可重试任务时提示先到①完成旧数据采集。AI 生成失败可重试,③更新失败不会被②误重试。文档补充 `docs/troubleshooting.md` 的同名排查条目。 - 测试:新增 `test_generate_batch_retries_failed_generation_record_after_existing_result`、`test_generate_batch_does_not_retry_apply_failed_records`、`test_generate_tab_explains_collect_failed_records_are_not_generatable`。 + + +## 【2026-07-06】文档记录 · T-105b 启动登录复用已打开 Chrome + +- 现象:④账号管理同一账号重复点击「启动登录」会再次打开 Chrome 窗口,而不是复用已打开账号 Chrome 创建/激活 tab。 +- 根因:当前链路 `AccountsTab.launch_login()` → `accounts.launch_for_login()` → `chrome.launch_chrome()` → `subprocess.Popen()` 每次直接启动进程,未使用已有 `chrome.is_running(debug_port)` 做幂等判断;界面上的“已启动”只是显示状态,不是保护逻辑。 +- 方案:新增 T-105b,要求④启动登录先检查账号 CDP 端口;已运行时不再 Popen,复用现有 Chrome/CDP 并打开/激活卖家中心登录 tab,写 `chrome_launch result=reused`;未运行时才启动并写 `result=launched`。不自动登录、不填密码、不改变①/③预检不自动启动 Chrome 的规则。 +- 文档:同步 `docs/06-tasks.md`、`docs/api.md`、`docs/routes.md`、`docs/04-architecture.md`、`docs/troubleshooting.md`、`docs/current-state.md`。 +- 验证:文档-only 更新,未运行单元测试。 +## 【2026-07-06】T-105b 完成 · 启动登录复用已打开 Chrome + +- 状态:DONE +- 代码:`app/accounts.py` 的 `launch_for_login()` 改为幂等入口,先用 `chrome.is_running(debug_port)` 检查账号 Chrome/CDP 是否已运行;已运行时复用现有 Chrome,通过 CDP 打开或激活卖家中心登录 tab,不再调用 `subprocess.Popen()`;未运行时才启动 Chrome、等待 CDP 就绪并打开登录 tab。`app/cdp.py` 新增 `activate_tab()` 复用浏览器 target 激活能力。 +- GUI:④账号管理启动登录运行日志区分 `result=launched` 与 `result=reused`;复用时状态栏提示“该账号 Chrome 已打开,已复用现有窗口”。 +- 边界:不自动登录、不填写密码、不绕过验证码;①/③账号预检仍只检测,不自动启动 Chrome。 +- 文档:`docs/06-tasks.md` 将 T-105b 标为 DONE;同步 `docs/api.md`、`docs/troubleshooting.md`、`docs/current-state.md`。 +- 验证:`python -m py_compile app\cdp.py app\accounts.py app\gui\tabs\accounts.py tests\test_accounts.py tests\test_gui.py` 通过;`python -m unittest discover -s tests -p test_accounts.py` 通过(9 tests);`python -m unittest discover -s tests -p test_chrome.py` 通过(6 tests);`python -m unittest discover -s tests -p test_gui.py` 通过(88 tests)。 diff --git a/tests/test_accounts.py b/tests/test_accounts.py index da0a21b..a1e1368 100644 --- a/tests/test_accounts.py +++ b/tests/test_accounts.py @@ -76,17 +76,76 @@ class AccountsTests(TempDirMixin, unittest.TestCase): self.assert_removed(temp_dir) - def test_launch_for_login_only_launches_chrome(self): + def test_launch_for_login_starts_chrome_when_port_not_running(self): with self.make_temp_dir() as temp_dir: cfg = self.make_config(temp_dir) account = accounts.create_account("主店", "alias", debug_port=9222, config=cfg) - process = object() - with mock.patch("app.accounts.chrome.launch_chrome", return_value=process) as launch: + class FakeProcess: + pid = 1234 + + with mock.patch("app.accounts.chrome.is_running", return_value=False) as is_running, \ + mock.patch("app.accounts.chrome.launch_chrome", return_value=FakeProcess()) as launch, \ + mock.patch("app.accounts.chrome.wait_debug_ready", return_value=True) as wait_ready, \ + mock.patch("app.accounts.cdp.http_get", return_value=[]), \ + mock.patch( + "app.accounts.cdp.create_tab_info", + return_value={ + "id": "target-new", + "url": "https://seller.shopee.tw/portal/", + "webSocketDebuggerUrl": "ws-new", + }, + ) as create_tab_info, \ + mock.patch("app.accounts.cdp.activate_tab") as activate_tab: result = accounts.launch_for_login("alias", config=cfg) - self.assertIs(process, result) + self.assertEqual("launched", result["action"]) + self.assertTrue(result["launched"]) + self.assertFalse(result["reused"]) + self.assertEqual(1234, result["pid"]) + self.assertEqual("target-new", result["target_id"]) + is_running.assert_called_once_with(9222) launch.assert_called_once_with(account, config=cfg) + wait_ready.assert_called_once_with(9222, timeout=60) + create_tab_info.assert_called_once_with( + "https://seller.shopee.tw/portal/", + host="127.0.0.1:9222", + ) + activate_tab.assert_called_once_with("target-new", host="127.0.0.1:9222") + + self.assert_removed(temp_dir) + + def test_launch_for_login_reuses_running_chrome_without_starting_process(self): + with self.make_temp_dir() as temp_dir: + cfg = self.make_config(temp_dir) + accounts.create_account("主店", "alias", debug_port=9222, config=cfg) + existing_tab = { + "type": "page", + "id": "target-existing", + "url": "https://seller.shopee.tw/portal/", + "webSocketDebuggerUrl": "ws-existing", + } + + with mock.patch("app.accounts.chrome.is_running", return_value=True) as is_running, \ + mock.patch("app.accounts.chrome.launch_chrome") as launch, \ + mock.patch("app.accounts.chrome.wait_debug_ready") as wait_ready, \ + mock.patch("app.accounts.cdp.http_get", return_value=[existing_tab]) as http_get, \ + mock.patch("app.accounts.cdp.create_tab_info") as create_tab_info, \ + mock.patch("app.accounts.cdp.activate_tab") as activate_tab: + result = accounts.launch_for_login("alias", config=cfg) + + self.assertEqual("reused", result["action"]) + self.assertTrue(result["reused"]) + self.assertFalse(result["launched"]) + self.assertIsNone(result["pid"]) + self.assertFalse(result["created_tab"]) + self.assertEqual("target-existing", result["target_id"]) + is_running.assert_called_once_with(9222) + http_get.assert_called_once_with("/json", host="127.0.0.1:9222") + activate_tab.assert_called_once_with("target-existing", host="127.0.0.1:9222") + launch.assert_not_called() + wait_ready.assert_not_called() + create_tab_info.assert_not_called() self.assert_removed(temp_dir) diff --git a/tests/test_gui.py b/tests/test_gui.py index 7d3d3bb..311f86a 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -4284,12 +4284,14 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.addCleanup(tab.close) tab.table.selectRow(0) - class FakeProcess: - pid = 1234 - with mock.patch( "app.gui.accounts.launch_for_login", - return_value=FakeProcess(), + return_value={ + "action": "launched", + "pid": 1234, + "target_id": "target-new", + "url": "https://seller.shopee.tw/portal/", + }, ) as launch_for_login: tab.launch_login() @@ -4302,8 +4304,44 @@ class GuiTests(TempDirMixin, unittest.TestCase): events = db.list_run_log_events(run_log.id, path=cfg["db_path"]) messages = "\n".join(event.message for event in events) self.assertIn("step=launch_chrome result=start", messages) - self.assertIn("step=launch_chrome result=success", messages) + self.assertIn("step=launch_chrome result=launched", messages) self.assertIn("pid=1234", messages) + self.assertIn("target_id=target-new", messages) + + self.assert_removed(temp_dir) + + def test_accounts_tab_launch_login_reuse_writes_chrome_launch_run_log(self): + with self.make_temp_dir() as temp_dir: + cfg = self.make_config(temp_dir) + account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg) + statuses = [] + tab = AccountsTab(config=cfg, status_callback=statuses.append) + self.addCleanup(tab.close) + tab.table.selectRow(0) + + with mock.patch( + "app.gui.accounts.launch_for_login", + return_value={ + "action": "reused", + "pid": None, + "target_id": "target-existing", + "url": "https://seller.shopee.tw/portal/", + }, + ) as launch_for_login: + tab.launch_login() + + launch_for_login.assert_called_once_with(account, config=cfg) + self.assertEqual("已启动", tab.login_statuses["alias-a"]) + self.assertIn("已复用现有窗口", statuses[-1]) + run_log = db.list_run_logs(limit=1, run_type="chrome_launch", path=cfg["db_path"])[0] + self.assertEqual("done", run_log.status) + self.assertEqual(1, run_log.success_count) + events = db.list_run_log_events(run_log.id, path=cfg["db_path"]) + messages = "\n".join(event.message for event in events) + self.assertIn("step=launch_chrome result=start", messages) + self.assertIn("step=launch_chrome result=reused", messages) + self.assertIn("target_id=target-existing", messages) + self.assertIn("url=https://seller.shopee.tw/portal/", messages) self.assert_removed(temp_dir) if __name__ == "__main__":