feat(settings): support gateway source switching

This commit is contained in:
chengma
2026-07-20 16:29:28 +08:00
parent ed1d8ee764
commit 6a0cd1c763
18 changed files with 746 additions and 77 deletions
+89 -24
View File
@@ -5,6 +5,8 @@ from __future__ import annotations
from contextlib import contextmanager
import os
from PySide6.QtCore import Signal
from ... import ai as ai_module
from ... import chrome
from ... import cmhub_models
@@ -14,7 +16,7 @@ from ..workers import CMHubSettingsWorker as _RealCMHubSettingsWorker
PLAINTEXT_CMHUB_API_KEY_WARNING = (
"cmhub API Key 会以本地明文保存到 data/config/cmhub.json,仅供本机调用 cmhub 网关使用。"
"默认网关 API Key 会以本地明文保存到 data/config/cmhub.json,仅供本机调用默认网关使用。"
"该文件已 gitignore,UI 打码显示,日志/导出不记录明文。"
)
@@ -29,7 +31,9 @@ def CMHubSettingsWorker(*args, **kwargs):
class SettingsTab(QWidget):
"""Tab 5: AI model definitions stored in data/config/ai_models.json."""
BACKEND_ITEMS = [("直连模型", "direct"), ("cmhub 网关", "cmhub")]
settingsSaved = Signal(str)
BACKEND_ITEMS = [("默认网关", "cmhub"), ("自定义网关", "direct")]
CATEGORY_ITEMS = [("文本", "text"), ("图像", "image")]
API_TYPE_ITEMS = [("chat", "chat"), ("images_edits", "images_edits"), ("auto", "auto")]
RESOLUTION_ITEMS = ["512", "1k", "2k", "4k"]
@@ -77,13 +81,37 @@ class SettingsTab(QWidget):
for label, value in self.BACKEND_ITEMS:
self.backend_combo.addItem(label, value)
self.backend_combo.setVisible(False)
self.gateway_default_button = QPushButton("默认网关")
self.gateway_default_button.setObjectName("gatewayDefaultButton")
self.gateway_default_button.setCheckable(True)
self.gateway_custom_button = QPushButton("自定义网关")
self.gateway_custom_button.setObjectName("gatewayCustomButton")
self.gateway_custom_button.setCheckable(True)
self.gateway_button_group = QButtonGroup(self)
self.gateway_button_group.setExclusive(True)
self.gateway_button_group.addButton(self.gateway_default_button)
self.gateway_button_group.addButton(self.gateway_custom_button)
self.gateway_selector = QWidget()
self.gateway_selector.setObjectName("gatewaySourceSelector")
gateway_selector_layout = QHBoxLayout(self.gateway_selector)
gateway_selector_layout.setContentsMargins(0, 0, 0, 0)
gateway_selector_layout.setSpacing(0)
gateway_selector_layout.addWidget(self.gateway_default_button)
gateway_selector_layout.addWidget(self.gateway_custom_button)
self.gateway_selector.setStyleSheet(
"QPushButton { min-width: 108px; padding: 6px 12px; border: 1px solid #b8c0ca; "
"background: #f6f8fa; color: #57606a; }"
"QPushButton:first-child { border-top-left-radius: 4px; border-bottom-left-radius: 4px; }"
"QPushButton:last-child { border-left: 0; border-top-right-radius: 4px; border-bottom-right-radius: 4px; }"
"QPushButton:checked { background: #0969da; border-color: #0969da; color: white; font-weight: 600; }"
)
self.cmhub_base_url_edit = QLineEdit()
self.cmhub_base_url_edit.setObjectName("cmhubBaseUrlEdit")
self.cmhub_base_url_edit.setPlaceholderText("https://host(不要带 /api 或 /api/v1)")
self.cmhub_api_key_edit = QLineEdit()
self.cmhub_api_key_edit.setObjectName("cmhubApiKeyEdit")
self.cmhub_api_key_edit.setEchoMode(QLineEdit.Password)
self.cmhub_api_key_edit.setPlaceholderText("从 cmhub 网页端复制 API Key")
self.cmhub_api_key_edit.setPlaceholderText("从默认网关网页端复制 API Key")
self.cmhub_title_alias_combo = QComboBox()
self.cmhub_title_alias_combo.setObjectName("cmhubTitleAliasCombo")
self.cmhub_image_alias_combo = QComboBox()
@@ -103,10 +131,10 @@ class SettingsTab(QWidget):
self.cmhub_result_label = QLabel("")
self.cmhub_result_label.setObjectName("cmhubResultLabel")
self.cmhub_result_label.setWordWrap(True)
self.cmhub_base_url_hint_label = QLabel("Base URL 只填网关根,如 https://host;不要带 /api 或 /api/v1。")
self.cmhub_base_url_hint_label = QLabel("Base URL 只填默认网关根,如 https://host;不要带 /api 或 /api/v1。")
self.cmhub_base_url_hint_label.setObjectName("cmhubBaseUrlHintLabel")
self.cmhub_base_url_hint_label.setWordWrap(True)
self.cmhub_key_hint_label = QLabel("API Key 仅在 cmhub 网页端创建时显示一次;复制到此处后会本地明文保存并打码显示。")
self.cmhub_key_hint_label = QLabel("API Key 仅在默认网关网页端创建时显示一次;复制到此处后会本地明文保存并打码显示。")
self.cmhub_key_hint_label.setObjectName("cmhubKeyHintLabel")
self.cmhub_key_hint_label.setWordWrap(True)
@@ -235,7 +263,7 @@ class SettingsTab(QWidget):
("状态", self.enabled_checkbox),
("服务商名", self.name_edit),
("类别", self.category_combo),
("api_type", self.api_type_combo),
("接口类型", self.api_type_combo),
("模型ID", self.model_id_edit),
("连接超时(秒)", self.connect_timeout_spin),
("网址", self.url_edit, True),
@@ -296,8 +324,15 @@ class SettingsTab(QWidget):
model_detail_layout.addLayout(action_layout)
model_detail_layout.addWidget(self.test_result_label)
self.direct_gateway_notice = QLabel("图片理解与商品套图仅支持默认网关。")
self.direct_gateway_notice.setObjectName("directGatewayNoticeLabel")
self.direct_gateway_notice.setStyleSheet("color: #6b7280;")
self.direct_role_panel = QWidget()
self.direct_role_panel.setLayout(direct_role_form)
direct_role_layout = QVBoxLayout(self.direct_role_panel)
direct_role_layout.setContentsMargins(0, 0, 0, 0)
direct_role_layout.setSpacing(8)
direct_role_layout.addLayout(direct_role_form)
direct_role_layout.addWidget(self.direct_gateway_notice)
cmhub_form = self._three_column_form(
[
@@ -332,7 +367,7 @@ class SettingsTab(QWidget):
self.settings_panel_layout = panel_layout
panel_layout.setContentsMargins(13, 18, 13, 18)
self.ai_model_section_title = self._section_title(
"cmhub 网关",
"生成网关",
"settingsAiModelSectionTitle",
)
self.model_detail_section_title = self._section_title(
@@ -352,6 +387,8 @@ class SettingsTab(QWidget):
"settingsInfrastructureSectionTitle",
)
panel_layout.addWidget(self.ai_model_section_title)
panel_layout.addWidget(self.gateway_selector)
panel_layout.addSpacing(8)
panel_layout.addWidget(self.model_picker_panel)
panel_layout.addSpacing(14)
panel_layout.addWidget(self.model_detail_section_title)
@@ -394,7 +431,8 @@ class SettingsTab(QWidget):
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.backend_combo.currentIndexChanged.connect(self._on_backend_changed)
self.gateway_default_button.toggled.connect(self._on_gateway_source_toggled)
self.gateway_custom_button.toggled.connect(self._on_gateway_source_toggled)
self.cmhub_refresh_button.clicked.connect(self.refresh_cmhub_models)
self.cmhub_test_button.clicked.connect(self.test_cmhub_connection)
self.resolution_combo.currentIndexChanged.connect(
@@ -480,7 +518,6 @@ class SettingsTab(QWidget):
self.chrome_path_edit,
)
combos = (
self.backend_combo,
self.cmhub_title_alias_combo,
self.cmhub_image_alias_combo,
self.cmhub_vision_alias_combo,
@@ -516,6 +553,9 @@ class SettingsTab(QWidget):
for widget in checkboxes:
widget.toggled.connect(self._mark_dirty)
self.gateway_default_button.toggled.connect(self._mark_dirty)
self.gateway_custom_button.toggled.connect(self._mark_dirty)
def _mark_dirty(self, *args):
if self._suspend_dirty > 0:
return
@@ -576,13 +616,36 @@ class SettingsTab(QWidget):
self._cmhub_auto_refresh_done = True
self.refresh_cmhub_models()
def _selected_backend(self):
return "direct" if self.gateway_custom_button.isChecked() else "cmhub"
def _set_selected_backend(self, backend):
value = "direct" if str(backend or "").strip().lower() == "direct" else "cmhub"
target = self.gateway_custom_button if value == "direct" else self.gateway_default_button
for button in (self.gateway_default_button, self.gateway_custom_button):
previous = button.blockSignals(True)
button.setChecked(button is target)
button.blockSignals(previous)
previous = self.backend_combo.blockSignals(True)
self._set_combo_by_data(self.backend_combo, value)
self.backend_combo.blockSignals(previous)
def _on_gateway_source_toggled(self, checked):
if checked:
self._on_backend_changed()
def _on_backend_changed(self, index=None):
backend = self._selected_backend()
previous = self.backend_combo.blockSignals(True)
self._set_combo_by_data(self.backend_combo, backend)
self.backend_combo.blockSignals(previous)
self.backend_combo.setVisible(False)
self.model_picker_panel.setVisible(False)
self.model_detail_section_title.setVisible(False)
self.model_detail_panel.setVisible(False)
self.direct_role_panel.setVisible(False)
self.cmhub_panel.setVisible(True)
is_direct = backend == "direct"
self.model_picker_panel.setVisible(is_direct)
self.model_detail_section_title.setVisible(is_direct)
self.model_detail_panel.setVisible(is_direct)
self.direct_role_panel.setVisible(is_direct)
self.cmhub_panel.setVisible(not is_direct)
self._set_cmhub_running(self.cmhub_thread is not None)
self._update_button_state()
@@ -733,8 +796,10 @@ class SettingsTab(QWidget):
self._replace_config(saved)
self._populate_app_settings()
self._set_dirty(False)
self._set_status("设置已保存")
backend_label = "自定义网关" if self._selected_backend() == "direct" else "默认网关"
self._set_status("设置已保存,当前使用%s" % backend_label)
QMessageBox.information(self, "保存设置", "设置已保存")
self.settingsSaved.emit(self._selected_backend())
return True
def _app_settings_values(self):
@@ -748,7 +813,7 @@ class SettingsTab(QWidget):
self._show_error("默认调试端口必须在调试端口范围内")
return None
ai_cfg = appconfig.ai_config(self.config)
backend = "cmhub"
backend = self._selected_backend()
text_model = self.default_text_model_combo.currentData() or ai_cfg.get("default_text_model")
image_model = self.default_image_model_combo.currentData() or ai_cfg.get("default_image_model")
cmhub_cfg = self._cmhub_settings_values(backend)
@@ -847,7 +912,7 @@ class SettingsTab(QWidget):
return
self._populate_role_model_combos()
ai_cfg = appconfig.ai_config(self.config)
self._set_combo_by_data(self.backend_combo, "cmhub")
self._set_selected_backend(ai_cfg.get("backend", "cmhub"))
cmhub_cfg = appconfig.cmhub_config(self.config)
self.cmhub_base_url_edit.setText(appconfig.normalize_cmhub_base_url(cmhub_cfg.get("base_url", "")))
self._loaded_cmhub_api_key = appconfig.get_cmhub_api_key(path=self.cmhub_config_path)
@@ -1092,7 +1157,7 @@ class SettingsTab(QWidget):
def _start_cmhub_worker(self, include_balance):
if self.cmhub_thread is not None:
self._set_status("cmhub 检测正在进行...")
self._set_status("默认网关检测正在进行...")
return
base_url = appconfig.normalize_cmhub_base_url(self.cmhub_base_url_edit.text())
if base_url != self.cmhub_base_url_edit.text().strip():
@@ -1104,7 +1169,7 @@ class SettingsTab(QWidget):
if not api_key:
missing.append("API Key")
if missing:
self._show_error("cmhub 配置不完整:缺少 " + "、".join(missing))
self._show_error("默认网关配置不完整:缺少 " + "、".join(missing))
return
worker = CMHubSettingsWorker(
base_url,
@@ -1121,7 +1186,7 @@ class SettingsTab(QWidget):
self.cmhub_worker = worker
self.cmhub_thread = thread
self._set_cmhub_running(True)
message = "正在测试 cmhub 连接并查询余额..." if include_balance else "正在刷新 cmhub 别名..."
message = "正在测试默认网关连接并查询余额..." if include_balance else "正在刷新默认网关别名..."
self.cmhub_result_label.setText(message)
self._set_status(message)
thread.start()
@@ -1183,8 +1248,8 @@ class SettingsTab(QWidget):
def _cmhub_success_subject(self, payload):
account_name = self._cmhub_account_display_name(payload)
if account_name:
return f"cmhub 账号「{account_name}」连接成功"
return "cmhub 连接成功"
return f"账号「{account_name}」连接默认网关成功"
return "默认网关连接成功"
def _cmhub_account_display_name(self, payload):
if not isinstance(payload, dict):
@@ -1237,7 +1302,7 @@ class SettingsTab(QWidget):
return ""
def _on_cmhub_failed(self, _task_id, error):
message = f"cmhub 连接失败:{error}"
message = "默认网关连接失败:%s" % str(error or "连接失败").replace("cmhub", "默认网关")
self.cmhub_result_label.setText(message)
self._set_status(message)