| src/main/java/com/zy/ai/controller/WcsDiagnosisController.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/WcsDiagnosisService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/core/enums/RedisKeyType.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/views/ai/diagnosis.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
@@ -3,10 +3,13 @@ import com.baomidou.mybatisplus.mapper.EntityWrapper; import com.zy.ai.entity.DeviceConfigsData; import com.zy.ai.entity.DeviceRealTimeData; import com.zy.ai.entity.ChatCompletionRequest; 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.core.annotations.ManagerAuth; import com.zy.common.web.BaseController; import com.zy.asrs.entity.BasCrnp; import com.zy.asrs.entity.WrkMast; import com.zy.asrs.service.BasCrnpService; @@ -33,7 +36,7 @@ @RestController @RequestMapping("/ai/diagnose") @RequiredArgsConstructor public class WcsDiagnosisController { public class WcsDiagnosisController extends BaseController { @Autowired private WcsDiagnosisService wcsDiagnosisService; @@ -178,7 +181,9 @@ } @GetMapping("/askStream") public SseEmitter askStream(@RequestParam("prompt") String prompt) { public SseEmitter askStream(@RequestParam("prompt") String prompt, @RequestParam(value = "chatId", required = false) String chatId, @RequestParam(value = "reset", required = false, defaultValue = "false") boolean reset) { SseEmitter emitter = new SseEmitter(0L); new Thread(() -> { try { @@ -237,7 +242,7 @@ request.setDeviceRealtimeData(deviceRealTimeDataList); request.setDeviceConfigs(deviceConfigsDataList); wcsDiagnosisService.askStream(request, prompt, emitter); wcsDiagnosisService.askStream(request, prompt, chatId, reset, emitter); } catch (Exception e) { emitter.completeWithError(e); } @@ -245,6 +250,21 @@ return emitter; } @GetMapping("/chats") public List<Map<String, Object>> listChats() { return wcsDiagnosisService.listChats(); } @DeleteMapping("/chats/{chatId}") public Boolean deleteChat(@PathVariable("chatId") String chatId) { return wcsDiagnosisService.deleteChat(chatId); } @GetMapping("/chats/{chatId}/history") public List<ChatCompletionRequest.Message> getChatHistory(@PathVariable("chatId") String chatId) { return wcsDiagnosisService.getChatHistory(chatId); } /** * POST /api/ai/diagnose/wcs */ src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -3,18 +3,23 @@ import com.alibaba.fastjson.JSON; import com.zy.ai.entity.ChatCompletionRequest; import com.zy.ai.entity.WcsDiagnosisRequest; import com.zy.common.utils.RedisUtil; import com.zy.core.enums.RedisKeyType; 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; import java.util.Map; @Service @RequiredArgsConstructor public class WcsDiagnosisService { private final LlmChatService llmChatService; private final RedisUtil redisUtil; private static final long CHAT_TTL_SECONDS = 7L * 24 * 3600; /** * 针对“系统不执行任务 / 不知道哪个设备没在运行”的通用 AI 诊断 @@ -134,6 +139,162 @@ }); } public void askStream(WcsDiagnosisRequest request, String prompt, String chatId, boolean reset, SseEmitter emitter) { List<ChatCompletionRequest.Message> base = new ArrayList<>(); ChatCompletionRequest.Message system = new ChatCompletionRequest.Message(); system.setRole("system"); system.setContent( "你是一名资深 WCS(仓储控制系统)与自动化立库专家,熟悉:堆垛机、输送线、提升机、穿梭车等设备的任务分配和运行逻辑,也熟悉常见的系统卡死、任务不执行、设备空闲但无任务等问题模式。\n\n" + "在回答用户问题时,需要结合下面给出的系统当前上下文信息(任务、设备实时状态、设备配置、系统日志等),以简洁、明确的中文作答,并在需要时给出可执行的排查建议。" ); base.add(system); List<ChatCompletionRequest.Message> history = null; String historyKey = null; String metaKey = null; if (chatId != null && !chatId.isEmpty()) { historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; metaKey = RedisKeyType.AI_CHAT_META.key + chatId; if (reset) { redisUtil.del(historyKey, metaKey); } List<Object> stored = redisUtil.lGet(historyKey, 0, -1); if (stored != null && !stored.isEmpty()) { history = new ArrayList<>(stored.size()); for (Object o : stored) { ChatCompletionRequest.Message m = convertToMessage(o); if (m != null) history.add(m); } if (!history.isEmpty()) base.addAll(history); } else { history = new ArrayList<>(); } } ChatCompletionRequest.Message contextMsg = new ChatCompletionRequest.Message(); contextMsg.setRole("user"); contextMsg.setContent(buildUserContent(request)); base.add(contextMsg); ChatCompletionRequest.Message questionMsg = new ChatCompletionRequest.Message(); questionMsg.setRole("user"); questionMsg.setContent("【用户提问】\n" + (prompt == null ? "" : prompt)); base.add(questionMsg); StringBuilder assistantBuffer = new StringBuilder(); final String finalChatId = chatId; final String finalHistoryKey = historyKey; final String finalMetaKey = metaKey; final String finalPrompt = prompt; llmChatService.chatStream(base, 0.2, 2048, s -> { try { String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n"); if (!safe.isEmpty()) { emitter.send(SseEmitter.event().data(safe)); assistantBuffer.append(s); } } catch (Exception ignore) {} }, () -> { try { if (finalChatId != null && !finalChatId.isEmpty()) { ChatCompletionRequest.Message q = new ChatCompletionRequest.Message(); q.setRole("user"); q.setContent(finalPrompt == null ? "" : finalPrompt); ChatCompletionRequest.Message a = new ChatCompletionRequest.Message(); a.setRole("assistant"); a.setContent(assistantBuffer.toString()); redisUtil.lSet(finalHistoryKey, q); redisUtil.lSet(finalHistoryKey, a); redisUtil.expire(finalHistoryKey, CHAT_TTL_SECONDS); Map<Object, Object> old = redisUtil.hmget(finalMetaKey); Long createdAt = old != null && old.get("createdAt") != null ? (old.get("createdAt") instanceof Number ? ((Number) old.get("createdAt")).longValue() : Long.valueOf(String.valueOf(old.get("createdAt")))) : System.currentTimeMillis(); Map<String, Object> meta = new java.util.HashMap<>(); meta.put("chatId", finalChatId); meta.put("title", buildTitleFromPrompt(finalPrompt)); meta.put("createdAt", createdAt); meta.put("updatedAt", System.currentTimeMillis()); redisUtil.hmset(finalMetaKey, meta, CHAT_TTL_SECONDS); } emitter.complete(); } catch (Exception ignore) {} }, e -> { try { emitter.completeWithError(e); } catch (Exception ignore) {} }); } public List<Map<String, Object>> listChats() { java.util.Set<String> keys = redisUtil.scanKeys(RedisKeyType.AI_CHAT_META.key, 1000); List<Map<String, Object>> resp = new ArrayList<>(); if (keys != null) { for (String key : keys) { Map<Object, Object> m = redisUtil.hmget(key); if (m != null && !m.isEmpty()) { java.util.HashMap<String, Object> item = new java.util.HashMap<>(); for (Map.Entry<Object, Object> e : m.entrySet()) { item.put(String.valueOf(e.getKey()), e.getValue()); } String chatId = String.valueOf(item.get("chatId")); String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; item.put("size", redisUtil.lGetListSize(historyKey)); resp.add(item); } } } return resp; } public boolean deleteChat(String chatId) { if (chatId == null || chatId.isEmpty()) return false; String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; String metaKey = RedisKeyType.AI_CHAT_META.key + chatId; redisUtil.del(historyKey, metaKey); return true; } public List<ChatCompletionRequest.Message> getChatHistory(String chatId) { if (chatId == null || chatId.isEmpty()) return java.util.Collections.emptyList(); String historyKey = RedisKeyType.AI_CHAT_HISTORY.key + chatId; List<Object> stored = redisUtil.lGet(historyKey, 0, -1); List<ChatCompletionRequest.Message> result = new ArrayList<>(); if (stored != null) { for (Object o : stored) { ChatCompletionRequest.Message m = convertToMessage(o); if (m != null) result.add(m); } } return result; } private ChatCompletionRequest.Message convertToMessage(Object o) { if (o instanceof ChatCompletionRequest.Message) { return (ChatCompletionRequest.Message) o; } if (o instanceof Map) { Map<?, ?> map = (Map<?, ?>) o; ChatCompletionRequest.Message m = new ChatCompletionRequest.Message(); Object role = map.get("role"); Object content = map.get("content"); m.setRole(role == null ? null : String.valueOf(role)); m.setContent(content == null ? null : String.valueOf(content)); return m; } return null; } private String buildTitleFromPrompt(String prompt) { if (prompt == null || prompt.isEmpty()) return "未命名会话"; String p = prompt.replaceAll("\n", " ").trim(); return p.length() > 20 ? p.substring(0, 20) : p; } private String buildUserContent(WcsDiagnosisRequest request) { StringBuilder sb = new StringBuilder(); src/main/java/com/zy/core/enums/RedisKeyType.java
@@ -32,6 +32,8 @@ CRN_IO_EXECUTE_FINISH_LIMIT("crn_io_execute_finish_limit_"), CURRENT_CIRCLE_TASK_CRN_NO("current_circle_task_crn_no_"), AI_CHAT_HISTORY("ai_chat_history_"), AI_CHAT_META("ai_chat_meta_"), ; public String key; src/main/webapp/views/ai/diagnosis.html
@@ -9,7 +9,7 @@ body { background: #f5f7fa; } .container { max-width: 1100px; margin: 24px auto; } .actions { display: flex; gap: 12px; align-items: center; } .output { height: 65vh; } .output { height: 60vh; } .markdown-body { font-size: 14px; line-height: 1.4; white-space: pre-wrap; word-break: break-word; } .markdown-body p { margin: 4px 0; } .markdown-body ul, .markdown-body ol { margin: 4px 0 4px 16px; padding: 0; } @@ -37,11 +37,16 @@ <span>WCS AI 助手</span> </div> <div class="actions"> <div class="actions" style="flex-wrap: wrap;"> <el-button type="primary" :loading="loading" :disabled="streaming" @click="start">一键诊断系统</el-button> <el-button type="warning" :disabled="!streaming" @click="stop">停止</el-button> <el-button @click="clear">清空</el-button> <span class="status">{{ statusText }}</span> <el-button @click="clear">清空当前聊天</el-button> <span class="status" style="margin-right: 12px;">{{ statusText }}</span> <el-select v-model="currentChatId" placeholder="选择会话" style="min-width:240px;" @change="switchChat" :disabled="streaming"> <el-option v-for="c in chats" :key="c.chatId" :label="(c.title||('会话 '+c.chatId)) + '('+(c.size||0)+')'" :value="c.chatId" /> </el-select> <el-button type="success" plain icon="el-icon-plus" @click="newChat" :disabled="streaming">新会话</el-button> <el-button type="danger" plain icon="el-icon-delete" @click="deleteChat" :disabled="!currentChatId || streaming">删除会话</el-button> </div> <el-divider></el-divider> @@ -102,7 +107,10 @@ renderIntervalMs: 120, stepChars: 6, userInput: '', autoScrollThreshold: 80 autoScrollThreshold: 80, chats: [], currentChatId: '', resetting: false }; }, computed: { @@ -117,6 +125,42 @@ } }, methods: { loadChats: function() { var self = this; fetch(baseUrl + '/ai/diagnose/chats', { headers: { 'token': localStorage.getItem('token') } }) .then(function(r){ return r.json(); }) .then(function(arr){ if (Array.isArray(arr)) { self.chats = arr; } }); }, switchChat: function() { var self = this; if (!self.currentChatId) { self.clear(); return; } fetch(baseUrl + '/ai/diagnose/chats/' + encodeURIComponent(self.currentChatId) + '/history', { headers: { 'token': localStorage.getItem('token') } }) .then(function(r){ return r.json(); }) .then(function(arr){ if (!Array.isArray(arr)) return; var msgs = []; for (var i=0;i<arr.length;i++) { var m = arr[i]; if (m.role === 'assistant') msgs.push({ role: 'assistant', md: m.content || '', html: DOMPurify.sanitize(marked.parse((m.content||'').replace(/\\n/g,'\n'))), ts: self.nowStr() }); else msgs.push({ role: 'user', text: m.content || '', ts: self.nowStr() }); } self.messages = msgs; self.$nextTick(function(){ self.scrollToBottom(true); }); }); }, newChat: function() { var id = Date.now() + '_' + Math.random().toString(36).substr(2,8); this.currentChatId = id; this.resetting = true; this.clear(); }, deleteChat: function() { var self = this; if (!self.currentChatId) return; fetch(baseUrl + '/ai/diagnose/chats/' + encodeURIComponent(self.currentChatId), { method: 'DELETE', headers: { 'token': localStorage.getItem('token') } }) .then(function(r){ return r.json(); }) .then(function(ok){ if (ok === true) { self.currentChatId = ''; self.clear(); self.loadChats(); self.newChat(); } }); }, shouldAutoScroll: function() { var el = this.$refs.chat; if (!el) return false; @@ -150,6 +194,8 @@ this.messages.push({ role: 'assistant', md: '', html: '', ts: this.nowStr() }); this.scrollToBottom(true); var url = baseUrl + '/ai/diagnose/askStream?prompt=' + encodeURIComponent(msg); if (this.currentChatId) url += '&chatId=' + encodeURIComponent(this.currentChatId); if (this.resetting) url += '&reset=true'; this.source = new EventSource(url); var self = this; this.source.onopen = function() { @@ -169,6 +215,7 @@ self.stop(); }; this.userInput = ''; this.resetting = false; }, start: function() { if (this.streaming) return; @@ -246,12 +293,17 @@ last.html = DOMPurify.sanitize(marked.parse(renderSource)); } this.$nextTick(function() { this.scrollToBottom(true); }.bind(this)); this.loadChats(); }, clear: function() { this.messages = []; this.pendingText = ''; } } ,mounted: function() { this.loadChats(); this.newChat(); } }); </script> </body>