本地 RAG 知识库
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

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();
}
}