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,118 @@ 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 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(300); 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); WcsDiagnosisResponse response = diagnose(request); return response; } /** * 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,21 @@ 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; @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,63 @@ 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 java.util.List; @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(); } } src/main/java/com/zy/ai/service/WcsDiagnosisService.java
New file @@ -0,0 +1,117 @@ package com.zy.ai.service; import com.alibaba.fastjson.JSON; import com.zy.ai.entity.ChatCompletionRequest; import com.zy.ai.entity.WcsDiagnosisRequest; 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); } 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/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/resources/application.yml
@@ -65,3 +65,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
@@ -11,6 +11,8 @@ 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"> <encoder> @@ -55,6 +57,7 @@ </appender> <root level="INFO"> <appender-ref ref="AI_LOG"/> <appender-ref ref="CONSOLE"/> <appender-ref ref="INFO_FILE"/> <appender-ref ref="ERROR_FILE"/>