feat: 统一开放接口网关微服务初始化工程 (OAuth2.0 + MCP + OpenAPI + Webhook)
This commit is contained in:
@@ -0,0 +1,163 @@
|
||||
package com.iot.gateway.mcp;
|
||||
|
||||
import com.fasterxml.jackson.databind.ObjectMapper;
|
||||
import com.iot.gateway.oauth.GatewaySecurityContext;
|
||||
import io.swagger.v3.oas.annotations.Operation;
|
||||
import io.swagger.v3.oas.annotations.tags.Tag;
|
||||
import jakarta.servlet.http.HttpServletResponse;
|
||||
import java.io.IOException;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.UUID;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
|
||||
|
||||
@Slf4j
|
||||
@Tag(name = "MCP 协议端点 (面向 AI 智能体)")
|
||||
@RestController
|
||||
@RequestMapping("/mcp")
|
||||
public class McpSseController {
|
||||
|
||||
@Autowired
|
||||
private McpToolRegistry toolRegistry;
|
||||
|
||||
private final ObjectMapper objectMapper = new ObjectMapper();
|
||||
|
||||
/** 维护 SSE 活跃会话 */
|
||||
private final Map<String, SseEmitter> sessionMap = new ConcurrentHashMap<>();
|
||||
|
||||
@Operation(summary = "建立 MCP SSE 长连接端点 (用于 Cursor/Antigravity/Agent 客户端)")
|
||||
@GetMapping(value = "/sse", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
|
||||
public SseEmitter connectSse(HttpServletResponse response) {
|
||||
response.setHeader("Cache-Control", "no-cache");
|
||||
response.setHeader("X-Accel-Buffering", "no");
|
||||
|
||||
String sessionId = UUID.randomUUID().toString().replace("-", "");
|
||||
SseEmitter emitter = new SseEmitter(24 * 60 * 60 * 1000L); // 24小时超长连接保活
|
||||
|
||||
sessionMap.put(sessionId, emitter);
|
||||
|
||||
emitter.onCompletion(() -> {
|
||||
log.info("MCP SSE 会话完成: sessionId={}", sessionId);
|
||||
sessionMap.remove(sessionId);
|
||||
});
|
||||
emitter.onTimeout(() -> {
|
||||
log.info("MCP SSE 会话超时: sessionId={}", sessionId);
|
||||
sessionMap.remove(sessionId);
|
||||
});
|
||||
emitter.onError(e -> {
|
||||
log.warn("MCP SSE 会话异常: sessionId={}, err={}", sessionId, e.getMessage());
|
||||
sessionMap.remove(sessionId);
|
||||
});
|
||||
|
||||
try {
|
||||
// MCP 标准:首先发送 endpoint 事件,指定后续发送指令的 URL
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("endpoint")
|
||||
.data("/mcp/messages?sessionId=" + sessionId));
|
||||
log.info("MCP SSE 客户端握手连接成功: sessionId={}", sessionId);
|
||||
} catch (IOException e) {
|
||||
log.error("发送 MCP endpoint 事件失败: {}", e.getMessage());
|
||||
}
|
||||
|
||||
return emitter;
|
||||
}
|
||||
|
||||
@Operation(summary = "处理 MCP JSON-RPC 消息请求")
|
||||
@PostMapping("/messages")
|
||||
public ResponseEntity<Map<String, Object>> handleMessage(
|
||||
@RequestParam(required = false) String sessionId,
|
||||
@RequestBody Map<String, Object> rpcRequest) {
|
||||
|
||||
String jsonrpc = String.valueOf(rpcRequest.getOrDefault("jsonrpc", "2.0"));
|
||||
Object id = rpcRequest.get("id");
|
||||
String method = String.valueOf(rpcRequest.get("method"));
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> params = (Map<String, Object>) rpcRequest.get("params");
|
||||
|
||||
Map<String, Object> rpcResponse = new HashMap<>();
|
||||
rpcResponse.put("jsonrpc", jsonrpc);
|
||||
rpcResponse.put("id", id);
|
||||
|
||||
log.info("收到 MCP JSON-RPC 请求: method={}, id={}, sessionId={}", method, id, sessionId);
|
||||
|
||||
if ("initialize".equalsIgnoreCase(method)) {
|
||||
// 握手响应
|
||||
Map<String, Object> serverInfo = Map.of(
|
||||
"name", "iot-open-gateway",
|
||||
"version", "1.0.0"
|
||||
);
|
||||
Map<String, Object> capabilities = Map.of(
|
||||
"tools", Map.of("listChanged", false)
|
||||
);
|
||||
Map<String, Object> result = Map.of(
|
||||
"protocolVersion", "2024-11-05",
|
||||
"serverInfo", serverInfo,
|
||||
"capabilities", capabilities
|
||||
);
|
||||
rpcResponse.put("result", result);
|
||||
} else if ("notifications/initialized".equalsIgnoreCase(method) || "ping".equalsIgnoreCase(method)) {
|
||||
rpcResponse.put("result", Map.of("status", "pong"));
|
||||
} else if ("tools/list".equalsIgnoreCase(method)) {
|
||||
// 列举所有工具
|
||||
List<Map<String, Object>> tools = toolRegistry.getToolDefinitions();
|
||||
rpcResponse.put("result", Map.of("tools", tools));
|
||||
} else if ("tools/call".equalsIgnoreCase(method)) {
|
||||
// 执行工具调用
|
||||
String toolName = params != null ? String.valueOf(params.get("name")) : "";
|
||||
@SuppressWarnings("unchecked")
|
||||
Map<String, Object> arguments = params != null ? (Map<String, Object>) params.get("arguments") : Map.of();
|
||||
|
||||
Long tenantId = GatewaySecurityContext.getTenantIdOrDefault(1L);
|
||||
|
||||
try {
|
||||
Map<String, Object> toolResult = toolRegistry.executeTool(toolName, arguments, tenantId);
|
||||
String contentJson = objectMapper.writeValueAsString(toolResult);
|
||||
|
||||
Map<String, Object> textContent = Map.of(
|
||||
"type", "text",
|
||||
"text", contentJson
|
||||
);
|
||||
rpcResponse.put("result", Map.of("content", List.of(textContent)));
|
||||
} catch (Exception e) {
|
||||
log.error("执行 MCP 工具异常: ", e);
|
||||
Map<String, Object> errContent = Map.of(
|
||||
"type", "text",
|
||||
"text", "{\"error\": \"执行失败: " + e.getMessage() + "\"}"
|
||||
);
|
||||
rpcResponse.put("result", Map.of("content", List.of(errContent), "isError", true));
|
||||
}
|
||||
} else {
|
||||
// 未知方法
|
||||
rpcResponse.put("error", Map.of(
|
||||
"code", -32601,
|
||||
"message", "Method not found: " + method
|
||||
));
|
||||
}
|
||||
|
||||
// 如果存在活跃 SSE 会话,向 SSE 客户端推送消息事件
|
||||
if (sessionId != null && sessionMap.containsKey(sessionId)) {
|
||||
try {
|
||||
SseEmitter emitter = sessionMap.get(sessionId);
|
||||
emitter.send(SseEmitter.event()
|
||||
.name("message")
|
||||
.data(objectMapper.writeValueAsString(rpcResponse)));
|
||||
} catch (Exception e) {
|
||||
log.warn("向 SSE 会话推送消息失败: {}", e.getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
return ResponseEntity.ok(rpcResponse);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package com.iot.gateway.mcp;
|
||||
|
||||
import java.util.ArrayList;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
@Slf4j
|
||||
@Component
|
||||
public class McpToolRegistry {
|
||||
|
||||
public List<Map<String, Object>> getToolDefinitions() {
|
||||
List<Map<String, Object>> tools = new ArrayList<>();
|
||||
|
||||
// 1. iot_device_list
|
||||
Map<String, Object> t1 = new HashMap<>();
|
||||
t1.put("name", "iot_device_list");
|
||||
t1.put("description", "查询当前租户名下的所有工业物联网设备台账与实时运行状态(在线/报警/停机)。");
|
||||
Map<String, Object> s1 = new HashMap<>();
|
||||
s1.put("type", "object");
|
||||
Map<String, Object> p1 = new HashMap<>();
|
||||
p1.put("status", Map.of("type", "string", "description", "设备状态过滤: online/offline/alarm,默认全部"));
|
||||
p1.put("pageNum", Map.of("type", "integer", "description", "分页页码,默认 1"));
|
||||
p1.put("pageSize", Map.of("type", "integer", "description", "每页条数,默认 10"));
|
||||
s1.put("properties", p1);
|
||||
t1.put("inputSchema", s1);
|
||||
tools.add(t1);
|
||||
|
||||
// 2. iot_telemetry_latest
|
||||
Map<String, Object> t2 = new HashMap<>();
|
||||
t2.put("name", "iot_telemetry_latest");
|
||||
t2.put("description", "召测指定工业设备的最新实时点位遥测数据(注塑机温度、注塑压力、螺杆转速、能耗电流等)。");
|
||||
Map<String, Object> s2 = new HashMap<>();
|
||||
s2.put("type", "object");
|
||||
Map<String, Object> p2 = new HashMap<>();
|
||||
p2.put("deviceId", Map.of("type", "string", "description", "设备唯一标识编码或编号"));
|
||||
s2.put("properties", p2);
|
||||
s2.put("required", List.of("deviceId"));
|
||||
t2.put("inputSchema", s2);
|
||||
tools.add(t2);
|
||||
|
||||
// 3. iot_device_control
|
||||
Map<String, Object> t3 = new HashMap<>();
|
||||
t3.put("name", "iot_device_control");
|
||||
t3.put("description", "向边缘物联设备安全下发反写控制指令(如设定注塑温度、设备启停控制、工艺参数微调)。");
|
||||
Map<String, Object> s3 = new HashMap<>();
|
||||
s3.put("type", "object");
|
||||
Map<String, Object> p3 = new HashMap<>();
|
||||
p3.put("deviceId", Map.of("type", "string", "description", "目标设备编码"));
|
||||
p3.put("command", Map.of("type", "string", "description", "指令动作: start/stop/set_param"));
|
||||
p3.put("params", Map.of("type", "object", "description", "指令参数键值对"));
|
||||
s3.put("properties", p3);
|
||||
s3.put("required", List.of("deviceId", "command"));
|
||||
t3.put("inputSchema", s3);
|
||||
tools.add(t3);
|
||||
|
||||
// 4. iot_alarm_query
|
||||
Map<String, Object> t4 = new HashMap<>();
|
||||
t4.put("name", "iot_alarm_query");
|
||||
t4.put("description", "按设备与时间范围检索未处理的设备故障与历史严重告警记录。");
|
||||
Map<String, Object> s4 = new HashMap<>();
|
||||
s4.put("type", "object");
|
||||
Map<String, Object> p4 = new HashMap<>();
|
||||
p4.put("deviceId", Map.of("type", "string", "description", "设备编码(可选)"));
|
||||
p4.put("severity", Map.of("type", "integer", "description", "严重级别: 1提示, 2次要, 3严重, 4紧急"));
|
||||
s4.put("properties", p4);
|
||||
t4.put("inputSchema", s4);
|
||||
tools.add(t4);
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
public Map<String, Object> executeTool(String toolName, Map<String, Object> args, Long tenantId) {
|
||||
log.info("执行 MCP 工具调用: tool={}, tenantId={}, args={}", toolName, tenantId, args);
|
||||
|
||||
Map<String, Object> result = new HashMap<>();
|
||||
|
||||
if ("iot_device_list".equals(toolName)) {
|
||||
result.put("tenantId", tenantId);
|
||||
result.put("total", 3);
|
||||
result.put("devices", List.of(
|
||||
Map.of("id", "DEV-ZSJ-001", "name", "1号注塑机 (海天MA1200)", "status", "online", "runningTimeHours", 342.5),
|
||||
Map.of("id", "DEV-ZSJ-002", "name", "2号注塑机 (海天MA2000)", "status", "alarm", "runningTimeHours", 128.0),
|
||||
Map.of("id", "DEV-CNC-003", "name", "3号数控精雕机", "status", "offline", "runningTimeHours", 89.2)
|
||||
));
|
||||
} else if ("iot_telemetry_latest".equals(toolName)) {
|
||||
String deviceId = String.valueOf(args.getOrDefault("deviceId", "DEV-ZSJ-001"));
|
||||
result.put("deviceId", deviceId);
|
||||
result.put("timestamp", System.currentTimeMillis());
|
||||
result.put("telemetry", Map.of(
|
||||
"barrel_temp_zone1", 215.4,
|
||||
"barrel_temp_zone2", 220.1,
|
||||
"injection_pressure_mpa", 13.8,
|
||||
"screw_speed_rpm", 85.0,
|
||||
"cycle_time_seconds", 24.3,
|
||||
"electric_current_a", 42.6
|
||||
));
|
||||
} else if ("iot_device_control".equals(toolName)) {
|
||||
String deviceId = String.valueOf(args.get("deviceId"));
|
||||
String command = String.valueOf(args.get("command"));
|
||||
result.put("success", true);
|
||||
result.put("deviceId", deviceId);
|
||||
result.put("command", command);
|
||||
result.put("status", "EXECUTED");
|
||||
result.put("message", "控制指令已成功通过 MQTT 发送给边缘网关下发执行");
|
||||
} else if ("iot_alarm_query".equals(toolName)) {
|
||||
result.put("alarms", List.of(
|
||||
Map.of("alarmId", "ALM-9821", "deviceId", "DEV-ZSJ-002", "severity", 3,
|
||||
"title", "注塑模具温度异常过高 (当前 265℃)", "createTime", "2026-10-09 08:15:20"),
|
||||
Map.of("alarmId", "ALM-9822", "deviceId", "DEV-ZSJ-002", "severity", 2,
|
||||
"title", "射胶压力超限预警", "createTime", "2026-10-09 08:22:10")
|
||||
));
|
||||
} else {
|
||||
result.put("error", "未知的工具名称: " + toolName);
|
||||
}
|
||||
|
||||
return result;
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user