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

96 lines
10 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-571
title: ③更新抢前台降频:每账号只提一次 Chrome 前台 + 拖拽/上传失败安全升级前台恢复
phase: 7
deps: [T-402, T-562]
status: DONE
created: 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 链路。
- 不做⑤设置开关。
## 执行记录
- 2026-07-09:完成 T-571。
- `app/gui/workers.py`:`ApplyWorker` 增加线程安全的本轮账号前台集合,同一账号本轮首条更新传 `bring_to_front=True`,后续同账号任务传 `False`;分批更新跨批次保持该集合,多账号并行下用锁保护。
- `app/editor.py`:`apply_task()` 增加 `bring_to_front` 参数并透传 `open_product()`;后台态封面替换遇到 `UPLOAD_STILL_PROCESSING` / `UPLOAD_TIMEOUT` / `DRAG_NOT_FIRST` / `NEW_IMAGE_NOT_FOUND` 时,只将当前 tab 提前台做一次非破坏性安全恢复,不完整重跑 `replace_cover()`。
- `app/editor.py`:`UPLOAD_*` 失败返回新增 `before_srcs` 上传前快照;恢复上传时用 `before_srcs` 识别本次新图,固定恢复等待预算 120 秒;拖拽恢复只基于已有 `new_src` 重读图片列表并重拖。
- `tests/test_editor_login.py`:覆盖 `before_srcs` 字段、后台拖拽失败前台重拖、后台上传处理中前台续等、缺少 `before_srcs` 时不猜测恢复且不点击更新。
- `tests/test_gui.py`:覆盖同账号跨分批只首条前台、不同账号并行各自首条前台,并更新 fake `apply_task` 签名兼容 `bring_to_front`。
- 文档:同步 `docs/04-architecture.md` 与 `docs/routes.md` 的③更新前台策略。
- 验证通过:`py -3.10 -m unittest tests.test_editor_login`、`py -3.10 -m unittest tests.test_gui`、`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`。