feat(generate): filter tasks by product status

This commit is contained in:
chengma
2026-07-20 10:32:04 +08:00
parent 9852b1c9c1
commit fa101c16ce
4 changed files with 75 additions and 10 deletions
+16 -1
View File
@@ -1144,6 +1144,10 @@ class GenerateTab(QWidget):
self.status_filter.setObjectName("statusFilter")
for label, value in self.STATUS_FILTERS:
self.status_filter.addItem(label, value)
self.product_status_filter = QComboBox()
self.product_status_filter.setObjectName("generateProductStatusFilter")
for label, value in product_status.PRODUCT_STATUS_FILTER_ITEMS:
self.product_status_filter.addItem(label, value)
self.refresh_button = QPushButton("刷新")
self.open_image_dir_button = QPushButton("打开图片文件夹")
self.open_image_dir_button.setObjectName("openImageDirButton")
@@ -1158,7 +1162,9 @@ class GenerateTab(QWidget):
filter_layout.addWidget(self.shop_filter, 1)
filter_layout.addWidget(QLabel("商品ID"))
filter_layout.addWidget(self.item_filter, 1)
filter_layout.addWidget(QLabel("状态"))
filter_layout.addWidget(QLabel("商品状态"))
filter_layout.addWidget(self.product_status_filter, 1)
filter_layout.addWidget(QLabel("生成状态"))
filter_layout.addWidget(self.status_filter, 1)
filter_layout.addWidget(self.refresh_button)
filter_layout.addWidget(self.open_image_dir_button)
@@ -1240,6 +1246,7 @@ class GenerateTab(QWidget):
self.batch_filter.currentIndexChanged.connect(self.refresh_tasks)
self.shop_filter.currentIndexChanged.connect(self.refresh_tasks)
self.item_filter.textChanged.connect(self.refresh_tasks)
self.product_status_filter.currentIndexChanged.connect(self.refresh_tasks)
self.status_filter.currentIndexChanged.connect(self.refresh_tasks)
self.refresh_button.clicked.connect(self.refresh_tasks)
self.open_image_dir_button.clicked.connect(self.open_image_directory)
@@ -2103,6 +2110,7 @@ class GenerateTab(QWidget):
self.batch_filter.setEnabled(not running)
self.shop_filter.setEnabled(not running)
self.item_filter.setEnabled(not running)
self.product_status_filter.setEnabled(not running)
self.status_filter.setEnabled(not running)
self.title_template_combo.setEnabled(not running)
self.new_title_template_button.setEnabled(not running)
@@ -2411,6 +2419,7 @@ class GenerateTab(QWidget):
selected_batch = self.batch_filter.currentData()
selected_shop = self.shop_filter.currentData()
selected_status = self.status_filter.currentData() or "all"
selected_product_status = self.product_status_filter.currentData() or "all"
item_query = self.item_filter.text().strip()
self._populate_batch_filter(batches, selected_batch)
selected_batch = self.batch_filter.currentData()
@@ -2421,6 +2430,7 @@ class GenerateTab(QWidget):
task for task in batch_tasks
if self._matches_shop(task, selected_shop)
and self._matches_item(task, item_query)
and self._matches_product_status(task, selected_product_status)
and self._matches_status(task, selected_status)
]
except Exception as exc:
@@ -2524,6 +2534,11 @@ class GenerateTab(QWidget):
return True
return item_query in str(getattr(task, "item_id", ""))
def _matches_product_status(self, task, selected_product_status):
if selected_product_status in (None, "all"):
return True
return product_status.raw_status(getattr(task, "product_status", None)) == selected_product_status
def _matches_status(self, task, selected_status):
if selected_status in (None, "all"):
return True
+2 -2
View File
@@ -106,12 +106,12 @@
- **标题提示词**:模板下拉(读 `data/prompts/title/*.txt`)+「新建 / 保存模板 / 模板操作(另存为、重命名、删除)」+ 多行输入 + 下方工具条「插入旧标题」(插 `{旧标题}`)/「保存标题提示词」(写 `data/title_prompt.txt`)。启动时自动加载 `title_prompt.txt` 回显,不自动用模板覆盖工作文本;用户选择模板时才把模板内容载入输入框。若标题提示词包含 `{旧标题}`,生成前替换为当前任务旧标题且不重复追加旧标题块;不包含时保持旧行为自动追加旧标题。
- **封面提示词**:模板控件压缩到一行(读 `data/prompts/cover/*.txt`,新建/保存模板/另存为/重命名/删除)+ 多行输入 + 下方工具条「插入标题」(插 `{新标题}`)/「预览」(变量替换后查看)。
- 变量:标题提示词本阶段只支持 `{旧标题}`;封面提示词支持 `{旧标题}`/`{新标题}`/`{商品id}`/`{店铺}`,生成前按任务替换。
- 右上:按导入批次 / 店铺 / 商品ID / 状态筛选任务;商品ID输入框按包含匹配 `item_id`,清空表示全部。
- 右上:按导入批次 / 店铺 / 商品ID / 商品状态 / 生成状态筛选任务;商品状态位于生成状态左侧,选项与①一致,按原始快照区分架上商品、未上架、审核中、状态未知和待检测;生成状态继续表示待生成、已生成、失败、略过、已更新。商品状态筛选只缩小当前列表,商品ID输入框按包含匹配 `item_id`,清空表示全部。
- 筛选行提供「打开图片文件夹」按钮,用于只读打开本地图片目录:选中某行时打开该商品所在账号图片文件夹(优先打开已有新/旧封面文件的真实父目录,缺失时回退到规范账号目录);未选行且选择具体批次时打开该批次图片文件夹;未选行且为全部批次时打开图片根目录。目录不存在只中文提示,不自动创建目录,不修改任务状态。
- 右下:任务列表(店铺名、商品id、旧标题、新标题、标题状态、图片状态)+ AI生成运行日志;标题/图片状态由 `new_title`、`new_cover_path`、`stage/status` 和失败步骤推导,帮助用户区分“标题未生成 / 图片未生成 / 标题成功但图片失败”。商品ID列按原等分宽度约 50% 显示;标题状态和图片状态列在 T-554 基础上再缩到约 33%,缩出的宽度平均给旧标题和新标题。已生成、未提交线上、非运行中的任务可双击「新标题」列本地微调,写回 `tasks.new_title`,清空 `last_error` 并回到可更新;双击其他列弹窗展示旧封面、新封面和历史候选图。
- 底部**单个「开始生成」+「停止」**,并增加「生成内容」下拉:默认只生成标题,可选只生成封面或生成标题和封面;只生成封面不调用生文,有新标题时优先使用,没有时用已采集旧标题作为封面prompt参考,新旧标题都为空才不纳入。开始前先从真实候选重新分组商品状态,弹出与①共用的纵向范围确认框:默认「生成架上商品」(即仅检测结果为正常的商品),「生成全部商品」为警示橙色描边,还会处理未上架、审核中、状态未知商品,可能额外消耗点数;未上架、审核中、状态未知默认不入队、不请求 AI、不消耗点数,用户明确选择全部范围才可入队。商品状态功能上线前的历史空状态会启动时迁移为默认正常;上线后仍为空的记录按未知处理。范围确认不跨轮记忆,取消或候选在确认期间变化均不启动生成;无异常候选时该警示选项禁用。标题/图片两条进度条右侧分别显示同宽用时标签(`生标题用时 N 秒` / `生图用时 N 秒`),运行中每秒递增,完成/停止后冻结;原图片进度条右侧的失败数和 cmhub 余额不再占用该位置。cmhub 模式会把用户设置的图片并发内部限制到最大 5,并用同样最大 5 的独立下载线程池拉取 `image_url`,不新增用户可见下载并发配置;运行日志显示用户设置并发和实际并发。下拉状态持久化到 `config.json` 的 `ai.generate_mode`,并继续写回旧兼容 `ai.generate_cover`。
- 生成参数(标题/图片并发数、失败重试、分辨率、jpg 质量、cmhub 网关/Key/别名)在 **设置**;②只暴露本轮生成标题/封面/图文的内容模式。设置不新增“下载并发”控件;cmhub 图片下载并发由程序按实际生图并发自动计算,最大 5。
- 只生成标题时标题成功即写库并进入 `generated`,保留已有封面;只生成封面时通过组件级写库只保存 `new_cover_path`,不覆盖已有标题,也不把旧标题写入空的 `new_title`;生成标题和封面时按缺失组件增量补齐。“有新封面、无新标题”时标题状态为待生成、图片状态为已生成,后续补标题不重复生图。三种模式都写 `run_type=generate` 的 `run_logs/run_log_events` 和用户可读滚动日志;日志开头明确显示本轮生成内容。点击「开始生成」时先清空②界面可见日志并写入本轮开始摘要,运行中只追加本轮日志;不删除历史 `run_logs/run_log_events` 或本地 `data/logs/`。进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志不自动混入当前运行界面。「停止」取消未开始项,可再次「开始生成」对剩余继续。
- 只生成标题时标题成功即写库并进入 `generated`,保留已有封面;只生成封面时通过组件级写库只保存 `new_cover_path`,不覆盖已有标题,也不把旧标题写入空的 `new_title`;生成标题和封面时按缺失组件增量补齐。“有新封面、无新标题”时标题状态为待生成、图片状态为已生成,后续补标题不重复生图。三种模式都写 `run_type=generate` 的 `run_logs/run_log_events` 和用户可读滚动日志;日志开头明确显示本轮生成内容。点击「开始生成」时先清空②界面可见日志并写入本轮开始摘要,运行中只追加本轮日志;不删除历史 `run_logs/run_log_events` 或本地 `data/logs/`。进入页面默认可显示“本轮日志会在开始运行后显示”,历史日志不自动混入当前运行界面。「停止」取消未开始项,可再次「开始生成」对剩余继续。即使用户通过商品状态筛选查看异常商品,也必须在范围确认框主动选择「生成全部商品」才会入队;默认「生成架上商品」不会因筛选而放宽。
- 「重置生成结果」支持选中任务或当前筛选结果,运行中禁用;确认框提供「重置标题 / 重置封面 / 重置全部」,只改本地 DB,默认不删除本地新封面文件。若范围内包含已提交线上记录,必须提示本地重置不回滚蝦皮,重生成后再更新会再次提交线上。
- **无逐条人工审核环节**;新标题默认使用 AI 输出,但允许对已生成且未提交线上的单行做本地微调。封面画廊内「重置图片」只清当前任务 `new_cover_path` 并归档旧图,不启动单条生图;用户翻看完后用状态筛选「待生成」并点击「开始生成」批量补封面。生成完即可进入 ③,③ 开始更新前会做批量确认。
+10 -7
View File
@@ -1,7 +1,7 @@
---
id: T-671
title: AI生成商品状态筛选与生成状态命名
status: TODO
status: DONE
phase: 7
deps: [T-669]
created: 2026-07-20
@@ -23,11 +23,11 @@ created: 2026-07-20
## 验收标准
- [ ] ②筛选行显示「商品状态」和「生成状态」两个明确标签,商品状态位于生成状态左侧。
- [ ] 商品状态下拉能分别筛出架上商品、未上架、审核中、状态未知和待检测,并可与生成状态、批次、店铺、商品 ID 筛选叠加。
- [ ] 商品状态筛选不会改变 SQLite 数据、标题/封面生成结果或进度状态。
- [ ] 筛选异常商品后点击开始生成,默认范围仍不执行异常商品;只有用户在确认框主动选择「生成全部商品」才会执行。
- [ ] `tests/test_gui.py` 覆盖控件顺序和中文标签、筛选交集、NULL/unknown 区分,以及异常筛选下生成范围确认仍生效。
- [x] ②筛选行显示「商品状态」和「生成状态」两个明确标签,商品状态位于生成状态左侧。
- [x] 商品状态下拉能分别筛出架上商品、未上架、审核中、状态未知和待检测,并可与生成状态、批次、店铺、商品 ID 筛选叠加。
- [x] 商品状态筛选不会改变 SQLite 数据、标题/封面生成结果或进度状态。
- [x] 筛选异常商品后点击开始生成,默认范围仍不执行异常商品;只有用户在确认框主动选择「生成全部商品」才会执行。
- [x] `tests/test_gui.py` 覆盖控件顺序和中文标签、筛选交集、NULL/unknown 区分,以及异常筛选下生成范围确认仍生效。
## 验证
@@ -47,4 +47,7 @@ git diff --check
## 执行记录
- 待实现。
- ②筛选行新增「商品状态」下拉,原「状态」改名为「生成状态」;商品状态使用 T-669 的共享选项和原始空值识别,与批次、店铺、商品 ID、生成状态叠加筛选。
- 开始生成仍只从当前筛选后的 `model.tasks` 取候选,并继续执行既有商品状态范围确认;异常商品在默认「生成架上商品」范围内不入队。
- 更新路由文档,并补充控件标签、组合筛选、待检测/状态未知与默认安全计划的 GUI 回归测试。
- 验证通过:`py -3.10 -m unittest discover -s tests`(623 项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`。
+47
View File
@@ -20,6 +20,7 @@ from app import (
db,
image_paths,
image_studio,
product_status,
prompts,
update_check,
update_installer,
@@ -35,6 +36,7 @@ from PySide6.QtWidgets import (
QAbstractItemView,
QApplication,
QComboBox,
QLabel,
QLineEdit,
QListView,
QPlainTextEdit,
@@ -3343,6 +3345,10 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("打开图片文件夹", tab.open_image_dir_button.text())
self.assertEqual("openImageDirButton", tab.open_image_dir_button.objectName())
self.assertIn("当前批次文件夹", tab.open_image_dir_button.toolTip())
self.assertEqual("generateProductStatusFilter", tab.product_status_filter.objectName())
label_texts = [label.text() for label in tab.findChildren(QLabel)]
self.assertIn("商品状态", label_texts)
self.assertIn("生成状态", label_texts)
self.assertEqual(
["只生成标题", "只生成封面", "生成标题和封面"],
[tab.generate_mode_combo.itemText(index) for index in range(tab.generate_mode_combo.count())],
@@ -5439,6 +5445,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab._set_generate_running(True)
self.assertFalse(tab.reset_generate_button.isEnabled())
self.assertFalse(tab.product_status_filter.isEnabled())
self.assertFalse(tab.title_template_combo.isEnabled())
self.assertFalse(tab.new_title_template_button.isEnabled())
self.assertFalse(tab.save_title_template_button.isEnabled())
@@ -8089,12 +8096,23 @@ class GuiTests(TempDirMixin, unittest.TestCase):
db.set_collected(tasks_a[2].id, "旧标题C", "old-c.jpg", path=cfg["db_path"])
db.set_generated(tasks_a[2].id, "新标题C", "new-c.jpg", path=cfg["db_path"])
db.mark_failed(tasks_b[0].id, "generate", "生成失败", path=cfg["db_path"])
db.set_product_status(tasks_a[0].id, "normal", path=cfg["db_path"])
db.set_product_status(tasks_a[1].id, "unlisted", path=cfg["db_path"])
db.set_product_status(tasks_b[0].id, "unknown", path=cfg["db_path"])
tab = GenerateTab(config=cfg)
self.addCleanup(tab.close)
tab.batch_filter.setCurrentIndex(tab.batch_filter.findData(None))
self.assertEqual("generateItemFilter", tab.item_filter.objectName())
self.assertEqual("generateProductStatusFilter", tab.product_status_filter.objectName())
self.assertEqual(
["全部商品状态", "架上商品", "未上架", "审核中", "状态未知", "待检测"],
[
tab.product_status_filter.itemText(index)
for index in range(tab.product_status_filter.count())
],
)
self.assertEqual(4, tab.model.rowCount())
self.assertEqual("主店", tab.model.index(0, 0).data())
self.assertEqual("51100639510", tab.model.index(0, 1).data())
@@ -8110,6 +8128,35 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertTrue(all(tab.model.index(row, 4).data() == "待生成" for row in range(2)))
self.assertEqual("任务 2/4 条", tab.summary_label.text())
tab.product_status_filter.setCurrentIndex(
tab.product_status_filter.findData("normal")
)
self.assertEqual(1, tab.model.rowCount())
self.assertEqual("51100639510", tab.model.index(0, 1).data())
normal_plan = product_status.build_generation_plan(
tab._generation_candidates("title"),
"title",
"normal_only",
)
self.assertEqual(1, len(normal_plan["execution_tasks"]))
tab.product_status_filter.setCurrentIndex(
tab.product_status_filter.findData("unlisted")
)
self.assertEqual(1, tab.model.rowCount())
unlisted_plan = product_status.build_generation_plan(
tab._generation_candidates("title"),
"title",
"normal_only",
)
self.assertEqual([], unlisted_plan["execution_tasks"])
tab.status_filter.setCurrentIndex(tab.status_filter.findData("all"))
tab.product_status_filter.setCurrentIndex(
tab.product_status_filter.findData("unchecked")
)
self.assertEqual(1, tab.model.rowCount())
self.assertEqual("51100639512", tab.model.index(0, 1).data())
tab.product_status_filter.setCurrentIndex(tab.product_status_filter.findData("all"))
tab.shop_filter.setCurrentIndex(tab.shop_filter.findData("alias-b"))
self.assertEqual(1, tab.model.rowCount())
self.assertEqual("副店", tab.model.index(0, 0).data())