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
-
769src/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; |
|||
} |
|||
} |
|||
769
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