5 changed files with 653 additions and 335 deletions
-
1CLAUDE.md
-
38src/main/java/com/wok/supportbot/controller/AiModelConfigController.java
-
170src/main/java/com/wok/supportbot/service/ModelListFetcher.java
-
727src/main/resources/static/components/ModelConfigManager.js
-
10src/main/resources/static/js/api.js
@ -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<String, String> 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<ModelInfo> 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<ModelInfo> data; |
||||
|
} |
||||
|
|
||||
|
/** |
||||
|
* 单个模型信息 |
||||
|
*/ |
||||
|
@Data |
||||
|
@JsonIgnoreProperties(ignoreUnknown = true) |
||||
|
public static class ModelInfo { |
||||
|
private String id; |
||||
|
private String object; |
||||
|
@JsonProperty("owned_by") |
||||
|
private String ownedBy; |
||||
|
} |
||||
|
} |
||||
727
src/main/resources/static/components/ModelConfigManager.js
File diff suppressed because it is too large
View File
File diff suppressed because it is too large
View File
Write
Preview
Loading…
Cancel
Save
Reference in new issue