T-576 增加生成和更新内容模式

This commit is contained in:
chengma
2026-07-09 17:44:03 +08:00
parent 9827c8c6d8
commit ab87be8223
17 changed files with 679 additions and 183 deletions
+5
View File
@@ -987,6 +987,8 @@ class AITests(TempDirMixin, unittest.TestCase):
cfg, key_path = self._cmhub_config(temp_dir)
cfg["db_path"] = os.path.join(temp_dir, "cmshopee.db")
cfg["image_dir"] = os.path.join(temp_dir, "images")
cfg["ai"]["generate_mode"] = "cover"
cfg["ai"]["generate_mode"] = "cover"
cfg["ai"]["generate_cover"] = True
cfg["ai"]["image_concurrency"] = 10
titles = ["旧标题%s" % index for index in range(7)]
@@ -1599,6 +1601,7 @@ class AITests(TempDirMixin, unittest.TestCase):
cfg = self._config()
cfg["db_path"] = os.path.join(temp_dir, "cmshopee.db")
cfg["image_dir"] = os.path.join(temp_dir, "images")
cfg["ai"]["generate_mode"] = "cover"
cfg["ai"]["generate_cover"] = True
batch_id, tasks = self._collected_tasks(temp_dir, cfg, ["旧标题"])
db.set_generated(tasks[0].id, "手动标题", None, path=cfg["db_path"])
@@ -1621,6 +1624,8 @@ class AITests(TempDirMixin, unittest.TestCase):
)
self.assertTrue(summary["ok"])
self.assertEqual("cover", summary["generate_mode"])
self.assertTrue(summary["generate_cover"])
self.assertEqual(1, summary["total"])
self.assertEqual(0, summary["title_total"])
self.assertEqual(0, summary["title_done"])
+27
View File
@@ -27,6 +27,9 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
self.assertEqual(os.path.join(temp_dir, "config", "cmhub.json"), config["cmhub_config_path"])
self.assertEqual(240, appconfig.response_timeout(config))
self.assertFalse(appconfig.ai_config(config)["generate_cover"])
self.assertEqual("title", appconfig.ai_generate_mode(config))
self.assertEqual("title", appconfig.shopee_update_config(config)["update_mode"])
self.assertFalse(appconfig.shopee_update_config(config)["allow_cover_update"])
updated = appconfig.update_config(
{"ai": {"resolution": "2k"}},
@@ -35,6 +38,30 @@ class AppConfigTests(TempDirMixin, unittest.TestCase):
self.assertEqual(360, appconfig.response_timeout(updated))
self.assertEqual((9222, 9260), appconfig.debug_port_range(updated))
legacy = appconfig.save_config(
{
"ai": {"generate_cover": True},
"shopee_update": {"allow_cover_update": True},
},
path=config_path,
)
self.assertEqual("title_cover", appconfig.ai_generate_mode(legacy))
self.assertTrue(appconfig.ai_config(legacy)["generate_cover"])
self.assertEqual("title_cover", appconfig.shopee_update_config(legacy)["update_mode"])
self.assertTrue(appconfig.shopee_update_config(legacy)["allow_cover_update"])
cover_only = appconfig.save_config(
{
"ai": {"generate_mode": "cover"},
"shopee_update": {"update_mode": "cover"},
},
path=config_path,
)
self.assertEqual("cover", appconfig.ai_generate_mode(cover_only))
self.assertTrue(appconfig.ai_config(cover_only)["generate_cover"])
self.assertEqual("cover", appconfig.shopee_update_config(cover_only)["update_mode"])
self.assertTrue(appconfig.shopee_update_config(cover_only)["allow_cover_update"])
self.assert_removed(temp_dir)
def test_config_save_does_not_persist_runtime_paths(self):
+53 -35
View File
@@ -19,7 +19,7 @@ if gui.QT_IMPORT_ERROR is not None:
from PySide6.QtCore import QItemSelectionModel, QRect
from PySide6.QtGui import QImage, QKeyEvent, QTextCursor
from PySide6.QtWidgets import QApplication, QCheckBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView
from PySide6.QtWidgets import QApplication, QComboBox, QLineEdit, QPlainTextEdit, QProgressBar, QTableView
from app.gui import (
AccountDialog,
@@ -850,6 +850,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
)
self.assertFalse(tab.allow_real_submit_checkbox.isChecked())
self.assertFalse(tab.allow_cover_update_checkbox.isChecked())
self.assertTrue(tab.allow_cover_update_checkbox.isHidden())
self.assertEqual(-1, tab.shopee_update_form_layout.indexOf(tab.allow_cover_update_checkbox))
self.assertEqual(1, tab.max_items_per_run_spin.value())
self.assertFalse(tab.close_success_tab_checkbox.isChecked())
self.assertFalse(tab.parallel_accounts_checkbox.isChecked())
@@ -866,12 +868,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab.shopee_update_form_layout,
tab.parallel_accounts_group,
)
cover_row, cover_col, _row_span, _col_span = widget_position(
tab.shopee_update_form_layout,
tab.allow_cover_update_checkbox,
)
self.assertEqual(cover_row, group_row)
self.assertGreater(group_col, cover_col)
self.assertGreaterEqual(group_row, 0)
self.assertGreaterEqual(group_col, 0)
self.assertEqual(4, group_col_span)
group_widgets = [
@@ -1153,7 +1151,6 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab.debug_port_end_spin.setValue(9350)
tab.cdp_ready_timeout_spin.setValue(45)
tab.allow_real_submit_checkbox.setChecked(True)
tab.allow_cover_update_checkbox.setChecked(True)
tab.max_items_per_run_spin.setValue(2)
tab.close_success_tab_checkbox.setChecked(True)
tab.parallel_accounts_checkbox.setChecked(True)
@@ -1183,7 +1180,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
{
"test_item_id": "51100639510",
"allow_real_submit": True,
"allow_cover_update": True,
"allow_cover_update": False,
"update_mode": "title",
"max_items_per_run": 2,
"close_success_tab": True,
"dry_run": False,
@@ -1235,7 +1233,6 @@ class GuiTests(TempDirMixin, unittest.TestCase):
apply_tab = window.tabs.widget(TAB_TITLES.index("③ 更新蝦皮"))
settings_tab.allow_real_submit_checkbox.setChecked(True)
settings_tab.allow_cover_update_checkbox.setChecked(True)
settings_tab.max_items_per_run_spin.setValue(3)
settings_tab.close_success_tab_checkbox.setChecked(True)
settings_tab.parallel_accounts_checkbox.setChecked(True)
@@ -1247,7 +1244,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
safety_cfg = apply_tab._shopee_update_config()
self.assertEqual("123456789", safety_cfg["test_item_id"])
self.assertTrue(safety_cfg["allow_real_submit"])
self.assertTrue(safety_cfg["allow_cover_update"])
self.assertFalse(safety_cfg["allow_cover_update"])
self.assertEqual("title", safety_cfg["update_mode"])
self.assertEqual(3, safety_cfg["max_items_per_run"])
self.assertTrue(safety_cfg["close_success_tab"])
self.assertFalse(safety_cfg["dry_run"])
@@ -1826,7 +1824,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertIsInstance(tab.title_prompt_edit, QPlainTextEdit)
self.assertIsInstance(tab.cover_prompt_edit, QPlainTextEdit)
self.assertIsInstance(tab.task_table, QTableView)
self.assertIsInstance(tab.generate_cover_checkbox, QCheckBox)
self.assertIsInstance(tab.generate_mode_combo, QComboBox)
self.assertEqual("标题提示词", tab.title_prompt_edit.placeholderText())
self.assertEqual("封面提示词", tab.cover_prompt_edit.placeholderText())
self.assertEqual("保存标题提示词", tab.save_title_button.text())
@@ -1834,8 +1832,11 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("开始生成", tab.generate_button.text())
self.assertEqual("停止", tab.stop_generate_button.text())
self.assertEqual("重置生成结果", tab.reset_generate_button.text())
self.assertEqual("生成封面图片(成本较高)", tab.generate_cover_checkbox.text())
self.assertFalse(tab.generate_cover_checkbox.isChecked())
self.assertEqual(
["只生成标题", "只生成封面", "生成标题和封面"],
[tab.generate_mode_combo.itemText(index) for index in range(tab.generate_mode_combo.count())],
)
self.assertEqual("title", tab.generate_mode_combo.currentData())
self.assertFalse(tab.stop_generate_button.isEnabled())
self.assertEqual("进度:标题0/0 · 图片0/0 · 失败0", tab.progress_label.text())
self.assertIsInstance(tab.title_progress_bar, QProgressBar)
@@ -2179,7 +2180,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_generate_tab_persists_generate_cover_toggle(self):
def test_generate_tab_persists_generate_mode_selection(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
statuses = []
@@ -2187,16 +2188,18 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.addCleanup(tab.close)
self.assertFalse(appconfig.ai_config(cfg)["generate_cover"])
tab.generate_cover_checkbox.setChecked(True)
tab.generate_mode_combo.setCurrentIndex(tab.generate_mode_combo.findData("cover"))
saved = appconfig.load_config(cfg["config_path"])
self.assertTrue(saved["ai"]["generate_cover"])
self.assertEqual("cover", saved["ai"]["generate_mode"])
self.assertTrue(cfg["ai"]["generate_cover"])
self.assertIn("会同时生成封面图片", statuses[-1])
self.assertEqual("cover", cfg["ai"]["generate_mode"])
self.assertIn("只生成封面", statuses[-1])
self.assert_removed(temp_dir)
def test_generate_tab_refreshes_to_generate_filter_when_cover_toggle_changes(self):
def test_generate_tab_refreshes_to_generate_filter_when_mode_changes_to_cover(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
@@ -2224,7 +2227,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tab.status_filter.setCurrentIndex(tab.status_filter.findData("to_generate"))
self.assertEqual(0, tab.model.rowCount())
tab.generate_cover_checkbox.setChecked(True)
tab.generate_mode_combo.setCurrentIndex(tab.generate_mode_combo.findData("cover"))
self.assertEqual(1, tab.model.rowCount())
self.assertEqual("51100639510", tab.model.task_at(0).item_id)
@@ -3788,7 +3791,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual(0, progress[-1]["cover_total"])
self.assertEqual(1, progress[-1]["generated_done"])
joined_logs = "\n".join(logs)
self.assertIn("本轮仅生成标题,不生成图片", joined_logs)
self.assertIn("本轮生成内容:只生成标题", joined_logs)
self.assertIn("已保存,仅生成标题", joined_logs)
self.assertIn("[完成] AI 生成完成:标题1/1,图片0/0,失败0", joined_logs)
run_log = db.list_run_logs(limit=1, run_type="generate", path=cfg["db_path"])[0]
@@ -3803,6 +3806,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
cfg = self.make_config(temp_dir)
cfg["ai"] = appconfig.ai_config(cfg)
cfg["ai"]["backend"] = "direct"
cfg["ai"]["generate_mode"] = "cover"
cfg["ai"]["generate_cover"] = True
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
@@ -3860,7 +3864,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual(0, progress[-1]["title_total"])
self.assertEqual(1, progress[-1]["cover_total"])
joined_logs = "\n".join(logs)
self.assertIn("[开始] 本轮生成 1 条:标题0,图片1", joined_logs)
self.assertIn("本轮生成内容:只生成封面", joined_logs)
self.assertIn("标题0,图片1", joined_logs)
self.assertIn("已有标题,跳过生文", joined_logs)
self.assertIn("[完成] AI 生成完成:标题0/0,图片1/1,失败0", joined_logs)
@@ -3944,7 +3949,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertTrue(fake_thread.started)
self.assertNotIn("上一轮失败日志", text)
self.assertIn("本轮AI生成开始:任务 1 条", text)
self.assertIn("生成封面:否", text)
self.assertIn("生成内容:只生成标题", text)
self.assert_removed(temp_dir)
@@ -3952,6 +3957,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
cfg["ai"] = appconfig.ai_config(cfg)
cfg["ai"]["generate_mode"] = "cover"
cfg["ai"]["generate_cover"] = True
accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
batch_id = db.create_batch(["input.xlsx"], path=cfg["db_path"])
@@ -3999,7 +4005,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual("图片 0/1", tab.cover_progress_label.text())
text = tab.run_log_view.toPlainText()
self.assertIn("本轮AI生成开始:任务 1 条", text)
self.assertIn("生成封面:是", text)
self.assertIn("生成内容:只生成封面", text)
self.assert_removed(temp_dir)
@@ -4145,6 +4151,11 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual(["店铺", "商品ID", "新标题", "新封面", "阶段", "结果"], tab.model.HEADERS)
self.assertEqual("检查本轮更新", tab.preview_update_button.text())
self.assertEqual("开始更新", tab.start_update_button.text())
self.assertIsInstance(tab.update_mode_combo, QComboBox)
self.assertEqual(
["只更新标题", "只更新封面", "更新标题和封面"],
[tab.update_mode_combo.itemText(index) for index in range(tab.update_mode_combo.count())],
)
self.assertEqual("applyItemFilter", tab.item_filter.objectName())
self.assertEqual("停止", tab.stop_update_button.text())
self.assertEqual("重置更新状态", tab.reset_update_button.text())
@@ -4293,6 +4304,8 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertIn("任务数:1", message)
self.assertIn("预计批次:1", message)
self.assertIn("提交线上", message)
self.assertIn("更新内容:更新标题和封面", message)
self.assertIn("本轮将更新线上封面", message)
self.assertIn("状态:已生成", message)
self.assertIn("商品ID:51100639510", message)
self.assertNotIn("测试商品ID", message)
@@ -4360,9 +4373,10 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertFalse(tab.apply_worker.dry_run)
self.assertFalse(tab.apply_worker.parallel_accounts)
self.assertEqual(1, tab.apply_worker.batch_size)
self.assertEqual("title_cover", tab.apply_worker.update_mode)
log_text = tab.run_log_view.toPlainText()
self.assertNotIn("上一轮更新失败", log_text)
self.assertIn("本轮更新开始:任务 1 条,每批 1 条", log_text)
self.assertIn("本轮更新开始:任务 1 条,更新内容:更新标题和封面,每批 1 条", log_text)
self.assertFalse(tab.preview_update_button.isEnabled())
self.assertFalse(tab.start_update_button.isEnabled())
self.assertTrue(tab.stop_update_button.isEnabled())
@@ -4440,7 +4454,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertEqual(2, tab.apply_worker.max_parallel_accounts)
log_text = tab.run_log_view.toPlainText()
self.assertNotIn("上一轮检查失败", log_text)
self.assertIn("本轮检查开始:任务 1 条,每批 1 条", log_text)
self.assertIn("本轮检查开始:任务 1 条,更新内容:只更新标题,每批 1 条", log_text)
self.assertEqual("开始检查本轮更新:1 条", statuses[-1])
self.assert_removed(temp_dir)
@@ -4486,7 +4500,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir)
def test_apply_tab_blocks_cover_update_when_cover_switch_is_off(self):
def test_apply_tab_blocks_cover_update_when_cover_is_missing_for_selected_mode(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
self.allow_shopee_update(cfg, allow_cover=False)
@@ -4508,18 +4522,22 @@ class GuiTests(TempDirMixin, unittest.TestCase):
)
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
db.set_collected(task.id, "旧标题", "old.jpg", path=cfg["db_path"])
db.set_generated(task.id, "新标题", "new.jpg", path=cfg["db_path"])
db.set_generated(task.id, "新标题", None, path=cfg["db_path"])
tab = ApplyTab(config=cfg)
self.addCleanup(tab.close)
tab.update_mode_combo.setCurrentIndex(tab.update_mode_combo.findData("cover"))
with mock.patch.object(tab, "_show_update_safety_error") as safety_error, \
with mock.patch("app.gui.QMessageBox.warning") as warning, \
mock.patch("app.gui.QMessageBox.question") as question, \
mock.patch("app.gui.run_worker") as run_worker:
tab.start_update()
message = safety_error.call_args[0][0]
self.assertIn("允许更新封面", message)
self.assertIn("⑤设置", message)
warning.assert_called_once()
self.assertEqual("更新内容未生成", warning.call_args[0][1])
message = warning.call_args[0][2]
self.assertIn("缺少新封面", message)
self.assertIn("只更新封面", message)
self.assertIn("51100639510", message)
question.assert_not_called()
run_worker.assert_not_called()
@@ -4621,7 +4639,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
progress = []
rows = []
def fake_apply(account, task, close_success_tab=False, on_step=None, bring_to_front=True):
def fake_apply(account, task, close_success_tab=False, on_step=None, bring_to_front=True, update_mode=None):
applied_aliases.append(account.alias)
close_flags.append(close_success_tab)
foreground_flags.append(bring_to_front)
@@ -4725,7 +4743,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
tasks = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])
foreground_flags = []
def fake_apply(account, task, close_success_tab=False, on_step=None, bring_to_front=True):
def fake_apply(account, task, close_success_tab=False, on_step=None, bring_to_front=True, update_mode=None):
foreground_flags.append(bring_to_front)
return {"committed": True, "error": None}
@@ -4850,7 +4868,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
thread_names = set()
foreground_by_alias = {}
def fake_apply(account, task, close_success_tab=False, on_step=None, bring_to_front=True):
def fake_apply(account, task, close_success_tab=False, on_step=None, bring_to_front=True, update_mode=None):
thread_names.add(threading.current_thread().name)
foreground_by_alias[account.alias] = bring_to_front
started[account.alias].set()
@@ -6716,7 +6734,7 @@ class GuiTests(TempDirMixin, unittest.TestCase):
task = db.list_tasks(batch_id=batch_id, path=cfg["db_path"])[0]
diagnostic_log_dir = os.path.join(temp_dir, "logs")
def fake_apply(account, task, close_success_tab=False, on_step=None, bring_to_front=True):
def fake_apply(account, task, close_success_tab=False, on_step=None, bring_to_front=True, update_mode=None):
on_step({"step": "open_product", "result": "start"})
on_step({"step": "replace_cover", "result": "failed", "detail": "token=SECRET"})
return {