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