refactor: split gui into package
This commit is contained in:
@@ -0,0 +1,800 @@
|
||||
"""Tab 5: settings UI."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from ..widgets import *
|
||||
from ..workers import AIModelTestWorker as _RealAIModelTestWorker
|
||||
|
||||
|
||||
def AIModelTestWorker(*args, **kwargs):
|
||||
return _call_package_attr("AIModelTestWorker", _RealAIModelTestWorker, *args, **kwargs)
|
||||
|
||||
class SettingsTab(QWidget):
|
||||
"""Tab 5: AI model definitions stored in config/ai_models.json."""
|
||||
|
||||
CATEGORY_ITEMS = [("文本", "text"), ("图像", "image")]
|
||||
API_TYPE_ITEMS = [("chat", "chat"), ("images_edits", "images_edits"), ("auto", "auto")]
|
||||
RESOLUTION_ITEMS = ["512", "1k", "2k", "4k"]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
parent=None,
|
||||
config=None,
|
||||
config_path=None,
|
||||
ai_models_path=None,
|
||||
status_callback=None,
|
||||
):
|
||||
super().__init__(parent)
|
||||
self.config = appconfig.load_config() if config is None else config
|
||||
self.config_path = (
|
||||
config_path
|
||||
or self.config.get("config_path")
|
||||
or appconfig.CONFIG_PATH
|
||||
)
|
||||
self.ai_models_path = (
|
||||
ai_models_path
|
||||
or self.config.get("ai_models_path")
|
||||
or appconfig.AI_MODELS_PATH
|
||||
)
|
||||
self.status_callback = status_callback
|
||||
self.models = []
|
||||
self.current_model_name = None
|
||||
self.test_worker = None
|
||||
self.test_thread = None
|
||||
self._compat_test_item_id = ""
|
||||
|
||||
self.model_combo = QComboBox()
|
||||
self.model_combo.setObjectName("aiModelCombo")
|
||||
self.add_model_button = QPushButton("新增")
|
||||
self.delete_model_button = QPushButton("删除")
|
||||
|
||||
self.enabled_checkbox = QCheckBox("启用")
|
||||
self.name_edit = QLineEdit()
|
||||
self.name_edit.setObjectName("modelNameEdit")
|
||||
self.category_combo = QComboBox()
|
||||
self.category_combo.setObjectName("modelCategoryCombo")
|
||||
for label, value in self.CATEGORY_ITEMS:
|
||||
self.category_combo.addItem(label, value)
|
||||
self.api_type_combo = QComboBox()
|
||||
self.api_type_combo.setObjectName("modelApiTypeCombo")
|
||||
for label, value in self.API_TYPE_ITEMS:
|
||||
self.api_type_combo.addItem(label, value)
|
||||
self.model_id_edit = QLineEdit()
|
||||
self.model_id_edit.setObjectName("modelIdEdit")
|
||||
self.url_edit = QLineEdit()
|
||||
self.url_edit.setObjectName("modelUrlEdit")
|
||||
self.api_key_edit = QLineEdit()
|
||||
self.api_key_edit.setObjectName("modelApiKeyEdit")
|
||||
self.api_key_edit.setEchoMode(QLineEdit.Password)
|
||||
self.connect_timeout_spin = QSpinBox()
|
||||
self.connect_timeout_spin.setObjectName("connectTimeoutSpin")
|
||||
self.connect_timeout_spin.setRange(1, 3600)
|
||||
self.connect_timeout_spin.setValue(30)
|
||||
self.save_model_button = QPushButton("保存")
|
||||
self.test_connection_button = QPushButton("测试连接")
|
||||
self.test_result_label = QLabel("")
|
||||
self.test_result_label.setWordWrap(True)
|
||||
self.default_text_model_combo = QComboBox()
|
||||
self.default_text_model_combo.setObjectName("defaultTextModelCombo")
|
||||
self.default_image_model_combo = QComboBox()
|
||||
self.default_image_model_combo.setObjectName("defaultImageModelCombo")
|
||||
self.title_concurrency_spin = QSpinBox()
|
||||
self.title_concurrency_spin.setObjectName("titleConcurrencySpin")
|
||||
self.title_concurrency_spin.setRange(1, 64)
|
||||
self.image_concurrency_spin = QSpinBox()
|
||||
self.image_concurrency_spin.setObjectName("imageConcurrencySpin")
|
||||
self.image_concurrency_spin.setRange(1, 64)
|
||||
self.retry_spin = QSpinBox()
|
||||
self.retry_spin.setObjectName("retrySpin")
|
||||
self.retry_spin.setRange(0, 20)
|
||||
self.resolution_combo = QComboBox()
|
||||
self.resolution_combo.setObjectName("resolutionCombo")
|
||||
for resolution in self.RESOLUTION_ITEMS:
|
||||
self.resolution_combo.addItem(resolution, resolution)
|
||||
self.response_timeout_label = QLabel("")
|
||||
self.jpg_quality_spin = QSpinBox()
|
||||
self.jpg_quality_spin.setObjectName("jpgQualitySpin")
|
||||
self.jpg_quality_spin.setRange(1, 100)
|
||||
self.chrome_path_edit = QLineEdit()
|
||||
self.chrome_path_edit.setObjectName("chromePathEdit")
|
||||
self.user_data_root_edit = QLineEdit()
|
||||
self.user_data_root_edit.setObjectName("userDataRootEdit")
|
||||
self.image_dir_edit = QLineEdit()
|
||||
self.image_dir_edit.setObjectName("imageDirEdit")
|
||||
self.db_path_edit = QLineEdit()
|
||||
self.db_path_edit.setObjectName("dbPathEdit")
|
||||
self.default_debug_port_spin = QSpinBox()
|
||||
self.default_debug_port_spin.setObjectName("defaultDebugPortSpin")
|
||||
self.default_debug_port_spin.setRange(1, 65535)
|
||||
self.debug_port_start_spin = QSpinBox()
|
||||
self.debug_port_start_spin.setObjectName("debugPortStartSpin")
|
||||
self.debug_port_start_spin.setRange(1, 65535)
|
||||
self.debug_port_end_spin = QSpinBox()
|
||||
self.debug_port_end_spin.setObjectName("debugPortEndSpin")
|
||||
self.debug_port_end_spin.setRange(1, 65535)
|
||||
self.cdp_ready_timeout_spin = QSpinBox()
|
||||
self.cdp_ready_timeout_spin.setObjectName("cdpReadyTimeoutSpin")
|
||||
self.cdp_ready_timeout_spin.setRange(1, 3600)
|
||||
self.save_config_button = QPushButton("保存设置")
|
||||
self.allow_real_submit_checkbox = QCheckBox("允许真实提交线上商品")
|
||||
self.allow_real_submit_checkbox.setObjectName("allowRealSubmitCheckbox")
|
||||
self.allow_cover_update_checkbox = QCheckBox("允许更新封面")
|
||||
self.allow_cover_update_checkbox.setObjectName("allowCoverUpdateCheckbox")
|
||||
self.max_items_per_run_spin = QSpinBox()
|
||||
self.max_items_per_run_spin.setObjectName("maxItemsPerRunSpin")
|
||||
self.max_items_per_run_spin.setRange(1, 9999)
|
||||
self.max_items_per_run_spin.setToolTip("作为每批最大更新条数;正式更新会分批处理当前筛选全部可更新记录。")
|
||||
self.close_success_tab_checkbox = QCheckBox("成功后关闭本次新开编辑页")
|
||||
self.close_success_tab_checkbox.setObjectName("closeSuccessTabCheckbox")
|
||||
self.parallel_accounts_checkbox = QCheckBox("多账号并行更新")
|
||||
self.parallel_accounts_checkbox.setObjectName("parallelAccountsCheckbox")
|
||||
self.max_parallel_accounts_spin = QSpinBox()
|
||||
self.max_parallel_accounts_spin.setObjectName("maxParallelAccountsSpin")
|
||||
self.max_parallel_accounts_spin.setRange(1, 16)
|
||||
self.max_parallel_accounts_label = QLabel("最大并行账号数")
|
||||
self.parallel_accounts_group = QWidget()
|
||||
self.parallel_accounts_group.setObjectName("parallelAccountsGroup")
|
||||
parallel_accounts_layout = QHBoxLayout(self.parallel_accounts_group)
|
||||
parallel_accounts_layout.setContentsMargins(0, 0, 0, 0)
|
||||
parallel_accounts_layout.setSpacing(12)
|
||||
parallel_accounts_layout.addWidget(self.parallel_accounts_checkbox)
|
||||
parallel_accounts_layout.addWidget(self.max_parallel_accounts_label)
|
||||
parallel_accounts_layout.addWidget(self.max_parallel_accounts_spin)
|
||||
parallel_accounts_layout.addStretch(1)
|
||||
|
||||
model_picker_layout = QHBoxLayout()
|
||||
model_picker_layout.addWidget(self.model_combo, 1)
|
||||
model_picker_layout.addWidget(self.add_model_button)
|
||||
model_picker_layout.addWidget(self.delete_model_button)
|
||||
|
||||
action_layout = QHBoxLayout()
|
||||
action_layout.addWidget(self.save_model_button)
|
||||
action_layout.addWidget(self.test_connection_button)
|
||||
action_layout.addStretch(1)
|
||||
|
||||
form = self._three_column_form(
|
||||
[
|
||||
("状态", self.enabled_checkbox),
|
||||
("服务商名", self.name_edit),
|
||||
("类别", self.category_combo),
|
||||
("api_type", self.api_type_combo),
|
||||
("模型ID", self.model_id_edit),
|
||||
("连接超时(秒)", self.connect_timeout_spin),
|
||||
("网址", self.url_edit, True),
|
||||
("密钥", self.api_key_edit, True),
|
||||
]
|
||||
)
|
||||
|
||||
ai_form = self._three_column_form(
|
||||
[
|
||||
("标题大模型", self.default_text_model_combo),
|
||||
("图片大模型", self.default_image_model_combo),
|
||||
("标题并发数", self.title_concurrency_spin),
|
||||
("图片并发数", self.image_concurrency_spin),
|
||||
("失败重试次数", self.retry_spin),
|
||||
("分辨率", self.resolution_combo),
|
||||
("返回超时", self.response_timeout_label),
|
||||
("jpg质量", self.jpg_quality_spin),
|
||||
]
|
||||
)
|
||||
|
||||
port_range_layout = QHBoxLayout()
|
||||
port_range_layout.setContentsMargins(0, 0, 0, 0)
|
||||
port_range_layout.addWidget(self.debug_port_start_spin)
|
||||
port_range_layout.addWidget(QLabel("到"))
|
||||
port_range_layout.addWidget(self.debug_port_end_spin)
|
||||
port_range_widget = QWidget()
|
||||
port_range_widget.setLayout(port_range_layout)
|
||||
|
||||
path_form = self._three_column_form(
|
||||
[
|
||||
("Chrome路径", self.chrome_path_edit, True),
|
||||
("账号数据根目录", self.user_data_root_edit),
|
||||
("图片目录", self.image_dir_edit),
|
||||
("DB路径", self.db_path_edit),
|
||||
("默认调试端口", self.default_debug_port_spin),
|
||||
("调试端口范围", port_range_widget),
|
||||
("CDP就绪超时(秒)", self.cdp_ready_timeout_spin),
|
||||
]
|
||||
)
|
||||
|
||||
self.shopee_update_form_layout = self._three_column_form(
|
||||
[
|
||||
("每批最大更新条数", 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),
|
||||
]
|
||||
)
|
||||
|
||||
panel = QWidget()
|
||||
panel.setMaximumWidth(1800)
|
||||
panel_layout = QVBoxLayout(panel)
|
||||
self.settings_panel_layout = panel_layout
|
||||
panel_layout.setContentsMargins(13, 18, 13, 18)
|
||||
self.ai_model_section_title = self._section_title(
|
||||
"AI 模型",
|
||||
"settingsAiModelSectionTitle",
|
||||
)
|
||||
self.model_detail_section_title = self._section_title(
|
||||
"模型详情",
|
||||
"settingsModelDetailSectionTitle",
|
||||
)
|
||||
self.generation_section_title = self._section_title(
|
||||
"角色与生成参数",
|
||||
"settingsGenerationSectionTitle",
|
||||
)
|
||||
self.shopee_update_section_title = self._section_title(
|
||||
"Shopee 更新安全 / 执行模式",
|
||||
"settingsShopeeUpdateSectionTitle",
|
||||
)
|
||||
self.infrastructure_section_title = self._section_title(
|
||||
"基础设施(路径与端口)",
|
||||
"settingsInfrastructureSectionTitle",
|
||||
)
|
||||
panel_layout.addWidget(self.ai_model_section_title)
|
||||
panel_layout.addLayout(model_picker_layout)
|
||||
panel_layout.addSpacing(14)
|
||||
panel_layout.addWidget(self.model_detail_section_title)
|
||||
panel_layout.addLayout(form)
|
||||
panel_layout.addLayout(action_layout)
|
||||
panel_layout.addWidget(self.test_result_label)
|
||||
panel_layout.addSpacing(18)
|
||||
panel_layout.addWidget(self.generation_section_title)
|
||||
panel_layout.addLayout(ai_form)
|
||||
panel_layout.addSpacing(18)
|
||||
panel_layout.addWidget(self.shopee_update_section_title)
|
||||
panel_layout.addLayout(self.shopee_update_form_layout)
|
||||
panel_layout.addSpacing(18)
|
||||
panel_layout.addWidget(self.infrastructure_section_title)
|
||||
panel_layout.addLayout(path_form)
|
||||
panel_layout.addWidget(self.save_config_button)
|
||||
panel_layout.addStretch(1)
|
||||
|
||||
scroll = QScrollArea()
|
||||
scroll.setWidgetResizable(True)
|
||||
scroll_content = QWidget()
|
||||
scroll_layout = QHBoxLayout(scroll_content)
|
||||
scroll_layout.setContentsMargins(0, 0, 0, 0)
|
||||
scroll_layout.addStretch(1)
|
||||
scroll_layout.addWidget(panel)
|
||||
scroll_layout.addStretch(1)
|
||||
scroll.setWidget(scroll_content)
|
||||
|
||||
layout = QVBoxLayout(self)
|
||||
layout.setContentsMargins(18, 18, 18, 18)
|
||||
layout.addWidget(scroll, 1)
|
||||
|
||||
self.model_combo.currentIndexChanged.connect(self.load_selected_model)
|
||||
self.add_model_button.clicked.connect(self.add_model)
|
||||
self.delete_model_button.clicked.connect(self.delete_model)
|
||||
self.save_model_button.clicked.connect(self.save_model)
|
||||
self.test_connection_button.clicked.connect(self.test_connection)
|
||||
self.resolution_combo.currentIndexChanged.connect(
|
||||
self._update_response_timeout_label
|
||||
)
|
||||
self.save_config_button.clicked.connect(self.save_app_settings)
|
||||
|
||||
self.refresh_models()
|
||||
self._populate_app_settings()
|
||||
|
||||
def _three_column_form(self, fields):
|
||||
layout = QGridLayout()
|
||||
layout.setHorizontalSpacing(18)
|
||||
layout.setVerticalSpacing(8)
|
||||
for column in (1, 3, 5):
|
||||
layout.setColumnStretch(column, 1)
|
||||
row = 0
|
||||
column_pair = 0
|
||||
for field in fields:
|
||||
label = field[0]
|
||||
widget = field[1]
|
||||
span_pairs = self._form_field_span_pairs(field)
|
||||
if span_pairs > 3 - column_pair:
|
||||
row += 1
|
||||
column_pair = 0
|
||||
column = column_pair * 2
|
||||
self._add_form_field(layout, row, column, label, widget, span_pairs)
|
||||
column_pair += span_pairs
|
||||
if column_pair >= 3:
|
||||
row += 1
|
||||
column_pair = 0
|
||||
return layout
|
||||
|
||||
def _form_field_span_pairs(self, field):
|
||||
if len(field) <= 2:
|
||||
return 1
|
||||
span = field[2]
|
||||
if isinstance(span, bool):
|
||||
return 3 if span else 1
|
||||
return max(1, min(3, int(span or 1)))
|
||||
|
||||
def _add_form_field(self, layout, row, column, label, widget, span_pairs):
|
||||
if label:
|
||||
layout.addWidget(QLabel(label), row, column)
|
||||
layout.addWidget(widget, row, column + 1, 1, span_pairs * 2 - 1)
|
||||
else:
|
||||
layout.addWidget(widget, row, column, 1, span_pairs * 2)
|
||||
|
||||
def _section_title(self, text, object_name):
|
||||
label = QLabel(text)
|
||||
label.setObjectName(object_name)
|
||||
label.setStyleSheet("color: #24292f; font-weight: 600; padding-top: 4px;")
|
||||
return label
|
||||
|
||||
def _set_status(self, message):
|
||||
if self.status_callback is not None:
|
||||
self.status_callback(message)
|
||||
|
||||
def refresh_models(self, selected=None):
|
||||
try:
|
||||
self.models = appconfig.list_ai_models(
|
||||
path=self.ai_models_path,
|
||||
reveal_api_key=True,
|
||||
)
|
||||
except Exception as exc:
|
||||
self.models = []
|
||||
self.current_model_name = None
|
||||
self._show_error(exc)
|
||||
|
||||
current = selected or self.current_model_name
|
||||
self.model_combo.blockSignals(True)
|
||||
self.model_combo.clear()
|
||||
for model in self.models:
|
||||
label = f"{model['name']} · {self._category_label(model['category'])}"
|
||||
if not model.get("enabled", True):
|
||||
label += " · 已停用"
|
||||
self.model_combo.addItem(label, model["name"])
|
||||
index = self.model_combo.findData(current)
|
||||
self.model_combo.setCurrentIndex(index if index >= 0 else (0 if self.models else -1))
|
||||
self.model_combo.blockSignals(False)
|
||||
self.load_selected_model()
|
||||
if hasattr(self, "default_text_model_combo"):
|
||||
self._populate_role_model_combos()
|
||||
|
||||
def load_selected_model(self, index=None):
|
||||
name = self.model_combo.currentData()
|
||||
model = self._model_by_name(name)
|
||||
self.current_model_name = model["name"] if model else None
|
||||
self._populate_form(model)
|
||||
self._update_button_state()
|
||||
|
||||
def add_model(self, checked=False):
|
||||
name = self._unique_model_name("新文本模型")
|
||||
model = {
|
||||
"name": name,
|
||||
"category": "text",
|
||||
"enabled": True,
|
||||
"url": "",
|
||||
"model": "",
|
||||
"api_key": "",
|
||||
"api_type": "chat",
|
||||
"connect_timeout_seconds": 30,
|
||||
"timeout_seconds": 0,
|
||||
"extra_body": {},
|
||||
}
|
||||
try:
|
||||
appconfig.add_ai_model(model, path=self.ai_models_path)
|
||||
except Exception as exc:
|
||||
self._show_error(exc)
|
||||
return
|
||||
self.refresh_models(selected=name)
|
||||
self._set_status(f"AI 模型已新增:{name}")
|
||||
|
||||
def save_model(self, checked=False):
|
||||
model = self._form_values()
|
||||
if model is None:
|
||||
return
|
||||
current = self._current_model()
|
||||
if self._should_warn_plaintext_api_key(model, current):
|
||||
self._show_plaintext_api_key_warning()
|
||||
try:
|
||||
if self.current_model_name is None:
|
||||
appconfig.add_ai_model(model, path=self.ai_models_path)
|
||||
else:
|
||||
appconfig.update_ai_model(
|
||||
self.current_model_name,
|
||||
path=self.ai_models_path,
|
||||
**model,
|
||||
)
|
||||
except Exception as exc:
|
||||
self._show_error(exc)
|
||||
return
|
||||
self.refresh_models(selected=model["name"])
|
||||
self._set_status(f"AI 模型已保存:{model['name']}")
|
||||
|
||||
def delete_model(self, checked=False):
|
||||
model = self._current_model()
|
||||
if model is None:
|
||||
return
|
||||
if not self._can_delete_model(model):
|
||||
self._set_status("每个类别至少保留一个模型,当前模型不能删除")
|
||||
return
|
||||
answer = QMessageBox.question(
|
||||
self,
|
||||
"删除 AI 模型",
|
||||
f"确认删除模型「{model['name']}」?",
|
||||
QMessageBox.Yes | QMessageBox.No,
|
||||
QMessageBox.No,
|
||||
)
|
||||
if answer != QMessageBox.Yes:
|
||||
return
|
||||
try:
|
||||
appconfig.delete_ai_model(model["name"], path=self.ai_models_path)
|
||||
except Exception as exc:
|
||||
self._show_error(exc)
|
||||
return
|
||||
self.refresh_models()
|
||||
self._set_status(f"AI 模型已删除:{model['name']}")
|
||||
|
||||
def test_connection(self, checked=False):
|
||||
if self.test_thread is not None:
|
||||
self._set_status("模型连接测试正在进行...")
|
||||
return
|
||||
model = self._current_model()
|
||||
if model is None:
|
||||
return
|
||||
if self.name_edit.text().strip() != model["name"]:
|
||||
self._set_status("请先保存模型名称变更后再测试连接")
|
||||
return
|
||||
worker = AIModelTestWorker(
|
||||
model["name"],
|
||||
ai_models_path=self.ai_models_path,
|
||||
db_path=_database_path(config=self.config),
|
||||
diagnostic_log_dir=diagnostics.DEFAULT_LOG_DIR,
|
||||
)
|
||||
worker.finished.connect(self._on_test_finished)
|
||||
worker.failed.connect(self._on_test_failed)
|
||||
thread = run_worker(worker, thread_name="AIModelTestWorker", start=False)
|
||||
thread.finished.connect(lambda: self._forget_test_thread(thread))
|
||||
self.test_worker = worker
|
||||
self.test_thread = thread
|
||||
self._set_test_running(True)
|
||||
self.test_result_label.setText("正在测试连接...")
|
||||
self._set_status(f"正在测试 AI 模型连接:{model['name']}")
|
||||
thread.start()
|
||||
|
||||
def save_app_settings(self, checked=False):
|
||||
settings = self._app_settings_values()
|
||||
if settings is None:
|
||||
return
|
||||
try:
|
||||
saved = appconfig.save_config(settings, path=self.config_path)
|
||||
except Exception as exc:
|
||||
self._show_error(exc)
|
||||
return
|
||||
self._replace_config(saved)
|
||||
self._populate_app_settings()
|
||||
self._set_status("设置已保存")
|
||||
QMessageBox.information(self, "保存设置", "设置已保存")
|
||||
|
||||
def _app_settings_values(self):
|
||||
start_port = self.debug_port_start_spin.value()
|
||||
end_port = self.debug_port_end_spin.value()
|
||||
default_port = self.default_debug_port_spin.value()
|
||||
if start_port > end_port:
|
||||
self._show_error("调试端口范围起始值不能大于结束值")
|
||||
return None
|
||||
if not (start_port <= default_port <= end_port):
|
||||
self._show_error("默认调试端口必须在调试端口范围内")
|
||||
return None
|
||||
text_model = self.default_text_model_combo.currentData()
|
||||
image_model = self.default_image_model_combo.currentData()
|
||||
if not text_model or not image_model:
|
||||
self._show_error("标题大模型和图片大模型不能为空")
|
||||
return None
|
||||
|
||||
ai_cfg = appconfig.ai_config(self.config)
|
||||
ai_cfg.update(
|
||||
{
|
||||
"default_text_model": text_model,
|
||||
"default_image_model": image_model,
|
||||
"title_concurrency": self.title_concurrency_spin.value(),
|
||||
"image_concurrency": self.image_concurrency_spin.value(),
|
||||
"retry": self.retry_spin.value(),
|
||||
"jpg_quality": self.jpg_quality_spin.value(),
|
||||
"resolution": self.resolution_combo.currentData() or "1k",
|
||||
"resolution_timeouts": dict(ai_cfg.get("resolution_timeouts", {})),
|
||||
}
|
||||
)
|
||||
|
||||
settings = {
|
||||
key: value
|
||||
for key, value in self.config.items()
|
||||
if key not in {"config_path", "ai_models_path"}
|
||||
}
|
||||
settings.update(
|
||||
{
|
||||
"chrome_path": self.chrome_path_edit.text().strip(),
|
||||
"user_data_root": self.user_data_root_edit.text().strip(),
|
||||
"image_dir": self.image_dir_edit.text().strip(),
|
||||
"db_path": self.db_path_edit.text().strip(),
|
||||
"default_debug_port": default_port,
|
||||
"debug_port_range": [start_port, end_port],
|
||||
"cdp_ready_timeout": self.cdp_ready_timeout_spin.value(),
|
||||
"ai": ai_cfg,
|
||||
"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(),
|
||||
"max_items_per_run": self.max_items_per_run_spin.value(),
|
||||
"close_success_tab": self.close_success_tab_checkbox.isChecked(),
|
||||
"dry_run": False,
|
||||
"parallel_accounts": self.parallel_accounts_checkbox.isChecked(),
|
||||
"max_parallel_accounts": self.max_parallel_accounts_spin.value(),
|
||||
},
|
||||
}
|
||||
)
|
||||
return settings
|
||||
|
||||
def _replace_config(self, saved):
|
||||
internal = {}
|
||||
if self.config_path != appconfig.CONFIG_PATH:
|
||||
internal["config_path"] = self.config_path
|
||||
if self.ai_models_path != appconfig.AI_MODELS_PATH:
|
||||
internal["ai_models_path"] = self.ai_models_path
|
||||
self.config.clear()
|
||||
self.config.update(saved)
|
||||
self.config.update(internal)
|
||||
|
||||
def _populate_app_settings(self):
|
||||
self._populate_role_model_combos()
|
||||
ai_cfg = appconfig.ai_config(self.config)
|
||||
self._set_combo_by_data(
|
||||
self.default_text_model_combo,
|
||||
ai_cfg.get("default_text_model", ""),
|
||||
)
|
||||
self._set_combo_by_data(
|
||||
self.default_image_model_combo,
|
||||
ai_cfg.get("default_image_model", ""),
|
||||
)
|
||||
self.title_concurrency_spin.setValue(
|
||||
int(ai_cfg.get("title_concurrency", 4) or 4)
|
||||
)
|
||||
self.image_concurrency_spin.setValue(
|
||||
int(ai_cfg.get("image_concurrency", 4) or 4)
|
||||
)
|
||||
self.retry_spin.setValue(int(ai_cfg.get("retry", 2) or 0))
|
||||
self._set_combo_by_data(
|
||||
self.resolution_combo,
|
||||
str(ai_cfg.get("resolution", "1k")),
|
||||
)
|
||||
self.jpg_quality_spin.setValue(int(ai_cfg.get("jpg_quality", 90) or 90))
|
||||
self.chrome_path_edit.setText(appconfig.chrome_path(self.config))
|
||||
self.user_data_root_edit.setText(appconfig.user_data_root(self.config))
|
||||
self.image_dir_edit.setText(appconfig.image_dir(self.config))
|
||||
self.db_path_edit.setText(appconfig.db_path(self.config))
|
||||
self.default_debug_port_spin.setValue(
|
||||
int(appconfig.default_debug_port(self.config))
|
||||
)
|
||||
start_port, end_port = appconfig.debug_port_range(self.config)
|
||||
self.debug_port_start_spin.setValue(int(start_port))
|
||||
self.debug_port_end_spin.setValue(int(end_port))
|
||||
self.cdp_ready_timeout_spin.setValue(
|
||||
int(appconfig.cdp_ready_timeout(self.config))
|
||||
)
|
||||
update_cfg = self._shopee_update_config()
|
||||
self._compat_test_item_id = str(update_cfg.get("test_item_id", ""))
|
||||
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.max_items_per_run_spin.setValue(
|
||||
max(1, int(update_cfg.get("max_items_per_run", 1) or 1))
|
||||
)
|
||||
self.close_success_tab_checkbox.setChecked(
|
||||
bool(update_cfg.get("close_success_tab", False))
|
||||
)
|
||||
self.parallel_accounts_checkbox.setChecked(
|
||||
bool(update_cfg.get("parallel_accounts", False))
|
||||
)
|
||||
self.max_parallel_accounts_spin.setValue(
|
||||
max(1, int(update_cfg.get("max_parallel_accounts", 2) or 2))
|
||||
)
|
||||
self._update_response_timeout_label()
|
||||
|
||||
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
|
||||
|
||||
def _populate_role_model_combos(self):
|
||||
ai_cfg = appconfig.ai_config(self.config)
|
||||
self._populate_role_combo(
|
||||
self.default_text_model_combo,
|
||||
"text",
|
||||
ai_cfg.get("default_text_model"),
|
||||
)
|
||||
self._populate_role_combo(
|
||||
self.default_image_model_combo,
|
||||
"image",
|
||||
ai_cfg.get("default_image_model"),
|
||||
)
|
||||
|
||||
def _populate_role_combo(self, combo, category, selected):
|
||||
combo.blockSignals(True)
|
||||
combo.clear()
|
||||
for model in self.models:
|
||||
if model.get("category") == category and model.get("enabled", True):
|
||||
combo.addItem(model.get("name", ""), model.get("name", ""))
|
||||
if combo.count() == 0:
|
||||
combo.addItem("无可用模型", None)
|
||||
index = combo.findData(selected)
|
||||
combo.setCurrentIndex(index if index >= 0 else 0)
|
||||
combo.blockSignals(False)
|
||||
|
||||
def _update_response_timeout_label(self, index=None):
|
||||
ai_cfg = appconfig.ai_config(self.config)
|
||||
resolution = self.resolution_combo.currentData() or ai_cfg.get("resolution", "1k")
|
||||
timeouts = ai_cfg.get("resolution_timeouts", {})
|
||||
timeout = timeouts.get(str(resolution))
|
||||
if timeout is None:
|
||||
self.response_timeout_label.setText("未配置")
|
||||
return
|
||||
self.response_timeout_label.setText(f"{int(timeout)} 秒")
|
||||
|
||||
def _form_values(self):
|
||||
current = self._current_model() or {}
|
||||
name = self.name_edit.text().strip()
|
||||
if not name:
|
||||
self._show_error("AI 模型服务商名不能为空")
|
||||
return None
|
||||
extra_body = current.get("extra_body", {})
|
||||
if not isinstance(extra_body, dict):
|
||||
extra_body = {}
|
||||
return {
|
||||
"name": name,
|
||||
"category": self.category_combo.currentData() or "text",
|
||||
"enabled": self.enabled_checkbox.isChecked(),
|
||||
"url": self.url_edit.text().strip(),
|
||||
"model": self.model_id_edit.text().strip(),
|
||||
"api_key": self.api_key_edit.text(),
|
||||
"api_type": self.api_type_combo.currentData() or "auto",
|
||||
"connect_timeout_seconds": self.connect_timeout_spin.value(),
|
||||
"timeout_seconds": int(current.get("timeout_seconds", 0) or 0),
|
||||
"extra_body": dict(extra_body),
|
||||
}
|
||||
|
||||
def _populate_form(self, model):
|
||||
widgets = [
|
||||
self.enabled_checkbox,
|
||||
self.name_edit,
|
||||
self.category_combo,
|
||||
self.api_type_combo,
|
||||
self.model_id_edit,
|
||||
self.url_edit,
|
||||
self.api_key_edit,
|
||||
self.connect_timeout_spin,
|
||||
]
|
||||
for widget in widgets:
|
||||
widget.blockSignals(True)
|
||||
if model is None:
|
||||
self.enabled_checkbox.setChecked(False)
|
||||
self.name_edit.clear()
|
||||
self.category_combo.setCurrentIndex(0)
|
||||
self.api_type_combo.setCurrentIndex(0)
|
||||
self.model_id_edit.clear()
|
||||
self.url_edit.clear()
|
||||
self.api_key_edit.clear()
|
||||
self.connect_timeout_spin.setValue(30)
|
||||
else:
|
||||
self.enabled_checkbox.setChecked(bool(model.get("enabled", True)))
|
||||
self.name_edit.setText(model.get("name", ""))
|
||||
self._set_combo_by_data(self.category_combo, model.get("category", "text"))
|
||||
self._set_combo_by_data(self.api_type_combo, model.get("api_type", "auto"))
|
||||
self.model_id_edit.setText(model.get("model", ""))
|
||||
self.url_edit.setText(model.get("url", ""))
|
||||
self.api_key_edit.setText(model.get("api_key", ""))
|
||||
self.connect_timeout_spin.setValue(
|
||||
int(model.get("connect_timeout_seconds", 30) or 30)
|
||||
)
|
||||
for widget in widgets:
|
||||
widget.blockSignals(False)
|
||||
|
||||
def _set_combo_by_data(self, combo, value):
|
||||
index = combo.findData(value)
|
||||
combo.setCurrentIndex(index if index >= 0 else 0)
|
||||
|
||||
def _update_button_state(self):
|
||||
has_model = self._current_model() is not None
|
||||
testing = self.test_thread is not None
|
||||
for widget in (
|
||||
self.enabled_checkbox,
|
||||
self.name_edit,
|
||||
self.category_combo,
|
||||
self.api_type_combo,
|
||||
self.model_id_edit,
|
||||
self.url_edit,
|
||||
self.api_key_edit,
|
||||
self.connect_timeout_spin,
|
||||
self.save_model_button,
|
||||
):
|
||||
widget.setEnabled(has_model and not testing)
|
||||
self.add_model_button.setEnabled(not testing)
|
||||
self.delete_model_button.setEnabled(
|
||||
has_model and not testing and self._can_delete_model(self._current_model())
|
||||
)
|
||||
self.test_connection_button.setEnabled(has_model and not testing)
|
||||
|
||||
def _set_test_running(self, running):
|
||||
self._update_button_state()
|
||||
self.test_connection_button.setEnabled(
|
||||
not running and self._current_model() is not None
|
||||
)
|
||||
|
||||
def _forget_test_thread(self, thread):
|
||||
if self.test_thread is thread:
|
||||
self.test_thread = None
|
||||
self.test_worker = None
|
||||
self._set_test_running(False)
|
||||
|
||||
def _on_test_finished(self, payload):
|
||||
if payload.get("ok"):
|
||||
status = payload.get("status")
|
||||
suffix = f"(HTTP {status})" if status else ""
|
||||
message = f"测试连接成功:{payload.get('name')}{suffix}"
|
||||
else:
|
||||
error = payload.get("error") or "连接失败"
|
||||
status = payload.get("status")
|
||||
status_text = f"HTTP {status}," if status else ""
|
||||
message = f"测试连接失败:{status_text}{error}"
|
||||
self.test_result_label.setText(message)
|
||||
self._set_status(message)
|
||||
|
||||
def _on_test_failed(self, _task_id, error):
|
||||
message = f"测试连接失败:{error}"
|
||||
self.test_result_label.setText(message)
|
||||
self._set_status(message)
|
||||
|
||||
def _show_error(self, error):
|
||||
message = str(error)
|
||||
QMessageBox.warning(self, "设置", message)
|
||||
self._set_status(message)
|
||||
|
||||
def _should_warn_plaintext_api_key(self, model, current):
|
||||
new_key = str((model or {}).get("api_key") or "")
|
||||
current_key = str((current or {}).get("api_key") or "")
|
||||
return bool(new_key) and new_key != current_key
|
||||
|
||||
def _show_plaintext_api_key_warning(self):
|
||||
QMessageBox.warning(
|
||||
self,
|
||||
PLAINTEXT_SECRET_TITLE,
|
||||
PLAINTEXT_API_KEY_WARNING,
|
||||
)
|
||||
|
||||
def _current_model(self):
|
||||
return self._model_by_name(self.current_model_name)
|
||||
|
||||
def _model_by_name(self, name):
|
||||
for model in self.models:
|
||||
if model.get("name") == name:
|
||||
return model
|
||||
return None
|
||||
|
||||
def _unique_model_name(self, base):
|
||||
names = {model.get("name") for model in self.models}
|
||||
if base not in names:
|
||||
return base
|
||||
counter = 2
|
||||
while f"{base} {counter}" in names:
|
||||
counter += 1
|
||||
return f"{base} {counter}"
|
||||
|
||||
def _can_delete_model(self, model):
|
||||
if model is None:
|
||||
return False
|
||||
category = model.get("category")
|
||||
return sum(1 for item in self.models if item.get("category") == category) > 1
|
||||
|
||||
def _category_label(self, category):
|
||||
return {"text": "文本", "image": "图像"}.get(category, category)
|
||||
|
||||
|
||||
Reference in New Issue
Block a user