| src/main/java/com/zy/ai/service/LlmChatService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/ai/service/WcsDiagnosisService.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/java/com/zy/core/task/MakeMainProcessPseudocodeScheduler.java | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/resources/application.yml | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 | |
| src/main/webapp/views/ai/diagnosis.html | ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史 |
src/main/java/com/zy/ai/service/LlmChatService.java
@@ -14,6 +14,9 @@ import java.util.List; import java.util.function.Consumer; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicBoolean; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; @@ -80,7 +83,6 @@ req.setMax_tokens(maxTokens != null ? maxTokens : 1024); req.setStream(true); System.out.println(JSON.toJSONString(req)); Flux<String> flux = llmWebClient.post() .uri("/chat/completions") @@ -92,35 +94,74 @@ .bodyToFlux(String.class) .doOnError(ex -> log.error("调用 LLM 流式失败", ex)); flux.subscribe(payload -> { String s = payload; if (s == null || s.isEmpty()) return; if (s.startsWith("data:")) { s = s.substring(5); if (s.startsWith(" ")) s = s.substring(1); } // 保留模型输出中的换行,只在判断结束标记时忽略空白 if ("[DONE]".equals(s.trim())) return; AtomicBoolean doneSeen = new AtomicBoolean(false); AtomicBoolean errorSeen = new AtomicBoolean(false); LinkedBlockingQueue<String> queue = new LinkedBlockingQueue<>(); Thread drain = new Thread(() -> { 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"); // 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); while (true) { String s = queue.poll(2, TimeUnit.SECONDS); if (s != null) { try { onChunk.accept(s); } catch (Exception ignore) {} } if (doneSeen.get() && queue.isEmpty()) { if (!errorSeen.get()) { try { if (onComplete != null) onComplete.run(); } catch (Exception ignore) {} } break; } } } catch (Exception ignore) {} } catch (InterruptedException ignore) { ignore.printStackTrace(); } }); drain.setDaemon(true); drain.start(); flux.subscribe(payload -> { if (payload == null || payload.isEmpty()) return; String[] events = payload.split("\\r?\\n\\r?\\n"); for (String part : events) { String s = part; if (s == null || s.isEmpty()) continue; if (s.startsWith("data:")) { s = s.substring(5); if (s.startsWith(" ")) s = s.substring(1); } if ("[DONE]".equals(s.trim())) { doneSeen.set(true); continue; } 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) { try { queue.offer(content); } catch (Exception ignore) {} } } } } catch (Exception e) { e.printStackTrace(); } } }, err -> { errorSeen.set(true); doneSeen.set(true); if (onError != null) onError.accept(err); }, () -> { if (onComplete != null) onComplete.run(); if (!doneSeen.get()) { errorSeen.set(true); doneSeen.set(true); if (onError != null) onError.accept(new RuntimeException("LLM 流意外完成")); } else { doneSeen.set(true); } }); } } src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -90,7 +90,6 @@ llmChatService.chatStream(messages, 0.2, 2048, s -> { try { // SSE 协议不允许原样携带换行,先转为 \n 传输,前端再还原 String safe = s == null ? "" : s.replace("\r", "").replace("\n", "\\n"); if (!safe.isEmpty()) { emitter.send(SseEmitter.event().data(safe)); src/main/java/com/zy/core/task/MakeMainProcessPseudocodeScheduler.java
@@ -25,7 +25,7 @@ @Autowired private RedisUtil redisUtil; @Scheduled(cron = "1 * * * * ? ") @Scheduled(cron = "0/3 * * * * ? ") public void refreshPseudocodeDaily() { try { initMainProcessPseudocode(); src/main/resources/application.yml
@@ -74,3 +74,6 @@ base-url: http://47.76.147.249:9998/e/7g7kqxxt1ei2un71 api-key: app-mP0O6aY5WpbfaHs7BNnjVkli model: deepseek-ai/DeepSeek-V3.2 # base-url: http://34.2.134.223:3000/v1 # api-key: sk-WabrmtOezCFwVo7XvVOrO3QkmfcKG7T7jy0BaVnmQTWm5GXh # model: gemini-3-pro-preview src/main/webapp/views/ai/diagnosis.html
@@ -27,6 +27,7 @@ .avatar { width: 24px; height: 24px; display: flex; align-items: center; margin-right: 8px; } .time { font-size: 12px; color: #909399; text-align: right; margin-top: 6px; } .output .el-card__body { height: 100%; padding: 0; } .assistant-running { display: flex; align-items: center; gap: 8px; color: #909399; } </style> </head> <body> @@ -40,7 +41,7 @@ <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> <!-- <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" /> @@ -56,7 +57,11 @@ <div v-for="(m,i) in messages" :key="i" class="msg" :class="m.role"> <div class="avatar" v-html="m.role === 'assistant' ? assistantIcon : userIcon"></div> <div class="bubble"> <div v-if="m.role === 'assistant'" class="markdown-body" v-html="m.html"></div> <div v-if="m.role === 'assistant' && m.html" class="markdown-body" v-html="m.html"></div> <div v-else-if="m.role === 'assistant' && streaming && i === messages.length - 1" class="assistant-running"> <span v-html="assistantIcon"></span> <span>AI助手正在运行中</span> </div> <div v-else v-text="m.text"></div> <div class="time">{{ m.ts }}</div> </div> @@ -281,15 +286,19 @@ this.source.close(); this.source = null; } var last = this.messages.length > 0 ? this.messages[this.messages.length - 1] : null; if (last && last.role === 'assistant' && this.pendingText && this.pendingText.length > 0) { last.md = (last.md || '') + this.pendingText; this.pendingText = ''; } this.streaming = false; this.loading = false; if (this.typingTimer) { clearInterval(this.typingTimer); this.typingTimer = null; } var last = this.messages.length > 0 ? this.messages[this.messages.length - 1] : null; if (last && last.role === 'assistant') { var renderSource = (last.md || '').replace(/\\n/g, '\n'); var renderSource = (last.md || '').replace(/\n/g, '\n'); last.html = DOMPurify.sanitize(marked.parse(renderSource)); } this.$nextTick(function() { this.scrollToBottom(true); }.bind(this));