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

121 lines
6.1 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-604
title: AI工场拉取主图 QThread 生命周期闪退修复
phase: 7
deps: [T-591]
status: TODO
created: 2026-07-11
---
## 问题 / 背景
运行最新代码,在⑥AI工场输入商品ID后点击「拉取主图」,程序可能闪退,控制台报错:
```text
QThread: Destroyed while thread is still running
```
初步分析确认这不是 Shopee/CDP 读取主图本身的错误,而是 AI工场 tab 的后台线程生命周期管理不一致导致:
- `app/gui/tabs/image_studio.py` 的 `_start_worker()` 只用 `self._running_thread` 保存当前 `QThread`。
- `ImageStudioPullImagesWorker` 返回结果或失败后,`_on_pull_finished()` / `_on_worker_failed()` 会调用 `_finish_worker()`。
- `_finish_worker()` 立刻把 `self._running_thread = None`。
- 此时 `worker.finished` 虽然已经发出,但 `QThread` 可能还没有真正发出 `thread.finished`、事件循环还没退出。
- Python 引用被提前清掉后,Qt 线程对象可能在仍运行时被销毁,从而触发闪退。
对比①采集、②AI生成、③更新、⑤设置等模块,它们都是把线程引用保留到 `thread.finished` 后再清理。⑥AI工场当前没有按同样模式处理,所以「拉取主图」「下载原图」「生成图片」「继续查询」「导出终选」这些共用 `_start_worker()` 的操作理论上都有同类风险,只是拉取主图在失败/快速返回时更容易触发。
还有一个交互缺口:`pull_main_images()` 如果 `current_project` 不为空,会忽略用户刚在输入框里填的新商品ID,继续使用当前项目的旧 `account_alias/item_id`。这会造成“输入商品ID拉取主图”行为不符合用户预期。
## 方案
### 1. 修复 AI工场 QThread 生命周期
修改 `app/gui/tabs/image_studio.py`:
- `_start_worker(worker, thread_name)` 改成与其他 tab 一致的模式:
- 调用 `run_worker(worker, thread_name=thread_name, start=False)`。
- 连接 `thread.finished` 到新的 `_forget_running_thread(thread)`。
- 先保存 `self._running_worker = worker`、`self._running_thread = thread`。
- 设置运行态后再 `thread.start()`。
- 新增 `_forget_running_thread(thread)`:
- 只有 `self._running_thread is thread` 时,才把 `self._running_thread = None`、`self._running_worker = None`。
- 只在 `thread.finished` 后清理线程引用。
- `_finish_worker()` 不再直接清空 `self._running_thread`。
- 它只负责恢复按钮/控件运行态。
- 如需避免重复恢复 UI,可以增加 `_worker_running` 或通过当前线程引用判断,保持幂等。
### 2. 避免失败路径重复处理
当前 `BaseWorker.run()` 异常时会先发 `failed`,再发 `finished({"ok": False})`。AI工场的各个 finished 槽又会在 `ok=False` 时转调 `_on_worker_failed()`,可能导致同一个失败重复弹窗/重复清理。
本任务同步收敛 AI工场失败处理:
- 对 `worker.failed` 仍保留 `_on_worker_failed()`,显示中文错误和恢复 UI。
- 对各 `_on_*_finished(summary)` 中 `summary.get("ok") is False` 的分支,避免再次弹窗;可以直接返回,或只在当前仍处于 running 且未处理失败时处理一次。
- 不改 `BaseWorker` 的全局行为,避免影响其他 tab。
### 3. 拉取主图尊重当前输入
修改 `pull_main_images()`:
- 点击「拉取主图」时,以当前账号下拉和商品ID输入框为准。
- 如果当前输入与 `current_project` 不一致,允许创建/切换到对应项目后再拉取。
- 不再因为 `current_project is not None` 就强制覆盖成旧项目的 `account_alias/item_id`。
- 保留输入为空、账号为空时的中文阻断提示。
### 4. 不改 CDP 读取主图业务链路
本任务不修改:
- `app/image_studio.py::pull_remote_main_image_urls()` 的业务语义。
- `app/editor.py` 的 `open_product()`、`read_product_image_urls()`、`close_readonly_product()`。
- 账号登录检测、Chrome 启动、Shopee 页面选择器。
如果拉取失败,仍应通过现有 worker 错误路径给中文弹窗和日志,不应闪退。
## 验收要点
- 在⑥AI工场输入商品ID,点击「拉取主图」,成功时不闪退,主图列表刷新。
- 商品ID无效、账号未登录、Chrome/CDP不可用、Shopee页面加载失败等快速失败场景下,不再出现 `QThread: Destroyed while thread is still running`。
- 「拉取主图」失败只弹一次错误提示,不重复弹窗、不重复写同一条失败日志。
- 拉取主图运行期间按钮禁用;完成或失败后按钮恢复。
- 连续多次拉取不同商品ID时,使用当前输入框里的商品ID,不被旧 `current_project` 覆盖。
- 「下载原图」「开始生成」「继续查询任务」「导出终选」这些共用 AI工场 `_start_worker()` 的操作不回归,完成/失败后线程引用都在 `thread.finished` 后清理。
- 不改变 CDP 读取蝦皮主图逻辑,不改变商品详情页打开/关闭规则。
## 测试要求
优先补 GUI 单元测试,覆盖线程生命周期和拉取参数:
- `tests/test_gui.py` 或新增合适测试文件:
- mock `run_worker()` 返回可控 fake thread,确认 `_start_worker()` 先保存线程,只有触发 `thread.finished` 后才清空引用。
- 调用 `_finish_worker()` 后确认不会提前清空 `self._running_thread`。
- 构造 `current_project` 与输入框商品ID不一致的场景,确认 `ImageStudioPullImagesWorker` 使用当前输入的商品ID。
验证命令:
```bash
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
```
如能人工验证,补充运行 GUI 后的实测:
- 输入一个有效商品ID拉取主图成功。
- 输入一个无效商品ID拉取主图失败但不闪退。
## 边界(不改什么)
- 不改 `app/workers.py` 的全局 `BaseWorker.run()` 和 `run_worker()` 行为,避免影响①②③⑤。
- 不改 Shopee/CDP 选择器和读取主图逻辑。
- 不改 AI工场 DB schema。
- 不改 cmhub 生图、下载、导出业务规则。
- 不引入新的多线程框架。
## 执行记录
待执行。