"""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