Files
chengma f99cde8e05
Tests / Python 3.11 / Windows (push) Has been cancelled
fix(collect): handle login target close race
2026-07-14 12:08:12 +08:00

12 KiB
Raw Permalink Blame History

id, title, phase, deps, status, created
id title phase deps status created
T-630 ①采集关闭商品页后的登录检测竞态修复 2
T-620
DONE 2026-07-14

问题 / 背景

①「导入采集」按同一账号连续处理多个商品时,上一条商品采集成功并关闭程序自动创建的商品 tab 后,下一条任务开始前的登录检测偶尔会出现:

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/<target_id> 关闭本轮自动创建的商品 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 也不会把它归类成明确未登录。本任务继续解决误报、额外等待和日志歧义,不改变这两项既有容错语义。

方案

  • 修改 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/<target_id> 后轮询 /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,例如:
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 流程由自动回归覆盖。