Files
cmshoppe/docs/tasks/T-633.md
T

143 lines
13 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-633
title: ④新账号首次启动登录只打开一个 Chrome 窗口
phase: 1
deps: [T-105b]
status: DONE
created: 2026-07-14
---
## 问题 / 背景
用户在④「账号管理」新增账号后,选中新账号并点击「启动登录」,观察到同时打开两个新的 Chrome 窗口或页面。新增账号本身只写账号记录并创建独立 `user_data_dir`,不应启动 Chrome;一次「启动登录」也只应准备一个账号 Chrome,并在其中展示一个蝦皮卖家中心登录/管理页面。
当前代码核查结果:
1. `AccountsTab` 只存在一处 `launch_button.clicked.connect(self.launch_login)`,`launch_login()` 每轮只调用一次 `accounts.launch_for_login()`;没有发现新增账号后重复绑定按钮信号的静态证据。
2. 冷启动路径为:
- `chrome.is_running(debug_port) == false`;
- `chrome.launch_chrome()` 调用一次 `subprocess.Popen()`;
- `chrome.wait_debug_ready()` 等待 CDP;
- `_open_or_activate_login_tab()` 再查找蝦皮页面,未找到时调用 `cdp.create_tab_info(portal_url)`。
3. `chrome.build_launch_args()` 当前只传 Chrome 路径、调试端口、`remote-allow-origins` 和账号 `user-data-dir`,没有携带 `https://<region_host>/portal/`。因此新 Chrome 会先按自身启动策略创建空白/新标签页;CDP 就绪后代码又执行一次 `Target.createTarget` 打开卖家中心。部分 Chrome 版本或窗口状态下,这会表现为一个初始窗口加一个卖家中心窗口,而不是同一窗口内唯一页面。
- Windows 全新临时 profile 实测进一步确认:Chrome 会先创建独立的 `chrome://intro/` 首次运行窗口并忽略命令行 URL;旧判断不把该页视为安全启动页,随后兜底 `Target.createTarget`,最终稳定复现 2 个 page target 和 2 个 `Chrome_WidgetWin_1` 顶层窗口。
4. 现有 `tests/test_accounts.py` 的冷启动测试明确断言 `launch_chrome()` 后还会调用一次 `create_tab_info()`,说明当前自动回归把“双阶段建页”当成了正常行为,未覆盖“首次启动只产生一个可见窗口/页面”的产品要求。
5. 本地 `chrome_launch` 运行日志中,最近两次启动记录相隔约 25 秒,各自只有一次 `start -> launched`,并记录不同 PID。该证据不支持“单次 GUI 回调调用两次 `Popen`”,但说明还需要防止用户连续点击或首轮 CDP 消失后再次启动。任务必须同时区分:
- 一次启动进程内额外创建页面/窗口;
- 两次独立用户操作或重入导致两轮 `Popen`。
T-105b 已解决“同账号 Chrome 的 CDP 端口仍在运行时,重复点击必须复用而不能再次 `Popen`”。本任务补齐新账号/冷启动阶段的单窗口语义和 GUI 防重入,不回退既有幂等复用规则。
## 方案
### 1. 冷启动直接携带卖家中心 URL
- 给 `chrome.build_launch_args()` / `chrome.launch_chrome()` 增加可选 `initial_url` 参数;不传时保持现有调用兼容,传入时把规范化 URL 作为唯一启动页面追加到 Chrome 参数末尾。
- 传入 `initial_url` 的冷启动登录路径同时追加 `--no-first-run` 和 `--no-default-browser-check`,避免全新 profile 的 `chrome://intro/`/默认浏览器提示抢占独立窗口;不传 `initial_url` 的快捷方式和其他入口不追加这些参数。
- `accounts.launch_for_login()` 在确认账号端口未运行后,计算 `https://<region_host>/portal/`,调用:
```python
chrome.launch_chrome(account, config=config, initial_url=portal_url)
```
- URL 必须来自账号已规范化的 `region_host`,只允许 `http/https` 的蝦皮卖家中心地址;不得从账号名、备注或其他未验证文本拼接命令行。
- 不加 `--new-window`,不同时传多个 URL,不修改每账号独立 `user-data-dir`、CDP 端口或 `remote-allow-origins` 参数。
- `create_shortcut()` 和其他直接使用 `build_launch_args()` 的调用保持兼容;是否让账号快捷方式也直接打开卖家中心,只有在不改变既有参数测试和用户预期的前提下才可同步,不能因本任务无意改掉其他启动入口。
### 2. 复用冷启动的初始 page target
- `wait_debug_ready()` 成功后,不立即把“当前没有已完成导航的蝦皮 URL”解释为必须新建 target。
- 对本轮刚启动的 Chrome,在一个短且有上限的预算内重新枚举 `/json`,等待命令行携带的初始卖家中心页面出现;建议总预算不超过 2~3 秒、短间隔轮询,不使用无条件固定长 `sleep`。
- 如果已经出现卖家中心或 `accounts.shopee.<区域>/seller/login` 页面,直接激活该 target,返回 `created_tab=false` / `startup_target_reused=true`,不得再调用 `Target.createTarget`。
- 如果初始 URL 尚未完成,但本轮新 Chrome 已有唯一的普通启动 page target(如 `about:blank` / `chrome://newtab` / `chrome://intro/`),优先通过该 target 导航到卖家中心并激活,避免另建窗口。该行为只适用于“本轮刚启动”的冷启动路径;端口启动前已经运行的 Chrome 不得把 `chrome://intro/` 当作可覆盖页面。
- 只有在有界等待结束后仍没有任何可复用的 page target 时,才允许兜底创建一个卖家中心 target;返回值和运行日志必须标记 `created_tab=true` 与兜底原因。
- 对“端口启动前已经运行”的 Chrome 继续沿用 T-105b:优先激活现有卖家中心/登录页;若没有,新增一个卖家中心 tab。不得为了单窗口目标导航、覆盖或关闭用户已有的普通页面。
### 3. GUI 增加启动中防重入
- `AccountsTab` 增加按账号别名或调试端口判断的启动中状态;一次启动未结束时,同账号再次触发 `launch_login()` 必须直接忽略,不进入第二次 `accounts.launch_for_login()`。
- 点击后立即禁用「启动登录」以及会改变当前账号选择/配置的相关操作;无论启动成功还是失败,都在 `finally` 或等价收口中恢复正确按钮状态。
- 当前启动链路在 GUI 主线程同步等待约 1~2 秒,本任务第一版可保留线程模型,不强制顺带重构为新 worker;但必须避免阻塞期间排队的第二次点击在首轮结束后再触发一次完整启动。
- 被防重入拦截时不弹重复警告窗口,可在状态栏显示“该账号 Chrome 正在启动,请稍候”,并写脱敏运行事件 `result=ignored reason=launch_in_progress`,便于区分用户连续点击和底层重复建页。
- 防重入状态只能约束 GUI 入口,底层 `accounts.launch_for_login()` 仍必须依赖 CDP 端口检查实现真正幂等,不能把按钮禁用当作唯一保护。
### 4. 补齐可诊断启动结果
- `launch_for_login()` 返回值至少继续保留 `action/pid/target_id/url/created_tab`,并可增加:
- `initial_url`;
- `startup_target_reused`;
- `page_target_count`;
- `fallback_reason`。
- `run_type=chrome_launch` 日志应能判断本轮是:
- 复用已经运行的 Chrome;
- 新启动并复用初始页面;
- 新启动后因无可用页面而兜底建 tab;
- 启动中重复点击被忽略。
- 日志只记录账号别名、端口、PID、target ID、URL 和状态;不得记录密码、Cookie、token、页面 HTML 或完整 Chrome Preferences。
- 不把 Chrome 的多进程架构误报为多个浏览器窗口。实机验收以可见顶层窗口、CDP page target 和运行日志三者共同判断。
### 5. 测试与实机验证
- 更新 `tests/test_chrome.py`:
- `initial_url` 为空时参数与当前兼容;
- 传入卖家中心 URL 时只追加一次且位置正确;
- 非法 scheme/空 host 被拒绝;
- 账号隔离参数和 Chrome 路径不回归。
- 更新 `tests/test_accounts.py`:
- 冷启动把卖家中心 URL 传给 `launch_chrome()`;
- 初始 target 已出现时直接激活,不调用 `create_tab_info()`;
- 初始 target 延迟出现时在短预算内重查并复用,不重复建页;
- 冷启动只有空白 page 时导航该 page,不创建第二个 target;
- 完全没有 page target 时只兜底创建一次;
- 已运行 Chrome 的 T-105b 复用路径不调用 `launch_chrome()`,也不覆盖普通用户页面。
- 更新 `tests/test_gui.py`:
- 一次点击只调用一次 `accounts.launch_for_login()`;
- 启动中第二次触发被忽略;
- 成功/失败后按钮状态恢复;
- 新增账号后不会自动启动 Chrome;
- 运行日志能区分 `launched/reused/ignored`。
- Windows 实机使用一个全新临时账号配置验证:
- 点击一次「启动登录」只出现一个该账号 Chrome 顶层窗口;
- 窗口内直接显示卖家中心或蝦皮登录页,不残留额外空白窗口;
- `/json` 中只有业务需要的初始页面,不因冷启动固定多出一个空白 target;
- 快速连续点击不会产生第二轮 Chrome 启动;
- Chrome 已运行后再次点击只激活/复用现有窗口;
- 关闭 Chrome 后再次启动仍只出现一个窗口。
## 验收要点
- 新增账号后第一次点击「启动登录」,只启动一个 Chrome 浏览器实例,并只产生一个用户可见顶层窗口。
- 初始窗口直接进入卖家中心或蝦皮登录页,不先开空白窗口再额外创建卖家中心窗口。
- 单次 GUI 操作最多调用一次 `subprocess.Popen`;启动中连续点击也不会排队触发第二轮启动。
- 同账号 CDP 已运行时继续复用,绝不新增 Chrome 进程;用户已有普通 tab 不被覆盖或关闭。
- 冷启动没有可复用 target 的异常情况下最多兜底创建一个 tab,不无限等待、不循环建页。
- 日志能明确区分新启动、复用、兜底建页和重复点击被忽略,且不包含敏感信息。
- 不自动填写账号密码、不自动提交登录、不绕过验证码或风控。
- 自动验证:
- `py -3.10 -m unittest tests.test_chrome`
- `py -3.10 -m unittest tests.test_accounts`
- `py -3.10 -m unittest tests.test_gui`
- `py -3.10 -m unittest discover -s tests`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `git diff --check`
## 边界(不改什么)
- 不修改账号表、SQLite schema、账号 slug 或 `user_data_dir` 生成规则。
- 不删除、迁移或复用其他账号的 Chrome 数据目录;不主动结束 Chrome 进程来掩盖重复窗口。
- 不通过固定长等待、隐藏窗口、最小化窗口或 `--new-window` 参数规避问题。
- 不改变①采集“可自动确保 Chrome 就绪”和③更新“只检测、不自动启动”的既有边界。
- 不修改 Shopee 商品详情页 CDP 选择器、采集、标题/封面更新、AI/cmhub、Excel 或自动升级流程。
- 不自动登录、不填写或提交密码、不读取 Cookie 值、不绕过验证码、风控或权限校验。
- 实现时同步更新 `docs/04-architecture.md`、`docs/api.md` 和必要的 `docs/routes.md`;不修改冻结的 `docs/06-tasks.md`。
## 执行记录
- 2026-07-14 完成。`chrome.build_launch_args()` / `launch_chrome()` 已支持经过校验的蝦皮 `initial_url`;冷启动登录同时使用 `--no-first-run`、`--no-default-browser-check`,卖家中心 URL 保持为唯一末尾 URL。未传 `initial_url` 的快捷方式和既有启动调用参数不变。
- `accounts.launch_for_login()` 已区分冷启动与已运行 Chrome:冷启动在 2 秒有界预算内复用初始卖家中心/登录 target,唯一 `about:blank`、新标签页或 `chrome://intro/` 可导航复用,完全无安全 target 时才创建一次并返回兜底原因;已运行 Chrome 不改写或关闭用户普通页面。
- ④账号管理已增加启动中状态和连续点击保护;启动期间禁用账号列表及增删改/登录操作,底层调用成功或失败后恢复控件。同账号启动中或系统队列紧接着投递的重复点击不会再次调用 `launch_for_login()`,并写 `result=ignored reason=launch_in_progress`。启动日志新增 `created_tab/startup_target_reused/startup_page_navigated/page_target_count/fallback_reason`,且继续统一脱敏。
- 已同步 `docs/04-architecture.md`、`docs/api.md`、`docs/routes.md`;测试覆盖启动 URL/首次运行参数校验、初始 target 立即或延迟出现、空白/intro 复用、无 target 单次兜底、运行中普通页保护、新增账号不自动启动、GUI 重入及成功/失败控件恢复。
- Windows 全新临时 profile 初次探测复现了 `chrome://intro/` 导致 2 个 page target/2 个顶层窗口,补齐首次运行参数和 intro 复用后复测通过:首次启动为 `launched`、`created_tab=false`、1 个 page target、1 个 `Chrome_WidgetWin_1`;再次调用为 `reused` 且 target 不变;关闭后使用同一 profile 再次冷启动仍为 1 个页面、1 个窗口。验证未读取或填写凭据,临时 Chrome 已通过 CDP 关闭并清理。
- 最终干净验证工作树中 `py -3.10 -m unittest discover -s tests` 共 495 项全部通过;`python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check` 全部通过。工作区中用户未提交的提示词、版本号、历史任务归档和 HTML 样本未纳入验证快照或提交。