From d95c9212271a616f4cd6dbefc57e12b36b7731a0 Mon Sep 17 00:00:00 2001 From: wanghanlin <1533525126@qq.com> Date: Sat, 4 Jul 2026 17:52:42 +0800 Subject: [PATCH] =?UTF-8?q?=20=E9=87=8D=E6=9E=84=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E9=A1=B5=E9=9D=A2=EF=BC=8C=E5=AF=B9=E6=A0=87cc-switch=E4=B8=89?= =?UTF-8?q?=E8=A6=81=E7=B4=A0=E8=AE=BE=E8=AE=A1=EF=BC=8C=E6=96=B0=E5=A2=9E?= =?UTF-8?q?=E5=8A=A8=E6=80=81=E6=A8=A1=E5=9E=8B=E5=88=97=E8=A1=A8=E8=8E=B7?= =?UTF-8?q?=E5=8F=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CLAUDE.md | 1 + .../controller/AiModelConfigController.java | 38 + .../supportbot/service/ModelListFetcher.java | 170 ++++ .../static/components/ModelConfigManager.js | 769 ++++++++++-------- src/main/resources/static/js/api.js | 10 + 5 files changed, 653 insertions(+), 335 deletions(-) create mode 100644 src/main/java/com/wok/supportbot/service/ModelListFetcher.java diff --git a/CLAUDE.md b/CLAUDE.md index 027a941..11c1208 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -77,6 +77,7 @@ AI 智能客服系统,基于 Spring AI Alibaba + 通义千问 + PGVector,支 - **EmbeddingModel 运行时切换**: 通过 `EmbeddingModelFactory` + `DynamicEmbeddingModel` 代理,按 DB 活跃配置动态创建/缓存 EmbeddingModel,`PgVectorStoreConfig` 和 `InMemoryVectorStoreConfig` 注入 `DynamicEmbeddingModel`,向量化模型配置变更后无需重启即可生效 - **多提供商支持**: DashScope(通义千问)+ OpenAI 兼容提供商(DeepSeek / 豆包 / Kimi / 智谱 / OpenAI),ChatModel 和 EmbeddingModel 均通过对应 API 手动构建 - **缓存刷新**: 配置增删改激活时 Controller 自动调用 `ChatModelFactory.clearCache()` + `EmbeddingModelFactory.clearCache()` + `AssistantApp.clearCache()` +- **动态模型列表获取**: `ModelListFetcher` 通过调用各提供商的 `/v1/models` 兼容端点(DashScope 用 `/compatible-mode/v1/models`),动态获取可用模型列表。前端填入 API Key + API 地址后,点击「获取模型」即可自动填充模型名称下拉列表(`` 支持搜索选择 + 自定义输入) ### 依赖版本 - Spring AI BOM: `1.0.1`,统一管理所有 `org.springframework.ai` 依赖版本 diff --git a/src/main/java/com/wok/supportbot/controller/AiModelConfigController.java b/src/main/java/com/wok/supportbot/controller/AiModelConfigController.java index ec1394c..1087a7b 100644 --- a/src/main/java/com/wok/supportbot/controller/AiModelConfigController.java +++ b/src/main/java/com/wok/supportbot/controller/AiModelConfigController.java @@ -40,6 +40,9 @@ public class AiModelConfigController { @Autowired private ModelHealthService modelHealthService; + @Autowired + private com.wok.supportbot.service.ModelListFetcher modelListFetcher; + // ==================== 分页列表 ==================== /** @@ -500,6 +503,41 @@ public class AiModelConfigController { } } + // ==================== 获取模型列表(动态发现) ==================== + + /** + * 通过提供商 API 获取可用模型列表 + * 前端填入 API Key + API 地址后,可调用此接口自动发现可用模型,减少手动输入 + * + * @param body 请求体:{ baseUrl?, apiKey, provider? } + * @return 模型列表 [{ id, owned_by }] + */ + @PostMapping("/model-config/fetch-models") + @PreAuthorize("hasRole('admin')") + public ResponseEntity> fetchModels(@RequestBody Map body) { + try { + String apiKey = body.get("apiKey"); + if (apiKey == null || apiKey.trim().isEmpty()) { + return ResponseEntity.badRequest().body(Map.of( + "success", false, "message", "请先填写 API Key")); + } + + String baseUrl = body.getOrDefault("baseUrl", ""); + String provider = body.getOrDefault("provider", ""); + + var models = modelListFetcher.fetchModels(baseUrl, apiKey, provider); + return ResponseEntity.ok(Map.of( + "success", true, + "data", models + )); + } catch (Exception e) { + return ResponseEntity.status(500).body(Map.of( + "success", false, + "message", "获取模型列表失败:" + e.getMessage() + )); + } + } + // ==================== 缓存刷新 ==================== /** diff --git a/src/main/java/com/wok/supportbot/service/ModelListFetcher.java b/src/main/java/com/wok/supportbot/service/ModelListFetcher.java new file mode 100644 index 0000000..68d3020 --- /dev/null +++ b/src/main/java/com/wok/supportbot/service/ModelListFetcher.java @@ -0,0 +1,170 @@ +package com.wok.supportbot.service; + +import com.fasterxml.jackson.annotation.JsonIgnoreProperties; +import com.fasterxml.jackson.annotation.JsonProperty; +import com.fasterxml.jackson.databind.ObjectMapper; +import lombok.Data; +import lombok.extern.slf4j.Slf4j; +import org.springframework.stereotype.Service; +import org.springframework.web.client.RestClient; + +import java.util.Collections; +import java.util.List; +import java.util.Map; + +/** + * 模型列表获取服务 + * 通过调用各 AI 提供商的 /v1/models 兼容端点,动态获取可用模型列表。 + * 对标 cc-switch 理念:填入 API Key + 地址即可自动发现模型。 + */ +@Service +@Slf4j +public class ModelListFetcher { + + private static final ObjectMapper OBJECT_MAPPER = new ObjectMapper(); + + /** + * 各提供商默认的 API 基础地址(与 ChatModelFactory.DEFAULT_BASE_URLS 对应) + */ + private static final Map DEFAULT_BASE_URLS = Map.of( + "dashscope", "https://dashscope.aliyuncs.com/compatible-mode", + "deepseek", "https://api.deepseek.com", + "moonshot", "https://api.moonshot.cn/v1", + "volcengine", "https://ark.cn-beijing.volces.com/api/v3", + "zhipu", "https://open.bigmodel.cn/api/paas/v4", + "openai", "https://api.openai.com" + ); + + /** + * 获取模型列表 + * + * @param baseUrl API 基础地址(可为空,使用提供商默认值) + * @param apiKey API Key + * @param provider 提供商标识(可为空,自动推断) + * @return 模型列表 [{ id, owned_by }] + */ + public List fetchModels(String baseUrl, String apiKey, String provider) { + // 解析实际 baseUrl + String actualBaseUrl = resolveBaseUrl(baseUrl, provider); + + // 拼接 /v1/models 端点 + String modelsUrl = resolveModelsUrl(actualBaseUrl, provider); + + log.info("获取模型列表: provider={}, url={}", provider, modelsUrl); + + try { + RestClient restClient = RestClient.builder() + .defaultHeader("Authorization", "Bearer " + apiKey) + .defaultHeader("Content-Type", "application/json") + .build(); + + String responseJson = restClient.get() + .uri(modelsUrl) + .retrieve() + .body(String.class); + + // 解析 OpenAI 标准格式 { data: [{ id, object, owned_by }] } + ModelsResponse response = OBJECT_MAPPER.readValue(responseJson, ModelsResponse.class); + if (response.getData() == null) { + return Collections.emptyList(); + } + + // 按 id 排序,方便前端展示 + response.getData().sort((a, b) -> { + if (a.getId() == null) return 1; + if (b.getId() == null) return -1; + return a.getId().compareToIgnoreCase(b.getId()); + }); + + log.info("获取模型列表成功: provider={}, count={}", provider, response.getData().size()); + return response.getData(); + + } catch (Exception e) { + log.warn("获取模型列表失败: provider={}, url={}, error={}", provider, modelsUrl, e.getMessage()); + throw new RuntimeException("获取模型列表失败: " + e.getMessage(), e); + } + } + + /** + * 解析 API 基础地址 + * 优先级:用户传入的 baseUrl > 提供商默认值 + */ + private String resolveBaseUrl(String baseUrl, String provider) { + if (baseUrl != null && !baseUrl.isBlank()) { + return baseUrl.endsWith("/") ? baseUrl.substring(0, baseUrl.length() - 1) : baseUrl; + } + if (provider != null && DEFAULT_BASE_URLS.containsKey(provider.toLowerCase())) { + return DEFAULT_BASE_URLS.get(provider.toLowerCase()); + } + throw new RuntimeException("请填写 API 地址或选择提供商"); + } + + /** + * 拼接 models 端点 URL + * 各厂商路径差异: + * - dashscope: /compatible-mode/v1/models + * - openai/deepseek: baseUrl + /v1/models + * - moonshot/volcengine/zhipu: baseUrl 已含版本段(/v1、/api/v3、/api/paas/v4),直接 + /models + */ + private String resolveModelsUrl(String baseUrl, String provider) { + String p = provider != null ? provider.toLowerCase() : ""; + + switch (p) { + case "dashscope": + // DashScope 兼容模式端点 + if (baseUrl.contains("/compatible-mode")) { + return baseUrl + "/v1/models"; + } + return baseUrl + "/compatible-mode/v1/models"; + + case "moonshot": + // moonshot 的 baseUrl 已含 /v1 + return appendPath(baseUrl, "/models"); + + case "volcengine": + // volcengine 的 baseUrl 已含 /api/v3 + return appendPath(baseUrl, "/models"); + + case "zhipu": + // zhipu 的 baseUrl 已含 /api/paas/v4 + return appendPath(baseUrl, "/models"); + + default: + // openai/deepseek/other: 标准 /v1/models + return appendPath(baseUrl, "/v1/models"); + } + } + + /** + * 拼接路径,避免重复斜杠 + */ + private String appendPath(String baseUrl, String path) { + if (baseUrl.endsWith("/")) { + baseUrl = baseUrl.substring(0, baseUrl.length() - 1); + } + return baseUrl + path; + } + + // ==================== 响应数据结构 ==================== + + /** + * OpenAI /v1/models 响应格式 + */ + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ModelsResponse { + private List data; + } + + /** + * 单个模型信息 + */ + @Data + @JsonIgnoreProperties(ignoreUnknown = true) + public static class ModelInfo { + private String id; + private String object; + @JsonProperty("owned_by") + private String ownedBy; + } +} diff --git a/src/main/resources/static/components/ModelConfigManager.js b/src/main/resources/static/components/ModelConfigManager.js index 644f237..a7ce370 100644 --- a/src/main/resources/static/components/ModelConfigManager.js +++ b/src/main/resources/static/components/ModelConfigManager.js @@ -1,8 +1,9 @@ /** - * ⚙️ AI 模型配置管理组件 - * 展示模型配置列表、新增/编辑/激活/删除/测试/复制/导入导出配置 + * ⚙️ AI 模型配置管理组件(cc-switch 优化版) + * 核心三要素:API Key + API 地址 + 模型名称 + * 新增:动态获取模型列表、datalist 自动补全、应用类型标签按钮 * F1: 连接测试 | F2: 配置复制 | F3: 高级参数面板 | F4: 健康看板 - * F5: 模型能力元信息 | F6: Fallback 链 | F7: 导入导出 + * F6: Fallback 链 | F7: 导入导出 */ import { ref, computed, onMounted, onUnmounted, watch } from 'vue' import * as api from '../js/api.js' @@ -11,118 +12,46 @@ import { toast, formatDate } from '../js/utils.js' // 应用类型选项 const APP_TYPE_OPTIONS = [ { value: '', label: '全部类型' }, - { value: 'CHAT', label: '智能客服对话' }, - { value: 'EMBEDDING', label: '文本向量化' }, - { value: 'RAG_REWRITE', label: 'RAG查询重写' }, - { value: 'RERANK', label: '重排序' } + { value: 'CHAT', label: '💬 智能客服对话' }, + { value: 'EMBEDDING', label: '📐 文本向量化' }, + { value: 'RAG_REWRITE', label: '🔄 RAG查询重写' }, + { value: 'RERANK', label: '📊 重排序' } ] -// 提供商选项 +// 应用类型标签(用于弹窗内的标签按钮组) +const APP_TYPE_TABS = [ + { value: 'CHAT', label: '💬 对话', shortLabel: '对话' }, + { value: 'EMBEDDING', label: '📐 向量化', shortLabel: '向量化' }, + { value: 'RAG_REWRITE', label: '🔄 RAG重写', shortLabel: 'RAG重写' }, + { value: 'RERANK', label: '📊 重排序', shortLabel: '重排序' } +] + +// 提供商选项(快捷预设,非必填) const PROVIDER_OPTIONS = [ + { value: '', label: '手动填写地址' }, { value: 'dashscope', label: '通义千问 (DashScope)' }, { value: 'deepseek', label: 'DeepSeek (深度求索)' }, { value: 'volcengine', label: '豆包 (字节跳动)' }, { value: 'moonshot', label: 'Kimi (月之暗面)' }, { value: 'zhipu', label: '智谱 AI (GLM)' }, - { value: 'openai', label: 'OpenAI' }, - { value: 'other', label: '其他' } + { value: 'openai', label: 'OpenAI' } ] -// F5: 提供商默认配置(含模型能力元信息) -const PROVIDER_DEFAULTS = { - dashscope: { - baseUrl: '', - chatModels: [ - { name: 'qwen-turbo', maxContextTokens: 131072, maxOutputTokens: 8192, supportsVision: false, supportsFunctionCall: true }, - { name: 'qwen-plus', maxContextTokens: 131072, maxOutputTokens: 8192, supportsVision: false, supportsFunctionCall: true }, - { name: 'qwen-max', maxContextTokens: 32768, maxOutputTokens: 8192, supportsVision: false, supportsFunctionCall: true }, - { name: 'qwen-long', maxContextTokens: 10000000, maxOutputTokens: 6000, supportsVision: false, supportsFunctionCall: false } - ], - embeddingModels: [ - { name: 'text-embedding-v2', maxContextTokens: 8192, maxOutputTokens: 0, supportsVision: false, supportsFunctionCall: false } - ], - defaultChatModel: 'qwen-turbo', - defaultEmbeddingModel: 'text-embedding-v2', - tip: '通义千问团队版/私有化部署需填写专属 Base URL,公共版可留空' - }, - deepseek: { - baseUrl: 'https://api.deepseek.com', - chatModels: [ - { name: 'deepseek-chat', maxContextTokens: 65536, maxOutputTokens: 8192, supportsVision: false, supportsFunctionCall: true }, - { name: 'deepseek-reasoner', maxContextTokens: 65536, maxOutputTokens: 8192, supportsVision: false, supportsFunctionCall: false } - ], - embeddingModels: [], - defaultChatModel: 'deepseek-chat', - defaultEmbeddingModel: '', - tip: '' - }, - volcengine: { - baseUrl: 'https://ark.cn-beijing.volces.com/api/v3', - chatModels: [ - { name: 'doubao-1-5-pro-32k', maxContextTokens: 32768, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true }, - { name: 'doubao-1-5-lite-32k', maxContextTokens: 32768, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true }, - { name: 'doubao-pro-32k', maxContextTokens: 32768, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true }, - { name: 'doubao-1-5-thinking-pro', maxContextTokens: 131072, maxOutputTokens: 16384, supportsVision: false, supportsFunctionCall: true }, - { name: 'doubao-seed-1.6', maxContextTokens: 131072, maxOutputTokens: 16384, supportsVision: true, supportsFunctionCall: true } - ], - embeddingModels: [ - { name: 'doubao-embedding-text-240515', maxContextTokens: 8192, maxOutputTokens: 0, supportsVision: false, supportsFunctionCall: false }, - { name: 'doubao-embedding-large', maxContextTokens: 8192, maxOutputTokens: 0, supportsVision: false, supportsFunctionCall: false }, - { name: 'doubao-embedding-vision-251215', maxContextTokens: 8192, maxOutputTokens: 0, supportsVision: true, supportsFunctionCall: false } - ], - defaultChatModel: 'doubao-1-5-pro-32k', - defaultEmbeddingModel: 'doubao-embedding-text-240515', - tip: '豆包对话推荐 doubao-1-5-pro-32k;向量化推荐 doubao-embedding-text-240515(2048维)。模型名称可在火山引擎 ARK 控制台获取' - }, - moonshot: { - baseUrl: 'https://api.moonshot.cn/v1', - chatModels: [ - { name: 'moonshot-v1-8k', maxContextTokens: 8192, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true }, - { name: 'moonshot-v1-32k', maxContextTokens: 32768, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true }, - { name: 'moonshot-v1-128k', maxContextTokens: 131072, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true } - ], - embeddingModels: [], - defaultChatModel: 'moonshot-v1-8k', - defaultEmbeddingModel: '', - tip: '' - }, - zhipu: { - baseUrl: 'https://open.bigmodel.cn/api/paas/v4', - chatModels: [ - { name: 'glm-4-plus', maxContextTokens: 128000, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true }, - { name: 'glm-4-flash', maxContextTokens: 128000, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true }, - { name: 'glm-4-long', maxContextTokens: 1000000, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true }, - { name: 'glm-4', maxContextTokens: 128000, maxOutputTokens: 4096, supportsVision: true, supportsFunctionCall: true } - ], - embeddingModels: [], - defaultChatModel: 'glm-4-flash', - defaultEmbeddingModel: '', - tip: '' - }, - openai: { - baseUrl: 'https://api.openai.com', - chatModels: [ - { name: 'gpt-4o', maxContextTokens: 128000, maxOutputTokens: 16384, supportsVision: true, supportsFunctionCall: true }, - { name: 'gpt-4o-mini', maxContextTokens: 128000, maxOutputTokens: 16384, supportsVision: true, supportsFunctionCall: true }, - { name: 'gpt-3.5-turbo', maxContextTokens: 16385, maxOutputTokens: 4096, supportsVision: false, supportsFunctionCall: true } - ], - embeddingModels: [ - { name: 'text-embedding-3-small', maxContextTokens: 8191, maxOutputTokens: 0, supportsVision: false, supportsFunctionCall: false }, - { name: 'text-embedding-3-large', maxContextTokens: 8191, maxOutputTokens: 0, supportsVision: false, supportsFunctionCall: false }, - { name: 'text-embedding-ada-002', maxContextTokens: 8191, maxOutputTokens: 0, supportsVision: false, supportsFunctionCall: false } - ], - defaultChatModel: 'gpt-4o-mini', - defaultEmbeddingModel: 'text-embedding-3-small', - tip: '' - }, - other: { - baseUrl: '', - chatModels: [], - embeddingModels: [], - defaultChatModel: '', - defaultEmbeddingModel: '', - tip: '使用 OpenAI 兼容 API 的其他提供商,请填写 Base URL 和模型名称' - } +// 提供商默认地址映射(仅用于快捷填充,不控制模型列表) +const PROVIDER_BASE_URLS = { + dashscope: 'https://dashscope.aliyuncs.com/compatible-mode', + deepseek: 'https://api.deepseek.com', + volcengine: 'https://ark.cn-beijing.volces.com/api/v3', + moonshot: 'https://api.moonshot.cn/v1', + zhipu: 'https://open.bigmodel.cn/api/paas/v4', + openai: 'https://api.openai.com' +} + +// 提供商使用提示 +const PROVIDER_TIPS = { + dashscope: '通义千问团队版/私有化部署需填写专属 Base URL,公共版可留空', + volcengine: '模型名称可在火山引擎 ARK 控制台获取,如 doubao-1-5-pro-32k', + other: '使用 OpenAI 兼容 API 的其他提供商,请填写完整 API 地址' } // 格式化工具:Token 数显示 @@ -170,7 +99,7 @@ export default { - +
🔗 Fallback 链(按优先级排列,主模型在顶部) @@ -187,28 +116,11 @@ export default {

什么是 Fallback 链?

Fallback 链是一种高可用性机制,当主模型出现故障时,系统会自动切换到备用模型,保证服务不中断。

- -

如何工作?

-
    -
  • 📌 主模型:优先级最高(数值最大)的配置,优先使用
  • -
  • 🔄 自动切换:主模型连续失败 3 次后,自动切换到下一个备用模型
  • -
  • 自动恢复:故障模型 5 分钟后自动恢复,重新加入 Fallback 链
  • -
  • 💚 健康检查:系统每 5 分钟自动检测所有模型的健康状态
  • -
-

如何配置?

  • ✅ 勾选「设为活跃配置」启用模型
  • 📊 调整「优先级」数值(数值越大,优先级越高)
  • 🔀 拖拽列表项可快速调整顺序
  • -
  • 🟢🟡🔴 查看健康状态指示灯(绿=在线,黄=未知,红=离线)
  • -
- -

典型场景:

-
    -
  • 🎯 API 限流:主模型触发限流 → 自动切换到备用模型
  • -
  • 🚨 服务故障:提供商宕机 → 自动接管备用模型
  • -
  • 性能优化:手动调整优先级,将响应更快的模型设为主模型
@@ -230,7 +142,6 @@ export default { {{ item.name }} {{ item.model_name }} P={{ item.priority }} - {{ healthMap[item.id].status === 'ONLINE' ? '🟢' : healthMap[item.id].status === 'UNKNOWN' ? '🟡' : '🔴' }} @@ -249,16 +160,15 @@ export default { 配置名称 应用类型 模型名称 - 提供商 - 温度 + API 地址 API Key - F4: 状态 + 状态 操作 - 暂无配置记录 + 暂无配置记录 @@ -270,26 +180,16 @@ export default { {{ c.model_name }} - -
- {{ formatTokens(getModelMeta(c).maxContextTokens) }} ctx · {{ formatTokens(getModelMeta(c).maxOutputTokens) }} out - · 👁 - · 🔧 -
- {{ getProviderLabel(c.provider) }} - {{ c.temperature != null ? c.temperature : '-' }} + + {{ c.base_url || getProviderBaseUrl(c.provider) || '-' }} + {{ c.api_key || '-' }} -
- - 🟢 活跃 - - - ⚫ 未激活 - - + 🟢 活跃 + ⚫ 未激活 @@ -301,11 +201,9 @@ export default { - - @@ -340,148 +238,207 @@ export default {
- +