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
+131 -24
View File
@@ -17,6 +17,11 @@ def WriteBackWorker(*args, **kwargs):
class ApplyTab(QWidget):
"""Tab 3: list generated tasks and confirm the update scope."""
UPDATE_MODE_ITEMS = [
("只更新标题", "title"),
("只更新封面", "cover"),
("更新标题和封面", "title_cover"),
]
STATUS_FILTERS = [
("已生成", "generated"),
("失败", "failed"),
@@ -36,6 +41,7 @@ class ApplyTab(QWidget):
):
super().__init__(parent)
self.config = appconfig.load_config() if config is None else config
self.config_path = 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
@@ -110,8 +116,20 @@ class ApplyTab(QWidget):
self.write_back_button = QPushButton("回写结果到 Excel")
self.stop_update_button.setEnabled(False)
self.write_back_button.setEnabled(False)
self.update_mode_combo = QComboBox()
self.update_mode_combo.setObjectName("applyUpdateModeCombo")
self.update_mode_combo.setToolTip("选择本轮要更新标题、封面,或同时更新标题和封面")
for label, value in self.UPDATE_MODE_ITEMS:
self.update_mode_combo.addItem(label, value)
self._set_combo_by_data(
self.update_mode_combo,
self._shopee_update_config().get("update_mode", "title"),
)
action_layout = QHBoxLayout()
action_layout.addWidget(QLabel("更新内容"))
action_layout.addWidget(self.update_mode_combo)
action_layout.addSpacing(16)
action_layout.addWidget(self.preview_update_button)
action_layout.addWidget(self.start_update_button)
action_layout.addWidget(self.stop_update_button)
@@ -137,6 +155,7 @@ class ApplyTab(QWidget):
self.refresh_button.clicked.connect(self.refresh_tasks)
self.preview_update_button.clicked.connect(self.preview_update)
self.start_update_button.clicked.connect(self.start_update)
self.update_mode_combo.currentIndexChanged.connect(self._on_update_mode_changed)
self.stop_update_button.clicked.connect(self.stop_update)
self.reset_update_button.clicked.connect(self.reset_apply_status)
self.task_table.customContextMenuRequested.connect(self.show_task_context_menu)
@@ -227,12 +246,18 @@ class ApplyTab(QWidget):
return
tasks = [
task for task in self.model.tasks
if self._is_actionable_task(task)
if self._is_update_candidate_task(task)
]
if not tasks:
self._set_status("当前筛选结果没有可更新任务")
return
update_cfg = self._shopee_update_config()
update_mode = self._current_update_mode()
content_error = self._update_content_error(tasks, update_mode)
if content_error:
QMessageBox.warning(self, "更新内容未生成", content_error)
self._set_status(content_error.replace("\n", " "))
return
dry_run = bool(dry_run)
safety_error = self._update_safety_error(tasks, dry_run=dry_run)
if safety_error:
@@ -256,6 +281,7 @@ class ApplyTab(QWidget):
config=self.config,
close_success_tab=bool(update_cfg.get("close_success_tab", False)),
dry_run=dry_run,
update_mode=update_mode,
parallel_accounts=bool(update_cfg.get("parallel_accounts", False)),
max_parallel_accounts=max(
1,
@@ -368,11 +394,10 @@ class ApplyTab(QWidget):
return self.model.task_at(0)
return None
def _is_actionable_task(self, task):
def _is_update_candidate_task(self, task):
return (
getattr(task, "stage", None) == "generated"
and getattr(task, "status", None) in {"success", "pending", "failed"}
and bool(getattr(task, "new_title", None) or getattr(task, "new_cover_path", None))
)
def _populate_batch_filter(self, batches, selected_batch):
@@ -464,7 +489,9 @@ class ApplyTab(QWidget):
def _confirmation_message(self, tasks, dry_run=False):
update_cfg = self._shopee_update_config()
cover_text = "允许" if update_cfg.get("allow_cover_update") else "不允许"
update_mode = self._current_update_mode()
update_mode_text = self._update_mode_label(update_mode)
cover_warning = "\n本轮将更新线上封面,请确认封面内容无误。" if appconfig.update_mode_includes_cover(update_mode) else ""
close_text = "是" if update_cfg.get("close_success_tab") else "否"
batch_size = max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
batch_count = (len(tasks) + batch_size - 1) // batch_size if tasks else 0
@@ -484,13 +511,15 @@ class ApplyTab(QWidget):
+ f"店铺:{self._shop_filter_label()}\n"
+ f"商品ID:{self._item_filter_label()}\n"
+ f"状态:{self._status_label()}\n"
+ f"更新内容:{update_mode_text}\n"
+ f"任务数:{len(tasks)}\n"
+ f"预计批次:{batch_count}\n\n"
+ "安全设置:"
+ f"封面更新={cover_text},"
+ f"每批最大更新条数={batch_size},"
+ f"成功后关闭新页={close_text},"
+ f"多账号并行={parallel_text}\n\n"
+ cover_warning
+ ("\n\n" if cover_warning else "")
+ (
"检查只写运行日志,不打开蝦皮、不点击「更新」、不改任务状态。"
if dry_run
@@ -507,17 +536,6 @@ class ApplyTab(QWidget):
"设置未开启「允许真实提交线上商品」,已阻止本次更新。\n"
"请到⑤设置 > 蝦皮更新安全开启该开关后再开始更新。"
)
if not update_cfg.get("allow_cover_update", False):
cover_tasks = [
str(getattr(task, "item_id", ""))
for task in tasks
if getattr(task, "new_cover_path", None)
]
if cover_tasks:
return (
"设置未开启「允许更新封面」,当前任务包含新封面路径,已阻止本次更新。\n"
"请到⑤设置 > 蝦皮更新安全开启该开关,或先筛掉含新封面的任务。"
)
return None
def _show_update_safety_error(self, message):
@@ -534,13 +552,99 @@ class ApplyTab(QWidget):
self.open_settings_callback()
def _shopee_update_config(self):
defaults = appconfig.default_config().get("shopee_update", {})
loaded = self.config.get("shopee_update", {})
if not isinstance(loaded, dict):
loaded = {}
merged = dict(defaults)
merged.update(loaded)
return merged
return appconfig.shopee_update_config(self.config)
def _current_update_mode(self):
return appconfig.normalize_update_mode(
self.update_mode_combo.currentData(),
allow_cover_update=False,
)
def _update_mode_label(self, mode):
mode = appconfig.normalize_update_mode(mode)
return {
"title": "只更新标题",
"cover": "只更新封面",
"title_cover": "更新标题和封面",
}.get(mode, "只更新标题")
def _set_combo_by_data(self, combo, value):
index = combo.findData(value)
combo.setCurrentIndex(index if index >= 0 else 0)
def _on_update_mode_changed(self, index=None):
previous = self._shopee_update_config().get("update_mode", "title")
if self._save_update_mode_setting(show_status=True):
return
self.update_mode_combo.blockSignals(True)
self._set_combo_by_data(self.update_mode_combo, previous)
self.update_mode_combo.blockSignals(False)
def _save_update_mode_setting(self, show_status=True):
update_mode = self._current_update_mode()
update_cfg = self._shopee_update_config()
update_cfg["update_mode"] = update_mode
update_cfg["allow_cover_update"] = appconfig.update_mode_includes_cover(update_mode)
payload = {
key: value
for key, value in self.config.items()
if key not in {"config_path", "ai_models_path", "cmhub_config_path", "data_dir"}
}
payload["shopee_update"] = update_cfg
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", "cmhub_config_path", "data_dir"}
}
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:
self._set_status(f"更新内容已设置为:{self._update_mode_label(update_mode)}")
return True
def _update_content_error(self, tasks, update_mode):
missing_title = [
task for task in tasks
if appconfig.update_mode_includes_title(update_mode)
and not str(getattr(task, "new_title", "") or "").strip()
]
missing_cover = [
task for task in tasks
if appconfig.update_mode_includes_cover(update_mode)
and not str(getattr(task, "new_cover_path", "") or "").strip()
]
if not missing_title and not missing_cover:
return None
lines = []
mode_text = self._update_mode_label(update_mode)
if missing_title:
lines.append(f"当前筛选结果中有 {len(missing_title)} 条缺少新标题,不能执行“{mode_text}”。")
lines.append("请先回到②AI生成选择“只生成标题”或“生成标题和封面”。")
lines.append(f"示例商品ID:{self._sample_item_ids(missing_title)}")
if missing_cover:
if lines:
lines.append("")
lines.append(f"当前筛选结果中有 {len(missing_cover)} 条缺少新封面,不能执行“{mode_text}”。")
lines.append("请先回到②AI生成选择“只生成封面”或“生成标题和封面”。")
lines.append(f"示例商品ID:{self._sample_item_ids(missing_cover)}")
return "\n".join(lines)
def _sample_item_ids(self, tasks):
values = [
str(getattr(task, "item_id", "") or "").strip()
for task in tasks[:5]
]
values = [value for value in values if value]
suffix = f" 等 {len(tasks)} 条" if len(tasks) > 5 else ""
return "、".join(values) + suffix if values else "无"
def _set_apply_running(self, running):
self.preview_update_button.setEnabled(not running)
@@ -552,6 +656,7 @@ class ApplyTab(QWidget):
self.shop_filter.setEnabled(not running)
self.item_filter.setEnabled(not running)
self.status_filter.setEnabled(not running)
self.update_mode_combo.setEnabled(not running)
self._update_write_back_button()
def _set_result_write_back_running(self, running):
@@ -563,6 +668,7 @@ class ApplyTab(QWidget):
self.shop_filter.setEnabled(not running)
self.item_filter.setEnabled(not running)
self.status_filter.setEnabled(not running)
self.update_mode_combo.setEnabled(not running)
self.write_back_button.setEnabled(False if running else bool(self._active_batch_ids()))
def _forget_apply_thread(self, thread):
@@ -637,8 +743,9 @@ class ApplyTab(QWidget):
def _reset_run_log(self, tasks, dry_run=False, batch_size=1):
self.run_log_view.clear()
action = "检查" if dry_run else "更新"
mode_text = self._update_mode_label(self._current_update_mode())
self._append_run_log(
f"本轮{action}开始:任务 {len(tasks)} 条,每批 {batch_size} 条"
f"本轮{action}开始:任务 {len(tasks)} 条,更新内容:{mode_text},每批 {batch_size} 条"
)
def _load_latest_run_log(self):
+72 -38
View File
@@ -751,6 +751,7 @@ def _cover_regenerate_config(config):
runtime_config = dict(config or {})
ai_settings = appconfig.ai_config(runtime_config)
ai_settings["generate_cover"] = True
ai_settings["generate_mode"] = "cover"
runtime_config["ai"] = ai_settings
return runtime_config
@@ -842,6 +843,11 @@ class GenerateTab(QWidget):
"cover": "封面",
"all": "标题和封面",
}
GENERATE_MODE_ITEMS = [
("只生成标题", "title"),
("只生成封面", "cover"),
("生成标题和封面", "title_cover"),
]
TABLE_COLUMN_WEIGHTS = (1.0, 0.5, 1.8375, 1.8375, 0.4125, 0.4125)
def __init__(
@@ -910,11 +916,12 @@ class GenerateTab(QWidget):
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.generate_mode_combo = QComboBox()
self.generate_mode_combo.setObjectName("generateModeCombo")
self.generate_mode_combo.setToolTip("选择本轮生成标题、封面,或同时生成标题和封面")
for label, value in self.GENERATE_MODE_ITEMS:
self.generate_mode_combo.addItem(label, value)
self._set_combo_by_data(self.generate_mode_combo, appconfig.ai_generate_mode(self.config))
self.progress_label = QLabel("进度:标题0/0 · 图片0/0 · 失败0")
self.title_progress_label = QLabel("标题 0/0")
self.title_progress_label.setObjectName("generateTitleProgressLabel")
@@ -1007,7 +1014,7 @@ class GenerateTab(QWidget):
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.model.set_generate_cover_enabled(self.generate_cover_checkbox.isChecked())
self.model.set_generate_cover_enabled(self._current_generate_mode_includes_cover())
self.task_table.setModel(self.model)
self.task_table.setSelectionBehavior(QAbstractItemView.SelectRows)
self.task_table.setSelectionMode(QAbstractItemView.ExtendedSelection)
@@ -1060,7 +1067,8 @@ class GenerateTab(QWidget):
button_layout.addWidget(self.reset_generate_button)
bottom_layout = QHBoxLayout()
bottom_layout.addWidget(self.generate_cover_checkbox)
bottom_layout.addWidget(QLabel("生成内容"))
bottom_layout.addWidget(self.generate_mode_combo)
bottom_layout.addLayout(progress_layout, 1)
bottom_layout.addLayout(button_layout)
@@ -1084,7 +1092,7 @@ class GenerateTab(QWidget):
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_mode_combo.currentIndexChanged.connect(self._on_generate_mode_changed)
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)
@@ -1115,11 +1123,11 @@ class GenerateTab(QWidget):
def _show_current_run_log_empty(self):
self.run_log_view.setPlainText("本轮日志会在开始运行后显示")
def _reset_generate_run_log(self, tasks, generate_cover):
def _reset_generate_run_log(self, tasks, generate_mode):
self.run_log_view.clear()
cover_text = "是" if generate_cover else "否"
mode_text = self._generate_mode_label(generate_mode)
self._append_generate_log(
f"本轮AI生成开始:任务 {len(tasks)} 条,生成封面:{cover_text}"
f"本轮AI生成开始:任务 {len(tasks)} 条,生成内容:{mode_text}"
)
def _reset_generation_elapsed(self):
@@ -1208,16 +1216,17 @@ class GenerateTab(QWidget):
self._refresh_elapsed_labels()
self._update_elapsed_timer_state()
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):
self.model.set_generate_cover_enabled(bool(checked))
def _on_generate_mode_changed(self, index=None):
previous = appconfig.ai_generate_mode(self.config)
current = self._current_generate_mode()
if self._save_generate_mode_setting(show_status=True):
self.model.set_generate_cover_enabled(appconfig.generate_mode_includes_cover(current))
self.refresh_tasks()
return
self.generate_cover_checkbox.blockSignals(True)
self.generate_cover_checkbox.setChecked(previous)
self.generate_cover_checkbox.blockSignals(False)
self.model.set_generate_cover_enabled(previous)
self.generate_mode_combo.blockSignals(True)
self._set_combo_by_data(self.generate_mode_combo, previous)
self.generate_mode_combo.blockSignals(False)
self.model.set_generate_cover_enabled(appconfig.generate_mode_includes_cover(previous))
def _apply_task_table_column_widths(self):
if self.model.columnCount() != len(self.TABLE_COLUMN_WEIGHTS):
@@ -1237,9 +1246,11 @@ class GenerateTab(QWidget):
used += section_width
header.resizeSection(column, section_width)
def _save_generate_cover_setting(self, show_status=True):
generate_cover = bool(self.generate_cover_checkbox.isChecked())
def _save_generate_mode_setting(self, show_status=True):
generate_mode = self._current_generate_mode()
generate_cover = appconfig.generate_mode_includes_cover(generate_mode)
ai_settings = appconfig.ai_config(self.config)
ai_settings["generate_mode"] = generate_mode
ai_settings["generate_cover"] = generate_cover
payload = {
key: value
@@ -1250,7 +1261,7 @@ class GenerateTab(QWidget):
try:
saved = appconfig.save_config(payload, path=self.config_path)
except Exception as exc:
self._set_status(f"生成封面开关保存失败:{exc}")
self._set_status(f"生成内容设置保存失败:{exc}")
return False
internal = {
key: value
@@ -1263,10 +1274,30 @@ class GenerateTab(QWidget):
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}")
self._set_status(f"AI生成设置已保存:{self._generate_mode_label(generate_mode)}")
return True
def _current_generate_mode(self):
return appconfig.normalize_generate_mode(
self.generate_mode_combo.currentData(),
generate_cover=False,
)
def _current_generate_mode_includes_cover(self):
return appconfig.generate_mode_includes_cover(self._current_generate_mode())
def _generate_mode_label(self, mode):
mode = appconfig.normalize_generate_mode(mode)
return {
"title": "只生成标题",
"cover": "只生成封面",
"title_cover": "生成标题和封面",
}.get(mode, "只生成标题")
def _set_combo_by_data(self, combo, value):
index = combo.findData(value)
combo.setCurrentIndex(index if index >= 0 else 0)
def _load_latest_generate_run_log(self):
try:
logs = db.list_run_logs(limit=1, run_type="generate", path=self.db_path)
@@ -1433,21 +1464,23 @@ class GenerateTab(QWidget):
if self.generate_thread is not None:
self._set_status("AI 生成正在进行...")
return
if not self._save_generate_cover_setting(show_status=False):
if not self._save_generate_mode_setting(show_status=False):
return
generate_cover = bool(self.generate_cover_checkbox.isChecked())
generate_mode = self._current_generate_mode()
generate_cover = appconfig.generate_mode_includes_cover(generate_mode)
tasks = [
task for task in self.model.tasks
if ai.is_generatable_task(task, generate_cover=generate_cover)
if ai.is_generatable_task(task, generate_mode=generate_mode)
]
if not tasks:
self._set_status(
"当前筛选结果没有可生成的缺失内容;请先在①导入采集完成采集,或勾选“生成封面图片(成本较高)”补封面"
)
if generate_mode == "cover":
self._set_status("当前筛选结果没有可生成封面的任务,请先生成标题。")
else:
self._set_status("当前筛选结果没有可生成的缺失内容;请先在①导入采集完成采集,或调整生成内容")
return
component_totals = ai.generation_component_totals(
tasks,
generate_cover=generate_cover,
generate_mode=generate_mode,
)
prompt_values = {
"title": self.title_prompt_edit.toPlainText(),
@@ -1466,11 +1499,11 @@ class GenerateTab(QWidget):
worker.failed.connect(self._on_generate_failed)
worker.finished.connect(self._on_generate_finished)
worker.cancelled.connect(self._on_generate_cancelled)
self._reset_generate_run_log(tasks, generate_cover)
self._reset_generate_run_log(tasks, generate_mode)
self._reset_cmhub_balance_label()
self._start_generation_elapsed(
component_totals["title_total"],
component_totals["cover_total"] if generate_cover else 0,
component_totals["cover_total"],
)
thread = run_worker(worker, thread_name="GenerateWorker", start=False)
thread.finished.connect(lambda: self._forget_generate_thread(thread))
@@ -1483,10 +1516,11 @@ class GenerateTab(QWidget):
"title_total": component_totals["title_total"],
"title_done": 0,
"cover_done": 0,
"cover_total": component_totals["cover_total"] if generate_cover else 0,
"cover_total": component_totals["cover_total"],
"generated_done": 0,
"failed": 0,
"generate_cover": generate_cover,
"generate_mode": generate_mode,
}
)
self._set_status(f"开始 AI 生成:{len(tasks)} 条")
@@ -1739,7 +1773,7 @@ class GenerateTab(QWidget):
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)
self.generate_mode_combo.setEnabled(not running)
self._sync_cover_gallery_regenerate_guard()
def _set_cover_gallery_regenerate_running(self, running):
@@ -1984,7 +2018,7 @@ class GenerateTab(QWidget):
_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_generate_cover_enabled(self.generate_cover_checkbox.isChecked())
self.model.set_generate_cover_enabled(self._current_generate_mode_includes_cover())
self.model.set_tasks(filtered_tasks, accounts_rows)
self._apply_task_table_column_widths()
self.summary_label.setText(
@@ -2082,8 +2116,8 @@ class GenerateTab(QWidget):
if selected_status in (None, "all"):
return True
if selected_status == "to_generate":
generate_cover = bool(self.generate_cover_checkbox.isChecked())
return ai.is_generatable_task(task, generate_cover=generate_cover) and task.status != "failed"
generate_mode = self._current_generate_mode()
return ai.is_generatable_task(task, generate_mode=generate_mode) and task.status != "failed"
if selected_status == "generated":
return task.stage == "generated"
if selected_status == "applied":
+13 -13
View File
@@ -208,6 +208,7 @@ class SettingsTab(QWidget):
self.allow_real_submit_checkbox.setObjectName("allowRealSubmitCheckbox")
self.allow_cover_update_checkbox = QCheckBox("允许更新封面")
self.allow_cover_update_checkbox.setObjectName("allowCoverUpdateCheckbox")
self.allow_cover_update_checkbox.setVisible(False)
self.max_items_per_run_spin = QSpinBox()
self.max_items_per_run_spin.setObjectName("maxItemsPerRunSpin")
self.max_items_per_run_spin.setRange(1, 9999)
@@ -294,7 +295,6 @@ class SettingsTab(QWidget):
("每批最大更新条数", self.max_items_per_run_spin),
("", self.allow_real_submit_checkbox),
("", self.close_success_tab_checkbox),
("", self.allow_cover_update_checkbox),
("", self.parallel_accounts_group, 2),
]
)
@@ -519,7 +519,6 @@ class SettingsTab(QWidget):
self.cmhub_check_balance_checkbox,
self.enabled_checkbox,
self.allow_real_submit_checkbox,
self.allow_cover_update_checkbox,
self.close_success_tab_checkbox,
self.parallel_accounts_checkbox,
)
@@ -780,6 +779,14 @@ class SettingsTab(QWidget):
for key, value in self.config.items()
if key not in {"config_path", "ai_models_path", "cmhub_config_path", "data_dir"}
}
try:
current_update_cfg = appconfig.shopee_update_config(appconfig.load_config(self.config_path))
except Exception:
current_update_cfg = self._shopee_update_config()
update_mode = appconfig.normalize_update_mode(
current_update_cfg.get("update_mode"),
allow_cover_update=current_update_cfg.get("allow_cover_update", False),
)
settings.update(
{
"chrome_path": self.chrome_path_edit.text().strip(),
@@ -796,7 +803,8 @@ class SettingsTab(QWidget):
"shopee_update": {
"test_item_id": str(self._compat_test_item_id or ""),
"allow_real_submit": self.allow_real_submit_checkbox.isChecked(),
"allow_cover_update": self.allow_cover_update_checkbox.isChecked(),
"allow_cover_update": appconfig.update_mode_includes_cover(update_mode),
"update_mode": update_mode,
"max_items_per_run": self.max_items_per_run_spin.value(),
"close_success_tab": self.close_success_tab_checkbox.isChecked(),
"dry_run": False,
@@ -912,9 +920,7 @@ class SettingsTab(QWidget):
self.allow_real_submit_checkbox.setChecked(
bool(update_cfg.get("allow_real_submit", False))
)
self.allow_cover_update_checkbox.setChecked(
bool(update_cfg.get("allow_cover_update", False))
)
self.allow_cover_update_checkbox.setChecked(bool(update_cfg.get("allow_cover_update", False)))
self.max_items_per_run_spin.setValue(
max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
)
@@ -931,13 +937,7 @@ class SettingsTab(QWidget):
self._on_backend_changed()
def _shopee_update_config(self):
defaults = appconfig.default_config().get("shopee_update", {})
loaded = self.config.get("shopee_update", {})
if not isinstance(loaded, dict):
loaded = {}
merged = dict(defaults)
merged.update(loaded)
return merged
return appconfig.shopee_update_config(self.config)
def _populate_role_model_combos(self):
ai_cfg = appconfig.ai_config(self.config)
+54 -10
View File
@@ -14,6 +14,24 @@ _USER_LOG_PATH_RE = re.compile(
)
def _generation_mode_label(mode):
mode = appconfig.normalize_generate_mode(mode)
return {
"title": "只生成标题",
"cover": "只生成封面",
"title_cover": "生成标题和封面",
}.get(mode, "只生成标题")
def _update_mode_label(mode):
mode = appconfig.normalize_update_mode(mode)
return {
"title": "只更新标题",
"cover": "只更新封面",
"title_cover": "更新标题和封面",
}.get(mode, "只更新标题")
class GenerateWorker(BaseWorker):
"""Generate titles and covers for eligible collected or failed generation tasks."""
@@ -53,14 +71,15 @@ class GenerateWorker(BaseWorker):
}
self._account_by_alias = account_by_alias
ai_cfg = appconfig.ai_config(self.config)
generate_cover = bool(ai_cfg.get("generate_cover", False))
generate_mode = appconfig.ai_generate_mode(self.config)
generate_cover = appconfig.generate_mode_includes_cover(generate_mode)
eligible = [
task for task in self.tasks
if ai.is_generatable_task(task, generate_cover=generate_cover)
if ai.is_generatable_task(task, generate_mode=generate_mode)
]
component_totals = ai.generation_component_totals(
eligible,
generate_cover=generate_cover,
generate_mode=generate_mode,
)
self._eligible_total = len(eligible)
self._task_positions = {
@@ -69,11 +88,13 @@ class GenerateWorker(BaseWorker):
}
batch_ids = self._batch_ids(eligible)
self._run_id = self._create_run_log(eligible, batch_ids)
mode_text = _generation_mode_label(generate_mode)
if generate_cover:
if appconfig.ai_backend(self.config) == "cmhub":
concurrency = ai.cmhub_image_concurrency_plan(ai_cfg)
start_message = "[开始] 本轮生成 {total} 条:标题{title_total},图片{cover_total};标题并发{title_concurrency},图片并发{image_concurrency},cmhub实际生图并发{request_concurrency},下载并发{download_concurrency};开始时间 {started_at}".format(
start_message = "[开始] 本轮生成 {total} 条:本轮生成内容:{mode_text};标题{title_total},图片{cover_total};标题并发{title_concurrency},图片并发{image_concurrency},cmhub实际生图并发{request_concurrency},下载并发{download_concurrency};开始时间 {started_at}".format(
total=len(eligible),
mode_text=mode_text,
title_total=component_totals["title_total"],
cover_total=component_totals["cover_total"],
title_concurrency=ai_cfg.get("title_concurrency", 1),
@@ -83,8 +104,9 @@ class GenerateWorker(BaseWorker):
started_at=self._run_started_at_text,
)
else:
start_message = "[开始] 本轮生成 {total} 条:标题{title_total},图片{cover_total};标题并发{title_concurrency},图片并发{image_concurrency};开始时间 {started_at}".format(
start_message = "[开始] 本轮生成 {total} 条:本轮生成内容:{mode_text};标题{title_total},图片{cover_total};标题并发{title_concurrency},图片并发{image_concurrency};开始时间 {started_at}".format(
total=len(eligible),
mode_text=mode_text,
title_total=component_totals["title_total"],
cover_total=component_totals["cover_total"],
title_concurrency=ai_cfg.get("title_concurrency", 1),
@@ -92,8 +114,9 @@ class GenerateWorker(BaseWorker):
started_at=self._run_started_at_text,
)
else:
start_message = "[开始] 本轮生成 {total} 条:本轮仅生成标题,不生成图片;标题{title_total};标题并发{title_concurrency};开始时间 {started_at}".format(
start_message = "[开始] 本轮生成 {total} 条:本轮生成内容:{mode_text};标题{title_total};标题并发{title_concurrency};开始时间 {started_at}".format(
total=len(eligible),
mode_text=mode_text,
title_total=component_totals["title_total"],
title_concurrency=ai_cfg.get("title_concurrency", 1),
started_at=self._run_started_at_text,
@@ -112,6 +135,7 @@ class GenerateWorker(BaseWorker):
"on_event": self._on_generation_event,
"on_error": self._on_generation_error,
"generate_cover": generate_cover,
"generate_mode": generate_mode,
},
on_progress=self._emit_generate_progress,
should_stop=self._should_stop_generation,
@@ -130,6 +154,7 @@ class GenerateWorker(BaseWorker):
"failed": len(eligible),
"cancelled": self.should_cancel(),
"generate_cover": generate_cover,
"generate_mode": generate_mode,
}
user_error = self._user_log_detail(error) or "未知错误"
self._log_run_event(
@@ -550,6 +575,7 @@ class ApplyWorker(BaseWorker):
preflight=True,
close_success_tab=False,
dry_run=False,
update_mode=None,
parallel_accounts=False,
max_parallel_accounts=1,
batch_size=None,
@@ -562,6 +588,10 @@ class ApplyWorker(BaseWorker):
self.preflight = preflight
self.close_success_tab = close_success_tab
self.dry_run = bool(dry_run)
self.update_mode = appconfig.normalize_update_mode(
update_mode,
allow_cover_update=appconfig.shopee_update_config(config).get("allow_cover_update", False),
)
self.parallel_accounts = bool(parallel_accounts)
self.max_parallel_accounts = max(1, int(max_parallel_accounts or 1))
self.batch_size = None if batch_size is None else max(1, int(batch_size or 1))
@@ -595,8 +625,9 @@ class ApplyWorker(BaseWorker):
}
self._run_id = self._create_run_log(eligible, batch_ids)
self._log_run_event(
"step=start result=start detail=运行开始:{mode},任务{total},每批最多{batch_size},批次{batch_count},{parallel}".format(
"step=start result=start detail=运行开始:{mode},更新内容{update_mode},任务{total},每批最多{batch_size},批次{batch_count},{parallel}".format(
mode="检查本轮更新" if self.dry_run else "真实更新",
update_mode=_update_mode_label(self.update_mode),
total=total,
batch_size=batch_size,
batch_count=len(batches),
@@ -662,7 +693,16 @@ class ApplyWorker(BaseWorker):
return (
getattr(task, "stage", None) == "generated"
and getattr(task, "status", None) in {"success", "pending", "failed"}
and bool(getattr(task, "new_title", None) or getattr(task, "new_cover_path", None))
and (
(
appconfig.update_mode_includes_title(self.update_mode)
and bool(getattr(task, "new_title", None))
)
or (
appconfig.update_mode_includes_cover(self.update_mode)
and bool(getattr(task, "new_cover_path", None))
)
)
)
def _preflight_block(self, eligible, account_rows, account_by_alias):
@@ -823,9 +863,9 @@ class ApplyWorker(BaseWorker):
)
return "skipped"
action_parts = []
if getattr(task, "new_title", None):
if appconfig.update_mode_includes_title(self.update_mode) and getattr(task, "new_title", None):
action_parts.append("标题")
if getattr(task, "new_cover_path", None):
if appconfig.update_mode_includes_cover(self.update_mode) and getattr(task, "new_cover_path", None):
action_parts.append("封面")
action_text = "+".join(action_parts) or "无变更"
self._log_run_event(
@@ -899,6 +939,7 @@ class ApplyWorker(BaseWorker):
close_success_tab=self.close_success_tab,
on_step=on_step,
bring_to_front=bring_to_front,
update_mode=self.update_mode,
)
committed = bool(result.get("committed")) and not result.get("error")
error = result.get("error")
@@ -1027,6 +1068,7 @@ class ApplyWorker(BaseWorker):
"dry_run": self.dry_run,
"batch_size": self._current_batch_size,
"batch_count": self._batch_count,
"update_mode": self.update_mode,
}
)
@@ -1064,6 +1106,7 @@ class ApplyWorker(BaseWorker):
"parallel_accounts": self.parallel_accounts,
"batch_size": self._current_batch_size,
"batch_count": self._batch_count,
"update_mode": self.update_mode,
"run_id": self._run_id,
}
if blocked:
@@ -1086,6 +1129,7 @@ class ApplyWorker(BaseWorker):
"max_parallel_accounts": self.max_parallel_accounts,
"batch_size": self._current_batch_size,
"batch_count": self._batch_count,
"update_mode": self.update_mode,
},
path=self.db_path,
)