pom.xml
@@ -5,7 +5,7 @@ <parent> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-parent</artifactId> <version>2.1.3.RELEASE</version> <version>2.5.14</version> <relativePath/> <!-- lookup parent from repository --> </parent> <groupId>com.zy</groupId> @@ -126,6 +126,11 @@ <artifactId>truelicense-core</artifactId> <version>1.33</version> </dependency> <!-- WebClient --> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> </dependencies> <build> src/main/java/com/zy/ai/config/LlmConfig.java
New file @@ -0,0 +1,20 @@ package com.zy.ai.config; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.web.reactive.function.client.WebClient; @Configuration public class LlmConfig { @Value("${llm.base-url}") private String baseUrl; @Bean public WebClient llmWebClient() { return WebClient.builder() .baseUrl(baseUrl) .build(); } } src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
New file @@ -0,0 +1,192 @@ package com.zy.ai.controller; import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.zy.ai.entity.DeviceConfigsData; import com.zy.ai.entity.DeviceRealTimeData; import com.zy.ai.entity.WcsDiagnosisRequest; import com.zy.ai.entity.WcsDiagnosisResponse; import com.zy.ai.log.AiLogAppender; import com.zy.ai.service.WcsDiagnosisService; import com.zy.asrs.entity.BasCrnp; import com.zy.asrs.entity.WrkMast; import com.zy.asrs.service.BasCrnpService; 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.StationProtocol; import com.zy.core.thread.CrnThread; import com.zy.core.thread.StationThread; import lombok.RequiredArgsConstructor; 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; import java.util.List; import java.util.Map; @Slf4j @RestController @RequestMapping("/ai/diagnose") @RequiredArgsConstructor public class WcsDiagnosisController { @Autowired private WcsDiagnosisService wcsDiagnosisService; @Autowired private WrkMastService wrkMastService; @Autowired private BasCrnpService basCrnpService; @GetMapping("/runAi") public WcsDiagnosisResponse runAi() { 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); 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 */ @PostMapping("/wcs") public WcsDiagnosisResponse diagnose(@RequestBody WcsDiagnosisRequest request) { String analysis = wcsDiagnosisService.diagnose(request); WcsDiagnosisResponse resp = new WcsDiagnosisResponse(); resp.setAnalysis(analysis); resp.setOriginalRequest(request); return resp; } } src/main/java/com/zy/ai/entity/ChatCompletionRequest.java
New file @@ -0,0 +1,22 @@ package com.zy.ai.entity; import lombok.Data; import java.util.List; @Data public class ChatCompletionRequest { private String model; private List<Message> messages; // 可选参数 private Double temperature; private Integer max_tokens; private Boolean stream; @Data public static class Message { private String role; // "user" / "assistant" / "system" private String content; } } src/main/java/com/zy/ai/entity/ChatCompletionResponse.java
New file @@ -0,0 +1,45 @@ package com.zy.ai.entity; import com.fasterxml.jackson.annotation.JsonIgnoreProperties; import com.fasterxml.jackson.annotation.JsonProperty; import lombok.Data; import java.util.List; @Data @JsonIgnoreProperties(ignoreUnknown = true) public class ChatCompletionResponse { private String id; @JsonProperty("object") private String objectName; private Long created; private List<Choice> choices; private Usage usage; @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class Choice { private Integer index; private ChatCompletionRequest.Message message; @JsonProperty("finish_reason") private String finishReason; } @Data @JsonIgnoreProperties(ignoreUnknown = true) public static class Usage { @JsonProperty("prompt_tokens") private Integer promptTokens; @JsonProperty("completion_tokens") private Integer completionTokens; @JsonProperty("total_tokens") private Integer totalTokens; } } src/main/java/com/zy/ai/entity/DeviceConfigsData.java
New file @@ -0,0 +1,14 @@ package com.zy.ai.entity; import lombok.Data; @Data public class DeviceConfigsData { private Integer deviceNo; private String deviceType; private Object deviceData; } src/main/java/com/zy/ai/entity/DeviceRealTimeData.java
New file @@ -0,0 +1,14 @@ package com.zy.ai.entity; import lombok.Data; @Data public class DeviceRealTimeData { private Integer deviceNo; private String deviceType; private Object deviceData; } src/main/java/com/zy/ai/entity/WcsDiagnosisRequest.java
New file @@ -0,0 +1,57 @@ package com.zy.ai.entity; import com.zy.asrs.entity.WrkMast; import lombok.Data; import java.util.List; import java.util.Map; /** * WCS AI 诊断请求 * 支持: * - 任务信息 * - 设备实时数据 * - 设备配置信息 * - 系统日志 * - 额外上下文 */ @Data public class WcsDiagnosisRequest { /** * 当前关注的设备号(可选,例如堆垛机号=1),如果是整体系统诊断可以不填 */ private Integer craneNo; /** * 当前你观察到的现象/问题描述(可选) * 例如:系统不执行任务,不知道哪个设备没在运行 */ private String alarmMessage; /** * 系统日志(按时间顺序) */ private List<String> logs; /** * 任务信息列表(当前待执行/在执行/挂起任务) */ private List<WrkMast> tasks; /** * 设备当前实时数据(状态位、运行模式、心跳时间等) */ private List<DeviceRealTimeData> deviceRealtimeData; /** * 设备配置信息 */ private List<DeviceConfigsData> deviceConfigs; /** * 额外上下文,例如: * warehouseCode, shift, wcsVersion, plcVersion 等 */ private Map<String, Object> extraContext; } src/main/java/com/zy/ai/entity/WcsDiagnosisResponse.java
New file @@ -0,0 +1,21 @@ package com.zy.ai.entity; import lombok.Data; /** * WCS AI 诊断响应 */ @Data public class WcsDiagnosisResponse { /** * AI 诊断完整文本(Markdown) */ private String analysis; /** * 可选:保留原始请求,前端调试用 */ private Object originalRequest; } src/main/java/com/zy/ai/log/AiLogAppender.java
New file @@ -0,0 +1,53 @@ package com.zy.ai.log; import ch.qos.logback.classic.spi.ILoggingEvent; import ch.qos.logback.core.AppenderBase; import java.time.Instant; import java.time.ZoneId; import java.time.format.DateTimeFormatter; import java.util.List; import java.util.concurrent.LinkedBlockingDeque; import java.util.stream.Collectors; public class AiLogAppender extends AppenderBase<ILoggingEvent> { // 保存最近 2000 条日志 private static final LinkedBlockingDeque<String> LOG_BUFFER = new LinkedBlockingDeque<>(2000); private static final DateTimeFormatter TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss.SSS") .withZone(ZoneId.systemDefault()); @Override protected void append(ILoggingEvent event) { String time = TIME_FORMATTER.format(Instant.ofEpochMilli(event.getTimeStamp())); String thread = event.getThreadName(); String level = event.getLevel().toString(); String loggerName = event.getLoggerName(); String message = event.getFormattedMessage(); String logLine = String.format( "%s [%s] %-5s %s - %s", time, thread, level, loggerName, message ); // 放进环形缓冲区 if (LOG_BUFFER.remainingCapacity() == 0) { LOG_BUFFER.pollFirst(); // 移除最旧的 } LOG_BUFFER.offerLast(logLine); } public static List<String> getRecentLogs(int limit) { int size = LOG_BUFFER.size(); int skip = Math.max(0, size - limit); return LOG_BUFFER.stream() .skip(skip) .collect(Collectors.toList()); } } src/main/java/com/zy/ai/service/LlmChatService.java
New file @@ -0,0 +1,116 @@ package com.zy.ai.service; import com.zy.ai.entity.ChatCompletionRequest; import com.zy.ai.entity.ChatCompletionResponse; import lombok.RequiredArgsConstructor; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Value; import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; 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 @RequiredArgsConstructor public class LlmChatService { private final WebClient llmWebClient; @Value("${llm.api-key}") private String apiKey; @Value("${llm.model}") private String model; /** * 通用对话方法:传入 messages,返回大模型文本回复 */ public String chat(List<ChatCompletionRequest.Message> messages, Double temperature, Integer maxTokens) { ChatCompletionRequest req = new ChatCompletionRequest(); req.setModel(model); req.setMessages(messages); req.setTemperature(temperature != null ? temperature : 0.3); req.setMax_tokens(maxTokens != null ? maxTokens : 1024); ChatCompletionResponse response = llmWebClient.post() .uri("/chat/completions") .header(HttpHeaders.AUTHORIZATION, "Bearer " + apiKey) .contentType(MediaType.APPLICATION_JSON) .bodyValue(req) // 2.5.14 已支持 bodyValue .retrieve() .bodyToMono(ChatCompletionResponse.class) .doOnError(ex -> log.error("调用 LLM 失败", ex)) .onErrorResume(ex -> Mono.empty()) .block(); if (response == null || response.getChoices() == null || response.getChoices().isEmpty() || response.getChoices().get(0).getMessage() == null) { return "AI 诊断失败:未获取到有效回复。"; } 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
New file @@ -0,0 +1,156 @@ package com.zy.ai.service; 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; import java.util.ArrayList; import java.util.List; @Service @RequiredArgsConstructor public class WcsDiagnosisService { private final LlmChatService llmChatService; /** * 针对“系统不执行任务 / 不知道哪个设备没在运行”的通用 AI 诊断 */ public String diagnose(WcsDiagnosisRequest request) { List<ChatCompletionRequest.Message> messages = new ArrayList<>(); // 1. system:定义专家身份 + 输出结构 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); // 2. user:把具体的数据组织成文本(JSON 形式方便模型看结构) ChatCompletionRequest.Message user = new ChatCompletionRequest.Message(); user.setRole("user"); user.setContent(buildUserContent(request)); messages.add(user); // 调用大模型 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(); sb.append("【问题描述】\n"); if (request.getAlarmMessage() != null && !request.getAlarmMessage().isEmpty()) { sb.append(request.getAlarmMessage()).append("\n\n"); } else { sb.append("系统当前不执行任务,但具体原因不明,请根据以下信息帮助判断。\n\n"); } sb.append("【设备信息】\n"); sb.append("关注设备(如果有指定): ") .append(request.getCraneNo() != null ? request.getCraneNo() : "未指定,需整体分析") .append("\n\n"); if (request.getExtraContext() != null && !request.getExtraContext().isEmpty()) { sb.append("【额外上下文 extraContext】\n"); sb.append(JSON.toJSONString(request.getExtraContext(), true)).append("\n\n"); } if (request.getTasks() != null && !request.getTasks().isEmpty()) { sb.append("【任务信息 tasks】\n"); sb.append("下面是当前相关任务列表的 JSON 数据:\n"); sb.append(JSON.toJSONString(request.getTasks(), true)).append("\n\n"); } else { sb.append("【任务信息 tasks】\n"); sb.append("当前未提供任务信息。\n\n"); } if (request.getDeviceRealtimeData() != null && !request.getDeviceRealtimeData().isEmpty()) { sb.append("【设备实时数据 deviceRealtimeData】\n"); sb.append("下面是各设备当前实时状态的 JSON 数据:\n"); sb.append(JSON.toJSONString(request.getDeviceRealtimeData(), true)).append("\n\n"); } else { sb.append("【设备实时数据 deviceRealtimeData】\n"); sb.append("当前未提供设备实时数据。\n\n"); } if (request.getDeviceConfigs() != null && !request.getDeviceConfigs().isEmpty()) { sb.append("【设备配置信息 deviceConfigs】\n"); sb.append("下面是各设备配置的 JSON 数据:\n"); sb.append(JSON.toJSONString(request.getDeviceConfigs(), true)).append("\n\n"); } else { sb.append("【设备配置信息 deviceConfigs】\n"); sb.append("当前未提供设备配置信息。\n\n"); } sb.append("【系统日志 logs(按时间顺序)】\n"); if (request.getLogs() != null && !request.getLogs().isEmpty()) { for (String logLine : request.getLogs()) { sb.append(logLine).append("\n"); } } else { sb.append("当前未提供日志信息。\n"); } sb.append("\n请根据以上所有信息,结合你的经验进行分析诊断。"); return sb.toString(); } } src/main/java/com/zy/asrs/entity/BasCrnp.java
@@ -79,6 +79,10 @@ @TableField("control_rows") private String controlRows; @ApiModelProperty(value= "深库位排号") @TableField("deep_rows") private String deepRows; /** * 入站列表 */ src/main/java/com/zy/asrs/utils/Utils.java
@@ -20,14 +20,9 @@ import com.zy.core.model.protocol.CrnProtocol; import com.zy.core.thread.CrnThread; import java.lang.reflect.Field; import java.text.DecimalFormat; import java.util.ArrayList; import java.util.HashMap; import java.util.List; import java.util.Arrays; import java.util.Comparator; import java.util.HashSet; import java.util.Set; import java.util.*; public class Utils { @@ -212,4 +207,20 @@ } return list; } public static Map<String, Object> convertObjectToMap(Object obj) { Map<String, Object> map = new HashMap<>(); Class<?> clazz = obj.getClass(); Field[] fields = clazz.getDeclaredFields(); for (Field field : fields) { field.setAccessible(true); try { map.put(field.getName(), field.get(obj)); }catch (Exception e){ e.printStackTrace(); } } return map; } } src/main/java/com/zy/common/config/AspectConfig.java
@@ -63,7 +63,7 @@ if (annotation != null && !Cools.isEmpty(annotation.memo())) { saveErrLog(joinPoint, request, errorResponse, ex, annotation.memo()); } throw ex; return errorResponse; } finally { long end = System.currentTimeMillis(); // log.info("请求日志的打印"); @@ -122,7 +122,7 @@ private Object buildErrorResponse(Throwable ex) { if (ex instanceof CoolException) { return R.parse(ex.getMessage()); return R.error(ex.getMessage()); } return R.error(); } src/main/java/com/zy/common/config/CoolExceptionHandler.java
@@ -26,7 +26,7 @@ @ExceptionHandler(CoolException.class) public R handleRRException(CoolException e) { return R.parse(e.getMessage()); return R.error(e.getMessage()); } } src/main/java/com/zy/common/service/CommonService.java
@@ -38,8 +38,6 @@ @Autowired private NavigateUtils navigateUtils; @Autowired private CommonService commonService; @Autowired private RedisUtil redisUtil; /** @@ -139,12 +137,12 @@ ioPri = param.getTaskPri().doubleValue(); } Integer sourceCrnNo = commonService.findCrnNoByLocNo(sourceLocMast.getLocNo()); Integer sourceCrnNo = this.findCrnNoByLocNo(sourceLocMast.getLocNo()); if (sourceCrnNo == null) { throw new CoolException("未找到对应堆垛机"); } Integer crnNo = commonService.findCrnNoByLocNo(locMast.getLocNo()); Integer crnNo = this.findCrnNoByLocNo(locMast.getLocNo()); if (crnNo == null) { throw new CoolException("未找到对应堆垛机"); } @@ -202,7 +200,7 @@ ioPri = param.getTaskPri().doubleValue(); } Integer crnNo = commonService.findCrnNoByLocNo(locMast.getLocNo()); Integer crnNo = this.findCrnNoByLocNo(locMast.getLocNo()); if (crnNo == null) { throw new CoolException("未找到对应堆垛机"); } @@ -256,12 +254,12 @@ ioPri = param.getTaskPri().doubleValue(); } Integer crnNo = commonService.findCrnNoByLocNo(locMast.getLocNo()); Integer crnNo = this.findCrnNoByLocNo(locMast.getLocNo()); if (crnNo == null) { throw new CoolException("未找到对应堆垛机"); } Integer sourceStationId = commonService.findOutStationId(crnNo, param.getStaNo()); Integer sourceStationId = this.findOutStationId(crnNo, param.getStaNo()); if (sourceStationId == null) { throw new CoolException("未找到输送目标站点可走行路径"); } src/main/java/com/zy/core/ServerBootstrap.java
@@ -17,7 +17,6 @@ import org.springframework.scheduling.annotation.Async; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; import javax.annotation.PreDestroy; import java.util.List; @@ -35,7 +34,6 @@ @Autowired private DeviceConfigService deviceConfigService; @PostConstruct @Async public void init() throws InterruptedException { News.info("核心控制层开始初始化..............................................."); src/main/java/com/zy/core/plugin/FakeProcess.java
@@ -582,6 +582,8 @@ StationCommand command = stationThread.getMoveCommand(9998, wrkMast.getSourceStaNo(), 0, 0); MessageQueue.offer(SlaveType.Devp, stationObjModel.getDeviceNo(), new Task(2, command)); } }else if(wrkMast.getWrkSts() == WrkStsType.LOC_MOVE_RUN.sts){ updateWrkSts = WrkStsType.COMPLETE_LOC_MOVE.sts; }else{ News.error("堆垛机处于等待确认且任务完成状态,但工作状态异常。堆垛机号={},工作号={}", basCrnp.getCrnNo(), crnProtocol.getTaskNo()); continue; src/main/java/com/zy/core/thread/impl/ZySiemensCrnThread.java
@@ -124,7 +124,6 @@ ZyCrnStatusEntity crnStatus = zyCrnConnectDriver.getStatus(); if (crnStatus == null) { OutputQueue.CRN.offer(MessageFormat.format("【{0}】读取堆垛机plc状态信息失败 ===>> [id:{1}] [ip:{2}] [port:{3}]", DateUtils.convert(new Date()), deviceConfig.getDeviceNo(), deviceConfig.getIp(), deviceConfig.getPort())); News.error("SiemensCrn读取堆垛机plc状态信息失败 ===>> [id:{}] [ip:{}] [port:{}]", deviceConfig.getDeviceNo(), deviceConfig.getIp(), deviceConfig.getPort()); return; } src/main/java/com/zy/core/utils/CrnOperateProcessUtils.java
@@ -91,6 +91,9 @@ crnProtocol.setLastIo("O"); } } //库位移转 this.crnExecuteLocTransfer(basCrnp, crnThread); } } } @@ -285,6 +288,58 @@ } } private synchronized void crnExecuteLocTransfer(BasCrnp basCrnp, CrnThread crnThread) { CrnProtocol crnProtocol = crnThread.getStatus(); if(crnProtocol == null){ return; } Integer crnNo = basCrnp.getCrnNo(); List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<WrkMast>() .eq("crn_no", crnNo) .eq("wrk_sts", WrkStsType.NEW_LOC_MOVE.sts) ); for (WrkMast wrkMast : wrkMasts) { // 获取源库位信息 LocMast sourceLocMast = locMastService.selectById(wrkMast.getSourceLocNo()); if (sourceLocMast == null) { News.taskInfo(wrkMast.getWrkNo(), "源库位:{} 信息不存在", wrkMast.getSourceLocNo()); continue; } if(!sourceLocMast.getLocSts().equals("R")){ News.taskInfo(wrkMast.getWrkNo(), "源库位:{} 状态异常,不属于出库预约状态", wrkMast.getSourceLocNo()); continue; } // 获取库位信息 LocMast locMast = locMastService.selectById(wrkMast.getLocNo()); if (locMast == null) { News.taskInfo(wrkMast.getWrkNo(), "库位:{} 信息不存在", wrkMast.getLocNo()); continue; } if (!locMast.getLocSts().equals("S")) { News.taskInfo(wrkMast.getWrkNo(), "库位:{} 状态异常,不属于入库预约状态", wrkMast.getLocNo()); continue; } CrnCommand command = crnThread.getPickAndPutCommand(wrkMast.getSourceLocNo(), wrkMast.getLocNo(), wrkMast.getWrkNo(), crnNo); wrkMast.setWrkSts(WrkStsType.LOC_MOVE_RUN.sts); wrkMast.setCrnNo(crnNo); wrkMast.setSystemMsg(""); wrkMast.setIoTime(new Date()); if (wrkMastService.updateById(wrkMast)) { MessageQueue.offer(SlaveType.Crn, crnNo, new Task(2, command)); News.info("堆垛机命令下发成功,堆垛机号={},任务数据={}", crnNo, JSON.toJSON(command)); return; } } } //堆垛机任务执行完成 public synchronized void crnIoExecuteFinish() { List<BasCrnp> basCrnps = basCrnpService.selectList(new EntityWrapper<>()); @@ -320,6 +375,8 @@ updateWrkSts = WrkStsType.COMPLETE_INBOUND.sts; }else if(wrkMast.getWrkSts() == WrkStsType.OUTBOUND_RUN.sts){ updateWrkSts = WrkStsType.OUTBOUND_RUN_COMPLETE.sts; }else if(wrkMast.getWrkSts() == WrkStsType.LOC_MOVE_RUN.sts){ updateWrkSts = WrkStsType.COMPLETE_LOC_MOVE.sts; }else{ News.error("堆垛机处于等待确认且任务完成状态,但工作状态异常。堆垛机号={},工作号={}", basCrnp.getCrnNo(), crnProtocol.getTaskNo()); continue; src/main/java/com/zy/system/entity/license/LicenseCheckListener.java
@@ -1,6 +1,7 @@ package com.zy.system.entity.license; import com.core.common.Cools; import com.zy.core.ServerBootstrap; import com.zy.system.entity.LicenseInfos; import com.zy.system.service.LicenseInfosService; import com.zy.system.timer.LicenseTimer; @@ -57,6 +58,8 @@ private LicenseTimer licenseTimer; @Autowired private LicenseInfosService licenseInfosService; @Autowired private ServerBootstrap serverBootstrap; @Override public void onApplicationEvent(ContextRefreshedEvent event) { @@ -108,9 +111,13 @@ Long num = endTime - starTime;//时间戳相差的毫秒数 int day = (int) (num / 24 / 60 / 60 / 1000); licenseTimer.setLicenseDays(day); try { serverBootstrap.init(); }catch (Exception e){ e.printStackTrace(); } } return install != null; } catch (Exception e) { e.printStackTrace(); src/main/resources/application.yml
@@ -35,7 +35,8 @@ logging: path: ./stock/out/@pom.build.finalName@/logs file: path: ./stock/out/@pom.build.finalName@/logs super: pwd: xltys1995 @@ -65,3 +66,8 @@ loggingPath: ./stock/out/@pom.build.finalName@/deviceLogs # 日志过期时间 单位天 expireDays: 7 llm: base-url: https://api.siliconflow.cn/v1 api-key: sk-sxdtebtquwrugzrmaqqqkzdzmrgzhzmplwwuowysdasccent model: Qwen/Qwen3-VL-32B-Instruct src/main/resources/logback-spring.xml
@@ -3,6 +3,9 @@ <include resource="org/springframework/boot/logging/logback/defaults.xml"/> <springProperty scope="context" name="LOG_FILE_PATH" source="logging.file.path"/> <property name="LOG_PATH" value="${LOG_FILE_PATH:-./stock/out/@pom.build.finalName@/logs}"/> <property name="CONSOLE_LOG_PATTERN" value="%date{yyyy-MM-dd HH:mm:ss}|%highlight(%-5level)|%boldYellow(%thread)|%boldGreen(%logger) %msg%n"> </property> @@ -10,6 +13,8 @@ <property name="CONSOLE_LOG_PATTERN" value="%d{yyyy-MM-dd HH:mm:ss.SSS} %highlight(-%5p) ${PID:-} [%15.15t] %-40.40logger{39} : %m%n"> </property> <appender name="AI_LOG" class="com.zy.ai.log.AiLogAppender"/> <!--控制台--> <appender name="CONSOLE" class="ch.qos.logback.core.ConsoleAppender"> @@ -55,8 +60,9 @@ </appender> <root level="INFO"> <appender-ref ref="AI_LOG"/> <appender-ref ref="CONSOLE"/> <appender-ref ref="INFO_FILE"/> <appender-ref ref="ERROR_FILE"/> </root> </configuration> </configuration> src/main/resources/mapper/BasCrnpMapper.xml
@@ -19,6 +19,7 @@ <result column="out_station_list" property="outStationList" /> <result column="max_in_task" property="maxInTask" /> <result column="max_out_task" property="maxOutTask" /> <result column="deep_rows" property="deepRows" /> </resultMap> 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/static/js/basCrnp/basCrnp.js
@@ -28,6 +28,7 @@ ,{field: 'inEnable', align: 'center',title: '可入(checkBox)'} ,{field: 'outEnable', align: 'center',title: '可出(checkBox)'} ,{field: 'controlRows', align: 'center',title: '控制库位排号'} ,{field: 'deepRows', align: 'center',title: '深库位排号'} ,{field: 'inStationList', align: 'center',title: '入库站列表'} ,{field: 'outStationList', align: 'center',title: '出库站列表'} ,{field: 'maxInTask', align: 'center',title: '最大入库任务数'} 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> src/main/webapp/views/basCrnp/basCrnp.html
@@ -107,6 +107,12 @@ </div> </div> <div class="layui-form-item"> <label class="layui-form-label">深库位排号: </label> <div class="layui-input-block"> <input class="layui-input" name="deepRows" placeholder="请输入深库位排号"> </div> </div> <div class="layui-form-item"> <label class="layui-form-label">入库站列表: </label> <div class="layui-input-block"> <input class="layui-input" name="inStationList" placeholder="请输入入库站列表"> src/main/webapp/views/index.html
@@ -102,7 +102,7 @@ <div class="layui-body"></div> <!-- 底部 --> <div class="layui-footer layui-text"> copyright © 2023 浙江中扬立库技术有限公司 all rights reserved. copyright © 2026 浙江中扬立库技术有限公司 all rights reserved. <span class="pull-right">Version 1.0.0</span> </div>