From 429ed3c1c1cdf23f6db02377e8c1a4fd879eabaa Mon Sep 17 00:00:00 2001 From: DXC Date: Thu, 25 Jun 2026 18:11:50 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B5=8B=E8=AF=95=E4=BF=AE=E6=94=B9?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/gui/dialogs.py | 124 ++++++++++++++------------ src/gui/panels/step13_report_panel.py | 3 +- src/postprocessing/report_word.py | 28 +++--- 3 files changed, 83 insertions(+), 72 deletions(-) diff --git a/src/gui/dialogs.py b/src/gui/dialogs.py index 3c95a6f..b6a21e1 100644 --- a/src/gui/dialogs.py +++ b/src/gui/dialogs.py @@ -165,24 +165,43 @@ class BandConfirmDialog(QDialog): from PyQt5.QtCore import QSettings - AI_SETTINGS_ORG = "IrisWaterQuality" AI_SETTINGS_APP = "WQ_GUI" +# 扩充预设字典,覆盖市面主流大模型标准接口 AI_DEFAULTS = { - "ollama": { - "api_base_url": "http://localhost:11434", - "vision_model": "qwen3-vl:8b", - "text_model": "qwen3-vl:8b", + "aliyun": { + "api_base_url": "https://dashscope.aliyuncs.com/compatible-mode/v1/chat/completions", + "vision_model": "qwen-vl-max", + "text_model": "qwen-max", + }, + "zhipu": { + "api_base_url": "https://open.bigmodel.cn/api/paas/v4/chat/completions", + "vision_model": "glm-4v", + "text_model": "glm-4", + }, + "deepseek": { + "api_base_url": "https://api.deepseek.com/chat/completions", + "vision_model": "deepseek-chat", # DeepSeek暂无独立视觉API,可用通用或自行更换 + "text_model": "deepseek-chat", + }, + "openai": { + "api_base_url": "https://api.openai.com/v1/chat/completions", + "vision_model": "gpt-4o", + "text_model": "gpt-4o", }, "minimax": { - "api_base_url": "https://api.minimaxi.com/v1/text/chatcompletion_v2", - "vision_model": "abab6.5s-chat", + "api_base_url": "https://api.minimax.chat/v1/chat/completions", + "vision_model": "abab6.5g-chat", "text_model": "abab6.5s-chat", }, + "ollama": { + "api_base_url": "http://localhost:11434", + "vision_model": "qwen2-vl", + "text_model": "qwen2.5", + }, } - class AISettingsDialog(QDialog): """AI 引擎可视化配置弹窗,配置持久化到 QSettings。""" @@ -195,41 +214,22 @@ class AISettingsDialog(QDialog): self._init_ui() def _load_settings(self): - """从 QSettings 读取已有配置;无记录则回退到环境变量或默认值。""" + """从 QSettings 读取已有配置;无记录则回退到预设字典或环境变量。""" s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP) - self._provider = s.value("ai_provider", "minimax", type=str) + # 默认推荐 Aliyun (通义千问) + self._provider = s.value("ai_provider", "Aliyun", type=str) - # API Key 不设默认值(敏感信息,首次必须由用户输入) - self._api_key = s.value("minimax_api_key", "", type=str) + # 【向后兼容补丁】优先读取新规范的 api_key,如果为空,尝试读取旧版本留下的 minimax_api_key + self._api_key = s.value("api_key", "", type=str) + if not self._api_key: + self._api_key = s.value("minimax_api_key", "", type=str) - # 已保存的 URL 和模型;若 QSettings 无记录则读环境变量 - if self._provider == "ollama": - self._api_base_url = ( - s.value("api_base_url", "") - or os.environ.get("OLLAMA_URL", AI_DEFAULTS["ollama"]["api_base_url"]) - ) - self._vision_model = ( - s.value("vision_model", "") - or os.environ.get("OLLAMA_VISION_MODEL", AI_DEFAULTS["ollama"]["vision_model"]) - ) - self._text_model = ( - s.value("text_model", "") - or os.environ.get("OLLAMA_TEXT_MODEL", AI_DEFAULTS["ollama"]["text_model"]) - ) - else: - self._api_base_url = ( - s.value("api_base_url", "") - or os.environ.get("MINIMAX_BASE_URL", AI_DEFAULTS["minimax"]["api_base_url"]) - ) - self._vision_model = ( - s.value("vision_model", "") - or os.environ.get("MINIMAX_VISION_MODEL", AI_DEFAULTS["minimax"]["vision_model"]) - ) - self._text_model = ( - s.value("text_model", "") - or os.environ.get("MINIMAX_TEXT_MODEL", AI_DEFAULTS["minimax"]["text_model"]) - ) + provider_key = self._provider.lower() + defaults = AI_DEFAULTS.get(provider_key, {"api_base_url": "", "vision_model": "", "text_model": ""}) + self._api_base_url = s.value("api_base_url", "", type=str) or defaults.get("api_base_url", "") + self._vision_model = s.value("vision_model", "", type=str) or defaults.get("vision_model", "") + self._text_model = s.value("text_model", "", type=str) or defaults.get("text_model", "") self._timeout = s.value("timeout_s", 120, type=int) def _init_ui(self): @@ -240,9 +240,15 @@ class AISettingsDialog(QDialog): provider_row = QHBoxLayout() provider_row.addWidget(QLabel("AI 引擎提供商:")) self._provider_combo = QComboBox() - self._provider_combo.addItems(["Ollama", "Minimax"]) - self._provider_combo.setCurrentText("Ollama" if self._provider == "ollama" else "Minimax") - self._provider_combo.currentIndexChanged.connect(self._on_provider_changed) + + # ★ 核心改动:开启可编辑模式,允许用户随意输入第三方代理商名字 + self._provider_combo.setEditable(True) + self._provider_combo.addItems(["Aliyun", "Zhipu", "DeepSeek", "OpenAI", "Minimax", "Ollama"]) + self._provider_combo.setCurrentText(self._provider) + + # 当文本改变时自动带出推荐配置 + self._provider_combo.currentTextChanged.connect(self._on_provider_changed) + provider_row.addWidget(self._provider_combo, 1) provider_row.addStretch(1) layout.addLayout(provider_row) @@ -251,7 +257,7 @@ class AISettingsDialog(QDialog): url_row = QHBoxLayout() url_row.addWidget(QLabel("API Base URL:")) self._url_edit = QLineEdit(self._api_base_url) - self._url_edit.setPlaceholderText("例如: http://localhost:11434") + self._url_edit.setPlaceholderText("填入兼容 OpenAI 规范的完整 URL") url_row.addWidget(self._url_edit, 1) layout.addLayout(url_row) @@ -259,7 +265,7 @@ class AISettingsDialog(QDialog): key_row = QHBoxLayout() key_row.addWidget(QLabel("API Key:")) self._key_edit = QLineEdit(self._api_key) - self._key_edit.setPlaceholderText("输入 API Key(敏感信息,已加密存储)") + self._key_edit.setPlaceholderText("输入 API Key(本地加密存储)") self._key_edit.setEchoMode(QLineEdit.Password) key_row.addWidget(self._key_edit, 1) layout.addLayout(key_row) @@ -288,10 +294,10 @@ class AISettingsDialog(QDialog): # ── 说明 ────────────────────────────────────────────────────────────── hint = QLabel( - "提示:切换引擎后将自动填充推荐默认值(可手动修改)。" - "API Key 仅本地加密存储,不会明文暴露。" + "提示:可以直接在下拉框输入任意名称。选择预设服务商会自动填充推荐的兼容接口 URL。\n" + "若使用全能多模态大模型(如 gpt-4o / qwen-vl-max 等),视觉与文本模型填入相同名称即可。" ) - hint.setStyleSheet("color: #888; font-size: 10px;") + hint.setStyleSheet("color: #888; font-size: 11px;") hint.setWordWrap(True) layout.addWidget(hint) @@ -306,18 +312,20 @@ class AISettingsDialog(QDialog): btn_box.addButton(cancel_btn, QDialogButtonBox.RejectRole) layout.addWidget(btn_box) - def _on_provider_changed(self): - """切换 Provider 时自动填充推荐默认值。""" - provider = self._provider_combo.currentText().lower() - defaults = AI_DEFAULTS.get(provider, AI_DEFAULTS["minimax"]) - self._url_edit.setText(defaults["api_base_url"]) - self._vision_edit.setText(defaults["vision_model"]) - self._text_edit.setText(defaults["text_model"]) + def _on_provider_changed(self, text): + """切换或输入 Provider 时自动填充推荐默认值。""" + provider_key = text.lower() + if provider_key in AI_DEFAULTS: + defaults = AI_DEFAULTS[provider_key] + self._url_edit.setText(defaults["api_base_url"]) + self._vision_edit.setText(defaults["vision_model"]) + self._text_edit.setText(defaults["text_model"]) def _save_and_close(self): """持久化到 QSettings 并关闭。""" s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP) - provider = self._provider_combo.currentText().lower() + # 获取用户输入的文本(无论是选的还是自己打字的) + provider = self._provider_combo.currentText().strip() s.setValue("ai_provider", provider) s.setValue("api_base_url", self._url_edit.text().strip()) s.setValue("api_key", self._key_edit.text().strip()) @@ -334,11 +342,13 @@ class AISettingsDialog(QDialog): 返回键:ai_provider / api_base_url / api_key / vision_model / text_model / timeout_s """ s = QSettings(AI_SETTINGS_ORG, AI_SETTINGS_APP) - provider = s.value("ai_provider", "minimax", type=str) + # 默认返回 Aliyun + provider = s.value("ai_provider", "Aliyun", type=str) return { "ai_provider": provider, "api_base_url": s.value("api_base_url", "", type=str), - "api_key": s.value("api_key", "", type=str), + # 同样向后兼容读取旧版本的 key + "api_key": s.value("api_key", s.value("minimax_api_key", "", type=str), type=str), "vision_model": s.value("vision_model", "", type=str), "text_model": s.value("text_model", "", type=str), "timeout_s": s.value("timeout_s", 120, type=int), diff --git a/src/gui/panels/step13_report_panel.py b/src/gui/panels/step13_report_panel.py index b160e68..bc455f5 100644 --- a/src/gui/panels/step13_report_panel.py +++ b/src/gui/panels/step13_report_panel.py @@ -64,8 +64,9 @@ class ReportWorkerThread(QThread): ) else: ai_cfg = ReportGenerationConfig( - ai_provider="minimax", + ai_provider=provider, minimax_api_key=s.value("api_key", "", type=str) or "", + minimax_base_url=s.value("api_base_url", "", type=str) or None, # <--- 新增这行,把界面上的 URL 传过去 minimax_vision_model=s.value("vision_model", "", type=str) or None, minimax_text_model=s.value("text_model", "", type=str) or None, minimax_timeout_s=timeout, diff --git a/src/postprocessing/report_word.py b/src/postprocessing/report_word.py index ba9563c..4f7ec47 100644 --- a/src/postprocessing/report_word.py +++ b/src/postprocessing/report_word.py @@ -91,6 +91,7 @@ class ReportGenerationConfig: ollama_timeout_s: Optional[int] = None # Minimax 专属 minimax_api_key: Optional[str] = None + minimax_base_url: Optional[str] = None # <--- 新增这行 minimax_vision_model: Optional[str] = None minimax_text_model: Optional[str] = None minimax_timeout_s: Optional[int] = None @@ -164,8 +165,11 @@ class WaterQualityReportGenerator: if cfg and cfg.minimax_api_key else os.environ.get("MINIMAX_API_KEY", "") ) + # 接收外部传入的万能 URL,默认给一个国际标准的 completions 端点 self.minimax_base_url = ( - os.environ.get("MINIMAX_BASE_URL", "https://api.minimaxi.com/v1/text/chatcompletion_v2").rstrip("/") + cfg.minimax_base_url.rstrip("/") + if cfg and getattr(cfg, 'minimax_base_url', None) + else os.environ.get("MINIMAX_BASE_URL", "https://api.openai.com/v1/chat/completions").rstrip("/") ) self.minimax_vision_model = ( cfg.minimax_vision_model @@ -332,8 +336,6 @@ class WaterQualityReportGenerator: return "(Minimax API Key 未配置,请设置 MINIMAX_API_KEY 环境变量)" url = self.minimax_base_url - if "chatcompletion_v2" in url: - url = "https://api.minimax.chat/v1/chat/completions" payload: Dict[str, Any] = { "model": self.minimax_text_model, @@ -382,11 +384,11 @@ class WaterQualityReportGenerator: except HTTPError as e: body = e.read().decode("utf-8", errors="ignore") print(f"[Minimax HTTP {e.code}] {body}") - return f"(Minimax调用失败 HTTP {e.code}:{e.reason})" + return f"(Cloud API 调用失败 HTTP {e.code}:{e.reason})" except (URLError, TimeoutError) as e: - return f"(Minimax调用失败:{e})" + return f"(Cloud API 调用失败:{e})" except Exception as e: - return f"(Minimax解析失败:{e})" + return f"(Cloud API 解析失败:{e})" def _call_minimax_vision(self, system_prompt: str, user_prompt: str, image_path: Path) -> str: """调用 Minimax 视觉模型(多模态),图片转为 base64 后传入(自动兼容 OpenAI 标准端点)""" if not self.minimax_api_key: @@ -399,8 +401,6 @@ class WaterQualityReportGenerator: return f"(读取图片失败:{e})" url = self.minimax_base_url - if "chatcompletion_v2" in url: - url = "https://api.minimax.chat/v1/chat/completions" payload: Dict[str, Any] = { "model": self.minimax_vision_model, @@ -464,11 +464,11 @@ class WaterQualityReportGenerator: except HTTPError as e: body = e.read().decode("utf-8", errors="ignore") print(f"[Minimax Vision HTTP {e.code}] {body}") - return f"(Minimax Vision调用失败 HTTP {e.code}:{e.reason})" + return f"(Cloud API Vision 调用失败 HTTP {e.code}:{e.reason})" except (URLError, TimeoutError) as e: - return f"(Minimax Vision调用失败:{e})" + return f"(Cloud API Vision 调用失败:{e})" except Exception as e: - return f"(Minimax Vision解析失败:{e})" + return f"(Cloud API Vision 解析失败:{e})" def _ai_chat( self, model: str, @@ -480,13 +480,13 @@ class WaterQualityReportGenerator: 统一 AI 调用入口。根据 self.ai_provider 路由到不同后端实现。 model 参数在 ollama 模式下直接使用;在 minimax 模式下忽略(使用类级别配置的模型)。 """ - if self.ai_provider == "minimax": + if self.ai_provider == "ollama": + return self._ollama_chat(model, system_prompt, user_prompt, image_path) + else: if image_path is not None: return self._call_minimax_vision(system_prompt, user_prompt, image_path) else: return self._call_minimax_text(system_prompt, user_prompt) - else: - return self._ollama_chat(model, system_prompt, user_prompt, image_path) def _get_prompt_for_image(self, image_type: str, param: str, figure_num: int) -> Dict[str, str]: """按图片类型返回 system/user 提示词,注入水质遥感专家级约束。"""