From 53fa1addcd22557f6541092a151a3064779d3f93 Mon Sep 17 00:00:00 2001
From: Junjie <DELL@qq.com>
Date: 星期五, 12 十二月 2025 13:28:42 +0800
Subject: [PATCH] #AI

---
 src/main/webapp/views/ai/diagnosis.html                        |   62 +++++++++++-
 src/main/java/com/zy/ai/controller/WcsDiagnosisController.java |   26 ++++
 src/main/java/com/zy/ai/service/WcsDiagnosisService.java       |  161 ++++++++++++++++++++++++++++++++
 src/main/java/com/zy/core/enums/RedisKeyType.java              |    2 
 4 files changed, 243 insertions(+), 8 deletions(-)

diff --git a/src/main/java/com/zy/ai/controller/WcsDiagnosisController.java b/src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
index 41dbf15..07eff9f 100644
--- a/src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
+++ b/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
      */
diff --git a/src/main/java/com/zy/ai/service/WcsDiagnosisService.java b/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
index a61d222..ce9f36e 100644
--- a/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
+++ b/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();
 
diff --git a/src/main/java/com/zy/core/enums/RedisKeyType.java b/src/main/java/com/zy/core/enums/RedisKeyType.java
index a66469a..4e0222d 100644
--- a/src/main/java/com/zy/core/enums/RedisKeyType.java
+++ b/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;
diff --git a/src/main/webapp/views/ai/diagnosis.html b/src/main/webapp/views/ai/diagnosis.html
index e087e17..612b817 100644
--- a/src/main/webapp/views/ai/diagnosis.html
+++ b/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>

--
Gitblit v1.9.1