--- id: T-630 title: ①采集关闭商品页后的登录检测竞态修复 phase: 2 deps: [T-620] status: DONE created: 2026-07-14 --- ## 问题 / 背景 ①「导入采集」按同一账号连续处理多个商品时,上一条商品采集成功并关闭程序自动创建的商品 tab 后,下一条任务开始前的登录检测偶尔会出现: ```text step=login_check result=retry detail=账号 <别名> 第1/3次登录检测暂不确定,2秒后重试: 原因=NO_SESSION_COOKIE,URL=<上一条商品详情页>,Cookie名称=未读到登录Cookie ``` 用户容易把这条日志理解为“上一条商品首次新增 tab 采集失败,重试后才成功”。实际运行日志已经确认这是跨任务时序问题: - run 353 中,事件 5082 表示任务 70、商品 48663456321 已采集成功; - 紧接着的事件 5083 实际绑定任务 71、商品 52313423890,但可见 message 没有任务信息; - 该登录检测返回的 URL 仍是刚完成的商品 48663456321; - 任务 71 随后直接进入 `db_write/open_product/wait_ready` 并采集成功; - 任务 72 开始前再次出现同样现象,登录检测 URL 又停留在任务 71 的商品页。 已核实的代码原因: 1. `CollectWorker` 在每条 eligible 任务进入采集前调用 `_confirmed_login_status()`。 2. `editor.collect()` 完成后由 `_close_collected_product()` 调用 `/json/close/` 关闭本轮自动创建的商品 tab,但 Chrome target 从 `/json` 列表消失是异步的。 3. 下一条任务立即进入 `editor.login_status()`;当前实现从 `/json` 中用 `next(...)` 选择第一个 Shopee page,可能选中正在销毁、但仍短暂可见的上一条商品 target。 4. `_wait_for_login_probe()` 调用 `Network.getAllCookies` 失败时吞掉异常并把 `cookie_names` 改为空集合,无法区分“Cookie API 调用失败”和“Cookie API 成功但确实没有会话 Cookie”,最终误报 `NO_SESSION_COOKIE`。 5. 外层登录确认等待约 2 秒后重新执行完整检测;此时旧 target 已消失,因此第二次检测成功并继续采集。第一次页面探测默认最多等待约 8 秒,加上 2 秒重试间隔,单条任务可能无意义增加约 10 秒。 6. 单条采集 `elapsed_ms` 从登录检测之后才开始计算,因此这段等待不会出现在“采集成功 elapsed_ms”中,进一步造成用户看到的总等待与日志耗时不一致。 T-563 已保证 `NO_SESSION_COOKIE` 属于不确定状态,不会因此把同账号后续任务批量略过;T-620 也不会把它归类成明确未登录。本任务继续解决误报、额外等待和日志歧义,不改变这两项既有容错语义。 ## 方案 ### 1. 区分 Cookie 缺失与 CDP 探测失败 - 修改 `app/editor.py` 的登录探测返回结构,使 `_wait_for_login_probe()` 或等价 helper 至少保留: - 当前 URL; - 已成功读取到的 Cookie 名称; - Cookie API 是否至少成功调用一次; - 最近一次探测错误类别,不保存异常中的敏感值。 - `Network.getAllCookies`、URL 读取或 websocket 调用因 target 关闭而失败时,不得把结果伪装成空 Cookie 列表。 - 只有满足以下条件时才能返回 `reason=NO_SESSION_COOKIE`: - 至少一次 Cookie API 调用成功; - 页面 target 在该次探测中仍可用; - 返回的 Shopee Cookie 名称确实不包含 `SPC_ST` / `SPC_U`。 - 如果整个探测周期没有一次成功的 Cookie 读取,应返回不确定原因,例如 `LOGIN_CHECK_TARGET_UNAVAILABLE` 或 `LOGIN_CHECK_FAILED`;外围中文日志说明“登录状态暂时无法读取”,不得显示成账号退出登录。 - 继续只记录 Cookie 名称,不记录 Cookie 值。 ### 2. 失效 target 快速重选 - `editor.login_status()` 不再把一次登录检测永久绑定到最先枚举到的 Shopee page。 - 连接 page 失败、websocket 已关闭、`Network.getAllCookies` 持续失败或 target 已不在最新 `/json` 列表时,应在本次登录检测预算内重新枚举 page targets,并改连其他仍有效的 Shopee 页面。 - 有多个 Shopee target 时优先选择可连接且 URL 稳定的卖家中心页面;不得因为列表第一项是正在关闭的商品页就等待完整 8 秒。 - 如果当前没有有效 Shopee page,可沿用既有规则打开卖家中心根地址进行检测;不自动登录、不填密码。 - 显式命中 `accounts.shopee.<区域>/seller/login` 等登录 URL 时,仍返回明确 `LOGIN_PAGE`,不得被本任务放宽成不确定状态。 - 可评估通过浏览器级 CDP `Storage.getCookies` 读取 Cookie,使 Cookie 检测不依赖具体 page target;只有在真实 Chrome/CDP 验证可用、账号隔离边界正确并同步架构事实后才能采用。第一版不以未经验证的浏览器级接口作为完成前提。 ### 3. 商品 tab 关闭完成确认 - 在 `app/cdp.py` 增加有上限的 target 关闭确认 helper,或在既有关闭接口中提供可选等待能力:调用 `/json/close/` 后轮询 `/json`,直到该 target ID 消失。 - 等待必须短且有明确上限,建议总预算不超过 2 秒、轮询间隔约 0.1~0.2 秒;不得使用固定 `sleep(2)` 让每条任务无条件变慢。 - ①采集和⑥商品套图只对“本轮程序自动创建并需要关闭”的只读商品 tab 使用该确认;复用用户原有 tab 仍只断开 CDP,不关闭、不等待其消失。 - 关闭确认超时不能覆盖原本已经成功的采集结果;记录不确定诊断后,下一条登录检测仍须依靠第 1、2 节的容错完成有效 target 重选。 - 不改变③更新蝦皮成功提交后已有的 2 秒观察窗口和前台 tab 策略。 ### 4. 登录重试日志可追踪 - `CollectWorker._confirmed_login_status(account, context, task)` 在 `task` 非空时,用户可见 retry/uncertain/recovered 日志必须包含当前任务 ID 和商品 ID,例如: ```text step=login_check result=retry detail=任务 71 商品 52313423890 账号 <别名> 登录状态暂时无法读取,正在重新检测 ``` - 如果第一次不确定、后续检测恢复已登录,应增加一条简洁中文恢复日志,明确本次继续的是哪条任务;不得让 retry 后直接跳到 `db_write` 而没有恢复结果。 - 底层原因、URL、Cookie 名称和尝试次数继续写结构化运行日志/本地诊断日志;普通可见文案不得裸露 websocket、异常堆栈或 Cookie 值。 - 登录检测耗时可写 `elapsed_ms` 作为诊断字段,但不修改现有采集成功 `elapsed_ms` 的历史口径;界面 T-628 的“本条耗时”已经从 `task_started` 开始覆盖登录检测等待。 ### 5. 测试与实机验证 - 更新 `tests/test_editor_login.py`: - `/json` 第一项是正在关闭的旧商品 target、第二项是有效 Shopee target时,能快速改连有效 target并读取登录 Cookie; - Cookie API 对旧 target 抛出“target closed”类错误时,不返回 `NO_SESSION_COOKIE`; - Cookie API 成功返回但确实没有 `SPC_ST` / `SPC_U` 时,仍返回 `NO_SESSION_COOKIE`; - 明确登录页仍返回 `LOGIN_PAGE`; - 没有有效 Shopee page 时沿用打开卖家中心页的逻辑。 - 更新 `tests/test_cdp.py` 或对应 CDP 测试: - target 很快消失时关闭确认成功; - target 未在预算内消失时按超时结果返回,不无限等待; - 不关闭用户原有 tab。 - 更新 `tests/test_gui.py` 的 `CollectWorker` 测试: - 首次探测 target 失效、后续重选成功时,当前任务继续采集且不记略过/失败; - retry 和 recovered 日志包含当前任务 ID、商品 ID; - 连续不确定仍保持 T-563 的“继续尝试当前商品、不批量略过”语义; - 明确 `LOGIN_PAGE` 仍整组进入需登录分类。 - 使用测试账号连续采集至少 3 个测试商品,确认: - 上一条自动新建商品 tab 关闭后,下一条不会因旧 URL 产生假 `NO_SESSION_COOKIE`; - 不再每条额外等待约 8~10 秒; - 每条只读商品 tab 按既有规则关闭,Chrome 不残留程序新建的商品页; - 采集标题、封面下载和 SQLite/Excel 结果不回归。 ## 验收要点 - 连续采集同账号多个商品时,下一条登录检测不会连接刚关闭的上一条商品 target并误报 `NO_SESSION_COOKIE`。 - Cookie API 调用失败和成功返回空 Cookie 是两个不同状态;任何 CDP异常都不能伪装成账号缺少会话 Cookie。 - target 关闭竞态能在短预算内恢复,不等待完整登录超时再由外层重试。 - 上一条采集成功不会因关闭确认超时被改写为失败;下一条仍能继续。 - 日志能明确看出 retry/recovered 属于哪条任务和商品,不再与上一条采集成功日志混淆。 - `LOGIN_PAGE` 仍是明确未登录;`NO_SESSION_COOKIE` / target 暂不可用仍不批量略过后续任务。 - ①采集继续后台只读打开商品页;不切换 Chrome 前台,不影响③更新蝦皮的前台交互策略。 - 自动验证: - `py -3.10 -m unittest tests.test_editor_login` - `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` ## 边界(不改什么) - 不删除采集前预检或采集中途登录检测,只修正探测目标、状态语义和等待方式。 - 不通过增加固定 sleep、放大登录超时或把所有 `NO_SESSION_COOKIE` 当作已登录来掩盖问题。 - 不自动登录、不填写账号密码、不读取或记录 Cookie 值、不绕过验证码和风控。 - 不修改商品详情页选择器、采集标题/封面逻辑、Excel/SQLite schema、AI生成或③更新蝦皮流程。 - 不为解决本问题把①采集改回前台打开商品页;该竞态发生在登录检测选择已关闭 target,与前后台展示无关。 ## 执行记录 - 2026-07-14 完成。 - `app/cdp.py` 新增有界 `wait_target_closed()` / `close_tab_and_wait()`:关闭 target 后按 ID 轮询 `/json`,默认最多等待 2 秒,不使用固定休眠。 - `app/editor.py` 的登录检测现在保留 Cookie API 是否成功、探测错误类别和尝试次数;URL/Cookie 探测失败返回 `LOGIN_CHECK_TARGET_UNAVAILABLE`,不会伪装成 `NO_SESSION_COOKIE`。多个 Shopee 页面并存时优先稳定 portal,失效 target 会在同一检测预算内快速重选。 - ①采集及⑥共用的后台只读商品页,在成功采集或 `open_product()` 打开失败时都会确认程序新建 target 已消失;关闭确认超时仅返回 `close_target_confirmed=False` 并记录警告,不覆盖采集结果。③前台更新和提交后 2 秒观察逻辑未修改。 - `CollectWorker` 的登录 retry/recovered 日志已带任务 ID、商品 ID 和账号;首次不确定、后续恢复时明确记录恢复。关闭确认超时写可见警告和本地诊断,任务仍按采集成功落库。账号管理把 target 暂不可用显示为“登录状态暂不可确认”。 - 已同步 `docs/04-architecture.md` 与 `docs/api.md`,并补充 CDP、editor、账号状态和 worker 回归测试。 - 自动验证通过: - `py -3.10 -m unittest tests.test_editor_login`:50 项通过; - 仅导出本任务暂存内容的干净副本运行 `py -3.10 -m unittest tests.test_gui`:182 项通过; - 同一干净副本运行 `py -3.10 -m unittest discover -s tests`:470 项通过; - `python -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check` 均通过。 - 真实 CDP 验证:当前已有 1 个账号 Chrome 运行且登录检测成功;最终代码连续 3 轮“后台新建只读 target → 有界关闭 → 立即检测登录”均通过,关闭确认均为 true、登录均为 true、`probe_attempts=1`,单轮分别耗时约 0.14 秒、0.05 秒、0.03 秒,无假 `NO_SESSION_COOKIE`,且 target 均已确认消失。 - 商品采集实测限制:按仓库规则尝试测试商品 `51100639510` 时,Shopee 返回“商品失效:please input correct product id”,无法继续完成该商品的标题/封面三轮实采;未改用真实运营商品规避测试边界。标题、封面下载和 SQLite 流程由自动回归覆盖。