feat: 完成T-303 AI批量生成

- 新增 generate_batch,先并发生成标题再并发生成封面,成功逐条 set_generated 落库

- Tab② 接入开始生成、停止、进度展示和双击新旧封面预览

- 新增 GenerateWorker,通过 worker signal 回传进度与行刷新

- 补充批量生成成功、失败、停止取消和 GUI worker 单元测试

- 同步任务看板、API、路由、当前状态与 progress 文档
This commit is contained in:
chengma
2026-06-27 16:26:08 +08:00
parent fe1e0a7d32
commit 789e82991f
9 changed files with 678 additions and 21 deletions
+62
View File
@@ -22,6 +22,7 @@ from app.gui import (
AccountsTab,
CollectWorker,
CollectTab,
GenerateWorker,
GenerateTab,
MainWindow,
TAB_STYLE,
@@ -91,6 +92,10 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("标题提示词", tab.title_prompt_edit.placeholderText())
self.assertEqual("封面提示词", tab.cover_prompt_edit.placeholderText())
self.assertEqual("保存标题提示词", tab.save_title_button.text())
self.assertEqual("开始生成", tab.generate_button.text())
self.assertEqual("停止", tab.stop_generate_button.text())
self.assertFalse(tab.stop_generate_button.isEnabled())
self.assertEqual("进度:标题0/0 · 封面0/0 · 失败0", tab.progress_label.text())
self.assertEqual("默认", tab.cover_template_combo.currentText())
self.assertEqual(["店铺", "商品ID", "旧标题", "新标题", "状态"], tab.model.HEADERS)
self.assertEqual("任务 0/0 条", tab.summary_label.text())
@@ -161,6 +166,10 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab.preview_cover_prompt()
self.assertIn("预览 旧标题 新标题 51100639510 主店", info.call_args[0][2])
with mock.patch("app.gui.QDialog.exec", return_value=0) as exec_dialog:
tab.show_task_images(tab.model.index(0, 0))
exec_dialog.assert_called_once()
tab.cover_prompt_edit.moveCursor(QTextCursor.End)
tab.insert_title_placeholder()
self.assertTrue(tab.cover_prompt_edit.toPlainText().endswith("{新标题}"))
@@ -171,6 +180,59 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_generate_worker_calls_generate_batch_and_emits_signals(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
db.insert_tasks(
batch_id,
[
{
"source_file_abs": os.path.join(temp_dir, "input.xlsx"),
"source_sheet": "商品",
"source_row": 2,
"account_name": "Excel主店",
"alias": "alias-a",
"item_id": "51100639510",
}
],
path=cfg["db_path"],
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
db.set_collected(task.id, "旧标题", "old.jpg", path=cfg["db_path"])
tasks = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
progress = []
rows = []
def fake_generate_batch(tasks_arg, prompt_values, ai_cfg=None, on_progress=None, should_stop=None):
self.assertEqual(tasks, tasks_arg)
self.assertEqual({"title": "标题提示", "cover": "封面提示"}, prompt_values)
self.assertEqual(account, ai_cfg["account_by_alias"]["alias-a"])
self.assertEqual(cfg["db_path"], ai_cfg["db_path"])
self.assertFalse(should_stop())
on_progress({"total": 1, "title_done": 1, "cover_done": 0, "failed": 0})
ai_cfg["on_task_update"](tasks[0].id, {"stage": "generated"})
return {"ok": True, "total": 1, "title_done": 1, "cover_done": 1, "failed": 0}
worker = GenerateWorker(
tasks,
{"title": "标题提示", "cover": "封面提示"},
db_path=cfg["db_path"],
config=cfg,
)
worker.progress.connect(progress.append)
worker.row_updated.connect(lambda task_id, fields: rows.append((task_id, fields)))
with mock.patch("app.gui.ai.generate_batch", side_effect=fake_generate_batch):
summary = worker.execute()
self.assertEqual(1, summary["cover_done"])
self.assertEqual([{"total": 1, "title_done": 1, "cover_done": 0, "failed": 0}], progress)
self.assertEqual([(tasks[0].id, {"stage": "generated"})], rows)
self.assert_removed(temp_dir)
def test_generate_tab_lists_tasks_and_filters_by_shop_status_and_batch(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)