Files
chengma b68e22c094
Tests / Python 3.11 / Windows (push) Has been cancelled
fix(product-suite): finalize and cancel generation
2026-07-16 17:12:41 +08:00

12 KiB
Raw Permalink Blame History

id, title, phase, deps, status, created
id title phase deps status created
T-639 商品套图生成完成状态复位与可响应停止 7
T-638
DONE 2026-07-16

问题 / 背景

⑥「商品套图」点击「生成套图」后出现两个关联问题:

  1. 本轮预定数量的图片已经全部生成并显示,但按钮仍停留在「停止生成」,没有完成弹窗,也不会恢复为「生成套图」。
  2. 用户手动点击停止并确认后,按钮显示「正在停止...」,但长时间没有结束,表现为停止无效。

本地 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() 后直接调用 worker execute(),没有覆盖真实 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)」按钮、编辑控件和原图操作;
  • 刷新当前结果;
  • 正常完成、失败或停止只给一次中文汇总。

收尾来源:

  1. 正常 worker.finished/cancelled/failed;
  2. QThread.finished 兜底;
  3. 本轮所有 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 汇总数量完整。
  • 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 改名导致的失败,该未提交用户改动未纳入本任务。