Files

1 line
55 KiB
JSON

{"repo": "48645970/guanlan", "files": {"guanlan/core/services/ai/config.py": {"b151fe89745321bee119133651f3eba4f32d4c2e": {"t": 1785745986.1994107, "n": 7399, "h": "3f5300278cc20a00", "c": "# -*- coding: utf-8 -*-\n\"\"\"\n观澜量化 - AI 服务配置管理\n\nAuthor: 海山观澜\n\"\"\"\n\nimport copy\nimport json\nfrom pathlib import Path\n\nfrom guanlan.core.services.ai.models import ModelConfig, ConfigError\nfrom guanlan.core.utils.logger import get_logger\n\n\nlogger = get_logger(\"ai_config\")\n\n\n# 默认配置模板\nDEFAULT_CONFIG = {\n \"default_model\": \"deepseek-chat\",\n \"models\": {\n \"deepseek-chat\": {\n \"api_base\": \"https://api.deepseek.com/v1\",\n \"api_key\": \"\",\n \"model\": \"deepseek-chat\",\n \"max_tokens\": 4096,\n \"temperature\": 0.7,\n \"supports_vision\": False\n },\n \"deepseek-reasoner\": {\n \"api_base\": \"https://api.deepseek.com/v1\",\n \"api_key\": \"\",\n \"model\": \"deepseek-reasoner\",\n \"max_tokens\": 8192,\n \"temperature\": 0.7,\n \"supports_vision\": False\n },\n \"glm-4v-flash\": {\n \"api_base\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"api_key\": \"\",\n \"model\": \"glm-4v-flash\",\n \"max_tokens\": 4096,\n \"temperature\": 0.7,\n \"supports_vision\": True\n },\n \"glm-4.7-flash\": {\n \"api_base\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"api_key\": \"\",\n \"model\": \"glm-4.7-flash\",\n \"max_tokens\": 4096,\n \"temperature\": 0.7,\n \"supports_vision\": False\n },\n \"glm-4.6v-flash\": {\n \"api_base\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"api_key\": \"\",\n \"model\": \"glm-4.6v-flash\",\n \"max_tokens\": 4096,\n \"temperature\": 0.7,\n \"supports_vision\": True\n }\n }\n}\n\n\ndef _get_default_config_path() -> Path:\n \"\"\"获取默认配置文件路径\"\"\"\n from guanlan.core.constants import CONFIG_DIR\n return CONFIG_DIR / \"config\" / \"ai.json\"\n\n\nclass AIConfig:\n \"\"\"\n AI 服务配置管理\n\n 负责加载、保存、验证配置文件。\n\n Examples\n --------\n >>> # 使用默认路径 (~/.vntrader/ai_service_setting.json)\n >>> config = AIConfig()\n >>>\n >>> # 指定配置文件路径\n >>> config = AIConfig(\"config/ai.json\")\n >>>\n >>> config.list_models()\n ['deepseek-chat', 'deepseek-reasoner', 'glm-4v-flash']\n \"\"\"\n\n def __init__(self, config_path: str | Path | None = None):\n \"\"\"\n 初始化配置管理器\n\n Parameters\n ----------\n config_path : str | Path | None\n 配置文件路径,None 则使用默认路径\n \"\"\"\n if config_path is None:\n self._filepath = _get_default_config_path()\n else:\n self._filepath = Path(config_path)\n\n self._data: dict = {}\n self._load()\n\n def _load(self) -> None:\n \"\"\"加载配置文件\"\"\"\n if self._filepath.exists():\n try:\n with open(self._filepath, encoding=\"utf-8\") as f:\n self._data = json.load(f)\n logger.info(f\"配置加载成功: {self._filepath}\")\n except Exception as e:\n logger.error(f\"配置加载失败: {e}\")\n self._data = copy.deepcopy(DEFAULT_CONFIG)\n else:\n # 创建默认配置\n self._data = copy.deepcopy(DEFAULT_CONFIG)\n self.save()\n logger.info(f\"已创建默认配置: {self._filepath}\")\n\n def save(self) -> None:\n \"\"\"保存配置到文件\"\"\"\n try:\n with open(self._filepath, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._data, f, indent=2, ensure_ascii=False)\n logger.info(f\"配置保存成功: {self._filepath}\")\n except Exception as e:\n logger.error(f\"配置保存失败: {e}\")\n raise ConfigError(f\"配置保存失败: {e}\")\n\n @property\n def default_model(self) -> str:\n \"\"\"获取默认模型名称\"\"\"\n return self._data.get(\"default_model\", \"deepseek-chat\")\n\n @default_model.setter\n def default_model(self, model_name: str) -> None:\n \"\"\"设置默认模型\"\"\"\n if model_name not in self.list_models():\n raise ConfigError(f\"模型不存在: {model_name}\")\n self._data[\"default_model\"] = model_name\n\n def list_models(self) -> list[str]:\n \"\"\"列出所有模型名称\"\"\"\n return list(self._data.get(\"models\", {}).keys())\n\n def list_vision_models(self) -> list[str]:\n \"\"\"列出支持图片的模型\"\"\"\n models = []\n for name, cfg in self._data.get(\"models\", {}).items():\n if cfg.get(\"supports_vision\", False):\n models.append(name)\n return models\n\n def get_model_config(self, model_name: str) -> ModelConfig:\n \"\"\"\n 获取指定模型配置\n\n Parameters\n ----------\n model_name : str\n 模型名称\n\n Returns\n -------\n ModelConfig\n 模型配置对象\n\n Raises\n ------\n ConfigError\n 模型不存在时抛出\n \"\"\"\n models = self._data.get(\"models\", {})\n if model_name not in models:\n raise ConfigError(f\"模型不存在: {model_name}\")\n return ModelConfig.from_dict(models[model_name])\n\n def add_model(self, name: str, config: ModelConfig) -> None:\n \"\"\"\n 添加模型配置\n\n Parameters\n ----------\n name : str\n 模型名称\n config : ModelConfig\n 模型配置\n \"\"\"\n if \"models\" not in self._data:\n self._data[\"models\"] = {}\n self._data[\"models\"][name] = config.to_dict()\n logger.info(f\"已添加模型: {name}\")\n\n def update_model(self, name: str, **kwargs) -> None:\n \"\"\"\n 更新模型配置\n\n Parameters\n ----------\n name : str\n 模型名称\n **kwargs\n 要更新的配置项\n \"\"\"\n models = self._data.get(\"models\", {})\n if name not in models:\n raise ConfigError(f\"模型不存在: {name}\")\n\n for key, value in kwargs.items():\n if key in models[name]:\n models[name][key] = value\n logger.info(f\"已更新模型配置: {name}\")\n\n def remove_model(self, name: str) -> None:\n \"\"\"\n 移除模型配置\n\n Parameters\n ----------\n name : str\n 模型名称\n \"\"\"\n models = self._data.get(\"models\", {})\n if name in models:\n del models[name]\n logger.info(f\"已移除模型: {name}\")\n\n def validate(self) -> list[str]:\n \"\"\"\n 验证配置完整性\n\n Returns\n -------\n list[str]\n 缺少 API Key 的模型列表\n \"\"\"\n missing = []\n for name, cfg in self._data.get(\"models\", {}).items():\n if not cfg.get(\"api_key\"):\n missing.append(name)\n return missing\n\n\n# 全局配置实例\n_config: AIConfig | None = None\n_config_path: str | Path | None = None\n\n\ndef get_config(config_path: str | Path | None = None) -> AIConfig:\n \"\"\"\n 获取配置实例\n\n Parameters\n ----------\n config_path : str | Path | None\n 配置文件路径,首次调用时指定,后续调用可省略\n\n Returns\n -------\n AIConfig\n 配置管理器实例\n\n Examples\n --------\n >>> # 使用自定义路径\n >>> config = get_config(\"config/ai.json\")\n >>>\n >>> # 后续调用自动复用\n >>> config = get_config()\n \"\"\"\n global _config, _config_path\n\n # 如果指定了新路径,重新加载\n if config_path is not None and config_path != _config_path:\n _config_path = config_path\n _config = AIConfig(config_path)\n\n # 首次调用或需要创建\n if _config is None:\n _config = AIConfig(_config_path)\n\n return _config\n\n\ndef reset_config() -> None:\n \"\"\"重置全局配置(用于测试或切换配置文件)\"\"\"\n global _config, _config_path\n _config = None\n _config_path = None\n\n\n__all__ = [\n \"AIConfig\",\n \"get_config\",\n \"reset_config\",\n \"DEFAULT_CONFIG\",\n]\n"}, "acd28c3a8c8191d69c1a9f0bfb5d94878a3e8103": {"t": 1785758539.7174692, "n": 7399, "h": "3f5300278cc20a00", "c": "# -*- coding: utf-8 -*-\n\"\"\"\n观澜量化 - AI 服务配置管理\n\nAuthor: 海山观澜\n\"\"\"\n\nimport copy\nimport json\nfrom pathlib import Path\n\nfrom guanlan.core.services.ai.models import ModelConfig, ConfigError\nfrom guanlan.core.utils.logger import get_logger\n\n\nlogger = get_logger(\"ai_config\")\n\n\n# 默认配置模板\nDEFAULT_CONFIG = {\n \"default_model\": \"deepseek-chat\",\n \"models\": {\n \"deepseek-chat\": {\n \"api_base\": \"https://api.deepseek.com/v1\",\n \"api_key\": \"\",\n \"model\": \"deepseek-chat\",\n \"max_tokens\": 4096,\n \"temperature\": 0.7,\n \"supports_vision\": False\n },\n \"deepseek-reasoner\": {\n \"api_base\": \"https://api.deepseek.com/v1\",\n \"api_key\": \"\",\n \"model\": \"deepseek-reasoner\",\n \"max_tokens\": 8192,\n \"temperature\": 0.7,\n \"supports_vision\": False\n },\n \"glm-4v-flash\": {\n \"api_base\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"api_key\": \"\",\n \"model\": \"glm-4v-flash\",\n \"max_tokens\": 4096,\n \"temperature\": 0.7,\n \"supports_vision\": True\n },\n \"glm-4.7-flash\": {\n \"api_base\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"api_key\": \"\",\n \"model\": \"glm-4.7-flash\",\n \"max_tokens\": 4096,\n \"temperature\": 0.7,\n \"supports_vision\": False\n },\n \"glm-4.6v-flash\": {\n \"api_base\": \"https://open.bigmodel.cn/api/paas/v4\",\n \"api_key\": \"\",\n \"model\": \"glm-4.6v-flash\",\n \"max_tokens\": 4096,\n \"temperature\": 0.7,\n \"supports_vision\": True\n }\n }\n}\n\n\ndef _get_default_config_path() -> Path:\n \"\"\"获取默认配置文件路径\"\"\"\n from guanlan.core.constants import CONFIG_DIR\n return CONFIG_DIR / \"config\" / \"ai.json\"\n\n\nclass AIConfig:\n \"\"\"\n AI 服务配置管理\n\n 负责加载、保存、验证配置文件。\n\n Examples\n --------\n >>> # 使用默认路径 (~/.vntrader/ai_service_setting.json)\n >>> config = AIConfig()\n >>>\n >>> # 指定配置文件路径\n >>> config = AIConfig(\"config/ai.json\")\n >>>\n >>> config.list_models()\n ['deepseek-chat', 'deepseek-reasoner', 'glm-4v-flash']\n \"\"\"\n\n def __init__(self, config_path: str | Path | None = None):\n \"\"\"\n 初始化配置管理器\n\n Parameters\n ----------\n config_path : str | Path | None\n 配置文件路径,None 则使用默认路径\n \"\"\"\n if config_path is None:\n self._filepath = _get_default_config_path()\n else:\n self._filepath = Path(config_path)\n\n self._data: dict = {}\n self._load()\n\n def _load(self) -> None:\n \"\"\"加载配置文件\"\"\"\n if self._filepath.exists():\n try:\n with open(self._filepath, encoding=\"utf-8\") as f:\n self._data = json.load(f)\n logger.info(f\"配置加载成功: {self._filepath}\")\n except Exception as e:\n logger.error(f\"配置加载失败: {e}\")\n self._data = copy.deepcopy(DEFAULT_CONFIG)\n else:\n # 创建默认配置\n self._data = copy.deepcopy(DEFAULT_CONFIG)\n self.save()\n logger.info(f\"已创建默认配置: {self._filepath}\")\n\n def save(self) -> None:\n \"\"\"保存配置到文件\"\"\"\n try:\n with open(self._filepath, \"w\", encoding=\"utf-8\") as f:\n json.dump(self._data, f, indent=2, ensure_ascii=False)\n logger.info(f\"配置保存成功: {self._filepath}\")\n except Exception as e:\n logger.error(f\"配置保存失败: {e}\")\n raise ConfigError(f\"配置保存失败: {e}\")\n\n @property\n def default_model(self) -> str:\n \"\"\"获取默认模型名称\"\"\"\n return self._data.get(\"default_model\", \"deepseek-chat\")\n\n @default_model.setter\n def default_model(self, model_name: str) -> None:\n \"\"\"设置默认模型\"\"\"\n if model_name not in self.list_models():\n raise ConfigError(f\"模型不存在: {model_name}\")\n self._data[\"default_model\"] = model_name\n\n def list_models(self) -> list[str]:\n \"\"\"列出所有模型名称\"\"\"\n return list(self._data.get(\"models\", {}).keys())\n\n def list_vision_models(self) -> list[str]:\n \"\"\"列出支持图片的模型\"\"\"\n models = []\n for name, cfg in self._data.get(\"models\", {}).items():\n if cfg.get(\"supports_vision\", False):\n models.append(name)\n return models\n\n def get_model_config(self, model_name: str) -> ModelConfig:\n \"\"\"\n 获取指定模型配置\n\n Parameters\n ----------\n model_name : str\n 模型名称\n\n Returns\n -------\n ModelConfig\n 模型配置对象\n\n Raises\n ------\n ConfigError\n 模型不存在时抛出\n \"\"\"\n models = self._data.get(\"models\", {})\n if model_name not in models:\n raise ConfigError(f\"模型不存在: {model_name}\")\n return ModelConfig.from_dict(models[model_name])\n\n def add_model(self, name: str, config: ModelConfig) -> None:\n \"\"\"\n 添加模型配置\n\n Parameters\n ----------\n name : str\n 模型名称\n config : ModelConfig\n 模型配置\n \"\"\"\n if \"models\" not in self._data:\n self._data[\"models\"] = {}\n self._data[\"models\"][name] = config.to_dict()\n logger.info(f\"已添加模型: {name}\")\n\n def update_model(self, name: str, **kwargs) -> None:\n \"\"\"\n 更新模型配置\n\n Parameters\n ----------\n name : str\n 模型名称\n **kwargs\n 要更新的配置项\n \"\"\"\n models = self._data.get(\"models\", {})\n if name not in models:\n raise ConfigError(f\"模型不存在: {name}\")\n\n for key, value in kwargs.items():\n if key in models[name]:\n models[name][key] = value\n logger.info(f\"已更新模型配置: {name}\")\n\n def remove_model(self, name: str) -> None:\n \"\"\"\n 移除模型配置\n\n Parameters\n ----------\n name : str\n 模型名称\n \"\"\"\n models = self._data.get(\"models\", {})\n if name in models:\n del models[name]\n logger.info(f\"已移除模型: {name}\")\n\n def validate(self) -> list[str]:\n \"\"\"\n 验证配置完整性\n\n Returns\n -------\n list[str]\n 缺少 API Key 的模型列表\n \"\"\"\n missing = []\n for name, cfg in self._data.get(\"models\", {}).items():\n if not cfg.get(\"api_key\"):\n missing.append(name)\n return missing\n\n\n# 全局配置实例\n_config: AIConfig | None = None\n_config_path: str | Path | None = None\n\n\ndef get_config(config_path: str | Path | None = None) -> AIConfig:\n \"\"\"\n 获取配置实例\n\n Parameters\n ----------\n config_path : str | Path | None\n 配置文件路径,首次调用时指定,后续调用可省略\n\n Returns\n -------\n AIConfig\n 配置管理器实例\n\n Examples\n --------\n >>> # 使用自定义路径\n >>> config = get_config(\"config/ai.json\")\n >>>\n >>> # 后续调用自动复用\n >>> config = get_config()\n \"\"\"\n global _config, _config_path\n\n # 如果指定了新路径,重新加载\n if config_path is not None and config_path != _config_path:\n _config_path = config_path\n _config = AIConfig(config_path)\n\n # 首次调用或需要创建\n if _config is None:\n _config = AIConfig(_config_path)\n\n return _config\n\n\ndef reset_config() -> None:\n \"\"\"重置全局配置(用于测试或切换配置文件)\"\"\"\n global _config, _config_path\n _config = None\n _config_path = None\n\n\n__all__ = [\n \"AIConfig\",\n \"get_config\",\n \"reset_config\",\n \"DEFAULT_CONFIG\",\n]\n"}}, "guanlan/ui/view/window/ai_settings.py": {"b151fe89745321bee119133651f3eba4f32d4c2e": {"t": 1785746905.399963, "n": 17144, "h": "27a8ab8c19187b8d", "c": "# -*- coding: utf-8 -*-\n\"\"\"\n观澜量化 - AI 配置管理窗口\n\n卡片式布局,支持模型增删改查、设置默认模型。\n\nAuthor: 海山观澜\n\"\"\"\n\nfrom PySide6.QtCore import Qt, QSize, QPropertyAnimation, QEasingCurve\nfrom PySide6.QtGui import QIcon, QTransform\nfrom PySide6.QtWidgets import (\n QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,\n)\n\nfrom qfluentwidgets import (\n SubtitleLabel, StrongBodyLabel, BodyLabel,\n LineEdit, SpinBox, DoubleSpinBox, CheckBox, RadioButton,\n PushButton, PrimaryPushButton, ToolButton, TransparentToolButton,\n CardWidget, MessageBox, MessageBoxBase,\n SmoothScrollArea,\n InfoBar, InfoBarPosition,\n FluentIcon, FluentWidget,\n isDarkTheme, qconfig,\n)\n\nfrom guanlan.ui.common.icon import get_icon_path\nfrom guanlan.ui.common.mixin import CursorFixMixin\nfrom guanlan.ui.common.style import StyleSheet, Theme\nfrom guanlan.core.services.ai import get_config, ModelConfig\n\n\n# 交互控件类型(点击时不触发折叠)\n_INTERACTIVE_TYPES = (\n PushButton, PrimaryPushButton, ToolButton,\n CheckBox, LineEdit, SpinBox, DoubleSpinBox,\n)\n\n\nclass ModelAddDialog(MessageBoxBase):\n \"\"\"添加模型对话框\"\"\"\n\n def __init__(self, existing_names: list[str], parent=None):\n super().__init__(parent)\n\n self.existing_names = existing_names\n self.model_name: str = \"\"\n\n self.titleLabel = SubtitleLabel(\"添加模型\", self)\n self.viewLayout.addWidget(self.titleLabel)\n\n self.name_edit = LineEdit(self)\n self.name_edit.setPlaceholderText(\"输入模型名称,如:deepseek-chat\")\n self.name_edit.setClearButtonEnabled(True)\n self.viewLayout.addWidget(self.name_edit)\n\n self.yesButton.setText(\"添加\")\n self.cancelButton.setText(\"取消\")\n self.widget.setMinimumWidth(400)\n\n def validate(self) -> bool:\n \"\"\"验证输入(基类 MessageBoxBase 在确认时自动调用)\"\"\"\n name = self.name_edit.text().strip()\n if not name:\n InfoBar.error(\n title=\"输入错误\",\n content=\"模型名称不能为空\",\n position=InfoBarPosition.TOP,\n parent=self,\n )\n return False\n if name in self.existing_names:\n InfoBar.error(\n title=\"输入错误\",\n content=f\"模型名称「{name}」已存在\",\n position=InfoBarPosition.TOP,\n parent=self,\n )\n return False\n\n self.model_name = name\n return True\n\n\nclass AIModelCard(CardWidget):\n \"\"\"AI 模型卡片\n\n 显示模型摘要信息,点击展开/收起编辑表单。\n \"\"\"\n\n def __init__(self, model_name: str, is_default: bool, parent=None):\n super().__init__(parent)\n\n self.model_name = model_name\n self._is_default = is_default\n self._expanded = False\n\n self._init_ui()\n self._connect_signals()\n self._load_form_data()\n\n def _init_ui(self) -> None:\n \"\"\"初始化界面\"\"\"\n self.main_layout = QVBoxLayout(self)\n self.main_layout.setContentsMargins(16, 12, 16, 12)\n self.main_layout.setSpacing(0)\n\n # ── 摘要行 ──\n self._init_summary()\n\n # ── 编辑表单(默认隐藏)──\n self._init_form()\n\n def _init_summary(self) -> None:\n \"\"\"初始化摘要行\"\"\"\n summary = QHBoxLayout()\n summary.setSpacing(20)\n\n # 折叠箭头\n self.arrow_btn = ToolButton(FluentIcon.CHEVRON_RIGHT, self)\n self.arrow_btn.setFixedSize(20, 20)\n self.arrow_btn.setIconSize(QSize(10, 10))\n self.arrow_btn.clicked.connect(self._toggle_expand)\n summary.addWidget(self.arrow_btn)\n\n # 模型名称\n self.name_label = StrongBodyLabel(self.model_name, self)\n self.name_label.setMinimumWidth(120)\n summary.addWidget(self.name_label)\n\n summary.addStretch(1)\n\n # 默认模型单选框\n self.default_radio = RadioButton(\"默认模型\", self)\n self.default_radio.setChecked(self._is_default)\n summary.addWidget(self.default_radio)\n\n # 删除按钮\n self.delete_btn = TransparentToolButton(FluentIcon.DELETE, self)\n summary.addWidget(self.delete_btn)\n\n self.main_layout.addLayout(summary)\n\n def _init_form(self) -> None:\n \"\"\"初始化编辑表单(默认隐藏)\"\"\"\n self.form_widget = QWidget(self)\n self.form_widget.setVisible(False)\n self.form_widget.setMaximumHeight(0)\n\n form_layout = QVBoxLayout(self.form_widget)\n form_layout.setContentsMargins(0, 12, 0, 4)\n\n # 表单字段\n form = QFormLayout()\n form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)\n form.setHorizontalSpacing(16)\n form.setVerticalSpacing(10)\n\n # API Base\n self.api_base_edit = LineEdit(self.form_widget)\n self.api_base_edit.setPlaceholderText(\"https://api.deepseek.com/v1\")\n self.api_base_edit.setMinimumWidth(300)\n form.addRow(BodyLabel(\"API Base\", self.form_widget), self.api_base_edit)\n\n # API Key\n self.api_key_edit = LineEdit(self.form_widget)\n self.api_key_edit.setPlaceholderText(\"sk-...\")\n self.api_key_edit.setEchoMode(LineEdit.EchoMode.Password)\n self.api_key_edit.setMinimumWidth(300)\n form.addRow(BodyLabel(\"API Key\", self.form_widget), self.api_key_edit)\n\n # 模型 ID\n self.model_id_edit = LineEdit(self.form_widget)\n self.model_id_edit.setPlaceholderText(\"deepseek-chat\")\n self.model_id_edit.setMinimumWidth(300)\n form.addRow(BodyLabel(\"模型 ID\", self.form_widget), self.model_id_edit)\n\n # Max Tokens\n self.max_tokens_spin = SpinBox(self.form_widget)\n self.max_tokens_spin.setRange(1, 32768)\n self.max_tokens_spin.setValue(4096)\n self.max_tokens_spin.setMinimumWidth(150)\n form.addRow(BodyLabel(\"Max Tokens\", self.form_widget), self.max_tokens_spin)\n\n # Temperature\n self.temperature_spin = DoubleSpinBox(self.form_widget)\n self.temperature_spin.setRange(0.0, 2.0)\n self.temperature_spin.setSingleStep(0.1)\n self.temperature_spin.setValue(0.7)\n self.temperature_spin.setDecimals(1)\n self.temperature_spin.setMinimumWidth(150)\n form.addRow(BodyLabel(\"Temperature\", self.form_widget), self.temperature_spin)\n\n # 支持图片\n self.supports_vision_check = CheckBox(\"支持图片输入\", self.form_widget)\n form.addRow(BodyLabel(\"能力\", self.form_widget), self.supports_vision_check)\n\n form_layout.addLayout(form)\n\n # 保存按钮\n btn_layout = QHBoxLayout()\n btn_layout.addStretch(1)\n self.save_btn = PrimaryPushButton(\"保存\", self.form_widget)\n self.save_btn.setFixedWidth(100)\n btn_layout.addWidget(self.save_btn)\n form_layout.addLayout(btn_layout)\n\n self.main_layout.addWidget(self.form_widget)\n\n def _load_form_data(self) -> None:\n \"\"\"加载模型配置到表单\"\"\"\n try:\n config = get_config()\n model_config = config.get_model_config(self.model_name)\n\n self.api_base_edit.setText(model_config.api_base)\n self.api_key_edit.setText(model_config.api_key)\n self.model_id_edit.setText(model_config.model)\n self.max_tokens_spin.setValue(model_config.max_tokens)\n self.temperature_spin.setValue(model_config.temperature)\n self.supports_vision_check.setChecked(model_config.supports_vision)\n except Exception:\n # 新建模型,使用默认值\n pass\n\n def _collect_form_data(self) -> ModelConfig:\n \"\"\"从表单收集数据\"\"\"\n return ModelConfig(\n api_base=self.api_base_edit.text().strip(),\n api_key=self.api_key_edit.text().strip(),\n model=self.model_id_edit.text().strip(),\n max_tokens=self.max_tokens_spin.value(),\n temperature=self.temperature_spin.value(),\n supports_vision=self.supports_vision_check.isChecked(),\n )\n\n def _connect_signals(self) -> None:\n \"\"\"连接信号\"\"\"\n self.save_btn.clicked.connect(self._on_save)\n\n def _toggle_expand(self) -> None:\n \"\"\"展开/收起编辑表单\"\"\"\n self._expanded = not self._expanded\n\n # 旋转箭头图标\n angle = 90 if self._expanded else 0\n icon_size = self.arrow_btn.iconSize()\n pixmap = FluentIcon.CHEVRON_RIGHT.icon().pixmap(icon_size)\n rotated = pixmap.transformed(QTransform().rotate(angle), Qt.SmoothTransformation)\n self.arrow_btn.setIcon(QIcon(rotated))\n\n if self._expanded:\n self.form_widget.setVisible(True)\n target_height = self.form_widget.sizeHint().height()\n\n anim = QPropertyAnimation(self.form_widget, b\"maximumHeight\", self)\n anim.setDuration(200)\n anim.setStartValue(0)\n anim.setEndValue(target_height)\n anim.setEasingCurve(QEasingCurve.OutCubic)\n anim.start()\n self._expand_anim = anim\n else:\n anim = QPropertyAnimation(self.form_widget, b\"maximumHeight\", self)\n anim.setDuration(200)\n anim.setStartValue(self.form_widget.height())\n anim.setEndValue(0)\n anim.setEasingCurve(QEasingCurve.OutCubic)\n anim.finished.connect(lambda: self.form_widget.setVisible(False))\n anim.start()\n self._expand_anim = anim\n\n def _is_interactive_widget(self, widget: QWidget | None) -> bool:\n \"\"\"检查控件或其祖先是否为交互控件\"\"\"\n while widget is not None and widget is not self:\n if isinstance(widget, _INTERACTIVE_TYPES):\n return True\n widget = widget.parentWidget()\n return False\n\n def mouseReleaseEvent(self, event):\n \"\"\"点击卡片非交互区域展开/收起\"\"\"\n if event.button() == Qt.LeftButton:\n child = self.childAt(event.position().toPoint())\n if not self._is_interactive_widget(child):\n self._toggle_expand()\n super().mouseReleaseEvent(event)\n\n def _on_save(self) -> None:\n \"\"\"保存编辑\"\"\"\n model_config = self._collect_form_data()\n\n # 验证必填字段\n if not model_config.api_base:\n InfoBar.error(\n title=\"保存失败\",\n content=\"API Base 不能为空\",\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n return\n if not model_config.api_key:\n InfoBar.error(\n title=\"保存失败\",\n content=\"API Key 不能为空\",\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n return\n if not model_config.model:\n InfoBar.error(\n title=\"保存失败\",\n content=\"模型 ID 不能为空\",\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n return\n\n # 保存配置\n try:\n config = get_config()\n config.add_model(self.model_name, model_config)\n config.save()\n\n InfoBar.success(\n title=\"保存成功\",\n content=f\"模型「{self.model_name}」配置已保存\",\n duration=3000,\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n except Exception as e:\n InfoBar.error(\n title=\"保存失败\",\n content=str(e),\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n\n def set_default(self, is_default: bool) -> None:\n \"\"\"设置默认状态\"\"\"\n self._is_default = is_default\n self.default_radio.setChecked(is_default)\n\n\nclass AISettingsWindow(CursorFixMixin, FluentWidget):\n \"\"\"AI 配置管理窗口\"\"\"\n\n def __init__(self, parent=None) -> None:\n super().__init__(parent)\n\n self.cards: dict[str, AIModelCard] = {}\n\n self._init_ui()\n self._load_models()\n\n def _init_ui(self) -> None:\n \"\"\"初始化界面\"\"\"\n self.setWindowTitle(\"AI 配置\")\n self.setFixedSize(850, 600)\n\n # 标题栏\n self.titleBar.setFixedHeight(48)\n self.titleBar.vBoxLayout.insertStretch(0, 1)\n self.titleBar.minBtn.hide()\n self.titleBar.maxBtn.hide()\n\n icon_path = get_icon_path()\n if icon_path:\n icon = QIcon(icon_path)\n self.setWindowIcon(icon)\n self.titleBar.setIcon(icon)\n\n # 内容容器\n self._content_widget = QWidget(self)\n self._content_widget.setObjectName(\"dialogContent\")\n\n content_layout = QVBoxLayout(self._content_widget)\n content_layout.setContentsMargins(20, 16, 20, 12)\n\n layout = QVBoxLayout(self)\n layout.setContentsMargins(0, self.titleBar.height(), 0, 0)\n layout.addWidget(self._content_widget)\n\n self._apply_content_style()\n qconfig.themeChanged.connect(self._apply_content_style)\n\n # ── 工具栏 ──\n toolbar = QHBoxLayout()\n toolbar.addStretch(1)\n\n self.add_btn = PrimaryPushButton(\"添加模型\", self, FluentIcon.ADD)\n self.add_btn.clicked.connect(self._add_model)\n toolbar.addWidget(self.add_btn)\n\n content_layout.addLayout(toolbar)\n\n # ── 卡片滚动区 ──\n self.scroll_area = SmoothScrollArea(self)\n self.scroll_area.setWidgetResizable(True)\n\n self.scroll_widget = QWidget()\n self.cards_layout = QVBoxLayout(self.scroll_widget)\n self.cards_layout.setContentsMargins(0, 8, 0, 8)\n self.cards_layout.setSpacing(8)\n self.cards_layout.addStretch(1)\n\n self.scroll_area.setWidget(self.scroll_widget)\n content_layout.addWidget(self.scroll_area, 1)\n\n def _load_models(self) -> None:\n \"\"\"从配置文件加载模型列表\"\"\"\n config = get_config()\n models = config.list_models()\n default_model = config.default_model\n\n for model_name in models:\n is_default = (model_name == default_model)\n self._create_card(model_name, is_default)\n\n def _create_card(self, model_name: str, is_default: bool) -> AIModelCard:\n \"\"\"创建并添加一张模型卡片\"\"\"\n card = AIModelCard(model_name, is_default, self.scroll_widget)\n card.delete_btn.clicked.connect(lambda: self._remove_model(model_name))\n card.default_radio.clicked.connect(lambda: self._set_default_model(model_name))\n\n # 插入到 stretch 之前\n self.cards_layout.insertWidget(self.cards_layout.count() - 1, card)\n self.cards[model_name] = card\n return card\n\n def _add_model(self) -> None:\n \"\"\"添加模型\"\"\"\n existing = list(self.cards.keys())\n dialog = ModelAddDialog(existing, self)\n if dialog.exec():\n model_name = dialog.model_name\n\n # 判断是否为第一个模型\n config = get_config()\n is_first = len(config.list_models()) == 0\n\n # 创建默认配置并保存\n default_config = ModelConfig(\n api_base=\"https://api.example.com/v1\",\n api_key=\"\",\n model=model_name,\n max_tokens=4096,\n temperature=0.7,\n supports_vision=False,\n )\n config.add_model(model_name, default_config)\n\n # 如果是第一个模型,设为默认\n if is_first:\n config.default_model = model_name\n\n config.save()\n\n # 创建卡片\n card = self._create_card(model_name, is_default=is_first)\n # 自动展开编辑\n card._toggle_expand()\n\n # 刷新首页 AI 面板模型列表\n self._refresh_ai_chat_models()\n\n def _remove_model(self, model_name: str) -> None:\n \"\"\"删除模型\"\"\"\n card = self.cards.get(model_name)\n if not card:\n return\n\n box = MessageBox(\n \"删除确认\",\n f\"确定删除模型「{model_name}」?\",\n self,\n )\n if not box.exec():\n return\n\n # 从布局移除\n self.cards_layout.removeWidget(card)\n card.deleteLater()\n self.cards.pop(model_name)\n\n # 从配置移除\n config = get_config()\n config.remove_model(model_name)\n\n # 如果删除的是默认模型,选择第一个作为默认\n if config.default_model == model_name:\n remaining = config.list_models()\n if remaining:\n new_default = remaining[0]\n config.default_model = new_default\n if new_default in self.cards:\n self.cards[new_default].set_default(True)\n\n config.save()\n\n # 刷新首页 AI 面板模型列表\n self._refresh_ai_chat_models()\n\n def _set_default_model(self, model_name: str) -> None:\n \"\"\"设置默认模型\"\"\"\n config = get_config()\n config.default_model = model_name\n config.save()\n\n # 更新所有卡片的默认状态\n for name, card in self.cards.items():\n card.set_default(name == model_name)\n\n InfoBar.success(\n title=\"设置成功\",\n content=f\"已将「{model_name}」设为默认模型\",\n duration=3000,\n position=InfoBarPosition.TOP,\n parent=self,\n )\n\n def _refresh_ai_chat_models(self) -> None:\n \"\"\"刷新首页 AI 聊天面板的模型列表\"\"\"\n try:\n from guanlan.core.events import signal_bus\n signal_bus.ai_models_changed.emit()\n except Exception:\n # 如果信号总线未定义该信号,忽略\n pass\n\n def _apply_content_style(self) -> None:\n \"\"\"应用内容区域样式\"\"\"\n theme = Theme.DARK if isDarkTheme() else Theme.LIGHT\n StyleSheet.apply(self._content_widget, [\n \"common.qss\", \"window.qss\",\n ], theme)\n"}, "ce3dbae11055716664289de1bfea7b056c8f63fa": {"t": 1785758541.8327491, "n": 17144, "h": "27a8ab8c19187b8d", "c": "# -*- coding: utf-8 -*-\n\"\"\"\n观澜量化 - AI 配置管理窗口\n\n卡片式布局,支持模型增删改查、设置默认模型。\n\nAuthor: 海山观澜\n\"\"\"\n\nfrom PySide6.QtCore import Qt, QSize, QPropertyAnimation, QEasingCurve\nfrom PySide6.QtGui import QIcon, QTransform\nfrom PySide6.QtWidgets import (\n QWidget, QVBoxLayout, QHBoxLayout, QFormLayout,\n)\n\nfrom qfluentwidgets import (\n SubtitleLabel, StrongBodyLabel, BodyLabel,\n LineEdit, SpinBox, DoubleSpinBox, CheckBox, RadioButton,\n PushButton, PrimaryPushButton, ToolButton, TransparentToolButton,\n CardWidget, MessageBox, MessageBoxBase,\n SmoothScrollArea,\n InfoBar, InfoBarPosition,\n FluentIcon, FluentWidget,\n isDarkTheme, qconfig,\n)\n\nfrom guanlan.ui.common.icon import get_icon_path\nfrom guanlan.ui.common.mixin import CursorFixMixin\nfrom guanlan.ui.common.style import StyleSheet, Theme\nfrom guanlan.core.services.ai import get_config, ModelConfig\n\n\n# 交互控件类型(点击时不触发折叠)\n_INTERACTIVE_TYPES = (\n PushButton, PrimaryPushButton, ToolButton,\n CheckBox, LineEdit, SpinBox, DoubleSpinBox,\n)\n\n\nclass ModelAddDialog(MessageBoxBase):\n \"\"\"添加模型对话框\"\"\"\n\n def __init__(self, existing_names: list[str], parent=None):\n super().__init__(parent)\n\n self.existing_names = existing_names\n self.model_name: str = \"\"\n\n self.titleLabel = SubtitleLabel(\"添加模型\", self)\n self.viewLayout.addWidget(self.titleLabel)\n\n self.name_edit = LineEdit(self)\n self.name_edit.setPlaceholderText(\"输入模型名称,如:deepseek-chat\")\n self.name_edit.setClearButtonEnabled(True)\n self.viewLayout.addWidget(self.name_edit)\n\n self.yesButton.setText(\"添加\")\n self.cancelButton.setText(\"取消\")\n self.widget.setMinimumWidth(400)\n\n def validate(self) -> bool:\n \"\"\"验证输入(基类 MessageBoxBase 在确认时自动调用)\"\"\"\n name = self.name_edit.text().strip()\n if not name:\n InfoBar.error(\n title=\"输入错误\",\n content=\"模型名称不能为空\",\n position=InfoBarPosition.TOP,\n parent=self,\n )\n return False\n if name in self.existing_names:\n InfoBar.error(\n title=\"输入错误\",\n content=f\"模型名称「{name}」已存在\",\n position=InfoBarPosition.TOP,\n parent=self,\n )\n return False\n\n self.model_name = name\n return True\n\n\nclass AIModelCard(CardWidget):\n \"\"\"AI 模型卡片\n\n 显示模型摘要信息,点击展开/收起编辑表单。\n \"\"\"\n\n def __init__(self, model_name: str, is_default: bool, parent=None):\n super().__init__(parent)\n\n self.model_name = model_name\n self._is_default = is_default\n self._expanded = False\n\n self._init_ui()\n self._connect_signals()\n self._load_form_data()\n\n def _init_ui(self) -> None:\n \"\"\"初始化界面\"\"\"\n self.main_layout = QVBoxLayout(self)\n self.main_layout.setContentsMargins(16, 12, 16, 12)\n self.main_layout.setSpacing(0)\n\n # ── 摘要行 ──\n self._init_summary()\n\n # ── 编辑表单(默认隐藏)──\n self._init_form()\n\n def _init_summary(self) -> None:\n \"\"\"初始化摘要行\"\"\"\n summary = QHBoxLayout()\n summary.setSpacing(20)\n\n # 折叠箭头\n self.arrow_btn = ToolButton(FluentIcon.CHEVRON_RIGHT, self)\n self.arrow_btn.setFixedSize(20, 20)\n self.arrow_btn.setIconSize(QSize(10, 10))\n self.arrow_btn.clicked.connect(self._toggle_expand)\n summary.addWidget(self.arrow_btn)\n\n # 模型名称\n self.name_label = StrongBodyLabel(self.model_name, self)\n self.name_label.setMinimumWidth(120)\n summary.addWidget(self.name_label)\n\n summary.addStretch(1)\n\n # 默认模型单选框\n self.default_radio = RadioButton(\"默认模型\", self)\n self.default_radio.setChecked(self._is_default)\n summary.addWidget(self.default_radio)\n\n # 删除按钮\n self.delete_btn = TransparentToolButton(FluentIcon.DELETE, self)\n summary.addWidget(self.delete_btn)\n\n self.main_layout.addLayout(summary)\n\n def _init_form(self) -> None:\n \"\"\"初始化编辑表单(默认隐藏)\"\"\"\n self.form_widget = QWidget(self)\n self.form_widget.setVisible(False)\n self.form_widget.setMaximumHeight(0)\n\n form_layout = QVBoxLayout(self.form_widget)\n form_layout.setContentsMargins(0, 12, 0, 4)\n\n # 表单字段\n form = QFormLayout()\n form.setLabelAlignment(Qt.AlignRight | Qt.AlignVCenter)\n form.setHorizontalSpacing(16)\n form.setVerticalSpacing(10)\n\n # API Base\n self.api_base_edit = LineEdit(self.form_widget)\n self.api_base_edit.setPlaceholderText(\"https://api.deepseek.com/v1\")\n self.api_base_edit.setMinimumWidth(300)\n form.addRow(BodyLabel(\"API Base\", self.form_widget), self.api_base_edit)\n\n # API Key\n self.api_key_edit = LineEdit(self.form_widget)\n self.api_key_edit.setPlaceholderText(\"sk-...\")\n self.api_key_edit.setEchoMode(LineEdit.EchoMode.Password)\n self.api_key_edit.setMinimumWidth(300)\n form.addRow(BodyLabel(\"API Key\", self.form_widget), self.api_key_edit)\n\n # 模型 ID\n self.model_id_edit = LineEdit(self.form_widget)\n self.model_id_edit.setPlaceholderText(\"deepseek-chat\")\n self.model_id_edit.setMinimumWidth(300)\n form.addRow(BodyLabel(\"模型 ID\", self.form_widget), self.model_id_edit)\n\n # Max Tokens\n self.max_tokens_spin = SpinBox(self.form_widget)\n self.max_tokens_spin.setRange(1, 32768)\n self.max_tokens_spin.setValue(4096)\n self.max_tokens_spin.setMinimumWidth(150)\n form.addRow(BodyLabel(\"Max Tokens\", self.form_widget), self.max_tokens_spin)\n\n # Temperature\n self.temperature_spin = DoubleSpinBox(self.form_widget)\n self.temperature_spin.setRange(0.0, 2.0)\n self.temperature_spin.setSingleStep(0.1)\n self.temperature_spin.setValue(0.7)\n self.temperature_spin.setDecimals(1)\n self.temperature_spin.setMinimumWidth(150)\n form.addRow(BodyLabel(\"Temperature\", self.form_widget), self.temperature_spin)\n\n # 支持图片\n self.supports_vision_check = CheckBox(\"支持图片输入\", self.form_widget)\n form.addRow(BodyLabel(\"能力\", self.form_widget), self.supports_vision_check)\n\n form_layout.addLayout(form)\n\n # 保存按钮\n btn_layout = QHBoxLayout()\n btn_layout.addStretch(1)\n self.save_btn = PrimaryPushButton(\"保存\", self.form_widget)\n self.save_btn.setFixedWidth(100)\n btn_layout.addWidget(self.save_btn)\n form_layout.addLayout(btn_layout)\n\n self.main_layout.addWidget(self.form_widget)\n\n def _load_form_data(self) -> None:\n \"\"\"加载模型配置到表单\"\"\"\n try:\n config = get_config()\n model_config = config.get_model_config(self.model_name)\n\n self.api_base_edit.setText(model_config.api_base)\n self.api_key_edit.setText(model_config.api_key)\n self.model_id_edit.setText(model_config.model)\n self.max_tokens_spin.setValue(model_config.max_tokens)\n self.temperature_spin.setValue(model_config.temperature)\n self.supports_vision_check.setChecked(model_config.supports_vision)\n except Exception:\n # 新建模型,使用默认值\n pass\n\n def _collect_form_data(self) -> ModelConfig:\n \"\"\"从表单收集数据\"\"\"\n return ModelConfig(\n api_base=self.api_base_edit.text().strip(),\n api_key=self.api_key_edit.text().strip(),\n model=self.model_id_edit.text().strip(),\n max_tokens=self.max_tokens_spin.value(),\n temperature=self.temperature_spin.value(),\n supports_vision=self.supports_vision_check.isChecked(),\n )\n\n def _connect_signals(self) -> None:\n \"\"\"连接信号\"\"\"\n self.save_btn.clicked.connect(self._on_save)\n\n def _toggle_expand(self) -> None:\n \"\"\"展开/收起编辑表单\"\"\"\n self._expanded = not self._expanded\n\n # 旋转箭头图标\n angle = 90 if self._expanded else 0\n icon_size = self.arrow_btn.iconSize()\n pixmap = FluentIcon.CHEVRON_RIGHT.icon().pixmap(icon_size)\n rotated = pixmap.transformed(QTransform().rotate(angle), Qt.SmoothTransformation)\n self.arrow_btn.setIcon(QIcon(rotated))\n\n if self._expanded:\n self.form_widget.setVisible(True)\n target_height = self.form_widget.sizeHint().height()\n\n anim = QPropertyAnimation(self.form_widget, b\"maximumHeight\", self)\n anim.setDuration(200)\n anim.setStartValue(0)\n anim.setEndValue(target_height)\n anim.setEasingCurve(QEasingCurve.OutCubic)\n anim.start()\n self._expand_anim = anim\n else:\n anim = QPropertyAnimation(self.form_widget, b\"maximumHeight\", self)\n anim.setDuration(200)\n anim.setStartValue(self.form_widget.height())\n anim.setEndValue(0)\n anim.setEasingCurve(QEasingCurve.OutCubic)\n anim.finished.connect(lambda: self.form_widget.setVisible(False))\n anim.start()\n self._expand_anim = anim\n\n def _is_interactive_widget(self, widget: QWidget | None) -> bool:\n \"\"\"检查控件或其祖先是否为交互控件\"\"\"\n while widget is not None and widget is not self:\n if isinstance(widget, _INTERACTIVE_TYPES):\n return True\n widget = widget.parentWidget()\n return False\n\n def mouseReleaseEvent(self, event):\n \"\"\"点击卡片非交互区域展开/收起\"\"\"\n if event.button() == Qt.LeftButton:\n child = self.childAt(event.position().toPoint())\n if not self._is_interactive_widget(child):\n self._toggle_expand()\n super().mouseReleaseEvent(event)\n\n def _on_save(self) -> None:\n \"\"\"保存编辑\"\"\"\n model_config = self._collect_form_data()\n\n # 验证必填字段\n if not model_config.api_base:\n InfoBar.error(\n title=\"保存失败\",\n content=\"API Base 不能为空\",\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n return\n if not model_config.api_key:\n InfoBar.error(\n title=\"保存失败\",\n content=\"API Key 不能为空\",\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n return\n if not model_config.model:\n InfoBar.error(\n title=\"保存失败\",\n content=\"模型 ID 不能为空\",\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n return\n\n # 保存配置\n try:\n config = get_config()\n config.add_model(self.model_name, model_config)\n config.save()\n\n InfoBar.success(\n title=\"保存成功\",\n content=f\"模型「{self.model_name}」配置已保存\",\n duration=3000,\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n except Exception as e:\n InfoBar.error(\n title=\"保存失败\",\n content=str(e),\n position=InfoBarPosition.TOP,\n parent=self.window(),\n )\n\n def set_default(self, is_default: bool) -> None:\n \"\"\"设置默认状态\"\"\"\n self._is_default = is_default\n self.default_radio.setChecked(is_default)\n\n\nclass AISettingsWindow(CursorFixMixin, FluentWidget):\n \"\"\"AI 配置管理窗口\"\"\"\n\n def __init__(self, parent=None) -> None:\n super().__init__(parent)\n\n self.cards: dict[str, AIModelCard] = {}\n\n self._init_ui()\n self._load_models()\n\n def _init_ui(self) -> None:\n \"\"\"初始化界面\"\"\"\n self.setWindowTitle(\"AI 配置\")\n self.setFixedSize(850, 600)\n\n # 标题栏\n self.titleBar.setFixedHeight(48)\n self.titleBar.vBoxLayout.insertStretch(0, 1)\n self.titleBar.minBtn.hide()\n self.titleBar.maxBtn.hide()\n\n icon_path = get_icon_path()\n if icon_path:\n icon = QIcon(icon_path)\n self.setWindowIcon(icon)\n self.titleBar.setIcon(icon)\n\n # 内容容器\n self._content_widget = QWidget(self)\n self._content_widget.setObjectName(\"dialogContent\")\n\n content_layout = QVBoxLayout(self._content_widget)\n content_layout.setContentsMargins(20, 16, 20, 12)\n\n layout = QVBoxLayout(self)\n layout.setContentsMargins(0, self.titleBar.height(), 0, 0)\n layout.addWidget(self._content_widget)\n\n self._apply_content_style()\n qconfig.themeChanged.connect(self._apply_content_style)\n\n # ── 工具栏 ──\n toolbar = QHBoxLayout()\n toolbar.addStretch(1)\n\n self.add_btn = PrimaryPushButton(\"添加模型\", self, FluentIcon.ADD)\n self.add_btn.clicked.connect(self._add_model)\n toolbar.addWidget(self.add_btn)\n\n content_layout.addLayout(toolbar)\n\n # ── 卡片滚动区 ──\n self.scroll_area = SmoothScrollArea(self)\n self.scroll_area.setWidgetResizable(True)\n\n self.scroll_widget = QWidget()\n self.cards_layout = QVBoxLayout(self.scroll_widget)\n self.cards_layout.setContentsMargins(0, 8, 0, 8)\n self.cards_layout.setSpacing(8)\n self.cards_layout.addStretch(1)\n\n self.scroll_area.setWidget(self.scroll_widget)\n content_layout.addWidget(self.scroll_area, 1)\n\n def _load_models(self) -> None:\n \"\"\"从配置文件加载模型列表\"\"\"\n config = get_config()\n models = config.list_models()\n default_model = config.default_model\n\n for model_name in models:\n is_default = (model_name == default_model)\n self._create_card(model_name, is_default)\n\n def _create_card(self, model_name: str, is_default: bool) -> AIModelCard:\n \"\"\"创建并添加一张模型卡片\"\"\"\n card = AIModelCard(model_name, is_default, self.scroll_widget)\n card.delete_btn.clicked.connect(lambda: self._remove_model(model_name))\n card.default_radio.clicked.connect(lambda: self._set_default_model(model_name))\n\n # 插入到 stretch 之前\n self.cards_layout.insertWidget(self.cards_layout.count() - 1, card)\n self.cards[model_name] = card\n return card\n\n def _add_model(self) -> None:\n \"\"\"添加模型\"\"\"\n existing = list(self.cards.keys())\n dialog = ModelAddDialog(existing, self)\n if dialog.exec():\n model_name = dialog.model_name\n\n # 判断是否为第一个模型\n config = get_config()\n is_first = len(config.list_models()) == 0\n\n # 创建默认配置并保存\n default_config = ModelConfig(\n api_base=\"https://api.example.com/v1\",\n api_key=\"\",\n model=model_name,\n max_tokens=4096,\n temperature=0.7,\n supports_vision=False,\n )\n config.add_model(model_name, default_config)\n\n # 如果是第一个模型,设为默认\n if is_first:\n config.default_model = model_name\n\n config.save()\n\n # 创建卡片\n card = self._create_card(model_name, is_default=is_first)\n # 自动展开编辑\n card._toggle_expand()\n\n # 刷新首页 AI 面板模型列表\n self._refresh_ai_chat_models()\n\n def _remove_model(self, model_name: str) -> None:\n \"\"\"删除模型\"\"\"\n card = self.cards.get(model_name)\n if not card:\n return\n\n box = MessageBox(\n \"删除确认\",\n f\"确定删除模型「{model_name}」?\",\n self,\n )\n if not box.exec():\n return\n\n # 从布局移除\n self.cards_layout.removeWidget(card)\n card.deleteLater()\n self.cards.pop(model_name)\n\n # 从配置移除\n config = get_config()\n config.remove_model(model_name)\n\n # 如果删除的是默认模型,选择第一个作为默认\n if config.default_model == model_name:\n remaining = config.list_models()\n if remaining:\n new_default = remaining[0]\n config.default_model = new_default\n if new_default in self.cards:\n self.cards[new_default].set_default(True)\n\n config.save()\n\n # 刷新首页 AI 面板模型列表\n self._refresh_ai_chat_models()\n\n def _set_default_model(self, model_name: str) -> None:\n \"\"\"设置默认模型\"\"\"\n config = get_config()\n config.default_model = model_name\n config.save()\n\n # 更新所有卡片的默认状态\n for name, card in self.cards.items():\n card.set_default(name == model_name)\n\n InfoBar.success(\n title=\"设置成功\",\n content=f\"已将「{model_name}」设为默认模型\",\n duration=3000,\n position=InfoBarPosition.TOP,\n parent=self,\n )\n\n def _refresh_ai_chat_models(self) -> None:\n \"\"\"刷新首页 AI 聊天面板的模型列表\"\"\"\n try:\n from guanlan.core.events import signal_bus\n signal_bus.ai_models_changed.emit()\n except Exception:\n # 如果信号总线未定义该信号,忽略\n pass\n\n def _apply_content_style(self) -> None:\n \"\"\"应用内容区域样式\"\"\"\n theme = Theme.DARK if isDarkTheme() else Theme.LIGHT\n StyleSheet.apply(self._content_widget, [\n \"common.qss\", \"window.qss\",\n ], theme)\n"}}}}