Files
cmbot/tasks.md
T

1594 lines
93 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 开发任务清单
## 任务文档说明
本文档用于跟踪自动合成印花服饰效果图工具的整体开发任务。后续 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. 已完成基础工作
- [x] 编写项目愿景文档:`docs/01-product-vision.md`
- [x] 编写 PRD:`docs/02-prd.md`
- [x] 编写技术栈文档:`docs/03-technical-stack.md`
- [x] 编写开发规则文档:`docs/04-development-rules.md`
- [x] 编写项目架构文档:`docs/05-project-architecture.md`
- [x] 编写 UI 效果图提示词:`docs/06-ui-mockup-prompt.md`
- [x] 整理 Claude UI 提示词版本:`docs/06-ui-mockup-prompt-claude.md`
- [x] 保存 UI 效果图设计稿:`docs/ui-v1.html`
- [x] 保存 UI 效果图截图:`docs/ui-v1.png`
- [x] 编写 UI 设计文档:`docs/07-ui-design.md`
- [x] 编写图像编辑器设计文档:`docs/08-image-editor-design.md`
- [x] 编写本地打包发布文档:`docs/09-packaging-release.md`
- [x] 创建 `requirements.txt`
- [x] 创建 `src/version.py`,统一维护 `APP_NAME` 和 `APP_VERSION`
- [x] 创建 `src/main.py` 最小 PySide6 入口
- [x] 创建 `src/app/main_window.py` 最小主窗口
- [x] 创建 `src/app/widgets/` 占位模块
- [x] 创建 `src/core/` 占位模块
- [x] 创建 `src/services/` 占位模块
- [x] 创建 `src/resources/` 资源目录
- [x] 创建 `tests/` 测试目录
- [x] 验证本机 Python 版本为 `3.7.9`
- [x] 验证 PySide6 版本为 `6.5.3`
- [x] 通过 `python -m py_compile` 语法检查
- [x] 验证最小 PySide6 窗口可以创建并关闭
- [x] 已提交文档规划代码
- [x] 已提交 PySide6 工程骨架代码
## 1. 工程基础完善
### 1.1 日志服务
前置阅读:
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
任务:
- [x] 先读取 `src/services/log_service.py` 现有内容
- [x] 完善 `src/services/log_service.py`
- [x] 启动时创建 `logs/` 目录
- [x] 日志文件按启动时间或日期命名
- [x] 日志格式包含时间、模块、级别、消息
- [x] 在 `src/main.py` 中初始化日志
验收:
- [x] 启动程序后生成日志文件
- [x] 程序启动信息写入日志
- [x] 不使用 `print` 作为正式日志
### 1.2 路径与资源服务
前置阅读:
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
- `docs/09-packaging-release.md`
任务:
- [x] 在 `src/services/file_service.py` 中新增路径辅助函数(不单独建模块)
- [x] 实现 `get_app_dir()`:返回程序根目录,兼容开发环境和 PyInstaller 打包环境
- [x] 实现 `get_resource_path(relative_path)`:返回资源文件绝对路径
- [x] 实现 `get_config_path(relative_path)`:返回配置文件绝对路径
- [x] 实现 `get_log_dir()`:返回日志目录路径
- [x] 实现 `get_output_dir()`:返回默认输出目录路径
- [x] 保证 Windows 中文路径可用
验收:
- [x] 路径函数不使用开发机绝对路径
- [x] 程序目录下缺少 `logs/` 或 `output/` 时可自动创建
### 1.3 配置服务
前置阅读:
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
任务:
- [x] 先读取 `src/services/config_service.py` 现有内容
- [x] 完善 `src/services/config_service.py`
- [x] 定义默认配置
- [x] 支持读取 JSON 配置
- [x] 支持保存 JSON 配置
- [x] 配置损坏时使用安全默认值并记录日志
验收:
- [x] 缺少配置文件时程序可启动
- [x] 配置文件损坏时程序可启动并记录日志
- [x] 不会无提示清空用户配置
## 2. 核心数据模型
前置阅读:
- `docs/02-prd.md`
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
- `docs/08-image-editor-design.md`
任务:
- [x] 先读取 `src/core/models.py` 现有内容
- [x] 完善 `src/core/models.py`
- [x] 定义 `ImageAsset`
- [x] 定义 `TransformState`
- [x] 定义 `Template`
- [x] 定义 `ExportOptions`
- [x] 定义 `BatchOptions`
- [x] 定义 `ComposeResult`
- [x] 定义 `BatchResult`
验收:
- [x] `models.py` 不依赖 PySide6 UI 控件
- [x] 变换状态包含 `x/y/width/height/rotation/keep_aspect_ratio`
- [x] 模型可以被 core、services 和 UI 层复用
## 3. 文件扫描与素材导入
前置阅读:
- `docs/02-prd.md`
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
- `docs/07-ui-design.md`
任务:
- [x] 先读取 `src/services/file_service.py` 现有内容
- [x] 完善 `src/services/file_service.py`
- [x] 支持递归扫描衣服图片文件夹
- [x] 支持递归扫描印花图片文件夹
- [x] 支持格式:PNG、JPG、JPEG、WEBP
- [x] 忽略不支持格式并记录日志
- [x] 保留来源子文件夹信息
- [x] 生成安全输出文件名
验收:
- [x] 可以扫描包含子文件夹的中文路径
- [x] 不修改原始素材文件
- [x] 不支持文件格式不会导致扫描失败
## 4. 模板服务
前置阅读:
- `docs/02-prd.md`
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
任务:
- [x] 先读取 `src/services/template_service.py` 现有内容
- [x] 完善 `src/services/template_service.py`
- [x] 定义内置模板
- [x] 支持读取自定义模板 JSON
- [x] 支持保存自定义模板 JSON
- [x] 支持新增、重命名、删除自定义模板
- [x] 校验模板字段
验收:
- [x] 内置模板和自定义模板可区分
- [x] 单个模板损坏不影响全部模板加载
- [x] UI 不直接操作模板 JSON 文件
## 5. 图片合成核心
前置阅读:
- `docs/02-prd.md`
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
- `docs/08-image-editor-design.md`
任务:
- [x] 先读取 `src/core/composer.py` 现有内容
- [x] 完善 `src/core/composer.py`
- [x] 使用 Pillow 读取衣服底图和印花图
- [x] 支持透明 PNG alpha 合成
- [x] 支持按 `TransformState` 缩放印花
- [x] 支持按 `TransformState` 旋转印花
- [x] 支持旋转后按中心点对齐粘贴
- [x] 支持 PNG 导出
- [x] 支持 JPG 导出
- [x] 输出文件名避免默认覆盖
验收:
- [x] 合成逻辑不依赖 PySide6 UI 控件
- [x] 输出尺寸默认与衣服底图一致
- [x] 透明 PNG 不出现黑底或白底
- [x] 印花部分超出画布时不会报错
## 6. 批量任务核心
前置阅读:
- `docs/02-prd.md`
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
任务:
- [x] 先读取 `src/core/batch.py` 现有内容
- [x] 完善 `src/core/batch.py`
- [x] 支持多衣服 × 单印花
- [x] 支持单衣服 × 多印花
- [x] 支持一一匹配
- [x] 支持全组合
- [x] 复用 `core/composer.py`
- [x] 单个任务失败时继续处理剩余任务
- [x] 汇总成功数量、失败数量和失败原因
验收:
- [x] 不复制第二套合成算法
- [x] 单张失败不会中断整个批量任务
- [x] 失败原因可供 UI 和日志使用
## 6.5 低对比组合筛选核心
前置阅读:
- `docs/02-prd.md`
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
- `docs/08-image-editor-design.md`
任务:
- [x] 定义可见度状态:`正常`、`偏低`、`不明显`、`无法判断`
- [x] 新增低对比分析核心函数,位置应符合架构分层,不能写进 UI 事件
- [x] 基于当前模板或 `TransformState` 获取衣服目标区域
- [x] 只分析衣服目标区域颜色,不使用整张衣服图判断
- [x] 只统计印花 alpha 有效像素,例如 `alpha > 20`
- [x] 计算 RGB 颜色距离
- [x] 计算亮度差
- [x] 根据阈值输出可见度状态和评分
- [x] 单个组合分析失败时返回 `无法判断`,不得中断整个队列
- [x] 为后续队列项保存可见度结果预留字段或结果结构
验收:
- [x] 白色衣服配浅色印花时可标记为 `偏低` 或 `不明显`
- [x] 深色衣服配深色印花时可标记为 `偏低` 或 `不明显`
- [x] 透明 PNG 的透明区域不参与印花颜色判断
- [x] 低对比分析不修改原始衣服图片和原始印花图片
- [x] 分析逻辑不依赖 PySide6 UI 控件
- [x] 失败项不会中断批量队列生成或导出流程
## 7. 主界面布局
前置阅读:
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
- `docs/07-ui-design.md`
任务:
- [x] 先读取 `src/app/main_window.py` 现有内容
- [x] 将 `src/app/main_window.py` 从最小窗口扩展为主布局
- [x] ~~实现顶部标题和版本号~~(后移除内容区自绘标题栏:与系统窗口标题重复,名称/版本改由系统标题栏显示,见 17.7)
- [x] 实现流程页签区域
- [x] 实现左侧素材栏容器
- [x] 实现中间预览区容器
- [x] 实现右侧参数栏容器
- [x] 实现底部合成队列容器
- [x] 实现底部状态栏
验收:
- [x] 窗口标题显示 `APP_NAME` 和 `APP_VERSION`
- [x] UI 布局接近 `docs/ui-v1.png`
- [x] 不把所有业务逻辑塞进 `main_window.py`
## 8. 素材列表 UI
前置阅读:
- `docs/02-prd.md`
- `docs/05-project-architecture.md`
- `docs/07-ui-design.md`
任务:
- [x] 先读取 `src/app/widgets/image_list_panel.py` 现有内容
- [x] 实现衣服图片列表
- [x] 实现印花图片列表
- [x] 实现打开文件夹按钮
- [x] 实现全选和取消全选
- [x] 实现单张勾选和取消勾选
- [x] 显示已选数量和总数量(例如:`含子文件夹 · 共 12`)
- [x] 显示来源子文件夹标签和文件名
- [x] 实现排序选择器(默认按文件夹+名称排序)
- [x] 点击素材后更新当前预览选择
验收:
- [x] 支持递归导入素材
- [x] 当前预览选中态和批量勾选态可区分
- [x] 不在 UI 层重复实现文件扫描规则
## 9. 图像预览编辑器
前置阅读:
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
- `docs/07-ui-design.md`
- `docs/08-image-editor-design.md`
任务:
- [x] 先读取 `src/app/widgets/image_canvas.py` 现有内容
- [x] 使用 `QGraphicsView / QGraphicsScene`
- [x] 显示衣服底图(scene 坐标与原图像素坐标保持一致)
- [x] 显示印花图层
- [x] 实现印花拖动
- [x] 实现缩放控制点(至少四角)
- [x] 实现旋转控制点(顶部中心外侧)
- [x] 实现预览缩放(通过 `QGraphicsView` view transform,不修改 scene 内容)
- [x] 实现预览顶部栏:显示当前底图文件名、印花文件名、操作模式按钮(移动/缩放/旋转)、缩放比例控制
- [x] 实现画布操作提示 Overlay(拖动移动 / 四角缩放 / 顶部旋转 / Shift 锁比例)
- [x] 将交互结果同步为 `TransformState`
验收:
- [x] 预览缩放不改变真实合成参数
- [x] 拖动后坐标为衣服原图像素坐标
- [x] 缩放后宽高同步
- [x] 旋转后角度同步
- [x] 不使用截图作为导出结果
## 10. 参数面板 UI
前置阅读:
- `docs/05-project-architecture.md`
- `docs/07-ui-design.md`
- `docs/08-image-editor-design.md`
任务:
- [x] 先读取 `src/app/widgets/transform_panel.py` 现有内容
- [x] 完善 `src/app/widgets/transform_panel.py`
- [x] 实现 X/Y 坐标输入
- [x] 实现宽度/高度输入
- [x] 实现锁定宽高比例
- [x] 实现角度输入
- [x] 实现左旋 90 度和右旋 90 度按钮
- [x] 与 `ImageCanvas` 双向同步 `TransformState`
验收:
- [x] 参数输入后预览同步更新
- [x] 预览交互后参数输入框同步更新
- [x] 程序化更新输入框不会造成信号循环
## 11. 模板面板 UI
前置阅读:
- `docs/02-prd.md`
- `docs/05-project-architecture.md`
- `docs/07-ui-design.md`
任务:
- [x] 先读取 `src/app/widgets/template_panel.py` 现有内容
- [x] 完善 `src/app/widgets/template_panel.py`
- [x] 显示模板下拉框
- [x] 支持选择模板
- [x] 支持保存模板
- [x] 支持另存为模板
- [x] 支持重置为模板
- [x] 通过 `template_service` 操作模板
验收:
- [x] 选择模板后更新预览和参数面板
- [x] UI 不直接修改模板 JSON
- [x] 自定义模板重启后仍可用
## 12. 导出面板 UI
前置阅读:
- `docs/05-project-architecture.md`
- `docs/07-ui-design.md`
- `docs/09-packaging-release.md`
任务:
- [x] 先读取 `src/app/widgets/export_panel.py` 现有内容
- [x] 完善 `src/app/widgets/export_panel.py`
- [x] 实现输出目录选择
- [x] 实现输出格式选择
- [x] 实现质量选择
- [x] 实现导出当前单张
- [x] ~~实现应用为模板入口~~(后移除:与模板区域「另存为」重复,见 17.3)
- [x] ~~实现重置为模板入口~~(后移除:与模板区域「重置为模板」重复,见 17.3)
- [x] 显示当前操作影响范围提示
验收:
- [x] 缺少衣服或印花时导出按钮禁用
- [x] 输出目录不可写时提示用户并记录日志
- [x] 单张导出调用 `core/composer.py`
## 13. 合成队列 UI
前置阅读:
- `docs/02-prd.md`
- `docs/05-project-architecture.md`
- `docs/07-ui-design.md`
任务:
- [x] 实现底部合成队列表格(建议使用 `QTableView` + 模型视图结构)
- [x] 实现队列头部批量模式切换:多衣服×单印花 / 单衣服×多印花 / 一一匹配 / 全组合(矩阵)
- [x] 实现队列统计显示(共 N 项 · 完成 · 进行 · 失败 · 待导出)
- [x] 实现「重置全部」按钮
- [x] 实现「导出选中」按钮
- [x] 实现「开始批量导出」按钮(导出中切换为停止/暂停)
- [x] 实现队列折叠/展开
- [x] 显示任务状态(已完成 / 待导出 / 导出中 / 失败)
- [x] 导出中任务显示进度条
- [x] 显示失败原因
- [x] 支持选中队列项并更新预览区和参数面板
验收:
- [x] 队列统计显示总数、完成、进行、失败、待导出
- [x] 导出中任务显示进度条
- [x] 失败行显示可理解原因
- [x] 选中队列项可以加载对应状态
## 14. 微调状态
前置阅读:
- `docs/07-ui-design.md`
- `docs/08-image-editor-design.md`
任务:
- [x] 定义队列项微调状态
- [x] 手动调整队列项后标记为 `已微调`
- [x] 微调项保存独立 `TransformState`
- [x] 批量导出时微调项使用自己的参数
- [x] 未微调项使用模板参数
验收:
- [x] 已微调项不会被无提示覆盖
- [x] 队列表格能显示 `模板` 和 `已微调`
## 15. 测试
前置阅读:
- `docs/04-development-rules.md`
- `docs/05-project-architecture.md`
- `docs/08-image-editor-design.md`
任务:
- [x] 编写 `tests/test_composer.py`
- [x] 编写 `tests/test_templates.py`
- [x] 测试透明 PNG 合成
- [x] 测试缩放参数
- [x] 测试旋转中心
- [x] 测试文件扫描
- [x] 测试模板读写
- [x] 测试批量任务生成
验收:
- [x] 核心逻辑测试不依赖 GUI
- [x] 测试可在 Python 3.7 环境运行
## 16. 打包
前置阅读:
- `docs/03-technical-stack.md`
- `docs/04-development-rules.md`
- `docs/09-packaging-release.md`
任务:
- [x] 创建打包脚本
- [x] 验证 PyInstaller `onedir` 打包
- [x] 打包资源文件
- [x] 打包默认配置和模板
- [x] 验证打包后程序启动
- [x] 验证标题栏版本号
- [x] 验证单张导出
验收:
- [ ] 打包产物可在无 Python 环境的 Windows 电脑启动
- [x] 发布包不包含源代码、缓存、测试输出和私人配置
## 17. 体验优化(持久化与独立重置)
第二阶段根据实际使用反馈新增。
### 17.1 用户偏好持久化
前置阅读:
- `docs/02-prd.md`(6.6 模板选择、6.9 批量合成导出、9 数据与文件结构)
- `docs/05-project-architecture.md`(4.12 config_service)
任务:
- [x] 先读取 `src/services/config_service.py` 与 `src/app/main_window.py` 现有内容
- [x] 在 `main_window` 启动时调用 `load_config()` 并把初值分发给各面板(集中管理,不让控件各自读写)
- [x] 在 `config_service` 默认配置中新增 `last_template`、`last_batch_mode`
- [x] 记住并在启动时恢复上次选择的模板;自定义模板已删除时回退到第一个内置模板
- [x] 记住并在启动时恢复上次选择的批量模式;持久化值非法时回退默认(全组合)
- [x] 面板选择变化时「改一次存一次」回写配置
- [x] `template_panel` 增加模板选择变化信号(携带模板名)
- [x] `queue_panel` 增加 `set_batch_mode()` 与批量模式变化信号
验收:
- [x] 重启后自动恢复上次选择的模板和批量模式
- [x] 自定义模板被删除或批量模式值非法时安全回退,不报错
- [x] UI 控件不直接读写配置文件
### 17.2 位置 / 尺寸 / 旋转独立重置
前置阅读:
- `docs/07-ui-design.md`(7 右侧参数栏、10 视觉规范)
- `docs/08-image-editor-design.md`
任务:
- [x] 先读取 `src/app/widgets/transform_panel.py` 现有内容
- [x] 在 `位置` / `尺寸` / `旋转` 三个分区标题右侧各加一个独立 `重置` 按钮
- [x] 各重置只把对应参数还原为当前选中模板的值,互不影响(位置→仅 X/Y;尺寸→仅宽/高,位置不变;旋转→仅角度)
- [x] 移除合并的 `重置位置 / 尺寸 / 角度` 按钮与角度旁的 `归零` 按钮
- [x] 重置按钮采用幽灵样式(静止灰、悬停蓝、按下深蓝、禁用更灰),不使用红色
- [x] 未选模板或未加载衣服/印花时三个按钮置灰
- [x] 由 `main_window` 取模板换算值并按项合并进当前 `TransformState`
验收:
- [x] 位置、尺寸、旋转可分别独立重置,互不影响
- [x] 尺寸重置只改变宽 / 高,不改变位置 X / Y
- [x] 按钮视觉层级符合 `docs/07-ui-design.md` 第 10 节
### 17.3 按钮去重(导出面板模板入口)
前置阅读:
- `docs/07-ui-design.md`(7.1 模板区域、7.7 操作按钮)
任务:
- [x] 移除导出面板的 `应用为模板` 与 `重置为模板` 按钮(与模板区域的 `另存为` / `重置为模板` 是同一动作的重复入口)
- [x] 模板相关操作统一留在模板区域,导出面板只负责输出与导出
验收:
- [x] 同一个动作在界面上只出现一次,命名一致
- [x] 导出面板仅保留输出设置与 `导出当前单张`
### 17.4 「重置为模板」归位并更名为「位置尺寸旋转重置为当前模板」
前置阅读:
- `docs/07-ui-design.md`(7 右侧参数栏、10 视觉规范)
任务:
- [x] 把模板区域的 `重置为模板` 按钮移出,移到参数栏底部(旋转分区下方)
- [x] 更名为 `位置尺寸旋转重置为当前模板`,与三个分项 `↺ 重置` 构成「全部 + 分项」两级
- [x] 还原逻辑改为把位置/尺寸/旋转整体设为模板值,并与分项重置同走 `_apply_preview_transform`
- [x] 与三个分项重置一起受「未加载图片时置灰」控制
- [x] 采用安静、全宽、悬停出强调色的样式(不使用红色)
验收:
- [x] 模板区域只剩 `保存` / `另存为`,不含整体重置
- [x] `位置尺寸旋转重置为当前模板` 在旋转分区下方,未加载图片时置灰
- [x] 点击后位置、尺寸、旋转一并还原为当前选中模板
### 17.5 模板删除按钮
前置阅读:
- `docs/07-ui-design.md`(7.1 模板区域)
任务:
- [x] 在模板区域增加 `删除` 按钮(复用 `template_service.delete_template`)
- [x] `删除` 仅对自定义模板可用,选中内置模板时隐藏(与 `保存` 一致)
- [x] 删除前弹二次确认对话框;确认后才删除
- [x] 删除后刷新下拉框并回选第一个内置模板,同步持久化选择
- [x] `删除` 用红色文字提示危险但保持安静样式
验收:
- [x] 可在界面上删除自定义模板,无需手改 `templates.json`
- [x] 内置模板不显示 `删除`
- [x] 删除有二次确认,取消则不删
### 17.6 参数/导出面板紧凑化与命名
前置阅读:
- `docs/07-ui-design.md`(7.2 位置区域、7.3 尺寸区域、7.5 输出设置区域)
任务:
- [x] 「位置」的 X、Y 输入框放在同一行(标签简化为 `X` / `Y`)
- [x] 「尺寸」的宽、高输入框放在同一行(标签简化为 `宽` / `高`),`锁定宽高比例` 在其下方
- [x] 导出面板「格式」「质量」放在同一行
- [x] 底部整体重置按钮更名为 `位置尺寸旋转重置为当前模板`
- [x] 仅改布局/文案,不动同步与导出逻辑
验收:
- [x] X/Y 一行、宽/高一行、格式/质量一行,界面更紧凑
- [x] 拖动/缩放预览后输入框仍正常同步
- [x] 质量仍仅在 JPG 时可用
### 17.7 移除重复的自绘标题栏与无效「设置」按钮
前置阅读:
- `docs/07-ui-design.md`(3 整体布局、4.1 标题栏)
任务:
- [x] 移除标题栏里点了无反应的占位「设置」按钮
- [x] 移除内容区自绘标题栏(与系统窗口标题重复),名称/版本由系统标题栏(`setWindowTitle`)显示
- [x] 同步清理相关样式与未用常量
验收:
- [x] 界面不再出现重复的软件名称/版本号
- [x] 系统窗口标题仍显示 `APP_NAME` 与 `APP_VERSION`
- [x] 工作区因移除标题栏而获得更多纵向空间
### 17.8 加载文件夹后默认预览第一张
前置阅读:
- `docs/07-ui-design.md`(5.1 衣服图片面板)
任务:
- [x] 衣服、印花面板加载文件夹后,自动选中并预览第一张(按当前排序)
- [x] 空文件夹不选;把选中逻辑抽成按行选择以复用
- [x] 与现有「模板粘性」衔接:自动选印花后按当前模板落位
验收:
- [x] 加载文件夹后画布不再为空、无需手动点选
- [x] 两个文件夹加载顺序无关,最终都能得到合成预览
### 17.9 输出按「时间戳 → 印花」分组到子文件夹
前置阅读:
- `docs/02-prd.md`(6.8 单张合成导出、9 数据与文件结构)
任务:
- [x] `make_safe_output_path` 输出到 `<输出目录>/<印花文件名>/<衣服文件名>.<扩展名>`
- [x] 新增 `timestamped_run_dir(base)`,每次导出运行在输出目录下新建时间戳文件夹
- [x] 一次导出运行(批量/单张)共用同一个时间戳;批量在开始时计算一次
- [x] 单张导出、队列导出、`core.batch.run_batch` 均接入时间戳层
- [x] 子文件夹由 `composer.compose` 保存时自动创建;重名仍追加 `_1`/`_2`
验收:
- [x] 路径形如 `输出目录/<时间戳>/<印花文件名>/<衣服文件名>.<扩展名>`
- [x] 重复运行落在不同时间戳文件夹,互不覆盖
- [x] 一次批量运行内所有图片共用同一个时间戳文件夹
### 17.10 记住上次的衣服/印花文件夹
前置阅读:
- `docs/02-prd.md`(6.1 图片加载、9 数据与文件结构)
- `docs/05-project-architecture.md`(4.12 config_service)
任务:
- [x] `_AssetPanel` 打开对话框时定位到上次目录(`set_start_dir`),并在选中后发 `folder_opened` 信号
- [x] `ImageListPanel` 暴露 `set_garment_start_dir` / `set_print_start_dir` 与 `garment_folder_opened` / `print_folder_opened`
- [x] `main_window` 启动恢复 `last_garment_dir` / `last_print_dir`,并在打开文件夹时「改一次存一次」回写
- [x] UI 不直接读写配置,统一经主窗口
验收:
- [x] 重启后打开「打开文件夹」对话框定位到上次的衣服/印花目录
- [x] 目录已不存在时安全回退,不报错
### 17.11 修复:批量导出忽略所选输出格式/质量/目录
前置阅读:
- `docs/07-ui-design.md`(7.5 输出设置区域、8 底部合成队列)
问题:
- `queue_panel.set_export_options()` 从未被调用,导出面板也无「选项变化」信号,导致队列始终使用默认 `ExportOptions`(PNG、默认目录),批量导出无视 UI 所选格式/质量/目录。
任务:
- [x] 导出面板新增 `export_options_changed` 信号与公开 `current_options()`
- [x] 格式/质量/输出目录任一变化即发出当前 `ExportOptions`
- [x] `main_window` 接到 `queue_panel.set_export_options`,并在启动时同步一次
- [x] 单张导出 `_do_export` 改用同一 `current_options()` 来源
验收:
- [x] 选 JPG 后批量导出的文件为 `.jpg` 且按 JPEG 保存
- [x] 批量导出跟随所选输出目录与质量
### 17.12 合并图文件名加上印花名
前置阅读:
- `docs/02-prd.md`(6.8 单张合成导出、9 数据与文件结构)
任务:
- [x] `make_safe_output_path` 的文件名由 `<衣服文件名>` 改为 `<衣服文件名>_<印花文件名>`
- [x] 目录分组结构不变(仍 `输出目录/<时间戳>/<印花文件名>/`)
- [x] 重名仍追加 `_1`/`_2` 避免覆盖
验收:
- [x] 输出文件形如 `输出目录/<时间戳>/TY030/1_TY030.png`
- [x] 单张导出与批量导出一致
### 17.13 隐藏「导出当前单张」按钮
前置阅读:
- `docs/07-ui-design.md`(7.7 操作按钮、10 视觉规范)
任务:
- [x] 隐藏导出面板的 `导出当前单张` 按钮(`setVisible(False)`)
- [x] 保留按钮与 `_do_export` 逻辑,便于后续需要时再显示
- [x] 导出统一经底部合成队列完成
验收:
- [x] 导出面板不再显示 `导出当前单张`,只剩输出设置
- [x] 队列的批量/选中导出不受影响
### 17.14 修复:选中未微调队列项时预览未按模板落位
前置阅读:
- `docs/02-prd.md`(6.6 模板选择)
- `docs/07-ui-design.md`(12.4 选中队列项)
问题:
- `_on_queue_item_activated` 仅在 `item.transform` 存在(已微调)时覆盖画布变换;未微调项保留 `load_print` 的写死默认(印花≈衣服宽 40%、居中),预览尺寸与选中模板不一致——「模板粘性」在队列项激活路径的遗漏分支。
任务:
- [x] 选中队列项且 `item.transform` 为空时,调用 `template_panel.apply_current()` 按当前模板落位
- [x] 仍在 `_loading_queue_item` 保护下,避免被误标为「已微调」
- [x] 与预览流程(`_on_print_preview`)行为一致
验收:
- [x] 点击未微调队列项,预览印花为选中模板要求的大小/位置
- [x] 已微调项仍显示其自身参数,不被覆盖
### 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 上,印花跑到左上、尺寸只剩一半)。
- 影响批量导出与「导出选中」中所有未微调项。
任务:
- [x] 队列改存模板对象(比例坐标 `Template`)而非单个像素变换:`set_template()` 取代 `set_transform()`
- [x] 导出时按每项自身衣服/印花尺寸现算:抽纯函数 `core.composer.resolve_transform()`(已微调项用自身变换,否则 `template.to_transform_state(gw,gh,pw,ph)`)
- [x] widget 仅负责读图尺寸(`core.composer.image_size()`,只读 header)+ 单次运行内缓存
- [x] `main_window` 在 `template_changed`/`template_applied` 时把模板推给队列,并在启动时初始同步
- [x] 新增 4 个纯函数单测覆盖:已微调优先、不同尺寸各自换算、空模板/空尺寸返回 None
验收:
- [x] 一个批次内含不同尺寸的衣服/印花时,未微调项各自按模板正确落位
- [ ] GUI 实测:混合尺寸批量导出,结果图印花位置/大小均正确
### 17.16 局域网更新 · 阶段②:启动时检测并通知
前置阅读:
- `docs/10-lan-update.md`(§7 更新源、§8 流程、§16 阶段②)
- `docs/02-prd.md`(app_config 的 `update_source`)
说明:
- 阶段①(数据目录分离)已在 commit `76f2c6d` 完成。本任务实现阶段②「只读通知」,不自动安装。
任务:
- [x] 配置项 `update_source`(空 = 不检查)写入 `DEFAULT_CONFIG`
- [x] `services/update_service.py`:`parse_version` / `is_newer` / `check_for_update`,读取 `<source>/manifest.json`,任何不可达/损坏/非更新一律返回 None(不抛错、不阻塞)
- [x] 主窗口顶部通知横幅(默认隐藏):「发现新版本 vX.Y.Z」+「打开更新目录」+ 关闭
- [x] 检查在后台守护线程进行,经 Qt 队列信号回主线程显示横幅(更新源不可达不卡启动)
- [x] 「打开更新目录」用 `QDesktopServices` 打开 `manifest.source`
- [x] 15 个纯函数单测覆盖版本比较与 `check_for_update` 各分支
验收:
- [x] 单测通过;更新源为空/不可达时静默跳过
- [x] GUI 实测:配置可达更新源 + 高版本 manifest → 启动后显示横幅,点击打开目录
- [x] GUI 实测:更新源不可达 → 正常启动、无横幅、无卡顿
### 17.17 局域网更新 · 阶段③:自动安装启动器(脚本)
前置阅读:
- `docs/10-lan-update.md`(§3 架构、§8 流程、§16 阶段③)
说明:
- 实现 PowerShell 启动器 `scripts/update.ps1`,跑通「检查 → staging → 校验 → 原子切换 → 启动 → 降级」整条链路。先以脚本验证流程,后续再决定是否编译为 `Launcher.exe`。
任务:
- [x] `scripts/update.ps1`:读 `current.txt` 与 `data\config\app_config.json` 的 `update_source`
- [x] 读远端 `manifest.json`、语义化版本比较,仅当远端更高才更新
- [x] robocopy 拷到 `staging\<ver>.tmp` → 校验 `marker` → 重命名进 `versions\<ver>` → 原子写 `current.txt`(ascii 无 BOM)
- [x] 启动 `versions\<current>\CMBot.exe` 并设 `CMBOT_DATA_DIR`
- [x] 降级:源不可达 / robocopy 失败 / marker 缺失 / 坏 manifest 一律启动本地现版本
- [x] 假版本目录验证 6 个用例(更新、幂等、源不可达、未配置源、下载损坏、坏 manifest)全过
- [x] 接入真实安装结构:新增 `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。
任务:
- [x] 文档:`docs/10` 由 UNC/robocopy 全面改为 HTTP(源/清单/流程/安全/阶段);`docs/02`、`docs/05` 同步配置项
- [x] 配置项 `update_user` / `update_pass` 写入 `DEFAULT_CONFIG`
- [x] `update_service.check_for_update` 支持 `http(s)://` 源 + Basic Auth(当前仅 `open()` 本地文件,填 URL 静默返回 None),补单测
- [x] 启动器 `update.ps1` 改 HTTP:带凭据下载 zip → 校验 SHA-256 → 解压到 `staging\app.new` → `app/app.old` 切换回滚
- [x] manifest 字段由 `source`/`files`/`marker` 改为 `url`/`sha256`/`size`
- [x] 发布流程脚本:build → 打 zip → 算 SHA-256 → 写 manifest → 上传
- [x] BOM 兼容:`check_for_update` 用 `utf-8-sig` 解码,`build.ps1` 写 manifest 不带 BOM(否则 PS5.1 的 UTF8 BOM 会让 app 内 `json.loads` 静默失效);补 2 个 BOM 单测
- [x] `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`):始终可写、按用户隔离、不随程序更新丢失。
任务:
- [x] 文档:`docs/10` 改为便携 + `Launcher.exe` + `~/.cmbot` 模型(§3/§4/§5/§8/§9/§11/§16)
- [x] `get_data_dir()` 三级回退:`CMBOT_DATA_DIR` → 打包态 `~/.cmbot` → 开发态项目根;`tests/test_file_service.py` 5 个单测
- [x] `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 真实端到端验证
- [x] `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 跑
- [x] 退休 `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 内通知都跟着用。
任务:
- [x] 文档:`docs/07` §3 布局、§4.1 去掉「暂不提供设置入口」、新增 §4.3 入口 + §4.4 对话框
- [x] 页签栏右端加低调 `⚙ 配置` 按钮(`addStretch` 与编号页签隔开,非编号页签)
- [x] `src/app/widgets/settings_dialog.py`(`QDialog`,纯 UI):更新地址 / 账号 / 密码(掩码+显示) / 测试连接 / 当前版本 + 安全提示
- [x] `测试连接`:后台线程调用 `update_service.load_manifest`(区分「连不上」与「已是最新」),反馈最新/发现新版/连接失败
- [x] `main_window`:打开时注入当前 config,保存时集中 `save_config`,UI 不直接写配置
- [x] 保存后重新触发一次在线更新检查(横幅刷新;`_update_found` 改为只连一次避免重复)
- [x] 取消不改动配置
验收:
- [x] 单测:`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 内、切换在启动器」,启动永不阻塞。
任务:
- [x] `services/installer.py`:`download_and_stage()`(app 运行时下载→SHA-256→解压→`staging\app.new`)、`apply_staged()`(启动器秒切 `app/app.old`,含「不比当前新则丢弃」「移动失败回滚」)、`staged_version()`、`is_writable`
- [x] `launcher.py` 瘦身:seed + `apply_staged` + 启动,不联网;`tests/test_launcher.py` 重写
- [x] `main_window`:发现新版在 `⚙ 配置` 左侧显示蓝色提示文字(删除旧阻塞横幅与「打开更新目录」)
- [x] `settings_dialog`:加「检查并更新」→ 下载暂存 → 「下次启动生效」
- [x] `tests/test_installer.py` 11 个单测 + 本地 HTTP server 真实端到端(下载暂存→app 不动→应用切换)
- [x] 文档:`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\`**(每次更新会被替换、导出会丢)。
任务:
- [x] 文档:`docs/10` §4/§5、`docs/02` 配置说明、`docs/07` §7.5 对齐
- [x] `file_service.get_output_dir()`:打包态优先 `<安装根>\合并后的图片`(安装根 = `get_app_dir().parent`),不可写回退 `get_data_dir()/output`;开发态用项目目录;`tests/test_file_service.py` 3 个单测
- [x] 导出面板默认值随之变化(沿用 `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`):
- [x] 在线更新(HTTP 检测 + 通知 + `Launcher.exe` 自动安装 + 回滚)
- [x] 局域网/在线分发(便携包 + HTTP 文件服务 + manifest)
- [x] ~~多版本启动器~~(改为 `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 设计与效果图
- [x] 分析旧项目 → `docs/旧ai穿搭项目.md`
- [x] 模块设计文档 `docs/11-ai-outfit.md`(架构/Excel 适配/数据模型/AI 服务/提示词/并发/输出/界面/配置/阶段/验收)
- [x] UI 效果图 `docs/ui-ai-outfit.html` + `docs/ui-ai-outfit.png`(三栏:左设置 / 中「最近结果缩略图条 + 处理明细表」/ 右运行日志;与印花页同款外壳、纯蓝统一)
- [x] 确认取舍:不做实时单图大预览(批量工具,并发会有「显示哪行」歧义);预览=按钮弹 `QDialog`;保留「保存话术」按钮;并发由 Python 线程负责、Qt 仅 signal 回主线程
### 19.1 核心与服务(无 GUI,可单测)— docs/11 §14 阶段 1
前置阅读:`docs/11-ai-outfit.md`(§3–§9)
- [x] 前置依赖:`requirements.txt` 增加 Python 3.7 兼容的 `requests` / `urllib3` / `openpyxl` 锁定版本,并同步 `docs/03-technical-stack.md`
- [x] `core/models.py` 新增 `OutfitTask` / `OutfitResult`(纯 dataclass,Python 3.7 兼容,不依赖 PySide6)
- [x] `services/excel_service.py`:读行 → `List[OutfitTask]`、写回 D/E/F、占用检测、跳过「完成」/按设置重试「失败」/空字段安全跳过、每行即存
- [x] `services/ai_image_service.py`:移植旧项目 `ImageApiClient`(多模型、多请求格式 `chat/gemini/images/images_edits`、传图 data-url、递归取图、URL 归一化、字段校验);注意 PEP585 类型注解改 Python 3.7 写法
- [x] `core/ai_outfit.py`:单行生成纯逻辑编排(提示词渲染 + 调用 + 保存 JPG + 产出 `OutfitResult`);§8「超时按分辨率动态决定(512/1K/2K/4K→180/240/360/600,可被 `timeout_seconds` 覆盖)」由 `ai_image_service.resolution_timeout` 实现
- [x] 单测:`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)
- [x] `core/outfit_batch.py` 纯逻辑 `OutfitBatchRunner`:`ThreadPoolExecutor(并发数)` + `RateLimiter(请求间隔)` + 单任务冷却 + 阶梯重试 + 温和停止 + >30s 心跳;全可测(`QObject` 包装放 §19.3 UI 接线)
- [x] 子线程只经回调/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`
- [x] 页签栏接 `QStackedWidget`(页 0=添加印花工作区+队列,页 1=AI 穿搭面板);启用页签 1(导出上架仍禁用)
- [x] `app/widgets/ai_outfit_panel.py`:左设置(Excel/输出/模型/话术编辑+保存+插入占位符+预览弹窗/生成设置)、中(最近结果缩略图 + 处理明细表)、右(进度+统计+开始/停止+导出失败清单+打开输出目录+实时日志)
- [x] 接线后台线程:`QThread` + `_OutfitWorker(QObject)` 包 `OutfitBatchRunner`,signal 回主线程刷日志/进度/明细/缩略图;每行写回 Excel;结束摘要弹窗;失败清单导出 CSV
- [ ] GUI 实测(真机):选 Excel/模型跑通、停止生效、失败清单正确(离屏冒烟已过,待真机)
### 19.4 配置与提示词 — docs/11 §14 阶段 4
前置阅读:`docs/11-ai-outfit.md`(§7、§11)
- [x] `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 用例
- [x] `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}`,缺这段。决策:简体文案、始终自动附加(不做开关)。
- [x] `core/ai_outfit.py`:加 `OUTPUT_REQUIREMENTS` 常量 + `build_output_requirements(resolution)`;`render_prompt(template, task, resolution=None)` 在替换占位符后附加该段(`resolution` 为空不加);`generate_outfit_image` 传入当前 resolution
- [x] `app/widgets/ai_outfit_panel.py`:内嵌预览改用 `render_prompt`(带当前分辨率),分辨率下拉变化时刷新预览,使「预览 = 实际发送」
- [x] 单测:带 `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 模型」下拉时弹信息框告知影响(**只告知、不拦截、不还原**)。决策:每次实际更换都弹一次。
- [x] `app/widgets/ai_outfit_panel.py`:给 `_resolution` / `_model_combo` 接 **`activated`** 信号(非 `currentIndexChanged`,避免启动/载配置误弹);仅值实际改变时 `QMessageBox.information`,保持新选项、不回退
- [x] 分辨率文案带 `ai_image_service.resolution_timeout` 的超时秒数(512/1K/2K/4K→180/240/360/600)+「下次生效」
- [x] AI 模型文案带该模型 `api_type` + 计费/效果可能不同 +「下次生效」
- [x] 离屏验证:程序化赋值(`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`(范式参考)
需求:把单一话术升级为多套命名话术(下拉切换 / 新建 / 另存为 / 重命名 / 删除 / 保存,记住上次)。决策:**全部自定义**(不分内置,播种一套「默认」);**切换前脏数据弹窗**提醒是否保存;**含重命名**。
- [x] `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`
- [x] `ai_outfit_panel.py`:「通用话术」组加 模板下拉 + 新建 / 另存为 / 重命名 / 删除(保留 插入标题 / 保存);切换载入文本 + 刷新预览;记住上次所选并启动恢复(经 `config_changed` 回主窗口集中存 `outfit_prompt_name`)
- [x] 脏数据保护:编辑框与当前套已存文本不同则在切换/新建/另存为前弹「保存 / 不保存 / 取消」(取消还原下拉);名字唯一(空/重名拒绝);删后选邻近、不可删到 0(删除单独二次确认)
- [x] 「开始生成」前把当前编辑存回所选套(`_store_current_text`);生成/预览仍走 `render_prompt`(§7.1 尾巴不变)
- [x] 单测 `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 后显示替换效果)」。预览应不看状态。
- [x] `services/excel_service.py`:新增 `read_all_rows(excel)`,返回每一有效数据行(标题/货号/衣服图齐全)的 `OutfitTask`,**忽略 E 列状态**
- [x] `ai_outfit_panel._reload_sample_rows()` 改调 `read_all_rows`(预览专用);生成仍走 `load_outfit_tasks`,`_on_tasks_loaded` 仍用实际任务
- [x] 单测 `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)。
- [x] 改进 1:`_on_finished(total==0)` 弹 `_show_no_pending_message`——统计 `read_all_rows` 的「完成 N / 失败 M」,提示「已完成 N 跳过;失败 M 可勾『重试上次失败的行』;重做已完成请清空状态(E)列」(无有效行时另提示)
- [x] 改进 2:`_on_tasks_loaded` 去掉 `_fill_sample_combo(tasks)`,预览样本下拉只由 `read_all_rows`/`_reload_sample_rows` 维护,与运行解耦
- [x] 离屏验证:全表完成 → `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,按钮全是系统灰;复用印花页的「主操作蓝 / 次级灰 / 危险红」三类按钮规范。
- [x] `ai_outfit_panel.py`:面板 `setObjectName("aiOutfitPanel")` + `_apply_styles()`(`_build_ui` 末尾调用),QSS 以 `#aiOutfitPanel` 限定作用域,避免波及弹窗按钮
- [x] 主操作蓝(`#openFolderBtn`/`#queueBatchBtn` 同款)= 开始生成(`aiStartBtn`);危险安静红(`#templateDeleteBtn` 同款)= 停止生成(`aiStopBtn`)+ 话术删除(`aiPromptDeleteBtn`);其余走面板内 `QPushButton` 默认次级灰(`#queueActionBtn`/`#templateBtn` 同款)
- [x] 给 开始/停止/话术删除 三个按钮设 objectName;纯样式、不动行为
- [x] 离屏 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 即全局节流);失败重试跳过已存在输出,幂等。
- [x] `core/ai_outfit.py`:`list_directory_images(dir)`(顶层、扩展名过滤、排序、忽略子目录);`make_outfit_subdir_path(output_dir, subdir, stem)`(`输出目录/<安全子目录>/<安全源图名>.jpg`,不加 `_n` 后缀)
- [x] `core/ai_outfit.py`:`generate_outfit_image` 加目录分支(新参 `request_interval`/`image_log`)——列图片、空目录→失败、复用一个 `ImageApiClient`、逐张跳过已存在/节流/存子目录/发日志、聚合成单个 `OutfitResult`(`output_path`=子目录、`output_paths`=各 jpg、`success`=全成功且≥1、`error`=失败张数)
- [x] `core/models.py`:`OutfitResult` 加 `output_paths: List[str]`(单文件模式留空)
- [x] `ai_outfit_panel.py`:`gen` 闭包传 `request_interval`/`image_log=self.log.emit`;`_add_result_thumb` 逐张加缩略图;`_on_progress` 明细「结果」列显示「子目录 (N 张)」;`_basename` 处理目录末尾分隔符
- [x] `tests/test_ai_outfit.py`:扇出/幂等跳过/空目录/部分失败/命名过滤等用例;既有单文件用例保持绿;全套 12 文件 py37 全绿
- [x] 离屏冒烟:临时 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**。
- [x] 新增 `packaging/default_config/ai_models.json`,格式为 `{ "models": [...] }`;`display_name` → `name`;保留 `url/model/api_type/timeout_seconds/connect_timeout_seconds/extra_body`;`api_key` 留空或占位
- [x] 更新 `packaging/default_config/app_config.json`,新增 `outfit_model: "Nano Banana 2"`,对应旧配置的 `last_selected_model=nano_banana_2`
- [x] 更新 `src/launcher.py` 首次播种白名单,把 `ai_models.json` 与 `app_config.json`、`templates.json` 一起复制到 `~/.cmbot/config`;已有用户文件不得覆盖
- [x] 补 `tests/test_launcher.py`:首次播种复制 `ai_models.json`,已有 `ai_models.json` 不覆盖
- [x] 补配置读取验证:`load_ai_models()` 能读取出厂模板的两个模型名,且模板不含真实 key
- [x] 验证:`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 行:预览下拉显示占位符,且「开始生成」也判定无待处理行。货号不进提示词、目录行输出名沿用源图名,对目录用法是多余约束。已确认:货号改可选。
- [x] `excel_service.py`:`read_all_rows` 与 `load_outfit_tasks` 完整性判定改为**只要求 标题 + 衣服图路径**非空;货号可空(状态过滤不变)
- [x] `core/ai_outfit.py`:单文件分支货号为空时输出名回退 `Path(garment_path).stem`(货号非空仍用 `货号.jpg`);目录行不变
- [x] `ai_outfit_panel.py`:样本下拉标签货号为空时显示「(无货号)」,明细表货号列允许空
- [x] `tests/test_excel_service.py` / `test_ai_outfit.py`:空货号行被 `read_all_rows`/`load_outfit_tasks` 收录;单文件空货号 → 输出按源图名命名;既有用例保持绿
- [x] 离屏冒烟:用「标题填、货号空、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。
- [x] `config_service.py` 增加私有 helper(如 `_seed_factory_config_if_missing(filename)`),只负责「目标不存在时,从当前 `app\config` 复制」
- [x] `load_ai_models()` 在读取前调用该 helper,确保旧 Launcher/自更新场景也能补出 `~/.cmbot/config/ai_models.json`
- [x] helper 只在源文件存在且目标不存在时复制;复制失败记录日志并继续返回空列表,不影响程序启动
- [x] 不覆盖用户已有 `ai_models.json`,不合并、不重写真实 key
- [x] 补 `tests/test_config_service.py`:缺用户文件 + 有出厂模板 → 自动复制并加载;已有用户文件 → 不覆盖;源模板不存在 → 返回空且不抛异常
- [x] 验证:相关单测和全套 `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` 继续优先,只有为空时才使用新默认目录。
- [x] 文档已更新:`docs/11-ai-outfit.md` / `docs/10-lan-update.md` / `docs/ui-ai-outfit.html` 均指向 `穿搭图片\`
- [x] `file_service.py` 增加 AI 穿搭默认输出目录辅助函数(如 `get_outfit_output_dir()`):打包态优先 `<安装根>\穿搭图片`,不可写回退 `get_data_dir()/output/穿搭图片`,开发态用项目目录下 `穿搭图片`
- [x] `ai_outfit_panel.py` 默认输出目录改用该 helper;`outfit_output_dir` 非空时仍使用用户保存值
- [x] `core/ai_outfit.py` 目录行输出保持 `AI 穿搭输出目录/<目录叶子名>/<源图名>.jpg`,单文件行仍按当前输出目录落盘
- [x] 补测试:默认目录路径、不可写回退、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`,但业务含义改为目录内图片并发;若后续改字段名,需要迁移旧配置。
- [x] 文档更新:`docs/11-ai-outfit.md` 明确 Excel 行并发固定 1、图片并发数只作用于目录内部;`docs/ui-ai-outfit.html` 示例文案改为「图片并发数」
- [x] `ai_outfit_panel.py`:右侧生成设置 label 从「并发数」改为「图片并发数」,日志启动行同时显示「Excel 行并发 1 / 图片并发 N」
- [x] `_OutfitWorker` / `OutfitBatchRunner`:外层 Excel 行任务固定顺序处理,不再用界面并发值作为行 `max_workers`
- [x] `generate_outfit_image`:增加或接入 `image_concurrency` 参数;单文件行保持顺序单张处理
- [x] `core/ai_outfit.py` 目录分支:使用 `ThreadPoolExecutor(max_workers=图片并发数)` 并发生成目录内图片;已存在输出仍跳过,部分失败仍聚合到整行结果
- [x] 保留「新请求间隔」启动节流;图片并发数 >1 时日志/缩略图完成顺序允许与文件名排序不同
- [x] 补测试: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。
任务:
- [x] `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 取文本,返回第一条标题:首个非空行、去序号/引号、单行化)
- [x] `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)
- [x] `src/core/models.py`:新增 `TitleResult`(task/success/generated_title/error/attempts)
- [x] `src/services/excel_service.py`:新增 `write_title_result(excel_path, row_index, title)`(只写 A 列并保存,不动 D/E/F)
- [x] `src/services/config_service.py`:`DEFAULT_CONFIG` 增 `title_model`;新增 `load_title_prompt`/`save_title_prompt`(`title_prompt.txt`)+ `DEFAULT_TITLE_PROMPT`
- [x] `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;「生成标题」与「开始生成」互斥
- [x] 测试:`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` 离屏可构建
- [x] 验证:相关单测 + 全套 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`,换模型只改配置不改代码。
任务:
- [x] `ai_outfit_panel.py`:`_build_title_group` 去掉「标题模型」下拉(`_title_model_combo` 及其 `_compact_combo`/填充/`_set_title_running` 里的禁用);`apply_config` 不再填标题下拉、`_emit_config` 不再写 `title_model`(值由配置/手动维护,UI 不覆盖)
- [x] `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`)
- [x] `config_service.py`:`DEFAULT_CONFIG["title_model"]` 默认值由 `""` 改为 `"GPT-5.5 文本"`;注释改为「标题模型名字(对应 ai_models.json 的 name,§17.3 配置定名)」
- [x] `docs/ai_models.sample.json`:补一条 `name="GPT-5.5 文本"`、`api_type=chat`、`model=gpt-5.5`(占位 key)的文本模型示例,作为标题模型样板
- [x] 测试:更新 `test_ai_outfit_panel.py`(断言**无**标题模型下拉、`_title_model_combo` 不存在);新增 `_resolve_title_model_config` 命中/缺失(报错返回 None)用例
- [x] 验证:相关单测 + 全套 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` **不改**。
任务:
- [x] `ai_outfit_panel.py` 模板按钮行:在「重命名」与「删除」之间插入「保存」(连 `_save_prompt`);删掉编辑框下方原「保存话术」按钮;**保留**编辑框下方「插入标题」按钮,旁边新增「预览最终提示词」按钮
- [x] `ai_outfit_panel.py` 预览弹窗(新 `_OutfitPreviewDialog` 或方法):非模态 `QDialog`,含数据行下拉(`_compact_combo` + `read_all_rows(excel)` 填,状态无关)+ 只读 `QPlainTextEdit`;内容 = `render_prompt(话术正文, 选中行, 当前分辨率)`;缺 `{title}` 时顶部提示;未选 Excel/无行 → 显示话术原文 + 提示
- [x] `ai_outfit_panel.py` 弹窗联动:话术 `textChanged` / 分辨率 `currentIndexChanged` / 数据行下拉变化 → 刷新弹窗预览(仅弹窗存在时);弹窗用 `_reload_sample_rows` 思路在打开时/选 Excel 后填行
- [x] `ai_outfit_panel.py` 生成:保持现有 `_start` 的「缺 `{title}` 弹窗询问是否继续」(标题靠占位符);不引入自动前置
- [x] 测试:面板离屏断言——按钮行含「保存」、编辑框下方仍有「插入标题」、有「预览最终提示词」按钮;打开预览弹窗后选数据行/改分辨率,预览文本 = `render_prompt` 结果(含输出要求);`render_prompt` 既有用例保持绿(不改该函数)
- [x] 验证:相关单测 + 全套 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)类型上判不出,仍只能运行时由「未找到文字标题」逐行暴露——固有限制,不在本任务范围。
任务:
- [x] `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`,再图片类型判定
- [x] 行为不变确认:`_resolve_title_model_config` 拿到 error 仍是 `QMessageBox.warning` + 返回 None;`_start_title` 据此中止(已有逻辑,无需改)
- [x] 测试:`test_ai_outfit_panel.py` 加用例——`title_model` 命中 `images_edits` 模型 → `_find_title_model` 返回 `(None, 含「图片模型」的错误)`;既有命中(chat)/缺失用例保持绿
- [x] 验证:相关单测 + 全套 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)。
任务:
- [x] `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` 保留(= 多条取首条)
- [x] `core/ai_title.py`:新增 `generate_titles(prompt, model_config, api_client=None) -> List[str]`(一次请求、`image_path=None` 纯文本);移除 `generate_title`/`_reference_image`/`render_title_prompt`(不再逐行看图、不替换占位符——提示词原样发)
- [x] `config_service.py`:`DEFAULT_TITLE_PROMPT` 改批量风格(让模型生成多条、每行一条、不带序号/引号;示例含数量如「生成 10 条」)
- [x] `ai_outfit_panel.py` `_TitleWorker`:改为一次 `generate_titles` → 按序 `write_title_result` 回填、逐条 `progress` 刷新明细表;`N>行数`多的丢+日志、`N<行数`后面行留空+日志;请求异常 → 日志 + 成功 0 收尾;去掉行间 `request_interval` sleep
- [x] `ai_outfit_panel.py` `_start_title`:不再传 `request_interval`;图片模型开跑前拦截(§19.21)保持
- [x] 测试:`test_ai_text_service.py` 加 `extract_titles_from_response`(多行→多条、去序号/引号、丢空)+ `generate_texts`(mock,返回多条、无图 payload 不含 image_url);`test_ai_title.py` 改为 `generate_titles`(一次请求多条、客户端异常);删旧 `generate_title` 用例
- [x] 验证:相关单测 + 全套 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 全是 `| 編號 | 標題 | 字元數估算 |`。改用**提示词约定「标题之间用逗号分隔」+ 解析按逗号拆分**,简单可靠、避开表格坑。
任务:
- [x] `ai_text_service.py`:`_clean_titles(text)` 改为**按逗号拆分**——先把原始文本按 `,`/`,`(并把换行也当分隔,兜底)切成段,每段走 `_clean_title_line`(去首尾空白/序号/符号/引号),丢空,返回列表。`extract_titles_from_response`/`extract_text_from_response`(取首条)签名不变
- [x] `config_service.py`:`DEFAULT_TITLE_PROMPT` 改为「逗号分隔」风格——明确「标题之间用逗号分隔、不要 Markdown 表格/换行/序号/引号/表情」,数量由用户写
- [x] 测试:`test_ai_text_service.py` 更新/新增——逗号分隔(半角/全角)→ 多条;混换行+逗号→拆开;段内去序号/引号;Markdown 表格输入**不再**当多条(要么单条要么靠提示词避免,至少不产出表头那种垃圾的多条断言);`extract_text_from_response` 取首条
- [x] 验证:相关单测 + 全套 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 反馈。已确认修复方向:快速停止 + 反馈。
任务:
- [x] `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)`(或闭包捕获),向下传递
- [x] `core/ai_outfit.py`:`generate_outfit_image(..., should_stop=None)` 透传;`_generate_directory_outfit` 改**有界提交**——始终最多 `image_concurrency` 张在飞,每张完成后、提交下一张前检查 `should_stop()`,已停止则停止提交剩余、让在飞收尾返回;结果 error 注明「已停止,N 张未生成」,`output_paths` 带已生成的;单文件行进入前检查一次 `should_stop()`(已停止→跳过结果)
- [x] `ai_outfit_panel.py`:`_OutfitWorker.gen(task, should_stop)` 闭包接新参并透传 `generate_outfit_image`;`_stop()` 把 `停止生成` 文案改「停止中…」(保持禁用);`_set_running(False)` 复位文案为「停止生成」
- [x] 测试:`test_ai_outfit.py` —— 目录行设 `should_stop` 在第 k 张后置真 → 之后不再新增生成、返回「已停止」结果且已生成的在 `output_paths`;`test_outfit_batch.py` —— 停止后不起新行、当前行提前返回、`finished` 及时;既有用例保持绿(注意 `generate_func` 新签名向后兼容/同步改 mock)
- [x] 验证:相关单测 + 全套 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` / 处理明细表)
背景:
处理明细表中「行」「货号」「状态」等字段内容短,却占用过多横向空间;「标题」字段更长、更需要可读宽度。应缩小短字段列,把主要剩余宽度给「标题」。
任务:
- [x] `ai_outfit_panel.py`:新增处理明细表列宽配置;「行」「货号」「状态」固定窄列,「衣服图」「结果 / 原因」给可拖动默认宽度,「标题」使用 Stretch 占用剩余空间
- [x] `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 的「标题」列。
任务:
- [x] `ai_outfit_panel.py` `_configure_detail_table_columns`:把「衣服图」(col 3) 初值 150→约 90、「结果 / 原因」(col 5) 初值 220→约 130(仍 `Interactive` 可手拖);「行/货号/状态」固定窄列与「标题」Stretch 不变 → 标题自动更宽
- [x] `test_ai_outfit_panel.py`:在既有列宽用例补断言——`columnWidth(3) <= 90`、`columnWidth(5) <= 130`(标题仍 Stretch、短列仍 Fixed 不变)
- [x] 验证:`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 可空。
任务:
- [x] `excel_service.py`:`read_all_rows(excel_path, require_title=True)` 加开关——`require_title=False` 时只在 `_is_empty(garment_path)` 跳过(允许 A 空);默认 `True` 行为不变。更新 docstring
- [x] `ai_outfit_panel.py`:`_TitleWorker.run` 改调 `read_all_rows(self._excel_path, require_title=False)`;`_reload_after_titles` 仍可用默认(重载后 A 已填);图片生成/预览/`_show_no_pending_message` 不动(仍要求 A)
- [x] `tests/test_excel_service.py`:补用例——A 空、C 非空的行:`require_title=False` 收录、默认(True)跳过;既有用例保持绿
- [x] 验证:相关单测 + 全套 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` 也可能只走代码内置默认,打包默认文件未被启动器/运行时补种。升级后会出现标题模型缺失、默认提示词文件缺失的问题。
任务:
- [x] 文档更新:`docs/11-ai-outfit.md`、`docs/10-lan-update.md`、`docs/09-packaging-release.md` 已明确出厂 title model 追加和两个 prompt txt 的非覆盖式补种
- [x] `packaging/default_config/ai_models.json` 加入 title model 模板(`api_key` 留空,不提交真实 key),并保持图片模型顺序符合 `tests/test_config_service.py` 预期
- [x] `packaging/default_config/outfit_prompt.txt` / `title_prompt.txt` 纳入发布包默认配置
- [x] `src/launcher.py` `CONFIG_FILES` 加入 `outfit_prompt.txt` / `title_prompt.txt`,首次运行播种
- [x] `config_service`:`load_outfit_prompt` / `load_title_prompt` 读取前调用运行时兜底复制;用户文件存在不覆盖
- [x] `config_service`:`load_ai_models` 在用户文件存在但缺 `app_config.title_model` 时,从 factory `ai_models.json` 追加同名模型;不覆盖同名模型、不改 `api_key`;factory 无同名只记录日志
- [x] 测试:缺 prompt 文件时从 factory 复制;已有 prompt 不覆盖;已有 `ai_models.json` 缺标题模型时追加;已有同名标题模型不重复;factory 无同名不报错
- [x] 验证:`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 文本`),用户可能误选文本模型去生成图片,导致开始生成后失败。
任务:
- [x] 文档更新:`docs/11-ai-outfit.md` 已明确右栏图片 `AI 模型` 下拉填充时跳过 `name == app_config.title_model` 的模型;不新增 `usage` 字段
- [x] `ai_outfit_panel.py`:`_fill_model_combo()` 按 `self._title_model_name` 过滤图片模型下拉;标题模型仍保留在 `self._models` 中供 `_resolve_title_model_config()` 使用
- [x] 行为:如果过滤后没有可选图片模型,显示「未配置可用图片模型」类占位并保持开始生成前校验提示
- [x] 测试:标题模型不出现在图片 AI 模型下拉;图片模型仍显示;标题生成仍能按 `title_model` 找到同名模型;只有标题模型时下拉为空/占位
- [x] 验证:`py_compile`、`test_ai_outfit_panel.py`、全套 `python -m unittest discover -s tests`、离屏启动 AI 穿搭页通过
### 19.30 独立脚本:合并图匹配印花清单 — `match_stamp.py`
背景:
需要一个独立脚本(不引用现有项目代码)按“方案2”生成匹配清单:例如合并图 `output\20260623_094529\TY037\1_TY037.png`,从文件名后缀提取 `TY037`,在印花文件夹 `D:\chengma\印花和底图\已处理印花\卡通71(66大码200斤 KEKE已上)\横1` 中匹配 `TY037.png`,输出 CSV 清单。
任务:
- [x] 新增 `match_stamp.py`,只使用 Python 标准库,不导入 `src/` 或项目服务代码
- [x] 脚本顶部提供 `OUTPUT_DIR` / `STAMP_DIR` / `REPORT_PATH`,默认生成 `match_stamp_report.csv`
- [x] 递归扫描合并图和印花图,按合并图文件名最后一个 `_` 后的编码匹配印花文件 stem
- [x] CSV 字段:`status` / `code` / `merged_image` / `stamp_image` / `merged_folder` / `message`,支持 `matched` / `missing` / `duplicate` / `bad_name`
- [x] 验证:`python -m py_compile match_stamp.py`;用示例目录生成系统临时 CSV,匹配 96 张、缺失 0、重复 0、坏名 0
### 19.31 合并图按图片内容匹配印花 — 需求与方案文档
背景:
§19.30 的 `match_stamp.py` 是按文件名后缀匹配,例如 `1_TY037.png -> TY037.png`。用户澄清真实需求是:给定一张衣服图片(可理解为合并后的图片)和一个印花目录,通过类似 OpenCV template matching 的图像内容匹配,找出衣服图中印花与目录内哪张印花最相似;先不保存 CSV,直接 `print` 结果。
任务:
- [x] 新增 `docs/12-stamp-template-matching.md`,单独记录需求、输入输出、方案比较、推荐方案和依赖
- [x] 明确推荐第一版:多尺度 `matchTemplate` + alpha mask + 边缘辅助分数,直接打印 Top N
- [x] 明确第三方库:最小 `opencv-python` + `numpy`,建议用 `Pillow` 处理 Windows 中文路径
- [x] 明确约束:独立脚本、不引用项目代码、不修改图片、第一版不写 CSV/Excel、不接入 GUI
- [x] 验证:文档格式检查通过
### 19.32 独立脚本:按图片内容匹配合并图印花 — `match_stamp.py`
前置阅读:
- `docs/12-stamp-template-matching.md`
- `match_stamp.py`
背景:
§19.31 已明确真实需求不是按 `1_TY037.png -> TY037.png` 的文件名规则匹配,而是给定一张衣服 / 合并图和一个印花目录,通过图像内容匹配找出最相似的印花。第一版继续保持为独立脚本,不接入 GUI,不写 CSV,直接打印结果。
任务:
- [x] 将 `match_stamp.py` 从“递归扫描 output 并按文件名写 CSV”改为“单张合并图 + 印花目录内容匹配”
- [x] 使用 `Pillow` 读取图片以兼容 Windows 中文路径,再转为 `numpy` / OpenCV 数组
- [x] 遍历印花目录内 PNG/JPG/JPEG/WEBP 图片,对透明 PNG 使用 alpha mask 并裁剪透明边界
- [x] 使用多尺度模板匹配,主分数使用 RGB 彩色 `matchTemplate`,边缘图分数作为辅助
- [x] 输出最佳匹配和 Top N,包含综合分、模板分、边缘分、坐标、缩放比例和匹配尺寸
- [x] 找不到高可信结果时打印低分提示;不保存 CSV,不修改任何图片
- [x] 支持命令行覆盖 `--image`、`--stamp-dir`、`--top`
- [x] 验证:`python -m py_compile match_stamp.py` 通过;临时构造一张合并图和两个候选印花,Top 1 命中正确印花