feat(generate): filter tasks by product status
This commit is contained in:
@@ -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
@@ -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
@@ -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`。
|
||||
|
||||
@@ -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())
|
||||
|
||||
Reference in New Issue
Block a user