6.1 KiB
6.1 KiB
id, title, phase, deps, status, created
| id | title | phase | deps | status | created | |
|---|---|---|---|---|---|---|
| T-604 | AI工场拉取主图 QThread 生命周期闪退修复 | 7 |
|
TODO | 2026-07-11 |
问题 / 背景
运行最新代码,在⑥AI工场输入商品ID后点击「拉取主图」,程序可能闪退,控制台报错:
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。
- mock
验证命令:
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 生图、下载、导出业务规则。
- 不引入新的多线程框架。
执行记录
待执行。