From 70af762aa04fccfd68f211a9991c9a36f0228dfd Mon Sep 17 00:00:00 2001
From: Junjie <DELL@qq.com>
Date: 星期四, 11 十二月 2025 17:53:43 +0800
Subject: [PATCH] #AI

---
 src/main/java/com/zy/ai/service/LlmChatService.java            |   55 ++++++++++
 src/main/java/com/zy/ai/entity/ChatCompletionRequest.java      |    3 
 src/main/java/com/zy/ai/controller/WcsDiagnosisController.java |   78 +++++++++++++++
 src/main/java/com/zy/ai/service/WcsDiagnosisService.java       |   41 ++++++++
 src/main/webapp/static/js/ai/diagnose.js                       |   43 ++++++++
 src/main/webapp/views/ai/diagnose.html                         |   49 +++++++++
 6 files changed, 264 insertions(+), 5 deletions(-)

diff --git a/src/main/java/com/zy/ai/controller/WcsDiagnosisController.java b/src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
index 34167fa..45c90e3 100644
--- a/src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
+++ b/src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
@@ -22,6 +22,7 @@
 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;
@@ -47,7 +48,7 @@
 
         request.setAlarmMessage("绯荤粺涓嶆墽琛屼换鍔�");
 
-        List<String> logs = AiLogAppender.getRecentLogs(300);
+        List<String> logs = AiLogAppender.getRecentLogs(100);
         request.setLogs(logs);
 
         List<WrkMast> wrkMasts = wrkMastService.selectList(new EntityWrapper<>());
@@ -99,8 +100,81 @@
         }
 
         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;
     }
 
     /**
@@ -115,4 +189,4 @@
         resp.setOriginalRequest(request);
         return resp;
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/zy/ai/entity/ChatCompletionRequest.java b/src/main/java/com/zy/ai/entity/ChatCompletionRequest.java
index 599bee0..690cdb5 100644
--- a/src/main/java/com/zy/ai/entity/ChatCompletionRequest.java
+++ b/src/main/java/com/zy/ai/entity/ChatCompletionRequest.java
@@ -12,10 +12,11 @@
     // 鍙�夊弬鏁�
     private Double temperature;
     private Integer max_tokens;
+    private Boolean stream;
 
     @Data
     public static class Message {
         private String role;    // "user" / "assistant" / "system"
         private String content;
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/java/com/zy/ai/service/LlmChatService.java b/src/main/java/com/zy/ai/service/LlmChatService.java
index b001d0e..e8c7a77 100644
--- a/src/main/java/com/zy/ai/service/LlmChatService.java
+++ b/src/main/java/com/zy/ai/service/LlmChatService.java
@@ -10,8 +10,13 @@
 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
@@ -60,4 +65,52 @@
 
         return response.getChoices().get(0).getMessage().getContent();
     }
-}
\ No newline at end of file
+
+    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();
+        });
+    }
+}
diff --git a/src/main/java/com/zy/ai/service/WcsDiagnosisService.java b/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
index 9f90c1d..64ad536 100644
--- a/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
+++ b/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -3,6 +3,7 @@
 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;
 
@@ -52,6 +53,44 @@
 
         // 璋冪敤澶фā鍨�
         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锛夐澶栦笂涓嬫枃锛坋xtraContext锛夛細濡備粨搴撲唬鐮併�乄CS 鐗堟湰绛塡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) {
@@ -114,4 +153,4 @@
 
         return sb.toString();
     }
-}
\ No newline at end of file
+}
diff --git a/src/main/webapp/static/js/ai/diagnose.js b/src/main/webapp/static/js/ai/diagnose.js
new file mode 100644
index 0000000..0b23ae0
--- /dev/null
+++ b/src/main/webapp/static/js/ai/diagnose.js
@@ -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(''); });
+});
diff --git a/src/main/webapp/views/ai/diagnose.html b/src/main/webapp/views/ai/diagnose.html
new file mode 100644
index 0000000..67e952b
--- /dev/null
+++ b/src/main/webapp/views/ai/diagnose.html
@@ -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>

--
Gitblit v1.9.1