12 KiB
12 KiB
id, title, phase, deps, status, created
| id | title | phase | deps | status | created | |
|---|---|---|---|---|---|---|
| T-639 | 商品套图生成完成状态复位与可响应停止 | 7 |
|
DONE | 2026-07-16 |
问题 / 背景
⑥「商品套图」点击「生成套图」后出现两个关联问题:
- 本轮预定数量的图片已经全部生成并显示,但按钮仍停留在「停止生成」,没有完成弹窗,也不会恢复为「生成套图」。
- 用户手动点击停止并确认后,按钮显示「正在停止...」,但长时间没有结束,表现为停止无效。
本地 SQLite 最近一轮现场显示:同一项目 5 个 job 全部为 succeeded,没有 queued/submitted/running,生成资产也已全部入库。这说明服务端生成、图片下载和保存已经完成,卡住的是客户端生成生命周期,不是 cmhub 仍在继续生成。
当前代码存在以下缺口:
SuiteTaskState.generation_running()只判断state.worker is not None;只有_on_generation_finished()收到finished/cancelled信号后才清除state.worker/thread。没有QThread.finished、本轮 job 终态或超时看门狗兜底,最终信号未正常处理时 GUI 会永久保持运行状态。- 生成信号目前通过捕获
state的 lambda 回调,没有本轮唯一标识。旧 worker 晚到的 progress/finished 信号可能影响同一任务后续新一轮生成。 BaseWorker.cancel()只设置布尔标记。run_jobs()在wait(..., FIRST_COMPLETED)中没有短超时,所有 future 都在运行或阻塞时,主调度循环无法及时观察停止标记。future.cancel()只能取消尚未开始的任务,已进入提交、轮询或下载的任务需要各阶段主动检查停止标记。- 提交请求单次最长约 36 秒、轮询请求单次最长约 15 秒;图片下载使用最长 900 秒读取预算,
_download_and_save_job_image()没有向下载层传入should_stop。 - Windows curl 下载使用阻塞式
subprocess.run(),requests 下载虽然按块读取,但块循环没有检查停止状态。因此下载阶段是手动停止最明显的无响应区。 - 当前测试只 mock
run_jobs()后直接调用 workerexecute(),没有覆盖真实QThread完成信号、终态兜底、旧信号隔离和下载中停止。
方案
1. 每轮生成增加唯一运行标识
修改 app/gui/tabs/product_suite.py、app/gui/workers.py:
- 每次
start_generation()创建不可复用的run_token,写入当前SuiteTaskState和ProductSuiteGenerateWorker。 - progress、finished、cancelled、failed 与线程结束处理都必须携带或能够解析该
run_token。 - GUI 处理信号前同时校验
state.key + run_token;不是当前活动轮次的旧信号只做必要日志,不修改按钮、进度、结果筛选或完成弹窗。 - 一轮只允许执行一次统一 finalize;
finished、cancelled、failed、线程结束兜底和终态看门狗即使先后到达,也不能重复弹窗或重复清状态。 - 生成信号优先连接到
ProductSuiteTab的 QObject 绑定槽或等价明确主线程上下文,避免 ThreadPoolExecutor/QThread 发出的信号通过无上下文 lambda 直接操作 QWidget。
不修改历史 job 的 task_key/task_id 幂等语义;run_token 只用于本次 GUI/worker 生命周期,不写入 SQLite schema。
2. 统一生成收尾与数据库终态兜底
新增统一 finalize 入口,负责:
- 停止本轮完成看门狗;
- 清除当前匹配轮次的
state.worker/state.thread/state.started_at; - 根据本轮
job_ids刷新成功、失败、停止数量; - 恢复「生成套图(N)」按钮、编辑控件和原图操作;
- 刷新当前结果;
- 正常完成、失败或停止只给一次中文汇总。
收尾来源:
- 正常
worker.finished/cancelled/failed; QThread.finished兜底;- 本轮所有
job_ids已连续两次确认进入终态,但 worker 最终信号仍未完成的终态看门狗。
终态集合为 succeeded/failed/expired/cancelled。只有本轮已知 job_ids 数量等于计划数量,且每一项都进入终态时,才可用数据库结果兜底完成;不得把历史图片数量或项目全部历史 job 数量当成本轮完成依据。
若线程已结束但仍有非终态 job:
- GUI 仍必须解除永久卡死状态;
- 非终态且已有
task_id的 job 保留/修正为可继续查询语义; - 没有
task_id且未开始的 job 标记为cancelled; - 显示中文提示「生成线程已结束,部分任务可稍后继续查询」,不能误报全部成功。
若数据库已确认本轮全部终态而旧线程仍未退出:
- GUI 可结束当前轮次并恢复操作;
- 旧 worker/thread 引用继续保留到真实
QThread.finished,不得提前销毁; - 旧线程后续信号因
run_token失效而不得覆盖新轮次。
3. 调度循环及时响应停止
修改 app/image_studio_generation.py:
wait(..., FIRST_COMPLETED)增加约100~250ms的短超时,使主循环能够持续检查should_stop()。- 用户停止后立即取消尚未开始的 futures,并把对应 job 标记为
cancelled。 - 已经进入全局 semaphore 等待、但尚未提交 cmhub 的任务必须在获得槽位后首先检查停止标记,不再提交。
- 已有
task_id的任务停止本地等待后保留JOB_RECOVERY_RESUME,提示服务端任务可能继续完成、下次可继续查询。 - 停止汇总必须满足
success + failed + cancelled == total,不遗留永远处于 queued/running 的本轮 job。 ThreadPoolExecutor仍采用协作式停止,不使用线程强杀。
4. 提交、轮询和下载阶段支持停止
提交和轮询:
- 请求前后都检查
should_stop()。 - 已发出的 HTTP 请求不做不安全强杀;最迟在本次有界请求返回后停止。提交约受 36 秒单次预算约束,轮询约受 15 秒单次预算约束。
- 轮询 sleep 继续使用可取消等待,停止后不再发下一次 GET。
图片下载:
_download_and_save_job_image()、ai._download_cmhub_image_with_retry()及内部 requests/curl 下载路径增加可选should_stop,默认None保持现有调用兼容。- 下载开始前、每次重试前后、重试等待期间和保存 JPEG 前检查停止。
- requests 流式下载每个数据块后检查停止;停止时关闭 response、丢弃内存数据和临时文件。
- Windows curl 从阻塞式
subprocess.run()调整为隐藏窗口的subprocess.Popen()+ 短间隔poll();检测停止时终止 curl,等待有界退出,必要时再 kill,并清理 curl 配置和临时图片。 - 下载取消必须映射为
cancelled,不能记录成普通下载失败;已拿到task_id的 job 保留JOB_RECOVERY_RESUME。 - 若图片已下载到临时路径但尚未入资产库时收到停止,删除本轮临时文件,不新增 asset。
5. 停止按钮和用户反馈
- 第一次点击「停止生成」并确认后设置取消标记,按钮显示「正在停止...」。
- 停止处理中再次点击不重复弹确认框,只提示「正在停止当前套图任务」。
- 进度区域继续显示已完成、失败、停止数量;不能冻结在旧值。
- 停止完成汇总明确区分:
- 已成功保存的图片;
- 已失败任务;
- 已停止、后续可继续查询的任务。
- 不向用户展示 cmhub API 路径、图片 URL、Python 异常对象或线程实现细节。
6. 诊断日志
增加脱敏后的商品套图生命周期日志,至少包含:
run_token的短标识、项目 ID、本轮计划数和 job ID 数;- stop requested;
- worker finished/cancelled/failed;
- thread finished fallback;
- terminal watchdog finalize;
- 最终 success/failed/cancelled/active 数量和总用时。
日志不得记录提示词全文、API Key、图片 URL、账号密码或 Cookie。
验收要点
- 本轮计划 N 张全部成功后,最后一个 job 入库后约 2 秒内自动恢复「生成套图(N)」并只弹一次中文完成汇总。
- 数据库本轮 job 已全部终态但 worker 最终信号被模拟丢失时,终态看门狗仍能恢复 GUI,不永久显示「停止生成」。
- worker 线程异常结束时,
QThread.finished兜底清理状态并给中文提示。 - 旧轮次的迟到 progress/finished 信号不会清除或覆盖新轮次状态。
- 停止后尚未开始的任务立即取消,不再提交 cmhub。
- 停止轮询中的任务时,不再发下一次轮询;已有
task_id的 job 可在后续继续查询。 - 停止 requests 下载时会在数据块边界退出并关闭 response,不保存半张图片。
- 停止 curl 下载时 curl 子进程被有界终止,且不会弹出黑色控制台窗口。
- 停止后本轮所有 job 最终都有明确状态,汇总满足
success + failed + cancelled == total。 - 停止过程中按钮和进度仍有反馈,结束后控件恢复,不需要重启软件。
- 正常生成、历史重试、继续查询、全局最多 5 个 cmhub 在途任务和点数记录语义不受影响。
- 不出现
QThread: Destroyed while thread is still running。
测试
tests/test_product_suite_gui.py:- 正常完成恢复按钮并只汇总一次;
- worker finished 信号缺失时由终态看门狗完成;
- thread finished 异常兜底;
- 旧
run_token信号不影响新轮次; - 停止按钮重复点击语义。
tests/test_product_suite_workers.py:- worker 所有进度/结果携带同一
run_token; - cancel 汇总数量完整。
- worker 所有进度/结果携带同一
tests/test_image_studio_generation.py:- 调度 wait 短超时后能取消排队 future;
- semaphore 等待任务停止后不提交;
- 轮询停止保留 resume;
- 下载停止删除临时文件并记 cancelled。
tests/test_ai.py:- requests 分块下载取消并关闭 response;
- curl Popen 取消、terminate/kill 和临时文件清理;
- 正常 requests/curl 下载兼容。
- 运行:
py -3.10 -m unittest tests.test_product_suite_gui
py -3.10 -m unittest tests.test_product_suite_workers
py -3.10 -m unittest tests.test_image_studio_generation
py -3.10 -m unittest tests.test_ai
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
边界(不改什么)
- 不修改 SQLite schema,不新增持久化
run_token字段。 - 不调用或假设存在 cmhub 服务端取消任务接口;客户端停止不代表服务端已停止,也不承诺退回点数。
- 不使用
QThread.terminate()、Python 线程强杀或结束整个应用进程。 - 不删除已成功保存的历史生成图片。
- 不改变套图数量计算、分类设置、提示词模板、比例、模型别名、并发上限或计费规则。
- 不修改 CDP、蝦皮主图拉取、①导入采集、②AI生成、③更新蝦皮、④账号管理或⑤设置。
执行记录
- 2026-07-16:每轮商品套图生成新增内存
run_token,worker 的 progress/summary 携带同一 token,GUI 改用主线程绑定槽处理信号。正常结果、QThread.finished和每750ms检查的本轮 job 终态看门狗统一进入幂等 finalize;旧轮次迟到信号会被忽略,旧线程引用仍由模块级容器保留到真实结束。 - 2026-07-16:统一 finalize 按本轮明确
job_ids查询 SQLite,恢复按钮和编辑控件,并只显示一次中文完成/停止/异常汇总。线程异常结束时,已有task_id的非终态 job 记为cancelled + resume,未提交任务记为cancelled + regenerate;生命周期诊断日志只记录 token 短值、项目ID、数量、来源和用时,不记录提示词、URL或密钥。 - 2026-07-16:
image_studio_generation.run_jobs()的 future wait 改为200ms短轮询,停止后及时取消未开始任务;提交/轮询请求返回后再次检查停止。下载链路新增可选should_stop:requests 在流式数据块边界取消并关闭 response,curl 改为隐藏窗口Popen,停止时有界 terminate/kill,重试等待可取消,停止后的本地文件不入资产库。 - 2026-07-16:补齐真实 QThread 完成、终态信号丢失兜底、线程异常收尾、旧 token、重复停止、立即停止、排队 future 取消、下载取消 resume、requests response 关闭和 curl 子进程/临时文件清理测试。相关 90 项通过;隔离工作树全量 533 项 unittest、Ruff、compileall、
git diff --check全部通过。当前主工作区全量测试仅有3项原有封面默认模板papa1改名导致的失败,该未提交用户改动未纳入本任务。