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.
418 lines
16 KiB
418 lines
16 KiB
package com.wok.supportbot.controller;
|
|
|
|
import com.wok.supportbot.app.AssistantApp;
|
|
import com.wok.supportbot.config.McpClientManager;
|
|
import com.wok.supportbot.entity.McpServerConfig;
|
|
import com.wok.supportbot.mcp.McpToolCallbackAdapter;
|
|
import com.wok.supportbot.service.McpServerConfigService;
|
|
import lombok.extern.slf4j.Slf4j;
|
|
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.ArrayList;
|
|
import java.util.LinkedHashMap;
|
|
import java.util.List;
|
|
import java.util.Map;
|
|
|
|
/**
|
|
* MCP Server 配置管理控制器
|
|
* 提供 MCP Server 配置的增删改查、启用/禁用、连接测试、缓存刷新等 API
|
|
*/
|
|
@RestController
|
|
@Slf4j
|
|
public class McpServerConfigController {
|
|
|
|
@Autowired
|
|
private McpServerConfigService mcpServerConfigService;
|
|
|
|
@Autowired
|
|
private McpClientManager mcpClientManager;
|
|
|
|
@Autowired
|
|
private AssistantApp assistantApp;
|
|
|
|
// ==================== 分页列表 ====================
|
|
|
|
/**
|
|
* 获取 MCP Server 配置列表(分页)
|
|
*
|
|
* @param page 页码(默认1)
|
|
* @param size 每页大小(默认10)
|
|
* @return 分页配置列表
|
|
*/
|
|
@GetMapping("/mcp-server/list")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> listConfigs(
|
|
@RequestParam(defaultValue = "1") int page,
|
|
@RequestParam(defaultValue = "10") int size,
|
|
@RequestParam(required = false) String sortField,
|
|
@RequestParam(required = false) String sortOrder) {
|
|
try {
|
|
Map<String, Object> result = mcpServerConfigService.listConfigs(page, size, sortField, sortOrder);
|
|
Map<String, Object> data = new 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()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 单条详情 ====================
|
|
|
|
/**
|
|
* 获取单条 MCP Server 配置详情
|
|
*
|
|
* @param id 配置ID
|
|
* @return 配置详情
|
|
*/
|
|
@GetMapping("/mcp-server/{id}")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> getConfigDetail(@PathVariable("id") Long id) {
|
|
try {
|
|
Map<String, Object> config = mcpServerConfigService.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()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 可用工具列表 ====================
|
|
|
|
/**
|
|
* 获取所有已启用 MCP Server 暴露的工具清单(按 Server 分组)。
|
|
* 工具名为带 mcp_ 前缀的注册名,与角色授权(allowed_mcp_tools)存储值一致,供「客服角色管理」授权 UI 使用。
|
|
*
|
|
* @return [{ configId, serverName, transportType, tools: [{ name, rawName, description }] }]
|
|
*/
|
|
@GetMapping("/mcp-server/tools")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> listAvailableTools() {
|
|
try {
|
|
List<Map<String, Object>> servers = mcpClientManager.listAvailableTools();
|
|
List<Map<String, Object>> result = new ArrayList<>();
|
|
for (Map<String, Object> server : servers) {
|
|
Map<String, Object> out = new LinkedHashMap<>();
|
|
out.put("configId", server.get("config_id"));
|
|
out.put("serverName", server.get("name"));
|
|
out.put("transportType", server.get("transport_type"));
|
|
|
|
List<Map<String, Object>> tools = new ArrayList<>();
|
|
@SuppressWarnings("unchecked")
|
|
List<Map<String, Object>> rawTools = (List<Map<String, Object>>) server.get("tools");
|
|
if (rawTools != null) {
|
|
String configId = (String) server.get("config_id");
|
|
for (Map<String, Object> tool : rawTools) {
|
|
String rawName = (String) tool.get("name");
|
|
Map<String, Object> toolOut = new LinkedHashMap<>();
|
|
toolOut.put("name", McpToolCallbackAdapter.buildToolName(configId, rawName));
|
|
toolOut.put("rawName", rawName);
|
|
toolOut.put("description", tool.get("description"));
|
|
tools.add(toolOut);
|
|
}
|
|
}
|
|
out.put("tools", tools);
|
|
result.add(out);
|
|
}
|
|
return ResponseEntity.ok(Map.of("success", true, "data", result));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "获取工具列表失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 新建配置 ====================
|
|
|
|
/**
|
|
* 新建 MCP Server 配置
|
|
*
|
|
* @param config 配置对象
|
|
* @return 创建结果
|
|
*/
|
|
@PostMapping("/mcp-server")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> createConfig(@RequestBody McpServerConfig config) {
|
|
try {
|
|
Map<String, Object> created = mcpServerConfigService.createConfig(config);
|
|
// 增量操作:仅为新建配置创建客户端,不影响其他已有连接
|
|
Object idObj = created.get("id");
|
|
if (idObj != null) {
|
|
try {
|
|
mcpClientManager.addClient(Long.parseLong(idObj.toString()));
|
|
} catch (Exception e) {
|
|
log.warn("新建配置后创建客户端失败(不影响配置保存): {}", e.getMessage());
|
|
}
|
|
// MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集
|
|
assistantApp.clearCache();
|
|
}
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", created,
|
|
"message", "配置创建成功"
|
|
));
|
|
} catch (IllegalArgumentException 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()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 更新配置 ====================
|
|
|
|
/**
|
|
* 更新 MCP Server 配置
|
|
*
|
|
* @param id 配置ID
|
|
* @param config 更新内容
|
|
* @return 更新结果
|
|
*/
|
|
@PutMapping("/mcp-server/{id}")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> updateConfig(
|
|
@PathVariable("id") Long id,
|
|
@RequestBody McpServerConfig config) {
|
|
try {
|
|
Map<String, Object> updated = mcpServerConfigService.updateConfig(id, config);
|
|
// 增量操作:仅重建更新的配置客户端,不影响其他已有连接
|
|
mcpClientManager.rebuildClient(id);
|
|
// MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集
|
|
assistantApp.clearCache();
|
|
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()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 删除配置 ====================
|
|
|
|
/**
|
|
* 删除 MCP Server 配置(逻辑删除)
|
|
*
|
|
* @param id 配置ID
|
|
* @return 删除结果
|
|
*/
|
|
@DeleteMapping("/mcp-server/{id}")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> deleteConfig(@PathVariable("id") Long id) {
|
|
try {
|
|
mcpServerConfigService.deleteConfig(id);
|
|
// 增量操作:仅移除被删除的配置客户端,不影响其他已有连接
|
|
mcpClientManager.removeClient(id);
|
|
// MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集
|
|
assistantApp.clearCache();
|
|
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
|
|
* @param body 请求体(可选):{active: true/false}
|
|
* @param active query 兜底参数(可选):兼容不带 body 的旧调用方(active 放 query string)
|
|
* @return 操作结果
|
|
*/
|
|
@PutMapping("/mcp-server/{id}/toggle")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> toggleActive(
|
|
@PathVariable("id") Long id,
|
|
@RequestBody(required = false) Map<String, Boolean> body,
|
|
@RequestParam(value = "active", required = false) Boolean activeParam) {
|
|
try {
|
|
Boolean active = (body != null) ? body.get("active") : null;
|
|
if (active == null) {
|
|
active = activeParam;
|
|
}
|
|
if (active == null) {
|
|
return ResponseEntity.badRequest().body(Map.of(
|
|
"success", false,
|
|
"message", "active 参数不能为空"
|
|
));
|
|
}
|
|
mcpServerConfigService.toggleActive(id, active);
|
|
// 增量操作:启用时重建客户端,禁用时移除客户端
|
|
if (active) {
|
|
mcpClientManager.rebuildClient(id);
|
|
} else {
|
|
mcpClientManager.disableClient(id);
|
|
}
|
|
// MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集
|
|
assistantApp.clearCache();
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"message", active ? "配置已启用" : "配置已禁用"
|
|
));
|
|
} 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()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 测试连接 ====================
|
|
|
|
/**
|
|
* 测试 MCP Server 连接
|
|
* 创建临时客户端进行握手 + 工具列表查询,不影响正常缓存
|
|
*
|
|
* @param id 配置ID
|
|
* @return 测试结果(success / latencyMs / toolsCount / errorMessage)
|
|
*/
|
|
@PostMapping("/mcp-server/{id}/test")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> testConnection(@PathVariable("id") Long id) {
|
|
try {
|
|
Map<String, Object> result = mcpClientManager.testConnection(id);
|
|
return ResponseEntity.ok(result);
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "测试失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 刷新连接 ====================
|
|
|
|
/**
|
|
* 刷新所有 MCP 客户端连接
|
|
* 清空现有缓存并重新建立所有已启用配置的客户端连接
|
|
*
|
|
* @return 刷新结果
|
|
*/
|
|
@PostMapping("/mcp-server/refresh")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> refreshAll() {
|
|
try {
|
|
mcpClientManager.refreshAll();
|
|
// MCP 工具清单可能变化,清空 AssistantApp 的 ChatClient 缓存,避免继续使用旧工具集
|
|
assistantApp.clearCache();
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"message", "MCP 客户端连接已刷新"
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "刷新失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
// ==================== 健康检查 ====================
|
|
|
|
/**
|
|
* 获取所有 MCP Server 的健康状态
|
|
* 返回各配置的在线/离线状态、延迟、最后检查时间等信息
|
|
*
|
|
* @return 所有 MCP Server 的健康状态
|
|
*/
|
|
@GetMapping("/mcp-server/health")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> getAllHealthStatus() {
|
|
try {
|
|
Map<String, McpClientManager.HealthStatus> healthMap = mcpClientManager.getAllHealthStatus();
|
|
Map<String, Object> data = new LinkedHashMap<>();
|
|
data.put("success", true);
|
|
data.put("data", healthMap);
|
|
return ResponseEntity.ok(data);
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "获取健康状态失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
|
|
/**
|
|
* 手动触发单个 MCP Server 的健康检查
|
|
* 对指定配置的客户端执行 listTools 探测,返回最新健康状态
|
|
*
|
|
* @param id 配置ID
|
|
* @return 该配置的健康状态
|
|
*/
|
|
@PostMapping("/mcp-server/{id}/health-check")
|
|
@PreAuthorize("hasRole('admin')")
|
|
public ResponseEntity<Map<String, Object>> checkSingleHealth(@PathVariable("id") Long id) {
|
|
try {
|
|
McpClientManager.HealthStatus status = mcpClientManager.checkHealthForConfig(id);
|
|
return ResponseEntity.ok(Map.of(
|
|
"success", true,
|
|
"data", Map.of(
|
|
"configId", id.toString(),
|
|
"status", status.status(),
|
|
"latencyMs", status.latencyMs(),
|
|
"lastCheckTime", status.lastCheckTime().toString(),
|
|
"errorMessage", status.errorMessage() != null ? status.errorMessage() : ""
|
|
)
|
|
));
|
|
} catch (Exception e) {
|
|
return ResponseEntity.status(500).body(Map.of(
|
|
"success", false,
|
|
"message", "健康检查失败:" + e.getMessage()
|
|
));
|
|
}
|
|
}
|
|
}
|