T-568 支持封面画廊单任务重生

This commit is contained in:
chengma
2026-07-09 11:49:06 +08:00
parent a391d2db99
commit ae4daacbbf
3 changed files with 527 additions and 5 deletions
+239 -3
View File
@@ -73,18 +73,40 @@ class CoverGalleryDialog(QDialog):
THUMBNAIL_SIZE = 180 THUMBNAIL_SIZE = 180
OLD_THUMBNAIL_SIZE = 300 OLD_THUMBNAIL_SIZE = 300
def __init__(self, task, image_root, db_path, account=None, parent=None): def __init__(
self,
task,
image_root,
db_path,
account=None,
parent=None,
config=None,
prompt_values=None,
diagnostic_log_dir=None,
bulk_running_checker=None,
regenerate_running_callback=None,
gallery_updated_callback=None,
):
super().__init__(parent) super().__init__(parent)
self.task = task self.task = task
self.image_root = image_root self.image_root = image_root
self.db_path = db_path self.db_path = db_path
self.account = account self.account = account
self.config = config or {}
self.prompt_values = dict(prompt_values or {})
self.diagnostic_log_dir = diagnostic_log_dir
self.bulk_running_checker = bulk_running_checker
self.regenerate_running_callback = regenerate_running_callback
self.gallery_updated_callback = gallery_updated_callback
self.current_path = _normalize_file_path(getattr(task, "new_cover_path", None)) self.current_path = _normalize_file_path(getattr(task, "new_cover_path", None))
self.selected_path = None self.selected_path = None
self.changed = False self.changed = False
self.candidate_buttons = {} self.candidate_buttons = {}
self.button_group = QButtonGroup(self) self.button_group = QButtonGroup(self)
self.button_group.setExclusive(True) self.button_group.setExclusive(True)
self.regenerate_worker = None
self.regenerate_thread = None
self._regenerate_last_error = ""
self.candidates = image_paths.list_task_cover_candidates(image_root, task, account=account) self.candidates = image_paths.list_task_cover_candidates(image_root, task, account=account)
self.setWindowTitle(f"封面画廊:{getattr(task, 'item_id', '')}") self.setWindowTitle(f"封面画廊:{getattr(task, 'item_id', '')}")
@@ -95,6 +117,9 @@ class CoverGalleryDialog(QDialog):
layout.addLayout(body_layout, 1) layout.addLayout(body_layout, 1)
button_layout = QHBoxLayout() button_layout = QHBoxLayout()
self.regenerate_button = QPushButton("重新生成")
self.regenerate_button.clicked.connect(self.regenerate_cover)
button_layout.addWidget(self.regenerate_button)
button_layout.addStretch(1) button_layout.addStretch(1)
self.save_button = QPushButton("保存") self.save_button = QPushButton("保存")
self.cancel_button = QPushButton("取消") self.cancel_button = QPushButton("取消")
@@ -106,6 +131,7 @@ class CoverGalleryDialog(QDialog):
self._sync_initial_selection() self._sync_initial_selection()
self._update_status() self._update_status()
self._update_regenerate_button_state()
self._fit_to_screen() self._fit_to_screen()
def _old_cover_panel(self): def _old_cover_panel(self):
@@ -247,6 +273,67 @@ class CoverGalleryDialog(QDialog):
self.accept() self.accept()
return True return True
def regenerate_cover(self, checked=False):
if self.regenerate_thread is not None:
return False
if self._bulk_generate_running():
self.status_label.setText("②批量生成正在运行,完成后才能重新生成单个封面")
self._update_regenerate_button_state()
return False
if not str(getattr(self.task, "new_title", "") or "").strip():
QMessageBox.warning(self, "重新生成封面", "当前任务缺少新标题,不能单独重新生成封面")
return False
if not self._confirm_regenerate():
return False
try:
db.reset_generated(
self.task.id,
reset_title=False,
reset_cover=True,
path=self.db_path,
)
fresh_task = db.get_task(self.task.id, path=self.db_path)
except Exception as exc:
QMessageBox.warning(self, "重新生成封面", str(exc))
self.status_label.setText(f"重新生成封面失败:{exc}")
return False
if fresh_task is None:
QMessageBox.warning(self, "重新生成封面", "任务不存在或已删除")
self.status_label.setText("重新生成封面失败:任务不存在或已删除")
return False
self._reload_task_and_candidates(fresh_task)
self.status_label.setText(f"开始重新生成商品 {getattr(fresh_task, 'item_id', '')} 的封面图片")
worker = GenerateWorker(
[fresh_task],
self.prompt_values,
db_path=self.db_path,
config=_cover_regenerate_config(self.config),
diagnostic_log_dir=self.diagnostic_log_dir,
)
worker.progress.connect(self._on_regenerate_progress)
worker.row_updated.connect(self._on_regenerate_row_updated)
worker.log.connect(self._on_regenerate_log)
worker.failed.connect(self._on_regenerate_failed)
worker.finished.connect(self._on_regenerate_finished)
worker.cancelled.connect(self._on_regenerate_cancelled)
thread = run_worker(worker, thread_name="CoverRegenerateWorker", start=False)
thread.finished.connect(lambda: self._forget_regenerate_thread(thread))
self.regenerate_worker = worker
self.regenerate_thread = thread
self._set_regenerate_running(True)
thread.start()
return True
def _confirm_regenerate(self):
box = QMessageBox(self)
box.setWindowTitle("重新生成封面")
box.setText("将重新生成当前商品封面,可能消耗 cmhub 点数。")
confirm_button = box.addButton("重新生成", QMessageBox.AcceptRole)
cancel_button = box.addButton("取消", QMessageBox.RejectRole)
box.setDefaultButton(cancel_button)
box.exec()
return box.clickedButton() is confirm_button
def _needs_committed_confirmation(self): def _needs_committed_confirmation(self):
return int(getattr(self.task, "committed", 0) or 0) == 1 or getattr(self.task, "stage", None) == "applied" return int(getattr(self.task, "committed", 0) or 0) == 1 or getattr(self.task, "stage", None) == "applied"
@@ -266,6 +353,105 @@ class CoverGalleryDialog(QDialog):
dialog = OriginalImageDialog(image_path, self) dialog = OriginalImageDialog(image_path, self)
dialog.exec() dialog.exec()
def _bulk_generate_running(self):
return bool(self.bulk_running_checker and self.bulk_running_checker())
def _set_regenerate_running(self, running):
self.save_button.setEnabled(not running and self.selected_path is not None)
self.regenerate_button.setEnabled(not running and not self._bulk_generate_running())
self.regenerate_button.setToolTip(
"②批量生成正在运行,完成后才能重新生成单个封面"
if self._bulk_generate_running()
else ("正在重新生成当前商品封面" if running else "")
)
if self.regenerate_running_callback is not None:
self.regenerate_running_callback(bool(running))
def _update_regenerate_button_state(self):
running = self.regenerate_thread is not None
self.regenerate_button.setEnabled(not running and not self._bulk_generate_running())
self.regenerate_button.setToolTip(
"②批量生成正在运行,完成后才能重新生成单个封面"
if self._bulk_generate_running()
else ("正在重新生成当前商品封面" if running else "")
)
def _reload_task_and_candidates(self, task=None):
if task is not None:
self.task = task
self.current_path = _normalize_file_path(getattr(self.task, "new_cover_path", None))
self.selected_path = None
self.candidates = image_paths.list_task_cover_candidates(self.image_root, self.task, account=self.account)
self._rebuild_candidate_items()
self._sync_initial_selection()
self._update_status()
def _rebuild_candidate_items(self):
_clear_layout(self.candidate_layout)
self.candidate_buttons = {}
self.button_group = QButtonGroup(self)
self.button_group.setExclusive(True)
if self.candidates:
for index, candidate_path in enumerate(self.candidates):
self.candidate_layout.addWidget(self._candidate_item(candidate_path, index))
self.candidate_layout.addStretch(1)
else:
empty_label = QLabel("暂无生成封面图片")
empty_label.setObjectName("coverGalleryEmptyLabel")
empty_label.setMinimumSize(320, self.THUMBNAIL_SIZE)
empty_label.setAlignment(Qt.AlignCenter)
self.candidate_layout.addWidget(empty_label)
def _on_regenerate_progress(self, payload):
if payload.get("failed"):
self.status_label.setText("封面重新生成失败,历史候选仍可选择保存")
def _on_regenerate_row_updated(self, task_id, fields):
if self.gallery_updated_callback is not None:
self.gallery_updated_callback(self.task)
def _on_regenerate_log(self, message):
text = str(message or "").strip()
if not text:
return
if text.startswith("[失败]"):
self._regenerate_last_error = text
self.status_label.setText(text)
elif "成功" in text or "完成" in text:
self.status_label.setText(text)
def _on_regenerate_failed(self, task_id, error):
self._regenerate_last_error = str(error or "未知错误")
self.status_label.setText(f"封面重新生成失败:{self._regenerate_last_error}")
def _on_regenerate_finished(self, summary):
self._finish_regenerate(summary, cancelled=False)
def _on_regenerate_cancelled(self, summary):
self._finish_regenerate(summary, cancelled=True)
def _finish_regenerate(self, summary, cancelled=False):
fresh_task = db.get_task(self.task.id, path=self.db_path)
if fresh_task is not None:
self._reload_task_and_candidates(fresh_task)
self._set_regenerate_running(False)
if self.gallery_updated_callback is not None:
self.gallery_updated_callback(self.task)
if cancelled or summary.get("cancelled"):
self.status_label.setText("封面重新生成已停止,历史候选仍可选择保存")
return
if summary.get("ok") and summary.get("cover_done", 0) > 0:
self.status_label.setText("封面重新生成完成")
return
detail = self._regenerate_last_error or summary.get("error") or "请稍后重试"
self.status_label.setText(f"封面重新生成失败:{detail}")
def _forget_regenerate_thread(self, thread):
if self.regenerate_thread is thread:
self.regenerate_thread = None
self.regenerate_worker = None
self._set_regenerate_running(False)
def _fit_to_screen(self): def _fit_to_screen(self):
available = _available_geometry() available = _available_geometry()
if available is None: if available is None:
@@ -281,6 +467,23 @@ def _load_image(image_path):
return QImage(str(image_path or "")) return QImage(str(image_path or ""))
def _clear_layout(layout):
while layout.count():
item = layout.takeAt(0)
widget = item.widget()
if widget is not None:
widget.setParent(None)
widget.deleteLater()
def _cover_regenerate_config(config):
runtime_config = dict(config or {})
ai_settings = appconfig.ai_config(runtime_config)
ai_settings["generate_cover"] = True
runtime_config["ai"] = ai_settings
return runtime_config
def _build_image_label(image_path, size, empty_text): def _build_image_label(image_path, size, empty_text):
label = QLabel() label = QLabel()
label.setAlignment(Qt.AlignCenter) label.setAlignment(Qt.AlignCenter)
@@ -391,6 +594,8 @@ class GenerateTab(QWidget):
self.current_cover_template = None self.current_cover_template = None
self.generate_worker = None self.generate_worker = None
self.generate_thread = None self.generate_thread = None
self.generate_running = False
self.cover_gallery_regenerate_running = False
self._title_elapsed_start = None self._title_elapsed_start = None
self._cover_elapsed_start = None self._cover_elapsed_start = None
self._title_elapsed_seconds = 0 self._title_elapsed_seconds = 0
@@ -948,6 +1153,9 @@ class GenerateTab(QWidget):
self._set_status("封面提示词预览已生成") self._set_status("封面提示词预览已生成")
def start_generate(self, checked=False): def start_generate(self, checked=False):
if self.cover_gallery_regenerate_running:
self._set_status("封面画廊正在重新生成,完成后再开始批量生成")
return
if self.generate_thread is not None: if self.generate_thread is not None:
self._set_status("AI 生成正在进行...") self._set_status("AI 生成正在进行...")
return return
@@ -1219,6 +1427,15 @@ class GenerateTab(QWidget):
db_path=self.db_path, db_path=self.db_path,
account=account, account=account,
parent=self, parent=self,
config=self.config,
prompt_values={
"title": self.title_prompt_edit.toPlainText(),
"cover": self.cover_prompt_edit.toPlainText(),
},
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
bulk_running_checker=lambda: self.generate_thread is not None,
regenerate_running_callback=self._set_cover_gallery_regenerate_running,
gallery_updated_callback=self._on_cover_gallery_updated,
) )
if dialog.exec() == QDialog.Accepted: if dialog.exec() == QDialog.Accepted:
self.refresh_tasks() self.refresh_tasks()
@@ -1228,9 +1445,10 @@ class GenerateTab(QWidget):
self._set_status(f"当前新封面未变更:{task.item_id}") self._set_status(f"当前新封面未变更:{task.item_id}")
def _set_generate_running(self, running): def _set_generate_running(self, running):
self.generate_button.setEnabled(not running) self.generate_running = bool(running)
self.generate_button.setEnabled(not running and not self.cover_gallery_regenerate_running)
self.stop_generate_button.setEnabled(running) self.stop_generate_button.setEnabled(running)
self.reset_generate_button.setEnabled(not running) self.reset_generate_button.setEnabled(not running and not self.cover_gallery_regenerate_running)
self.refresh_button.setEnabled(not running) self.refresh_button.setEnabled(not running)
self.batch_filter.setEnabled(not running) self.batch_filter.setEnabled(not running)
self.shop_filter.setEnabled(not running) self.shop_filter.setEnabled(not running)
@@ -1244,6 +1462,24 @@ class GenerateTab(QWidget):
self.rename_cover_template_action.setEnabled(not running) self.rename_cover_template_action.setEnabled(not running)
self.delete_cover_template_action.setEnabled(not running) self.delete_cover_template_action.setEnabled(not running)
self.generate_cover_checkbox.setEnabled(not running) self.generate_cover_checkbox.setEnabled(not running)
self._sync_cover_gallery_regenerate_guard()
def _set_cover_gallery_regenerate_running(self, running):
self.cover_gallery_regenerate_running = bool(running)
self._sync_cover_gallery_regenerate_guard()
def _sync_cover_gallery_regenerate_guard(self):
if self.generate_thread is not None or self.generate_running:
return
blocked = bool(self.cover_gallery_regenerate_running)
self.generate_button.setEnabled(not blocked)
self.reset_generate_button.setEnabled(not blocked)
tooltip = "封面画廊正在重新生成,完成后再开始批量生成" if blocked else ""
self.generate_button.setToolTip(tooltip)
self.reset_generate_button.setToolTip(tooltip)
def _on_cover_gallery_updated(self, task):
self.refresh_tasks()
def _forget_generate_thread(self, thread): def _forget_generate_thread(self, thread):
if self.generate_thread is thread: if self.generate_thread is thread:
+17 -2
View File
@@ -3,7 +3,7 @@ id: T-568
title: ②封面画廊单任务重新生成与并发守卫 title: ②封面画廊单任务重新生成与并发守卫
phase: 7 phase: 7
deps: [T-567] deps: [T-567]
status: TODO status: DONE
created: 2026-07-09 created: 2026-07-09
--- ---
@@ -73,4 +73,19 @@ T-567 只提供候选封面查看与保存。运营在对比时仍需要对当
## 执行记录 ## 执行记录
(做完在这里写:改了什么文件、跑了什么验证命令及结果、遇到的阻塞、关键决策。) - 2026-07-09 完成 T-568。
- 修改 `app/gui/tabs/generate.py`:
- 在 T-567 封面画廊底部新增「重新生成」按钮;点击后先弹中文确认,说明会重新生成当前商品封面并可能消耗 cmhub 点数。
- 确认后调用 `db.reset_generated(task_id, reset_title=False, reset_cover=True)`,由 T-566 归档当前新封面并清空封面指针;随后重新 `db.get_task()` 获取 fresh task,再启动单任务 `GenerateWorker([fresh_task])`,避免旧 task 残留 `new_cover_path` 导致跳过生图。
- 单任务重生使用当前②页面传入的标题/封面提示词和配置,但只在 worker 配置副本中强制 `ai.generate_cover=True`,不保存到设置,确保只补缺失封面、不重生标题。
- worker 完成后刷新②列表与画廊候选;成功时新 `_new.jpg` 默认选中;失败时保留已归档旧图并允许选择历史候选保存;归档改名失败时弹中文错误并不启动 worker。
- 增加并发守卫:②批量生成运行中时画廊「重新生成」禁用并显示 tooltip;画廊单任务重生运行中时②主界面「开始生成」和「重置生成结果」禁用/阻断。
- 修改 `tests/test_gui.py`:
- 用 fake `GenerateWorker`/thread 覆盖重新生成确认取消、fresh task 启动、临时强制 `generate_cover=True`、成功刷新并选中新图、失败后历史候选可保存、归档失败不启动 worker、批量生成与单任务重生互斥、主界面启动阻断。
- 验证通过:
- `py -3.10 -m unittest tests.test_gui`
- `py -3.10 -m unittest tests.test_gui tests.test_db`
- `python -m ruff check app tests main.py`
- `py -3.10 -m compileall app main.py`
- `py -3.10 -m unittest discover -s tests`(297 tests;PySide6 字体目录警告不影响结果)
- `git diff --check`
+271
View File
@@ -43,6 +43,45 @@ from app.gui.tabs.generate import CoverGalleryDialog, OriginalImageDialog
from app.gui.main_window import _fit_and_center_window from app.gui.main_window import _fit_and_center_window
class DummySignal:
def __init__(self):
self.callbacks = []
def connect(self, callback):
self.callbacks.append(callback)
def emit(self, *args):
for callback in list(self.callbacks):
callback(*args)
class FakeThread:
def __init__(self):
self.finished = DummySignal()
self.started = False
def start(self):
self.started = True
class FakeGenerateWorker:
instances = []
def __init__(self, tasks, prompt_values, db_path=None, config=None, diagnostic_log_dir=None):
self.tasks = list(tasks)
self.prompt_values = dict(prompt_values or {})
self.db_path = db_path
self.config = config
self.diagnostic_log_dir = diagnostic_log_dir
self.progress = DummySignal()
self.row_updated = DummySignal()
self.log = DummySignal()
self.failed = DummySignal()
self.finished = DummySignal()
self.cancelled = DummySignal()
FakeGenerateWorker.instances.append(self)
class GuiTests(TempDirMixin, unittest.TestCase): class GuiTests(TempDirMixin, unittest.TestCase):
@classmethod @classmethod
def setUpClass(cls): def setUpClass(cls):
@@ -89,6 +128,34 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assertTrue(image.save(path)) self.assertTrue(image.save(path))
return path return path
def _cover_gallery_task(self, temp_dir, generate_cover=True):
cfg = self.make_config(temp_dir)
cfg.setdefault("ai", {})["generate_cover"] = bool(generate_cover)
account = accounts.create_account("主店", "alias-a", debug_port=9222, config=cfg)
db.init_db(cfg["db_path"])
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]
old_cover = self.write_test_image(os.path.join(temp_dir, "old.jpg"))
canonical = image_paths.task_image_path(cfg["image_dir"], task, account, "new")
self.write_test_image(canonical)
db.set_collected(task.id, "旧标题", old_cover, path=cfg["db_path"])
db.set_generated(task.id, "新标题", canonical, path=cfg["db_path"])
return cfg, account, db.get_task(task.id, path=cfg["db_path"]), canonical
def assert_foreground(self, model, row, column, color): def assert_foreground(self, model, row, column, color):
value = model.data(model.index(row, column), gui.Qt.ForegroundRole) value = model.data(model.index(row, column), gui.Qt.ForegroundRole)
self.assertIsNotNone(value) self.assertIsNotNone(value)
@@ -141,6 +208,13 @@ class GuiTests(TempDirMixin, unittest.TestCase):
def clickedButton(self): def clickedButton(self):
return self.buttons[selected_label] return self.buttons[selected_label]
@staticmethod
def warning(parent, title, text):
box = FakeMessageBox(parent)
box.setWindowTitle(title)
box.setText(text)
return 0
return FakeMessageBox, boxes return FakeMessageBox, boxes
def test_collect_table_shows_product_unavailable_only_for_explicit_error(self): def test_collect_table_shows_product_unavailable_only_for_explicit_error(self):
@@ -2269,6 +2343,203 @@ class GuiTests(TempDirMixin, unittest.TestCase):
self.assert_removed(temp_dir) self.assert_removed(temp_dir)
def test_cover_gallery_regenerate_cancel_has_no_side_effect(self):
with self.make_temp_dir() as temp_dir:
cfg, account, task, canonical = self._cover_gallery_task(temp_dir)
dialog = CoverGalleryDialog(
task,
cfg["image_dir"],
cfg["db_path"],
account=account,
config=cfg,
prompt_values={"title": "标题", "cover": "封面"},
)
self.addCleanup(dialog.close)
message_box, _ = self.make_fake_message_box("取消")
FakeGenerateWorker.instances.clear()
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box), \
mock.patch("app.gui.tabs.generate.GenerateWorker", FakeGenerateWorker):
self.assertFalse(dialog.regenerate_cover())
self.assertEqual(canonical, db.get_task(task.id, path=cfg["db_path"]).new_cover_path)
self.assertTrue(os.path.exists(canonical))
self.assertEqual([], FakeGenerateWorker.instances)
self.assert_removed(temp_dir)
def test_cover_gallery_regenerate_uses_fresh_task_and_forces_cover_generation(self):
with self.make_temp_dir() as temp_dir:
cfg, account, task, canonical = self._cover_gallery_task(temp_dir, generate_cover=False)
fake_thread = FakeThread()
running_states = []
dialog = CoverGalleryDialog(
task,
cfg["image_dir"],
cfg["db_path"],
account=account,
config=cfg,
prompt_values={"title": "标题提示", "cover": "封面提示 {新标题}"},
regenerate_running_callback=running_states.append,
)
self.addCleanup(dialog.close)
message_box, _ = self.make_fake_message_box("重新生成")
FakeGenerateWorker.instances.clear()
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box), \
mock.patch("app.gui.tabs.generate.GenerateWorker", FakeGenerateWorker), \
mock.patch("app.gui.tabs.generate.run_worker", return_value=fake_thread):
self.assertTrue(dialog.regenerate_cover())
self.assertTrue(fake_thread.started)
self.assertEqual([True], running_states)
worker = FakeGenerateWorker.instances[0]
self.assertIsNone(worker.tasks[0].new_cover_path)
self.assertEqual("新标题", worker.tasks[0].new_title)
self.assertTrue(worker.config["ai"]["generate_cover"])
self.assertFalse(appconfig.ai_config(cfg).get("generate_cover", False))
self.assertFalse(os.path.exists(canonical))
self.assertTrue(any(path.endswith(".jpg") for path in dialog.candidates))
self.assertIn("开始重新生成商品", dialog.status_label.text())
self.assert_removed(temp_dir)
def test_cover_gallery_regenerate_success_refreshes_candidates_and_current_selection(self):
with self.make_temp_dir() as temp_dir:
cfg, account, task, canonical = self._cover_gallery_task(temp_dir)
fake_thread = FakeThread()
refreshed = []
dialog = CoverGalleryDialog(
task,
cfg["image_dir"],
cfg["db_path"],
account=account,
config=cfg,
prompt_values={"title": "标题", "cover": "封面"},
regenerate_running_callback=lambda running: None,
gallery_updated_callback=lambda task: refreshed.append(task.id),
)
self.addCleanup(dialog.close)
message_box, _ = self.make_fake_message_box("重新生成")
FakeGenerateWorker.instances.clear()
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box), \
mock.patch("app.gui.tabs.generate.GenerateWorker", FakeGenerateWorker), \
mock.patch("app.gui.tabs.generate.run_worker", return_value=fake_thread):
self.assertTrue(dialog.regenerate_cover())
worker = FakeGenerateWorker.instances[0]
self.write_test_image(canonical)
db.set_generated(task.id, "新标题", canonical, path=cfg["db_path"])
worker.finished.emit({"ok": True, "cover_done": 1, "failed": 0})
self.assertEqual(canonical, db.get_task(task.id, path=cfg["db_path"]).new_cover_path)
self.assertEqual(canonical, dialog.selected_path)
self.assertTrue(dialog.candidate_buttons[canonical].isChecked())
self.assertEqual("封面重新生成完成", dialog.status_label.text())
self.assertIn(task.id, refreshed)
self.assert_removed(temp_dir)
def test_cover_gallery_regenerate_failure_keeps_archived_candidate_selectable(self):
with self.make_temp_dir() as temp_dir:
cfg, account, task, canonical = self._cover_gallery_task(temp_dir)
fake_thread = FakeThread()
dialog = CoverGalleryDialog(
task,
cfg["image_dir"],
cfg["db_path"],
account=account,
config=cfg,
prompt_values={"title": "标题", "cover": "封面"},
)
self.addCleanup(dialog.close)
message_box, _ = self.make_fake_message_box("重新生成")
FakeGenerateWorker.instances.clear()
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box), \
mock.patch("app.gui.tabs.generate.GenerateWorker", FakeGenerateWorker), \
mock.patch("app.gui.tabs.generate.run_worker", return_value=fake_thread):
self.assertTrue(dialog.regenerate_cover())
worker = FakeGenerateWorker.instances[0]
worker.log.emit("[失败] 商品 51100639510 图片生成失败:测试失败")
worker.finished.emit({"ok": False, "cover_done": 0, "failed": 1})
self.assertFalse(os.path.exists(canonical))
self.assertIsNone(db.get_task(task.id, path=cfg["db_path"]).new_cover_path)
self.assertTrue(dialog.candidates)
archived = dialog.candidates[0]
dialog.candidate_buttons[archived].setChecked(True)
self.assertTrue(dialog.save_selection())
self.assertEqual(os.path.abspath(archived), db.get_task(task.id, path=cfg["db_path"]).new_cover_path)
self.assert_removed(temp_dir)
def test_cover_gallery_regenerate_archive_failure_does_not_start_worker(self):
with self.make_temp_dir() as temp_dir:
cfg, account, task, canonical = self._cover_gallery_task(temp_dir)
dialog = CoverGalleryDialog(
task,
cfg["image_dir"],
cfg["db_path"],
account=account,
config=cfg,
prompt_values={"title": "标题", "cover": "封面"},
)
self.addCleanup(dialog.close)
message_box, boxes = self.make_fake_message_box("重新生成")
FakeGenerateWorker.instances.clear()
with mock.patch("app.gui.tabs.generate.QMessageBox", message_box), \
mock.patch("app.db.os.rename", side_effect=PermissionError), \
mock.patch("app.gui.tabs.generate.GenerateWorker", FakeGenerateWorker):
self.assertFalse(dialog.regenerate_cover())
self.assertTrue(os.path.exists(canonical))
self.assertEqual(canonical, db.get_task(task.id, path=cfg["db_path"]).new_cover_path)
self.assertEqual([], FakeGenerateWorker.instances)
self.assertIn("请先关闭正在查看的封面图片再重置", boxes[-1].text)
self.assert_removed(temp_dir)
def test_cover_gallery_regenerate_disabled_while_bulk_generation_runs(self):
with self.make_temp_dir() as temp_dir:
cfg, account, task, canonical = self._cover_gallery_task(temp_dir)
dialog = CoverGalleryDialog(
task,
cfg["image_dir"],
cfg["db_path"],
account=account,
config=cfg,
prompt_values={"title": "标题", "cover": "封面"},
bulk_running_checker=lambda: True,
)
self.addCleanup(dialog.close)
self.assertFalse(dialog.regenerate_button.isEnabled())
self.assertIn("批量生成正在运行", dialog.regenerate_button.toolTip())
self.assertFalse(dialog.regenerate_cover())
self.assertEqual(canonical, db.get_task(task.id, path=cfg["db_path"]).new_cover_path)
self.assert_removed(temp_dir)
def test_generate_tab_blocks_batch_generate_while_gallery_regenerates(self):
with self.make_temp_dir() as temp_dir:
cfg = self.make_config(temp_dir)
statuses = []
tab = GenerateTab(config=cfg, status_callback=statuses.append)
self.addCleanup(tab.close)
tab._set_cover_gallery_regenerate_running(True)
tab.start_generate()
self.assertFalse(tab.generate_button.isEnabled())
self.assertIn("封面画廊正在重新生成", statuses[-1])
tab._set_cover_gallery_regenerate_running(False)
self.assertTrue(tab.generate_button.isEnabled())
self.assert_removed(temp_dir)
def test_generate_tab_allows_editing_generated_title_locally(self): def test_generate_tab_allows_editing_generated_title_locally(self):
with self.make_temp_dir() as temp_dir: with self.make_temp_dir() as temp_dir: