--- id: T-640 title: 商品套图单张失败重试保持当前轮结果 phase: 7 deps: [T-639] status: DONE created: 2026-07-16 --- ## 问题 / 背景 ⑥「商品套图」一轮生成中有单张图片失败时,用户点击失败图片卡片的「重试」,当前实现会把这次单张操作当成一轮全新的套图生成: - `retry_job()` 重新组装单条 spec 后直接调用完整的 `start_generation()`; - `start_generation()` 会清空 `current_job_ids`、重置本轮进度并退出历史视图; - worker 会新建一条 job,随后 GUI 只把这个新 job 识别为当前轮; - 原本同轮已经成功的图片会从「本轮」结果区消失,用户看到的是只有一张图片的“第二轮生成”。 新建 job 保留失败历史、计费和诊断记录是正确的,但「失败重试」在产品语义上应当是修复当前轮中的失败槽位,不应表现为重新开始一整轮。成功图片的「重新生成」与失败图片的「重试」也应保持不同语义。 ## 方案 ### 1. 分离活动生成 job 与当前结果 job 修改 `app/gui/tabs/product_suite.py` 的 `SuiteTaskState`: - `current_job_ids` 继续表示「本轮结果区当前有效的图片槽位」; - 新增仅用于当前运行生命周期的 `generation_job_ids`,表示本次实际提交、被进度/停止/终态看门狗跟踪的 job; - 正常整轮生成时同时重建两组 ID; - 单张失败重试时只清空 `generation_job_ids`,保留其他 `current_job_ids`; - T-639 的终态快照、停止、线程结束和看门狗统一只读取 `generation_job_ids`,不得把当前轮其他历史成功 job 重复计入本次重试汇总。 这些字段只保存在 GUI 内存,不修改 SQLite schema。 ### 2. 单张重试替换失败槽位 为 `start_generation()` 增加明确的单张重试上下文: - `retry_job()` 传入原失败 job ID,而不是把单条 spec 当作普通整轮生成; - worker 仍创建新的 job,不覆盖或删除原失败 job; - worker 首次返回新 job ID 后,在 `current_job_ids` 中把原失败 ID 原位替换为新 job ID; - 当前轮其他成功 job ID 保持不变; - 如果重试再次失败或被停止,新的失败/停止 job 继续占据该槽位,用户可以再次重试; - 「历史生成」继续列出原失败 job 与每次新建的重试 job,保留完整审计记录。 当前结果区按 `current_job_ids` 的槽位顺序展示,避免新建重试 job 因数据库更新时间排序而跳到第一张。 ### 3. 区分重试与整轮生成反馈 单张失败重试期间: - 进度只统计本次重试的 1 张,不重新统计当前轮全部图片; - 不清空当前轮结果区,不切换成只有一张图片的新一轮; - 完成弹窗使用「图片重试完成」或「图片重试已停止」等中文文案; - 汇总显示本次重试成功、失败、停止数量,不使用「本轮共 1 张」误导用户这是第二轮套图; - 状态栏使用「图片重试成功 / 图片重试失败 / 图片重试已停止」; - 原整轮「商品套图生成完成」与停止反馈保持不变。 从「历史生成」发起重试时保持历史视图,避免自动跳到一个无法表达原历史轮次的临时“本轮”;新 job 会按历史排序显示。 ### 4. 操作边界 - 只有 `failed/expired/cancelled` 卡片显示的「重试」使用当前轮修复语义; - 成功图片右键「重新生成」继续视为主动新增变体,不在本任务中改成失败槽位替换; - 重试会重新调用 cmhub,可能再次扣点;不伪装为恢复原请求; - 不删除旧失败 job、旧错误、计费记录或已成功图片; - 不修改 cmhub 请求、并发上限、提示词、图片下载、套图数量和项目存储结构。 ## 验收要点 - [ ] 一轮 N 张中有 1 张失败,点击该卡片「重试」后,当前结果区仍保留其余 N-1 张成功图片。 - [ ] 新 job ID 到达后,原失败卡片在原位置被新重试卡片替换,当前结果区仍为 N 个槽位。 - [ ] 重试成功后当前结果区显示 N 张成功图片,不表现为只有 1 张图片的第二轮。 - [ ] 重试再次失败或停止时,该槽位显示最新状态,并可再次重试。 - [ ] 原失败 job 和新重试 job 都可在「历史生成」中查看。 - [ ] 本次重试的进度、停止、线程结束和终态看门狗只统计新 job,不重复统计当前轮其他成功 job。 - [ ] 从历史视图点击重试时保持历史视图,新 job 正常进入历史列表。 - [ ] 正常整轮生成、历史生成、手动停止、T-639 终态兜底和旧 `run_token` 隔离不回归。 - [ ] 用户可见文案全部为中文,不暴露接口路径、图片 URL、提示词全文或密钥。 ## 测试 - `tests/test_product_suite_gui.py`: - 当前轮失败卡片重试后保留其他成功 job; - 新 job 原位替换失败 job,结果顺序稳定; - 活动 job 快照只包含重试 job; - 重试完成使用独立中文汇总; - 历史视图发起重试时不自动退出历史; - 正常整轮生成仍重建当前结果。 - 运行: ```bash py -3.10 -m unittest tests.test_product_suite_gui 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,不新增 `retry_of_job_id` 或持久化轮次表。 - 不删除或覆盖原失败 job,不修改历史计费记录。 - 不修改 cmhub API、提交/轮询/下载协议和失败重试次数。 - 不修改套图提示词模板、分类数量、比例、原图管理或导出逻辑。 - 不修改 CDP、蝦皮主图拉取、①导入采集、②AI生成、③更新蝦皮、④账号管理或⑤设置。 ## 执行记录 - 2026-07-16:`SuiteTaskState` 新增内存态 `generation_job_ids`、`generation_mode` 与 `generation_retry_job_id`。`generation_job_ids` 专门供当前活动请求的进度、停止、线程结束和终态看门狗使用;`current_job_ids` 只表示当前结果区的有效槽位,解决单张重试时旧成功 job 被错误纳入本次汇总的问题。未修改 SQLite schema。 - 2026-07-16:失败、过期或停止卡片的「重试」改为显式单张重试模式。worker 仍新建 job 保留历史和计费记录;新 job ID 返回后会原位替换当前结果区中的旧失败 job,其他成功图片保持不变。当前结果改按 `current_job_ids` 顺序展示;历史视图继续展示原失败与每次重试记录,从历史发起重试时不会强制退出历史视图。 - 2026-07-16:重试运行状态、停止确认、进度文案和完成弹窗改为「图片重试」语义;重试成功、失败、停止和未完整结束分别给中文反馈。正常整轮生成、停止、T-639 `run_token` 隔离与终态兜底保持原行为。 - 2026-07-16:补齐当前轮原位替换、历史记录保留、活动快照只统计新 job、重试再次失败仍可继续重试、历史视图保持和真实 QThread 完整退出测试。相关 42 项测试通过;在不包含用户未提交默认提示词改名的临时隔离 worktree 中,全量 535 项 unittest 通过,Ruff、`compileall` 与 `git diff --check` 均通过。