diff --git a/app/gui/tabs/settings.py b/app/gui/tabs/settings.py index a4f04a5..f318530 100644 --- a/app/gui/tabs/settings.py +++ b/app/gui/tabs/settings.py @@ -67,7 +67,10 @@ class SettingsTab(QWidget): settingsSaved = Signal(str) - BACKEND_ITEMS = [("默认网关", "cmhub"), ("自定义网关", "direct")] + BACKEND_ITEMS = [ + ("默认网关", "cmhub"), + ("自定义网关(仅支持 OpenAI 格式)", "direct"), + ] CATEGORY_ITEMS = [("文本", "text"), ("图像", "image")] API_TYPE_ITEMS = [("chat", "chat"), ("images_edits", "images_edits"), ("auto", "auto")] IMAGE_API_TYPE_ITEMS = [("OpenAI 图片编辑接口", "images_edits")] @@ -118,8 +121,11 @@ class SettingsTab(QWidget): self.backend_combo.setVisible(False) self.gateway_default_button = QRadioButton("默认网关") self.gateway_default_button.setObjectName("gatewayDefaultButton") - self.gateway_custom_button = QRadioButton("自定义网关") + self.gateway_custom_button = QRadioButton("自定义网关(仅支持 OpenAI 格式)") self.gateway_custom_button.setObjectName("gatewayCustomButton") + self.gateway_custom_button.setToolTip( + "仅支持 OpenAI 格式的文本接口和图片编辑接口。" + ) self.gateway_button_group = QButtonGroup(self) self.gateway_button_group.setExclusive(True) self.gateway_button_group.addButton(self.gateway_default_button) @@ -138,6 +144,13 @@ class SettingsTab(QWidget): "QRadioButton:focus { border: 1px solid #0969da; border-radius: 4px; }" "QRadioButton::indicator { width: 15px; height: 15px; }" ) + self.custom_gateway_hint_label = QLabel( + "只有服务商明确说明“支持 OpenAI 格式接口”时才选择此项。" + ) + self.custom_gateway_hint_label.setObjectName("customGatewayFormatHintLabel") + self.custom_gateway_hint_label.setStyleSheet("color: #57606a; padding: 0 12px;") + self.custom_gateway_hint_label.setWordWrap(True) + self.custom_gateway_hint_label.setVisible(False) self.cmhub_base_url_edit = QLineEdit() self.cmhub_base_url_edit.setObjectName("cmhubBaseUrlEdit") self.cmhub_base_url_edit.setPlaceholderText("https://host(不要带 /api 或 /api/v1)") @@ -419,6 +432,7 @@ class SettingsTab(QWidget): ) panel_layout.addWidget(self.ai_model_section_title) panel_layout.addWidget(self.gateway_selector) + panel_layout.addWidget(self.custom_gateway_hint_label) panel_layout.addSpacing(8) panel_layout.addWidget(self.model_picker_panel) panel_layout.addSpacing(8) @@ -674,6 +688,7 @@ class SettingsTab(QWidget): self.backend_combo.blockSignals(previous) self.backend_combo.setVisible(False) is_direct = backend == "direct" + self.custom_gateway_hint_label.setVisible(is_direct) self.model_picker_panel.setVisible(is_direct) self.model_detail_panel.setVisible(is_direct) self.direct_role_panel.setVisible(is_direct) diff --git a/docs/routes.md b/docs/routes.md index 0e7ea3b..cfce71b 100644 --- a/docs/routes.md +++ b/docs/routes.md @@ -171,7 +171,7 @@ ## 设置 - 设置页整体布局:内容区居中,左右留白已从 T-506 初始实现缩短到约 40%;实现上用最大内容宽度 + 自适应 margin,而不是写死窗口像素。所有设置组默认响应式 3 列表单:短字段占 1 格,URL/API Key/路径等长字段跨 2 格或 3 格;窄窗口自动降为 2 列/1 列。点击「保存设置」成功后,状态栏显示“设置已保存”,并弹出轻量提示框。T-531 已完成:保存成功会清除未保存标记;保存失败时保留未保存标记并阻止离开。设置页所有数字微调框禁用滚轮改值,滚动输入交给外层设置页,仍可通过箭头、键盘和直接输入修改。 -- 生成网关(T-677/T-680):在原「AI 模型配置」标题下使用同行标准单选框「默认网关 / 自定义网关」替代历史「AI 后端」下拉和不明显的分段按钮;启动/放弃未保存更改时按已保存的 `ai.backend=cmhub/direct` 自动勾选对应项,圆点与焦点状态表达当前选择。切换只改变待保存状态并立即切换原有面板,点击「保存设置」才写入配置;默认网关的 `data/config/cmhub.json` 与自定义模型的 `data/config/ai_models.json` 始终并存,模型新增、保存、删除仍立即写盘,不受来源待保存语义影响。除这两个选择控件外,不改变设置页其它组件、表单布局或业务逻辑。 +- 生成网关(T-677/T-680/T-684):在原「AI 模型配置」标题下使用同行标准单选框「默认网关 / 自定义网关(仅支持 OpenAI 格式)」替代历史「AI 后端」下拉和不明显的分段按钮;选中自定义网关时,在其下显示“只有服务商明确说明‘支持 OpenAI 格式接口’时才选择此项。”,并提供同义 tooltip。启动/放弃未保存更改时按已保存的 `ai.backend=cmhub/direct` 自动勾选对应项,圆点与焦点状态表达当前选择。切换只改变待保存状态并立即切换原有面板,点击「保存设置」才写入配置;默认网关的 `data/config/cmhub.json` 与自定义模型的 `data/config/ai_models.json` 始终并存,模型新增、保存、删除仍立即写盘,不受来源待保存语义影响。除这两个选择控件外,不改变设置页其它组件、表单布局或业务逻辑。 - 默认网关面板展示 Base URL、API Key、生文/生图/图片理解别名、连接超时、批量生成前检查余额、刷新别名和测试连接/查余额;Base URL 输入框旁提示“只填网关根,如 https://host”,保存/刷新前规整掉 `/api`、`/api/v1` 或其它路径。测试连接成功优先显示 `账号「<账号名>」连接默认网关成功`,邮箱脱敏;没有账号信息时显示「默认网关连接成功」。Key 只本地明文保存、UI 打码显示,不进入日志或导出。 - 自定义网关复用既有模型列表、其下紧凑显示的模型配置字段、默认文本模型与默认图像模型;不再显示重复的“模型详情”标题,模型选择区到配置表单保留 8px 间距。文本模型保留 `chat` / `auto`;图片模型固定使用「OpenAI 图片编辑接口」(`images_edits`),②和⑥通过 `/v1/images/edits` 以有序重复 `image[]` 上传,第一张是商品主体图、每次只请求 `n=1` 输出。历史 `chat` / `auto` 图片模型不会被改写或隐藏,但会标示「图片接口不支持」,生成前会被拦截。图片响应只接受 `data[].b64_json` / `data[].url`,不适配聊天响应或服务商私有结构。说明行明确「图片理解仅支持默认网关」。 - 图片模型按钮显示「检查图片配置」,只在本地检查接口类型、URL、模型 ID、密钥和超时,绝不对 `/images/edits` 发送无图片的测试请求;实际可用性必须以②或后续⑥的正常生成确认判断。允许保存不完整配置。②点击生成后、自定义 worker 创建前按本轮标题/封面选择校验所需模型的启用状态、类别、地址、模型 ID、API Key 和接口类型,失败提示去⑤补齐且不发请求、不改任务状态;自定义网关不显示默认网关余额,运行日志和状态栏说明“不计点数,费用由服务商收取”。 diff --git a/docs/tasks/T-684.md b/docs/tasks/T-684.md index 7db8524..96d00df 100644 --- a/docs/tasks/T-684.md +++ b/docs/tasks/T-684.md @@ -3,7 +3,7 @@ id: T-684 title: 明确自定义网关接口格式限制 phase: 5 deps: [T-680, T-681] -status: TODO +status: DONE created: 2026-07-21 --- @@ -60,4 +60,7 @@ git diff --check ## 执行记录 -- 待实现。 +- 将自定义来源单选项和内部兼容下拉标签统一改为“自定义网关(仅支持 OpenAI 格式)”;配置值仍为 `direct`,保存成功状态仍使用简洁的“自定义网关”。 +- 增加仅在 `direct` 选中时可见的说明“只有服务商明确说明‘支持 OpenAI 格式接口’时才选择此项。”,同时为单选项增加中文 tooltip;说明显隐不接入 dirty 信号。 +- 更新设置路由说明及 GUI 回归,覆盖单选项文字、tooltip、默认/自定义来源说明显隐和既有切换保存行为。 +- 验证通过:`py -3.10 -m unittest tests.test_gui`(213 项)、`py -3.10 -m unittest discover -s tests`(654 项)、`py -3.10 -m ruff check app tests main.py`、`py -3.10 -m compileall app main.py`、`git diff --check`。 diff --git a/tests/test_gui.py b/tests/test_gui.py index fa6035e..7713c87 100644 --- a/tests/test_gui.py +++ b/tests/test_gui.py @@ -3139,6 +3139,12 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assertIsInstance(tab.gateway_default_button, QRadioButton) self.assertIsInstance(tab.gateway_custom_button, QRadioButton) + self.assertEqual("默认网关", tab.gateway_default_button.text()) + self.assertEqual( + "自定义网关(仅支持 OpenAI 格式)", + tab.gateway_custom_button.text(), + ) + self.assertIn("OpenAI 格式", tab.gateway_custom_button.toolTip()) self.assertEqual(0, tab.gateway_selector.layout().spacing()) self.assertEqual(2, tab.gateway_selector.layout().count()) self.assertTrue(tab.gateway_default_button.isChecked()) @@ -3146,6 +3152,7 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assertFalse(tab.cmhub_panel.isHidden()) self.assertTrue(tab.model_picker_panel.isHidden()) self.assertTrue(tab.model_detail_panel.isHidden()) + self.assertTrue(tab.custom_gateway_hint_label.isHidden()) self.assertNotIn( "模型详情", [label.text() for label in tab.findChildren(QLabel)], @@ -3164,6 +3171,11 @@ class GuiTests(TempDirMixin, unittest.TestCase): self.assertTrue(tab.cmhub_panel.isHidden()) self.assertFalse(tab.model_picker_panel.isHidden()) self.assertFalse(tab.model_detail_panel.isHidden()) + self.assertFalse(tab.custom_gateway_hint_label.isHidden()) + self.assertEqual( + "只有服务商明确说明“支持 OpenAI 格式接口”时才选择此项。", + tab.custom_gateway_hint_label.text(), + ) self.assertFalse(tab.direct_role_panel.isHidden()) picker_index = tab.settings_panel_layout.indexOf(tab.model_picker_panel) detail_index = tab.settings_panel_layout.indexOf(tab.model_detail_panel)