--- id: T-642 title: 商品套图拉取主图可停止与本轮图片清理 phase: 7 deps: [T-641] status: TODO created: 2026-07-16 --- ## 问题 / 背景 ⑥「商品套图」拉取蝦皮主图时,按钮只显示「正在拉取...」。再次点击只提示任务正在运行,不能请求停止: - `pull_main_images()` 在 `state.pull_worker is not None` 时直接返回; - `ImageStudioPullImagesWorker.execute()` 没有检查 `BaseWorker.should_cancel()`; - URL 读取完成后会继续把资产加入下载队列,活动下载和排队下载也没有本轮拉取归属; - 旧 worker 的迟到完成信号可能在用户停止或开始下一次拉取后继续刷新项目和下载图片; - 用户无法选择停止后保留还是清除本次已经拉取的图片。 本任务在 T-641 的拉取前上下文确认基础上,建立可响应停止的拉取生命周期。清理范围必须严格限制为本轮新增的蝦皮图片,不能误删原有图片或用户本地上传图片。 ## 方案 ### 1. 为每轮拉取增加独立运行上下文 修改 `app/gui/tabs/product_suite.py` 的 `SuiteTaskState`: - 增加不可复用的 `pull_run_token`; - 增加 `pull_stop_requested`; - 记录拉取开始前的 `pull_before_asset_ids`; - 记录本轮返回或新增的 `pull_asset_ids`; - 记录用户停止后选择的清理策略:`keep` 或 `clear_current`; - URL 读取 worker、下载队列、活动下载和完成信号都必须能够关联当前 `state.key + pull_run_token`。 开始下一轮拉取前重建这些内存字段;不修改 SQLite schema,不把 run token 持久化。 ### 2. 拉取按钮改为可停止状态机 空闲时按钮显示「拉取蝦皮主图」,按 T-641 先做上下文确认。 运行时: - 按钮显示「停止拉取」并保持可点击; - 点击后弹中文三选一确认框: - 「停止并保留」:停止后保留本轮已经加入列表的图片; - 「停止并清除本次新增」:停止后只移除本轮新增的蝦皮图片; - 「继续拉取」:关闭弹窗,不设置取消标记。 - 选择停止后设置 `pull_stop_requested`,调用当前 URL worker 和本轮下载 worker 的 `cancel()`,按钮显示「正在停止...」。 - 停止处理中重复点击不再弹窗,只提示「正在停止当前拉取任务」。 - 停止完成后恢复「拉取蝦皮主图」,并显示保留数、清理数和失败数的中文汇总。 不使用 `QThread.terminate()`、Python 线程强杀或结束 Chrome 进程。 ### 3. URL 读取链路支持协作式取消 修改 `app/gui/workers.py`、`app/image_studio.py` 及必要的 editor/CDP 调用边界: - `ImageStudioPullImagesWorker.execute()` 在开始前、账号/项目校验后、打开商品页前后、读取图片 URL 前后检查 `should_cancel()`。 - 将可选 `should_stop` 回调沿现有 `pull_remote_main_image_urls()` 调用链传递;默认 `None` 保持其他调用兼容。 - 已发出的 CDP/HTTP 请求不做不安全强杀,最迟在当前有界请求返回后的安全边界停止。 - 本轮自动新开的商品详情 tab 继续按现有生命周期关闭;停止不得关闭用户原本已打开的普通 tab。 - 取消结果必须返回明确 `cancelled=True`,不能记成普通拉取失败。 - 不修改 Shopee 页面选择器、登录判断、风控处理或自动登录边界。 ### 4. 下载队列按拉取轮次隔离并支持停止 当前 `state.download_queue` / `state.downloads` 也可能包含用户点击未下载缩略图触发的下载,停止本轮拉取不能一并取消无关任务。 - 为本轮拉取产生的 asset ID 建立独立集合或在队列项中携带 `pull_run_token`。 - URL worker 返回后,只有 token 仍是当前活动轮次且未停止时,才把本轮图片加入下载队列。 - 停止时只取消属于当前 token 的排队下载和活动下载;用户此前手动触发的其他下载继续运行。 - `ImageStudioDownloadOriginalWorker` 继续在重试前后检查取消,并为底层远程下载增加可选 `should_stop`;流式下载在数据块边界停止、关闭 response、丢弃临时文件。 - 停止后到达的旧下载完成信号只做线程引用清理,不得重新把已清理图片加入当前列表或覆盖新一轮状态。 - 拉取和下载停止采用有界等待与 `QThread.finished` 兜底,避免 `QThread: Destroyed while thread is still running`。 ### 5. 只清理本轮新增蝦皮图片 选择「停止并清除本次新增」时: - 清理候选集合以 `pull_asset_ids - pull_before_asset_ids` 为准; - 只允许处理带蝦皮远程 URL、属于当前项目、由本轮同步得到的 `original` 资产; - 本地手动导入图片、拉取前已有图片、其他轮次图片不得进入清理集合; - 复用 `remove_original_assets_if_unused()` 或等价事务入口校验项目归属、资产类型和 job/selection 引用; - 已被生成 job 或终选引用的图片不得猜测性删除,保留并在汇总中提示; - 清理只影响软件中的本轮资产记录和展示,不修改蝦皮线上图片; - 第一版沿用现有原图移除边界,不删除用户本地源文件,也不为本任务扩大到磁盘垃圾清理。 若停止发生在 URL 资产入库前,清理集合可以为空;仍应正常完成停止,不报错。 ### 6. 迟到信号隔离与统一收尾 - progress、finished、cancelled、failed、下载完成和 `QThread.finished` 处理前校验 `state.key + pull_run_token`。 - 旧轮次迟到结果不得清空新 worker、恢复错误按钮文字、追加下载队列或弹出旧汇总。 - 建立幂等 finalize:正常完成、用户停止、worker 失败和线程结束兜底只收尾一次。 - 关闭任务或关闭软件时请求停止;线程引用保留到真实结束,不能提前销毁。 - 诊断日志只记录 token 短值、项目 ID、商品 ID、开始前数量、本轮数量、保留/清理/失败数量和耗时,不记录 Cookie、图片 URL、账号密码或完整本地路径。 ## 验收要点 - [ ] 拉取开始后按钮显示「停止拉取」,可点击并弹出三种中文选择。 - [ ] 选择「继续拉取」后任务不停止,按钮和进度保持运行状态。 - [ ] 选择停止后按钮显示「正在停止...」,重复点击不重复弹窗。 - [ ] URL 读取开始前停止时不会打开商品页或新增资产。 - [ ] CDP 请求进行中停止时,在当前有界请求返回后退出,不继续读取或排队下载。 - [ ] 停止会取消本轮排队下载和活动下载,但不取消用户此前手动触发的无关下载。 - [ ] 「停止并保留」保留已完成的本轮图片。 - [ ] 「停止并清除本次新增」只移除本轮新增蝦皮图片;拉取前已有图片和本地手动导入图片不受影响。 - [ ] 本轮新增图片已被 job/selection 引用时不删除,并给出中文汇总提示。 - [ ] 旧轮次迟到 URL/下载结果不会覆盖新一轮状态或重新显示已清理图片。 - [ ] 正常拉取完成后按钮恢复,继续按现有逻辑后台下载并显示结果。 - [ ] 关闭任务、关闭软件和停止过程中不出现 `QThread: Destroyed while thread is still running`。 - [ ] 不修改蝦皮线上商品,不自动登录,不绕过验证码或风控。 ## 测试 - `tests/test_product_suite_workers.py` 或 `tests/test_workers.py`: - URL worker 开始前、CDP 返回后取消; - 取消结果与普通失败区分。 - `tests/test_product_suite_gui.py`: - 按钮空闲/停止/正在停止状态; - 三选一确认语义; - 当前 token 信号处理与旧 token 忽略; - 停止只取消本轮下载; - 保留与清除本轮新增图片; - 被引用图片拒绝清理; - finalize 幂等和线程结束兜底。 - `tests/test_image_studio.py`、`tests/test_image_studio_images.py`: - `should_stop` 调用边界; - 清理集合项目归属、远程图片类型和引用保护; - 下载取消关闭 response、清理临时文件。 - 运行: ```bash py -3.10 -m unittest tests.test_product_suite_gui tests.test_product_suite_workers tests.test_image_studio tests.test_image_studio_images py -3.10 -m unittest discover -s tests py -3.10 -m ruff check app tests main.py py -3.10 -m compileall app main.py git diff --check ``` 涉及真实 CDP 取消边界时,在测试商品 `51100639510` 上验证:开始拉取后停止,确认本轮自动新建 tab 能在安全边界关闭;无法实跑时在执行记录中如实说明。 ## 边界(不改什么) - 不修改 SQLite schema,不持久化 run token 或停止策略。 - 不删除蝦皮线上图片,不删除拉取前已有图片或用户手动导入的本地图片。 - 不删除被 job/selection 引用的资产,不做猜测性清理。 - 不使用线程强杀、`QThread.terminate()` 或结束 Chrome 进程。 - 不修改 Shopee/CDP 选择器、登录策略、验证码/风控边界或商品更新流程。 - 不修改 cmhub、图片生成、提示词模板、套图分类数量、比例、导出逻辑、①导入采集、②AI生成、③更新蝦皮、④账号管理或⑤设置。 ## 执行记录 - 待执行。