Files
cmbot/tasks.md
T

101 KiB
Raw Blame History

开发任务清单

任务文档说明

本文档用于跟踪自动合成印花服饰效果图工具的整体开发任务。后续 AI 或开发者领取任务前,必须先阅读对应任务的前置文档,并严格遵守 docs/04-development-rules.md。

任务状态说明:

  • [x] 已完成。
  • [ ] 未开始。
  • [~] 进行中或部分完成。

任务执行顺序

任务编号即建议开发顺序,后续任务依赖前序任务的产出:

1(基础服务)→ 2(数据模型)→ 3/4(文件/模板服务)
→ 5(图片合成核心)→ 6(批量任务核心)→ 6.5(低对比组合筛选核心)→ 7(主界面布局)→ 8-13(各 UI 面板)→ 14(微调状态)→ 15(测试)→ 16(打包)

任务 1-6 完成前不应开始任务 7 及以后的 UI 任务,否则 UI 层将缺少可依赖的数据模型和服务层。

执行每个「完善 xxx.py」类任务前,必须先读取该文件的当前内容,了解骨架代码现状,再决定新增或修改哪些内容,避免覆盖已有实现。

0. 已完成基础工作

  • 编写项目愿景文档:docs/01-product-vision.md
  • 编写 PRD:docs/02-prd.md
  • 编写技术栈文档:docs/03-technical-stack.md
  • 编写开发规则文档:docs/04-development-rules.md
  • 编写项目架构文档:docs/05-project-architecture.md
  • 编写 UI 效果图提示词:docs/06-ui-mockup-prompt.md
  • 整理 Claude UI 提示词版本:docs/06-ui-mockup-prompt-claude.md
  • 保存 UI 效果图设计稿:docs/ui-v1.html
  • 保存 UI 效果图截图:docs/ui-v1.png
  • 编写 UI 设计文档:docs/07-ui-design.md
  • 编写图像编辑器设计文档:docs/08-image-editor-design.md
  • 编写本地打包发布文档:docs/09-packaging-release.md
  • 创建 requirements.txt
  • 创建 src/version.py,统一维护 APP_NAME 和 APP_VERSION
  • 创建 src/main.py 最小 PySide6 入口
  • 创建 src/app/main_window.py 最小主窗口
  • 创建 src/app/widgets/ 占位模块
  • 创建 src/core/ 占位模块
  • 创建 src/services/ 占位模块
  • 创建 src/resources/ 资源目录
  • 创建 tests/ 测试目录
  • 验证本机 Python 版本为 3.7.9
  • 验证 PySide6 版本为 6.5.3
  • 通过 python -m py_compile 语法检查
  • 验证最小 PySide6 窗口可以创建并关闭
  • 已提交文档规划代码
  • 已提交 PySide6 工程骨架代码

1. 工程基础完善

1.1 日志服务

前置阅读:

  • docs/04-development-rules.md
  • docs/05-project-architecture.md

任务:

  • 先读取 src/services/log_service.py 现有内容
  • 完善 src/services/log_service.py
  • 启动时创建 logs/ 目录
  • 日志文件按启动时间或日期命名
  • 日志格式包含时间、模块、级别、消息
  • 在 src/main.py 中初始化日志

验收:

  • 启动程序后生成日志文件
  • 程序启动信息写入日志
  • 不使用 print 作为正式日志

1.2 路径与资源服务

前置阅读:

  • docs/04-development-rules.md
  • docs/05-project-architecture.md
  • docs/09-packaging-release.md

任务:

  • 在 src/services/file_service.py 中新增路径辅助函数(不单独建模块)
  • 实现 get_app_dir():返回程序根目录,兼容开发环境和 PyInstaller 打包环境
  • 实现 get_resource_path(relative_path):返回资源文件绝对路径
  • 实现 get_config_path(relative_path):返回配置文件绝对路径
  • 实现 get_log_dir():返回日志目录路径
  • 实现 get_output_dir():返回默认输出目录路径
  • 保证 Windows 中文路径可用

验收:

  • 路径函数不使用开发机绝对路径
  • 程序目录下缺少 logs/ 或 output/ 时可自动创建

1.3 配置服务

前置阅读:

  • docs/04-development-rules.md
  • docs/05-project-architecture.md

任务:

  • 先读取 src/services/config_service.py 现有内容
  • 完善 src/services/config_service.py
  • 定义默认配置
  • 支持读取 JSON 配置
  • 支持保存 JSON 配置
  • 配置损坏时使用安全默认值并记录日志

验收:

  • 缺少配置文件时程序可启动
  • 配置文件损坏时程序可启动并记录日志
  • 不会无提示清空用户配置

2. 核心数据模型

前置阅读:

  • docs/02-prd.md
  • docs/04-development-rules.md
  • docs/05-project-architecture.md
  • docs/08-image-editor-design.md

任务:

  • 先读取 src/core/models.py 现有内容
  • 完善 src/core/models.py
  • 定义 ImageAsset
  • 定义 TransformState
  • 定义 Template
  • 定义 ExportOptions
  • 定义 BatchOptions
  • 定义 ComposeResult
  • 定义 BatchResult

验收:

  • models.py 不依赖 PySide6 UI 控件
  • 变换状态包含 x/y/width/height/rotation/keep_aspect_ratio
  • 模型可以被 core、services 和 UI 层复用

3. 文件扫描与素材导入

前置阅读:

  • docs/02-prd.md
  • docs/04-development-rules.md
  • docs/05-project-architecture.md
  • docs/07-ui-design.md

任务:

  • 先读取 src/services/file_service.py 现有内容
  • 完善 src/services/file_service.py
  • 支持递归扫描衣服图片文件夹
  • 支持递归扫描印花图片文件夹
  • 支持格式:PNG、JPG、JPEG、WEBP
  • 忽略不支持格式并记录日志
  • 保留来源子文件夹信息
  • 生成安全输出文件名

验收:

  • 可以扫描包含子文件夹的中文路径
  • 不修改原始素材文件
  • 不支持文件格式不会导致扫描失败

4. 模板服务

前置阅读:

  • docs/02-prd.md
  • docs/04-development-rules.md
  • docs/05-project-architecture.md

任务:

  • 先读取 src/services/template_service.py 现有内容
  • 完善 src/services/template_service.py
  • 定义内置模板
  • 支持读取自定义模板 JSON
  • 支持保存自定义模板 JSON
  • 支持新增、重命名、删除自定义模板
  • 校验模板字段

验收:

  • 内置模板和自定义模板可区分
  • 单个模板损坏不影响全部模板加载
  • UI 不直接操作模板 JSON 文件

5. 图片合成核心

前置阅读:

  • docs/02-prd.md
  • docs/04-development-rules.md
  • docs/05-project-architecture.md
  • docs/08-image-editor-design.md

任务:

  • 先读取 src/core/composer.py 现有内容
  • 完善 src/core/composer.py
  • 使用 Pillow 读取衣服底图和印花图
  • 支持透明 PNG alpha 合成
  • 支持按 TransformState 缩放印花
  • 支持按 TransformState 旋转印花
  • 支持旋转后按中心点对齐粘贴
  • 支持 PNG 导出
  • 支持 JPG 导出
  • 输出文件名避免默认覆盖

验收:

  • 合成逻辑不依赖 PySide6 UI 控件
  • 输出尺寸默认与衣服底图一致
  • 透明 PNG 不出现黑底或白底
  • 印花部分超出画布时不会报错

6. 批量任务核心

前置阅读:

  • docs/02-prd.md
  • docs/04-development-rules.md
  • docs/05-project-architecture.md

任务:

  • 先读取 src/core/batch.py 现有内容
  • 完善 src/core/batch.py
  • 支持多衣服 × 单印花
  • 支持单衣服 × 多印花
  • 支持一一匹配
  • 支持全组合
  • 复用 core/composer.py
  • 单个任务失败时继续处理剩余任务
  • 汇总成功数量、失败数量和失败原因

验收:

  • 不复制第二套合成算法
  • 单张失败不会中断整个批量任务
  • 失败原因可供 UI 和日志使用

6.5 低对比组合筛选核心

前置阅读:

  • docs/02-prd.md
  • docs/04-development-rules.md
  • docs/05-project-architecture.md
  • docs/08-image-editor-design.md

任务:

  • 定义可见度状态:正常、偏低、不明显、无法判断
  • 新增低对比分析核心函数,位置应符合架构分层,不能写进 UI 事件
  • 基于当前模板或 TransformState 获取衣服目标区域
  • 只分析衣服目标区域颜色,不使用整张衣服图判断
  • 只统计印花 alpha 有效像素,例如 alpha > 20
  • 计算 RGB 颜色距离
  • 计算亮度差
  • 根据阈值输出可见度状态和评分
  • 单个组合分析失败时返回 无法判断,不得中断整个队列
  • 为后续队列项保存可见度结果预留字段或结果结构

验收:

  • 白色衣服配浅色印花时可标记为 偏低 或 不明显
  • 深色衣服配深色印花时可标记为 偏低 或 不明显
  • 透明 PNG 的透明区域不参与印花颜色判断
  • 低对比分析不修改原始衣服图片和原始印花图片
  • 分析逻辑不依赖 PySide6 UI 控件
  • 失败项不会中断批量队列生成或导出流程

7. 主界面布局

前置阅读:

  • docs/04-development-rules.md
  • docs/05-project-architecture.md
  • docs/07-ui-design.md

任务:

  • 先读取 src/app/main_window.py 现有内容
  • 将 src/app/main_window.py 从最小窗口扩展为主布局
  • 实现顶部标题和版本号(后移除内容区自绘标题栏:与系统窗口标题重复,名称/版本改由系统标题栏显示,见 17.7)
  • 实现流程页签区域
  • 实现左侧素材栏容器
  • 实现中间预览区容器
  • 实现右侧参数栏容器
  • 实现底部合成队列容器
  • 实现底部状态栏

验收:

  • 窗口标题显示 APP_NAME 和 APP_VERSION
  • UI 布局接近 docs/ui-v1.png
  • 不把所有业务逻辑塞进 main_window.py

8. 素材列表 UI

前置阅读:

  • docs/02-prd.md
  • docs/05-project-architecture.md
  • docs/07-ui-design.md

任务:

  • 先读取 src/app/widgets/image_list_panel.py 现有内容
  • 实现衣服图片列表
  • 实现印花图片列表
  • 实现打开文件夹按钮
  • 实现全选和取消全选
  • 实现单张勾选和取消勾选
  • 显示已选数量和总数量(例如:含子文件夹 · 共 12)
  • 显示来源子文件夹标签和文件名
  • 实现排序选择器(默认按文件夹+名称排序)
  • 点击素材后更新当前预览选择

验收:

  • 支持递归导入素材
  • 当前预览选中态和批量勾选态可区分
  • 不在 UI 层重复实现文件扫描规则

9. 图像预览编辑器

前置阅读:

  • docs/04-development-rules.md
  • docs/05-project-architecture.md
  • docs/07-ui-design.md
  • docs/08-image-editor-design.md

任务:

  • 先读取 src/app/widgets/image_canvas.py 现有内容
  • 使用 QGraphicsView / QGraphicsScene
  • 显示衣服底图(scene 坐标与原图像素坐标保持一致)
  • 显示印花图层
  • 实现印花拖动
  • 实现缩放控制点(至少四角)
  • 实现旋转控制点(顶部中心外侧)
  • 实现预览缩放(通过 QGraphicsView view transform,不修改 scene 内容)
  • 实现预览顶部栏:显示当前底图文件名、印花文件名、操作模式按钮(移动/缩放/旋转)、缩放比例控制
  • 实现画布操作提示 Overlay(拖动移动 / 四角缩放 / 顶部旋转 / Shift 锁比例)
  • 将交互结果同步为 TransformState

验收:

  • 预览缩放不改变真实合成参数
  • 拖动后坐标为衣服原图像素坐标
  • 缩放后宽高同步
  • 旋转后角度同步
  • 不使用截图作为导出结果

10. 参数面板 UI

前置阅读:

  • docs/05-project-architecture.md
  • docs/07-ui-design.md
  • docs/08-image-editor-design.md

任务:

  • 先读取 src/app/widgets/transform_panel.py 现有内容
  • 完善 src/app/widgets/transform_panel.py
  • 实现 X/Y 坐标输入
  • 实现宽度/高度输入
  • 实现锁定宽高比例
  • 实现角度输入
  • 实现左旋 90 度和右旋 90 度按钮
  • 与 ImageCanvas 双向同步 TransformState

验收:

  • 参数输入后预览同步更新
  • 预览交互后参数输入框同步更新
  • 程序化更新输入框不会造成信号循环

11. 模板面板 UI

前置阅读:

  • docs/02-prd.md
  • docs/05-project-architecture.md
  • docs/07-ui-design.md

任务:

  • 先读取 src/app/widgets/template_panel.py 现有内容
  • 完善 src/app/widgets/template_panel.py
  • 显示模板下拉框
  • 支持选择模板
  • 支持保存模板
  • 支持另存为模板
  • 支持重置为模板
  • 通过 template_service 操作模板

验收:

  • 选择模板后更新预览和参数面板
  • UI 不直接修改模板 JSON
  • 自定义模板重启后仍可用

12. 导出面板 UI

前置阅读:

  • docs/05-project-architecture.md
  • docs/07-ui-design.md
  • docs/09-packaging-release.md

任务:

  • 先读取 src/app/widgets/export_panel.py 现有内容
  • 完善 src/app/widgets/export_panel.py
  • 实现输出目录选择
  • 实现输出格式选择
  • 实现质量选择
  • 实现导出当前单张
  • 实现应用为模板入口(后移除:与模板区域「另存为」重复,见 17.3)
  • 实现重置为模板入口(后移除:与模板区域「重置为模板」重复,见 17.3)
  • 显示当前操作影响范围提示

验收:

  • 缺少衣服或印花时导出按钮禁用
  • 输出目录不可写时提示用户并记录日志
  • 单张导出调用 core/composer.py

13. 合成队列 UI

前置阅读:

  • docs/02-prd.md
  • docs/05-project-architecture.md
  • docs/07-ui-design.md

任务:

  • 实现底部合成队列表格(建议使用 QTableView + 模型视图结构)
  • 实现队列头部批量模式切换:多衣服×单印花 / 单衣服×多印花 / 一一匹配 / 全组合(矩阵)
  • 实现队列统计显示(共 N 项 · 完成 · 进行 · 失败 · 待导出)
  • 实现「重置全部」按钮
  • 实现「导出选中」按钮
  • 实现「开始批量导出」按钮(导出中切换为停止/暂停)
  • 实现队列折叠/展开
  • 显示任务状态(已完成 / 待导出 / 导出中 / 失败)
  • 导出中任务显示进度条
  • 显示失败原因
  • 支持选中队列项并更新预览区和参数面板

验收:

  • 队列统计显示总数、完成、进行、失败、待导出
  • 导出中任务显示进度条
  • 失败行显示可理解原因
  • 选中队列项可以加载对应状态

14. 微调状态

前置阅读:

  • docs/07-ui-design.md
  • docs/08-image-editor-design.md

任务:

  • 定义队列项微调状态
  • 手动调整队列项后标记为 已微调
  • 微调项保存独立 TransformState
  • 批量导出时微调项使用自己的参数
  • 未微调项使用模板参数

验收:

  • 已微调项不会被无提示覆盖
  • 队列表格能显示 模板 和 已微调

15. 测试

前置阅读:

  • docs/04-development-rules.md
  • docs/05-project-architecture.md
  • docs/08-image-editor-design.md

任务:

  • 编写 tests/test_composer.py
  • 编写 tests/test_templates.py
  • 测试透明 PNG 合成
  • 测试缩放参数
  • 测试旋转中心
  • 测试文件扫描
  • 测试模板读写
  • 测试批量任务生成

验收:

  • 核心逻辑测试不依赖 GUI
  • 测试可在 Python 3.7 环境运行

16. 打包

前置阅读:

  • docs/03-technical-stack.md
  • docs/04-development-rules.md
  • docs/09-packaging-release.md

任务:

  • 创建打包脚本
  • 验证 PyInstaller onedir 打包
  • 打包资源文件
  • 打包默认配置和模板
  • 验证打包后程序启动
  • 验证标题栏版本号
  • 验证单张导出

验收:

  • 打包产物可在无 Python 环境的 Windows 电脑启动
  • 发布包不包含源代码、缓存、测试输出和私人配置

17. 体验优化(持久化与独立重置)

第二阶段根据实际使用反馈新增。

17.1 用户偏好持久化

前置阅读:

  • docs/02-prd.md(6.6 模板选择、6.9 批量合成导出、9 数据与文件结构)
  • docs/05-project-architecture.md(4.12 config_service)

任务:

  • 先读取 src/services/config_service.py 与 src/app/main_window.py 现有内容
  • 在 main_window 启动时调用 load_config() 并把初值分发给各面板(集中管理,不让控件各自读写)
  • 在 config_service 默认配置中新增 last_template、last_batch_mode
  • 记住并在启动时恢复上次选择的模板;自定义模板已删除时回退到第一个内置模板
  • 记住并在启动时恢复上次选择的批量模式;持久化值非法时回退默认(全组合)
  • 面板选择变化时「改一次存一次」回写配置
  • template_panel 增加模板选择变化信号(携带模板名)
  • queue_panel 增加 set_batch_mode() 与批量模式变化信号

验收:

  • 重启后自动恢复上次选择的模板和批量模式
  • 自定义模板被删除或批量模式值非法时安全回退,不报错
  • UI 控件不直接读写配置文件

17.2 位置 / 尺寸 / 旋转独立重置

前置阅读:

  • docs/07-ui-design.md(7 右侧参数栏、10 视觉规范)
  • docs/08-image-editor-design.md

任务:

  • 先读取 src/app/widgets/transform_panel.py 现有内容
  • 在 位置 / 尺寸 / 旋转 三个分区标题右侧各加一个独立 重置 按钮
  • 各重置只把对应参数还原为当前选中模板的值,互不影响(位置→仅 X/Y;尺寸→仅宽/高,位置不变;旋转→仅角度)
  • 移除合并的 重置位置 / 尺寸 / 角度 按钮与角度旁的 归零 按钮
  • 重置按钮采用幽灵样式(静止灰、悬停蓝、按下深蓝、禁用更灰),不使用红色
  • 未选模板或未加载衣服/印花时三个按钮置灰
  • 由 main_window 取模板换算值并按项合并进当前 TransformState

验收:

  • 位置、尺寸、旋转可分别独立重置,互不影响
  • 尺寸重置只改变宽 / 高,不改变位置 X / Y
  • 按钮视觉层级符合 docs/07-ui-design.md 第 10 节

17.3 按钮去重(导出面板模板入口)

前置阅读:

  • docs/07-ui-design.md(7.1 模板区域、7.7 操作按钮)

任务:

  • 移除导出面板的 应用为模板 与 重置为模板 按钮(与模板区域的 另存为 / 重置为模板 是同一动作的重复入口)
  • 模板相关操作统一留在模板区域,导出面板只负责输出与导出

验收:

  • 同一个动作在界面上只出现一次,命名一致
  • 导出面板仅保留输出设置与 导出当前单张

17.4 「重置为模板」归位并更名为「位置尺寸旋转重置为当前模板」

前置阅读:

  • docs/07-ui-design.md(7 右侧参数栏、10 视觉规范)

任务:

  • 把模板区域的 重置为模板 按钮移出,移到参数栏底部(旋转分区下方)
  • 更名为 位置尺寸旋转重置为当前模板,与三个分项 ↺ 重置 构成「全部 + 分项」两级
  • 还原逻辑改为把位置/尺寸/旋转整体设为模板值,并与分项重置同走 _apply_preview_transform
  • 与三个分项重置一起受「未加载图片时置灰」控制
  • 采用安静、全宽、悬停出强调色的样式(不使用红色)

验收:

  • 模板区域只剩 保存 / 另存为,不含整体重置
  • 位置尺寸旋转重置为当前模板 在旋转分区下方,未加载图片时置灰
  • 点击后位置、尺寸、旋转一并还原为当前选中模板

17.5 模板删除按钮

前置阅读:

  • docs/07-ui-design.md(7.1 模板区域)

任务:

  • 在模板区域增加 删除 按钮(复用 template_service.delete_template)
  • 删除 仅对自定义模板可用,选中内置模板时隐藏(与 保存 一致)
  • 删除前弹二次确认对话框;确认后才删除
  • 删除后刷新下拉框并回选第一个内置模板,同步持久化选择
  • 删除 用红色文字提示危险但保持安静样式

验收:

  • 可在界面上删除自定义模板,无需手改 templates.json
  • 内置模板不显示 删除
  • 删除有二次确认,取消则不删

17.6 参数/导出面板紧凑化与命名

前置阅读:

  • docs/07-ui-design.md(7.2 位置区域、7.3 尺寸区域、7.5 输出设置区域)

任务:

  • 「位置」的 X、Y 输入框放在同一行(标签简化为 X / Y)
  • 「尺寸」的宽、高输入框放在同一行(标签简化为 宽 / 高),锁定宽高比例 在其下方
  • 导出面板「格式」「质量」放在同一行
  • 底部整体重置按钮更名为 位置尺寸旋转重置为当前模板
  • 仅改布局/文案,不动同步与导出逻辑

验收:

  • X/Y 一行、宽/高一行、格式/质量一行,界面更紧凑
  • 拖动/缩放预览后输入框仍正常同步
  • 质量仍仅在 JPG 时可用

17.7 移除重复的自绘标题栏与无效「设置」按钮

前置阅读:

  • docs/07-ui-design.md(3 整体布局、4.1 标题栏)

任务:

  • 移除标题栏里点了无反应的占位「设置」按钮
  • 移除内容区自绘标题栏(与系统窗口标题重复),名称/版本由系统标题栏(setWindowTitle)显示
  • 同步清理相关样式与未用常量

验收:

  • 界面不再出现重复的软件名称/版本号
  • 系统窗口标题仍显示 APP_NAME 与 APP_VERSION
  • 工作区因移除标题栏而获得更多纵向空间

17.8 加载文件夹后默认预览第一张

前置阅读:

  • docs/07-ui-design.md(5.1 衣服图片面板)

任务:

  • 衣服、印花面板加载文件夹后,自动选中并预览第一张(按当前排序)
  • 空文件夹不选;把选中逻辑抽成按行选择以复用
  • 与现有「模板粘性」衔接:自动选印花后按当前模板落位

验收:

  • 加载文件夹后画布不再为空、无需手动点选
  • 两个文件夹加载顺序无关,最终都能得到合成预览

17.9 输出按「时间戳 → 印花」分组到子文件夹

前置阅读:

  • docs/02-prd.md(6.8 单张合成导出、9 数据与文件结构)

任务:

  • make_safe_output_path 输出到 <输出目录>/<印花文件名>/<衣服文件名>.<扩展名>
  • 新增 timestamped_run_dir(base),每次导出运行在输出目录下新建时间戳文件夹
  • 一次导出运行(批量/单张)共用同一个时间戳;批量在开始时计算一次
  • 单张导出、队列导出、core.batch.run_batch 均接入时间戳层
  • 子文件夹由 composer.compose 保存时自动创建;重名仍追加 _1/_2

验收:

  • 路径形如 输出目录/<时间戳>/<印花文件名>/<衣服文件名>.<扩展名>
  • 重复运行落在不同时间戳文件夹,互不覆盖
  • 一次批量运行内所有图片共用同一个时间戳文件夹

17.10 记住上次的衣服/印花文件夹

前置阅读:

  • docs/02-prd.md(6.1 图片加载、9 数据与文件结构)
  • docs/05-project-architecture.md(4.12 config_service)

任务:

  • _AssetPanel 打开对话框时定位到上次目录(set_start_dir),并在选中后发 folder_opened 信号
  • ImageListPanel 暴露 set_garment_start_dir / set_print_start_dir 与 garment_folder_opened / print_folder_opened
  • main_window 启动恢复 last_garment_dir / last_print_dir,并在打开文件夹时「改一次存一次」回写
  • UI 不直接读写配置,统一经主窗口

验收:

  • 重启后打开「打开文件夹」对话框定位到上次的衣服/印花目录
  • 目录已不存在时安全回退,不报错

17.11 修复:批量导出忽略所选输出格式/质量/目录

前置阅读:

  • docs/07-ui-design.md(7.5 输出设置区域、8 底部合成队列)

问题:

  • queue_panel.set_export_options() 从未被调用,导出面板也无「选项变化」信号,导致队列始终使用默认 ExportOptions(PNG、默认目录),批量导出无视 UI 所选格式/质量/目录。

任务:

  • 导出面板新增 export_options_changed 信号与公开 current_options()
  • 格式/质量/输出目录任一变化即发出当前 ExportOptions
  • main_window 接到 queue_panel.set_export_options,并在启动时同步一次
  • 单张导出 _do_export 改用同一 current_options() 来源

验收:

  • 选 JPG 后批量导出的文件为 .jpg 且按 JPEG 保存
  • 批量导出跟随所选输出目录与质量

17.12 合并图文件名加上印花名

前置阅读:

  • docs/02-prd.md(6.8 单张合成导出、9 数据与文件结构)

任务:

  • make_safe_output_path 的文件名由 <衣服文件名> 改为 <衣服文件名>_<印花文件名>
  • 目录分组结构不变(仍 输出目录/<时间戳>/<印花文件名>/)
  • 重名仍追加 _1/_2 避免覆盖

验收:

  • 输出文件形如 输出目录/<时间戳>/TY030/1_TY030.png
  • 单张导出与批量导出一致

17.13 隐藏「导出当前单张」按钮

前置阅读:

  • docs/07-ui-design.md(7.7 操作按钮、10 视觉规范)

任务:

  • 隐藏导出面板的 导出当前单张 按钮(setVisible(False))
  • 保留按钮与 _do_export 逻辑,便于后续需要时再显示
  • 导出统一经底部合成队列完成

验收:

  • 导出面板不再显示 导出当前单张,只剩输出设置
  • 队列的批量/选中导出不受影响

17.14 修复:选中未微调队列项时预览未按模板落位

前置阅读:

  • docs/02-prd.md(6.6 模板选择)
  • docs/07-ui-design.md(12.4 选中队列项)

问题:

  • _on_queue_item_activated 仅在 item.transform 存在(已微调)时覆盖画布变换;未微调项保留 load_print 的写死默认(印花≈衣服宽 40%、居中),预览尺寸与选中模板不一致——「模板粘性」在队列项激活路径的遗漏分支。

任务:

  • 选中队列项且 item.transform 为空时,调用 template_panel.apply_current() 按当前模板落位
  • 仍在 _loading_queue_item 保护下,避免被误标为「已微调」
  • 与预览流程(_on_print_preview)行为一致

验收:

  • 点击未微调队列项,预览印花为选中模板要求的大小/位置
  • 已微调项仍显示其自身参数,不被覆盖

17.15 修复:批量导出对未微调项复用单个像素变换

前置阅读:

  • docs/02-prd.md(6.6 模板选择)
  • docs/08-image-editor-design.md(变换状态)

问题:

  • QueuePanel 用单个像素级 TransformState(_template_transform,来自 template_applied,按「当时画布那张衣服/印花尺寸」算出)作为所有未微调项的回退变换。
  • TransformState 是「衣服原图像素坐标」,对尺寸不同的衣服/印花原样复用会错位、错尺寸(如 1000×1000 算出的 x=400/w=250 套到 2000×3000 上,印花跑到左上、尺寸只剩一半)。
  • 影响批量导出与「导出选中」中所有未微调项。

任务:

  • 队列改存模板对象(比例坐标 Template)而非单个像素变换:set_template() 取代 set_transform()
  • 导出时按每项自身衣服/印花尺寸现算:抽纯函数 core.composer.resolve_transform()(已微调项用自身变换,否则 template.to_transform_state(gw,gh,pw,ph))
  • widget 仅负责读图尺寸(core.composer.image_size(),只读 header)+ 单次运行内缓存
  • main_window 在 template_changed/template_applied 时把模板推给队列,并在启动时初始同步
  • 新增 4 个纯函数单测覆盖:已微调优先、不同尺寸各自换算、空模板/空尺寸返回 None

验收:

  • 一个批次内含不同尺寸的衣服/印花时,未微调项各自按模板正确落位
  • GUI 实测:混合尺寸批量导出,结果图印花位置/大小均正确

17.16 局域网更新 · 阶段②:启动时检测并通知

前置阅读:

  • docs/10-lan-update.md(§7 更新源、§8 流程、§16 阶段②)
  • docs/02-prd.md(app_config 的 update_source)

说明:

  • 阶段①(数据目录分离)已在 commit 76f2c6d 完成。本任务实现阶段②「只读通知」,不自动安装。

任务:

  • 配置项 update_source(空 = 不检查)写入 DEFAULT_CONFIG
  • services/update_service.py:parse_version / is_newer / check_for_update,读取 <source>/manifest.json,任何不可达/损坏/非更新一律返回 None(不抛错、不阻塞)
  • 主窗口顶部通知横幅(默认隐藏):「发现新版本 vX.Y.Z」+「打开更新目录」+ 关闭
  • 检查在后台守护线程进行,经 Qt 队列信号回主线程显示横幅(更新源不可达不卡启动)
  • 「打开更新目录」用 QDesktopServices 打开 manifest.source
  • 15 个纯函数单测覆盖版本比较与 check_for_update 各分支

验收:

  • 单测通过;更新源为空/不可达时静默跳过
  • GUI 实测:配置可达更新源 + 高版本 manifest → 启动后显示横幅,点击打开目录
  • GUI 实测:更新源不可达 → 正常启动、无横幅、无卡顿

17.17 局域网更新 · 阶段③:自动安装启动器(脚本)

前置阅读:

  • docs/10-lan-update.md(§3 架构、§8 流程、§16 阶段③)

说明:

  • 实现 PowerShell 启动器 scripts/update.ps1,跑通「检查 → staging → 校验 → 原子切换 → 启动 → 降级」整条链路。先以脚本验证流程,后续再决定是否编译为 Launcher.exe。

任务:

  • scripts/update.ps1:读 current.txt 与 data\config\app_config.json 的 update_source
  • 读远端 manifest.json、语义化版本比较,仅当远端更高才更新
  • robocopy 拷到 staging\<ver>.tmp → 校验 marker → 重命名进 versions\<ver> → 原子写 current.txt(ascii 无 BOM)
  • 启动 versions\<current>\CMBot.exe 并设 CMBOT_DATA_DIR
  • 降级:源不可达 / robocopy 失败 / marker 缺失 / 坏 manifest 一律启动本地现版本
  • 假版本目录验证 6 个用例(更新、幂等、源不可达、未配置源、下载损坏、坏 manifest)全过
  • 接入真实安装结构:新增 scripts/install_local.ps1 产出 %LOCALAPPDATA%\CMBot\app / data / staging 启动器布局
  • 真实双机实测

17.18 更新传输统一改为 HTTP

前置阅读:

  • docs/10-lan-update.md(已整体改为 HTTP:§7 源、§8 流程、§14 安全、§16 阶段)
  • docs/02-prd.md(update_source / update_user / update_pass)

背景:

  • 更新源改为 HTTP(S) 文件服务(已部署 gohttpserver + nginx,HTTP Basic Auth,示例 http://cm.xiapi.com/)。§17.16/§17.17 的本地文件版与 UNC/robocopy 版作为历史保留,本任务把传输统一到 HTTP。

任务:

  • 文档:docs/10 由 UNC/robocopy 全面改为 HTTP(源/清单/流程/安全/阶段);docs/02、docs/05 同步配置项
  • 配置项 update_user / update_pass 写入 DEFAULT_CONFIG
  • update_service.check_for_update 支持 http(s):// 源 + Basic Auth(当前仅 open() 本地文件,填 URL 静默返回 None),补单测
  • 启动器 update.ps1 改 HTTP:带凭据下载 zip → 校验 SHA-256 → 解压到 staging\app.new → app/app.old 切换回滚
  • manifest 字段由 source/files/marker 改为 url/sha256/size
  • 发布流程脚本:build → 打 zip → 算 SHA-256 → 写 manifest → 上传
  • BOM 兼容:check_for_update 用 utf-8-sig 解码,build.ps1 写 manifest 不带 BOM(否则 PS5.1 的 UTF8 BOM 会让 app 内 json.loads 静默失效);补 2 个 BOM 单测
  • update.ps1 HTTP 版以本地 HTTP server 假发布包端到端验证(拉清单→下载→SHA-256→app/app.old 切换)
  • 生产前将更新源切到 HTTPS、客户端改用只读账号
  • 真实环境(cm.xiapi.com)端到端实测

17.19 启动器改 Launcher.exe + 数据移到 ~/.cmbot(便携模型)

前置阅读:

  • docs/10-lan-update.md(§3 架构、§4 布局、§5 数据根三级回退、§9 权限、§11、§16 阶段④)

背景:

  • 改用编译的 Launcher.exe(Python + PyInstaller onefile)取代 update.ps1,对终端用户更友好。
  • 程序采用便携布局:解压任意可写目录即用,安装根 = Launcher.exe 所在目录,不限定 %LOCALAPPDATA%。
  • 用户数据移到 ~/.cmbot(%USERPROFILE%\.cmbot):始终可写、按用户隔离、不随程序更新丢失。

任务:

  • 文档:docs/10 改为便携 + Launcher.exe + ~/.cmbot 模型(§3/§4/§5/§8/§9/§11/§16)
  • get_data_dir() 三级回退:CMBOT_DATA_DIR → 打包态 ~/.cmbot → 开发态项目根;tests/test_file_service.py 5 个单测
  • src/launcher.py:复用 update_service,下载 zip→SHA-256→解压→app/app.old 切换→启动;安装根可写性检测;首次把 app\config\ 默认模板播种到 ~/.cmbot;update_service 扩展(UpdateInfo.sha256/size/min_supported、绝对 url 解析、make_auth_header/download);tests/test_launcher.py 11 个单测 + 本地 HTTP server 真实端到端验证
  • build.ps1 增产 Launcher.exe(PyInstaller onefile,console);发布目录改为便携布局 Launcher.exe + app\;产出两个 zip——自更新载荷 CMBot-<ver>.zip(app\ 内容,manifest.url 指向它)与便携安装包 CMBot-<ver>-portable.zip;launcher.py 日志初始化健壮化(--windowed/只读根不崩)。语法校验通过;实际 PyInstaller 构建需在 Windows 跑
  • 退休 scripts/update.ps1 与 scripts/install_local.ps1(已删除,git 历史可查)
  • 端到端实测(解压到 D 盘运行、自更新、回滚)

17.20 设置对话框(更新配置入口)

前置阅读:

  • docs/07-ui-design.md(4.3 设置入口、4.4 设置对话框)
  • docs/02-prd.md(update_source / update_user / update_pass)
  • docs/05-project-architecture.md(4.12 config 集中管理)

背景:

  • 更新地址/账号/密码目前只能手改 ~/.cmbot/config/app_config.json,加一个应用内设置入口,管理员配一次,launcher 与 app 内通知都跟着用。

任务:

  • 文档:docs/07 §3 布局、§4.1 去掉「暂不提供设置入口」、新增 §4.3 入口 + §4.4 对话框
  • 页签栏右端加低调 ⚙ 配置 按钮(addStretch 与编号页签隔开,非编号页签)
  • src/app/widgets/settings_dialog.py(QDialog,纯 UI):更新地址 / 账号 / 密码(掩码+显示) / 测试连接 / 当前版本 + 安全提示
  • 测试连接:后台线程调用 update_service.load_manifest(区分「连不上」与「已是最新」),反馈最新/发现新版/连接失败
  • main_window:打开时注入当前 config,保存时集中 save_config,UI 不直接写配置
  • 保存后重新触发一次在线更新检查(横幅刷新;_update_found 改为只连一次避免重复)
  • 取消不改动配置

验收:

  • 单测:load_manifest 返回 dict / 缺失抛错(2 个);对话框 Qt 符号导入校验通过
  • GUI 实测:改更新地址/账号/密码并持久化到 ~/.cmbot/config/app_config.json
  • GUI 实测:测试连接正确反馈三种结果
  • GUI 实测:改完无需重启 launcher,app 内横幅按新配置刷新

17.21 更新改为非阻塞:app 内下载 + 启动器只应用

前置阅读:

  • docs/10-lan-update.md(§3 两阶段架构、§6 时机、§8 流程、§12 体验)
  • docs/07-ui-design.md(4.3 有新版本提示、4.4 设置对话框「检查并更新」)

背景:

  • 原启动器在启动时下载更新、阻塞进主界面(57MB 包看着像卡死)。改为「下载在 app 内、切换在启动器」,启动永不阻塞。

任务:

  • services/installer.py:download_and_stage()(app 运行时下载→SHA-256→解压→staging\app.new)、apply_staged()(启动器秒切 app/app.old,含「不比当前新则丢弃」「移动失败回滚」)、staged_version()、is_writable
  • launcher.py 瘦身:seed + apply_staged + 启动,不联网;tests/test_launcher.py 重写
  • main_window:发现新版在 ⚙ 配置 左侧显示蓝色提示文字(删除旧阻塞横幅与「打开更新目录」)
  • settings_dialog:加「检查并更新」→ 下载暂存 → 「下次启动生效」
  • tests/test_installer.py 11 个单测 + 本地 HTTP server 真实端到端(下载暂存→app 不动→应用切换)
  • 文档:docs/10 §3/§6/§8/§12/§16/§18、docs/07 §4.3/§4.4 对齐
  • GUI 实测:提示文字出现 → 设置里更新 → 重启生效(真机)

17.22 默认导出目录改为程序旁的「合并后的图片」

前置阅读:

  • docs/10-lan-update.md(§4 布局、§5 路径规则)
  • docs/07-ui-design.md(7.5 输出设置)

背景:

  • 默认导出在 ~/.cmbot/output,藏在用户目录深处不好找。改为放在安装根(Launcher.exe 旁)的 合并后的图片\:好找、不随 app\ 更新替换。不放 app\(每次更新会被替换、导出会丢)。

任务:

  • 文档:docs/10 §4/§5、docs/02 配置说明、docs/07 §7.5 对齐
  • file_service.get_output_dir():打包态优先 <安装根>\合并后的图片(安装根 = get_app_dir().parent),不可写回退 get_data_dir()/output;开发态用项目目录;tests/test_file_service.py 3 个单测
  • 导出面板默认值随之变化(沿用 get_output_dir(),无需单独改)
  • GUI 实测:打包运行后默认导出到 <安装根>\合并后的图片,且更新后仍在

17.23 批量导出后生成 AI 穿搭 Excel — docs/02 §6.12 / §9

前置阅读:

  • docs/02-prd.md(§6.9 批量合成导出、§6.12 AI 穿搭 Excel、§9 目录结构)
  • docs/11-ai-outfit.md(§4 列定义、§4.1 目录行、§9.1 目录行输出)
  • src/app/widgets/queue_panel.py(_start_batch、_new_run_dir、_export_item)
  • src/services/excel_service.py(write_outfit_result,了解现有 openpyxl 写法)

背景:

添加印花批量导出后,输出到 <合并后的图片>/<时间戳>/<印花名>/。AI 穿搭的「C 列支持目录」(§19.12)可直接消费这些子目录,只缺一个指向它们的 Excel 文件。批量结束后自动生成 <合并后的图片>/<时间戳>.xlsx(六列格式,每印花子目录一行),用户切到「AI 穿搭」页、选该 Excel,即可直接对合成图批量生成穿搭效果图。

任务:

  • src/services/excel_service.py:新增 write_outfit_source_excel(excel_path, rows)
    • rows 为有序列表,每项为 (print_name: str, subdir_path: str)
    • 写表头 ["标题","货号","衣服图路径","生成结果图片路径","完成状态","失败原因"],数据从第 2 行起
    • A=print_name,B=空,C=subdir_path(末尾含 /),D/E/F=空
    • 用 openpyxl 创建新工作簿并保存;不抛出、不改动 AI 穿搭现有函数
  • src/app/widgets/queue_panel.py:_start_batch 完成后
    • 扫描 run_dir 的直接子目录,过滤出包含至少一个图片文件的(即有成功合成图),按目录名排序
    • 若有效子目录 > 0,调用 write_outfit_source_excel 生成 <run_dir>.xlsx(Path(run_dir).with_suffix(".xlsx"))
    • 写入失败只记日志,不影响批量完成的交互反馈
  • tests/test_excel_service.py:
    • write_outfit_source_excel 生成的文件表头正确、行数 = 传入 rows 数、A/B/C 列值正确(A=print_name,B=空,C=subdir 末尾有 /)、D/E/F 为空
  • 验收:批量导出完成后 <合并后的图片>/ 下出现 <时间戳>.xlsx;打开 AI 穿搭、选该文件,预览下拉显示各印花名行(货号列显示「无货号」);「开始生成」触发目录扇出,正常输出穿搭图

17.24 批量导出完成弹窗提示 AI 穿搭 Excel — docs/02 §6.12

前置阅读:

  • docs/02-prd.md(§6.12 AI 穿搭 Excel)
  • src/app/widgets/queue_panel.py(_start_batch、_write_outfit_excel)

背景:

§17.23 已在批量导出完成后生成同名 xlsx,但用户感知不到。需在 Excel 成功生成后弹出信息框,告知文件路径并引导切换到「AI 穿搭」页使用。

任务:

  • queue_panel.py:_write_outfit_excel 成功生成 Excel 后返回 excel_path,失败/跳过返回 None
  • _start_batch 拿到返回值后,若不为 None,调用 QMessageBox.information 弹出提示:
    • 标题:AI 穿搭 Excel 已生成
    • 内容:已生成 AI 穿搭 Excel 文件:\n<excel_path>\n\n可切换到「AI 穿搭」页,选择该文件直接开始生成穿搭图。
  • 验收:批量导出完成后弹框显示 xlsx 路径;点「确定」关闭;若无有效印花子目录则不弹框

18. 后续暂缓任务

以下任务第一阶段暂不做,后续需要时再新增设计文档:

  • 按电脑名控制版本
  • 强制更新与版本保留策略(mandatory / min_supported 生效、app.old 保留策略;docs/10 §16 阶段④)
  • [~] AI 穿搭(已转正、进入设计/实现,见 §19)
  • 导出上架流程
  • 自动抠图
  • 自动识别衣服区域
  • 移动端应用

已交付(原属暂缓,本季完成,见 §17.16–§17.20、docs/10-lan-update.md):

  • 在线更新(HTTP 检测 + 通知 + Launcher.exe 自动安装 + 回滚)
  • 局域网/在线分发(便携包 + HTTP 文件服务 + manifest)
  • 多版本启动器(改为 app/app.old 单版本切换,不再做多版本并排)

19. AI 穿搭模块

前置阅读:

  • docs/11-ai-outfit.md(本模块完整设计)
  • docs/旧ai穿搭项目.md(同事旧项目分析,可复用件)
  • docs/04-development-rules.md、docs/05-project-architecture.md(分层)

一句话目标:以 Excel 为数据源,按行读取「衣服图 + 标题/货号」,套提示词调 AI 图像 API 生成「人物穿着该衣服」的效果图,保存 JPG 并把结果路径写回 Excel。

19.0 设计与效果图

  • 分析旧项目 → docs/旧ai穿搭项目.md
  • 模块设计文档 docs/11-ai-outfit.md(架构/Excel 适配/数据模型/AI 服务/提示词/并发/输出/界面/配置/阶段/验收)
  • UI 效果图 docs/ui-ai-outfit.html + docs/ui-ai-outfit.png(三栏:左设置 / 中「最近结果缩略图条 + 处理明细表」/ 右运行日志;与印花页同款外壳、纯蓝统一)
  • 确认取舍:不做实时单图大预览(批量工具,并发会有「显示哪行」歧义);预览=按钮弹 QDialog;保留「保存话术」按钮;并发由 Python 线程负责、Qt 仅 signal 回主线程

19.1 核心与服务(无 GUI,可单测)— docs/11 §14 阶段 1

前置阅读:docs/11-ai-outfit.md(§3–§9)

  • 前置依赖:requirements.txt 增加 Python 3.7 兼容的 requests / urllib3 / openpyxl 锁定版本,并同步 docs/03-technical-stack.md
  • core/models.py 新增 OutfitTask / OutfitResult(纯 dataclass,Python 3.7 兼容,不依赖 PySide6)
  • services/excel_service.py:读行 → List[OutfitTask]、写回 D/E/F、占用检测、跳过「完成」/按设置重试「失败」/空字段安全跳过、每行即存
  • services/ai_image_service.py:移植旧项目 ImageApiClient(多模型、多请求格式 chat/gemini/images/images_edits、传图 data-url、递归取图、URL 归一化、字段校验);注意 PEP585 类型注解改 Python 3.7 写法
  • core/ai_outfit.py:单行生成纯逻辑编排(提示词渲染 + 调用 + 保存 JPG + 产出 OutfitResult);§8「超时按分辨率动态决定(512/1K/2K/4K→180/240/360/600,可被 timeout_seconds 覆盖)」由 ai_image_service.resolution_timeout 实现
  • 单测:test_excel_service / test_ai_image_service / test_ai_outfit / test_outfit_batch 共 40+ 用例(Excel 读写、提示词渲染、取图、命名去重、超时映射;API 用 mock);Python 3.7 通过、不依赖 GUI

19.2 批量编排 — docs/11 §14 阶段 2

前置阅读:docs/11-ai-outfit.md(§8)

  • core/outfit_batch.py 纯逻辑 OutfitBatchRunner:ThreadPoolExecutor(并发数) + RateLimiter(请求间隔) + 单任务冷却 + 阶梯重试 + 温和停止 + >30s 心跳;全可测(QObject 包装放 §19.3 UI 接线)
  • 子线程只经回调/signal 回主线程,不直接碰控件(runner 用 callback,UI 层转 signal)

19.3 UI 页签 — docs/11 §14 阶段 3

前置阅读:docs/11-ai-outfit.md(§10)、docs/07-ui-design.md(§4.2)、docs/ui-ai-outfit.png

  • 页签栏接 QStackedWidget(页 0=添加印花工作区+队列,页 1=AI 穿搭面板);启用页签 1(导出上架仍禁用)
  • app/widgets/ai_outfit_panel.py:左设置(Excel/输出/模型/话术编辑+保存+插入占位符+预览弹窗/生成设置)、中(最近结果缩略图 + 处理明细表)、右(进度+统计+开始/停止+导出失败清单+打开输出目录+实时日志)
  • 接线后台线程:QThread + _OutfitWorker(QObject) 包 OutfitBatchRunner,signal 回主线程刷日志/进度/明细/缩略图;每行写回 Excel;结束摘要弹窗;失败清单导出 CSV
  • GUI 实测(真机):选 Excel/模型跑通、停止生效、失败清单正确(离屏冒烟已过,待真机)

19.4 配置与提示词 — docs/11 §14 阶段 4

前置阅读:docs/11-ai-outfit.md(§7、§11)

  • config_service 增 load_ai_models() / load_outfit_prompt() / save_outfit_prompt();ai_models.json(密钥明文、不入库、BOM 容错)、outfit_prompt.txt(无 BOM)、app_config.json 并入 outfit_*(上次 Excel/输出/模型 + 批量设置),UI 经 config_changed 信号回主窗口集中存;tests/test_config_service.py 8 用例
  • requirements.txt 已锁 requests 2.31/openpyxl 3.1.3/urllib3 1.26;docs/03 增列依赖
  • 可选:应用内 AI 模型编辑界面(当前由管理员预置 ai_models.json,暂不做)

19.5 真机联调 — docs/11 §14 阶段 5

  • 真实中转 API + 小批量 Excel 端到端跑通,调穿搭提示词
  • 验收对照 docs/11 §16

19.6 提示词自动附加「批量生成输出要求」— docs/11 §7.1

前置阅读:docs/11-ai-outfit.md(§7.1)

背景:旧项目最终提示词 = 用户话术 + 自动附加的输出要求(参考解析度 / 固定 1:1 / 结合标题与参考图 / 不可跑版);当前 cmbot 的 render_prompt 只替换 {title},缺这段。决策:简体文案、始终自动附加(不做开关)。

  • core/ai_outfit.py:加 OUTPUT_REQUIREMENTS 常量 + build_output_requirements(resolution);render_prompt(template, task, resolution=None) 在替换占位符后附加该段(resolution 为空不加);generate_outfit_image 传入当前 resolution
  • app/widgets/ai_outfit_panel.py:内嵌预览改用 render_prompt(带当前分辨率),分辨率下拉变化时刷新预览,使「预览 = 实际发送」
  • 单测:带 resolution 追加尾巴且内容正确、无 resolution 不加;build_output_requirements 空/非空(加入 tests/test_ai_outfit.py,全套 12 文件绿)
  • 同步 docs/ui-ai-outfit.html 预览框示例含该段并重渲 .png(可选,未做)

19.7 切换分辨率 / AI 模型时信息提示 — docs/11 §10.2

前置阅读:docs/11-ai-outfit.md(§10.2)

需求:用户手动切换「分辨率」或「AI 模型」下拉时弹信息框告知影响(只告知、不拦截、不还原)。决策:每次实际更换都弹一次。

  • app/widgets/ai_outfit_panel.py:给 _resolution / _model_combo 接 activated 信号(非 currentIndexChanged,避免启动/载配置误弹);仅值实际改变时 QMessageBox.information,保持新选项、不回退
  • 分辨率文案带 ai_image_service.resolution_timeout 的超时秒数(512/1K/2K/4K→180/240/360/600)+「下次生效」
  • AI 模型文案带该模型 api_type + 计费/效果可能不同 +「下次生效」
  • 离屏验证:程序化赋值(apply_config/_fill_sample_combo)不弹(0 次);用户 1K→4K 弹 1 次(含 600s)、同项重选不弹、换模型弹(含 api_type)

19.8 话术模板(多套命名话术)— docs/11 §7.2

前置阅读:docs/11-ai-outfit.md(§7.2、§7.1)、docs/07-ui-design.md、src/app/widgets/template_panel.py(范式参考)

需求:把单一话术升级为多套命名话术(下拉切换 / 新建 / 另存为 / 重命名 / 删除 / 保存,记住上次)。决策:全部自定义(不分内置,播种一套「默认」);切换前脏数据弹窗提醒是否保存;含重命名。

  • config_service:outfit_prompts.json 读写 load_outfit_prompts() / save_outfit_prompts(list)(utf-8-sig 读 / 无 BOM 写 / 损坏回退「默认」;始终 ≥1 套;_normalize_prompts 丢弃非法项);app_config 加 outfit_prompt_name(当前选中名);首次迁移旧 outfit_prompt.txt → 一套「默认」,否则 DEFAULT_OUTFIT_PROMPT
  • ai_outfit_panel.py:「通用话术」组加 模板下拉 + 新建 / 另存为 / 重命名 / 删除(保留 插入标题 / 保存);切换载入文本 + 刷新预览;记住上次所选并启动恢复(经 config_changed 回主窗口集中存 outfit_prompt_name)
  • 脏数据保护:编辑框与当前套已存文本不同则在切换/新建/另存为前弹「保存 / 不保存 / 取消」(取消还原下拉);名字唯一(空/重名拒绝);删后选邻近、不可删到 0(删除单独二次确认)
  • 「开始生成」前把当前编辑存回所选套(_store_current_text);生成/预览仍走 render_prompt(§7.1 尾巴不变)
  • 单测 tests/test_config_service.py +6(seed/迁移/损坏回退/roundtrip 无 BOM/丢弃非法/BOM 容错,共 14);离屏验证 新建·另存为·切换·脏保存持久化·重命名·删除·不可删到 0;全套 12 文件绿

19.9 修复:全表完成后预览样本行下拉为空 — docs/11 §10.3

前置阅读:docs/11-ai-outfit.md(§10.3、§7)

问题:预览样本行用 load_outfit_tasks()(给生成用、跳过「完成」行)填,整表生成成功后返回空 → 下拉只剩「(选 Excel 后显示替换效果)」。预览应不看状态。

  • services/excel_service.py:新增 read_all_rows(excel),返回每一有效数据行(标题/货号/衣服图齐全)的 OutfitTask,忽略 E 列状态
  • ai_outfit_panel._reload_sample_rows() 改调 read_all_rows(预览专用);生成仍走 load_outfit_tasks,_on_tasks_loaded 仍用实际任务
  • 单测 tests/test_excel_service.py:read_all_rows 含「完成」「失败」行也返回、空字段行跳过(8 用例);离屏验证全表完成的 Excel 预览下拉有样本行;全套 12 文件绿

19.10 无待处理行提示 + 生成不清空预览样本 — docs/11 §10.4

前置阅读:docs/11-ai-outfit.md(§10.4、§10.3)

背景:整表都「完成」后点「开始生成」,当前只显示「已加载 0 行待处理任务 / 完成 0,失败 0」,看不懂;且 _on_tasks_loaded([]) 会把预览样本下拉清空(破坏 §10.3)。

  • 改进 1:_on_finished(total==0) 弹 _show_no_pending_message——统计 read_all_rows 的「完成 N / 失败 M」,提示「已完成 N 跳过;失败 M 可勾『重试上次失败的行』;重做已完成请清空状态(E)列」(无有效行时另提示)
  • 改进 2:_on_tasks_loaded 去掉 _fill_sample_combo(tasks),预览样本下拉只由 read_all_rows/_reload_sample_rows 维护,与运行解耦
  • 离屏验证:全表完成 → tasks_loaded([]) 后预览下拉仍 2 行、_on_finished(total=0) 弹"没有待处理的行"(含已完成1/失败1);total>0 仍弹常规"AI 穿搭"结束框;全套 12 文件绿

19.11 AI 穿搭按钮配色(对齐添加印花)— docs/11 §10.5

前置阅读:docs/11-ai-outfit.md(§10.5)、src/app/widgets/image_list_panel.py / queue_panel.py / template_panel.py(按钮 QSS 范式)

背景:AI 穿搭面板无 stylesheet,按钮全是系统灰;复用印花页的「主操作蓝 / 次级灰 / 危险红」三类按钮规范。

  • ai_outfit_panel.py:面板 setObjectName("aiOutfitPanel") + _apply_styles()(_build_ui 末尾调用),QSS 以 #aiOutfitPanel 限定作用域,避免波及弹窗按钮
  • 主操作蓝(#openFolderBtn/#queueBatchBtn 同款)= 开始生成(aiStartBtn);危险安静红(#templateDeleteBtn 同款)= 停止生成(aiStopBtn)+ 话术删除(aiPromptDeleteBtn);其余走面板内 QPushButton 默认次级灰(#queueActionBtn/#templateBtn 同款)
  • 给 开始/停止/话术删除 三个按钮设 objectName;纯样式、不动行为
  • 离屏 Qt grab 截图核对:开始=蓝、停止=红(禁用→灰)、话术删除=红、其余=灰描边;面板 stylesheet 非空;全套 12 文件绿

19.12 C 列支持「图片目录」→ 多图扇出到同名子目录 — docs/11 §4.1 / §9.1

前置阅读:docs/11-ai-outfit.md(§4.1、§9.1)、src/core/ai_outfit.py(generate_outfit_image/make_outfit_output_path/safe_product_filename)、src/core/outfit_batch.py、src/app/widgets/ai_outfit_panel.py(_OutfitWorker/_add_result_thumb/_on_progress/_basename)

背景:C 列除单张图片文件外,也可填一个目录(d:/images/a/)。对目录内每张图各生成一张穿搭图(N→N),全部存到 输出目录/a/,文件名沿用源图名;Excel 仍整行一个状态:D=子目录、E=全成功才「完成」、F=失败张数/原因。已确认:① N→N;② D 写子目录路径、整行一状态;③ 沿用源图名。

设计取舍:保持「一行=一个 OutfitTask=一个 worker=一次回写」,多图扇出放进 generate_outfit_image 内部 → 批处理器/进度/明细表/回写几乎不动。目录内顺序生成、逐张按「新请求间隔」本地节流(并发=1 即全局节流);失败重试跳过已存在输出,幂等。

  • core/ai_outfit.py:list_directory_images(dir)(顶层、扩展名过滤、排序、忽略子目录);make_outfit_subdir_path(output_dir, subdir, stem)(输出目录/<安全子目录>/<安全源图名>.jpg,不加 _n 后缀)
  • core/ai_outfit.py:generate_outfit_image 加目录分支(新参 request_interval/image_log)——列图片、空目录→失败、复用一个 ImageApiClient、逐张跳过已存在/节流/存子目录/发日志、聚合成单个 OutfitResult(output_path=子目录、output_paths=各 jpg、success=全成功且≥1、error=失败张数)
  • core/models.py:OutfitResult 加 output_paths: List[str](单文件模式留空)
  • ai_outfit_panel.py:gen 闭包传 request_interval/image_log=self.log.emit;_add_result_thumb 逐张加缩略图;_on_progress 明细「结果」列显示「子目录 (N 张)」;_basename 处理目录末尾分隔符
  • tests/test_ai_outfit.py:扇出/幂等跳过/空目录/部分失败/命名过滤等用例;既有单文件用例保持绿;全套 12 文件 py37 全绿
  • 离屏冒烟:临时 Excel 的 C 指向含 3 张图的目录 + mock API,断言 输出/<目录名>/ 下 3 个 jpg、D=子目录、E=完成、缩略图 3 张

19.13 AI 模型配置模板 — docs/11 §6.1 / docs/10 §5

前置阅读:docs/11-ai-outfit.md(§6.1、§11)、docs/10-lan-update.md(§4、§5)、src/services/config_service.py(load_ai_models)、src/launcher.py(首次播种)。

背景:当前 AI 模型配置只能由管理员手动创建 ~/.cmbot/config/ai_models.json。需要提供出厂模板,使用现有 api_config.json 中的两个模型(GPT Image 2 / Nano Banana 2)转换为当前程序可直接读取的 ai_models.json 列表结构,但不得提交真实 API key。

  • 新增 packaging/default_config/ai_models.json,格式为 { "models": [...] };display_name → name;保留 url/model/api_type/timeout_seconds/connect_timeout_seconds/extra_body;api_key 留空或占位
  • 更新 packaging/default_config/app_config.json,新增 outfit_model: "Nano Banana 2",对应旧配置的 last_selected_model=nano_banana_2
  • 更新 src/launcher.py 首次播种白名单,把 ai_models.json 与 app_config.json、templates.json 一起复制到 ~/.cmbot/config;已有用户文件不得覆盖
  • 补 tests/test_launcher.py:首次播种复制 ai_models.json,已有 ai_models.json 不覆盖
  • 补配置读取验证:load_ai_models() 能读取出厂模板的两个模型名,且模板不含真实 key
  • 验证:python -m unittest discover -s tests 通过;主窗口启动后 AI 模型下拉可显示模板模型(未填 key 时开始生成仍应提示缺 api_key)

19.14 货号(B列)改为可选 — docs/11 §4 / §9

前置阅读:docs/11-ai-outfit.md(§4、§9、§4.1)、src/services/excel_service.py(load_outfit_tasks/read_all_rows 完整性判定)、src/core/ai_outfit.py(make_outfit_output_path/generate_outfit_image 单文件分支)、src/app/widgets/ai_outfit_panel.py(_fill_sample_combo)

背景:用户表的「商品id(货号)」整列为空,而行有效性要求 标题+货号+图三者非空 → read_all_rows 与 load_outfit_tasks 都返回 0 行:预览下拉显示占位符,且「开始生成」也判定无待处理行。货号不进提示词、目录行输出名沿用源图名,对目录用法是多余约束。已确认:货号改可选。

  • excel_service.py:read_all_rows 与 load_outfit_tasks 完整性判定改为只要求 标题 + 衣服图路径非空;货号可空(状态过滤不变)
  • core/ai_outfit.py:单文件分支货号为空时输出名回退 Path(garment_path).stem(货号非空仍用 货号.jpg);目录行不变
  • ai_outfit_panel.py:样本下拉标签货号为空时显示「(无货号)」,明细表货号列允许空
  • tests/test_excel_service.py / test_ai_outfit.py:空货号行被 read_all_rows/load_outfit_tasks 收录;单文件空货号 → 输出按源图名命名;既有用例保持绿
  • 离屏冒烟:用「标题填、货号空、C=目录」的表,断言预览下拉有行、生成产出到子目录、E=完成;全套 py37 全绿

19.15 ai_models.json 运行时兜底补种 — docs/11 §6.1 / docs/10 §5

前置阅读:docs/11-ai-outfit.md(§6.1)、docs/10-lan-update.md(§5)、src/services/config_service.py(load_ai_models)、src/services/file_service.py(get_app_dir/get_config_path)。

背景:19.13 已把 ai_models.json 加入出厂模板和新启动器播种白名单。但用户通过自更新升级时,启动器播种发生在应用新版 app 之前,读取的是旧版 app\config;且旧 Launcher.exe 不自更新,可能根本不知道 ai_models.json。结果新版 app\config\ai_models.json 已存在,但 ~/.cmbot/config/ai_models.json 仍缺失。

设计取舍:由主程序运行时兜底补种新增配置文件。load_ai_models() 读取用户配置前,如果 get_config_path("ai_models.json") 不存在,则尝试从当前程序根 get_app_dir()/config/ai_models.json 复制到用户配置目录;已有用户文件永不覆盖。模板 key 为空,管理员仍需在 ~/.cmbot/config/ai_models.json 填真实 key。

  • config_service.py 增加私有 helper(如 _seed_factory_config_if_missing(filename)),只负责「目标不存在时,从当前 app\config 复制」
  • load_ai_models() 在读取前调用该 helper,确保旧 Launcher/自更新场景也能补出 ~/.cmbot/config/ai_models.json
  • helper 只在源文件存在且目标不存在时复制;复制失败记录日志并继续返回空列表,不影响程序启动
  • 不覆盖用户已有 ai_models.json,不合并、不重写真实 key
  • 补 tests/test_config_service.py:缺用户文件 + 有出厂模板 → 自动复制并加载;已有用户文件 → 不覆盖;源模板不存在 → 返回空且不抛异常
  • 验证:相关单测和全套 python -m unittest discover -s tests 通过;模拟旧 Launcher 场景(只放新版 app\config\ai_models.json,用户目录缺文件)时 AI 模型下拉能显示模板模型

19.16 AI 穿搭默认输出目录改为「穿搭图片」 — docs/11 §9 / docs/10 §4-5

前置阅读:docs/11-ai-outfit.md(§9、§9.1、§11)、docs/10-lan-update.md(§4、§5)、src/services/file_service.py(默认目录辅助函数)、src/app/widgets/ai_outfit_panel.py(输出目录默认值和配置恢复)、src/core/ai_outfit.py(输出路径生成)。

背景:添加印花页默认导出目录是程序旁的 合并后的图片\。AI 穿搭生成的是人物穿搭效果图,继续放到 合并后的图片\ 容易和印花合成产物混在一起。AI 穿搭应使用独立默认目录 穿搭图片\,仍放在安装根(Launcher.exe 旁),便于用户直接查找且不随 app\ 更新替换。

设计取舍:新增 AI 穿搭专用默认输出目录辅助函数,不改变添加印花页的 get_output_dir() 行为;用户已手动选择的 outfit_output_dir 继续优先,只有为空时才使用新默认目录。

  • 文档已更新:docs/11-ai-outfit.md / docs/10-lan-update.md / docs/ui-ai-outfit.html 均指向 穿搭图片\
  • file_service.py 增加 AI 穿搭默认输出目录辅助函数(如 get_outfit_output_dir()):打包态优先 <安装根>\穿搭图片,不可写回退 get_data_dir()/output/穿搭图片,开发态用项目目录下 穿搭图片
  • ai_outfit_panel.py 默认输出目录改用该 helper;outfit_output_dir 非空时仍使用用户保存值
  • core/ai_outfit.py 目录行输出保持 AI 穿搭输出目录/<目录叶子名>/<源图名>.jpg,单文件行仍按当前输出目录落盘
  • 补测试:默认目录路径、不可写回退、AI 穿搭面板默认值、目录行输出到 穿搭图片/<目录名>/
  • [~] 验证:相关单测通过,离屏启动 AI 穿搭页时输出框默认显示 穿搭图片;全套 python -m unittest discover -s tests 当前被工作区未提交的 packaging/default_config/ai_models.json 改动阻塞(模型顺序/API key 与出厂模板规范不一致),待清理该文件后重跑

19.17 AI 穿搭并发语义改为「行顺序 + 图片并发」 — docs/11 §8 / §9.1 / §10

前置阅读:docs/11-ai-outfit.md(§4.1、§8、§9.1、§10)、src/core/outfit_batch.py、src/core/ai_outfit.py、src/app/widgets/ai_outfit_panel.py。

背景:当前「并发数」实际用于 Excel 行任务并发;但现在 C 列常见用法是一行一个图片目录,目录内有多张衣服图。用户期望 Excel 行按顺序处理、每行一次聚合写回,界面并发参数用于控制当前目录内同时处理多少张图片。这样既符合「一行=一个子目录」的数据结构,也能把提速点放在同一目录内的多图生成上。

设计取舍:外层 OutfitBatchRunner 固定 Excel 行并发为 1;界面文案改为「图片并发数」。配置字段可先兼容复用现有 outfit_concurrency,但业务含义改为目录内图片并发;若后续改字段名,需要迁移旧配置。

  • 文档更新:docs/11-ai-outfit.md 明确 Excel 行并发固定 1、图片并发数只作用于目录内部;docs/ui-ai-outfit.html 示例文案改为「图片并发数」
  • ai_outfit_panel.py:右侧生成设置 label 从「并发数」改为「图片并发数」,日志启动行同时显示「Excel 行并发 1 / 图片并发 N」
  • _OutfitWorker / OutfitBatchRunner:外层 Excel 行任务固定顺序处理,不再用界面并发值作为行 max_workers
  • generate_outfit_image:增加或接入 image_concurrency 参数;单文件行保持顺序单张处理
  • core/ai_outfit.py 目录分支:使用 ThreadPoolExecutor(max_workers=图片并发数) 并发生成目录内图片;已存在输出仍跳过,部分失败仍聚合到整行结果
  • 保留「新请求间隔」启动节流;图片并发数 >1 时日志/缩略图完成顺序允许与文件名排序不同
  • 补测试:Excel 行顺序处理、目录内图片并发、单文件行不并发、已存在跳过、部分失败聚合、UI label/default/config 兼容
  • [~] 验证:语法检查、test_outfit_batch.py、test_ai_outfit.py、test_ai_outfit_panel.py 通过;全套 python -m unittest discover -s tests 当前被工作区未提交的 packaging/default_config/ai_models.json 改动阻塞(模型顺序与出厂模板规范不一致),待清理该文件后重跑

19.18 AI 穿搭左栏「标题生成」 — docs/11 §17

前置阅读:

  • docs/11-ai-outfit.md(§17 标题生成、§7 提示词、§10 界面、§4.1 目录行)
  • src/services/ai_image_service.py(AiModelConfig/image_to_data_url/detect_api_type/normalize_api_url/build_payload/generate,复用 HTTP 管道)
  • src/core/ai_outfit.py(render_prompt/looks_like_directory/list_directory_images)
  • src/services/excel_service.py(read_all_rows/write_outfit_result,COL_TITLE)
  • src/services/config_service.py(load_ai_models/load_outfit_prompt/DEFAULT_CONFIG)
  • src/app/widgets/ai_outfit_panel.py(_build_left/_OutfitWorker/预览相关方法)

背景:

「添加印花」批量导出的 Excel,A 列「标题」是占位印花名。需在 AI 穿搭页左栏新增独立的「生成标题」:用户写标题提示词,AI 看该行衣服图(视觉)生成电商标题,逐行回填 Excel A 列并刷新 GUI;之后「开始生成」跑图即用新标题。已确认:写回 A 列;独立按钮;看图(视觉);独立「标题模型」下拉;逐行各生成 1 条、第 n 条回填第 n 行 A、重载刷新 GUI。

任务:

  • src/services/ai_text_service.py(新建):AiTextClient(config, session=None) 复用 ai_image_service 管道;generate_text(prompt, image_path=None)(chat/gemini 带图视觉文本输出,images/images_edits 抛 AiTextServiceError);extract_text_from_response(chat/gemini 取文本,返回第一条标题:首个非空行、去序号/引号、单行化)
  • src/core/ai_title.py(新建):render_title_prompt(template, task)(替换 {title}/{product_id},不加图片输出要求尾巴);generate_title(task, prompt_template, model_config, api_client=None) -> TitleResult(目录行取 list_directory_images 首图,无图失败,never raises)
  • src/core/models.py:新增 TitleResult(task/success/generated_title/error/attempts)
  • src/services/excel_service.py:新增 write_title_result(excel_path, row_index, title)(只写 A 列并保存,不动 D/E/F)
  • src/services/config_service.py:DEFAULT_CONFIG 增 title_model;新增 load_title_prompt/save_title_prompt(title_prompt.txt)+ DEFAULT_TITLE_PROMPT
  • src/app/widgets/ai_outfit_panel.py:移除预览块(_sample_combo/_preview_*/_refresh_preview/_fill_sample_combo/_reload_sample_rows 及信号连接);新增「标题生成」组(提示词编辑 + 标题模型下拉 + 保存 + 生成标题)置于话术组上方;apply_config/_emit_config 接 title_model+标题提示词;_TitleWorker 顺序逐行生成+立即回填+刷新明细表标题列,完成后重载 Excel;「生成标题」与「开始生成」互斥
  • 测试:tests/test_ai_text_service.py(文本解析含多行只取第一条、images_edits 抛错、payload 含图);tests/test_ai_title.py(render、单文件/目录首图/无图失败/异常);tests/test_excel_service.py 加 write_title_result 只改 A;删预览后 ai_outfit_panel 离屏可构建
  • 验证:相关单测 + 全套 py37 通过(test_config_service 的 ai_models.json 顺序失败属并行 §19.13 遗留,与本改动无关);离屏冒烟(mock AiTextClient):选印花 Excel → 生成标题 → A 列改写、明细表刷新、D/E/F 未动、面板无预览控件

已提交 2fa488b(标题模型当时为下拉,§19.19 改为配置定名)。

19.19 标题模型改为「配置定名」、去掉下拉 — docs/11 §17.3 / §17.6

前置阅读:

  • docs/11-ai-outfit.md(§17.3 模型配置定名、§17.6 决策、§10 左栏)
  • src/app/widgets/ai_outfit_panel.py(_build_title_group/_fill_model_combo/_selected_title_model_config/apply_config/_emit_config/_set_title_running)
  • src/services/config_service.py(DEFAULT_CONFIG 的 title_model)
  • docs/ai_models.sample.json(chat 文本模型示例)

背景:

§19.18 给标题模型放了独立下拉。复盘改为「配置定名、去下拉」:标题模型是「配一次就固定」的,不像分辨率/话术需要每次切换;左栏 ~360px 已叠两个提示词组,少一个下拉更干净;还消掉「误选图片模型」的坑。模型名放 app_config.title_model(默认 GPT-5.5 文本),运行时按名字查 ai_models.json,换模型只改配置不改代码。

任务:

  • ai_outfit_panel.py:_build_title_group 去掉「标题模型」下拉(_title_model_combo 及其 _compact_combo/填充/_set_title_running 里的禁用);apply_config 不再填标题下拉、_emit_config 不再写 title_model(值由配置/手动维护,UI 不覆盖)
  • ai_outfit_panel.py:_selected_title_model_config() 改为 _resolve_title_model_config()——读 app_config.title_model 名字,在 self._models 里按 name 查;命中校验 api_config_errors 后返回 AiModelConfig;找不到/未配置时 QMessageBox.warning 明确报错(提示加文本模型并设 title_model)。_start_title 改用它;面板需持有当前 title_model 值(apply_config 存一份 self._title_model_name)
  • config_service.py:DEFAULT_CONFIG["title_model"] 默认值由 "" 改为 "GPT-5.5 文本";注释改为「标题模型名字(对应 ai_models.json 的 name,§17.3 配置定名)」
  • docs/ai_models.sample.json:补一条 name="GPT-5.5 文本"、api_type=chat、model=gpt-5.5(占位 key)的文本模型示例,作为标题模型样板
  • 测试:更新 test_ai_outfit_panel.py(断言无标题模型下拉、_title_model_combo 不存在);新增 _resolve_title_model_config 命中/缺失(报错返回 None)用例
  • 验证:相关单测 + 全套 py37 通过;离屏冒烟:title_model 命中 ai_models.json → 生成走该模型;改名/缺失 → 明确报错

19.20 穿搭话术:按钮行微调 +「插入标题」保留 + 预览改弹窗 — docs/11 §7 / §7.3 / §10

前置阅读:

  • docs/11-ai-outfit.md(§7 提示词、§7.1 输出要求、§7.2 多套模板、§7.3 预览弹窗、§10 界面)
  • src/core/ai_outfit.py(render_prompt/build_output_requirements)
  • src/app/widgets/ai_outfit_panel.py(_build_left 话术组、_prompt_edit、模板按钮行、_save_prompt/_store_current_text、_start/_OutfitWorker、§19.18 删掉的样本下拉/预览逻辑可参考)
  • tests/test_ai_outfit.py(render_prompt 既有用例,不改 render_prompt,保持原样)

背景:

话术组小改版(用户最终确认):①「保存话术」→「保存」、移到模板按钮行「重命名」之后(行变 新建/另存为/重命名/保存/删除);② 保留「插入标题」+ {title} 占位符(标题不自动前置——用户自定位置、不与旧话术重复);③ 「最终提示词预览」因左栏空间紧张(标题生成组 + 话术组并存)改为按需弹窗:编辑框下方「预览最终提示词」按钮 → 非模态 QDialog,内含数据行下拉 + 只读替换后提示词(含 §7.1 输出要求)。render_prompt 与 DEFAULT_OUTFIT_PROMPT 不改。

任务:

  • ai_outfit_panel.py 模板按钮行:在「重命名」与「删除」之间插入「保存」(连 _save_prompt);删掉编辑框下方原「保存话术」按钮;保留编辑框下方「插入标题」按钮,旁边新增「预览最终提示词」按钮
  • ai_outfit_panel.py 预览弹窗(新 _OutfitPreviewDialog 或方法):非模态 QDialog,含数据行下拉(_compact_combo + read_all_rows(excel) 填,状态无关)+ 只读 QPlainTextEdit;内容 = render_prompt(话术正文, 选中行, 当前分辨率);缺 {title} 时顶部提示;未选 Excel/无行 → 显示话术原文 + 提示
  • ai_outfit_panel.py 弹窗联动:话术 textChanged / 分辨率 currentIndexChanged / 数据行下拉变化 → 刷新弹窗预览(仅弹窗存在时);弹窗用 _reload_sample_rows 思路在打开时/选 Excel 后填行
  • ai_outfit_panel.py 生成:保持现有 _start 的「缺 {title} 弹窗询问是否继续」(标题靠占位符);不引入自动前置
  • 测试:面板离屏断言——按钮行含「保存」、编辑框下方仍有「插入标题」、有「预览最终提示词」按钮;打开预览弹窗后选数据行/改分辨率,预览文本 = render_prompt 结果(含输出要求);render_prompt 既有用例保持绿(不改该函数)
  • 验证:相关单测 + 全套 py37 通过;离屏冒烟:点「预览最终提示词」弹窗 → 选数据行标题替换、切分辨率输出要求刷新;左栏不因预览常驻而需要滚动

19.21 标题生成:命中图片模型时开跑前拦截 — docs/11 §17.3 / §17.5

前置阅读:

  • docs/11-ai-outfit.md(§17.3 模型配置定名、§17.5 验收)
  • src/app/widgets/ai_outfit_panel.py(_find_title_model/_resolve_title_model_config/_start_title)
  • src/services/ai_image_service.py(API_IMAGES/API_IMAGES_EDITS/detect_api_type)
  • tests/test_ai_outfit_panel.py(_find_title_model 命中/缺失用例)

背景:

现状①完全没配模型、②title_model 名字找不到对应条目,都已在 _find_title_model 开跑前弹窗+中止。缺口是③:名字命中了、但那条是图片模型(api_type=images/images_edits,如误指到 GPT Image 2)——当前不在开跑前拦,会开跑后逐行失败(日志/明细写「该模型是图片接口…」,不烧 API)。补一个开跑前拦截,体验对齐①②。

边界:api_type=chat 但实际返回图片的模型(如 Nano Banana)类型上判不出,仍只能运行时由「未找到文字标题」逐行暴露——固有限制,不在本任务范围。

任务:

  • ai_outfit_panel.py _find_title_model:命中条目后,用 detect_api_type(url, api_type) 判类型;若 ∈ {API_IMAGES, API_IMAGES_EDITS} → 返回错误「『{名字}』是图片模型({api_type}),不能生成文字标题,请改选 chat/gemini 文本模型」(config 为 None)。校验顺序:先 api_config_errors,再图片类型判定
  • 行为不变确认:_resolve_title_model_config 拿到 error 仍是 QMessageBox.warning + 返回 None;_start_title 据此中止(已有逻辑,无需改)
  • 测试:test_ai_outfit_panel.py 加用例——title_model 命中 images_edits 模型 → _find_title_model 返回 (None, 含「图片模型」的错误);既有命中(chat)/缺失用例保持绿
  • 验证:相关单测 + 全套 py37 通过;离屏冒烟:title_model 指向图片模型 → 点「生成标题」前即弹窗中止、不开跑

19.22 标题生成改为「一次请求·纯提示词·多条按序填」 — docs/11 §17.1 / §17.7

前置阅读:

  • docs/11-ai-outfit.md(§17 总述、§17.1 数据流、§17.2 文本服务、§17.4 运行、§17.7 决策)
  • src/services/ai_text_service.py(generate_text/extract_text_from_response/_clean_title)
  • src/core/ai_title.py(generate_title/render_title_prompt/_reference_image——本任务替换)
  • src/services/config_service.py(DEFAULT_TITLE_PROMPT)
  • src/app/widgets/ai_outfit_panel.py(_TitleWorker/_start_title/_on_title_*)
  • tests/test_ai_title.py / test_ai_text_service.py(既有用例需改)

背景:

用户确认把标题生成从「逐行看图、各生成 1 条、每行一次请求」改为一次请求、纯提示词(不传图)、生成多条、按序回填;数量由用户自写进提示词;条数与行数对不上时多丢/少留空 + 日志(§17.7)。

任务:

  • ai_text_service.py:新增 extract_titles_from_response(data) -> List[str](原始文本按行拆 + 逐行清洗,复用提取序号/引号的 _clean_title 逐行版);新增 AiTextClient.generate_texts(prompt, image_path=None) -> List[str](与 generate_text 共用一段 POST,返回多条);generate_text 保留(= 多条取首条)
  • core/ai_title.py:新增 generate_titles(prompt, model_config, api_client=None) -> List[str](一次请求、image_path=None 纯文本);移除 generate_title/_reference_image/render_title_prompt(不再逐行看图、不替换占位符——提示词原样发)
  • config_service.py:DEFAULT_TITLE_PROMPT 改批量风格(让模型生成多条、每行一条、不带序号/引号;示例含数量如「生成 10 条」)
  • ai_outfit_panel.py _TitleWorker:改为一次 generate_titles → 按序 write_title_result 回填、逐条 progress 刷新明细表;N>行数多的丢+日志、N<行数后面行留空+日志;请求异常 → 日志 + 成功 0 收尾;去掉行间 request_interval sleep
  • ai_outfit_panel.py _start_title:不再传 request_interval;图片模型开跑前拦截(§19.21)保持
  • 测试:test_ai_text_service.py 加 extract_titles_from_response(多行→多条、去序号/引号、丢空)+ generate_texts(mock,返回多条、无图 payload 不含 image_url);test_ai_title.py 改为 generate_titles(一次请求多条、客户端异常);删旧 generate_title 用例
  • 验证:相关单测 + 全套 py37 通过;离屏冒烟:一次请求返回 6 条 → 6 行 A 按序回填;返回 3 条/8 条 → 兜底(留空/丢弃)+ 日志;payload 不含图片

19.23 标题解析改为「按逗号分割」 — docs/11 §17.1 / §17.2 / §17.3

前置阅读:

  • docs/11-ai-outfit.md(§17.1 数据流、§17.2 文本服务、§17.3 默认提示词)
  • src/services/ai_text_service.py(_clean_titles/extract_titles_from_response/_clean_title_line)
  • src/services/config_service.py(DEFAULT_TITLE_PROMPT)
  • tests/test_ai_text_service.py(extract_titles 用例)

背景:

实测模型常返回 Markdown 表格(編號/標題/字元數估算 三列),按行解析会把表头/分隔线/带 | 数字的行当成标题(垃圾),写进 Excel A 全是 | 編號 | 標題 | 字元數估算 |。改用提示词约定「标题之间用逗号分隔」+ 解析按逗号拆分,简单可靠、避开表格坑。

任务:

  • ai_text_service.py:_clean_titles(text) 改为按逗号拆分——先把原始文本按 ,/,(并把换行也当分隔,兜底)切成段,每段走 _clean_title_line(去首尾空白/序号/符号/引号),丢空,返回列表。extract_titles_from_response/extract_text_from_response(取首条)签名不变
  • config_service.py:DEFAULT_TITLE_PROMPT 改为「逗号分隔」风格——明确「标题之间用逗号分隔、不要 Markdown 表格/换行/序号/引号/表情」,数量由用户写
  • 测试:test_ai_text_service.py 更新/新增——逗号分隔(半角/全角)→ 多条;混换行+逗号→拆开;段内去序号/引号;Markdown 表格输入不再当多条(要么单条要么靠提示词避免,至少不产出表头那种垃圾的多条断言);extract_text_from_response 取首条
  • 验证:相关单测 + 全套 py37 通过;离屏冒烟:逗号分隔字符串 → 按序回填各行 A

19.24 停止生成:打断当前目录行 + 「停止中…」反馈 — docs/11 §8

前置阅读:

  • docs/11-ai-outfit.md(§8 并发/限速/重试/停止)
  • src/core/outfit_batch.py(OutfitBatchRunner.run/stop/_run_one_with_retry/_stop_event)
  • src/core/ai_outfit.py(generate_outfit_image/_generate_directory_outfit 的 ThreadPoolExecutor 一次性提交)
  • src/app/widgets/ai_outfit_panel.py(_OutfitWorker.run 的 gen 闭包、_stop、_set_running)

背景:

点「开始生成」→ 几秒后点「停止生成」→ 之后几十秒两个按钮都灰、像卡死。根因(非死锁,会自行恢复):温和停止只 set _stop_event、不提交新行,但当前目录行的 _generate_directory_outfit 一次性把所有图片 submit 进线程池并 shutdown(wait=True) 等全部跑完(每张一次 AI 请求、读超时最高 600s),停止信号进不到这个循环;且 _stop() 后无 UI 反馈。已确认修复方向:快速停止 + 反馈。

任务:

  • core/outfit_batch.py:run() 调 generate_func 时传 should_stop=self._stop_event.is_set;_run_one_with_retry(task) → _run_one_with_retry(task) 内 self.generate_func(task, should_stop)(或闭包捕获),向下传递
  • core/ai_outfit.py:generate_outfit_image(..., should_stop=None) 透传;_generate_directory_outfit 改有界提交——始终最多 image_concurrency 张在飞,每张完成后、提交下一张前检查 should_stop(),已停止则停止提交剩余、让在飞收尾返回;结果 error 注明「已停止,N 张未生成」,output_paths 带已生成的;单文件行进入前检查一次 should_stop()(已停止→跳过结果)
  • ai_outfit_panel.py:_OutfitWorker.gen(task, should_stop) 闭包接新参并透传 generate_outfit_image;_stop() 把 停止生成 文案改「停止中…」(保持禁用);_set_running(False) 复位文案为「停止生成」
  • 测试:test_ai_outfit.py —— 目录行设 should_stop 在第 k 张后置真 → 之后不再新增生成、返回「已停止」结果且已生成的在 output_paths;test_outfit_batch.py —— 停止后不起新行、当前行提前返回、finished 及时;既有用例保持绿(注意 generate_func 新签名向后兼容/同步改 mock)
  • 验证:相关单测 + 全套 py37 通过;离屏冒烟:mock 计数/慢 generate,开跑后置 stop → 总调用被 should_stop 截断、finished 及时、_set_running(False) 恢复「开始生成」、停止按钮文案回「停止生成」

19.25 AI 穿搭处理明细表列宽微调 — docs/11 §10

前置阅读:

  • docs/11-ai-outfit.md(§10 界面)
  • docs/07-ui-design.md(表格与工具型布局原则)
  • src/app/widgets/ai_outfit_panel.py(_build_center / 处理明细表)

背景:

处理明细表中「行」「货号」「状态」等字段内容短,却占用过多横向空间;「标题」字段更长、更需要可读宽度。应缩小短字段列,把主要剩余宽度给「标题」。

任务:

  • ai_outfit_panel.py:新增处理明细表列宽配置;「行」「货号」「状态」固定窄列,「衣服图」「结果 / 原因」给可拖动默认宽度,「标题」使用 Stretch 占用剩余空间
  • test_ai_outfit_panel.py:补离屏测试,断言短字段列固定、标题列 Stretch
  • [~] 验证:python -m py_compile src/app/widgets/ai_outfit_panel.py、python tests/test_ai_outfit_panel.py、离屏启动主窗口通过;全套 python -m unittest discover -s tests 当前被工作区未提交的 packaging/default_config/ai_models.json 改动阻塞(模型顺序/额外条目与出厂模板规范不一致),待清理该文件后重跑

19.26 处理明细表:再缩「衣服图 / 结果·原因」、再宽「标题」 — docs/11 §10

前置阅读:

  • docs/11-ai-outfit.md(§10 中栏 列宽)
  • src/app/widgets/ai_outfit_panel.py(_configure_detail_table_columns)
  • tests/test_ai_outfit_panel.py(test_detail_table_compacts_short_columns_and_stretches_title)

背景:

承接 §19.25。「衣服图」「结果 / 原因」仍偏宽,再收窄,把更多横向空间让给 Stretch 的「标题」列。

任务:

  • ai_outfit_panel.py _configure_detail_table_columns:把「衣服图」(col 3) 初值 150→约 90、「结果 / 原因」(col 5) 初值 220→约 130(仍 Interactive 可手拖);「行/货号/状态」固定窄列与「标题」Stretch 不变 → 标题自动更宽
  • test_ai_outfit_panel.py:在既有列宽用例补断言——columnWidth(3) <= 90、columnWidth(5) <= 130(标题仍 Stretch、短列仍 Fixed 不变)
  • 验证:test_ai_outfit_panel.py + 全套 py37 通过(test_config_service 的 packaging 模板失败属并行 §19.13,无关);离屏断言新列宽

19.27 修复:标题生成对「标题(A) 空」的印花表误报无行 — docs/11 §17.1

前置阅读:

  • docs/11-ai-outfit.md(§17.1 行来源)
  • src/services/excel_service.py(read_all_rows 有效性判定 _is_empty(title) or _is_empty(garment_path))
  • src/app/widgets/ai_outfit_panel.py(_TitleWorker.run 调 read_all_rows、_on_title_finished 的「该表没有可处理的行」)
  • tests/test_excel_service.py(read_all_rows 用例)

背景(catch-22):

「生成标题」用 read_all_rows 取行,而它要求 标题(A)+原始图片路径(C) 都非空;但标题生成的目的就是填 A。印花导出表若 A 为空(被清空/早期导出),6 行全被跳过 → 误报「该表没有可处理的行」。实测 output/20260623_094529.xlsx:A 全空、C 是印花子目录。修复:标题生成的行来源只要求 C 非空、A 可空。

任务:

  • excel_service.py:read_all_rows(excel_path, require_title=True) 加开关——require_title=False 时只在 _is_empty(garment_path) 跳过(允许 A 空);默认 True 行为不变。更新 docstring
  • ai_outfit_panel.py:_TitleWorker.run 改调 read_all_rows(self._excel_path, require_title=False);_reload_after_titles 仍可用默认(重载后 A 已填);图片生成/预览/_show_no_pending_message 不动(仍要求 A)
  • tests/test_excel_service.py:补用例——A 空、C 非空的行:require_title=False 收录、默认(True)跳过;既有用例保持绿
  • 验证:相关单测 + 全套 py37 通过;离屏冒烟:A 全空、C=印花目录的表,标题生成能加载 N 行(不再「无可处理行」),mock 文本模型 → 按序回填 A

19.28 AI 穿搭出厂模型/提示词运行时补全 — docs/11 §6.1 / §7 / §17.3,docs/10 §5

前置阅读:

  • docs/11-ai-outfit.md(§6.1、§7、§11、§17.3)
  • docs/10-lan-update.md(§4、§5)
  • docs/09-packaging-release.md(§7)
  • src/services/config_service.py(load_ai_models / load_outfit_prompt / load_title_prompt)
  • src/launcher.py(CONFIG_FILES / seed_defaults)
  • packaging/default_config/

背景:

现有运行时兜底只处理 ai_models.json 缺失;老用户已有 ai_models.json 时不会补新增标题模型。title_prompt.txt / outfit_prompt.txt 也可能只走代码内置默认,打包默认文件未被启动器/运行时补种。升级后会出现标题模型缺失、默认提示词文件缺失的问题。

任务:

  • 文档更新:docs/11-ai-outfit.md、docs/10-lan-update.md、docs/09-packaging-release.md 已明确出厂 title model 追加和两个 prompt txt 的非覆盖式补种
  • packaging/default_config/ai_models.json 加入 title model 模板(api_key 留空,不提交真实 key),并保持图片模型顺序符合 tests/test_config_service.py 预期
  • packaging/default_config/outfit_prompt.txt / title_prompt.txt 纳入发布包默认配置
  • src/launcher.py CONFIG_FILES 加入 outfit_prompt.txt / title_prompt.txt,首次运行播种
  • config_service:load_outfit_prompt / load_title_prompt 读取前调用运行时兜底复制;用户文件存在不覆盖
  • config_service:load_ai_models 在用户文件存在但缺 app_config.title_model 时,从 factory ai_models.json 追加同名模型;不覆盖同名模型、不改 api_key;factory 无同名只记录日志
  • 测试:缺 prompt 文件时从 factory 复制;已有 prompt 不覆盖;已有 ai_models.json 缺标题模型时追加;已有同名标题模型不重复;factory 无同名不报错
  • 验证:py_compile、test_config_service.py、test_launcher.py、全套 python -m unittest discover -s tests、离屏启动主窗口并切到 AI 穿搭页通过

19.29 AI 模型下拉过滤标题模型 — docs/11 §10 / §17.3

前置阅读:

  • docs/11-ai-outfit.md(§10 右栏、§17.3 模型与提示词)
  • src/app/widgets/ai_outfit_panel.py(apply_config / _fill_model_combo / _resolve_title_model_config)
  • tests/test_ai_outfit_panel.py(模型下拉与标题模型查找用例)

背景:

§19.28 后出厂 ai_models.json 同时包含图片模型和标题文本模型。右栏「AI 模型」下拉用于图片生成,但当前会显示 app_config.title_model 对应的标题模型(默认 GPT-5.5 文本),用户可能误选文本模型去生成图片,导致开始生成后失败。

任务:

  • 文档更新:docs/11-ai-outfit.md 已明确右栏图片 AI 模型 下拉填充时跳过 name == app_config.title_model 的模型;不新增 usage 字段
  • ai_outfit_panel.py:_fill_model_combo() 按 self._title_model_name 过滤图片模型下拉;标题模型仍保留在 self._models 中供 _resolve_title_model_config() 使用
  • 行为:如果过滤后没有可选图片模型,显示「未配置可用图片模型」类占位并保持开始生成前校验提示
  • 测试:标题模型不出现在图片 AI 模型下拉;图片模型仍显示;标题生成仍能按 title_model 找到同名模型;只有标题模型时下拉为空/占位
  • 验证:py_compile、test_ai_outfit_panel.py、全套 python -m unittest discover -s tests、离屏启动 AI 穿搭页通过

19.30 标题生成读取超时改用 4K 档(240s→600s) — docs/11 §17.2 / §17.8

前置阅读:

  • docs/11-ai-outfit.md(§17.2 文本服务、§17.8 决策)
  • src/core/ai_title.py(generate_titles 调 generate_texts)
  • src/services/ai_text_service.py(generate_texts/generate_text 的 resolution 形参、_post 的 read_timeout 取值)
  • src/services/ai_image_service.py(RESOLUTION_TIMEOUTS / resolution_timeout)
  • tests/test_ai_title.py、tests/test_ai_text_service.py

背景:

标题是「一次请求、纯提示词、生成多条」(§17.7)。这次请求要经中转站转发 + 大模型排队 + 生成多条再整段返回,比一张图更耗时。原借 1K 档 240 秒读取超时,慢/排队型模型常在返回前 ReadTimeout。改为借 4K 档 600 秒(resolution_timeout:512/1K/2K/4K → 180/240/360/600 秒),只改标题链路,不动 generate_texts 默认 1K。

任务:

  • 文档更新:docs/11-ai-outfit.md §17.2 补读取超时说明、新增 §17.8 决策
  • ai_title.py:加常量 _TITLE_TIMEOUT_RESOLUTION = "4K"(带注释说明借 4K 档的 600s);generate_titles 改调 client.generate_texts(prompt, resolution=_TITLE_TIMEOUT_RESOLUTION)
  • 不动 generate_texts/generate_text 默认 resolution="1K";模型条目 timeout_seconds>0 仍优先覆盖(_post 逻辑不变)
  • 测试:tests/test_ai_title.py 断言 generate_titles 以 resolution="4K" 调 generate_texts(mock 客户端捕获 kwargs);既有用例保持绿
  • 验证:test_ai_title.py、test_ai_text_service.py、全套 py37 通过(test_config_service 的 packaging 模板失败属并行历史遗留,无关)

19.31 标题解析:过滤模型「前导思考」,只取标题 — docs/11 §17.2 / §17.3 / §17.9

前置阅读:

  • docs/11-ai-outfit.md(§17.1 / §17.2 / §17.3 / §17.9)
  • src/services/ai_text_service.py(_TITLE_SPLIT / _clean_titles / extract_titles_from_response)
  • src/services/config_service.py(DEFAULT_TITLE_PROMPT)
  • tests/test_ai_text_service.py

背景:

推理型文本模型即便提示词写明「只返回逗号连接的标题」,仍常在标题前面先输出一段思考散文(实测:「我會直接產出符合格式的標題,並先用字元計數檢查…然後一次輸出 42 個標題。【台灣現貨】…」)。这段散文自身带逗号,直接逗号切分会切出假标题混进结果。加一步剥离前导思考:哨兵优先(===TITLES===)+ 句号兜底(切最后一个 。!? 及之前)。发布包 title_prompt.txt 是用户并行改的精细模板(未提交),本任务不碰它,只改代码内置默认 + 解析。

任务:

  • 文档更新:docs/11 §17.2 补「剥离前导思考」、§17.3 补哨兵约定、新增 §17.9 决策
  • ai_text_service.py:加 _TITLE_SENTINEL(=+\s*TITLES\s*=+,忽略大小写)+ _strip_title_preamble(text);_clean_titles 切分前先调用
  • _strip_title_preamble:① 有哨兵取最后一个哨兵之后;② 无哨兵切最后一个 。!? 及之前,仅当其后仍含 [,,\r\n] 分隔符时才剥离(防末条句号清空)
  • config_service.py:DEFAULT_TITLE_PROMPT 补哨兵约定(思考写最前、单独一行 ===TITLES===、其后只放逗号标题)
  • 不碰 packaging/default_config/title_prompt.txt(用户精细模板,并行未提交工作)
  • 测试:真实前导思考样本(带 。+逗号列表)被剥离、取全部标题;哨兵优先于句号且忽略思考内逗号;无前导的纯逗号列表不变;末条带收尾 。、无前导时不误切清空
  • 验证:test_ai_text_service.py、test_ai_title.py、全套 py37 通过;离屏冒烟用真实返回样本走 extract_titles_from_response

19.32 标题生成等待计时显示 — docs/11 §17.10

前置阅读:

  • docs/11-ai-outfit.md(§17.4、§17.8、§17.10)
  • src/app/widgets/ai_outfit_panel.py(标题生成组、_start_title、_on_title_*、线程清理)
  • src/core/ai_title.py(标题生成使用 4K 档 600 秒)
  • src/services/ai_text_service.py(模型 timeout_seconds 覆盖读取超时)

背景:

用户在标题提示词里要求一次生成上百条标题时,标题生成是一次长请求;请求期间程序无法知道模型真实进度,旧进度条只在返回并写回 Excel 后才推进,容易让用户误以为卡死。需要在「标题生成提示词」label 右侧显示等待计时,例如 等待中 01:35 / 10:00。

任务:

  • ai_outfit_panel.py:在「标题生成提示词」label 右侧新增等待计时 QLabel,默认隐藏或空白,不挤压编辑框
  • 点击「生成标题」后启动 1 秒计时器,显示 等待中 mm:ss / MM:SS
  • 最大等待默认取标题生成 600 秒;若当前标题模型 timeout_seconds > 0,显示该覆盖值
  • 请求返回并进入写回阶段时,文案切换为 写入中 已完成/总数 或同等清晰状态;完成后短暂停留并复位
  • 失败、超时、停止、线程清理时停止计时并复位,避免残留旧状态
  • 不把该显示做成真实百分比进度,不影响右栏本次进度条和明细表刷新
  • 测试:离屏断言点击「生成标题」后计时 label 变为等待中;mock 超时值覆盖;完成/失败后复位

验收:

  • 长时间标题生成期间,用户能看到已等待时间和最长等待时间
  • 计时文案不暗示模型内部完成百分比
  • 标题生成和图片生成互斥、原有进度/明细行为不回退
  • 验证:python -m py_compile src/app/widgets/ai_outfit_panel.py tests/test_ai_outfit_panel.py、python tests/test_ai_outfit_panel.py、离屏启动主窗口通过;全套 python -m unittest discover -s tests 仅失败于既有未提交 packaging/default_config/ai_models.json 含非空 api_key,与本任务无关

19.33 标题等待计时状态颜色 — docs/11 §17.10

前置阅读:

  • docs/11-ai-outfit.md(§17.10)
  • src/app/widgets/ai_outfit_panel.py(_title_wait_label、_set_title_wait_text、_finish_title_wait)
  • tests/test_ai_outfit_panel.py(标题等待计时离屏测试)

背景:

标题等待计时 label 已能显示 等待中 mm:ss / MM:SS,但纯黑文字在左栏里容易被标题和提示词区域淹没。需要用低干扰状态色提高可见性,但不能做成真实进度或强警告。

任务:

  • 等待中 使用信息蓝 #0078d4
  • 写入中 和纯成功完成使用完成绿 #107c10
  • 生成失败、等待超时、部分失败完成使用错误红 #c42b1c
  • 只改变文字颜色,不增加背景色、闪烁或字号变化
  • 测试:离屏断言等待、写入、成功、失败状态使用对应颜色

验收:

  • 长等待时 label 更醒目但不喧宾夺主
  • 成功/失败状态一眼可辨
  • 不影响原有等待计时、写回进度和复位行为
  • 验证:python -m py_compile src/app/widgets/ai_outfit_panel.py tests/test_ai_outfit_panel.py、python tests/test_ai_outfit_panel.py、离屏启动主窗口通过;全套 python -m unittest discover -s tests 仅失败于既有未提交 packaging/default_config/ai_models.json 含非空 api_key,与本任务无关

19.34 添加印花预览:Esc 隐藏选中框与控制点 — docs/08 §5.1

前置阅读:

  • docs/08-image-editor-design.md(§5.1 预览控制层显隐)
  • src/app/widgets/image_canvas.py(_CanvasView、_sel_rect、_corner_handles、_rot_line、_rot_handle、_hit_test、_on_press)
  • tests/ 中现有离屏 GUI 测试写法

背景:

「添加印花」模块中,选择衣服文件夹和印花文件夹后,中间预览区域会把印花叠加在衣服图片上,并显示蓝色选中框、缩放控制点和旋转控制点。用户希望按 Esc 时临时隐藏这些控制框和控制点,让预览更接近最终效果;之后再次点击预览区的印花时,仍能恢复并按原有方式拖动、缩放、旋转。

任务:

  • image_canvas.py:让 _CanvasView 可以获得键盘焦点,并在点击预览区时获取焦点
  • image_canvas.py:新增控制层显隐状态,例如 _controls_visible
  • image_canvas.py:新增统一方法显示 / 隐藏 _sel_rect、_corner_handles、_rot_line、_rot_handle
  • image_canvas.py:按 Esc 时只隐藏交互控制层,不隐藏 _print_item
  • image_canvas.py:按 Esc 不修改 TransformState,不触发导出参数变化
  • image_canvas.py:控制层隐藏后,点击印花图片先恢复控制层,再沿用现有拖动逻辑
  • image_canvas.py:隐藏期间缩放控制点和旋转控制点不需要响应命中测试;恢复后原有缩放、旋转行为保持不变
  • image_canvas.py:重新加载印花图片或创建新的印花图层时,控制层默认显示
  • 测试或离屏验证:覆盖 Esc 隐藏、点击印花恢复、TransformState 不变

验收:

  • 点击预览区后按 Esc,蓝色选中框、缩放控制点、旋转连接线和旋转控制点隐藏
  • 按 Esc 后印花图片仍然显示,预览不变为空白
  • 控制层隐藏后再次点击印花,控制层恢复,并可继续拖动、缩放、旋转
  • 控制层显隐不改变 X/Y、宽高、角度,也不影响单张导出和批量导出
  • 通过必要语法检查和离屏 GUI 验证:python -m py_compile src/app/widgets/image_canvas.py tests/test_image_canvas.py、python tests/test_image_canvas.py

19.35 添加印花预览:点击空白区域隐藏控制层 — docs/08 §5.1

前置阅读:

  • docs/08-image-editor-design.md(§5.1 预览控制层显隐)
  • src/app/widgets/image_canvas.py(_hit_test、_on_press、_hide_controls)
  • tests/test_image_canvas.py

背景:

§19.34 已支持按 Esc 隐藏选中框和控制点。用户继续要求:点击图片两边的灰色预览区域时,也能隐藏当前印花的选中框和控制点,便于快速查看接近最终导出的干净预览。

任务:

  • 文档:补充点击未命中印花 / 控制点的预览空白区域时隐藏控制层
  • image_canvas.py:_on_press 命中测试为空时调用 _hide_controls(),不启动拖动
  • image_canvas.py:点击空白区域只隐藏控制层,不隐藏 _print_item,不修改 TransformState
  • 测试:补充点击空白 scene 坐标隐藏控制层且状态不变

验收:

  • 控制层显示时,点击预览两侧灰色区域会隐藏选中框、缩放控制点和旋转控制点
  • 点击空白区域后印花图片仍显示,X/Y、宽高、角度不变
  • 点击印花图片仍恢复控制层并保持原有拖动行为
  • 验证:python -m py_compile src/app/widgets/image_canvas.py tests/test_image_canvas.py、python tests/test_image_canvas.py