You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
552 lines
20 KiB
552 lines
20 KiB
package com.wok.supportbot.controller;
|
|
|
|
import com.wok.supportbot.app.AssistantApp;
|
|
import com.wok.supportbot.config.ChatModelFactory;
|
|
import com.wok.supportbot.config.EmbeddingModelFactory;
|
|
import com.wok.supportbot.config.ModelHealthService;
|
|
import com.wok.supportbot.entity.AiModelConfig;
|
|
import com.wok.supportbot.service.AiModelConfigService;
|
|
import org.springframework.beans.factory.annotation.Autowired;
|
|
import org.springframework.http.ResponseEntity;
|
|
import org.springframework.security.access.prepost.PreAuthorize;
|
|
import org.springframework.web.bind.annotation.*;
|
|
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* AI 模型配置管理控制器
|
|
* 提供模型配置的增删改查、激活等 API
|
|
*/
|
|
@RestController
|
|
public class AiModelConfigController {
|
|
|
|
/** 复用的 JSON 转换器,避免每次导入都新建实例 */
|
|
private static final com.fasterxml.jackson.databind.ObjectMapper OBJECT_MAPPER =
|
|
new com.fasterxml.jackson.databind.ObjectMapper();
|
|
|
|
@Autowired
|
|
private AiModelConfigService aiModelConfigService;
|
|
|
|
@Autowired
|
|
private ChatModelFactory chatModelFactory;
|
|
|
|
@Autowired
|
|
private EmbeddingModelFactory embeddingModelFactory;
|
|
|
|
@Autowired
|
|
private AssistantApp assistantApp;
|
|
|
|
@Autowired
|
|
private ModelHealthService modelHealthService;
|
|
|
|
@Autowired
|
|
private com.wok.supportbot.service.ModelListFetcher modelListFetcher;
|
|
|
|
// ==================== 分页列表 ====================
|
|
|
|
/**
|
|
* 获取模型配置列表(分页)
|
|
*
|
|
* @param page 页码(默认1)
|
|
* @param size 每页大小(默认10)
|
|
* @param appType 应用类型过滤(可选)
|
|
* @return 分页配置列表
|
|
*/
|
|
@GetMapping("/model-config/list")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> listConfigs(
|
|
@RequestParam(defaultValue = "1") int page,
|
|
@RequestParam(defaultValue = "10") int size,
|
|
@RequestParam(required = false) String appType) {
|
|
try {
|
|
Map<String, Object> result = aiModelConfigService.listConfigs(appType, page, size);
|
|
Map<String, Object> data = new java.util.LinkedHashMap<>();
|
|
data.put("success", true);
|
|
data.put("data", result.get("records"));
|
|
data.put("total", result.get("total"));
|
|
data.put("page", result.get("page"));
|
|
data.put("size", result.get("size"));
|
|
data.put("pages", result.get("pages"));
|
|
return ResponseEntity.ok(data);
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "查询失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 配置详情 ====================
|
|
|
|
/**
|
|
* 获取单条配置详情(API Key 脱敏)
|
|
*
|
|
* @param id 配置ID
|
|
* @return 配置详情
|
|
*/
|
|
@GetMapping("/model-config/{id}")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> getConfigDetail(@PathVariable("id") Long id) {
|
|
try {
|
|
AiModelConfig config = aiModelConfigService.getConfigDetail(id);
|
|
if (config == null) {
|
|
return ResponseEntity.status(404).body(Map.of(
|
|
"success", false,
|
|
"message", "配置不存在"
|
|
));
|
|
}
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", config
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "查询失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 获取活跃配置 ====================
|
|
|
|
/**
|
|
* 获取指定应用类型的活跃配置(API Key 脱敏)
|
|
*
|
|
* @param appType 应用类型
|
|
* @return 活跃配置
|
|
*/
|
|
@GetMapping("/model-config/active/{appType}")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> getActiveConfig(@PathVariable("appType") String appType) {
|
|
try {
|
|
AiModelConfig config = aiModelConfigService.getActiveConfig(appType);
|
|
if (config == null) {
|
|
return ResponseEntity.status(404).body(Map.of(
|
|
"success", false,
|
|
"message", "该应用类型无活跃配置"
|
|
));
|
|
}
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", config
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "查询失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 新建配置 ====================
|
|
|
|
/**
|
|
* 新建模型配置
|
|
*
|
|
* @param config 配置对象
|
|
* @return 创建结果
|
|
*/
|
|
@PostMapping("/model-config")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> createConfig(@RequestBody AiModelConfig config) {
|
|
try {
|
|
// 参数校验
|
|
if (config.getName() == null || config.getName().trim().isEmpty()) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "配置名称不能为空"
|
|
));
|
|
}
|
|
if (config.getAppType() == null || config.getAppType().trim().isEmpty()) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "应用类型不能为空"
|
|
));
|
|
}
|
|
if (config.getModelName() == null || config.getModelName().trim().isEmpty()) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "模型名称不能为空"
|
|
));
|
|
}
|
|
if (config.getApiKey() == null || config.getApiKey().trim().isEmpty()) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "API Key 不能为空"
|
|
));
|
|
}
|
|
if (config.getProvider() == null || config.getProvider().trim().isEmpty()) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "提供商不能为空"
|
|
));
|
|
}
|
|
|
|
AiModelConfig created = aiModelConfigService.createConfig(config);
|
|
refreshCache();
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", created,
|
|
"message", "配置创建成功"
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "创建失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 更新配置 ====================
|
|
|
|
/**
|
|
* 更新模型配置
|
|
*
|
|
* @param id 配置ID
|
|
* @param config 更新内容
|
|
* @return 更新结果
|
|
*/
|
|
@PutMapping("/model-config/{id}")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> updateConfig(
|
|
@PathVariable("id") Long id,
|
|
@RequestBody AiModelConfig config) {
|
|
try {
|
|
AiModelConfig updated = aiModelConfigService.updateConfig(id, config);
|
|
refreshCache();
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", updated,
|
|
"message", "配置更新成功"
|
|
));
|
|
} catch (RuntimeException e) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", e.getMessage()
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "更新失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 激活配置 ====================
|
|
|
|
/**
|
|
* 激活指定配置(同 app_type 互斥)
|
|
*
|
|
* @param id 配置ID
|
|
* @return 激活结果
|
|
*/
|
|
@PutMapping("/model-config/{id}/activate")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> activateConfig(@PathVariable("id") Long id) {
|
|
try {
|
|
aiModelConfigService.activateConfig(id);
|
|
refreshCache();
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"message", "配置已激活"
|
|
));
|
|
} catch (RuntimeException e) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", e.getMessage()
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "激活失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 删除配置 ====================
|
|
|
|
/**
|
|
* 删除配置(逻辑删除,活跃配置不可删)
|
|
*
|
|
* @param id 配置ID
|
|
* @return 删除结果
|
|
*/
|
|
@DeleteMapping("/model-config/{id}")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> deleteConfig(@PathVariable("id") Long id) {
|
|
try {
|
|
aiModelConfigService.deleteConfig(id);
|
|
refreshCache();
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"message", "配置删除成功"
|
|
));
|
|
} catch (RuntimeException e) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", e.getMessage()
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "删除失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== F1: 连接测试 ====================
|
|
|
|
/**
|
|
* 测试指定配置的连接可用性
|
|
* 根据配置构建临时 ChatModel / EmbeddingModel,发送简单请求验证 API Key 和模型是否可访问
|
|
*
|
|
* @param id 配置ID
|
|
* @return 测试结果:{success, latencyMs, errorMessage?, dimensions?}
|
|
*/
|
|
@PostMapping("/model-config/{id}/test")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> testConnection(@PathVariable("id") Long id) {
|
|
try {
|
|
AiModelConfig config = aiModelConfigService.getConfigWithFullKey(id);
|
|
if (config == null) {
|
|
return ResponseEntity.status(404).body(Map.of(
|
|
"success", false, "message", "配置不存在"));
|
|
}
|
|
|
|
Map<String, Object> result;
|
|
String appType = config.getAppType();
|
|
|
|
if ("EMBEDDING".equals(appType)) {
|
|
result = embeddingModelFactory.testConnection(config);
|
|
} else if ("RERANK".equals(appType)) {
|
|
// RERANK 类型暂无通用测试方式
|
|
result = Map.of("success", false, "latencyMs", 0,
|
|
"errorMessage", "RERANK 类型暂不支持连接测试");
|
|
} else {
|
|
// CHAT / RAG_REWRITE 均使用 ChatModel 测试
|
|
result = chatModelFactory.testConnection(config);
|
|
}
|
|
return ResponseEntity.ok(result);
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false, "message", "测试失败:" + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
// ==================== F2: 配置复制 ====================
|
|
|
|
/**
|
|
* 复制指定配置(快速克隆)
|
|
* 复制所有字段,name 加 " - 副本" 后缀,isActive 强制 false
|
|
*
|
|
* @param id 源配置ID
|
|
* @return 新创建的副本配置
|
|
*/
|
|
@PostMapping("/model-config/{id}/duplicate")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> duplicateConfig(@PathVariable("id") Long id) {
|
|
try {
|
|
AiModelConfig duplicated = aiModelConfigService.duplicateConfig(id);
|
|
refreshCache();
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", duplicated,
|
|
"message", "配置复制成功"
|
|
));
|
|
} catch (RuntimeException e) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false, "message", e.getMessage()));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false, "message", "复制失败:" + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
// ==================== F4: 健康状态 ====================
|
|
|
|
/**
|
|
* 批量获取所有配置的健康状态
|
|
*
|
|
* @return 各配置的健康状态:{configId: {status, latencyMs, lastCheckTime, errorMessage}}
|
|
*/
|
|
@GetMapping("/model-config/health")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> getHealthStatus() {
|
|
try {
|
|
Map<String, Object> healthData = modelHealthService.getAllHealthStatus();
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", healthData
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false, "message", "查询失败:" + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 手动触发单个配置的健康检查
|
|
*/
|
|
@PostMapping("/model-config/{id}/health-check")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> manualHealthCheck(@PathVariable("id") Long id) {
|
|
try {
|
|
Map<String, Object> result = modelHealthService.checkHealth(id);
|
|
return ResponseEntity.ok(Map.of("success", true, "data", result));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false, "message", "检查失败:" + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
// ==================== F6: 优先级管理 ====================
|
|
|
|
/**
|
|
* 更新配置优先级(用于 Fallback 链排序)
|
|
*/
|
|
@PutMapping("/model-config/{id}/priority")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> updatePriority(
|
|
@PathVariable("id") Long id,
|
|
@RequestBody Map<String, Integer> body) {
|
|
try {
|
|
Integer priority = body.get("priority");
|
|
if (priority == null) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false, "message", "priority 参数不能为空"));
|
|
}
|
|
aiModelConfigService.updatePriority(id, priority);
|
|
refreshCache();
|
|
return ResponseEntity.ok(Map.of("success", true, "message", "优先级更新成功"));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false, "message", "更新失败:" + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 停用配置(从 Fallback 链中移除,但不删除)
|
|
*/
|
|
@PutMapping("/model-config/{id}/deactivate")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> deactivateConfig(@PathVariable("id") Long id) {
|
|
try {
|
|
aiModelConfigService.deactivateConfig(id);
|
|
refreshCache();
|
|
return ResponseEntity.ok(Map.of("success", true, "message", "配置已停用"));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false, "message", "停用失败:" + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
// ==================== F7: 配置导入/导出 ====================
|
|
|
|
/**
|
|
* 导出所有配置为 JSON(API Key 始终脱敏)
|
|
*/
|
|
@GetMapping("/model-config/export")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> exportConfigs() {
|
|
try {
|
|
List<AiModelConfig> configs = aiModelConfigService.exportConfigs(true);
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", configs,
|
|
"total", configs.size()
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false, "message", "导出失败:" + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 批量导入配置
|
|
*
|
|
* @param body 请求体:{configs: [...], onConflict: "skip" | "overwrite"}
|
|
*/
|
|
@PostMapping("/model-config/import")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> importConfigs(@RequestBody Map<String, Object> body) {
|
|
try {
|
|
@SuppressWarnings("unchecked")
|
|
List<Map<String, Object>> rawConfigs = (List<Map<String, Object>>) body.get("configs");
|
|
if (rawConfigs == null || rawConfigs.isEmpty()) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false, "message", "导入数据不能为空"));
|
|
}
|
|
if (rawConfigs.size() > 100) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false, "message", "单次导入不能超过 100 条配置"));
|
|
}
|
|
|
|
String onConflict = body.getOrDefault("onConflict", "skip").toString();
|
|
|
|
// 将 Map 转换为 AiModelConfig 实体
|
|
List<AiModelConfig> configs = new java.util.ArrayList<>();
|
|
for (Map<String, Object> raw : rawConfigs) {
|
|
configs.add(OBJECT_MAPPER.convertValue(raw, AiModelConfig.class));
|
|
}
|
|
|
|
Map<String, Object> result = aiModelConfigService.importConfigs(configs, onConflict);
|
|
refreshCache();
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", result,
|
|
"message", "导入完成"
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false, "message", "导入失败:" + e.getMessage()));
|
|
}
|
|
}
|
|
|
|
// ==================== 获取模型列表(动态发现) ====================
|
|
|
|
/**
|
|
* 通过提供商 API 获取可用模型列表
|
|
* 前端填入 API Key + API 地址后,可调用此接口自动发现可用模型,减少手动输入
|
|
*
|
|
* @param body 请求体:{ baseUrl?, apiKey, provider? }
|
|
* @return 模型列表 [{ id, owned_by }]
|
|
*/
|
|
@PostMapping("/model-config/fetch-models")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> fetchModels(@RequestBody Map<String, String> 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()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 缓存刷新 ====================
|
|
|
|
/**
|
|
* 刷新 ChatModel 缓存
|
|
* 在配置变更(增删改激活)后调用,确保下次对话使用最新配置
|
|
*/
|
|
private void refreshCache() {
|
|
chatModelFactory.clearCache();
|
|
embeddingModelFactory.clearCache();
|
|
assistantApp.clearCache();
|
|
}
|
|
}
|