cc 3 nedēļas atpakaļ
vecāks
revīzija
305101c4f7

+ 64 - 0
.workbuddy-ai/memory/2026-09-17.md

@@ -359,3 +359,67 @@
 - 补全量 schema 回归测试的写法与两个误报教训(带引号匹配 key、`keyword` 走 `#{}` 不算注入面);
 - 测试命令补上 `TrackAnalysisToolTest`。
 
+---
+
+## 新增:AI 聊天界面输出「思考过程」(模型 reasoning_content)
+
+需求:把模型的思考过程也输出到聊天界面上。
+
+### 关键事实(先查清了才动手)
+- AgentScope 2.0.1 有完整思考链路:`ThinkingBlock`(消息块)+ `ThinkingBlockStart/Delta/EndEvent`
+  (`ReActAgent.ModelCallBlockLifecycle` 里按 model call 发射,blockId 恒为 `"thinking"`)。
+  `HarnessAgent.streamEvents` 直通 `ReActAgent`,事件不会被过滤。
+- **OpenAI 兼容扩展的流式解析器会读 `delta.reasoning_content` 并转成 `ThinkingBlock`**
+  (`OpenAIResponseParser:432`),回传时 `OpenAIMessageConverter.convertAssistantMessage` 也会
+  把它写回 `reasoningContent` —— 所以走 `openai:` 前缀(本项目 provider=DeepSeek 就是走这条)
+  一样能拿到思考内容,**不需要**改成 `deepseek:` 前缀。
+- **DeepSeek 官方文档:思考模式默认开启(effort=high)**,参数是 `{"thinking":{"type":"enabled|disabled"}}`,
+  模型名示例就是用户库里配的 `deepseek-flash`。⇒ 之前模型一直在吐 `reasoning_content`,只是后端把它丢了。
+- `ModelCreationContext.enableThinking` 是 AgentScope 的思考开关;DeepSeek/GLM provider 会据此
+  下发 `thinking` body param,**为 null 时不下发(保持厂商默认)**。
+
+### 改动
+后端:
+- `module/agent/service/impl/AgentChatServiceImpl.java`
+  - 新增 `ThinkingBlockStart/Delta/EndEvent` 分支 → SSE **`thinking` 帧**:
+    `{type:'thinking', phase:'start'|'delta'|'end', data?}`;
+    多段思考之间后端补 `\n\n` 分隔。
+  - 新增 `AtomicReference<StringBuilder> accumulatedThinking`,随 `AgentResultEvent` /
+    `doOnCancel` 落库。
+  - 新增私有静态 helper `buildThinkingMetadata(String)`(→ `{"thinking":"..."}`,空白返回 null)
+    与 `mergeThinkingMetadata(baseJson, thinkingJson)`(中断消息保留原标志位)。
+  - `persistAssistantMessage` / `persistPartialMessage` 增加 `thinkingJson` 参数,
+    写入 `agent_message.metadata`(**不新建列**,jsonb 直接放)。
+- `module/agent/service/AgentModelFactory.java`
+  - 新增 `readEnableThinking(AgentModel)`:从 `agent_model.config` 读 `enableThinking`(true/false),
+    有值才 `.enableThinking(...)`,缺省/非法 JSON 一律 null(不改既有行为)。
+  - 想显式关掉思考就填 `{"enableThinking": false}`。
+
+前端:
+- `ai/api/types.ts`:`SseEventName` 加 `'thinking'`;新增 `SseThinkingData`;`SseHandlers.onThinking`。
+- `ai/api/chatApi.ts`:`createParser` 增加 `case 'thinking'`。
+- `ai/utils/parseMessageContent.ts`:新增 `parseThinkingFromMetadata(raw)`(安全解析 metadata)。
+- `ai/store/chatStream.ts`:`ChatMessage` 增 `thinking?` / `thinkingActive?`;
+  `onThinking` 按 phase 累积;`hydrateMessage` 从 metadata 还原(无 thinking 时不写键);
+  `finally` / `stop()` 复位 `thinkingActive`。
+- 新增 `ai/components/ThinkingPanel.vue`:折叠面板(与 ToolCallPanel 同交互),
+  思考中自动展开 + 自动滚到底 + 脉冲点,结束后自动收起(用户手动点开过则不收起),
+  纯空白不渲染。
+- `ai/components/ChatMessageItem.vue`:面板置于工具面板之上;
+  `StreamStatusBar` 增加 `thinkingActive` prop → 阶段文案「正在深度思考」。
+
+### 验证(都跑过)
+- 后端 `mvn -o -pl ai-server compile` BUILD SUCCESS。
+- 前端 `vue-tsc`(src/ai 零错误)/ `eslint` / `stylelint` 全干净;dev server(:3100)逐模块 200。
+- esbuild 打包行为断言(放 temp,已清理):
+  - `chatApi` SSE 解析 **6/6**:thinking 生命周期、多段+工具/正文混排、末帧无空行、多字节跨 chunk、CRLF、无 phase。
+  - `chatStream` store **14/14**:累积、段间空行、空 delta、裸增量、hydrate 还原(含中断消息)、
+    无 thinking 不产生空键、非法/缺失 metadata 不抛、stop 复位。
+  - 反射跑后端私有静态方法 **10/10 + 8/8**:metadata JSON 转义/裁剪/空白→null/merge 保留标志位、
+    config 的 enableThinking 解析(true/false/缺键/空/非法/字符串值)。
+
+### 未做
+- 思考过程未进导出/复制(`messageToMarkdown` 只导正文)。
+- 后端仍跑在用户 IDEA 调试实例上,**需用户在 IDEA 重启后端**才能看到 SSE `thinking` 帧。
+
+

+ 3 - 0
ai-frontend/src/ai/api/chatApi.ts

@@ -141,6 +141,9 @@ function createParser(handlers: SseHandlers): SseFrameParser {
       case 'suggestions':
         handlers.onSuggestions?.(data as any);
         break;
+      case 'thinking':
+        handlers.onThinking?.(data as any);
+        break;
       case 'tool_call':
         handlers.onToolCall?.(data as any);
         break;

+ 17 - 0
ai-frontend/src/ai/api/types.ts

@@ -182,6 +182,7 @@ export interface SqlResultPage {
 export type SseEventName =
   | 'intent'
   | 'suggestions'
+  | 'thinking'
   | 'tool_call'
   | 'tool_input'
   | 'tool_result'
@@ -203,6 +204,20 @@ export interface SseSuggestionsData {
   questions?: string[];
 }
 
+/**
+ * 思考过程帧(模型 reasoning_content / thinking)
+ *
+ * 后端按 AgentScope 的 ThinkingBlock 生命周期下发:
+ * `start`(一段思考开始)→ `delta`(增量,带 data)→ `end`(本段结束)。
+ * 一轮 ReAct 可能有多段思考,段与段之间后端会补空行。
+ */
+export interface SseThinkingData {
+  type: 'thinking';
+  phase?: 'start' | 'delta' | 'end';
+  /** 增量文本(仅 phase=delta 时有值) */
+  data?: string;
+}
+
 /** 工具调用开始帧 */
 export interface SseToolCallData {
   type: 'tool_call';
@@ -246,6 +261,7 @@ export interface SseErrorData {
 export type SseEventData =
   | SseIntentData
   | SseSuggestionsData
+  | SseThinkingData
   | SseToolCallData
   | SseToolInputData
   | SseToolResultData
@@ -257,6 +273,7 @@ export type SseEventData =
 export interface SseHandlers {
   onIntent?: (data: SseIntentData) => void;
   onSuggestions?: (data: SseSuggestionsData) => void;
+  onThinking?: (data: SseThinkingData) => void;
   onToolCall?: (data: SseToolCallData) => void;
   onToolInput?: (data: SseToolInputData) => void;
   onToolResult?: (data: SseToolResultData) => void;

+ 5 - 1
ai-frontend/src/ai/components/ChatMessageItem.vue

@@ -13,8 +13,10 @@
       <!-- 用户消息:纯文本 -->
       <p v-if="message.role === 'user'" class="ai-msg__user-text">{{ message.content }}</p>
 
-      <!-- AI 消息:工具调用 → 状态 → 富内容块 -->
+      <!-- AI 消息:思考过程 → 工具调用 → 状态 → 富内容块 -->
       <template v-else>
+        <ThinkingPanel v-if="message.thinking" :text="message.thinking" :active="!!message.thinkingActive" />
+
         <ToolCallPanel v-if="message.toolSteps?.length" :steps="message.toolSteps" />
 
         <StreamStatusBar
@@ -23,6 +25,7 @@
           :intent="message.intent"
           :steps="message.toolSteps"
           :has-content="hasContent"
+          :thinking-active="!!message.thinkingActive"
         />
 
         <div v-if="message.messageType === 'error'" class="ai-msg__error">
@@ -74,6 +77,7 @@
   import { computed, ref } from 'vue';
   import type { ChatMessage } from '../store/chatStream';
   import BlockRenderer from './BlockRenderer.vue';
+  import ThinkingPanel from './ThinkingPanel.vue';
   import ToolCallPanel from './ToolCallPanel.vue';
   import StreamStatusBar from './StreamStatusBar.vue';
   import SuggestionBar from './SuggestionBar.vue';

+ 3 - 0
ai-frontend/src/ai/components/StreamStatusBar.vue

@@ -35,6 +35,8 @@
     steps?: ToolStep[];
     /** 是否已开始输出正文 */
     hasContent?: boolean;
+    /** 模型是否正在输出思考过程 */
+    thinkingActive?: boolean;
   }>();
 
   const elapsed = ref(0);
@@ -66,6 +68,7 @@
   const phaseText = computed(() => {
     if (runningTool.value) return `正在执行:${toolLabel(runningTool.value.toolName)}`;
     if (props.hasContent) return '正在生成回答';
+    if (props.thinkingActive) return '正在深度思考';
     if (props.steps?.length) return '正在整理结果';
     if (props.intent) return '正在规划分析步骤';
     return '正在理解问题';

+ 171 - 0
ai-frontend/src/ai/components/ThinkingPanel.vue

@@ -0,0 +1,171 @@
+<template>
+  <div v-if="hasText" class="ai-think" :class="{ 'is-active': active }">
+    <button class="ai-think__toggle" type="button" @click="toggle">
+      <span class="ai-think__dot"></span>
+      <span class="ai-think__title">{{ active ? '正在思考…' : '思考过程' }}</span>
+      <span class="ai-think__meta">{{ charCount }} 字</span>
+      <span class="ai-think__chevron" :class="{ 'is-open': expanded }">›</span>
+    </button>
+
+    <div v-show="expanded" ref="bodyRef" class="ai-think__body ai-scroll-area">
+      <pre class="ai-think__pre">{{ text }}</pre>
+    </div>
+  </div>
+</template>
+
+<script lang="ts" setup>
+  /**
+   * 思考过程面板
+   *
+   * 展示模型在给出结论前的推理内容(后端 reasoning_content / thinking,见 SSE `thinking` 帧)。
+   * 交互与工具面板一致:折叠为一行摘要,展开看全文。
+   * 差异点:思考是**边生成边展示**的 —— 开始时自动展开、内容增长时自动滚到底部、
+   * 结束后自动收起(用户手动点开过就不再自动收起,避免打断阅读)。
+   */
+  import { computed, nextTick, ref, watch } from 'vue';
+
+  const props = withDefaults(
+    defineProps<{
+      /** 思考全文(流式期间持续增长) */
+      text: string;
+      /** 是否仍在输出思考内容 */
+      active?: boolean;
+    }>(),
+    { active: false },
+  );
+
+  const expanded = ref(false);
+  /** 用户是否手动切换过折叠状态(手动操作后不再自动收起) */
+  const userToggled = ref(false);
+  const bodyRef = ref<HTMLElement>();
+
+  const charCount = computed(() => (props.text || '').length);
+  /** 只有空白(流式开头/纯换行)时不渲染面板,避免闪出一个空盒子 */
+  const hasText = computed(() => (props.text || '').trim().length > 0);
+
+  function toggle() {
+    userToggled.value = true;
+    expanded.value = !expanded.value;
+    if (expanded.value) scrollToBottom();
+  }
+
+  function scrollToBottom() {
+    nextTick(() => {
+      const el = bodyRef.value;
+      if (el) el.scrollTop = el.scrollHeight;
+    });
+  }
+
+  // 开始思考即展开;思考结束后自动收起(用户手动点开过则保持展开)
+  watch(
+    () => props.active,
+    (val) => {
+      if (val) {
+        if (!userToggled.value) expanded.value = true;
+        scrollToBottom();
+      } else if (!userToggled.value) {
+        expanded.value = false;
+      }
+    },
+    { immediate: true },
+  );
+
+  // 内容增长时跟随到底部(仅展开态,避免无谓重排)
+  watch(charCount, () => {
+    if (expanded.value) scrollToBottom();
+  });
+</script>
+
+<style lang="less" scoped>
+  .ai-think {
+    margin-bottom: var(--ai-sp-2);
+    border: 1px dashed var(--ai-border-strong);
+    border-radius: var(--ai-radius);
+    background: var(--ai-bg-subtle);
+    overflow: hidden;
+
+    &__toggle {
+      display: flex;
+      align-items: center;
+      gap: var(--ai-sp-2);
+      width: 100%;
+      padding: var(--ai-sp-2) var(--ai-sp-3);
+      font-size: 12px;
+      color: var(--ai-text-2);
+      background: transparent;
+      border: 0;
+      cursor: pointer;
+      text-align: left;
+
+      &:hover {
+        color: var(--ai-text);
+      }
+    }
+
+    &__dot {
+      flex: 0 0 auto;
+      width: 7px;
+      height: 7px;
+      border-radius: 50%;
+      background: var(--ai-border-strong);
+    }
+
+    &.is-active &__dot {
+      background: @primary-color;
+      animation: ai-think-pulse 1.2s ease-in-out infinite;
+    }
+
+    &__title {
+      flex: 0 0 auto;
+      color: var(--ai-text);
+    }
+
+    &__meta {
+      flex: 1 1 auto;
+      font-size: 11px;
+      opacity: 0.75;
+    }
+
+    &__chevron {
+      flex: 0 0 auto;
+      font-size: 14px;
+      line-height: 1;
+      transform: rotate(90deg);
+      transition: transform 0.18s ease-out;
+
+      &.is-open {
+        transform: rotate(-90deg);
+      }
+    }
+
+    &__body {
+      max-height: 320px;
+      padding: var(--ai-sp-2) var(--ai-sp-3);
+      overflow: auto;
+      border-top: 1px dashed var(--ai-border);
+    }
+
+    &__pre {
+      margin: 0;
+      font-family: inherit;
+      font-size: 12px;
+      line-height: 1.75;
+      color: var(--ai-text-2);
+      white-space: pre-wrap;
+      word-break: break-word;
+    }
+  }
+
+  @keyframes ai-think-pulse {
+    0%,
+    100% {
+      opacity: 1;
+      transform: scale(1);
+    }
+
+    50% {
+      opacity: 0.35;
+      transform: scale(0.82);
+    }
+  }
+</style>

+ 25 - 0
ai-frontend/src/ai/store/chatStream.ts

@@ -15,6 +15,7 @@ import type { ChatMessageVO, ChatSession, SseIntentData, ToolStep } from '../api
 import { RESCAN_THROTTLE_MS } from '../utils/constants';
 import {
   parseToolEvents,
+  parseThinkingFromMetadata,
   parseMessageBlocksIncremental,
   sessionTitle as resolveSessionTitle,
 } from '../utils/parseMessageContent';
@@ -31,6 +32,10 @@ export interface ChatMessage extends ChatMessageVO {
   blocks?: ContentBlock[];
   /** 工具调用步骤(流式期间实时累积) */
   toolSteps?: ToolStep[];
+  /** 模型思考过程(reasoning_content / thinking,流式累积;落库后从 metadata 还原) */
+  thinking?: string;
+  /** 当前是否仍在输出思考内容(用于面板的"思考中"状态与自动展开) */
+  thinkingActive?: boolean;
   /** 意图识别结果 */
   intent?: SseIntentData;
   /** 下一步引导建议 */
@@ -159,9 +164,11 @@ export const useChatStreamStore = defineStore('ai-chat-stream', {
     /** 把后端消息补全为前端渲染态 */
     hydrateMessage(m: ChatMessageVO): ChatMessage {
       const toolSteps = parseToolEvents(m.toolEvents);
+      const thinking = parseThinkingFromMetadata(m.metadata);
       return {
         ...m,
         toolSteps,
+        ...(thinking ? { thinking } : {}),
         blocks: parseMessageBlocksIncremental(undefined, m.content || '').blocks,
       };
     },
@@ -267,6 +274,22 @@ export const useChatStreamStore = defineStore('ai-chat-stream', {
             onSuggestions: (data) => {
               target.suggestions = data.questions || [];
             },
+            onThinking: (data) => {
+              // start/delta/end 三段:start 表示新一段思考(段间补空行),delta 追加正文
+              if (data.phase === 'start') {
+                if (target.thinking) target.thinking += '\n\n';
+                target.thinkingActive = true;
+                return;
+              }
+              if (data.phase === 'end') {
+                target.thinkingActive = false;
+                return;
+              }
+              const delta = data.data || '';
+              if (!delta) return;
+              target.thinking = (target.thinking || '') + delta;
+              target.thinkingActive = true;
+            },
             onToolCall: (data) => {
               target.toolSteps = [
                 ...(target.toolSteps || []),
@@ -311,6 +334,7 @@ export const useChatStreamStore = defineStore('ai-chat-stream', {
       } finally {
         flushRescan();
         target.streaming = false;
+        target.thinkingActive = false;
         this.streaming = false;
         abortController = null;
         // 同步会话元信息(标题/消息数/时间由后端更新)
@@ -332,6 +356,7 @@ export const useChatStreamStore = defineStore('ai-chat-stream', {
       const last = this.messages[this.messages.length - 1];
       if (last?.streaming) {
         last.streaming = false;
+        last.thinkingActive = false;
         if (!last.content) last.content = '(已停止生成)';
       }
     },

+ 16 - 0
ai-frontend/src/ai/utils/parseMessageContent.ts

@@ -30,6 +30,22 @@ export function parseToolEvents(raw?: string | null): ToolStep[] {
   }
 }
 
+/**
+ * 从消息 metadata(JSON 字符串)里取出思考过程。
+ *
+ * 后端把模型 reasoning_content 累积后写入 `agent_message.metadata` 的 `thinking` 字段,
+ * 刷新会话时用它还原「思考过程」面板;无该字段返回空串。
+ */
+export function parseThinkingFromMetadata(raw?: string | null): string {
+  if (!raw) return '';
+  try {
+    const parsed = JSON.parse(raw);
+    return typeof parsed?.thinking === 'string' ? parsed.thinking : '';
+  } catch {
+    return '';
+  }
+}
+
 /** 从工具结果文本里抽取表格载荷(execute_sql 的返回就是表格 JSON) */
 export function parseTableFromToolResult(raw?: string): SqlResultPage | null {
   if (!raw) return null;

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

@@ -1,8 +1,10 @@
 package com.zsjz.ai.module.agent.controller;
 
+import com.zsjz.ai.common.config.Result;
 import com.zsjz.ai.module.agent.dto.ChatRequestDTO;
 import com.zsjz.ai.module.agent.dto.CreateChatSessionDTO;
 import com.zsjz.ai.module.agent.dto.SendMessageDTO;
+import com.zsjz.ai.module.agent.rag.RagSchemaService;
 import com.zsjz.ai.module.agent.service.AgentChatService;
 import com.zsjz.ai.module.agent.vo.ChatSessionVO;
 import com.zsjz.ai.module.agent.vo.MessageVO;
@@ -28,6 +30,7 @@ import java.util.List;
 public class AgentChatController {
 
     private final AgentChatService agentChatService;
+    private final RagSchemaService ragSchemaService;
 
     /**
      * 创建会话
@@ -130,4 +133,10 @@ public class AgentChatController {
     public Flux<ServerSentEvent<String>> stream(@RequestBody ChatRequestDTO req) {
         return agentChatService.stream(req);
     }
+
+    @GetMapping("/reindexWorkspace")
+    public Result<Void> reindexWorkspace(Long id) {
+        ragSchemaService.reindexWorkspace(id);
+        return Result.succeed();
+    }
 }

+ 37 - 4
ai-server/src/main/java/com/zsjz/ai/module/agent/service/AgentModelFactory.java

@@ -1,6 +1,8 @@
 package com.zsjz.ai.module.agent.service;
 
+import com.fasterxml.jackson.databind.JsonNode;
 import com.zsjz.ai.common.exception.ServerException;
+import com.zsjz.ai.common.utils.Json;
 import com.zsjz.ai.module.agent.entity.AgentModel;
 import io.agentscope.core.model.Model;
 import io.agentscope.core.model.ModelCreationContext;
@@ -30,6 +32,9 @@ public class AgentModelFactory {
     private static final String PREFIX_DASHSCOPE = "dashscope:";
     private static final String PREFIX_OPENAI = "openai:";
 
+    /** config JSON 里控制「思考模式」的键名(true=开启,false=显式关闭,缺省=跟随厂商默认) */
+    private static final String CONFIG_ENABLE_THINKING = "enableThinking";
+
     /**
      * 根据模型配置构造 agentscope Model 实例
      *
@@ -49,16 +54,22 @@ public class AgentModelFactory {
         }
 
         String modelRef = resolvePrefix(config.getProvider()) + config.getModelId().trim();
-        ModelCreationContext ctx = ModelCreationContext.builder()
+        ModelCreationContext.Builder builder = ModelCreationContext.builder()
                 .apiKey(config.getApiKey().trim())
                 .baseUrl(StringUtils.hasText(config.getBaseUrl()) ? config.getBaseUrl().trim() : null)
                 .option("nativeStructuredOutput", true)
                 .option("nativeStructuredOutputWithTools", true)
-                .stream(true)
-                .build();
+                .stream(true);
+        // 思考模式开关:仅当 config 里显式写了 enableThinking 才下发,缺省保持厂商默认行为。
+        // 开启后模型会流式吐 reasoning_content / thinking,前端渲染为「思考过程」面板。
+        Boolean enableThinking = readEnableThinking(config);
+        if (enableThinking != null) {
+            builder.enableThinking(enableThinking);
+        }
+        ModelCreationContext ctx = builder.build();
         try {
             Model model = ModelRegistry.resolve(modelRef, ctx);
-            log.info("构造模型实例: ref={}, name={}", modelRef, config.getName());
+            log.info("构造模型实例: ref={}, name={}, enableThinking={}", modelRef, config.getName(), enableThinking);
             return model;
         } catch (RuntimeException e) {
             log.error("模型实例构造失败: ref={}, error={}", modelRef, e.getMessage());
@@ -66,6 +77,28 @@ public class AgentModelFactory {
         }
     }
 
+    /**
+     * 从模型 config JSON 读取思考模式开关。
+     *
+     * <p>约定:{@code {"enableThinking": true}} 开启 / {@code false} 显式关闭;
+     * 未配置或 JSON 非法时返回 {@code null}(交给各 provider 的默认行为,不改变既有表现)。
+     *
+     * @param config 模型配置实体
+     * @return true / false / null
+     */
+    private static Boolean readEnableThinking(AgentModel config) {
+        if (!StringUtils.hasText(config.getConfig())) {
+            return null;
+        }
+        try {
+            JsonNode node = Json.objectMapper().readTree(config.getConfig()).path(CONFIG_ENABLE_THINKING);
+            return node.isBoolean() ? node.booleanValue() : null;
+        } catch (Exception e) {
+            log.warn("模型 config 解析失败,忽略思考模式开关: name={}, error={}", config.getName(), e.getMessage());
+            return null;
+        }
+    }
+
     /**
      * provider 归一化:DB provider 值 → agentscope SPI 前缀
      */

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

@@ -4,7 +4,9 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.spring.service.impl.ServiceImpl;
 import com.fasterxml.jackson.core.JsonProcessingException;
+import com.fasterxml.jackson.databind.JsonNode;
 import com.fasterxml.jackson.databind.ObjectMapper;
+import com.fasterxml.jackson.databind.node.ObjectNode;
 import com.zsjz.ai.common.exception.ServerException;
 import com.zsjz.ai.module.agent.dto.ChatRequestDTO;
 import com.zsjz.ai.module.agent.dto.CreateChatSessionDTO;
@@ -284,7 +286,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
         return Flux.defer(() -> doStream(req))
                 .onErrorResume(ex -> {
                     String errorMsg = ex.getMessage() != null ? ex.getMessage() : "未知错误";
-                    log.warn("Chat stream error: sessionKey={}, error={}", req.getSessionKey(), errorMsg);
+                    log.warn("Chat stream error: sessionKey={}, ", req.getSessionKey(), ex);
                     // 错误信息持久化:确保刷新后仍可见
                     Long sessionId = parseSessionId(req.getSessionKey());
                     if (sessionId != null) {
@@ -364,6 +366,8 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
 
         // 流中断追踪:用户主动中止时,若 AgentResultEvent 尚未到达,将已累积的文本保存
         AtomicReference<StringBuilder> accumulatedText = new AtomicReference<>(new StringBuilder());
+        // 模型思考过程累积(reasoning_content / thinking):随流结束后写入消息 metadata
+        AtomicReference<StringBuilder> accumulatedThinking = new AtomicReference<>(new StringBuilder());
         AtomicBoolean resultPersisted = new AtomicBoolean(false);
 
         return events.concatMap(event -> {
@@ -411,11 +415,30 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             } else if (event instanceof TextBlockDeltaEvent e) {
                 accumulatedText.get().append(e.getDelta());
                 frames.add(sse("token", Map.of("type", "token", "data", e.getDelta())));
+            } else if (event instanceof ThinkingBlockStartEvent) {
+                // 模型思考过程(reasoning_content / thinking)开始:一轮 ReAct 推理可能有多段思考,
+                // 段间补空行分隔,保证前端拼接后可读
+                StringBuilder thinking = accumulatedThinking.get();
+                if (thinking.length() > 0) {
+                    thinking.append("\n\n");
+                }
+                frames.add(sse("thinking", Map.of("type", "thinking", "phase", "start")));
+            } else if (event instanceof ThinkingBlockDeltaEvent e) {
+                String delta = e.getDelta() != null ? e.getDelta() : "";
+                accumulatedThinking.get().append(delta);
+                Map<String, Object> data = new LinkedHashMap<>();
+                data.put("type", "thinking");
+                data.put("phase", "delta");
+                data.put("data", delta);
+                frames.add(sse("thinking", data));
+            } else if (event instanceof ThinkingBlockEndEvent) {
+                frames.add(sse("thinking", Map.of("type", "thinking", "phase", "end")));
             } else if (event instanceof AgentResultEvent e) {
-                // AI 回复落库(异步,不阻塞事件流);工具调用事件一并持久化成 tool_events
+                // AI 回复落库(异步,不阻塞事件流);工具调用事件与思考过程一并持久化
                 resultPersisted.set(true);
                 String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames);
-                Mono.fromRunnable(() -> persistAssistantMessage(sessionId, e.getResult(), toolEventsJson))
+                String thinkingJson = buildThinkingMetadata(accumulatedThinking.get().toString());
+                Mono.fromRunnable(() -> persistAssistantMessage(sessionId, e.getResult(), toolEventsJson, thinkingJson))
                         .subscribeOn(Schedulers.boundedElastic())
                         .subscribe();
             } else if (event instanceof AgentEndEvent) {
@@ -428,8 +451,9 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             if (!resultPersisted.get()) {
                 String partialText = accumulatedText.get().toString().trim();
                 String toolEventsJson = buildToolEventsJson(toolInputs, toolResults, toolNames);
+                String thinkingJson = buildThinkingMetadata(accumulatedThinking.get().toString());
                 if (!partialText.isEmpty() || !toolEventsJson.isEmpty()) {
-                    Mono.fromRunnable(() -> persistPartialMessage(sessionId, partialText, toolEventsJson))
+                    Mono.fromRunnable(() -> persistPartialMessage(sessionId, partialText, toolEventsJson, thinkingJson))
                             .subscribeOn(Schedulers.boundedElastic())
                             .subscribe();
                 }
@@ -456,8 +480,9 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
      * 将 AI 回复持久化为 assistant 消息。失败不影响流式输出。
      *
      * @param toolEventsJson 工具调用事件 JSON(execute_sql 入参/结果等),可为 null
+     * @param thinkingJson   思考过程 metadata JSON({@code {"thinking": "..."}}),可为 null
      */
-    private void persistAssistantMessage(Long sessionId, Msg result, String toolEventsJson) {
+    private void persistAssistantMessage(Long sessionId, Msg result, String toolEventsJson, String thinkingJson) {
         try {
             AgentChatSession session = chatSessionMapper.selectById(sessionId);
             if (session == null) {
@@ -475,6 +500,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             assistantMessage.setMessageType("text");
             assistantMessage.setStarred(false);
             assistantMessage.setToolEvents(toolEventsJson);
+            assistantMessage.setMetadata(thinkingJson);
             assistantMessage.setCreateAt(LocalDateTime.now());
             messageMapper.insert(assistantMessage);
             chatSessionMapper.update(
@@ -556,7 +582,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
     /**
      * 持久化用户中断时的部分内容(messageType=interrupted),标记为不完整回复。
      */
-    private void persistPartialMessage(Long sessionId, String partialText, String toolEventsJson) {
+    private void persistPartialMessage(Long sessionId, String partialText, String toolEventsJson, String thinkingJson) {
         try {
             AgentMessage msg = new AgentMessage();
             msg.setSessionId(sessionId);
@@ -564,7 +590,7 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
             msg.setContent(partialText.isEmpty() ? "(回复已中断)" : partialText + "\n\n*(回复已中断)*");
             msg.setContentType("text");
             msg.setMessageType("interrupted");
-            msg.setMetadata("{\"interrupted\": true}");
+            msg.setMetadata(mergeThinkingMetadata("{\"interrupted\": true}", thinkingJson));
             msg.setToolEvents(toolEventsJson);
             msg.setStarred(false);
             msg.setCreateAt(LocalDateTime.now());
@@ -599,6 +625,49 @@ public class AgentChatServiceImpl extends ServiceImpl<AgentChatSessionMapper, Ag
         return UUID.randomUUID().getMostSignificantBits() & Long.MAX_VALUE;
     }
 
+    /**
+     * 把流内累积的思考过程包成 metadata JSON({@code {"thinking": "..."}})。
+     *
+     * <p>思考过程存 agent_message.metadata(jsonb)而不是单独建列:刷新会话后前端仍能还原
+     * 「思考过程」面板;无思考内容时返回 null,保持 metadata 原样。
+     */
+    private static String buildThinkingMetadata(String thinking) {
+        String text = thinking == null ? "" : thinking.trim();
+        if (text.isEmpty()) {
+            return null;
+        }
+        try {
+            ObjectNode node = MAPPER.createObjectNode();
+            node.put("thinking", text);
+            return MAPPER.writeValueAsString(node);
+        } catch (JsonProcessingException e) {
+            return null;
+        }
+    }
+
+    /**
+     * 把思考过程合并进已有 metadata JSON(用于中断消息等场景,保留原有标志位)。
+     *
+     * @param baseJson     原有 metadata JSON(如 {@code {"interrupted": true}})
+     * @param thinkingJson {@link #buildThinkingMetadata(String)} 的产物,可为 null
+     */
+    private static String mergeThinkingMetadata(String baseJson, String thinkingJson) {
+        if (thinkingJson == null) {
+            return baseJson;
+        }
+        try {
+            JsonNode base = MAPPER.readTree(baseJson);
+            JsonNode thinking = MAPPER.readTree(thinkingJson).get("thinking");
+            if (!(base instanceof ObjectNode obj) || thinking == null) {
+                return baseJson;
+            }
+            obj.set("thinking", thinking);
+            return MAPPER.writeValueAsString(obj);
+        } catch (Exception e) {
+            return baseJson;
+        }
+    }
+
     /**
      * 将流内累积的工具调用聚合为 JSON 字符串(tool_events 列存储形态,与前端 ToolStep 对齐)。
      *

+ 28 - 21
ai-server/src/main/java/com/zsjz/ai/module/agent/tools/AgentToolRegistry.java

@@ -70,22 +70,34 @@ public class AgentToolRegistry {
 
     // ==================== 工具组名(也是 reset_equipped_tools 的取值) ====================
 
-    /** 通话分析组 */
+    /**
+     * 通话分析组
+     */
     public static final String GROUP_CALL = "call";
 
-    /** 交易分析组 */
+    /**
+     * 交易分析组
+     */
     public static final String GROUP_TRANS = "trans";
 
-    /** 轨迹 / 出行 / 快递组 */
+    /**
+     * 轨迹 / 出行 / 快递组
+     */
     public static final String GROUP_TRACK = "track";
 
-    /** 全文检索与行为时间轴组 */
+    /**
+     * 全文检索与行为时间轴组
+     */
     public static final String GROUP_OTG = "otg";
 
-    /** 关系图谱组 */
+    /**
+     * 关系图谱组
+     */
     public static final String GROUP_GRAPH = "graph";
 
-    /** 人员画像与亲密度组 */
+    /**
+     * 人员画像与亲密度组
+     */
     public static final String GROUP_PERSON = "person";
 
     /**
@@ -110,8 +122,7 @@ public class AgentToolRegistry {
             "stat_trans_continuous_summary", "stat_trans_continuous_detail",
             "stat_trans_fast_fund_flow", "stat_trans_financial",
             "stat_trans_fixed_deposit", "stat_trans_fixed_deposit_detail",
-            "stat_trans_frequency", "get_trans_fund_flow",
-            "stat_trans_flow_graph", "stat_trans_flow_trace",
+            "stat_trans_frequency",
             "stat_trans_sensitive_summary", "stat_trans_sensitive_detail");
 
     private static final List<String> TRACK_TOOLS = List.of(
@@ -124,13 +135,9 @@ public class AgentToolRegistry {
             "stat_together_travel_summary", "stat_together_travel_detail",
             "get_travel_record_detail");
 
-    private static final List<String> OTG_TOOLS = List.of(
-            "search_global", "list_special_dates",
-            "get_time_series", "jump_time_series", "get_time_series_detail");
+    private static final List<String> OTG_TOOLS = List.of();
 
-    private static final List<String> GRAPH_TOOLS = List.of(
-            "get_case_graph", "get_graph_trans_detail", "get_graph_call_detail",
-            "get_graph_node_detail", "list_graph_history", "get_graph_history_content");
+    private static final List<String> GRAPH_TOOLS = List.of("get_case_graph");
 
     private static final List<String> PERSON_TOOLS = List.of(
             "get_person_call_profile", "stat_person_call_station", "stat_person_call_top10",
@@ -254,7 +261,7 @@ public class AgentToolRegistry {
         toolkit.registerTool(new TrackAnalysisTool(trackCellTowerService, trackEnLocalService,
                 trackExpressInfoService, trackMeetService, trackTogetherLiveService,
                 trackTogetherTravelService, sqlResultStore));
-        toolkit.registerTool(new GraphAnalysisTool(graphService, sqlResultStore));
+        toolkit.registerTool(new GraphAnalysisTool(graphService));
         toolkit.registerTool(new PersonAnalysisTool(dataProfileService, intimacyService,
                 personBasicInfoService, personGroupService, sqlResultStore));
 
@@ -287,12 +294,12 @@ public class AgentToolRegistry {
                         + "★ 问「某人去过哪」「两人碰过面吗」「同住/开房」「同行航班车次」「快递收寄」时激活本组。",
                 TRACK_TOOLS);
 
-        createGroup(toolkit, GROUP_OTG,
-                "全文检索与行为时间轴(5 个工具):案件材料的全文关键词检索(search_global)、"
-                        + "特殊日期清单、行为时间轴及其跳转与明细。"
-                        + "★ 问「哪个文件里提到过某某」「原文怎么写的」「某人某段时间的行为时间轴」时激活本组;"
-                        + "做结构化字段的条件筛选/聚合/排序请用 execute_sql 或其它业务工具,不要用全文检索。",
-                OTG_TOOLS);
+        //createGroup(toolkit, GROUP_OTG,
+        //        "全文检索与行为时间轴(5 个工具):案件材料的全文关键词检索(search_global)、"
+        //                + "特殊日期清单、行为时间轴及其跳转与明细。"
+        //                + "★ 问「哪个文件里提到过某某」「原文怎么写的」「某人某段时间的行为时间轴」时激活本组;"
+        //                + "做结构化字段的条件筛选/聚合/排序请用 execute_sql 或其它业务工具,不要用全文检索。",
+        //        OTG_TOOLS);
 
         createGroup(toolkit, GROUP_GRAPH,
                 "关系图谱(6 个工具):案件关系网络取数(节点 + 连线)、图谱上的资金边/通话边/节点明细、"

+ 1 - 205
ai-server/src/main/java/com/zsjz/ai/module/agent/tools/GraphAnalysisTool.java

@@ -77,12 +77,6 @@ public class GraphAnalysisTool {
     /** 图谱维度:全部通话 / 共同通话 / 相互通话 */
     private static final List<Integer> CALL_TYPES = List.of(201, 202, 203);
 
-    /** 节点类型:银行卡/账户 */
-    private static final String CLAZZ_CARD = "card";
-
-    /** 节点类型:手机号 */
-    private static final String CLAZZ_TEL = "tel";
-
     /**
      * 进模型上下文的关系边上限。
      *
@@ -93,11 +87,9 @@ public class GraphAnalysisTool {
     private static final int MAX_EDGES_IN_CONTEXT = 500;
 
     private final GraphService graphService;
-    private final SqlResultStore sqlResultStore;
 
-    public GraphAnalysisTool(GraphService graphService, SqlResultStore sqlResultStore) {
+    public GraphAnalysisTool(GraphService graphService) {
         this.graphService = graphService;
-        this.sqlResultStore = sqlResultStore;
     }
 
     // ==================== 工具定义 ====================
@@ -216,182 +208,9 @@ public class GraphAnalysisTool {
         return 0d;
     }
 
-    /**
-     * 图谱资金边明细
-     */
-    @Tool(name = "get_graph_trans_detail",
-          description = "查询关系图谱上一条资金边(clazz=card_card)的明细交易记录(一行一笔),"
-                  + "可按本方卡号/对方卡号/对方类型(lx)/对方名称/交易摘要/金额/日期筛选,结果以可翻页表格返回。"
-                  + "★ 用户点击图谱上的资金边看明细时用本工具,不要用 execute_sql 手写 SQL —— "
-                  + "收支方向(支出/收入)与 lx=2/3 时用 node1 推导对方姓名/摘要的口径已固化在服务端。"
-                  + "参数直接取该边上的字段:personCardNo=node1、otherCardNo=node2、lx=lx。"
-                  + "★ lx 为 2(对方是人)或 3(对方是其他)时必须传 personCardNo,否则服务端会执行失败。"
-                  + "不传任何筛选会命中全案交易并因超过行数上限被拒绝,请至少给出 personCardNo / otherCardNo / 日期范围之一。",
-          readOnly = true)
-    public ToolResultBlock getGraphTransDetail(
-            @ToolParam(name = "query", description = "图谱资金边明细筛选条件,建议至少传一个定位参数")
-            GraphToolSpecs.GraphTransDetailSpec query) {
-        GraphToolSpecs.GraphTransDetailSpec spec =
-                query == null ? new GraphToolSpecs.GraphTransDetailSpec() : query;
-        try {
-            String personCardNo = trimToNull(spec.personCardNo);
-            Integer lx = spec.lx == null ? null : spec.lx.intValue();
-            if (lx != null && (lx == 2 || lx == 3) && personCardNo == null) {
-                return ToolResults.error("lx=" + lx + " 时必须传 personCardNo(服务端要用它推导对方姓名/交易摘要)");
-            }
-
-            GraphTransDetailQuery q = new GraphTransDetailQuery();
-            q.setPersonCardNo(personCardNo);
-            q.setOtherCardNo(trimToNull(spec.otherCardNo));
-            q.setLx(lx);
-            q.setOtherName(trimToNull(spec.otherName));
-            q.setExt1(trimToNull(spec.ext1));
-            if (spec.transAmount != null) {
-                // 比较符与金额缺一不可:XML 里是 `${p.transAmountSymbol} #{p.transAmount}`,
-                // 只给金额会拼出 `transAmount  1000` 这种非法 SQL
-                q.setTransAmount(spec.transAmount);
-                q.setTransAmountSymbol(symbolOrDefault(spec.transAmountSymbol, SymbolEnum.GE));
-            }
-            q.setStartDate(parseDate(spec.startDate, "startDate"));
-            q.setEndDate(parseDate(spec.endDate, "endDate"));
-            applyFetchWindow(q);
-
-            Page<TransRecord> page = graphService.transDetail(q);
-            return render(page, spec.pageSize);
-        } catch (Exception e) {
-            return fail("图谱资金边明细查询", e);
-        }
-    }
-
-    /**
-     * 图谱通话边明细
-     */
-    @Tool(name = "get_graph_call_detail",
-          description = "查询关系图谱上一条通话边(clazz=tel_tel)的明细通话记录(一行一通电话),"
-                  + "可按本方号码/对方号码/日期筛选,结果以可翻页表格返回。"
-                  + "★ 用户点击图谱上的通话边看明细时用本工具。参数直接取该边上的字段:"
-                  + "personPhone=node1、otherPhone=node2。"
-                  + "该查询按两个方向合并(号码 A 打给 B、B 打给 A 都会命中),不区分主被叫。"
-                  + "建议至少传 personPhone 或 otherPhone 之一,否则会命中全案话单并因超过行数上限被拒绝。",
-          readOnly = true)
-    public ToolResultBlock getGraphCallDetail(
-            @ToolParam(name = "query", description = "图谱通话边明细筛选条件,建议至少传一个号码")
-            GraphToolSpecs.GraphCallDetailSpec query) {
-        GraphToolSpecs.GraphCallDetailSpec spec =
-                query == null ? new GraphToolSpecs.GraphCallDetailSpec() : query;
-        try {
-            GraphCallDetailQuery q = new GraphCallDetailQuery();
-            q.setPersonPhone(trimToNull(spec.personPhone));
-            q.setOtherPhone(trimToNull(spec.otherPhone));
-            q.setStartDate(parseDate(spec.startDate, "startDate"));
-            q.setEndDate(parseDate(spec.endDate, "endDate"));
-            applyFetchWindow(q);
-
-            Page<CallRecord> page = graphService.callDetail(q);
-            return render(page, spec.pageSize);
-        } catch (Exception e) {
-            return fail("图谱通话边明细查询", e);
-        }
-    }
-
-    /**
-     * 图谱节点明细
-     */
-    @Tool(name = "get_graph_node_detail",
-          description = "查询关系图谱上某个节点的明细:clazz=card 时按卡号查交易明细(该卡作为本方卡或对方卡都算),"
-                  + "clazz=tel 时按号码查通话明细(该号作为本方号码)。"
-                  + "★ 用户点击图谱节点看明细时用本工具,clazz 与 node 必传(取值来自 nodeList 的 clazz / node)。"
-                  + "返回 JSON 结构:{\"pages\":{\"records\":[明细行...],\"total\":总行数,...},\"head\":表头定义,\"detail\":false},"
-                  + "即 records 为该节点的明细行(最多 pageSize 行,默认 50),total 是总行数。"
-                  + "本工具返回的是原始分页 JSON,不是表格;若要可翻页的完整明细表,"
-                  + "请改用 get_graph_trans_detail(资金)/ get_graph_call_detail(通话)并传入该节点与对端节点。",
-          readOnly = true)
-    public ToolResultBlock getGraphNodeDetail(
-            @ToolParam(name = "query", description = "图谱节点明细筛选条件,clazz 与 node 必传")
-            GraphToolSpecs.GraphNodeDetailSpec query) {
-        GraphToolSpecs.GraphNodeDetailSpec spec =
-                query == null ? new GraphToolSpecs.GraphNodeDetailSpec() : query;
-        try {
-            String clazz = trimToNull(spec.clazz);
-            if (clazz == null) {
-                return ToolResults.error("clazz 必传:card(银行卡/账户节点)或 tel(手机号节点)");
-            }
-            if (!CLAZZ_CARD.equals(clazz) && !CLAZZ_TEL.equals(clazz)) {
-                return ToolResults.error("clazz 只能取 card 或 tel,当前值:" + clazz);
-            }
-            String node = trimToNull(spec.node);
-            if (node == null) {
-                return ToolResults.error("node 必传:clazz=card 时传卡号,clazz=tel 时传手机号");
-            }
-
-            GraphCallDetailQuery q = new GraphCallDetailQuery();
-            q.setClazz(clazz);
-            q.setNode(node);
-            if (spec.transAmount != null) {
-                // 服务端用 getTransAmountSymbol() 取符号并拼进 SQL,比较符必须走白名单
-                q.setTransAmount(DataUtil.convertAmount(spec.transAmount));
-                q.setTransAmountSymbol(symbolOrDefault(spec.transAmountSymbol, SymbolEnum.EQ));
-            }
-            q.setStartDate(parseDate(spec.startDate, "startDate"));
-            q.setEndDate(parseDate(spec.endDate, "endDate"));
-            q.setPage(1);
-            q.setLimit(ToolResultTable.normalizePageSize(spec.pageSize));
-
-            Map<String, Object> result = graphService.nodeDetail(q);
-            return ToolResults.ok(Json.toStr(result));
-        } catch (Exception e) {
-            return fail("图谱节点明细查询", e);
-        }
-    }
-
-
-    /**
-     * 读取图谱历史内容
-     */
-    @Tool(name = "get_graph_history_content",
-          description = "读取某条关系图谱历史(快照)的内容 JSON。"
-                  + "★ 必须先调用 list_graph_history 拿到 id,再把该 id 传进来;id 不存在或文件已被清理会返回错误。"
-                  + "返回的 JSON 是保存图谱时前端存下的节点/连线数据,可直接据此复述或再喂给 render_graph 重新渲染。",
-          readOnly = true)
-    public ToolResultBlock getGraphHistoryContent(
-            @ToolParam(name = "id", description = "关系图谱历史ID,取自 list_graph_history 返回的 id") Long id) {
-        try {
-            if (id == null) {
-                return ToolResults.error("图谱历史不存在或内容为空: id=null");
-            }
-            String content = graphService.readGraphHisContent(id);
-            if (content == null || content.trim().isEmpty()) {
-                return ToolResults.error("图谱历史不存在或内容为空: id=" + id);
-            }
-            return ToolResults.ok(content);
-        } catch (Exception e) {
-            return fail("关系图谱历史内容读取", e);
-        }
-    }
 
     // ==================== 内部实现 ====================
 
-    /**
-     * 分页取全量:页大小设为 {@code FETCH_LIMIT + 1},让数据库只回上限内的数据,
-     * 同时用 {@code Page#getTotal()}(MP 的 count 查询)判断真实总数是否超限。
-     */
-    private static void applyFetchWindow(Query query) {
-        query.setPage(1);
-        query.setLimit(FETCH_LIMIT + 1L);
-    }
-
-    /**
-     * 统一出口:超限报错,否则写入结果缓存并返回首页表格 JSON
-     */
-    private ToolResultBlock render(Page<?> page, Number pageSize) {
-        long total = page.getTotal();
-        if (total > FETCH_LIMIT) {
-            return ToolResults.error("结果集过大(" + total + " 行,上限 " + FETCH_LIMIT
-                    + "),请收窄人员/卡号/号码/日期范围等条件后重试");
-        }
-        return ToolResultTable.store(sqlResultStore, null, page.getRecords(), 1,
-                ToolResultTable.normalizePageSize(pageSize));
-    }
 
     /**
      * 失败:区分「参数格式错」与「执行错」,后者补一句开案提示
@@ -420,29 +239,6 @@ public class GraphAnalysisTool {
         }
     }
 
-    /**
-     * 金额比较符白名单归一化。
-     *
-     * <p>绝不能把模型给的字符串原样拼进 SQL:Mapper 里是 {@code ${p.transAmountSymbol}}
-     * ({@code GraphCallDetailQuery} 里更是把枚举直接取成字符串),是纯粹的文本替换。
-     * 这里同时接受枚举名(GE/LE/…)与符号写法(&gt;=/&lt;=…),非法值直接报错。
-     */
-    private static SymbolEnum symbolOrDefault(String value, SymbolEnum defaultValue) {
-        String text = trimToNull(value);
-        if (text == null) {
-            return defaultValue;
-        }
-        return switch (text.toUpperCase()) {
-            case "GE", ">=" -> SymbolEnum.GE;
-            case "LE", "<=" -> SymbolEnum.LE;
-            case "GT", ">" -> SymbolEnum.GT;
-            case "LT", "<" -> SymbolEnum.LT;
-            case "EQ", "=" -> SymbolEnum.EQ;
-            default -> throw new IllegalArgumentException(
-                    "transAmountSymbol 取值不合法(应为 GE/LE/GT/LT/EQ 或 >=/<=/>/</=):" + value);
-        };
-    }
-
     /**
      * 图谱维度归一化:去 null、去重保序;空列表归一成 null
      */

+ 0 - 26
ai-server/src/main/java/com/zsjz/ai/module/agent/tools/TransAnalysisTool.java

@@ -536,32 +536,6 @@ public class TransAnalysisTool {
 
     // ==================== 明细 / 资金流向图 ====================
 
-    /**
-     * 交易明细(资金流向列表视图)
-     */
-    @Tool(name = "get_trans_fund_flow",
-          description = "查询资金流向明细列表(交易记录的列表视图,字段更贴合资金流向展示),"
-                  + "支持按本方姓名/卡号、对方姓名/卡号、收付方向、金额区间、交易摘要/网点/备注关键词、日期范围筛选。"
-                  + "★ 问「某人的资金流向」「和某人之间的转账明细」时用本工具;"
-                  + "它的返回结果可直接作为 stat_trans_flow_graph / stat_trans_flow_trace 的起点参数"
-                  + "(取 personCardNos / otherName / otherCardNo / transDateTime / debitCreditFlag)。",
-          readOnly = true)
-    public ToolResultBlock getTransFundFlow(
-            @ToolParam(name = "query", description = "资金流向筛选条件,字段全部可选但建议至少定位到人/卡")
-            TransToolSpecs.TransRecordSpec query) {
-        TransToolSpecs.TransRecordSpec spec =
-                query == null ? new TransToolSpecs.TransRecordSpec() : query;
-        try {
-            TransRecordQuery q = new TransRecordQuery();
-            fillTransRecordQuery(q, spec);
-            applyFetchWindow(q);
-
-            Page<TransFundFlowDTO> page = transFundFlowService.getTransFundFlowPage(q);
-            return render(page, spec.pageSize);
-        } catch (Exception e) {
-            return fail("资金流向明细查询", e);
-        }
-    }
 
     /**
      * 交易明细(原始记录)

+ 1 - 1
ai-server/src/main/java/com/zsjz/ai/module/agent/tools/TransToolSpecs.java

@@ -436,7 +436,7 @@ public final class TransToolSpecs {
     }
 
     /**
-     * 交易明细 / 资金流向明细的公共入参({@code get_trans_records} / {@code get_trans_fund_flow})
+     * 交易明细 / 资金流向明细的公共入参({@code get_trans_records} /)
      */
     @JsonInclude(JsonInclude.Include.NON_NULL)
     public static class TransRecordSpec {