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> listConfigs( @RequestParam(defaultValue = "1") int page, @RequestParam(defaultValue = "10") int size, @RequestParam(required = false) String appType) { try { Map result = aiModelConfigService.listConfigs(appType, page, size); Map 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> 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> 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> 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> 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> 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> 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> 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 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> 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> getHealthStatus() { try { Map 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> manualHealthCheck(@PathVariable("id") Long id) { try { Map 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> updatePriority( @PathVariable("id") Long id, @RequestBody Map 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> 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> exportConfigs() { try { List 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> importConfigs(@RequestBody Map body) { try { @SuppressWarnings("unchecked") List> rawConfigs = (List>) 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 configs = new java.util.ArrayList<>(); for (Map raw : rawConfigs) { configs.add(OBJECT_MAPPER.convertValue(raw, AiModelConfig.class)); } Map 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> 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() )); } } // ==================== 缓存刷新 ==================== /** * 刷新 ChatModel 缓存 * 在配置变更(增删改激活)后调用,确保下次对话使用最新配置 */ private void refreshCache() { chatModelFactory.clearCache(); embeddingModelFactory.clearCache(); assistantApp.clearCache(); } }