Преглед изворни кода

fix: 关系图谱只存查询条件,历史会话按条件复原全量数据

- 不再把全量图谱 JSON 写入 tool_events;仅保留工具入参(type/personNames/日期/金额/次数)与原有 500 条工具结果。- 新增 POST /chat/graph/restore:按 sessionId + 查询条件在会话案件上下文内重新调用 GraphService,返回全量 nodeList/edgeList。- 打开历史会话时,前端从 ToolStep.input 取条件调用复原接口,把结果挂回 ToolStep.graphPayload;失败时回退工具结果,不阻塞会话加载。
cc пре 22 часа
родитељ
комит
0e3cb6f155

+ 5 - 0
ai-electron/frontend/src/ai/api/chatApi.ts

@@ -49,6 +49,11 @@ export function listMessages(sessionId: string): Promise<ChatMessageVO[]> {
   return aiHttp.getRaw<ChatMessageVO[]>(`/chat/sessions/${sessionId}/messages`);
 }
 
+/** 按历史工具入参里的查询条件复原全量关系图谱(不持久化大 JSON) */
+export function restoreGraph(sessionId: string, query: Record<string, unknown>): Promise<Record<string, unknown>> {
+  return aiHttp.postRaw<Record<string, unknown>>('/chat/graph/restore', { sessionId, query });
+}
+
 /** 发送消息(非流式;流式请用 streamChat) */
 export function sendMessage(params: SendMessageParams): Promise<ChatMessageVO> {
   return aiHttp.postRaw<ChatMessageVO>('/chat/messages', params as unknown as Record<string, any>);

+ 34 - 0
ai-electron/frontend/src/ai/store/chatStream.ts

@@ -18,6 +18,7 @@ import {
   parseThinkingFromMetadata,
   parseMessageBlocksIncremental,
   normalizeToolPayloadText,
+  parseJsonPayload,
   sessionTitle as resolveSessionTitle,
 } from '../utils/parseMessageContent';
 import type { ContentBlock } from '../utils/scanner';
@@ -240,11 +241,44 @@ export const useChatStreamStore = defineStore('ai-chat-stream', {
         // 防止竞态:加载期间用户已切走
         if (this.activeSessionId !== sessionId) return;
         this.messages = (Array.isArray(list) ? list : []).map((m) => this.hydrateMessage(m));
+        await this.restoreHistoricalGraphs(sessionId, this.messages);
       } finally {
         this.loadingMessages = false;
       }
     },
 
+    /**
+     * 历史消息只落库查询条件(tool_events.input),不存全量图谱 JSON。
+     * 打开会话时按条件向后端复原全量关系图,避免 DB 存储大 JSON。
+     */
+    async restoreHistoricalGraphs(sessionId: string, messages: ChatMessage[]) {
+      const tasks: Promise<void>[] = [];
+      messages.forEach((message) => {
+        (message.toolSteps || []).forEach((step) => {
+          if (step.toolName !== 'get_case_graph' || step.status === 'error' || step.graphPayload || !step.input) {
+            return;
+          }
+          const parsed = parseJsonPayload(step.input);
+          const query =
+            parsed && typeof parsed === 'object' && !Array.isArray(parsed)
+              ? ((parsed as Record<string, unknown>).query ?? parsed)
+              : null;
+          if (!query || typeof query !== 'object' || Array.isArray(query)) return;
+          tasks.push(
+            chatApi
+              .restoreGraph(sessionId, query as Record<string, unknown>)
+              .then((graph) => {
+                step.graphPayload = graph;
+              })
+              .catch(() => {
+                /* 复原失败时保留工具结果回退,不阻塞会话打开 */
+              }),
+          );
+        });
+      });
+      await Promise.allSettled(tasks);
+    },
+
     /** 把后端消息补全为前端渲染态 */
     hydrateMessage(m: ChatMessageVO): ChatMessage {
       const toolSteps = parseToolEvents(m.toolEvents);

+ 0 - 1
ai-electron/frontend/src/ai/utils/parseMessageContent.ts

@@ -91,7 +91,6 @@ export function parseToolEvents(raw?: string | null): ToolStep[] {
         result: normalizeToolPayloadText(
           typeof it.result === 'string' ? it.result : it.result ? JSON.stringify(it.result) : '',
         ),
-        graphPayload: it.graphData,
         status: it.status || 'done',
       }));
   } catch {

+ 10 - 0
ai-server/src/main/java/com/zsjz/ai/module/agent/controller/AgentChatController.java

@@ -6,6 +6,7 @@ import com.zsjz.ai.common.exception.ServerException;
 import com.zsjz.ai.module.agent.attachment.ChatAttachmentService;
 import com.zsjz.ai.module.agent.dto.ChatRequestDTO;
 import com.zsjz.ai.module.agent.dto.CreateChatSessionDTO;
+import com.zsjz.ai.module.agent.dto.GraphRestoreDTO;
 import com.zsjz.ai.module.agent.dto.SendMessageDTO;
 import com.zsjz.ai.module.agent.rag.RagSchemaService;
 import com.zsjz.ai.module.agent.service.AgentChatService;
@@ -21,6 +22,7 @@ import org.springframework.web.multipart.MultipartFile;
 import reactor.core.publisher.Flux;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 聊天控制器
@@ -50,6 +52,14 @@ public class AgentChatController {
         return chatAttachmentService.upload(file, StpUtil.getLoginIdAsLong(), workspaceId, sessionId == null ? 0L : sessionId);
     }
 
+    /**
+     * 关系图谱会话复原:只按落库的查询条件重新取全量图谱,不持久化大 JSON。
+     */
+    @PostMapping("/graph/restore")
+    public Map<String, Object> restoreGraph(@RequestBody GraphRestoreDTO dto) {
+        return agentChatService.restoreGraph(dto);
+    }
+
     /**
      * 创建会话
      */

+ 18 - 0
ai-server/src/main/java/com/zsjz/ai/module/agent/dto/GraphRestoreDTO.java

@@ -0,0 +1,18 @@
+package com.zsjz.ai.module.agent.dto;
+
+import lombok.Data;
+
+import java.util.Map;
+
+/**
+ * 关系图谱会话复原请求:只传查询条件,不传/不存全量图谱 JSON。
+ */
+@Data
+public class GraphRestoreDTO {
+
+    /** 会话 ID;后端据此取案件上下文 */
+    private Long sessionId;
+
+    /** get_case_graph 的原始 query 条件(type/personNames/日期/金额/次数) */
+    private Map<String, Object> query;
+}

+ 7 - 0
ai-server/src/main/java/com/zsjz/ai/module/agent/service/AgentChatService.java

@@ -3,6 +3,7 @@ package com.zsjz.ai.module.agent.service;
 
 import com.zsjz.ai.module.agent.dto.ChatRequestDTO;
 import com.zsjz.ai.module.agent.dto.CreateChatSessionDTO;
+import com.zsjz.ai.module.agent.dto.GraphRestoreDTO;
 import com.zsjz.ai.module.agent.dto.SendMessageDTO;
 import com.zsjz.ai.module.agent.vo.ChatSessionVO;
 import com.zsjz.ai.module.agent.vo.MessageVO;
@@ -10,6 +11,7 @@ import org.springframework.http.codec.ServerSentEvent;
 import reactor.core.publisher.Flux;
 
 import java.util.List;
+import java.util.Map;
 
 /**
  * 聊天服务接口
@@ -69,6 +71,11 @@ public interface AgentChatService {
      */
     void toggleStarMessage(Long messageId);
 
+    /**
+     * 按历史工具入参中的查询条件复原全量关系图谱(不持久化大 JSON,只持久化条件)
+     */
+    Map<String, Object> restoreGraph(GraphRestoreDTO dto);
+
     /**
      * 流式对话
      */

+ 27 - 13
ai-server/src/main/java/com/zsjz/ai/module/agent/service/impl/AgentChatServiceImpl.java

@@ -12,6 +12,7 @@ import com.zsjz.ai.common.context.CaseContextHolder;
 import com.zsjz.ai.common.exception.ServerException;
 import com.zsjz.ai.module.agent.dto.ChatRequestDTO;
 import com.zsjz.ai.module.agent.dto.CreateChatSessionDTO;
+import com.zsjz.ai.module.agent.dto.GraphRestoreDTO;
 import com.zsjz.ai.module.agent.dto.SendMessageDTO;
 import com.zsjz.ai.module.agent.entity.AgentChatSession;
 import com.zsjz.ai.module.agent.entity.AgentMessage;
@@ -309,6 +310,28 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
         log.info("切换消息收藏状态成功: {}", messageId);
     }
 
+    @Override
+    public Map<String, Object> restoreGraph(GraphRestoreDTO dto) {
+        if (dto == null || dto.getSessionId() == null) {
+            throw new ServerException(400, "缺少会话ID");
+        }
+        if (dto.getQuery() == null || dto.getQuery().isEmpty()) {
+            throw new ServerException(400, "缺少关系图谱查询条件");
+        }
+        AgentChatSession session = requireSessionInOpenCase(requireSession(dto.getSessionId()));
+        try {
+            String input = MAPPER.writeValueAsString(Map.of("query", dto.getQuery()));
+            Map<String, Object> graph = buildFullGraphPayload(
+                    input, session.getCaseId(), CaseContextHolder.getUserId());
+            if (graph == null) {
+                throw new ServerException(400, "关系图谱条件无法复原,请重新查询");
+            }
+            return graph;
+        } catch (JsonProcessingException e) {
+            throw new ServerException(400, "关系图谱查询条件格式不正确");
+        }
+    }
+
     @Override
     public Flux<ServerSentEvent<String>> stream(ChatRequestDTO req) {
         if (!StringUtils.hasText(req.getMessage())) {
@@ -497,7 +520,6 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
                     Map<String, Object> fullGraph = buildFullGraphPayload(
                             input == null ? null : input.toString(), graphCaseId, graphUserId);
                     if (fullGraph != null) {
-                        process.graphData.put(e.getToolCallId(), fullGraph);
                         Map<String, Object> graphFrame = new LinkedHashMap<>();
                         graphFrame.put("type", "graph_data");
                         graphFrame.put("toolCallId", e.getToolCallId());
@@ -529,7 +551,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             } else if (event instanceof AgentResultEvent e) {
                 // AI 回复落库(异步,不阻塞事件流);工具调用事件与思考过程一并持久化
                 resultPersisted.set(true);
-                String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames, process.graphData);
+                String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames);
                 String thinkingJson = buildThinkingMetadata(accumulatedThinking.get().toString());
                 Mono.fromRunnable(() -> persistAssistantMessage(sessionId, e.getResult(), toolEventsJson, thinkingJson))
                         .subscribeOn(Schedulers.boundedElastic())
@@ -543,7 +565,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             // 用户主动中止:若 AgentResultEvent 尚未到达,保存已流式输出的部分内容
             if (!resultPersisted.get()) {
                 String partialText = accumulatedText.get().toString().trim();
-                String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames, process.graphData);
+                String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames);
                 String thinkingJson = buildThinkingMetadata(accumulatedThinking.get().toString());
                 if (!partialText.isEmpty() || !toolEventsJson.isEmpty()) {
                     Mono.fromRunnable(() -> persistPartialMessage(sessionId, partialText, toolEventsJson, thinkingJson))
@@ -714,7 +736,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             // 避免同轮的时间线/产物在历史里出现两份
             boolean processLost = process != null && !process.resultPersisted.get();
             String toolEventsJson = processLost
-                    ? buildToolEventsJson(process.toolInputs, process.toolResults, process.toolNames, process.graphData) : null;
+                    ? buildToolEventsJson(process.toolInputs, process.toolResults, process.toolNames) : null;
             String thinkingJson = processLost
                     ? buildThinkingMetadata(process.thinking.get().toString()) : null;
             AgentMessage msg = new AgentMessage();
@@ -865,8 +887,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
      */
     private static String buildToolEventsJson(Map<String, StringBuilder> toolInputs,
                                               Map<String, StringBuilder> toolResults,
-                                              Map<String, String> toolNames,
-                                              Map<String, Object> graphData) {
+                                              Map<String, String> toolNames) {
         if (toolNames.isEmpty()) {
             return null;
         }
@@ -881,11 +902,6 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             // 兜底再解一层:个别工具的 delta 可能在累积时才成完整字符串字面量
             ev.put("result", result != null ? unwrapJsonString(result.toString()) : "");
             ev.put("status", "done");
-            Object graph = graphData == null ? null : graphData.get(toolCallId);
-            if (graph != null) {
-                // 会话复原用:把本次查询的全量图谱随 tool_events 落库(查询条件已在 input 里)
-                ev.put("graphData", graph);
-            }
             events.add(ev);
         }
         try {
@@ -1017,8 +1033,6 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
         final Map<String, StringBuilder> toolInputs = new ConcurrentHashMap<>();
         final Map<String, StringBuilder> toolResults = new ConcurrentHashMap<>();
         final Map<String, String> toolNames = new ConcurrentHashMap<>();
-        /** 全量图谱按 toolCallId 暂存:随 tool_events 落库,供历史会话复原 */
-        final Map<String, Object> graphData = new ConcurrentHashMap<>();
         final AtomicReference<StringBuilder> text = new AtomicReference<>(new StringBuilder());
         final AtomicReference<StringBuilder> thinking = new AtomicReference<>(new StringBuilder());
         /** 完整回答是否已落库(落库后出错不再重复携带过程数据) */