diff --git a/docs/tasks/T-566.md b/docs/tasks/T-566.md new file mode 100644 index 0000000..b8cb62e --- /dev/null +++ b/docs/tasks/T-566.md @@ -0,0 +1,87 @@ +--- +id: T-566 +title: 封面历史归档数据层:重置改名保留旧图 + update_generated_cover + 目录枚举 +phase: 7 +deps: [T-564] +status: TODO +created: 2026-07-09 +--- + +## 问题 / 背景 + +运营需要对同一商品多次 AI 生图的结果做**对比挑选**(哪张更适合商品)。当前重置/重生封面时旧图被直接覆盖或删除(`reset_generated(delete_file=True)` 走 `os.remove`),历史生成图无法保留,也没有把 DB 封面指针改到某张历史图的手段。 + +设计定案(grill 评审收口): +- **不把多条路径塞进 `tasks.new_cover_path`**(一列多值反模式:所有读方按单路径消费、分隔符无安全选择、DB 与磁盘两个真相源必漂移)。`new_cover_path` 永远只存**当前生效的一张**。 +- **目录即数据库**:历史候选图只存在文件系统,靠文件名约定 `{task_id}_{item_id}_new_<时间戳>.jpg` 索引,弹窗打开时现场扫目录枚举。 +- 归档钩子放在**重置**(运营主动、非时间敏感),不放在生图写入热路径(避免在已扣点的成功路径上引入 Windows 文件锁/TOCTOU 新失败点)。 + +本任务是数据层前置;GUI 画廊弹窗见 T-567。 + +## 方案(改哪个文件、改成什么) + +### `app/db.py` — `reset_generated` 归档改名(替代删除) + +- `reset_generated(..., reset_cover=True)` 时,若 `new_cover_path` 指向的文件存在: + - 改名为 `{原名去扩展}_{YYYYMMDDHHMMSS}{ext}`,如 `42_51513423876_new.jpg` → `42_51513423876_new_20260709094800.jpg`; + - **撞名兜底**:目标名已存在(同秒重置两次)则追加 `_2`/`_3`; + - **顺序**:先 `os.rename` 成功、再清 DB 指针;改名抛 `PermissionError`(Windows 文件被预览/资源管理器占用)→ **整体中止本次重置**,抛 `DbError`(提示"请先关闭正在查看的封面图片再重置"),不留中间态(先清指针后改名失败会导致下次生成覆盖旧图、归档丢失); + - 旧文件不存在 → 无事发生,照常清指针(幂等,不报错)。 +- `delete_file=True` 的旧语义处理:归档取代删除(或保留 `delete_file` 为真删、新增 `archive_previous` 参数,默认对封面重置开启——具体参数形状实现时定,验收看行为)。 +- 仅重置标题(`reset_cover=False`)不触碰任何图片文件(既有行为不回归)。 + +### `app/db.py` — 新增 `update_generated_cover(task_id, new_cover_path)` + +- 校验任务存在、目标文件存在,目标路径必须是本地文件路径;供 T-567「保存」调用,把 `tasks.new_cover_path` 指向运营选中的那张(不复制、不改名文件)。 +- 状态语义必须明确: + - 写入后设置 `stage='generated'`、`status='pending'`、`last_error=NULL`、`updated_at=`,表示本地已有可用于③更新蝦皮的新封面; + - 不增加 `generate_attempts` / `apply_attempts`,因为这不是一次新的 AI 调用或线上提交; + - 不改 `committed`、`applied_at`、`apply_attempts`,保留“曾经提交过线上”的历史事实; + - 不改 `image_task_id` / `image_task_key`,它们只记录/续查 cmhub 异步生图任务,封面指针切换不应误清审计信息;真正重置封面仍由 `reset_generated(..., reset_cover=True)` 清空。 +- 对已 `committed=1` 或 `stage='applied'` 的任务,DB 层允许写入并按上面规则退回 `generated/pending`,由 GUI 在保存前弹“本地换封面不会回滚蝦皮,重复更新会再次提交线上”确认。 +- **有意的不对称,勿"顺手对齐"**:`update_generated_title`(`db.py:819`)对 committed/applied 任务在 DB 层直接拒绝,而本函数允许(退回 + GUI 警示)。这是设计决定:换封面重挑是运营核心场景、走「重置更新状态」同款警示口径(`apply.py:311/334`);改标题重提交没有此需求。后续不要参照 title 的守卫给本函数加拒绝逻辑,否则 committed 记录重挑封面功能会被改死。 + +### 批次软删除边界:不做归档图片清理 + +- 不修改 `delete_batch()` 的软删除语义,不删除 `//` 目录。 +- 理由:T-206 已定“删除批次”为软删除,只隐藏本地批次,不物理删除任务与图片;把本任务改成删目录会造成用户误删历史 AI 图和旧封面备份。 +- 归档图片清理如后续需要,另做独立“清理已软删除批次图片”工具,必须有明确二次确认和删除清单预览。 + +### `app/image_paths.py` — 新增画廊枚举函数 + +- `list_task_cover_candidates(image_root, task, account=None)`:扫 `///` 下匹配 `{task_id}_{item_id}_new*.jpg` 的文件,返回绝对路径列表;只包含新封面候选,不含 `_old` 旧封面。 +- 排序规则必须稳定、可测试: + - 最新优先; + - 规范名 `{task_id}_{item_id}_new.jpg` 视为“当前最新输出槽”,排在同任务候选最前; + - 归档名 `{task_id}_{item_id}_new_YYYYMMDDHHMMSS.jpg` 按时间戳倒序; + - 同秒撞名后缀 `_2` / `_3` 按数字倒序; + - 无法解析时间戳的匹配文件排在最后,并按文件名升序兜底。 +- GUI 默认选中由 DB 当前 `new_cover_path` 决定,不依赖排序;排序只影响画廊展示顺序。 +- 只读、不建表、不加列——文件系统是唯一真相源。 + +## 验收要点 + +- 重置封面且旧图存在 → 旧图改名为带 `YYYYMMDDHHMMSS` 后缀、原规范名腾出、DB 指针已清;同秒二次重置 → 追加 `_2` 不覆盖。 +- 改名 `PermissionError` → 本次重置整体失败、DB 指针未清、原文件原名保留(断言不留中间态)。 +- 旧图不存在 → 重置照常成功(幂等)。 +- 仅重置标题 → 不触碰图片文件。 +- `update_generated_cover` 写入后 `stage=generated/status=pending/last_error=NULL`;不增加 attempts,不改 committed/applied_at/apply_attempts/image_task_*;目标文件不存在 → 报错。 +- `delete_batch` 仍只做软删除,不删除 `/` 图片目录;归档图片继续保留。 +- `list_task_cover_candidates` 返回全部 `_new*` 候选、不含 `_old`,排序符合“规范名优先、归档时间倒序、撞名数字倒序、异常文件名兜底”的规则。 +- 验证命令(unittest,不引入 pytest): + - `py -3.10 -m unittest tests.test_db tests.test_image_paths` + - `python -m ruff check app tests main.py` + - `py -3.10 -m unittest discover -s tests` + - `git diff --check` + +## 边界(不改什么) + +- 不改 `tasks` 表 schema(不加列、不建子表);`new_cover_path` 仍为单路径。 +- 不改生图写入链(`_save_jpeg`/`persist_cover_success`/规范名 `..._new.jpg`);归档只钩在重置。 +- 不改 ③更新/Excel 回写对 `new_cover_path` 的消费方式。 +- 不做 GUI(画廊弹窗在 T-567)。 +- 不改 CDP/Shopee 交互、AI 请求链路。 + +## 执行记录 + +(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。) diff --git a/docs/tasks/T-567.md b/docs/tasks/T-567.md new file mode 100644 index 0000000..648032d --- /dev/null +++ b/docs/tasks/T-567.md @@ -0,0 +1,84 @@ +--- +id: T-567 +title: ②封面画廊基础:候选图查看、单选保存、原图弹窗 +phase: 7 +deps: [T-566] +status: TODO +created: 2026-07-09 +--- + +## 问题 / 背景 + +运营需要在软件内对比同一商品多次 AI 生图的候选,并把其中一张设为当前生效封面。当前②列表双击打开的「封面对照」弹窗只显示旧封面 + 当前新封面各一张,无法看到历史候选,也不能保存选中的历史图。 + +T-566 已提供数据层能力:重置封面时归档旧图、`db.update_generated_cover()` 切换当前封面指针、`image_paths.list_task_cover_candidates()` 枚举候选图。本任务只做 GUI 第一阶段:画廊查看 + 单选保存 + 原图弹窗。 + +## 方案(改哪个文件、改成什么) + +### `app/gui/tabs/generate.py` — 重构封面对照弹窗 + +- 将现有 `show_task_images` 改为封面画廊弹窗(可拆成独立 `QDialog` 类,如 `CoverGalleryDialog`)。 +- 布局: + - 左侧保留「旧封面」参照,推荐缩略图约 `300x300`; + - 右侧为候选画廊:横向 `QScrollArea`,每个候选项包含缩略图、`QRadioButton`、时间/当前标记; + - 底部按钮第一阶段只保留「保存」「取消」;不做「重新生成」「上一条/下一条」。 +- 尺寸: + - 默认 `resize(min(960x580, 屏幕 availableGeometry 的 90%))`; + - 不用 `setFixedSize()`,拉伸增量给画廊区域; + - 小屏下窗口标题栏必须完整可见。 + +### 候选加载与默认选中 + +- 打开弹窗时调用 T-566 `list_task_cover_candidates(image_root, task, account)` 现场扫目录。 +- 默认选中 = DB 当前 `task.new_cover_path` 指向的文件;不是无条件选最新。 +- 如果 DB 当前路径不存在但候选列表不空: + - 默认不自动改库; + - 画廊显示候选但不预选,状态 label 提示“当前生效封面文件不存在,请选择一张后保存”。 +- 如果无候选图: + - 右侧显示空状态“暂无生成封面图片”; + - 「保存」置灰。 +- 缩略图加载必须用 `QImage` 读入内存后转 `QPixmap`,不长期持有文件句柄,避免后续重置/重生时 Windows 文件锁影响归档改名。 + +### 保存 + +- 选中项与 DB 当前 `new_cover_path` 相同:直接关闭。 +- 选中项不同:调用 `db.update_generated_cover(task_id, selected_path)`;刷新②列表该行;关闭弹窗。 +- 若该任务 `committed=1` 或 `stage='applied'`,保存前必须弹确认: + - 文案说明“本地换封面不会回滚蝦皮,重复更新会再次提交线上”; + - 用户取消则不写库、不关闭。 +- 保存失败(文件不存在、DB 错误等)应弹中文错误并保留弹窗,不吞异常。 + +### 原图弹窗 + +- 双击任一候选缩略图打开原图弹窗: + - `QDialog + QScrollArea` 展示原始尺寸图片; + - 窗口最大不超过屏幕 availableGeometry 的 90%,超出可滚动; + - 标题显示图片文件名和原始分辨率,如 `1024x1024`。 + +## 验收要点 + +- 双击②任务列表仍能打开封面查看入口;旧封面仍作为参照显示。 +- 画廊列出该任务全部 `_new*` 候选,不显示 `_old`。 +- 默认选中 DB 当前 `new_cover_path`;DB 当前路径缺失时不自动改库。 +- 选择另一张并保存会调用 `db.update_generated_cover()`,②列表新封面路径刷新;未变更直接关闭。 +- 已提交线上任务保存前弹二次确认;取消不写库。 +- 双击缩略图可打开原图弹窗,超出屏幕可滚动,标题含分辨率。 +- 缩略图加载不持文件句柄;Windows 下打开画廊后仍可重命名候选文件。 +- 验证命令(unittest,不引入 pytest): + - `py -3.10 -m unittest tests.test_gui tests.test_db tests.test_image_paths` + - `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` + +## 边界(不改什么) + +- 不做单任务重新生成(T-568)。 +- 不做上一条/下一条切换和未保存三态拦截(T-569)。 +- 不改 T-566 的数据层契约。 +- 不改 `GenerateWorker`、`generate_batch`、cmhub 请求链路。 +- 不改 ③更新/Excel 回写/CDP/Shopee。 + +## 执行记录 + +(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。) diff --git a/docs/tasks/T-568.md b/docs/tasks/T-568.md new file mode 100644 index 0000000..2fa8a19 --- /dev/null +++ b/docs/tasks/T-568.md @@ -0,0 +1,76 @@ +--- +id: T-568 +title: ②封面画廊单任务重新生成与并发守卫 +phase: 7 +deps: [T-567] +status: TODO +created: 2026-07-09 +--- + +## 问题 / 背景 + +T-567 只提供候选封面查看与保存。运营在对比时仍需要对当前商品单独再生成一张新封面,并在画廊中立即看到新候选。该能力涉及 AI 点数、DB 归档、任务状态和后台 worker 并发,必须单独拆出,避免 T-567 弹窗基础功能过大。 + +## 方案(改哪个文件、改成什么) + +### 重新生成按钮 + +- 在 T-567 的封面画廊弹窗底部新增「重新生成」按钮。 +- 点击后弹确认框: + - 文案说明将重新生成当前商品封面,可能消耗 cmhub 点数; + - 用户取消则无副作用。 +- 用户确认后: + 1. 调用 `db.reset_generated(task_id, reset_title=False, reset_cover=True)`,由 T-566 归档当前 `new_cover_path` 并清空封面指针; + 2. reset 成功后必须重新 `db.get_task(task_id)` 获取最新任务对象,不能复用弹窗打开时的旧 task(旧对象可能还带旧 `new_cover_path`,导致 `generate_batch()` 判断无需补封面); + 3. 启动单任务 `GenerateWorker([fresh_task])`,沿用当前②页面的提示词、cmhub配置、生成参数和“生成封面图片”语义,但本任务强制只补当前任务缺失封面,不重生标题; + 4. 弹窗左下状态 label 显示“开始重新生成商品 的封面图片”。 + +### 完成/失败刷新 + +- 单任务生成完成: + - 刷新②列表该行; + - 重新调用 `list_task_cover_candidates()` 刷新画廊; + - 新生成的规范名 `_new.jpg` 应成为 DB 当前 `new_cover_path`,画廊默认选中它; + - 状态 label 显示“封面重新生成完成”。 +- 失败: + - 保留已归档旧图; + - 列表按既有生成失败口径显示; + - 状态 label 显示失败摘要; + - 画廊继续显示历史候选,允许用户选择历史图保存。 +- 若 `reset_generated()` 因 Windows 文件占用导致归档改名失败: + - 中止重新生成; + - 不启动 `GenerateWorker`; + - 弹中文提示“请先关闭正在查看的封面图片再重试”。 + +### 并发守卫 + +- ②批量生成运行中时,画廊「重新生成」按钮禁用,并显示 tooltip。 +- 画廊单任务重新生成运行中时,②主界面「开始生成」按钮也应禁用或阻断,避免两个 `GenerateWorker` 同时写同一 DB/图片目录。 +- 同一画廊弹窗运行中再次点击「重新生成」无效。 +- 本任务不新增 cmhub cancel 接口;关闭弹窗时若单任务仍在运行,只允许隐藏/关闭视图,不强杀服务端任务,遵守 T-564 续查语义。 + +## 验收要点 + +- 重新生成确认取消 → 不归档、不启动 worker、不改 DB。 +- 确认后先归档旧图,再用 fresh task 启动单任务生成;不会因旧 task 对象残留 `new_cover_path` 而跳过封面生成。 +- 成功后画廊出现新图并默认选中;②列表刷新为新封面路径。 +- 失败后历史候选仍可选择保存。 +- 归档改名失败时不清 DB 指针、不启动 worker。 +- 批量生成和单任务重生互斥,不允许两个 GenerateWorker 并行。 +- 验证命令: + - `py -3.10 -m unittest tests.test_gui tests.test_db` + - `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` + +## 边界(不改什么) + +- 不改 T-566 数据层契约。 +- 不改 cmhub submit/poll/download 逻辑。 +- 不做上一条/下一条切换和未保存拦截(T-569)。 +- 不改 ③更新/Excel/CDP/Shopee。 + +## 执行记录 + +(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。) diff --git a/docs/tasks/T-569.md b/docs/tasks/T-569.md new file mode 100644 index 0000000..6ec9af4 --- /dev/null +++ b/docs/tasks/T-569.md @@ -0,0 +1,69 @@ +--- +id: T-569 +title: ②封面画廊上下条切换与未保存选择拦截 +phase: 7 +deps: [T-567] +status: TODO +created: 2026-07-09 +--- + +## 问题 / 背景 + +T-567 的封面画廊只处理当前单条任务。运营批量检查时需要在弹窗内连续查看上一条/下一条商品,而不是关闭弹窗后回到列表再双击。切换记录时,如果当前画廊已经勾选了另一张候选但尚未保存,必须避免静默丢失选择。 + +## 方案(改哪个文件、改成什么) + +### 上一条/下一条按钮 + +- 在 T-567 封面画廊弹窗底部增加「上一条」「下一条」。 +- 目标任务来自②当前表格模型的相邻可见行,必须尊重当前筛选结果和排序。 +- 第一条时「上一条」置灰;最后一条时「下一条」置灰。 +- 切换记录时只刷新弹窗内容区和内部当前 task,不重建整个窗口;窗口大小和位置保持不变。 + +### 键盘切换 + +- 支持 `↑` / `↓` 键切换上一条/下一条,和按钮走同一个切换函数。 +- 画廊内 `QRadioButton` 设置 `ClickFocus` 或 `NoFocus`,避免方向键在 radio 组内改变候选选择而不是切换记录。 +- 输入框/按钮等获得焦点时如不适合消费方向键,应保持 Qt 默认行为;不为键盘切换破坏可访问性。 + +### 未保存选择拦截 + +- 判断条件:当前 radio 选中路径 != DB 当前 `task.new_cover_path`。 +- 在按钮切换、方向键切换、关闭弹窗前都必须进入同一套拦截流程。 +- 弹三态确认框,按钮必须是中文: + - 「保存」:调用 `db.update_generated_cover()`;如任务已提交线上,先弹 T-567 同款“本地换封面不会回滚蝦皮,重复更新会再次提交线上”确认;保存成功后继续切换/关闭。 + - 「不保存」:丢弃当前勾选,继续切换/关闭。 + - 「取消」:留在当前任务,不切换、不关闭。 +- 保存失败时保留当前任务和弹窗,显示中文错误。 + +### 与 T-568 的关系 + +- 如果 T-568 已完成且当前任务正在单任务重生: + - 可允许切换到其它任务,但切回时必须刷新最新 DB/候选; + - 当前任务运行中时画廊显示“生成中”状态,不显示旧 `new_cover_path` 为当前有效图误导用户。 +- 如果 T-568 尚未完成,本任务只需保证 T-567 基础画廊切换逻辑正确。 + +## 验收要点 + +- 上一条/下一条按钮按当前②可见列表相邻行切换;边界按钮置灰。 +- `↑` / `↓` 与按钮共用切换逻辑;方向键不会改变 radio 选中。 +- 切换记录不改变弹窗尺寸和屏幕位置。 +- 当前有未保存选择时,按钮切换、键盘切换、关闭弹窗都会弹中文三态确认。 +- 「保存 / 不保存 / 取消」三种路径行为正确;保存失败不切换。 +- 已提交线上任务保存前仍弹二次警示。 +- 验证命令: + - `py -3.10 -m unittest tests.test_gui` + - `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` + +## 边界(不改什么) + +- 不改 T-566 数据层契约。 +- 不改 T-568 单任务重新生成逻辑。 +- 不改 cmhub/AI 请求、DB schema、Excel、③更新、CDP/Shopee。 + +## 执行记录 + +(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。)