refactor: split gui into package
This commit is contained in:
@@ -0,0 +1,856 @@
|
||||
"""Tab 2: AI generation UI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..models import GenerateTaskTableModel
|
||||
from ..widgets import *
|
||||
from ..workers import GenerateWorker as _RealGenerateWorker
|
||||
|
||||
|
||||
def GenerateWorker(*args, **kwargs):
|
||||
return _call_package_attr("GenerateWorker", _RealGenerateWorker, *args, **kwargs)
|
||||
|
||||
class GenerateTab(QWidget):
|
||||
"""Tab 2: prompt area plus generation task filters/list."""
|
||||
|
||||
STATUS_FILTERS = [
|
||||
("全部状态", "all"),
|
||||
("待生成", "to_generate"),
|
||||
("已生成", "generated"),
|
||||
("失败", "failed"),
|
||||
("略过", "skipped"),
|
||||
("已更新", "applied"),
|
||||
]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
db_path=None,
|
||||
config=None,
|
||||
config_path=None,
|
||||
status_callback=None,
|
||||
title_prompt_path=None,
|
||||
cover_prompts_dir=None,
|
||||
open_accounts_callback=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.config = appconfig.load_config(config_path or appconfig.CONFIG_PATH) if config is None else config
|
||||
self.config_path = (
|
||||
config_path
|
||||
or self.config.get("config_path")
|
||||
or appconfig.CONFIG_PATH
|
||||
)
|
||||
self.db_path = _database_path(db_path, self.config)
|
||||
self.status_callback = status_callback
|
||||
self.open_accounts_callback = open_accounts_callback
|
||||
self.title_prompt_path = title_prompt_path or prompts.TITLE_PROMPT_PATH
|
||||
self.cover_prompts_dir = cover_prompts_dir or prompts.COVER_PROMPTS_DIR
|
||||
self.current_cover_template = None
|
||||
self.generate_worker = None
|
||||
self.generate_thread = None
|
||||
|
||||
self.title_prompt_edit = QPlainTextEdit()
|
||||
self.title_prompt_edit.setObjectName("titlePromptEdit")
|
||||
self.title_prompt_edit.setPlaceholderText("标题提示词")
|
||||
self.title_prompt_edit.setPlainText(
|
||||
prompts.load_title_prompt(self.title_prompt_path)
|
||||
)
|
||||
self.save_title_button = QPushButton("保存标题提示词")
|
||||
self.cover_prompt_edit = QPlainTextEdit()
|
||||
self.cover_prompt_edit.setObjectName("coverPromptEdit")
|
||||
self.cover_prompt_edit.setPlaceholderText("封面提示词")
|
||||
self.cover_template_combo = QComboBox()
|
||||
self.cover_template_combo.setObjectName("coverTemplateCombo")
|
||||
self.new_cover_template_button = QPushButton("新建")
|
||||
self.save_cover_template_button = QPushButton("保存")
|
||||
self.cover_template_actions_button = QPushButton("模板操作")
|
||||
self.cover_template_actions_button.setObjectName("coverTemplateActionsButton")
|
||||
self.cover_template_actions_menu = QMenu(self)
|
||||
self.save_cover_template_as_action = self.cover_template_actions_menu.addAction("另存为")
|
||||
self.save_cover_template_as_action.setObjectName("saveCoverTemplateAsAction")
|
||||
self.rename_cover_template_action = self.cover_template_actions_menu.addAction("重命名")
|
||||
self.rename_cover_template_action.setObjectName("renameCoverTemplateAction")
|
||||
self.delete_cover_template_action = self.cover_template_actions_menu.addAction("删除")
|
||||
self.delete_cover_template_action.setObjectName("deleteCoverTemplateAction")
|
||||
self.cover_template_actions_button.setMenu(self.cover_template_actions_menu)
|
||||
self.insert_title_button = QPushButton("插入标题")
|
||||
self.preview_prompt_button = QPushButton("预览")
|
||||
self.generate_button = QPushButton("开始生成")
|
||||
self.stop_generate_button = QPushButton("停止")
|
||||
self.reset_generate_button = QPushButton("重置生成结果")
|
||||
self.reset_generate_button.setObjectName("resetGenerateButton")
|
||||
self.stop_generate_button.setEnabled(False)
|
||||
ai_settings = appconfig.ai_config(self.config)
|
||||
self.generate_cover_checkbox = QCheckBox("生成封面图片(成本较高)")
|
||||
self.generate_cover_checkbox.setObjectName("generateCoverCheckbox")
|
||||
self.generate_cover_checkbox.setToolTip("关闭后只生成标题并保存为可更新,不调用图片模型")
|
||||
self.generate_cover_checkbox.setChecked(bool(ai_settings.get("generate_cover", False)))
|
||||
self.progress_label = QLabel("进度:标题0/0 · 图片0/0 · 失败0")
|
||||
self.title_progress_label = QLabel("标题 0/0")
|
||||
self.title_progress_label.setObjectName("generateTitleProgressLabel")
|
||||
self.title_progress_bar = QProgressBar()
|
||||
self.title_progress_bar.setObjectName("generateTitleProgressBar")
|
||||
self.title_progress_bar.setTextVisible(False)
|
||||
self.title_progress_bar.setRange(0, 1)
|
||||
self.title_progress_bar.setValue(0)
|
||||
self.cover_progress_label = QLabel("图片 0/0")
|
||||
self.cover_progress_label.setObjectName("generateCoverProgressLabel")
|
||||
self.cover_progress_bar = QProgressBar()
|
||||
self.cover_progress_bar.setObjectName("generateCoverProgressBar")
|
||||
self.cover_progress_bar.setTextVisible(False)
|
||||
self.cover_progress_bar.setRange(0, 1)
|
||||
self.cover_progress_bar.setValue(0)
|
||||
self.failed_progress_label = QLabel("失败 0")
|
||||
self.failed_progress_label.setObjectName("generateFailedProgressLabel")
|
||||
|
||||
left_panel = QWidget()
|
||||
left_layout = QVBoxLayout(left_panel)
|
||||
left_layout.setContentsMargins(0, 0, 12, 0)
|
||||
left_layout.addWidget(QLabel("标题提示词"))
|
||||
left_layout.addWidget(self.title_prompt_edit, 1)
|
||||
left_layout.addWidget(self.save_title_button)
|
||||
left_layout.addWidget(QLabel("封面提示词"))
|
||||
left_layout.addWidget(self.cover_template_combo)
|
||||
cover_template_layout = QHBoxLayout()
|
||||
cover_template_layout.addWidget(self.new_cover_template_button)
|
||||
cover_template_layout.addWidget(self.save_cover_template_button)
|
||||
cover_template_layout.addWidget(self.cover_template_actions_button)
|
||||
cover_template_layout.addStretch(1)
|
||||
left_layout.addLayout(cover_template_layout)
|
||||
left_layout.addWidget(self.cover_prompt_edit, 2)
|
||||
cover_action_layout = QHBoxLayout()
|
||||
cover_action_layout.addWidget(self.insert_title_button)
|
||||
cover_action_layout.addWidget(self.preview_prompt_button)
|
||||
left_layout.addLayout(cover_action_layout)
|
||||
|
||||
self.batch_filter = QComboBox()
|
||||
self.batch_filter.setObjectName("batchFilter")
|
||||
self.shop_filter = QComboBox()
|
||||
self.shop_filter.setObjectName("shopFilter")
|
||||
self.item_filter = QLineEdit()
|
||||
self.item_filter.setObjectName("generateItemFilter")
|
||||
self.item_filter.setPlaceholderText("商品ID")
|
||||
self.status_filter = QComboBox()
|
||||
self.status_filter.setObjectName("statusFilter")
|
||||
for label, value in self.STATUS_FILTERS:
|
||||
self.status_filter.addItem(label, value)
|
||||
self.refresh_button = QPushButton("刷新")
|
||||
|
||||
filter_layout = QHBoxLayout()
|
||||
filter_layout.addWidget(QLabel("批次"))
|
||||
filter_layout.addWidget(self.batch_filter, 2)
|
||||
filter_layout.addWidget(QLabel("店铺"))
|
||||
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(self.status_filter, 1)
|
||||
filter_layout.addWidget(self.refresh_button)
|
||||
|
||||
self.summary_label = QLabel("任务 0 条")
|
||||
self.batch_progress_label = _build_batch_progress_overview("generateBatchProgressOverview")
|
||||
(
|
||||
self.empty_state_card,
|
||||
self.empty_state_label,
|
||||
self.empty_state_button,
|
||||
) = _build_empty_state_card("generateEmptyStateCard")
|
||||
if self.open_accounts_callback is not None:
|
||||
self.empty_state_button.clicked.connect(self.open_accounts_callback)
|
||||
self.task_table = QTableView()
|
||||
self.model = GenerateTaskTableModel(self.task_table, db_path=self.db_path, status_callback=self._set_status)
|
||||
self.task_table.setModel(self.model)
|
||||
self.task_table.setSelectionBehavior(QAbstractItemView.SelectRows)
|
||||
self.task_table.setSelectionMode(QAbstractItemView.SingleSelection)
|
||||
self.task_table.setEditTriggers(QAbstractItemView.DoubleClicked | QAbstractItemView.EditKeyPressed)
|
||||
self.task_table.horizontalHeader().setSectionResizeMode(QHeaderView.Stretch)
|
||||
self.task_table.verticalHeader().setVisible(False)
|
||||
|
||||
self.run_log_view = QPlainTextEdit()
|
||||
self.run_log_view.setObjectName("generateRunLogView")
|
||||
self.run_log_view.setReadOnly(True)
|
||||
self.run_log_view.setMaximumHeight(128)
|
||||
self.run_log_view.setPlaceholderText("AI生成运行日志")
|
||||
|
||||
right_panel = QWidget()
|
||||
right_layout = QVBoxLayout(right_panel)
|
||||
right_layout.setContentsMargins(12, 0, 0, 0)
|
||||
right_layout.addLayout(filter_layout)
|
||||
right_layout.addWidget(self.summary_label)
|
||||
right_layout.addWidget(self.batch_progress_label)
|
||||
right_layout.addWidget(self.empty_state_card)
|
||||
right_layout.addWidget(self.task_table, 1)
|
||||
right_layout.addWidget(QLabel("AI生成运行日志"))
|
||||
right_layout.addWidget(self.run_log_view)
|
||||
|
||||
self.splitter = QSplitter(Qt.Horizontal)
|
||||
self.splitter.addWidget(left_panel)
|
||||
self.splitter.addWidget(right_panel)
|
||||
self.splitter.setStretchFactor(0, 1)
|
||||
self.splitter.setStretchFactor(1, 3)
|
||||
self.splitter.setSizes([280, 860])
|
||||
|
||||
title_progress_layout = QHBoxLayout()
|
||||
title_progress_layout.addWidget(self.title_progress_label)
|
||||
title_progress_layout.addWidget(self.title_progress_bar, 1)
|
||||
cover_progress_layout = QHBoxLayout()
|
||||
cover_progress_layout.addWidget(self.cover_progress_label)
|
||||
cover_progress_layout.addWidget(self.cover_progress_bar, 1)
|
||||
cover_progress_layout.addWidget(self.failed_progress_label)
|
||||
progress_layout = QVBoxLayout()
|
||||
progress_layout.addLayout(title_progress_layout)
|
||||
progress_layout.addLayout(cover_progress_layout)
|
||||
|
||||
button_layout = QHBoxLayout()
|
||||
button_layout.addStretch(1)
|
||||
button_layout.addWidget(self.generate_button)
|
||||
button_layout.addWidget(self.stop_generate_button)
|
||||
button_layout.addWidget(self.reset_generate_button)
|
||||
|
||||
bottom_layout = QHBoxLayout()
|
||||
bottom_layout.addWidget(self.generate_cover_checkbox)
|
||||
bottom_layout.addLayout(progress_layout, 1)
|
||||
bottom_layout.addLayout(button_layout)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(18, 18, 18, 18)
|
||||
layout.addWidget(self.splitter, 1)
|
||||
layout.addLayout(bottom_layout)
|
||||
|
||||
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.status_filter.currentIndexChanged.connect(self.refresh_tasks)
|
||||
self.refresh_button.clicked.connect(self.refresh_tasks)
|
||||
self.save_title_button.clicked.connect(self.save_title_prompt)
|
||||
self.cover_template_combo.currentIndexChanged.connect(self.load_selected_cover_template)
|
||||
self.new_cover_template_button.clicked.connect(self.new_cover_template)
|
||||
self.save_cover_template_button.clicked.connect(self.save_cover_template)
|
||||
self.save_cover_template_as_action.triggered.connect(self.save_cover_template_as)
|
||||
self.rename_cover_template_action.triggered.connect(self.rename_cover_template)
|
||||
self.delete_cover_template_action.triggered.connect(self.delete_cover_template)
|
||||
self.insert_title_button.clicked.connect(self.insert_title_placeholder)
|
||||
self.preview_prompt_button.clicked.connect(self.preview_cover_prompt)
|
||||
self.generate_cover_checkbox.toggled.connect(self._on_generate_cover_toggled)
|
||||
self.generate_button.clicked.connect(self.start_generate)
|
||||
self.stop_generate_button.clicked.connect(self.stop_generate)
|
||||
self.reset_generate_button.clicked.connect(self.reset_generated_result)
|
||||
self.task_table.doubleClicked.connect(self.show_task_images)
|
||||
|
||||
self.refresh_cover_templates()
|
||||
self.refresh_tasks()
|
||||
self._load_latest_generate_run_log()
|
||||
|
||||
def _set_status(self, message):
|
||||
if self.status_callback is not None:
|
||||
self.status_callback(message)
|
||||
|
||||
def _on_generate_log(self, message):
|
||||
self._append_generate_log(message)
|
||||
self._set_status(message)
|
||||
|
||||
def _append_generate_log(self, message):
|
||||
self.run_log_view.appendPlainText(str(message))
|
||||
scroll_bar = self.run_log_view.verticalScrollBar()
|
||||
scroll_bar.setValue(scroll_bar.maximum())
|
||||
|
||||
def _on_generate_cover_toggled(self, checked):
|
||||
previous = bool(appconfig.ai_config(self.config).get("generate_cover", False))
|
||||
if self._save_generate_cover_setting(show_status=True):
|
||||
return
|
||||
self.generate_cover_checkbox.blockSignals(True)
|
||||
self.generate_cover_checkbox.setChecked(previous)
|
||||
self.generate_cover_checkbox.blockSignals(False)
|
||||
|
||||
def _save_generate_cover_setting(self, show_status=True):
|
||||
generate_cover = bool(self.generate_cover_checkbox.isChecked())
|
||||
ai_settings = appconfig.ai_config(self.config)
|
||||
ai_settings["generate_cover"] = generate_cover
|
||||
payload = {
|
||||
key: value
|
||||
for key, value in self.config.items()
|
||||
if key not in {"config_path", "ai_models_path"}
|
||||
}
|
||||
payload["ai"] = ai_settings
|
||||
try:
|
||||
saved = appconfig.save_config(payload, path=self.config_path)
|
||||
except Exception as exc:
|
||||
self._set_status(f"生成封面开关保存失败:{exc}")
|
||||
return False
|
||||
internal = {
|
||||
key: value
|
||||
for key, value in self.config.items()
|
||||
if key in {"config_path", "ai_models_path"}
|
||||
}
|
||||
self.config.clear()
|
||||
self.config.update(saved)
|
||||
self.config.update(internal)
|
||||
if self.config_path != appconfig.CONFIG_PATH:
|
||||
self.config["config_path"] = self.config_path
|
||||
if show_status:
|
||||
mode = "会同时生成封面图片" if generate_cover else "只生成标题,不生成图片"
|
||||
self._set_status(f"AI生成设置已保存:{mode}")
|
||||
return True
|
||||
|
||||
def _load_latest_generate_run_log(self):
|
||||
try:
|
||||
logs = db.list_run_logs(limit=1, run_type="generate", path=self.db_path)
|
||||
if not logs:
|
||||
return
|
||||
events = db.list_run_log_events(logs[0].id, limit=40, path=self.db_path)
|
||||
except Exception:
|
||||
return
|
||||
lines = [
|
||||
f"{event.created_at} [{event.level}] {event.message}"
|
||||
for event in reversed(events)
|
||||
]
|
||||
self.run_log_view.setPlainText("\n".join(lines))
|
||||
scroll_bar = self.run_log_view.verticalScrollBar()
|
||||
scroll_bar.setValue(scroll_bar.maximum())
|
||||
|
||||
def save_title_prompt(self, checked=False):
|
||||
try:
|
||||
prompts.save_title_prompt(
|
||||
self.title_prompt_edit.toPlainText(),
|
||||
self.title_prompt_path,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._show_prompt_error(exc)
|
||||
return
|
||||
self._set_status("标题提示词已保存")
|
||||
|
||||
def refresh_cover_templates(self, selected=None):
|
||||
try:
|
||||
template_names = prompts.list_cover_templates(self.cover_prompts_dir)
|
||||
except Exception as exc:
|
||||
template_names = []
|
||||
self._show_prompt_error(exc)
|
||||
current = selected if selected is not None else self.current_cover_template
|
||||
self.cover_template_combo.blockSignals(True)
|
||||
self.cover_template_combo.clear()
|
||||
if template_names:
|
||||
for name in template_names:
|
||||
self.cover_template_combo.addItem(name, name)
|
||||
index = self.cover_template_combo.findData(current)
|
||||
self.cover_template_combo.setCurrentIndex(index if index >= 0 else 0)
|
||||
else:
|
||||
self.cover_template_combo.addItem("默认", None)
|
||||
self.cover_template_combo.setCurrentIndex(0)
|
||||
self.cover_template_combo.blockSignals(False)
|
||||
self.load_selected_cover_template()
|
||||
|
||||
def load_selected_cover_template(self, index=None):
|
||||
name = self.cover_template_combo.currentData()
|
||||
self.current_cover_template = name
|
||||
if name is None:
|
||||
self.cover_prompt_edit.setPlainText("")
|
||||
return
|
||||
try:
|
||||
self.cover_prompt_edit.setPlainText(
|
||||
prompts.load_cover_template(name, self.cover_prompts_dir)
|
||||
)
|
||||
except Exception as exc:
|
||||
self.cover_prompt_edit.setPlainText("")
|
||||
self._show_prompt_error(exc)
|
||||
|
||||
def new_cover_template(self, checked=False):
|
||||
name = self._ask_template_name("新建封面提示词模板")
|
||||
if not name:
|
||||
return
|
||||
try:
|
||||
prompts.save_cover_template(name, "", self.cover_prompts_dir)
|
||||
except Exception as exc:
|
||||
self._show_prompt_error(exc)
|
||||
return
|
||||
self.refresh_cover_templates(selected=name)
|
||||
self._set_status(f"封面提示词模板已新建:{name}")
|
||||
|
||||
def save_cover_template(self, checked=False):
|
||||
name = self.current_cover_template
|
||||
if name is None:
|
||||
self.save_cover_template_as()
|
||||
return
|
||||
try:
|
||||
prompts.save_cover_template(
|
||||
name,
|
||||
self.cover_prompt_edit.toPlainText(),
|
||||
self.cover_prompts_dir,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._show_prompt_error(exc)
|
||||
return
|
||||
self._set_status(f"封面提示词模板已保存:{name}")
|
||||
|
||||
def save_cover_template_as(self, checked=False):
|
||||
name = self._ask_template_name("另存封面提示词模板")
|
||||
if not name:
|
||||
return
|
||||
try:
|
||||
prompts.save_cover_template(
|
||||
name,
|
||||
self.cover_prompt_edit.toPlainText(),
|
||||
self.cover_prompts_dir,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._show_prompt_error(exc)
|
||||
return
|
||||
self.refresh_cover_templates(selected=name)
|
||||
self._set_status(f"封面提示词模板已另存为:{name}")
|
||||
|
||||
def rename_cover_template(self, checked=False):
|
||||
old_name = self.current_cover_template
|
||||
if old_name is None:
|
||||
self._set_status("没有可重命名的封面提示词模板")
|
||||
return
|
||||
new_name = self._ask_template_name("重命名封面提示词模板", text=old_name)
|
||||
if not new_name or new_name == old_name:
|
||||
return
|
||||
try:
|
||||
prompts.rename_cover_template(old_name, new_name, self.cover_prompts_dir)
|
||||
except Exception as exc:
|
||||
self._show_prompt_error(exc)
|
||||
return
|
||||
self.refresh_cover_templates(selected=new_name)
|
||||
self._set_status(f"封面提示词模板已重命名:{new_name}")
|
||||
|
||||
def delete_cover_template(self, checked=False):
|
||||
name = self.current_cover_template
|
||||
if name is None:
|
||||
self._set_status("没有可删除的封面提示词模板")
|
||||
return
|
||||
choice = QMessageBox.question(
|
||||
self,
|
||||
"删除封面提示词模板",
|
||||
f"确定删除「{name}」吗?",
|
||||
)
|
||||
if choice != QMessageBox.Yes:
|
||||
return
|
||||
try:
|
||||
prompts.delete_cover_template(name, self.cover_prompts_dir)
|
||||
except Exception as exc:
|
||||
self._show_prompt_error(exc)
|
||||
return
|
||||
self.refresh_cover_templates()
|
||||
self._set_status(f"封面提示词模板已删除:{name}")
|
||||
|
||||
def insert_title_placeholder(self, checked=False):
|
||||
self.cover_prompt_edit.insertPlainText("{新标题}")
|
||||
|
||||
def preview_cover_prompt(self, checked=False):
|
||||
task = self._selected_task()
|
||||
if task is None:
|
||||
self._set_status("没有可预览的任务")
|
||||
return
|
||||
rendered = prompts.render_prompt(
|
||||
self.cover_prompt_edit.toPlainText(),
|
||||
self._prompt_context(task),
|
||||
)
|
||||
QMessageBox.information(self, "封面提示词预览", rendered)
|
||||
self._set_status("封面提示词预览已生成")
|
||||
|
||||
def start_generate(self, checked=False):
|
||||
if self.generate_thread is not None:
|
||||
self._set_status("AI 生成正在进行...")
|
||||
return
|
||||
if not self._save_generate_cover_setting(show_status=False):
|
||||
return
|
||||
generate_cover = bool(self.generate_cover_checkbox.isChecked())
|
||||
tasks = [
|
||||
task for task in self.model.tasks
|
||||
if getattr(task, "stage", None) == "collected"
|
||||
]
|
||||
if not tasks:
|
||||
self._set_status("当前筛选结果没有可生成任务")
|
||||
return
|
||||
prompt_values = {
|
||||
"title": self.title_prompt_edit.toPlainText(),
|
||||
"cover": self.cover_prompt_edit.toPlainText(),
|
||||
}
|
||||
worker = GenerateWorker(
|
||||
tasks,
|
||||
prompt_values,
|
||||
db_path=self.db_path,
|
||||
config=self.config,
|
||||
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
|
||||
)
|
||||
worker.progress.connect(self._on_generate_progress)
|
||||
worker.row_updated.connect(self._on_generate_row_updated)
|
||||
worker.log.connect(self._on_generate_log)
|
||||
worker.failed.connect(self._on_generate_failed)
|
||||
worker.finished.connect(self._on_generate_finished)
|
||||
worker.cancelled.connect(self._on_generate_cancelled)
|
||||
self.run_log_view.clear()
|
||||
thread = run_worker(worker, thread_name="GenerateWorker", start=False)
|
||||
thread.finished.connect(lambda: self._forget_generate_thread(thread))
|
||||
self.generate_worker = worker
|
||||
self.generate_thread = thread
|
||||
self._set_generate_running(True)
|
||||
self._update_generate_progress(
|
||||
{
|
||||
"total": len(tasks),
|
||||
"title_done": 0,
|
||||
"cover_done": 0,
|
||||
"cover_total": len(tasks) if generate_cover else 0,
|
||||
"generated_done": 0,
|
||||
"failed": 0,
|
||||
"generate_cover": generate_cover,
|
||||
}
|
||||
)
|
||||
self._set_status(f"开始 AI 生成:{len(tasks)} 条")
|
||||
thread.start()
|
||||
|
||||
def stop_generate(self, checked=False):
|
||||
if self.generate_worker is not None:
|
||||
self.generate_worker.cancel()
|
||||
self._append_generate_log("[停止] 已收到停止请求,当前正在运行的任务结束后停止")
|
||||
self._set_status("正在停止 AI 生成...")
|
||||
|
||||
def reset_generated_result(self, checked=False):
|
||||
if self.generate_thread is not None:
|
||||
self._set_status("AI 生成正在进行,不能重置")
|
||||
return
|
||||
task = self._selected_task()
|
||||
if task is None:
|
||||
self._set_status("请选择要重置生成结果的任务")
|
||||
return
|
||||
has_ai_result = bool(getattr(task, "new_title", None) or getattr(task, "new_cover_path", None))
|
||||
if not has_ai_result and getattr(task, "stage", None) not in {"generated", "applied"}:
|
||||
self._set_status("选中任务没有可重置的生成结果")
|
||||
return
|
||||
lines = [
|
||||
"确定重置当前选中任务的本地生成结果吗?",
|
||||
"",
|
||||
f"商品ID:{task.item_id}",
|
||||
f"店铺:{self.model.account_name_for(task)}",
|
||||
"",
|
||||
"将清空新标题、新封面路径和错误信息,并退回到已采集状态。",
|
||||
"默认不删除本地新封面文件,不触碰 Shopee,也不会自动回写 Excel。",
|
||||
]
|
||||
if getattr(task, "new_cover_path", None):
|
||||
lines.append(f"本地新封面文件保留:{task.new_cover_path}")
|
||||
if getattr(task, "committed", 0):
|
||||
lines.extend([
|
||||
"",
|
||||
"注意:该记录曾经提交过线上。本地重置不会回滚 Shopee,后续重新生成/更新可能再次提交线上。",
|
||||
])
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"重置生成结果",
|
||||
"\n".join(lines),
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if answer != QMessageBox.Yes:
|
||||
self._set_status("已取消重置生成结果")
|
||||
return
|
||||
try:
|
||||
db.reset_generated(task.id, path=self.db_path)
|
||||
message = (
|
||||
"action=reset_generated step=db_write result=success "
|
||||
f"detail=清空AI生成结果 task_id={task.id}"
|
||||
)
|
||||
run_id = _write_reset_run_log(self.db_path, task, "reset_generated", message)
|
||||
except Exception as exc:
|
||||
QMessageBox.warning(self, "重置生成结果", str(exc))
|
||||
self._set_status(f"重置生成结果失败:{exc}")
|
||||
return
|
||||
self.refresh_tasks()
|
||||
self._append_generate_log(message)
|
||||
self._set_status(
|
||||
f"已重置生成结果:商品 {task.item_id},run_id={run_id}"
|
||||
)
|
||||
|
||||
def show_task_images(self, index):
|
||||
if index.isValid() and index.column() == 3:
|
||||
return
|
||||
task = self.model.task_at(index.row()) if index.isValid() else self._selected_task()
|
||||
if task is None:
|
||||
self._set_status("没有可预览的任务")
|
||||
return
|
||||
dialog = QDialog(self)
|
||||
dialog.setWindowTitle(f"封面对照:{task.item_id}")
|
||||
layout = QVBoxLayout(dialog)
|
||||
images_layout = QHBoxLayout()
|
||||
images_layout.addWidget(self._image_panel("旧封面", task.old_cover_path))
|
||||
images_layout.addWidget(self._image_panel("新封面", task.new_cover_path))
|
||||
layout.addLayout(images_layout)
|
||||
buttons = QDialogButtonBox(QDialogButtonBox.Close)
|
||||
buttons.rejected.connect(dialog.reject)
|
||||
layout.addWidget(buttons)
|
||||
dialog.resize(720, 420)
|
||||
dialog.exec()
|
||||
|
||||
def _image_panel(self, title, path):
|
||||
panel = QWidget()
|
||||
layout = QVBoxLayout(panel)
|
||||
layout.addWidget(QLabel(title))
|
||||
image_label = QLabel()
|
||||
image_label.setAlignment(Qt.AlignCenter)
|
||||
image_label.setMinimumSize(260, 260)
|
||||
image_label.setWordWrap(True)
|
||||
if path and os.path.exists(str(path)):
|
||||
pixmap = QPixmap(str(path))
|
||||
if not pixmap.isNull():
|
||||
image_label.setPixmap(
|
||||
pixmap.scaled(
|
||||
260,
|
||||
260,
|
||||
Qt.KeepAspectRatio,
|
||||
Qt.SmoothTransformation,
|
||||
)
|
||||
)
|
||||
else:
|
||||
image_label.setText(f"图片无法读取\n{path}")
|
||||
else:
|
||||
image_label.setText(f"无图片\n{path or ''}".strip())
|
||||
layout.addWidget(image_label, 1)
|
||||
return panel
|
||||
|
||||
def _set_generate_running(self, running):
|
||||
self.generate_button.setEnabled(not running)
|
||||
self.stop_generate_button.setEnabled(running)
|
||||
self.reset_generate_button.setEnabled(not running)
|
||||
self.refresh_button.setEnabled(not running)
|
||||
self.batch_filter.setEnabled(not running)
|
||||
self.shop_filter.setEnabled(not running)
|
||||
self.item_filter.setEnabled(not running)
|
||||
self.status_filter.setEnabled(not running)
|
||||
self.save_title_button.setEnabled(not running)
|
||||
self.new_cover_template_button.setEnabled(not running)
|
||||
self.save_cover_template_button.setEnabled(not running)
|
||||
self.cover_template_actions_button.setEnabled(not running)
|
||||
self.save_cover_template_as_action.setEnabled(not running)
|
||||
self.rename_cover_template_action.setEnabled(not running)
|
||||
self.delete_cover_template_action.setEnabled(not running)
|
||||
self.generate_cover_checkbox.setEnabled(not running)
|
||||
|
||||
def _forget_generate_thread(self, thread):
|
||||
if self.generate_thread is thread:
|
||||
self.generate_thread = None
|
||||
self.generate_worker = None
|
||||
|
||||
def _on_generate_progress(self, payload):
|
||||
self._update_generate_progress(payload)
|
||||
self._set_status("生成进度:" + self._generate_progress_text(payload))
|
||||
|
||||
def _on_generate_row_updated(self, task_id, fields):
|
||||
self.refresh_tasks()
|
||||
|
||||
def _on_generate_failed(self, task_id, error):
|
||||
self._set_status(f"AI 生成失败:{error}")
|
||||
|
||||
def _on_generate_finished(self, payload):
|
||||
self._set_generate_running(False)
|
||||
self.refresh_tasks()
|
||||
self._load_latest_generate_run_log()
|
||||
self._update_generate_progress(payload)
|
||||
if payload.get("error"):
|
||||
self._set_status(f"AI 生成失败:{payload.get('error')}")
|
||||
return
|
||||
self._set_status("AI 生成完成:" + self._generate_progress_text(payload))
|
||||
|
||||
def _on_generate_cancelled(self, payload):
|
||||
self._set_generate_running(False)
|
||||
self.refresh_tasks()
|
||||
self._load_latest_generate_run_log()
|
||||
self._update_generate_progress(payload)
|
||||
self._set_status("AI 生成已停止:" + self._generate_progress_text(payload))
|
||||
|
||||
def _update_generate_progress(self, payload):
|
||||
total = max(0, int(payload.get("total", 0) or 0))
|
||||
title_done = max(0, int(payload.get("title_done", 0) or 0))
|
||||
cover_done = max(0, int(payload.get("cover_done", 0) or 0))
|
||||
cover_total = self._cover_total_for_progress(payload, total)
|
||||
failed = max(0, int(payload.get("failed", 0) or 0))
|
||||
self.progress_label.setText("进度:" + self._generate_progress_text(payload))
|
||||
self.title_progress_label.setText(f"标题 {title_done}/{total}")
|
||||
self.cover_progress_label.setText(f"图片 {cover_done}/{cover_total}")
|
||||
self.failed_progress_label.setText(f"失败 {failed}")
|
||||
self._set_progress_bar(self.title_progress_bar, title_done, total)
|
||||
self._set_progress_bar(self.cover_progress_bar, cover_done, cover_total)
|
||||
|
||||
def _set_progress_bar(self, bar, done, total):
|
||||
maximum = max(1, int(total or 0))
|
||||
value = min(max(0, int(done or 0)), maximum)
|
||||
bar.setRange(0, maximum)
|
||||
bar.setValue(value)
|
||||
|
||||
def _cover_total_for_progress(self, payload, total):
|
||||
cover_total = payload.get("cover_total")
|
||||
if cover_total is None:
|
||||
cover_total = total if payload.get("generate_cover", True) else 0
|
||||
return max(0, int(cover_total or 0))
|
||||
|
||||
def _generate_progress_text(self, payload):
|
||||
total = max(0, int(payload.get("total", 0) or 0))
|
||||
cover_total = self._cover_total_for_progress(payload, total)
|
||||
return "标题{title}/{total} · 图片{cover}/{cover_total} · 失败{failed}".format(
|
||||
title=payload.get("title_done", 0),
|
||||
cover=payload.get("cover_done", 0),
|
||||
cover_total=cover_total,
|
||||
total=payload.get("total", 0),
|
||||
failed=payload.get("failed", 0),
|
||||
)
|
||||
|
||||
def _selected_task(self):
|
||||
index = self.task_table.currentIndex()
|
||||
if index.isValid():
|
||||
return self.model.task_at(index.row())
|
||||
if self.model.rowCount() > 0:
|
||||
return self.model.task_at(0)
|
||||
return None
|
||||
|
||||
def _prompt_context(self, task):
|
||||
return {
|
||||
"old_title": task.old_title,
|
||||
"new_title": task.new_title,
|
||||
"item_id": task.item_id,
|
||||
"account_name": self.model.account_name_for(task),
|
||||
"alias": task.alias,
|
||||
}
|
||||
|
||||
def _ask_template_name(self, title, text=""):
|
||||
value, ok = QInputDialog.getText(
|
||||
self,
|
||||
title,
|
||||
"模板名",
|
||||
QLineEdit.Normal,
|
||||
text,
|
||||
)
|
||||
if not ok:
|
||||
return None
|
||||
return str(value).strip()
|
||||
|
||||
def _show_prompt_error(self, error):
|
||||
message = str(error)
|
||||
QMessageBox.warning(self, "提示词管理", message)
|
||||
self._set_status(message)
|
||||
|
||||
def refresh_tasks(self, checked=False):
|
||||
try:
|
||||
db.init_db(self.db_path)
|
||||
batches = db.list_batches(path=self.db_path)
|
||||
accounts_rows = accounts.list_accounts(path=self.db_path, config=self.config)
|
||||
selected_batch = self.batch_filter.currentData()
|
||||
selected_shop = self.shop_filter.currentData()
|
||||
selected_status = self.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()
|
||||
batch_tasks = db.list_tasks(batch_id=selected_batch, path=self.db_path)
|
||||
self._populate_shop_filter(batch_tasks, accounts_rows, selected_shop)
|
||||
selected_shop = self.shop_filter.currentData()
|
||||
filtered_tasks = [
|
||||
task for task in batch_tasks
|
||||
if self._matches_shop(task, selected_shop)
|
||||
and self._matches_item(task, item_query)
|
||||
and self._matches_status(task, selected_status)
|
||||
]
|
||||
except Exception as exc:
|
||||
self.model.set_tasks([], [])
|
||||
self.summary_label.setText("任务读取失败")
|
||||
_set_batch_progress_overview(self.batch_progress_label, [])
|
||||
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
|
||||
self._set_status(f"AI 生成任务读取失败:{exc}")
|
||||
return
|
||||
self.model.set_tasks(filtered_tasks, accounts_rows)
|
||||
self.summary_label.setText(
|
||||
f"任务 {len(filtered_tasks)}/{len(batch_tasks)} 条"
|
||||
)
|
||||
_set_batch_progress_overview(self.batch_progress_label, batch_tasks)
|
||||
self._update_empty_state(batch_tasks, filtered_tasks, accounts_rows)
|
||||
|
||||
def _update_empty_state(self, batch_tasks, filtered_tasks, account_rows):
|
||||
if not account_rows:
|
||||
_set_empty_state(
|
||||
self.empty_state_card,
|
||||
self.empty_state_label,
|
||||
self.empty_state_button,
|
||||
"第一步:前往『④账号管理』配置并登录账号,再回到②生成标题和封面。",
|
||||
self.open_accounts_callback is not None,
|
||||
)
|
||||
return
|
||||
if not batch_tasks:
|
||||
_set_empty_state(
|
||||
self.empty_state_card,
|
||||
self.empty_state_label,
|
||||
self.empty_state_button,
|
||||
"还没有可生成任务。请先在①导入采集完成旧标题和旧封面采集。",
|
||||
)
|
||||
return
|
||||
if not filtered_tasks:
|
||||
_set_empty_state(
|
||||
self.empty_state_card,
|
||||
self.empty_state_label,
|
||||
self.empty_state_button,
|
||||
"当前筛选没有匹配的生成任务,请调整批次、店铺、商品ID或状态筛选。",
|
||||
)
|
||||
return
|
||||
_set_empty_state(self.empty_state_card, self.empty_state_label, self.empty_state_button)
|
||||
|
||||
def _populate_batch_filter(self, batches, selected_batch):
|
||||
previous = selected_batch if selected_batch in {batch.id for batch in batches} else None
|
||||
self.batch_filter.blockSignals(True)
|
||||
self.batch_filter.clear()
|
||||
self.batch_filter.addItem("全部批次", None)
|
||||
for batch in batches:
|
||||
self.batch_filter.addItem(self._batch_label(batch), batch.id)
|
||||
index = self.batch_filter.findData(previous)
|
||||
self.batch_filter.setCurrentIndex(index if index >= 0 else 0)
|
||||
self.batch_filter.blockSignals(False)
|
||||
|
||||
def _populate_shop_filter(self, tasks, account_rows, selected_shop):
|
||||
account_by_alias = {
|
||||
str(account.alias).strip(): account
|
||||
for account in account_rows
|
||||
if str(account.alias).strip()
|
||||
}
|
||||
aliases = []
|
||||
for task in tasks:
|
||||
alias = str(task.alias).strip()
|
||||
if alias and alias not in aliases:
|
||||
aliases.append(alias)
|
||||
previous = selected_shop if selected_shop in aliases else None
|
||||
self.shop_filter.blockSignals(True)
|
||||
self.shop_filter.clear()
|
||||
self.shop_filter.addItem("全部店铺", None)
|
||||
for alias in sorted(aliases, key=lambda value: self._shop_label(value, account_by_alias)):
|
||||
self.shop_filter.addItem(self._shop_label(alias, account_by_alias), alias)
|
||||
index = self.shop_filter.findData(previous)
|
||||
self.shop_filter.setCurrentIndex(index if index >= 0 else 0)
|
||||
self.shop_filter.blockSignals(False)
|
||||
|
||||
def _batch_label(self, batch):
|
||||
source_files = batch.source_files
|
||||
first_file = os.path.basename(source_files[0]) if source_files else batch.id
|
||||
return f"{batch.created_at} · {first_file}"
|
||||
|
||||
def _shop_label(self, alias, account_by_alias):
|
||||
account = account_by_alias.get(alias)
|
||||
if account is not None:
|
||||
return f"{account.account_name} ({alias})"
|
||||
return alias
|
||||
|
||||
def _matches_shop(self, task, selected_shop):
|
||||
return selected_shop is None or str(task.alias).strip() == selected_shop
|
||||
|
||||
def _matches_item(self, task, item_query):
|
||||
if not item_query:
|
||||
return True
|
||||
return item_query in str(getattr(task, "item_id", ""))
|
||||
|
||||
def _matches_status(self, task, selected_status):
|
||||
if selected_status in (None, "all"):
|
||||
return True
|
||||
if selected_status == "to_generate":
|
||||
return task.stage == "collected" and task.status in {"success", "pending"}
|
||||
if selected_status == "generated":
|
||||
return task.stage == "generated"
|
||||
if selected_status == "applied":
|
||||
return task.stage == "applied"
|
||||
if selected_status == "failed":
|
||||
return task.status == "failed"
|
||||
if selected_status == "skipped":
|
||||
return task.status == "skipped"
|
||||
return True
|
||||
|
||||
|
||||
Reference in New Issue
Block a user