From 411ff551ae7641dfc5c9331e99bf8b6e5770e2fa Mon Sep 17 00:00:00 2001
From: Junjie <fallin.jie@qq.com>
Date: 星期二, 30 十二月 2025 18:05:14 +0800
Subject: [PATCH] #mcp

---
 src/main/java/com/zy/ai/mcp/dto/ToolDefinition.java             |   25 +
 src/main/java/com/zy/ai/mcp/dto/JsonRpcResponse.java            |   25 +
 src/main/java/com/zy/ai/service/WcsQueryService.java            |  129 ++++++++
 src/main/java/com/zy/ai/mcp/dto/McpToolHandler.java             |    7 
 src/main/java/com/zy/ai/mcp/service/impl/WcsDataFacadeImpl.java |  324 ++++++++++++++++++++
 src/main/java/com/zy/ai/mcp/controller/McpController.java       |   90 +++++
 src/main/java/com/zy/ai/mcp/dto/ToolRegistry.java               |   30 +
 src/main/java/com/zy/ai/mcp/service/WcsDataFacade.java          |   25 +
 src/main/java/com/zy/ai/mcp/config/McpToolsBootstrap.java       |  232 ++++++++++++++
 src/main/java/com/zy/ai/mcp/dto/JsonRpcError.java               |   16 +
 src/main/java/com/zy/ai/mcp/dto/JsonRpcRequest.java             |   13 
 11 files changed, 916 insertions(+), 0 deletions(-)

diff --git a/src/main/java/com/zy/ai/mcp/config/McpToolsBootstrap.java b/src/main/java/com/zy/ai/mcp/config/McpToolsBootstrap.java
new file mode 100644
index 0000000..b15119f
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/config/McpToolsBootstrap.java
@@ -0,0 +1,232 @@
+package com.zy.ai.mcp.config;
+
+import com.alibaba.fastjson.JSONObject;
+import com.zy.ai.mcp.dto.McpToolHandler;
+import com.zy.ai.mcp.dto.ToolDefinition;
+import com.zy.ai.mcp.dto.ToolRegistry;
+import com.zy.ai.mcp.service.WcsDataFacade;
+
+import java.util.*;
+
+public class McpToolsBootstrap {
+
+    public static void registerAll(ToolRegistry registry, final WcsDataFacade facade) {
+
+        registry.register(tool(
+                "device.get_crn_status",
+                "Query realtime status of a crn device by deviceNo.",
+                schemaObj(
+                        propInt("crnNos", true)
+                ),
+                schemaObj(
+                        propObj("devices", true)
+                ),
+                new McpToolHandler() {
+                    public Object handle(JSONObject args) {
+                        return facade.getCrnDeviceStatus(args);
+                    }
+                }
+        ));
+
+        registry.register(tool(
+                "device.get_station_status",
+                "Query realtime status of a station device",
+                schemaObj(
+
+                ),
+                schemaObj(
+                        propObj("stations", true)
+                ),
+                new McpToolHandler() {
+                    public Object handle(JSONObject args) {
+                        return facade.getStationDeviceStatus(args);
+                    }
+                }
+        ));
+
+        registry.register(tool(
+                "device.get_rgv_status",
+                "Query realtime status of a rgv device by deviceNo.",
+                schemaObj(
+                        propInt("rgvNos", true)
+                ),
+                schemaObj(
+                        propObj("devices", true)
+                ),
+                new McpToolHandler() {
+                    public Object handle(JSONObject args) {
+                        return facade.getRgvDeviceStatus(args);
+                    }
+                }
+        ));
+
+        registry.register(tool(
+                "task.list",
+                "List tasks by filters (status/CrnDevice/RgvDevice//time window).",
+                schemaObj(
+                        propInt("crnNo", false),
+                        propInt("rgvNo", false),
+                        propArr("taskNos", false, "integer"),
+                        propInt("limit", false)
+                ),
+                schemaObj(propArr("tasks", true, "object")),
+                new McpToolHandler() {
+                    public Object handle(JSONObject args) {
+                        return facade.getTasks(args);
+                    }
+                }
+        ));
+
+        registry.register(tool(
+                "log.query",
+                "Query logs by keyword/level/time window/device/task. Return clipped log lines.",
+                schemaObj(
+                        propInt("limit", false)
+                ),
+                schemaObj(propArr("logs", true, "object")),
+                new McpToolHandler() {
+                    public Object handle(JSONObject args) {
+                        return facade.getLogs(args);
+                    }
+                }
+        ));
+
+        registry.register(tool(
+                "config.get_device_config",
+                "Get device config by deviceCode.",
+                schemaObj(
+                        propArr("crnNos", false, "integer"),
+                        propArr("rgvNos", false, "integer"),
+                        propArr("devpNos", false, "integer")
+                ),
+                schemaObj(propObj("deviceConfigs", true)),
+                new McpToolHandler() {
+                    public Object handle(JSONObject args) {
+                        return facade.getDeviceConfig(args);
+                    }
+                }
+        ));
+
+        registry.register(tool(
+                "config.get_system_config",
+                "Get key system configs for diagnosis.",
+                schemaObj(
+
+                ),
+                schemaObj(
+                        propObj("systemConfigs", true)
+                ),
+                new McpToolHandler() {
+                    public Object handle(JSONObject args) {
+                        return facade.getSystemConfig(args);
+                    }
+                }
+        ));
+
+//        // 鈽� 璇婃柇鑱氬悎蹇収锛氫竴娆℃嬁鍏�
+//        registry.register(tool(
+//                "build_diagnosis_snapshot",
+//                "Aggregate diagnosis snapshot: tasks + device realtime + configs + clipped logs for diagnosis.",
+//                schemaObj(
+//                        propStr("warehouseCode", true),
+//                        propArr("deviceCodes", false, "string"),     // 涓嶄紶鍒欐寜浠诲姟娑夊強璁惧鎺ㄥ
+//                        propStr("taskNo", false),
+//                        propStr("fromTime", true),
+//                        propStr("toTime", true),
+//                        propInt("taskLimit", false),
+//                        propInt("logLimit", false),
+//                        propArr("logKeywords", false, "string"),
+//                        propBool("includeSystemConfig", false),
+//                        propBool("includeDeviceConfig", false)
+//                ),
+//                schemaObj(
+//                        propObj("snapshot", true),
+//                        propArr("hints", false, "string")
+//                ),
+//                new McpToolHandler() {
+//                    public Object handle(JSONObject args) {
+//                        return facade.buildDiagnosisSnapshot(args);
+//                    }
+//                }
+//        ));
+    }
+
+    // ---------- schema helpers ----------
+    private static ToolDefinition tool(String name, String desc,
+                                       Map<String, Object> in, Map<String, Object> out,
+                                       McpToolHandler handler) {
+        ToolDefinition d = new ToolDefinition();
+        d.setName(name);
+        d.setDescription(desc);
+        d.setInputSchema(in);
+        d.setOutputSchema(out);
+        d.setHandler(handler);
+        return d;
+    }
+
+    private static Map<String, Object> schemaObj(Object... props) {
+        Map<String, Object> m = new LinkedHashMap<String, Object>();
+        m.put("type", "object");
+
+        Map<String, Object> properties = new LinkedHashMap<String, Object>();
+        List<String> required = new ArrayList<String>();
+
+        for (Object p : props) {
+            @SuppressWarnings("unchecked")
+            Map<String, Object> pm = (Map<String, Object>) p;
+            String name = String.valueOf(pm.get("name"));
+            boolean req = Boolean.TRUE.equals(pm.get("required"));
+            pm.remove("name");
+            pm.remove("required");
+            properties.put(name, pm);
+            if (req) required.add(name);
+        }
+
+        m.put("properties", properties);
+        if (!required.isEmpty()) m.put("required", required);
+        return m;
+    }
+
+    private static Map<String, Object> propStr(String name, boolean required) {
+        Map<String, Object> m = new LinkedHashMap<String, Object>();
+        m.put("name", name);
+        m.put("required", required);
+        m.put("type", "string");
+        return m;
+    }
+
+    private static Map<String, Object> propInt(String name, boolean required) {
+        Map<String, Object> m = new LinkedHashMap<String, Object>();
+        m.put("name", name);
+        m.put("required", required);
+        m.put("type", "integer");
+        return m;
+    }
+
+    private static Map<String, Object> propBool(String name, boolean required) {
+        Map<String, Object> m = new LinkedHashMap<String, Object>();
+        m.put("name", name);
+        m.put("required", required);
+        m.put("type", "boolean");
+        return m;
+    }
+
+    private static Map<String, Object> propObj(String name, boolean required) {
+        Map<String, Object> m = new LinkedHashMap<String, Object>();
+        m.put("name", name);
+        m.put("required", required);
+        m.put("type", "object");
+        return m;
+    }
+
+    private static Map<String, Object> propArr(String name, boolean required, String itemType) {
+        Map<String, Object> m = new LinkedHashMap<String, Object>();
+        m.put("name", name);
+        m.put("required", required);
+        m.put("type", "array");
+        Map<String, Object> items = new LinkedHashMap<String, Object>();
+        items.put("type", itemType);
+        m.put("items", items);
+        return m;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/mcp/controller/McpController.java b/src/main/java/com/zy/ai/mcp/controller/McpController.java
new file mode 100644
index 0000000..bf53c2e
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/controller/McpController.java
@@ -0,0 +1,90 @@
+package com.zy.ai.mcp.controller;
+
+import com.alibaba.fastjson.JSON;
+import com.alibaba.fastjson.JSONObject;
+import com.zy.ai.mcp.config.McpToolsBootstrap;
+import com.zy.ai.mcp.dto.JsonRpcRequest;
+import com.zy.ai.mcp.dto.JsonRpcResponse;
+import com.zy.ai.mcp.dto.ToolDefinition;
+import com.zy.ai.mcp.dto.ToolRegistry;
+import com.zy.ai.mcp.service.WcsDataFacade;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.*;
+
+import javax.annotation.PostConstruct;
+import java.util.*;
+
+@Slf4j
+@RestController
+@RequestMapping("/ai/mcp")
+public class McpController {
+
+    private final ToolRegistry registry = new ToolRegistry();
+
+    @Autowired
+    private WcsDataFacade wcsDataFacade;
+
+    public McpController(WcsDataFacade wcsDataFacade) {
+        this.wcsDataFacade = wcsDataFacade;
+    }
+
+    @PostConstruct
+    public void init() {
+        McpToolsBootstrap.registerAll(registry, wcsDataFacade);
+        log.info("MCP initialized, tools={}", registry.listTools().size());
+    }
+
+    @PostMapping(consumes = MediaType.APPLICATION_JSON_VALUE, produces = MediaType.APPLICATION_JSON_VALUE)
+    public Object handle(@RequestBody JsonRpcRequest req,
+                         @RequestHeader(value = "Authorization", required = false) String auth) {
+
+        // 锛堝缓璁級鍋氫竴涓畝鍗曢壌鏉冿細闃叉琚殢渚胯皟鐢ㄧ敓浜х郴缁�
+        // if (!"Bearer your-token".equals(auth)) return JsonRpcResponse.err(null, 401, "Unauthorized", null);
+
+        String id = req.getId();
+        String method = req.getMethod();
+        JSONObject params = JSON.parseObject(JSON.toJSONString(req.getParams()));
+
+        try {
+            if ("initialize".equals(method)) {
+                Map<String, Object> result = new LinkedHashMap<String, Object>();
+                result.put("serverName", "wcs-mcp");
+                result.put("serverVersion", "1.0.0");
+                result.put("capabilities", Arrays.asList("tools"));
+                return JsonRpcResponse.ok(id, result);
+            }
+
+            if ("tools/list".equals(method)) {
+                Map<String, Object> result = new LinkedHashMap<String, Object>();
+                result.put("tools", registry.listTools());
+                // cursor/paging 浣犲悗闈㈤渶瑕佸啀鍔�
+                return JsonRpcResponse.ok(id, result);
+            }
+
+            if ("tools/call".equals(method)) {
+                String toolName = params.getString("name");
+                JSONObject arguments = params.getJSONObject("arguments");
+                if (toolName == null || toolName.trim().isEmpty()) {
+                    return JsonRpcResponse.err(id, -32602, "Invalid params: missing tool name", null);
+                }
+                ToolDefinition def = registry.get(toolName);
+                if (def == null) {
+                    return JsonRpcResponse.err(id, -32601, "Method not found: tool " + toolName, null);
+                }
+                Object output = def.getHandler().handle(arguments == null ? new JSONObject() : arguments);
+
+                Map<String, Object> result = new LinkedHashMap<String, Object>();
+                result.put("content", output); // 浣犱篃鍙互鎸� MCP 甯歌杩斿洖缁撴瀯鍋� text/json 鍒嗘
+                return JsonRpcResponse.ok(id, result);
+            }
+
+            return JsonRpcResponse.err(id, -32601, "Method not found: " + method, null);
+
+        } catch (Exception e) {
+            log.error("MCP handle error, method={}, params={}", method, params, e);
+            return JsonRpcResponse.err(id, -32000, "Server error", e.getMessage());
+        }
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/mcp/dto/JsonRpcError.java b/src/main/java/com/zy/ai/mcp/dto/JsonRpcError.java
new file mode 100644
index 0000000..30da47b
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/dto/JsonRpcError.java
@@ -0,0 +1,16 @@
+package com.zy.ai.mcp.dto;
+
+import lombok.Data;
+
+@Data
+class JsonRpcError {
+    private int code;
+    private String message;
+    private Object data;
+
+    public JsonRpcError(int code, String message, Object data) {
+        this.code = code;
+        this.message = message;
+        this.data = data;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/mcp/dto/JsonRpcRequest.java b/src/main/java/com/zy/ai/mcp/dto/JsonRpcRequest.java
new file mode 100644
index 0000000..ee49c4e
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/dto/JsonRpcRequest.java
@@ -0,0 +1,13 @@
+package com.zy.ai.mcp.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+@Data
+public class JsonRpcRequest {
+    private String jsonrpc; // "2.0"
+    private String id;      // string/number 閮借锛岃繖閲岀敤 string
+    private String method;  // "tools/list" | "tools/call"
+    private Map<String, Object> params;
+}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/mcp/dto/JsonRpcResponse.java b/src/main/java/com/zy/ai/mcp/dto/JsonRpcResponse.java
new file mode 100644
index 0000000..7759350
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/dto/JsonRpcResponse.java
@@ -0,0 +1,25 @@
+package com.zy.ai.mcp.dto;
+
+import lombok.Data;
+
+@Data
+public class JsonRpcResponse {
+    private String jsonrpc = "2.0";
+    private String id;
+    private Object result;
+    private JsonRpcError error;
+
+    public static JsonRpcResponse ok(String id, Object result) {
+        JsonRpcResponse r = new JsonRpcResponse();
+        r.id = id;
+        r.result = result;
+        return r;
+    }
+
+    public static JsonRpcResponse err(String id, int code, String message, Object data) {
+        JsonRpcResponse r = new JsonRpcResponse();
+        r.id = id;
+        r.error = new JsonRpcError(code, message, data);
+        return r;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/mcp/dto/McpToolHandler.java b/src/main/java/com/zy/ai/mcp/dto/McpToolHandler.java
new file mode 100644
index 0000000..0671298
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/dto/McpToolHandler.java
@@ -0,0 +1,7 @@
+package com.zy.ai.mcp.dto;
+
+import com.alibaba.fastjson.JSONObject;
+
+public interface McpToolHandler {
+    Object handle(JSONObject arguments) throws Exception;
+}
diff --git a/src/main/java/com/zy/ai/mcp/dto/ToolDefinition.java b/src/main/java/com/zy/ai/mcp/dto/ToolDefinition.java
new file mode 100644
index 0000000..ead355f
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/dto/ToolDefinition.java
@@ -0,0 +1,25 @@
+package com.zy.ai.mcp.dto;
+
+
+import lombok.Data;
+
+import java.util.*;
+
+@Data
+public class ToolDefinition {
+    private String name;
+    private String description;
+    private Map<String, Object> inputSchema;   // JSON Schema as Map
+    private Map<String, Object> outputSchema;  // JSON Schema as Map
+    private McpToolHandler handler;
+
+    public Map<String, Object> toMcpToolJson() {
+        Map<String, Object> m = new LinkedHashMap<String, Object>();
+        m.put("name", name);
+        m.put("description", description);
+        m.put("inputSchema", inputSchema);
+        m.put("outputSchema", outputSchema);
+        return m;
+    }
+}
+
diff --git a/src/main/java/com/zy/ai/mcp/dto/ToolRegistry.java b/src/main/java/com/zy/ai/mcp/dto/ToolRegistry.java
new file mode 100644
index 0000000..1f44742
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/dto/ToolRegistry.java
@@ -0,0 +1,30 @@
+package com.zy.ai.mcp.dto;
+
+import java.util.*;
+import java.util.concurrent.ConcurrentHashMap;
+
+public class ToolRegistry {
+    private final Map<String, ToolDefinition> tools = new ConcurrentHashMap<String, ToolDefinition>();
+
+    public void register(ToolDefinition def) {
+        tools.put(def.getName(), def);
+    }
+
+    public List<Map<String, Object>> listTools() {
+        List<Map<String, Object>> list = new ArrayList<Map<String, Object>>();
+        for (ToolDefinition def : tools.values()) {
+            list.add(def.toMcpToolJson());
+        }
+        // 涓轰簡绋冲畾杈撳嚭锛屾寜 name 鎺掑簭
+        Collections.sort(list, new Comparator<Map<String, Object>>() {
+            public int compare(Map<String, Object> a, Map<String, Object> b) {
+                return String.valueOf(a.get("name")).compareTo(String.valueOf(b.get("name")));
+            }
+        });
+        return list;
+    }
+
+    public ToolDefinition get(String name) {
+        return tools.get(name);
+    }
+}
diff --git a/src/main/java/com/zy/ai/mcp/service/WcsDataFacade.java b/src/main/java/com/zy/ai/mcp/service/WcsDataFacade.java
new file mode 100644
index 0000000..26d459b
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/service/WcsDataFacade.java
@@ -0,0 +1,25 @@
+package com.zy.ai.mcp.service;
+
+import com.alibaba.fastjson.JSONObject;
+
+public interface WcsDataFacade {
+
+    Object getCrnDeviceStatus(JSONObject args);
+
+    Object getStationDeviceStatus(JSONObject args);
+
+    Object getRgvDeviceStatus(JSONObject args);
+
+    Object getTasks(JSONObject args);
+
+    Object getLogs(JSONObject args);
+
+    Object getDeviceConfig(JSONObject args);
+
+    Object getSystemConfig(JSONObject args);
+
+    /**
+     * 鈽� 鑱氬悎蹇収锛氭牳蹇冭瘖鏂緭鍏�
+     */
+    Object buildDiagnosisSnapshot(JSONObject args);
+}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/mcp/service/impl/WcsDataFacadeImpl.java b/src/main/java/com/zy/ai/mcp/service/impl/WcsDataFacadeImpl.java
new file mode 100644
index 0000000..4cf371b
--- /dev/null
+++ b/src/main/java/com/zy/ai/mcp/service/impl/WcsDataFacadeImpl.java
@@ -0,0 +1,324 @@
+package com.zy.ai.mcp.service.impl;
+
+import com.alibaba.fastjson.JSONArray;
+import com.alibaba.fastjson.JSONObject;
+import com.baomidou.mybatisplus.mapper.EntityWrapper;
+import com.zy.ai.entity.DeviceConfigsData;
+import com.zy.ai.log.AiLogAppender;
+import com.zy.ai.mcp.service.WcsDataFacade;
+import com.zy.asrs.entity.BasCrnp;
+import com.zy.asrs.entity.BasDevp;
+import com.zy.asrs.entity.BasRgv;
+import com.zy.asrs.entity.WrkMast;
+import com.zy.asrs.service.BasCrnpService;
+import com.zy.asrs.service.BasDevpService;
+import com.zy.asrs.service.BasRgvService;
+import com.zy.asrs.service.WrkMastService;
+import com.zy.core.cache.SlaveConnection;
+import com.zy.core.enums.SlaveType;
+import com.zy.core.model.StationObjModel;
+import com.zy.core.model.protocol.CrnProtocol;
+import com.zy.core.model.protocol.RgvProtocol;
+import com.zy.core.model.protocol.StationProtocol;
+import com.zy.core.thread.CrnThread;
+import com.zy.core.thread.RgvThread;
+import com.zy.core.thread.StationThread;
+import com.zy.system.entity.Config;
+import com.zy.system.service.ConfigService;
+import lombok.RequiredArgsConstructor;
+import org.springframework.beans.factory.annotation.Autowired;
+import org.springframework.stereotype.Service;
+
+import java.util.*;
+
+@Service("wcsDataFacade")
+@RequiredArgsConstructor
+public class WcsDataFacadeImpl implements WcsDataFacade {
+
+    @Autowired
+    private BasCrnpService basCrnpService;
+    @Autowired
+    private BasDevpService basDevpService;
+    @Autowired
+    private BasRgvService basRgvService;
+    @Autowired
+    private WrkMastService wrkMastService;
+    @Autowired
+    private ConfigService configService;
+
+    @Override
+    public Object getCrnDeviceStatus(JSONObject args) {
+        List<Integer> deviceNoList = optIntList(args, "crnNos");
+        EntityWrapper<BasCrnp> wrapper = new EntityWrapper<>();
+        if (deviceNoList != null && deviceNoList.size() > 0) {
+            wrapper.in("crn_no", deviceNoList);
+        }
+        JSONObject data = new JSONObject();
+
+        List<Object> deviceList = new ArrayList<>();
+        List<BasCrnp> basCrnps = basCrnpService.selectList(wrapper);
+        for (BasCrnp basCrnp : basCrnps) {
+            CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo());
+            if (crnThread == null) {
+                continue;
+            }
+            CrnProtocol protocol = crnThread.getStatus();
+            deviceList.add(protocol);
+        }
+
+        data.put("devices", deviceList);
+        return data;
+    }
+
+    @Override
+    public Object getStationDeviceStatus(JSONObject args) {
+        List<BasDevp> basDevps = basDevpService.selectList(new EntityWrapper<>());
+        JSONObject data = new JSONObject();
+
+        List<Object> stationList = new ArrayList<>();
+        for (BasDevp basDevp : basDevps) {
+            StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, basDevp.getDevpNo());
+            if (stationThread == null) {
+                continue;
+            }
+
+            Map<Integer, StationProtocol> map = stationThread.getStatusMap();
+
+            for (StationObjModel stationObjModel : basDevp.getStationList$()) {
+                StationProtocol stationProtocol = map.get(stationObjModel.getStationId());
+                if (stationProtocol == null) {
+                    continue;
+                }
+                stationList.add(stationProtocol);
+            }
+        }
+
+        data.put("stations", stationList);
+        return data;
+    }
+
+    @Override
+    public Object getRgvDeviceStatus(JSONObject args) {
+        List<Integer> deviceNoList = optIntList(args, "rgvNos");
+        EntityWrapper<BasRgv> wrapper = new EntityWrapper<>();
+        if (deviceNoList != null && deviceNoList.size() > 0) {
+            wrapper.in("rgv_no", deviceNoList);
+        }
+        JSONObject data = new JSONObject();
+
+        List<Object> deviceList = new ArrayList<>();
+        List<BasRgv> basRgvs = basRgvService.selectList(wrapper);
+        for (BasRgv basRgv : basRgvs) {
+            RgvThread rgvThread = (RgvThread) SlaveConnection.get(SlaveType.Rgv, basRgv.getRgvNo());
+            if (rgvThread == null) {
+                continue;
+            }
+            RgvProtocol rgvProtocol = rgvThread.getStatus();
+            deviceList.add(rgvProtocol);
+        }
+
+        data.put("devices", deviceList);
+        return data;
+    }
+
+    @Override
+    public Object getTasks(JSONObject args) {
+        int crnNo = optInt(args, "crnNo", -1);
+        int rgvNo = optInt(args, "rgvNo", -1);
+        List<Integer> taskNos = optIntList(args, "taskNos");
+        int limit = optInt(args, "limit", 200);
+
+        EntityWrapper<WrkMast> wrapper = new EntityWrapper<>();
+        if (taskNos != null && taskNos.size() > 0) {
+            wrapper.in("wrk_no", taskNos);
+        }
+
+        if (crnNo != -1) {
+            wrapper.eq("crn_no", crnNo);
+        }
+
+        if (rgvNo != -1) {
+            wrapper.eq("rgv_no", rgvNo);
+        }
+
+        List<WrkMast> tasks = wrkMastService.selectList(wrapper);
+        JSONObject data = new JSONObject();
+        data.put("tasks", tasks);
+        return data;
+    }
+
+    @Override
+    public Object getLogs(JSONObject args) {
+        int limit = optInt(args, "limit", 500);
+        List<String> logs = AiLogAppender.getRecentLogs(limit);
+        JSONObject data = new JSONObject();
+        data.put("logs", logs);
+        return data;
+    }
+
+    @Override
+    public Object getDeviceConfig(JSONObject args) {
+        JSONObject data = new JSONObject();
+        List<DeviceConfigsData> deviceConfigsDataList = new ArrayList<>();
+
+        List<Integer> crnNoList = optIntList(args, "crnNos");
+        EntityWrapper<BasCrnp> crnWrapper = new EntityWrapper<>();
+        if (crnNoList != null && crnNoList.size() > 0) {
+            crnWrapper.in("crn_no", crnNoList);
+        }
+
+        List<Integer> rgvNoList = optIntList(args, "rgvNos");
+        EntityWrapper<BasRgv> rgvWrapper = new EntityWrapper<>();
+        if (rgvNoList != null && rgvNoList.size() > 0) {
+            rgvWrapper.in("rgv_no", rgvNoList);
+        }
+
+        List<Integer> devpNoList = optIntList(args, "devpNos");
+        EntityWrapper<BasDevp> devpWrapper = new EntityWrapper<>();
+        if (devpNoList != null && devpNoList.size() > 0) {
+            devpWrapper.in("devp_no", devpNoList);
+        }
+
+        List<BasCrnp> basCrnps = basCrnpService.selectList(crnWrapper);
+        for (BasCrnp basCrnp : basCrnps) {
+            DeviceConfigsData deviceConfigsData = new DeviceConfigsData();
+            deviceConfigsData.setDeviceNo(basCrnp.getCrnNo());
+            deviceConfigsData.setDeviceType(String.valueOf(SlaveType.Crn));
+            deviceConfigsData.setDeviceData(basCrnp);
+            deviceConfigsDataList.add(deviceConfigsData);
+        }
+
+        List<BasRgv> basRgvs = basRgvService.selectList(rgvWrapper);
+        for (BasRgv basRgv : basRgvs) {
+            DeviceConfigsData deviceConfigsData = new DeviceConfigsData();
+            deviceConfigsData.setDeviceNo(basRgv.getRgvNo());
+            deviceConfigsData.setDeviceType(String.valueOf(SlaveType.Rgv));
+            deviceConfigsData.setDeviceData(basRgv);
+            deviceConfigsDataList.add(deviceConfigsData);
+        }
+
+        List<BasDevp> basDevps = basDevpService.selectList(devpWrapper);
+        for (BasDevp basDevp : basDevps) {
+            DeviceConfigsData deviceConfigsData = new DeviceConfigsData();
+            deviceConfigsData.setDeviceNo(basDevp.getDevpNo());
+            deviceConfigsData.setDeviceType(String.valueOf(SlaveType.Devp));
+            deviceConfigsData.setDeviceData(basDevp);
+            deviceConfigsDataList.add(deviceConfigsData);
+        }
+
+        data.put("deviceConfigs", deviceConfigsDataList);
+        return data;
+    }
+
+    @Override
+    public Object getSystemConfig(JSONObject args) {
+        JSONObject data = new JSONObject();
+        List<Config> systemConfigList = configService.selectList(new EntityWrapper<Config>().notIn("dingdingReportUrl"));
+        data.put("systemConfigs", systemConfigList);
+        return data;
+    }
+
+    @Override
+    public Object buildDiagnosisSnapshot(JSONObject args) {
+        String wh = mustStr(args, "warehouseCode");
+        List<String> crnDeviceNos = optStrList(args, "crnDeviceNos");
+        List<String> taskIds = optStrList(args, "taskIds");
+        int lookbackSeconds = optInt(args, "lookbackSeconds", 300);
+        int logMaxLines = optInt(args, "logMaxLines", 600);
+        boolean includeConfig = optBool(args, "includeConfig", true);
+
+        long now = System.currentTimeMillis();
+        long fromTs = now - lookbackSeconds * 1000L;
+
+        // 1) crn devices
+        JSONObject devArgs = new JSONObject();
+        devArgs.put("deviceNos", crnDeviceNos);
+        JSONObject devices = (JSONObject) getCrnDeviceStatus(devArgs);
+
+        // 2) tasks
+        JSONObject taskArgs = new JSONObject();
+        taskArgs.put("warehouseCode", wh);
+        taskArgs.put("taskIds", taskIds);
+        taskArgs.put("limit", 200);
+        JSONObject tasks = (JSONObject) getTasks(taskArgs);
+
+        // 3) logs (涓�娆℃�у彇鍥烇紝鐒跺悗鍋氬垎妗�+鎺掑簭+鎴柇)
+        JSONObject logArgs = new JSONObject();
+        logArgs.put("warehouseCode", wh);
+        logArgs.put("fromTs", fromTs);
+        logArgs.put("toTs", now);
+//        logArgs.put("deviceIds", deviceIds);
+        logArgs.put("taskIds", taskIds);
+        logArgs.put("maxLines", logMaxLines);
+        JSONObject logs = (JSONObject) getLogs(logArgs);
+
+        // 4) 缁撴瀯鍖栧揩鐓ц緭鍑猴紙寤鸿锛氬垎妗讹級
+        JSONObject snapshot = new JSONObject();
+        snapshot.put("warehouseCode", wh);
+        snapshot.put("generatedTs", now);
+        snapshot.put("timeRange", new JSONObject()
+                .fluentPut("fromTs", fromTs)
+                .fluentPut("toTs", now)
+                .fluentPut("lookbackSeconds", lookbackSeconds));
+
+        snapshot.put("devices", devices);
+        snapshot.put("tasks", tasks);
+        snapshot.put("logs", logs);
+
+        JSONArray hints = new JSONArray();
+        hints.add("Prefer diagnosing with snapshot.devices + snapshot.tasks + snapshot.logs");
+        hints.add("Logs are already filtered by time range; if missing, expand lookbackSeconds");
+        snapshot.put("hints", hints);
+
+        JSONObject data = new JSONObject();
+        data.put("snapshot", snapshot);
+        return data;
+    }
+
+    // --------- helpers ---------
+
+    private String mustStr(JSONObject o, String key) {
+        if (o == null || o.getString(key) == null || o.getString(key).trim().isEmpty())
+            throw new IllegalArgumentException(key + " is required");
+        return o.getString(key).trim();
+    }
+
+    private long mustLong(JSONObject o, String key) {
+        if (o == null || !o.containsKey(key)) throw new IllegalArgumentException(key + " is required");
+        return o.getLongValue(key);
+    }
+
+    private int optInt(JSONObject o, String key, int def) {
+        if (o == null || !o.containsKey(key)) return def;
+        return o.getIntValue(key);
+    }
+
+    private boolean optBool(JSONObject o, String key, boolean def) {
+        if (o == null || !o.containsKey(key)) return def;
+        return o.getBooleanValue(key);
+    }
+
+    private List<String> optStrList(JSONObject o, String key) {
+        if (o == null || !o.containsKey(key)) return Collections.emptyList();
+        JSONArray arr = o.getJSONArray(key);
+        if (arr == null) return Collections.emptyList();
+        List<String> list = new ArrayList<>();
+        for (int i = 0; i < arr.size(); i++) {
+            String s = arr.getString(i);
+            if (s != null && !s.trim().isEmpty()) list.add(s.trim());
+        }
+        return list;
+    }
+
+    private List<Integer> optIntList(JSONObject o, String key) {
+        if (o == null || !o.containsKey(key)) return Collections.emptyList();
+        JSONArray arr = o.getJSONArray(key);
+        if (arr == null) return Collections.emptyList();
+        List<Integer> list = new ArrayList<>();
+        for (int i = 0; i < arr.size(); i++) {
+            String s = arr.getString(i);
+            if (s != null && !s.trim().isEmpty()) list.add(Integer.parseInt(s.trim()));
+        }
+        return list;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/zy/ai/service/WcsQueryService.java b/src/main/java/com/zy/ai/service/WcsQueryService.java
new file mode 100644
index 0000000..b531aab
--- /dev/null
+++ b/src/main/java/com/zy/ai/service/WcsQueryService.java
@@ -0,0 +1,129 @@
+package com.zy.ai.service;
+
+import org.springframework.stereotype.Service;
+
+import java.text.SimpleDateFormat;
+import java.util.*;
+
+@Service
+public class WcsQueryService {
+
+    // 杩欓噷娉ㄥ叆浣犵幇鏈夌殑鍚勭被 Service/Mapper锛歞eviceService/taskService/logService/configService...
+
+    public Map<String, Object> buildDiagnosisSnapshot(Map<String, Object> args) {
+        Map<String, Object> focus = (Map<String, Object>) args.get("focus");
+        List<String> deviceIds = focus == null ? Collections.emptyList() : (List<String>) focus.get("deviceIds");
+        List<String> taskIds   = focus == null ? Collections.emptyList() : (List<String>) focus.get("taskIds");
+
+        int timeWindowSec = getInt(args, "timeWindowSec", 600);
+        int logLimit      = getInt(args, "logLimit", 400);
+        boolean includeConfigs = getBool(args, "includeConfigs", true);
+        boolean includeDerived = getBool(args, "includeDerivedSignals", true);
+
+        Date now = new Date();
+        Date from = new Date(now.getTime() - timeWindowSec * 1000L);
+
+        // 1) devices
+        List<Object> devices = new ArrayList<>();
+        for (String deviceId : safeList(deviceIds)) {
+            Map<String, Object> a = new HashMap<>();
+            a.put("deviceId", deviceId);
+            devices.add(getDeviceState(a));
+        }
+
+        // 2) tasks
+        Map<String, Object> taskArgs = new HashMap<>();
+        if (!safeList(taskIds).isEmpty()) {
+            // 浣犲彲浠ヨ listTasks 鏀寔 taskIds 绮剧‘杩囨护
+            taskArgs.put("taskIds", taskIds);
+        } else if (!safeList(deviceIds).isEmpty()) {
+            taskArgs.put("deviceId", deviceIds.get(0)); // 鎴栨敼鎴愭敮鎸� deviceIds 鏁扮粍
+            taskArgs.put("statuses", Arrays.asList("WAITING","RUNNING","SUSPENDED"));
+            taskArgs.put("limit", 200);
+        }
+        Map<String, Object> taskResult = (Map<String, Object>) listTasks(taskArgs);
+        List<Object> tasks = taskResult == null ? new ArrayList<>() : (List<Object>) taskResult.get("tasks");
+
+        // 3) logs
+        Map<String, Object> logArgs = new HashMap<>();
+        logArgs.put("timeFrom", iso(from));
+        logArgs.put("timeTo", iso(now));
+        logArgs.put("limit", logLimit);
+        if (!safeList(deviceIds).isEmpty()) logArgs.put("deviceId", deviceIds.get(0));
+        if (!safeList(taskIds).isEmpty())   logArgs.put("taskId", taskIds.get(0));
+        Map<String, Object> logResult = (Map<String, Object>) queryLogs(logArgs);
+        List<Object> logs = logResult == null ? new ArrayList<>() : (List<Object>) logResult.get("logs");
+
+        // 4) configs
+        Object configs = null;
+        if (includeConfigs) {
+            configs = getSystemConfig(new HashMap<String, Object>());
+        }
+
+        // 5) derived
+        Object derived = null;
+        if (includeDerived) {
+            derived = deriveSignals(devices, tasks, logs);
+        }
+
+        // output
+        Map<String, Object> out = new LinkedHashMap<>();
+        Map<String, Object> meta = new LinkedHashMap<>();
+        meta.put("snapshotId", UUID.randomUUID().toString());
+        meta.put("generatedAt", iso(now));
+        meta.put("timeFrom", iso(from));
+        meta.put("timeTo", iso(now));
+
+        out.put("meta", meta);
+        out.put("devices", devices);
+        out.put("tasks", tasks);
+        out.put("logs", logs);
+        out.put("configs", configs);
+        out.put("derivedSignals", derived);
+        return out;
+    }
+
+    private Map<String, Object> deriveSignals(List<Object> devices, List<Object> tasks, List<Object> logs) {
+        Map<String, Object> d = new LinkedHashMap<>();
+        List<String> flags = new ArrayList<>();
+        List<String> causes = new ArrayList<>();
+        List<String> bottlenecks = new ArrayList<>();
+
+        // TODO锛氭妸浣� WCS 棰嗗煙瑙勫垯濉炶繖閲岋紙蹇冭烦瓒呮椂銆佺瓑寰呯‘璁ゃ�佸懡浠ゆ棤ACK銆佺珯鍙版弧銆佷笅娓搁樆濉炵瓑锛�
+        // 鍏堢粰涓ず渚嬶細
+        if (tasks != null && !tasks.isEmpty() && (devices == null || devices.isEmpty())) {
+            flags.add("HAS_TASKS_BUT_NO_DEVICE_SNAPSHOT");
+        }
+
+        d.put("anomalyFlags", flags);
+        d.put("suspectedRootCauses", causes);
+        d.put("suspectedBottlenecks", bottlenecks);
+        return d;
+    }
+
+    // ======= 涓嬮潰杩欎簺鍑芥暟锛屼綘鐢ㄧ幇鏈� service 瀹炵幇鍗冲彲 =======
+    public Map<String, Object> getDeviceState(Map<String, Object> args) { /* ... */ return new HashMap<>(); }
+    public Map<String, Object> listTasks(Map<String, Object> args) { /* ... */ return new HashMap<>(); }
+    public Map<String, Object> queryLogs(Map<String, Object> args) { /* ... */ return new HashMap<>(); }
+    public Object getSystemConfig(Map<String, Object> args) { /* ... */ return new HashMap<>(); }
+
+    // helpers
+    private int getInt(Map<String, Object> m, String k, int def) {
+        Object v = m.get(k);
+        if (v == null) return def;
+        if (v instanceof Number) return ((Number) v).intValue();
+        return Integer.parseInt(String.valueOf(v));
+    }
+    private boolean getBool(Map<String, Object> m, String k, boolean def) {
+        Object v = m.get(k);
+        if (v == null) return def;
+        if (v instanceof Boolean) return (Boolean) v;
+        return Boolean.parseBoolean(String.valueOf(v));
+    }
+    private List<String> safeList(List<String> l) { return l == null ? Collections.emptyList() : l; }
+    private String iso(Date d) {
+        SimpleDateFormat f = new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSXXX");
+        f.setTimeZone(TimeZone.getTimeZone("Asia/Shanghai"));
+        return f.format(d);
+    }
+}
\ No newline at end of file

--
Gitblit v1.9.1