Files
cmshoppe/docs/tasks/T-571.md
T
chengmaandClaude Opus 4.8 d595e7455c docs(tasks): T-571 add before_srcs snapshot, bounded recovery wait, NEW_IMAGE_NOT_FOUND note
补两个恢复缺口:UPLOAD_* 失败返回须带上传前 src 快照(识别新图的
唯一可靠判据,否则只能启发式猜、违反不做猜测性二次操作原则);
提前台后的恢复等待须有固定时限(≤180s,防并行模式占死账号线程)。
另加 NEW_IMAGE_NOT_FOUND 可选纳入升级清单的备注(带 new_src、
恢复同 DRAG_NOT_FIRST 路径)。验收要点同步两条断言。

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-07-09 14:54:33 +08:00

8.8 KiB
Raw Blame History

id, title, phase, deps, status, created
id title phase deps status created
T-571 ③更新抢前台降频:每账号只提一次 Chrome 前台 + 拖拽/上传失败安全升级前台恢复 7
T-402
T-562
TODO 2026-07-09

问题 / 背景

③更新蝦皮时,apply_task(app/editor.py:1248)每条任务调 open_product(account, item_id),走默认 bring_to_front=True(editor.py:756):create_tab_info(background=False) 以激活态建 tab + Page.bringToFront(editor.py:777)把整个 Chrome 窗口提到 OS 前台。用户切到其他软件后,每开始一条任务(几十秒一条)Chrome 就抢一次前台,批量几百条 = 焦点被抢几百次,正在输入的内容可能误入 Chrome。

①采集已是 bring_to_front=False(T-562,editor.py:830)。①③不对称有技术原因,不能无脑把③改 False:①只读;③要做真实 UI 交互(cdp.drag 34 步拖拽换首图、上传等待、点更新)。CDP Input.dispatchMouseEvent 注入渲染进程不需要 OS 焦点,但 Windows 上 Chrome 有遮挡检测(occlusion tracking):窗口被完全遮挡或最小化 → visibilityState: hidden → rAF 暂停、JS timer 节流到 1 次/秒,Shopee 拖拽排序库/上传组件可能失灵(DRAG_NOT_FIRST、UPLOAD_STILL_PROCESSING 假超时)。部分可见则正常。

结论:「不抢前台」与「交互必成」在完全遮挡时真冲突 → 用「每账号一次 + 失败升级」组合化解。

方案(改哪个文件、改成什么)

方案 1:抢前台从「每任务」降为「每账号一次」

  • apply_task 增加 bring_to_front 参数(默认 True 保持函数级向后兼容),透传给 open_product。
  • ③ ApplyWorker(app/gui/workers.py 更新循环)维护「本轮已提过前台的账号集合」:同一账号第一条任务 bring_to_front=True(换账号提示一次,有「正在处理该店」的信号意义),该账号后续任务传 False。
  • 该集合必须在 ApplyWorker 生命周期内维护,跨当前本轮的所有分批生效;worker 重开/新一轮更新才重置。
  • 因 ③ 支持多账号并行,集合访问必须线程安全,建议 self._foregrounded_aliases = set() + self._foreground_lock = threading.Lock(),通过 helper 原子判断“本账号是否本轮首次前台”。

方案 2:拖拽/上传失败自动升级前台安全恢复(兜底)

  • apply_task(或 replace_cover 调用层)当封面替换结果 reason ∈ {DRAG_NOT_FIRST, UPLOAD_STILL_PROCESSING, UPLOAD_TIMEOUT} 且本次是后台态(bring_to_front=False)时:
    • Page.bringToFront 提前台;
    • 不得完整重跑 replace_cover()。replace_cover() 是破坏性流程,开头会删除当前第一张图;如果第一次已经删除旧封面,第二次完整重跑可能继续删除第二张原图或处理中的新图。
    • 只能在同一 tab 内做安全恢复:
      • UPLOAD_STILL_PROCESSING / UPLOAD_TIMEOUT:提前台后继续等待当前上传队列完成,重新读取 _image_rects() / _upload_state(),如果发现本次新上传图已取得 susercontent 地址,再进入拖到第一位校验;不得重新删除、不得重新选择本地文件。
      • DRAG_NOT_FIRST:使用第一次 replace_cover() 返回的 new_src,提前台后重新读取当前图片列表,只对已存在的 new_src 再拖到第一位并校验;不得重新上传、不得重新删除。
      • 如果缺少 new_src 或无法安全识别当前新图,直接按既有失败口径返回,保留现场,不做猜测性二次操作。
    • 安全恢复只做一次;仍失败 → 按既有失败口径返回,不无限重试。
    • 升级恢复要发 on_step 事件(如 cover_retry_foreground),run_logs 可见「后台上传/拖拽疑似受限,已提前台恢复一次」。
  • 只对上述三个「疑似后台节流」的 reason 升级;其他失败(OLD_COVER_BACKUP_MISSING、UPLOAD_DUPLICATE_IMAGE、UPLOAD_CROP_REQUIRED 等)与前台与否无关,不升级、不重试。
  • 升级重试前须重新读取页面图片状态(_image_rects/_upload_state),不能拿失败前的旧 rects 继续拖。
  • 可在 editor.py 内抽小 helper,复用现有图片状态读取、拖拽和上传完成判断逻辑;但不要改变 replace_cover() 对外返回结构,除非仅补充安全恢复所需的非破坏性上下文字段(例如 new_src、上传后的 count/状态快照)。
  • UPLOAD_* 失败返回必须带上传前 src 快照(before_srcs):恢复时识别「本次新上传的图」的唯一可靠判据是 src 不在上传前集合里(editor.py:982/1045)。当前 UPLOAD_STILL_PROCESSING/UPLOAD_TIMEOUT 返回不含 before_srcs 且 new_src 为 None——不补此字段,恢复只能启发式猜新图,违反上面「不做猜测性二次操作」的原则。
  • 恢复等待必须有明确时限:提前台后的「继续等待上传完成」不得无界,给固定预算(≤ 首跑同款 timeout=180s,建议 120s),超时按原口径失败。否则并行模式下一个卡死上传会占住一个账号线程不放。
  • 可选备注:NEW_IMAGE_NOT_FOUND(editor.py:1084,上传已拿到 new_src 但随后 rects 中找不到)同样可能是后台重渲染/节流所致,且已带 new_src、恢复方式与 DRAG_NOT_FIRST 同款(重读 rects + 重拖)。是否纳入升级清单由实现者定;纳入则复用 DRAG_NOT_FIRST 的恢复路径与单测口径。

文档同步(必须一起改,防止被当 bug 改回)

  • docs/04-architecture.md / docs/routes.md 里「③更新打开商品页保持前台」的表述更新为「每账号首任务提一次前台 + 失败升级」;注明 ①(T-562 完全不抢)与 ③(低频抢 + 升级)的不对称是有意设计及技术原因(遮挡节流)。

不做(本任务边界内明确排除)

  • 不加 --disable-backgrounding-occluded-windows 等 Chrome 启动参数(典型自动化指纹,触碰「不绕过 Shopee 风控」红线的灰区,收益不值)。
  • ⑤设置开关「更新时保持 Chrome 前台」本期不做,若后续有用户环境后台老失败再提任务。

验收要点

  • 同一账号连续 N 条任务:仅第一条调用 Page.bringToFront(单测 mock open_product/CDP 断言调用次数=账号数,不是任务数)。
  • 换账号 → 新账号第一条再提一次前台。
  • 分批更新时,同一账号跨多个批次仍只在本轮第一次提一次前台;新一轮 worker 才重置。
  • 多账号并行时,前台集合判断线程安全;同账号不会因并发/分批重复判为首次。
  • 后台态封面替换返回 DRAG_NOT_FIRST/UPLOAD_STILL_PROCESSING/UPLOAD_TIMEOUT → 自动提前台安全恢复一次;恢复成功计成功;恢复失败按原失败口径。
  • 后台安全恢复不得完整调用第二次 replace_cover();不得第二次删除第一张图;不得第二次重新上传同一文件。单测需覆盖该安全边界。
  • DRAG_NOT_FIRST 恢复只允许基于第一次返回的 new_src 重拖;new_src 缺失或页面找不到该图时失败并保留现场。
  • UPLOAD_STILL_PROCESSING / UPLOAD_TIMEOUT 恢复只允许继续等待当前上传结果;无法确认新图时失败并保留现场。
  • UPLOAD_* 失败返回含 before_srcs(上传前 src 快照),恢复用它识别新图(单测断言字段存在且被恢复逻辑消费)。
  • 恢复等待有固定时限(≤180s),超时按原口径失败、不无限等待(单测用 mock 时钟或短时限覆盖)。
  • 前台态(该账号首任务)失败 → 不重复升级(已在前台,无升级意义),按原口径。
  • 其他失败 reason(重复图/需裁剪/备份缺失等)→ 不触发升级重试。
  • 升级恢复有 run_logs 事件可见,文案为中文,不暴露技术栈细节给普通用户。
  • apply_task 默认参数不变,直接调用的旧代码行为不回归。
  • 既有 ③ 更新链路测试(tests/test_gui.py/tests/test_editor_login.py 相关)不回归。
  • 验证命令(unittest,不引入 pytest):
    • py -3.10 -m unittest tests.test_gui tests.test_editor_login
    • python -m ruff check app tests main.py
    • py -3.10 -m compileall app main.py
    • py -3.10 -m unittest discover -s tests
    • git diff --check

边界(不改什么)

  • 不改①采集就绪/前台策略(T-562 保持)、④登录页 activate_tab 行为。
  • 不改 change_title/click_update 的交互逻辑。
  • 不把后台失败恢复实现为第二次完整 replace_cover();不做可能二次删除线上图片的自动操作。
  • 不改 Chrome 启动参数、CDP 选择器、DB schema、AI/cmhub 链路。
  • 不做⑤设置开关。

执行记录

(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)