| src/main/java/com/zy/ai/controller/WcsDiagnosisController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/entity/ChatCompletionRequest.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/LlmChatService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/WcsDiagnosisService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/static/js/ai/diagnose.js | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/views/ai/diagnose.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
@@ -22,6 +22,7 @@ import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.*; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import java.util.ArrayList; import java.util.HashMap; @@ -47,7 +48,7 @@ request.setAlarmMessage("系统不执行任务"); List<String> logs = AiLogAppender.getRecentLogs(300); List<String> logs = AiLogAppender.getRecentLogs(100); request.setLogs(logs); List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<>()); @@ -99,10 +100,83 @@ } request.setDeviceRealtimeData(deviceRealTimeDataList); request.setDeviceConfigs(deviceConfigsDataList); WcsDiagnosisResponse response = diagnose(request); return response; } @GetMapping("/runAiStream") public SseEmitter runAiStream() { SseEmitter emitter = new SseEmitter(0L); new Thread(() -> { try { WcsDiagnosisRequest request = new WcsDiagnosisRequest(); request.setAlarmMessage("系统不执行任务"); List<String> logs = AiLogAppender.getRecentLogs(100); request.setLogs(logs); List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<>()); request.setTasks(wrkMasts); List<DeviceRealTimeData> deviceRealTimeDataList = new ArrayList<>(); List<DeviceConfigsData> deviceConfigsDataList = new ArrayList<>(); List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<>()); for (BasCrnp basCrnp : basCrnps) { CrnThread crnThread = (CrnThread) SlaveConnection.get(SlaveType.Crn, basCrnp.getCrnNo()); if (crnThread == null) { continue; } CrnProtocol protocol = crnThread.getStatus(); for (StationObjModel stationObjModel : basCrnp.getInStationList$()) { StationThread stationThread = (StationThread) SlaveConnection.get(SlaveType.Devp, stationObjModel.getDeviceNo()); if (stationThread == null) { continue; } Map<Integer, StationProtocol> map = stationThread.getStatusMap(); StationProtocol stationProtocol = map.get(stationObjModel.getStationId()); if (stationProtocol == null) { continue; } DeviceRealTimeData stationData = new DeviceRealTimeData(); stationData.setDeviceNo(stationObjModel.getDeviceNo()); stationData.setDeviceType(String.valueOf(SlaveType.Devp)); stationData.setDeviceData(stationProtocol); deviceRealTimeDataList.add(stationData); } DeviceRealTimeData deviceRealTimeData = new DeviceRealTimeData(); deviceRealTimeData.setDeviceNo(basCrnp.getCrnNo()); deviceRealTimeData.setDeviceType(String.valueOf(SlaveType.Crn)); deviceRealTimeData.setDeviceData(protocol); deviceRealTimeDataList.add(deviceRealTimeData); DeviceConfigsData deviceConfigsData = new DeviceConfigsData(); deviceConfigsData.setDeviceNo(basCrnp.getCrnNo()); deviceConfigsData.setDeviceType(String.valueOf(SlaveType.Crn)); deviceConfigsData.setDeviceData(basCrnp); deviceConfigsDataList.add(deviceConfigsData); } request.setDeviceRealtimeData(deviceRealTimeDataList); request.setDeviceConfigs(deviceConfigsDataList); wcsDiagnosisService.diagnoseStream(request, emitter); } catch (Exception e) { emitter.completeWithError(e); } }).start(); return emitter; } /** * POST /api/ai/diagnose/wcs */ src/main/java/com/zy/ai/entity/ChatCompletionRequest.java
@@ -12,6 +12,7 @@ // 可选参数 private Double temperature; private Integer max_tokens; private Boolean stream; @Data public static class Message { src/main/java/com/zy/ai/service/LlmChatService.java
@@ -10,8 +10,13 @@ import org.springframework.stereotype.Service; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Mono; import reactor.core.publisher.Flux; import java.util.List; import java.util.function.Consumer; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; @Slf4j @Service @@ -60,4 +65,52 @@ return response.getChoices().get(0).getMessage().getContent(); } public void chatStream(List<ChatCompletionRequest.Message> messages, Double temperature, Integer maxTokens, Consumer<String> onChunk, Runnable onComplete, Consumer<Throwable> onError) { ChatCompletionRequest req = new ChatCompletionRequest(); req.setModel(model); req.setMessages(messages); req.setTemperature(temperature != null ? temperature : 0.3); req.setMax_tokens(maxTokens != null ? maxTokens : 1024); req.setStream(true); Flux<String> flux = llmWebClient.post() .uri("/chat/completions") .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .contentType(MediaType.APPLICATION_JSON) .accept(MediaType.TEXT_EVENT_STREAM) .bodyValue(req) .retrieve() .bodyToFlux(String.class) .doOnError(ex -> log.error("调用 LLM 流式失败", ex)); flux.subscribe(payload -> { String s = payload == null ? null : payload.trim(); if (s == null || s.isEmpty()) return; if (s.startsWith("data:")) s = s.substring(5).trim(); if ("[DONE]".equals(s)) return; try { JSONObject obj = JSON.parseObject(s); JSONArray choices = obj.getJSONArray("choices"); if (choices != null && !choices.isEmpty()) { JSONObject c0 = choices.getJSONObject(0); JSONObject delta = c0.getJSONObject("delta"); if (delta != null) { String content = delta.getString("content"); if (content != null) onChunk.accept(content); } } } catch (Exception ignore) {} }, err -> { if (onError != null) onError.accept(err); }, () -> { if (onComplete != null) onComplete.run(); }); } } src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -3,6 +3,7 @@ import com.alibaba.fastjson.JSON; import com.zy.ai.entity.ChatCompletionRequest; import com.zy.ai.entity.WcsDiagnosisRequest; import org.springframework.web.servlet.mvc.method.annotation.SseEmitter; import lombok.RequiredArgsConstructor; import org.springframework.stereotype.Service; @@ -54,6 +55,44 @@ return llmChatService.chat(messages, 0.2, 2048); } public void diagnoseStream(WcsDiagnosisRequest request, SseEmitter emitter) { List<ChatCompletionRequest.Message> messages = new ArrayList<>(); ChatCompletionRequest.Message system = new ChatCompletionRequest.Message(); system.setRole("system"); system.setContent( "你是一名资深 WCS(仓储控制系统)与自动化立库专家,熟悉:堆垛机、输送线、提升机、穿梭车等设备的任务分配和运行逻辑,也熟悉常见的系统卡死、任务不执行、设备空闲但无任务等问题模式。\n\n" + "你将收到以下几类数据:\n" + "1)任务信息(tasks):当前待执行/在执行/挂起任务\n" + "2)设备实时数据(deviceRealtimeData):每台设备当前状态、是否在线、当前任务号等\n" + "3)设备配置信息(deviceConfigs):设备是否启用、服务区域、允许的任务类型等\n" + "4)系统日志(logs):按时间顺序的日志文本\n" + "5)额外上下文(extraContext):如仓库代码、WCS 版本等\n\n" + "你的目标是:帮助现场运维人员分析,为什么系统当前不执行任务,或者任务执行效率异常,指出可能是哪些设备导致的问题。\n\n" + "请按以下结构输出诊断结果(使用简体中文):\n" + "1. 问题概述(1-3 句话,概括当前系统状态)\n" + "2. 可疑设备列表(列出 1-N 个设备编号,并说明每个设备为什么可疑,例如:配置禁用/长时间空闲/状态异常/任务分配不到它等)\n" + "3. 可能原因(从任务分配、设备状态、配置错误、接口/通信异常等角度,列出 3-7 条)\n" + "4. 建议排查步骤(步骤 1、2、3...,每步要尽量具体、可操作,例如:在某页面查看某字段、检查某个开关、对比某个状态位等)\n" + "5. 风险评估(说明当前问题对业务影响程度:高/中/低,以及是否需要立即人工干预)\n" + "6. WCS 逻辑优化建议(如果从日志/数据看出可能的系统逻辑缺陷,请给出简要建议,例如增加某个防呆校验、告警、监控等)\n" ); messages.add(system); ChatCompletionRequest.Message user = new ChatCompletionRequest.Message(); user.setRole("user"); user.setContent(buildUserContent(request)); messages.add(user); llmChatService.chatStream(messages, 0.2, 2048, s -> { try { emitter.send(SseEmitter.event().data(s)); } catch (Exception ignore) {} }, () -> { try { emitter.complete(); } catch (Exception ignore) {} }, e -> { try { emitter.completeWithError(e); } catch (Exception ignore) {} }); } private String buildUserContent(WcsDiagnosisRequest request) { StringBuilder sb = new StringBuilder(); src/main/webapp/static/js/ai/diagnose.js
New file @@ -0,0 +1,43 @@ var sse; function startDiagnosis() { if (sse) { sse.close(); } $('#ai-output').text(''); $('#start-btn').attr('disabled', true); $('#stop-btn').attr('disabled', false); var url = baseUrl + '/ai/diagnose/runAiStream'; sse = new EventSource(url); sse.onmessage = function (e) { var curr = $('#ai-output').text(); $('#ai-output').text(curr + e.data); }; sse.onerror = function () { $('#start-btn').attr('disabled', false); $('#stop-btn').attr('disabled', true); if (sse) { sse.close(); } layer.msg('连接已关闭或发生错误'); }; } function stopDiagnosis() { if (sse) { sse.close(); sse = null; } $('#start-btn').attr('disabled', false); $('#stop-btn').attr('disabled', true); } $(function () { $('#stop-btn').attr('disabled', true); $('#start-btn').on('click', startDiagnosis); $('#stop-btn').on('click', stopDiagnosis); $('#clear-btn').on('click', function () { $('#ai-output').text(''); }); }); src/main/webapp/views/ai/diagnose.html
New file @@ -0,0 +1,49 @@ <!DOCTYPE html> <html lang="zh-CN"> <head> <meta charset="utf-8"> <title>AI诊断</title> <meta name="renderer" content="webkit"> <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1"> <meta name="viewport" content="width=device-width, initial-scale=1, maximum-scale=1"> <link rel="stylesheet" href="../../static/layui/css/layui.css" media="all"> <link rel="stylesheet" href="../../static/css/admin.css" media="all"> <link rel="stylesheet" href="../../static/css/cool.css" media="all"> </head> <body> <div class="layui-fluid"> <div class="layui-row"> <div class="layui-col-md12"> <div class="layui-card"> <div class="layui-card-header">AI诊断</div> <div class="layui-card-body"> <div class="layui-form toolbar" id="ai-toolbar"> <button id="start-btn" type="button" class="layui-btn layui-btn-normal">开始诊断</button> <button id="stop-btn" type="button" class="layui-btn layui-btn-danger">停止</button> <button id="clear-btn" type="button" class="layui-btn">清空</button> </div> <hr class="layui-bg-gray"> <div id="ai-output" style="white-space: pre-wrap; font-family: Menlo, Monaco, Consolas, monospace; min-height: 240px; padding: 12px; border: 1px solid #e6e6e6; border-radius: 4px;"></div> </div> </div> </div> </div> <div class="layui-row"> <div class="layui-col-md12"> <div class="layui-card"> <div class="layui-card-header">说明</div> <div class="layui-card-body"> <p>点击“开始诊断”后,前端将通过 SSE 与后端建立连接,并逐字显示 AI 的分析结果。</p> </div> </div> </div> </div> </div> <script type="text/javascript" src="../../static/js/jquery/jquery-3.3.1.min.js"></script> <script type="text/javascript" src="../../static/layui/layui.js" charset="utf-8"></script> <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script> <script type="text/javascript" src="../../static/js/ai/diagnose.js" charset="utf-8"></script> </body> </html>