#
Junjie
2 天以前 338f3b81425ab96d8c856909a775124af5365e3c
#
7个文件已修改
173 ■■■■■ 已修改文件
src/main/java/com/zy/ai/controller/WcsDiagnosisController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/entity/AiChatMessage.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/entity/ChatCompletionRequest.java 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/WcsDiagnosisService.java 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/service/impl/AiChatStoreServiceImpl.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sql/20260312_create_sys_ai_chat_storage.sql 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/webapp/views/ai/diagnosis.html 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/zy/ai/controller/WcsDiagnosisController.java
@@ -25,13 +25,14 @@
    private AiUtils aiUtils;
    @GetMapping("/runAiStream")
    public SseEmitter runAiStream() {
    public SseEmitter runAiStream(@RequestParam(value = "chatId", required = false) String chatId,
                                  @RequestParam(value = "reset", required = false, defaultValue = "false") boolean reset) {
        SseEmitter emitter = new SseEmitter(0L);
        new Thread(() -> {
            try {
                WcsDiagnosisRequest request = aiUtils.makeAiRequest(1000, "对当前系统进行巡检,如果有异常情况就进行详细的分析,如果没有异常情况则当成一次检查\n\n");
                wcsDiagnosisService.diagnoseStream(request, emitter);
                wcsDiagnosisService.diagnoseStream(request, chatId, reset, emitter);
            } catch (Exception e) {
                try { emitter.complete(); } catch (Exception ignore) {}
            }
src/main/java/com/zy/ai/entity/AiChatMessage.java
@@ -28,6 +28,9 @@
    private String content;
    @TableField("reasoning_content")
    private String reasoningContent;
    @TableField("create_time")
    private Date createTime;
}
src/main/java/com/zy/ai/entity/ChatCompletionRequest.java
@@ -21,6 +21,7 @@
    public static class Message {
        private String role;    // "user" / "assistant" / "system"
        private String content;
        private String reasoningContent;
        private String name;
        private String tool_call_id;
        private List<ToolCall> tool_calls;
src/main/java/com/zy/ai/service/WcsDiagnosisService.java
@@ -35,8 +35,14 @@
    @Autowired
    private AiChatStoreService aiChatStoreService;
    public void diagnoseStream(WcsDiagnosisRequest request, SseEmitter emitter) {
    public void diagnoseStream(WcsDiagnosisRequest request,
                               String chatId,
                               boolean reset,
                               SseEmitter emitter) {
        List<ChatCompletionRequest.Message> messages = new ArrayList<>();
        if (chatId != null && !chatId.isEmpty() && reset) {
            aiChatStoreService.deleteChat(chatId);
        }
        AiPromptTemplate promptTemplate = aiPromptTemplateService.resolvePublished(AiPromptScene.DIAGNOSE_STREAM.getCode());
        ChatCompletionRequest.Message mcpSystem = new ChatCompletionRequest.Message();
@@ -47,7 +53,11 @@
        mcpUser.setRole("user");
        mcpUser.setContent(aiUtils.buildDiagnosisUserContentMcp(request));
        runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, promptTemplate, 0.3, 2048, emitter, null);
        ChatCompletionRequest.Message storedUser = new ChatCompletionRequest.Message();
        storedUser.setRole("user");
        storedUser.setContent(buildDiagnoseDisplayPrompt(request));
        runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, storedUser, promptTemplate, 0.3, 2048, emitter, chatId);
    }
    public void askStream(String prompt,
@@ -77,7 +87,7 @@
        mcpUser.setRole("user");
        mcpUser.setContent(prompt == null ? "" : prompt);
        runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, promptTemplate, 0.3, 2048, emitter, finalChatId);
        runMcpStreamingDiagnosis(messages, mcpSystem, mcpUser, mcpUser, promptTemplate, 0.3, 2048, emitter, finalChatId);
    }
    public List<Map<String, Object>> listChats() {
@@ -101,6 +111,7 @@
    private void runMcpStreamingDiagnosis(List<ChatCompletionRequest.Message> baseMessages,
                                          ChatCompletionRequest.Message systemPrompt,
                                          ChatCompletionRequest.Message userQuestion,
                                          ChatCompletionRequest.Message storedUserQuestion,
                                          AiPromptTemplate promptTemplate,
                                          Double temperature,
                                          Integer maxTokens,
@@ -115,6 +126,7 @@
                throw new IllegalStateException("No MCP tools registered");
            }
            AgentUsageStats usageStats = new AgentUsageStats();
            StringBuilder reasoningBuffer = new StringBuilder();
            baseMessages.add(systemPrompt);
            baseMessages.add(userQuestion);
@@ -123,11 +135,13 @@
            messages.addAll(baseMessages);
 
            sse(emitter, "<think>\\n正在初始化诊断与工具环境...\\n");
            appendReasoning(reasoningBuffer, "正在初始化诊断与工具环境...\n");
            int maxRound = 10;
            int i = 0;
            while(true) {
                sse(emitter, "\\n正在分析(第" + (i + 1) + "轮)...\\n");
                appendReasoning(reasoningBuffer, "\n正在分析(第" + (i + 1) + "轮)...\n");
                ChatCompletionResponse resp = llmChatService.chatCompletion(messages, temperature, maxTokens, tools);
                if (resp == null || resp.getChoices() == null || resp.getChoices().isEmpty() || resp.getChoices().get(0).getMessage() == null) {
                    throw new IllegalStateException("LLM returned empty response");
@@ -137,6 +151,7 @@
                ChatCompletionRequest.Message assistant = resp.getChoices().get(0).getMessage();
                messages.add(assistant);
                sse(emitter, assistant.getContent());
                appendReasoning(reasoningBuffer, assistant == null ? null : assistant.getContent());
                List<ChatCompletionRequest.ToolCall> toolCalls = assistant.getTool_calls();
                if (toolCalls == null || toolCalls.isEmpty()) {
@@ -147,6 +162,7 @@
                    String toolName = tc != null && tc.getFunction() != null ? tc.getFunction().getName() : null;
                    if (toolName == null || toolName.trim().isEmpty()) continue;
                    sse(emitter, "\\n准备调用工具:" + toolName + "\\n");
                    appendReasoning(reasoningBuffer, "\n准备调用工具:" + toolName + "\n");
                    JSONObject args = new JSONObject();
                    if (tc.getFunction() != null && tc.getFunction().getArguments() != null && !tc.getFunction().getArguments().trim().isEmpty()) {
                        try {
@@ -166,6 +182,7 @@
                        output = err;
                    }
                    sse(emitter, "\\n工具返回,正在继续推理...\\n");
                    appendReasoning(reasoningBuffer, "\n工具返回,正在继续推理...\n");
                    ChatCompletionRequest.Message toolMsg = new ChatCompletionRequest.Message();
                    toolMsg.setRole("tool");
                    toolMsg.setTool_call_id(tc == null ? null : tc.getId());
@@ -177,6 +194,7 @@
            }
            sse(emitter, "\\n正在根据数据进行分析...\\n</think>\\n\\n");
            appendReasoning(reasoningBuffer, "\n正在根据数据进行分析...\n");
            ChatCompletionRequest.Message diagnosisMessage = new ChatCompletionRequest.Message();
            diagnosisMessage.setRole("system");
@@ -203,9 +221,10 @@
                        ChatCompletionRequest.Message a = new ChatCompletionRequest.Message();
                        a.setRole("assistant");
                        a.setContent(assistantBuffer.toString());
                        a.setReasoningContent(reasoningBuffer.toString());
                        aiChatStoreService.saveConversation(chatId,
                                buildTitleFromPrompt(userQuestion.getContent()),
                                userQuestion,
                                buildTitleFromPrompt(storedUserQuestion == null ? null : storedUserQuestion.getContent()),
                                storedUserQuestion == null ? userQuestion : storedUserQuestion,
                                a,
                                promptTemplate,
                                usageStats.getPromptTokens(),
@@ -260,6 +279,13 @@
        payload.put("totalTokens", usageStats.getTotalTokens());
        payload.put("llmCallCount", usageStats.getLlmCallCount());
        return payload;
    }
    private void appendReasoning(StringBuilder reasoningBuffer, String text) {
        if (reasoningBuffer == null || text == null || text.isEmpty()) {
            return;
        }
        reasoningBuffer.append(text);
    }
    private void sendLargeText(SseEmitter emitter, String text) {
@@ -378,6 +404,13 @@
        }
    }
    private String buildDiagnoseDisplayPrompt(WcsDiagnosisRequest request) {
        if (request == null || request.getAlarmMessage() == null || request.getAlarmMessage().trim().isEmpty()) {
            return "对当前系统进行巡检";
        }
        return request.getAlarmMessage().trim();
    }
    private static class AgentUsageStats {
        private long promptTokens;
        private long completionTokens;
src/main/java/com/zy/ai/service/impl/AiChatStoreServiceImpl.java
@@ -87,6 +87,7 @@
            ChatCompletionRequest.Message message = new ChatCompletionRequest.Message();
            message.setRole(row.getRole());
            message.setContent(row.getContent());
            message.setReasoningContent(row.getReasoningContent());
            result.add(message);
        }
        return result;
@@ -166,6 +167,7 @@
        row.setSeqNo(seqNo);
        row.setRole(cut(source.getRole(), 32));
        row.setContent(source.getContent());
        row.setReasoningContent(source.getReasoningContent());
        row.setCreateTime(now);
        aiChatMessageMapper.insert(row);
    }
src/main/resources/sql/20260312_create_sys_ai_chat_storage.sql
@@ -29,6 +29,7 @@
  `seq_no` INT NOT NULL COMMENT '顺序号',
  `role` VARCHAR(32) NOT NULL COMMENT '角色:user/assistant',
  `content` LONGTEXT COMMENT '消息内容',
  `reasoning_content` LONGTEXT COMMENT 'AI深度思考内容',
  `create_time` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  PRIMARY KEY (`id`),
  KEY `idx_sys_ai_chat_message_chat_seq` (`chat_id`, `seq_no`)
src/main/webapp/views/ai/diagnosis.html
@@ -374,6 +374,18 @@
      margin-bottom: 0;
    }
    .markdown-body > details.think-block + h1,
    .markdown-body > details.think-block + h2,
    .markdown-body > details.think-block + h3,
    .markdown-body > details.think-block + h4,
    .markdown-body > details.think-block + p,
    .markdown-body > details.think-block + ul,
    .markdown-body > details.think-block + ol,
    .markdown-body > details.think-block + pre,
    .markdown-body > details.think-block + blockquote {
      margin-top: 0;
    }
    .markdown-body p {
      margin: 0 0 10px;
    }
@@ -680,6 +692,7 @@
          renderIntervalMs: 120,
          stepChars: 6,
          runTokenUsage: null,
          sessionTokenBase: null,
          userInput: '',
          autoScrollThreshold: 80,
          chats: [],
@@ -732,14 +745,15 @@
          var current = this.findChat(this.currentChatId);
          if (!current && this.resetting) return '新建会话,等待首条消息';
          if (!current) return '会话 ' + this.currentChatId;
          var tokenText = this.tokenSummaryText(current);
          var tokenText = this.sessionTokenSummaryText(current, '累计');
          return tokenText ? (this.chatLabel(current) + ' · ' + tokenText) : this.chatLabel(current);
        },
        currentChatTokenSummary: function() {
          var current = this.findChat(this.currentChatId);
          return current ? this.tokenSummaryText(current) : '';
          return this.currentChatId ? this.sessionTokenSummaryText(current, '当前会话') : '';
        },
        currentRunTokenSummary: function() {
          if (this.currentChatId) return '';
          return this.runTokenUsage ? this.tokenSummaryText(this.runTokenUsage, '本次') : '';
        },
        inlinePrompts: function() {
@@ -811,12 +825,21 @@
          }
          return DOMPurify.sanitize(marked.parse(source));
        },
        composeAssistantMarkdown: function(content, reasoningContent) {
          var answer = content || '';
          var reasoning = reasoningContent || '';
          if (!reasoning) {
            return answer;
          }
          return '<think>\n' + reasoning + '\n</think>\n\n' + answer;
        },
        loadChats: function(preferKeepCurrent) {
          var self = this;
          fetch(baseUrl + '/ai/diagnose/chats', { headers: self.authHeaders() })
            .then(function(r) { return r.json(); })
            .then(function(arr) {
              self.chats = Array.isArray(arr) ? arr : [];
              self.syncSessionTokenState();
              if (preferKeepCurrent && self.currentChatId) return;
              if (!self.currentChatId && self.sortedChats.length > 0) {
                self.openChat(self.sortedChats[0].chatId);
@@ -842,13 +865,61 @@
        },
        chatOptionLabel: function(chat) {
          if (!chat) return '未命名会话';
          var suffix = this.tokenSummaryText(chat);
          var suffix = this.sessionTokenSummaryText(chat, '累计');
          return this.chatLabel(chat) + ' · ' + (chat.size || 0) + ' 条 · ' + this.chatUpdatedAt(chat) + (suffix ? (' · ' + suffix) : '');
        },
        numericValue: function(value) {
          if (value === null || value === undefined || value === '') return 0;
          var num = Number(value);
          return isNaN(num) ? 0 : num;
        },
        buildTokenSnapshot: function(prompt, completion, total) {
          return {
            prompt: this.numericValue(prompt),
            completion: this.numericValue(completion),
            total: this.numericValue(total)
          };
        },
        sessionTokenSnapshot: function(chat) {
          return this.buildTokenSnapshot(
            chat ? chat.sumPromptTokens : 0,
            chat ? chat.sumCompletionTokens : 0,
            chat ? chat.sumTotalTokens : 0
          );
        },
        expectedSessionTokenSnapshot: function() {
          if (!this.currentChatId || !this.runTokenUsage) {
            return null;
          }
          var base = this.sessionTokenBase || this.buildTokenSnapshot(0, 0, 0);
          return this.buildTokenSnapshot(
            base.prompt + this.numericValue(this.runTokenUsage.promptTokens),
            base.completion + this.numericValue(this.runTokenUsage.completionTokens),
            base.total + this.numericValue(this.runTokenUsage.totalTokens)
          );
        },
        mergedSessionTokenSnapshot: function(chat) {
          var snapshot = this.sessionTokenSnapshot(chat);
          var expected = this.expectedSessionTokenSnapshot();
          if (!expected) {
            return snapshot;
          }
          if (snapshot.total >= expected.total) {
            return snapshot;
          }
          return expected;
        },
        syncSessionTokenState: function() {
          if (!this.currentChatId || !this.runTokenUsage) {
            return;
          }
          var current = this.findChat(this.currentChatId);
          var snapshot = this.sessionTokenSnapshot(current);
          var expected = this.expectedSessionTokenSnapshot();
          if (expected && snapshot.total >= expected.total) {
            this.runTokenUsage = null;
            this.sessionTokenBase = null;
          }
        },
        tokenSummaryText: function(source, prefix) {
          if (!source) return '';
@@ -858,6 +929,12 @@
          var completion = this.numericValue(source.completionTokens != null ? source.completionTokens : source.lastCompletionTokens);
          var label = prefix || '上次';
          return label + ' tokens ' + total + '(输' + prompt + ' / 出' + completion + ')';
        },
        sessionTokenSummaryText: function(chat, prefix) {
          var snapshot = this.mergedSessionTokenSnapshot(chat);
          if (!snapshot || !snapshot.total) return '';
          var label = prefix || '当前会话';
          return label + ' tokens ' + snapshot.total + '(输' + snapshot.prompt + ' / 出' + snapshot.completion + ')';
        },
        chatUpdatedAt: function(chat) {
          if (!chat || !chat.updatedAt) return '刚刚创建';
@@ -882,6 +959,7 @@
          if (!chatId || this.streaming) return;
          this.currentChatId = chatId;
          this.runTokenUsage = null;
          this.sessionTokenBase = null;
          this.resetting = false;
          this.persistAssistantState();
          this.switchChat();
@@ -900,10 +978,11 @@
              for (var i = 0; i < arr.length; i++) {
                var m = arr[i] || {};
                if (m.role === 'assistant') {
                  var md = self.composeAssistantMarkdown(m.content || '', m.reasoningContent || '');
                  msgs.push({
                    role: 'assistant',
                    md: m.content || '',
                    html: self.renderMarkdown(m.content || '', false),
                    md: md,
                    html: self.renderMarkdown(md, false),
                    ts: self.nowStr()
                  });
                } else {
@@ -930,6 +1009,7 @@
          this.currentChatId = Date.now() + '_' + Math.random().toString(36).substr(2, 8);
          this.resetting = true;
          this.runTokenUsage = null;
          this.sessionTokenBase = this.buildTokenSnapshot(0, 0, 0);
          this.clear();
          this.persistAssistantState();
        },
@@ -946,6 +1026,8 @@
                self.clearAssistantState();
                self.currentChatId = '';
                self.clear();
                self.runTokenUsage = null;
                self.sessionTokenBase = null;
                self.loadChats(false);
              }
            })
@@ -993,6 +1075,8 @@
          if (this.streaming) return;
          var message = (this.userInput || '').trim();
          if (!message) return;
          var current = this.findChat(this.currentChatId);
          this.sessionTokenBase = this.sessionTokenSnapshot(current);
          this.loading = true;
          this.streaming = true;
          this.runTokenUsage = null;
@@ -1035,15 +1119,30 @@
        },
        start: function() {
          if (this.streaming) return;
          this.clear();
          var prompt = '对当前系统进行巡检,如果有异常情况就进行详细的分析,如果没有异常情况则当成一次检查';
          var current = this.findChat(this.currentChatId);
          this.sessionTokenBase = this.sessionTokenSnapshot(current);
          this.loading = true;
          this.streaming = true;
          this.runTokenUsage = null;
          this.messages.push({ role: 'user', text: prompt, ts: this.nowStr() });
          this.appendAssistantPlaceholder();
          this.scrollToBottom(true);
          var self = this;
          this.source = new EventSource(baseUrl + '/ai/diagnose/runAiStream');
          var url = baseUrl + '/ai/diagnose/runAiStream';
          var params = [];
          if (this.currentChatId) {
            params.push('chatId=' + encodeURIComponent(this.currentChatId));
          }
          if (this.resetting) {
            params.push('reset=true');
          }
          if (params.length > 0) {
            url += '?' + params.join('&');
          }
          this.persistAssistantState();
          this.source = new EventSource(url);
          this.source.onopen = function() {
            self.loading = false;
          };
@@ -1066,6 +1165,8 @@
          this.source.onerror = function() {
            self.stop();
          };
          this.resetting = false;
          this.persistAssistantState();
        },
        ensureTyping: function() {
          if (this.typingTimer) return;
@@ -1123,6 +1224,9 @@
        clear: function() {
          this.messages = [];
          this.pendingText = '';
          if (!this.currentChatId) {
            this.sessionTokenBase = null;
          }
        }
      },
      mounted: function() {