Forráskód Böngészése

fix: 恢复历史模型图展示,并持久化图谱查询条件与全量数据

- BlockRenderer 恢复模型 `graph 围栏渲染;仅当消息存在 get_case_graph 工具步骤时忽略模型图,旧会话/纯模型图仍正常展示。- 后端把全量图谱随 tool_events.graphData 落库,查询条件继续随 tool_events.input 落库;刷新/重开会话后前端直接还原全量图谱,不再依赖重新请求接口。- parseToolEvents 把 graphData 映射为 ToolStep.graphPayload,图谱块优先使用它。
cc 2 napja
szülő
commit
104a2d3936

+ 14 - 6
ai-electron/frontend/src/ai/components/BlockRenderer.vue

@@ -17,13 +17,18 @@
   />
 
   <!--
-    关系图谱不再渲染模型输出的 ```graph 围栏。
-    图谱必须来自 get_case_graph 的后端全量 graph_data,挂在对应工具调用下面;
-    模型生成的图数据不是原始接口数据,可能丢关系/连错端点,统一忽略。
+    历史模型图:消息里没有 get_case_graph 工具步骤时仍按模型围栏展示,保证旧会话可复原。
+    有工具步骤时以工具数据为准,忽略模型图,避免两份图并存/模型数据连错关系。
   -->
-  <div v-else-if="block.kind === 'graph'" class="ai-graph-ignored">
-    该历史图谱来自模型输出,已停用;请重新查询关系图谱以使用后端原始数据。
+  <GraphAnalysisBlock
+    v-else-if="block.kind === 'graph' && renderModelGraph && block.data"
+    :payload="block.data"
+    :meta="block.data?.title || ''"
+  />
+  <div v-else-if="block.kind === 'graph' && !renderModelGraph" class="ai-graph-ignored">
+    本次图谱已按 get_case_graph 后端原始数据展示,模型生成的图已忽略。
   </div>
+  <CodeBlock v-else-if="block.kind === 'graph'" :text="block.text" :lang="block.lang" />
 
   <!-- AI 研判结构化产物(```insight 围栏) -->
   <InsightBlock
@@ -50,6 +55,7 @@
   import CodeBlock from './blocks/CodeBlock.vue';
   import HtmlBlock from './blocks/HtmlBlock.vue';
   import EChartsBlock from './blocks/EChartsBlock.vue';
+  import GraphAnalysisBlock from './graph/GraphAnalysisBlock.vue';
   import InsightBlock from './blocks/InsightBlock.vue';
 
   withDefaults(
@@ -57,8 +63,10 @@
       block: ContentBlock;
       /** 所属消息是否仍在流式输出 */
       streaming?: boolean;
+      /** 是否渲染模型生成的 ```graph 围栏(有工具图谱时传 false) */
+      renderModelGraph?: boolean;
     }>(),
-    { streaming: false },
+    { streaming: false, renderModelGraph: true },
   );
 </script>
 

+ 5 - 0
ai-electron/frontend/src/ai/components/ChatMessageItem.vue

@@ -68,6 +68,7 @@
               :key="block.key"
               :block="block"
               :streaming="!!message.streaming"
+              :render-model-graph="!hasGraphToolStep"
             />
 
             <div v-if="message.streaming && !hasContent" class="ai-msg__skeleton">
@@ -161,6 +162,10 @@
 
   const hasContent = computed(() => !!(props.message.blocks || []).some((b) => b.text?.trim()));
 
+  const hasGraphToolStep = computed(() =>
+    (props.message.toolSteps || []).some((step) => step.toolName === 'get_case_graph' && step.status !== 'error'),
+  );
+
   const timeText = computed(() => {
     const raw = props.message.createAt;
     if (!raw) return '';

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

@@ -91,6 +91,7 @@ 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 {

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

@@ -497,6 +497,7 @@ 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());
@@ -528,7 +529,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             } else if (event instanceof AgentResultEvent e) {
                 // AI 回复落库(异步,不阻塞事件流);工具调用事件与思考过程一并持久化
                 resultPersisted.set(true);
-                String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames);
+                String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames, process.graphData);
                 String thinkingJson = buildThinkingMetadata(accumulatedThinking.get().toString());
                 Mono.fromRunnable(() -> persistAssistantMessage(sessionId, e.getResult(), toolEventsJson, thinkingJson))
                         .subscribeOn(Schedulers.boundedElastic())
@@ -542,7 +543,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             // 用户主动中止:若 AgentResultEvent 尚未到达,保存已流式输出的部分内容
             if (!resultPersisted.get()) {
                 String partialText = accumulatedText.get().toString().trim();
-                String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames);
+                String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames, process.graphData);
                 String thinkingJson = buildThinkingMetadata(accumulatedThinking.get().toString());
                 if (!partialText.isEmpty() || !toolEventsJson.isEmpty()) {
                     Mono.fromRunnable(() -> persistPartialMessage(sessionId, partialText, toolEventsJson, thinkingJson))
@@ -713,7 +714,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) : null;
+                    ? buildToolEventsJson(process.toolInputs, process.toolResults, process.toolNames, process.graphData) : null;
             String thinkingJson = processLost
                     ? buildThinkingMetadata(process.thinking.get().toString()) : null;
             AgentMessage msg = new AgentMessage();
@@ -864,7 +865,8 @@ 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, String> toolNames,
+                                              Map<String, Object> graphData) {
         if (toolNames.isEmpty()) {
             return null;
         }
@@ -879,6 +881,11 @@ 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 {
@@ -1010,6 +1017,8 @@ 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());
         /** 完整回答是否已落库(落库后出错不再重复携带过程数据) */