From 7fef7f8c7007eee9a54c7f6255391f7e46885825 Mon Sep 17 00:00:00 2001
From: Junjie <DELL@qq.com>
Date: 星期五, 12 十二月 2025 08:28:00 +0800
Subject: [PATCH] #AI
---
src/main/webapp/views/ai/diagnosis.html | 149 +++++++++++++++++++++++++++++++++++++
/dev/null | 49 ------------
src/main/java/com/zy/ai/service/LlmChatService.java | 14 ++
src/main/java/com/zy/ai/service/WcsDiagnosisService.java | 8 +
src/main/resources/application.yml | 2
5 files changed, 168 insertions(+), 54 deletions(-)
diff --git a/src/main/java/com/zy/ai/service/LlmChatService.java b/src/main/java/com/zy/ai/service/LlmChatService.java
index e8c7a77..5e709af 100644
--- a/src/main/java/com/zy/ai/service/LlmChatService.java
+++ b/src/main/java/com/zy/ai/service/LlmChatService.java
@@ -91,10 +91,14 @@
.doOnError(ex -> log.error("璋冪敤 LLM 娴佸紡澶辫触", ex));
flux.subscribe(payload -> {
- String s = payload == null ? null : payload.trim();
+ String s = payload;
if (s == null || s.isEmpty()) return;
- if (s.startsWith("data:")) s = s.substring(5).trim();
- if ("[DONE]".equals(s)) return;
+ if (s.startsWith("data:")) {
+ s = s.substring(5);
+ if (s.startsWith(" ")) s = s.substring(1);
+ }
+ // 淇濈暀妯″瀷杈撳嚭涓殑鎹㈣锛屽彧鍦ㄥ垽鏂粨鏉熸爣璁版椂蹇界暐绌虹櫧
+ if ("[DONE]".equals(s.trim())) return;
try {
JSONObject obj = JSON.parseObject(s);
JSONArray choices = obj.getJSONArray("choices");
@@ -103,6 +107,10 @@
JSONObject delta = c0.getJSONObject("delta");
if (delta != null) {
String content = delta.getString("content");
+// log.info("chunk = [{}] len = {}", content, content.length());
+// for (char ch : content.toCharArray()) {
+// log.info("char: {} ({})", (int) ch, ch == '\n' ? "\\n" : ch);
+// }
if (content != null) onChunk.accept(content);
}
}
diff --git a/src/main/java/com/zy/ai/service/WcsDiagnosisService.java b/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
index 64ad536..93cecbb 100644
--- a/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
+++ b/src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -85,7 +85,13 @@
messages.add(user);
llmChatService.chatStream(messages, 0.2, 2048, s -> {
- try { emitter.send(SseEmitter.event().data(s)); } catch (Exception ignore) {}
+ try {
+ // SSE 鍗忚涓嶅厑璁稿師鏍锋惡甯︽崲琛岋紝鍏堣浆涓� \n 浼犺緭锛屽墠绔啀杩樺師
+ String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n");
+ if (!safe.isEmpty()) {
+ emitter.send(SseEmitter.event().data(safe));
+ }
+ } catch (Exception ignore) {}
}, () -> {
try { emitter.complete(); } catch (Exception ignore) {}
}, e -> {
diff --git a/src/main/resources/application.yml b/src/main/resources/application.yml
index e85c675..011d12e 100644
--- a/src/main/resources/application.yml
+++ b/src/main/resources/application.yml
@@ -70,4 +70,4 @@
llm:
base-url: https://api.siliconflow.cn/v1
api-key: sk-sxdtebtquwrugzrmaqqqkzdzmrgzhzmplwwuowysdasccent
- model: Qwen/Qwen3-VL-32B-Instruct
+ model: deepseek-ai/DeepSeek-V3.2
diff --git a/src/main/webapp/static/js/ai/diagnose.js b/src/main/webapp/static/js/ai/diagnose.js
deleted file mode 100644
index 0b23ae0..0000000
--- a/src/main/webapp/static/js/ai/diagnose.js
+++ /dev/null
@@ -1,43 +0,0 @@
-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
deleted file mode 100644
index 67e952b..0000000
--- a/src/main/webapp/views/ai/diagnose.html
+++ /dev/null
@@ -1,49 +0,0 @@
-<!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>
diff --git a/src/main/webapp/views/ai/diagnosis.html b/src/main/webapp/views/ai/diagnosis.html
new file mode 100644
index 0000000..f046639
--- /dev/null
+++ b/src/main/webapp/views/ai/diagnosis.html
@@ -0,0 +1,149 @@
+<!DOCTYPE html>
+<html lang="zh-CN">
+<head>
+ <meta charset="UTF-8" />
+ <meta name="viewport" content="width=device-width, initial-scale=1.0" />
+ <title>WCS AI 璇婃柇</title>
+ <link rel="stylesheet" href="../../static/vue/element/element.css" />
+ <style>
+ body { background: #f5f7fa; }
+ .container { max-width: 1100px; margin: 24px auto; }
+ .actions { display: flex; gap: 12px; align-items: center; }
+ .output { min-height: 360px; }
+ .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; }
+ .markdown-body h1, .markdown-body h2, .markdown-body h3 { margin-top: 8px; }
+ .markdown-body pre { background: #f6f8fa; padding: 12px; border-radius: 6px; overflow: auto; }
+ .status { color: #909399; }
+ </style>
+</head>
+<body>
+ <div id="app" class="container">
+ <el-card shadow="hover">
+ <div slot="header" class="clearfix">
+ <span>WCS AI 璇婃柇锛圫SE 娴佸紡杈撳嚭锛�</span>
+ </div>
+
+ <div class="actions">
+ <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>
+ </div>
+
+ <el-divider></el-divider>
+
+ <el-card class="output" shadow="never">
+ <div class="markdown-body" v-html="renderedHtml"></div>
+ </el-card>
+ </el-card>
+ </div>
+
+ <script type="text/javascript" src="../../static/vue/js/vue.min.js"></script>
+ <script type="text/javascript" src="../../static/vue/element/element.js"></script>
+ <script type="text/javascript" src="../../static/js/common.js" charset="utf-8"></script>
+ <script src="https://cdn.jsdelivr.net/npm/marked/marked.min.js"></script>
+ <script src="https://cdn.jsdelivr.net/npm/dompurify@2.4.3/dist/purify.min.js"></script>
+ <script>
+ marked.setOptions({
+ gfm: true,
+ breaks: true
+ });
+
+ new Vue({
+ el: '#app',
+ data: function() {
+ return {
+ loading: false,
+ streaming: false,
+ source: null,
+ markdownBuffer: '',
+ renderedHtml: '',
+ pendingText: '',
+ typingTimer: null,
+ lastRenderTs: 0,
+ renderIntervalMs: 120,
+ stepChars: 6
+ };
+ },
+ computed: {
+ statusText: function() {
+ if (this.streaming) return '璇婃柇杩涜涓紙娴佸紡杈撳嚭锛�';
+ if (this.loading) return '杩炴帴涓�';
+ return '绌洪棽';
+ }
+ },
+ methods: {
+ start: function() {
+ if (this.streaming) return;
+ this.clear();
+ this.loading = true;
+ this.streaming = true;
+ var url = baseUrl + '/ai/diagnose/runAiStream';
+ this.source = new EventSource(url);
+ var self = this;
+ this.source.onopen = function() {
+ self.loading = false;
+ };
+ this.source.onmessage = function(e) {
+ if (!e || !e.data) return;
+ // 鍚庣鎶婃崲琛岃浆涔夋垚 \n锛岃繖閲岃繕鍘熶负鐪熸鐨勬崲琛�
+ var chunk = (e.data || '').replace(/\\n/g, '\n');
+ if (!chunk) return;
+ // 濡傛灉浠呭寘鍚崲琛岀锛堝鍗曠嫭 \n 鎴� \n\n锛夛紝涓㈠純閬垮厤绌虹櫧琛�
+ var normalized = chunk.replace(/\r/g, '');
+ if (/^\n+$/.test(normalized)) return;
+ self.pendingText += chunk;
+ self.ensureTyping();
+ };
+ this.source.onerror = function() {
+ self.stop();
+ };
+ },
+ ensureTyping: function() {
+ if (this.typingTimer) return;
+ var self = this;
+ this.typingTimer = setInterval(function() {
+ if (!self.streaming && self.pendingText.length === 0) {
+ clearInterval(self.typingTimer);
+ self.typingTimer = null;
+ return;
+ }
+ if (self.pendingText.length > 0) {
+ var n = Math.min(self.stepChars, self.pendingText.length);
+ self.markdownBuffer += self.pendingText.slice(0, n);
+ self.pendingText = self.pendingText.slice(n);
+ }
+ var now = Date.now();
+ if (now - self.lastRenderTs > self.renderIntervalMs) {
+ self.lastRenderTs = now;
+ var renderSource = self.markdownBuffer.replace(/\\n/g, '\n');
+ self.renderedHtml = DOMPurify.sanitize(marked.parse(renderSource));
+ }
+ }, 50);
+ },
+ stop: function() {
+ if (this.source) {
+ this.source.close();
+ this.source = null;
+ }
+ this.streaming = false;
+ this.loading = false;
+ if (this.typingTimer) {
+ clearInterval(this.typingTimer);
+ this.typingTimer = null;
+ }
+ var renderSource = this.markdownBuffer.replace(/\\n/g, '\n');
+ this.renderedHtml = DOMPurify.sanitize(marked.parse(renderSource));
+ },
+ clear: function() {
+ this.markdownBuffer = '';
+ this.renderedHtml = '';
+ this.pendingText = '';
+ }
+ }
+ });
+ </script>
+</body>
+</html>
--
Gitblit v1.9.1