From 007e470ab70d5d4fa503db8b9fc296f531941c5a Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期二, 28 四月 2026 10:18:54 +0800
Subject: [PATCH] feat(ai): 添加审批待办助手功能

---
 src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java                              |  794 ++++++++++++++++++----------
 src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java                            |   17 
 src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java                 |   85 ++
 src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java               |  191 +++++++
 src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java                          |   24 
 src/main/resources/application-dev-pro.yml                                          |    9 
 pom.xml                                                                             |    4 
 src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java                                |   19 
 src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java                          |   41 
 src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java                        |    1 
 src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java |   32 
 src/main/java/com/ruoyi/ai/pojo/AiChatSession.java                                  |   34 +
 src/main/java/com/ruoyi/ai/controller/XiaozhiController.java                        |  136 ++++
 doc/20260428_create_table_ai_chat_session.sql                                       |   16 
 src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java                        |   35 +
 src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java                         |  117 ++++
 src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java                                |   15 
 src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java                          |    7 
 src/main/java/com/ruoyi/ai/service/AiChatSessionService.java                        |   22 
 19 files changed, 1,251 insertions(+), 348 deletions(-)

diff --git a/doc/20260428_create_table_ai_chat_session.sql b/doc/20260428_create_table_ai_chat_session.sql
new file mode 100644
index 0000000..f490376
--- /dev/null
+++ b/doc/20260428_create_table_ai_chat_session.sql
@@ -0,0 +1,16 @@
+CREATE TABLE IF NOT EXISTS `ai_chat_session` (
+  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭',
+  `memory_id` varchar(64) NOT NULL COMMENT '浼氳瘽ID',
+  `user_id` bigint NOT NULL COMMENT '鐢ㄦ埛ID',
+  `tenant_id` bigint DEFAULT NULL COMMENT '绉熸埛ID',
+  `title` varchar(128) DEFAULT NULL COMMENT '浼氳瘽鏍囬',
+  `last_message` varchar(512) DEFAULT NULL COMMENT '鏈�鍚庝竴鏉℃秷鎭�',
+  `message_count` int NOT NULL DEFAULT 0 COMMENT '娑堟伅鏁伴噺',
+  `last_chat_time` datetime DEFAULT NULL COMMENT '鏈�鍚庤亰澶╂椂闂�',
+  `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+  `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+  PRIMARY KEY (`id`),
+  UNIQUE KEY `uk_ai_chat_session_memory_id` (`memory_id`),
+  KEY `idx_ai_chat_session_user_tenant` (`user_id`, `tenant_id`),
+  KEY `idx_ai_chat_session_last_chat_time` (`last_chat_time`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_general_ci COMMENT='AI鍘嗗彶浼氳瘽鍏冩暟鎹〃';
diff --git a/pom.xml b/pom.xml
index b645d4c..e840acd 100644
--- a/pom.xml
+++ b/pom.xml
@@ -157,6 +157,10 @@
             <groupId>dev.langchain4j</groupId>
             <artifactId>langchain4j-open-ai-spring-boot-starter</artifactId>
         </dependency>
+        <dependency>
+            <groupId>dev.langchain4j</groupId>
+            <artifactId>langchain4j-reactor</artifactId>
+        </dependency>
 
         <dependency>
             <groupId>dev.langchain4j</groupId>
diff --git a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
index 6e1a948..daaaf74 100644
--- a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
+++ b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
@@ -16,6 +16,8 @@
 
     private static final Pattern APPROVE_ID_PATTERN = Pattern.compile("\\b[A-Za-z]*\\d{8,}\\b");
     private static final Pattern LIMIT_PATTERN = Pattern.compile("(鍓峾鏈�杩�)?(\\d{1,2})鏉�");
+    private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
+    private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?)");
 
     private final ApproveTodoTools approveTodoTools;
 
@@ -23,7 +25,7 @@
         this.approveTodoTools = approveTodoTools;
     }
 
-    public String tryExecute(String message) {
+    public String tryExecute(String memoryId, String message) {
         if (!StringUtils.hasText(message)) {
             return null;
         }
@@ -32,46 +34,52 @@
         String approveId = extractApproveId(text);
 
         if (containsAny(text, "缁熻", "鍒嗘瀽", "鍥捐〃", "瓒嬪娍", "鍗犳瘮")) {
-            return approveTodoTools.getTodoStats();
+            return approveTodoTools.getTodoStats(
+                    memoryId,
+                    extractStartDate(text),
+                    extractEndDate(text),
+                    extractTimeRange(text)
+            );
         }
         if (containsAny(text, "娴佽浆", "杩涘害", "鑺傜偣", "鏃ュ織")) {
             return StringUtils.hasText(approveId)
-                    ? approveTodoTools.getTodoProgress(approveId)
+                    ? approveTodoTools.getTodoProgress(memoryId, approveId)
                     : missingApproveId("todo_progress", "鏌ヨ瀹℃壒杩涘害闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
         if (containsAny(text, "璇︽儏", "鏄庣粏") && !containsAny(text, "鍒楄〃")) {
             return StringUtils.hasText(approveId)
-                    ? approveTodoTools.getTodoDetail(approveId)
+                    ? approveTodoTools.getTodoDetail(memoryId, approveId)
                     : missingApproveId("todo_detail", "鏌ヨ瀹℃壒璇︽儏闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
         if (containsAny(text, "鍙栨秷瀹℃牳", "鎾ら攢瀹℃牳", "鍥為��瀹℃牳")) {
             return StringUtils.hasText(approveId)
-                    ? approveTodoTools.cancelReviewTodo(approveId, extractTail(text, "鍘熷洜"))
+                    ? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractTail(text, "鍘熷洜"))
                     : missingApproveId("cancel_review_action", "鍙栨秷瀹℃牳闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
         if (containsAny(text, "鍒犻櫎")) {
             return StringUtils.hasText(approveId)
-                    ? approveTodoTools.deleteTodo(approveId)
+                    ? approveTodoTools.deleteTodo(memoryId, approveId)
                     : missingApproveId("delete_action", "鍒犻櫎瀹℃壒鍗曢渶瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
         if (containsAny(text, "椹冲洖", "鎷掔粷")) {
             return StringUtils.hasText(approveId)
-                    ? approveTodoTools.reviewTodo(approveId, "reject", extractTail(text, "鍘熷洜"))
+                    ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractTail(text, "鍘熷洜"))
                     : missingApproveId("review_action", "椹冲洖瀹℃壒闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
         if (containsAny(text, "瀹℃牳閫氳繃", "瀹℃壒閫氳繃", "閫氳繃瀹℃壒", "鍚屾剰瀹℃壒", "瀹℃壒鍚屾剰")) {
             return StringUtils.hasText(approveId)
-                    ? approveTodoTools.reviewTodo(approveId, "approve", extractTail(text, "澶囨敞"))
+                    ? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "澶囨敞"))
                     : missingApproveId("review_action", "瀹℃壒閫氳繃闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
         if (StringUtils.hasText(approveId)
                 && containsAny(text, "閫氳繃", "鍚屾剰")
                 && !containsAny(text, "鏈�氳繃", "閫氳繃鐜�", "瀹℃壒閫氳繃鐜�", "瀹℃牳閫氳繃鐜�")) {
-            return approveTodoTools.reviewTodo(approveId, "approve", extractTail(text, "澶囨敞"));
+            return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "澶囨敞"));
         }
         if (containsAny(text, "淇敼")) {
             return StringUtils.hasText(approveId)
                     ? approveTodoTools.updateTodo(
+                    memoryId,
                     approveId,
                     extractValue(text, "鏍囬"),
                     extractDateValue(text, "寮�濮嬫棩鏈�"),
@@ -84,6 +92,7 @@
         }
         if (containsAny(text, "鍒楄〃", "寰呭姙", "鏌ヨ瀹℃壒")) {
             return approveTodoTools.listTodos(
+                    memoryId,
                     extractStatus(text),
                     extractApproveType(text),
                     extractKeyword(text),
@@ -165,33 +174,75 @@
                 .replace("寰呭姙", "")
                 .replace("鍒楄〃", "")
                 .replace("鍓�10鏉�", "")
-                .replace("鍓�20鏉�", "")
+                .replace("鏈�杩�10鏉�", "")
                 .trim();
         return cleaned.length() >= 2 ? cleaned : null;
     }
 
     private String extractValue(String text, String fieldName) {
-        Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)([^锛屻��,锛�;\\s]+)").matcher(text);
+        Pattern pattern = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗鏄�)?[:锛歖?[\\s]*([^,锛屻�傦紱;\\s]+)");
+        Matcher matcher = pattern.matcher(text);
         return matcher.find() ? matcher.group(2) : null;
     }
 
     private String extractDateValue(String text, String fieldName) {
-        Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)(\\d{4}-\\d{2}-\\d{2})").matcher(text);
-        return matcher.find() ? matcher.group(2) : null;
+        int index = text.indexOf(fieldName);
+        if (index < 0) {
+            return null;
+        }
+        Matcher matcher = DATE_PATTERN.matcher(text.substring(index));
+        return matcher.find() ? matcher.group(1) : null;
+    }
+
+    private String extractStartDate(String text) {
+        Matcher matcher = DATE_PATTERN.matcher(text);
+        return matcher.find() ? matcher.group(1) : null;
+    }
+
+    private String extractEndDate(String text) {
+        Matcher matcher = DATE_PATTERN.matcher(text);
+        if (!matcher.find()) {
+            return null;
+        }
+        return matcher.find() ? matcher.group(1) : null;
+    }
+
+    private String extractTimeRange(String text) {
+        if (containsAny(text, "浠婂ぉ", "鏄ㄦ棩", "鏄ㄥぉ", "鏈懆", "涓婂懆", "鏈湀", "涓婃湀", "鏈勾", "浠婂勾", "鍘诲勾")) {
+            return text;
+        }
+        if (Pattern.compile("杩慭\d+(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text).find()) {
+            return text;
+        }
+        if (Pattern.compile("鏈�杩慭\d+(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text).find()) {
+            return text;
+        }
+        if (text.contains("鍒�") || text.contains("鑷�")) {
+            return text;
+        }
+        return null;
     }
 
     private Integer extractIntegerValue(String text, String fieldName) {
-        Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)(\\d{1,2})").matcher(text);
+        if (!text.contains(fieldName)) {
+            return null;
+        }
+        Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗鏄�)?[:锛歖?[\\s]*(\\d{1,2})").matcher(text);
         return matcher.find() ? Integer.parseInt(matcher.group(2)) : null;
     }
 
     private BigDecimal extractBigDecimalValue(String text, String fieldName) {
-        Matcher matcher = Pattern.compile(fieldName + "(鏀逛负|淇敼涓簗涓簗鏄�)(\\d+(\\.\\d+)?)").matcher(text);
-        return matcher.find() ? new BigDecimal(matcher.group(2)) : null;
+        int index = text.indexOf(fieldName);
+        if (index < 0) {
+            return null;
+        }
+        Matcher matcher = NUMBER_PATTERN.matcher(text.substring(index));
+        return matcher.find() ? new BigDecimal(matcher.group(1)) : null;
     }
 
     private String extractTail(String text, String key) {
-        Matcher matcher = Pattern.compile(key + "(鏄瘄涓簗锛殀:)?(.+)").matcher(text);
+        Pattern pattern = Pattern.compile(key + "(鏄瘄涓�)?[:锛歖?[\\s]*(.+)");
+        Matcher matcher = pattern.matcher(text);
         return matcher.find() ? matcher.group(2).trim() : null;
     }
 
diff --git a/src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java b/src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java
new file mode 100644
index 0000000..131a9b7
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/FileAnalyzeAgent.java
@@ -0,0 +1,24 @@
+package com.ruoyi.ai.assistant;
+
+import dev.langchain4j.service.MemoryId;
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.spring.AiService;
+import reactor.core.publisher.Flux;
+
+import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
+
+@AiService(
+        wiringMode = EXPLICIT,
+        streamingChatModel = "qwenStreamingChatModel",
+        chatMemoryProvider = "chatMemoryProvider"
+)
+public interface FileAnalyzeAgent {
+
+    @SystemMessage("""
+            浣犳槸浼佷笟鏂囨。鍒嗘瀽鍔╂墜銆�
+            璇蜂弗鏍煎熀浜庣敤鎴锋彁渚涚殑鏂囦欢鍐呭杩涜鍒嗘瀽锛岃緭鍑鸿缁撴瀯鍖栥�佸噯纭�佺畝娲併��
+            鑻ユ枃浠跺唴瀹逛笉瓒充互鏀寔缁撹锛屾槑纭寚鍑衡�滀笉瓒充俊鎭�濆苟缁欏嚭闇�瑕佽ˉ鍏呯殑鏁版嵁椤广��
+            """)
+    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
+}
diff --git a/src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java b/src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java
new file mode 100644
index 0000000..3a5877b
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/context/AiSessionUserContext.java
@@ -0,0 +1,35 @@
+package com.ruoyi.ai.context;
+
+import com.ruoyi.framework.security.LoginUser;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.Map;
+import java.util.concurrent.ConcurrentHashMap;
+
+@Component
+public class AiSessionUserContext {
+
+    private final Map<String, LoginUser> loginUserByMemoryId = new ConcurrentHashMap<>();
+
+    public void bind(String memoryId, LoginUser loginUser) {
+        if (!StringUtils.hasText(memoryId) || loginUser == null) {
+            return;
+        }
+        loginUserByMemoryId.put(memoryId, loginUser);
+    }
+
+    public LoginUser get(String memoryId) {
+        if (!StringUtils.hasText(memoryId)) {
+            return null;
+        }
+        return loginUserByMemoryId.get(memoryId);
+    }
+
+    public void remove(String memoryId) {
+        if (!StringUtils.hasText(memoryId)) {
+            return;
+        }
+        loginUserByMemoryId.remove(memoryId);
+    }
+}
diff --git a/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java b/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
index e1223cc..eac43b2 100644
--- a/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
+++ b/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
@@ -2,35 +2,161 @@
 
 import com.ruoyi.ai.assistant.ApproveTodoAgent;
 import com.ruoyi.ai.assistant.ApproveTodoIntentExecutor;
+import com.ruoyi.ai.assistant.FileAnalyzeAgent;
 import com.ruoyi.ai.bean.ChatForm;
+import com.ruoyi.ai.context.AiSessionUserContext;
+import com.ruoyi.ai.service.AiChatSessionService;
+import com.ruoyi.ai.service.AiFileTextExtractor;
+import com.ruoyi.ai.store.MongoChatMemoryStore;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.security.LoginUser;
+import com.ruoyi.framework.web.controller.BaseController;
+import com.ruoyi.framework.web.domain.AjaxResult;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.UserMessage;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
 import org.springframework.web.bind.annotation.PostMapping;
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
 import reactor.core.publisher.Flux;
+
+import java.io.IOException;
+import java.util.List;
+import java.util.NoSuchElementException;
+import java.util.UUID;
 
 @Tag(name = "鍗忓悓鍔炲叕鍔╂墜")
 @RestController
 @RequestMapping("/xiaozhi")
-public class XiaozhiController {
+public class XiaozhiController extends BaseController {
+
+    private static final String FILE_ANALYZE_MEMORY_PREFIX = "file-analyze::";
 
     private final ApproveTodoAgent approveTodoAgent;
     private final ApproveTodoIntentExecutor approveTodoIntentExecutor;
+    private final FileAnalyzeAgent fileAnalyzeAgent;
+    private final AiSessionUserContext aiSessionUserContext;
+    private final MongoChatMemoryStore mongoChatMemoryStore;
+    private final AiFileTextExtractor aiFileTextExtractor;
+    private final AiChatSessionService aiChatSessionService;
 
-    public XiaozhiController(ApproveTodoAgent approveTodoAgent, ApproveTodoIntentExecutor approveTodoIntentExecutor) {
+    public XiaozhiController(ApproveTodoAgent approveTodoAgent,
+                             ApproveTodoIntentExecutor approveTodoIntentExecutor,
+                             FileAnalyzeAgent fileAnalyzeAgent,
+                             AiSessionUserContext aiSessionUserContext,
+                             MongoChatMemoryStore mongoChatMemoryStore,
+                             AiFileTextExtractor aiFileTextExtractor,
+                             AiChatSessionService aiChatSessionService) {
         this.approveTodoAgent = approveTodoAgent;
         this.approveTodoIntentExecutor = approveTodoIntentExecutor;
+        this.fileAnalyzeAgent = fileAnalyzeAgent;
+        this.aiSessionUserContext = aiSessionUserContext;
+        this.mongoChatMemoryStore = mongoChatMemoryStore;
+        this.aiFileTextExtractor = aiFileTextExtractor;
+        this.aiChatSessionService = aiChatSessionService;
     }
 
     @Operation(summary = "瀵硅瘽")
     @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
     public Flux<String> chat(@RequestBody ChatForm chatForm) {
-        String directResponse = approveTodoIntentExecutor.tryExecute(chatForm.getMessage());
-        if (directResponse != null) {
+        if (!StringUtils.hasText(chatForm.getMemoryId())) {
+            return Flux.just("memoryId涓嶈兘涓虹┖");
+        }
+        if (!StringUtils.hasText(chatForm.getMessage())) {
+            return Flux.just("message涓嶈兘涓虹┖");
+        }
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        String memoryId = chatForm.getMemoryId();
+        String userMessage = chatForm.getMessage();
+
+        aiSessionUserContext.bind(memoryId, loginUser);
+        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
+
+        String directResponse = approveTodoIntentExecutor.tryExecute(memoryId, userMessage);
+        if (StringUtils.isNotEmpty(directResponse)) {
+            mongoChatMemoryStore.appendMessages(
+                    memoryId,
+                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
+            );
+            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
             return Flux.just(directResponse);
         }
-        return approveTodoAgent.chat(chatForm.getMemoryId(), chatForm.getMessage());
+
+        return approveTodoAgent.chat(memoryId, userMessage)
+                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
+                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
+    }
+
+    @Operation(summary = "涓婁紶鏂囦欢鍒嗘瀽")
+    @PostMapping(value = "/analyze-file", consumes = "multipart/form-data", produces = "text/stream;charset=utf-8")
+    public Flux<String> analyzeFile(@RequestParam("file") MultipartFile file,
+                                    @RequestParam(value = "message", required = false) String message,
+                                    @RequestParam(value = "memoryId", required = false) String memoryId) {
+        String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString();
+        String finalMemoryId = rawMemoryId.startsWith(FILE_ANALYZE_MEMORY_PREFIX)
+                ? rawMemoryId
+                : FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId;
+
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        aiSessionUserContext.bind(finalMemoryId, loginUser);
+
+        String finalMessage = StringUtils.hasText(message) ? message : "璇峰垎鏋愯繖涓枃浠剁殑鏍稿績鍐呭骞剁粰鍑烘�荤粨";
+        String fileText;
+        try {
+            fileText = aiFileTextExtractor.extractText(file);
+        } catch (IllegalArgumentException ex) {
+            return Flux.just(ex.getMessage());
+        } catch (IOException ex) {
+            return Flux.just("鏂囦欢璇诲彇澶辫触");
+        }
+
+        if (!StringUtils.hasText(fileText)) {
+            return Flux.just("鏈彁鍙栧埌鏈夋晥鏂囦欢鍐呭");
+        }
+
+        String limitedContent = fileText.length() > 12000 ? fileText.substring(0, 12000) : fileText;
+        String userPrompt = "浣犲皢鍒嗘瀽鐢ㄦ埛涓婁紶鐨勬枃浠跺唴瀹广�俓n"
+                + "鏂囦欢鍚�: " + file.getOriginalFilename() + "\n"
+                + "鐢ㄦ埛闂: " + finalMessage + "\n"
+                + "鏂囦欢鍐呭濡備笅:\n"
+                + limitedContent;
+
+        aiChatSessionService.touchSession(finalMemoryId, loginUser, "鏂囦欢鍒嗘瀽: " + finalMessage);
+
+        return Flux.defer(() -> fileAnalyzeAgent.chat(finalMemoryId, userPrompt))
+                .onErrorResume(NoSuchElementException.class, ex -> {
+                    // DashScope 鍦ㄥ巻鍙叉秷鎭吋瀹瑰満鏅笅鍙兘鎶� NoSuchElementException锛屾竻绌哄悗閲嶈瘯涓�娆�
+                    mongoChatMemoryStore.deleteMessages(finalMemoryId);
+                    return fileAnalyzeAgent.chat(finalMemoryId, userPrompt);
+                })
+                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
+                .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
+    }
+
+    @Operation(summary = "浼氳瘽鍒楄〃")
+    @GetMapping("/history/sessions")
+    public AjaxResult listSessions() {
+        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
+    }
+
+    @Operation(summary = "浼氳瘽娑堟伅")
+    @GetMapping("/history/messages/{memoryId}")
+    public AjaxResult listMessages(@PathVariable String memoryId) {
+        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
+    }
+
+    @Operation(summary = "鍒犻櫎浼氳瘽")
+    @DeleteMapping("/history/{memoryId}")
+    public AjaxResult deleteSession(@PathVariable String memoryId) {
+        aiSessionUserContext.remove(memoryId);
+        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
     }
 }
diff --git a/src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java b/src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
new file mode 100644
index 0000000..9242b3e
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/dto/AiChatMessageDto.java
@@ -0,0 +1,15 @@
+package com.ruoyi.ai.dto;
+
+import lombok.AllArgsConstructor;
+import lombok.Data;
+import lombok.NoArgsConstructor;
+
+@Data
+@NoArgsConstructor
+@AllArgsConstructor
+public class AiChatMessageDto {
+
+    private String role;
+
+    private String content;
+}
diff --git a/src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java b/src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java
new file mode 100644
index 0000000..f62f3b5
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/dto/AiChatSessionDto.java
@@ -0,0 +1,19 @@
+package com.ruoyi.ai.dto;
+
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class AiChatSessionDto {
+
+    private String memoryId;
+
+    private String title;
+
+    private String lastMessage;
+
+    private Integer messageCount;
+
+    private Date lastChatTime;
+}
diff --git a/src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java b/src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java
new file mode 100644
index 0000000..8983396
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/mapper/AiChatSessionMapper.java
@@ -0,0 +1,7 @@
+package com.ruoyi.ai.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.ai.pojo.AiChatSession;
+
+public interface AiChatSessionMapper extends BaseMapper<AiChatSession> {
+}
diff --git a/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java b/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
index 90f1e02..0a239d8 100644
--- a/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
+++ b/src/main/java/com/ruoyi/ai/mongodbBean/ChatMessages.java
@@ -5,23 +5,26 @@
 import lombok.NoArgsConstructor;
 import org.bson.types.ObjectId;
 import org.springframework.data.annotation.Id;
+import org.springframework.data.mongodb.core.index.Indexed;
 import org.springframework.data.mongodb.core.mapping.Document;
 
-/**
- * @author :yys
- * @date : 2025/5/2 19:13
- */
+import java.util.Date;
+
 @Data
 @AllArgsConstructor
 @NoArgsConstructor
 @Document("chat_messages")
 public class ChatMessages {
-    //鍞竴鏍囪瘑锛屾槧灏勫埌 MongoDB 鏂囨。鐨� _id 瀛楁
+
     @Id
     private ObjectId id;
 
-    private String messageId;
+    @Indexed(unique = true)
+    private String memoryId;
 
-    private String content; //瀛樺偍褰撳墠鑱婂ぉ璁板綍鍒楄〃鐨刯son瀛楃涓�
+    private String content;
 
+    private Date createTime;
+
+    private Date updateTime;
 }
diff --git a/src/main/java/com/ruoyi/ai/pojo/AiChatSession.java b/src/main/java/com/ruoyi/ai/pojo/AiChatSession.java
new file mode 100644
index 0000000..cc289f8
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/pojo/AiChatSession.java
@@ -0,0 +1,34 @@
+package com.ruoyi.ai.pojo;
+
+import com.baomidou.mybatisplus.annotation.IdType;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+@TableName("ai_chat_session")
+public class AiChatSession {
+
+    @TableId(type = IdType.AUTO)
+    private Long id;
+
+    private String memoryId;
+
+    private Long userId;
+
+    private Long tenantId;
+
+    private String title;
+
+    private String lastMessage;
+
+    private Integer messageCount;
+
+    private Date lastChatTime;
+
+    private Date createTime;
+
+    private Date updateTime;
+}
diff --git a/src/main/java/com/ruoyi/ai/service/AiChatSessionService.java b/src/main/java/com/ruoyi/ai/service/AiChatSessionService.java
new file mode 100644
index 0000000..ef47e03
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/service/AiChatSessionService.java
@@ -0,0 +1,22 @@
+package com.ruoyi.ai.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.ai.dto.AiChatMessageDto;
+import com.ruoyi.ai.dto.AiChatSessionDto;
+import com.ruoyi.ai.pojo.AiChatSession;
+import com.ruoyi.framework.security.LoginUser;
+
+import java.util.List;
+
+public interface AiChatSessionService extends IService<AiChatSession> {
+
+    void touchSession(String memoryId, LoginUser loginUser, String userMessage);
+
+    void refreshSessionStats(String memoryId, LoginUser loginUser);
+
+    List<AiChatSessionDto> listCurrentUserSessions(LoginUser loginUser);
+
+    List<AiChatMessageDto> listCurrentUserMessages(String memoryId, LoginUser loginUser);
+
+    boolean deleteCurrentUserSession(String memoryId, LoginUser loginUser);
+}
diff --git a/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java b/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
new file mode 100644
index 0000000..82cf1eb
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
@@ -0,0 +1,117 @@
+package com.ruoyi.ai.service;
+
+import com.ruoyi.common.utils.StringUtils;
+import org.apache.poi.hssf.usermodel.HSSFWorkbook;
+import org.apache.poi.ss.usermodel.DataFormatter;
+import org.apache.poi.ss.usermodel.Row;
+import org.apache.poi.ss.usermodel.Sheet;
+import org.apache.poi.xssf.usermodel.XSSFWorkbook;
+import org.apache.poi.xwpf.extractor.XWPFWordExtractor;
+import org.apache.poi.xwpf.usermodel.XWPFDocument;
+import org.springframework.stereotype.Component;
+import org.springframework.web.multipart.MultipartFile;
+
+import java.io.ByteArrayInputStream;
+import java.io.IOException;
+import java.nio.charset.StandardCharsets;
+
+@Component
+public class AiFileTextExtractor {
+
+    private static final long MAX_FILE_SIZE = 10L * 1024 * 1024;
+
+    public String extractText(MultipartFile file) throws IOException {
+        if (file == null || file.isEmpty()) {
+            throw new IllegalArgumentException("鏂囦欢涓嶈兘涓虹┖");
+        }
+        if (file.getSize() > MAX_FILE_SIZE) {
+            throw new IllegalArgumentException("鏂囦欢杩囧ぇ锛岃鎺у埗鍦�10MB浠ュ唴");
+        }
+
+        String filename = file.getOriginalFilename();
+        String ext = getExtension(filename);
+        byte[] bytes = file.getBytes();
+
+        if (isPlainText(ext)) {
+            return decodeText(bytes);
+        }
+        if ("docx".equals(ext)) {
+            return extractDocx(bytes);
+        }
+        if ("xlsx".equals(ext)) {
+            return extractXlsx(bytes);
+        }
+        if ("xls".equals(ext)) {
+            return extractXls(bytes);
+        }
+        throw new IllegalArgumentException("鏆備笉鏀寔璇ユ枃浠剁被鍨�: " + ext);
+    }
+
+    private String extractDocx(byte[] bytes) throws IOException {
+        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+             XWPFDocument document = new XWPFDocument(inputStream);
+             XWPFWordExtractor extractor = new XWPFWordExtractor(document)) {
+            return extractor.getText();
+        }
+    }
+
+    private String extractXlsx(byte[] bytes) throws IOException {
+        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+             XSSFWorkbook workbook = new XSSFWorkbook(inputStream)) {
+            return extractWorkbook(workbook);
+        }
+    }
+
+    private String extractXls(byte[] bytes) throws IOException {
+        try (ByteArrayInputStream inputStream = new ByteArrayInputStream(bytes);
+             HSSFWorkbook workbook = new HSSFWorkbook(inputStream)) {
+            return extractWorkbook(workbook);
+        }
+    }
+
+    private String extractWorkbook(org.apache.poi.ss.usermodel.Workbook workbook) {
+        StringBuilder text = new StringBuilder();
+        DataFormatter formatter = new DataFormatter();
+        for (int i = 0; i < workbook.getNumberOfSheets(); i++) {
+            Sheet sheet = workbook.getSheetAt(i);
+            text.append("Sheet: ").append(sheet.getSheetName()).append("\n");
+            for (Row row : sheet) {
+                short lastCellNum = row.getLastCellNum();
+                if (lastCellNum <= 0) {
+                    text.append("\n");
+                    continue;
+                }
+                for (int c = 0; c < lastCellNum; c++) {
+                    String cellText = formatter.formatCellValue(row.getCell(c));
+                    text.append(cellText);
+                    if (c < lastCellNum - 1) {
+                        text.append('\t');
+                    }
+                }
+                text.append('\n');
+            }
+        }
+        return text.toString();
+    }
+
+    private String decodeText(byte[] bytes) {
+        String utf8 = new String(bytes, StandardCharsets.UTF_8);
+        if (utf8.contains("锟�")) {
+            return new String(bytes, java.nio.charset.Charset.forName("GBK"));
+        }
+        return utf8;
+    }
+
+    private String getExtension(String filename) {
+        if (!StringUtils.hasText(filename) || !filename.contains(".")) {
+            return "";
+        }
+        return filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
+    }
+
+    private boolean isPlainText(String ext) {
+        return StringUtils.inStringIgnoreCase(ext,
+                "txt", "md", "markdown", "json", "xml", "yaml", "yml", "csv", "log", "properties",
+                "java", "js", "ts", "vue", "html", "css", "sql", "py", "go", "sh", "bat");
+    }
+}
diff --git a/src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java b/src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
new file mode 100644
index 0000000..6d8c945
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/service/impl/AiChatSessionServiceImpl.java
@@ -0,0 +1,191 @@
+package com.ruoyi.ai.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.ai.dto.AiChatMessageDto;
+import com.ruoyi.ai.dto.AiChatSessionDto;
+import com.ruoyi.ai.mapper.AiChatSessionMapper;
+import com.ruoyi.ai.pojo.AiChatSession;
+import com.ruoyi.ai.service.AiChatSessionService;
+import com.ruoyi.ai.store.MongoChatMemoryStore;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.security.LoginUser;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.ToolExecutionResultMessage;
+import dev.langchain4j.data.message.UserMessage;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+
+import java.util.Date;
+import java.util.LinkedList;
+import java.util.List;
+import java.util.stream.Collectors;
+
+@Service
+@RequiredArgsConstructor
+public class AiChatSessionServiceImpl extends ServiceImpl<AiChatSessionMapper, AiChatSession> implements AiChatSessionService {
+
+    private static final int TITLE_MAX_LENGTH = 40;
+    private static final int LAST_MESSAGE_MAX_LENGTH = 300;
+
+    private final MongoChatMemoryStore mongoChatMemoryStore;
+
+    @Override
+    public void touchSession(String memoryId, LoginUser loginUser, String userMessage) {
+        if (!StringUtils.hasText(memoryId) || loginUser == null || loginUser.getUserId() == null) {
+            return;
+        }
+        Date now = new Date();
+        AiChatSession session = getSession(memoryId, loginUser.getUserId(), loginUser.getTenantId());
+        if (session == null) {
+            AiChatSession add = new AiChatSession();
+            add.setMemoryId(memoryId);
+            add.setUserId(loginUser.getUserId());
+            add.setTenantId(loginUser.getTenantId());
+            add.setTitle(buildTitle(userMessage));
+            add.setLastMessage(trimText(userMessage, LAST_MESSAGE_MAX_LENGTH));
+            add.setMessageCount(0);
+            add.setLastChatTime(now);
+            add.setCreateTime(now);
+            add.setUpdateTime(now);
+            save(add);
+            return;
+        }
+
+        AiChatSession update = new AiChatSession();
+        update.setId(session.getId());
+        if (!StringUtils.hasText(session.getTitle())) {
+            update.setTitle(buildTitle(userMessage));
+        }
+        update.setLastMessage(trimText(userMessage, LAST_MESSAGE_MAX_LENGTH));
+        update.setLastChatTime(now);
+        update.setUpdateTime(now);
+        updateById(update);
+    }
+
+    @Override
+    public void refreshSessionStats(String memoryId, LoginUser loginUser) {
+        if (!StringUtils.hasText(memoryId) || loginUser == null || loginUser.getUserId() == null) {
+            return;
+        }
+        AiChatSession session = getSession(memoryId, loginUser.getUserId(), loginUser.getTenantId());
+        if (session == null) {
+            return;
+        }
+        List<ChatMessage> messages = mongoChatMemoryStore.getMessages(memoryId);
+        AiChatSession update = new AiChatSession();
+        update.setId(session.getId());
+        update.setMessageCount(messages.size());
+        update.setLastMessage(trimText(lastMessageText(messages), LAST_MESSAGE_MAX_LENGTH));
+        update.setLastChatTime(new Date());
+        update.setUpdateTime(new Date());
+        updateById(update);
+    }
+
+    @Override
+    public List<AiChatSessionDto> listCurrentUserSessions(LoginUser loginUser) {
+        if (loginUser == null || loginUser.getUserId() == null) {
+            return new LinkedList<>();
+        }
+        LambdaQueryWrapper<AiChatSession> queryWrapper = new LambdaQueryWrapper<AiChatSession>()
+                .eq(AiChatSession::getUserId, loginUser.getUserId())
+                .orderByDesc(AiChatSession::getLastChatTime);
+        applyTenantCondition(queryWrapper, loginUser.getTenantId());
+        return list(queryWrapper).stream().map(item -> {
+            AiChatSessionDto dto = new AiChatSessionDto();
+            dto.setMemoryId(item.getMemoryId());
+            dto.setTitle(item.getTitle());
+            dto.setLastMessage(item.getLastMessage());
+            dto.setMessageCount(item.getMessageCount());
+            dto.setLastChatTime(item.getLastChatTime());
+            return dto;
+        }).collect(Collectors.toList());
+    }
+
+    @Override
+    public List<AiChatMessageDto> listCurrentUserMessages(String memoryId, LoginUser loginUser) {
+        if (!StringUtils.hasText(memoryId) || loginUser == null || loginUser.getUserId() == null) {
+            return new LinkedList<>();
+        }
+        AiChatSession session = getSession(memoryId, loginUser.getUserId(), loginUser.getTenantId());
+        if (session == null) {
+            return new LinkedList<>();
+        }
+        List<ChatMessage> messages = mongoChatMemoryStore.getMessages(memoryId);
+        return messages.stream().map(this::convertMessage).collect(Collectors.toList());
+    }
+
+    @Override
+    public boolean deleteCurrentUserSession(String memoryId, LoginUser loginUser) {
+        if (!StringUtils.hasText(memoryId) || loginUser == null || loginUser.getUserId() == null) {
+            return false;
+        }
+        LambdaQueryWrapper<AiChatSession> queryWrapper = new LambdaQueryWrapper<AiChatSession>()
+                .eq(AiChatSession::getMemoryId, memoryId)
+                .eq(AiChatSession::getUserId, loginUser.getUserId());
+        applyTenantCondition(queryWrapper, loginUser.getTenantId());
+        boolean removed = remove(queryWrapper);
+        mongoChatMemoryStore.deleteMessages(memoryId);
+        return removed;
+    }
+
+    private AiChatSession getSession(String memoryId, Long userId, Long tenantId) {
+        LambdaQueryWrapper<AiChatSession> queryWrapper = new LambdaQueryWrapper<AiChatSession>()
+                .eq(AiChatSession::getMemoryId, memoryId)
+                .eq(AiChatSession::getUserId, userId);
+        applyTenantCondition(queryWrapper, tenantId);
+        return getOne(queryWrapper, false);
+    }
+
+    private void applyTenantCondition(LambdaQueryWrapper<AiChatSession> queryWrapper, Long tenantId) {
+        if (tenantId == null) {
+            queryWrapper.isNull(AiChatSession::getTenantId);
+            return;
+        }
+        queryWrapper.eq(AiChatSession::getTenantId, tenantId);
+    }
+
+    private String buildTitle(String userMessage) {
+        if (!StringUtils.hasText(userMessage)) {
+            return "鏂颁細璇�";
+        }
+        return trimText(userMessage, TITLE_MAX_LENGTH);
+    }
+
+    private String trimText(String text, int maxLength) {
+        if (!StringUtils.hasText(text)) {
+            return "";
+        }
+        String source = text.trim();
+        if (source.length() <= maxLength) {
+            return source;
+        }
+        return source.substring(0, maxLength) + "...";
+    }
+
+    private String lastMessageText(List<ChatMessage> messages) {
+        if (messages == null || messages.isEmpty()) {
+            return "";
+        }
+        ChatMessage lastMessage = messages.get(messages.size() - 1);
+        return convertMessage(lastMessage).getContent();
+    }
+
+    private AiChatMessageDto convertMessage(ChatMessage message) {
+        if (message instanceof UserMessage userMessage) {
+            return new AiChatMessageDto("user", userMessage.singleText());
+        }
+        if (message instanceof AiMessage aiMessage) {
+            return new AiChatMessageDto("assistant", aiMessage.text());
+        }
+        if (message instanceof SystemMessage systemMessage) {
+            return new AiChatMessageDto("system", systemMessage.text());
+        }
+        if (message instanceof ToolExecutionResultMessage toolExecutionResultMessage) {
+            return new AiChatMessageDto("tool", toolExecutionResultMessage.text());
+        }
+        return new AiChatMessageDto("unknown", String.valueOf(message));
+    }
+}
diff --git a/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java b/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
index e94bf73..e88f0e9 100644
--- a/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
+++ b/src/main/java/com/ruoyi/ai/store/MongoChatMemoryStore.java
@@ -5,47 +5,58 @@
 import dev.langchain4j.data.message.ChatMessageDeserializer;
 import dev.langchain4j.data.message.ChatMessageSerializer;
 import dev.langchain4j.store.memory.chat.ChatMemoryStore;
-import org.springframework.beans.factory.annotation.Autowired;
+import lombok.RequiredArgsConstructor;
 import org.springframework.data.mongodb.core.MongoTemplate;
 import org.springframework.data.mongodb.core.query.Criteria;
 import org.springframework.data.mongodb.core.query.Query;
 import org.springframework.data.mongodb.core.query.Update;
 import org.springframework.stereotype.Component;
 
+import java.util.Date;
 import java.util.LinkedList;
 import java.util.List;
 
-/**
- * @author :yys
- * @date : 2025/5/2 19:18
- */
 @Component
+@RequiredArgsConstructor
 public class MongoChatMemoryStore implements ChatMemoryStore {
 
-    @Autowired
-    private MongoTemplate mongoTemplate;
+    private final MongoTemplate mongoTemplate;
 
     @Override
     public List<ChatMessage> getMessages(Object memoryId) {
-        Criteria criteria = Criteria.where("memoryId").is(memoryId);
-        Query query = new Query(criteria);
+        Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId)));
         ChatMessages chatMessages = mongoTemplate.findOne(query, ChatMessages.class);
-        if(chatMessages == null) return new LinkedList<>();
+        if (chatMessages == null || chatMessages.getContent() == null) {
+            return new LinkedList<>();
+        }
         return ChatMessageDeserializer.messagesFromJson(chatMessages.getContent());
     }
+
     @Override
     public void updateMessages(Object memoryId, List<ChatMessage> messages) {
-        Criteria criteria = Criteria.where("memoryId").is(memoryId);
-        Query query = new Query(criteria);
+        String memoryIdValue = memoryIdString(memoryId);
+        Query query = Query.query(Criteria.where("memoryId").is(memoryIdValue));
         Update update = new Update();
+        update.set("memoryId", memoryIdValue);
         update.set("content", ChatMessageSerializer.messagesToJson(messages));
-        //鏍规嵁query鏉′欢鑳芥煡璇㈠嚭鏂囨。锛屽垯淇敼鏂囨。锛涘惁鍒欐柊澧炴枃妗�
+        update.set("updateTime", new Date());
+        update.setOnInsert("createTime", new Date());
         mongoTemplate.upsert(query, update, ChatMessages.class);
     }
+
     @Override
     public void deleteMessages(Object memoryId) {
-        Criteria criteria = Criteria.where("memoryId").is(memoryId);
-        Query query = new Query(criteria);
+        Query query = Query.query(Criteria.where("memoryId").is(memoryIdString(memoryId)));
         mongoTemplate.remove(query, ChatMessages.class);
     }
+
+    public void appendMessages(Object memoryId, List<ChatMessage> appendList) {
+        List<ChatMessage> messages = new LinkedList<>(getMessages(memoryId));
+        messages.addAll(appendList);
+        updateMessages(memoryId, messages);
+    }
+
+    private String memoryIdString(Object memoryId) {
+        return memoryId == null ? "" : memoryId.toString();
+    }
 }
diff --git a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
index 431b848..8215ce8 100644
--- a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
+++ b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -2,25 +2,43 @@
 
 import com.alibaba.fastjson2.JSON;
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.ruoyi.ai.context.AiSessionUserContext;
 import com.ruoyi.approve.mapper.ApproveLogMapper;
 import com.ruoyi.approve.mapper.ApproveNodeMapper;
 import com.ruoyi.approve.mapper.ApproveProcessMapper;
 import com.ruoyi.approve.pojo.ApproveLog;
 import com.ruoyi.approve.pojo.ApproveNode;
 import com.ruoyi.approve.pojo.ApproveProcess;
+import com.ruoyi.approve.service.IApproveNodeService;
+import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.framework.security.LoginUser;
 import dev.langchain4j.agent.tool.P;
 import dev.langchain4j.agent.tool.Tool;
+import dev.langchain4j.agent.tool.ToolMemoryId;
 import org.springframework.stereotype.Component;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.StringUtils;
 
+import java.io.IOException;
 import java.math.BigDecimal;
 import java.text.ParseException;
 import java.text.SimpleDateFormat;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
+import java.time.YearMonth;
 import java.time.format.DateTimeFormatter;
-import java.util.*;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Comparator;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.StringJoiner;
+import java.util.regex.Matcher;
 import java.util.stream.Collectors;
 
 @Component
@@ -34,93 +52,103 @@
     private final ApproveProcessMapper approveProcessMapper;
     private final ApproveNodeMapper approveNodeMapper;
     private final ApproveLogMapper approveLogMapper;
+    private final IApproveNodeService approveNodeService;
+    private final ApproveProcessServiceImpl approveProcessService;
+    private final AiSessionUserContext aiSessionUserContext;
 
-    public ApproveTodoTools(
-            ApproveProcessMapper approveProcessMapper,
-            ApproveNodeMapper approveNodeMapper,
-            ApproveLogMapper approveLogMapper) {
+    public ApproveTodoTools(ApproveProcessMapper approveProcessMapper,
+                            ApproveNodeMapper approveNodeMapper,
+                            ApproveLogMapper approveLogMapper,
+                            IApproveNodeService approveNodeService,
+                            ApproveProcessServiceImpl approveProcessService,
+                            AiSessionUserContext aiSessionUserContext) {
         this.approveProcessMapper = approveProcessMapper;
         this.approveNodeMapper = approveNodeMapper;
         this.approveLogMapper = approveLogMapper;
+        this.approveNodeService = approveNodeService;
+        this.approveProcessService = approveProcessService;
+        this.aiSessionUserContext = aiSessionUserContext;
     }
 
-    @Tool(name = "鏌ヨ瀹℃壒寰呭姙鍒楄〃", value = "鏌ヨ瀹℃壒寰呭姙锛屾敮鎸佹寜鐘舵�併�佺被鍨嬨�佸叧閿瓧杩囨护锛岃繑鍥� Markdown 琛ㄦ牸銆�")
-    public String listTodos(
-            @P(value = "瀹℃壒鐘舵�侊紝鍙�夊�硷細all銆乸ending銆乸rocessing銆乤pproved銆乺ejected銆乺esubmitted", required = false) String status,
-            @P(value = "瀹℃壒绫诲瀷缂栧彿锛屽彲涓嶄紶", required = false) Integer approveType,
-            @P(value = "鍏抽敭瀛楋紝鍙尮閰嶆祦绋嬬紪鍙枫�佹爣棰樸�佺敵璇蜂汉銆佸綋鍓嶅鎵逛汉", required = false) String keyword,
-            @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit) {
+    @Tool(name = "鏌ヨ瀹℃壒寰呭姙鍒楄〃", value = "鏌ヨ褰撳墠鐧诲綍浜虹浉鍏崇殑瀹℃壒寰呭姙锛屼紭鍏堣繑鍥炶嚜宸卞緟澶勭悊鐨勫鎵癸紝鏀寔鎸夌姸鎬併�佺被鍨嬨�佸叧閿瓧杩囨护銆�")
+    public String listTodos(@ToolMemoryId String memoryId,
+                            @P(value = "瀹℃壒鐘舵�侊紝鍙�夊�硷細all銆乸ending銆乸rocessing銆乤pproved銆乺ejected銆乺esubmitted", required = false) String status,
+                            @P(value = "瀹℃壒绫诲瀷缂栧彿锛屽彲涓嶄紶", required = false) Integer approveType,
+                            @P(value = "鍏抽敭瀛楋紝鍙尮閰嶆祦绋嬬紪鍙枫�佹爣棰樸�佺敵璇蜂汉銆佸綋鍓嶅鎵逛汉", required = false) String keyword,
+                            @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit) {
+
+        LoginUser loginUser = currentLoginUser(memoryId);
+        Long userId = loginUser.getUserId();
+        Integer statusCode = parseStatus(status);
 
         LambdaQueryWrapper<ApproveProcess> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(ApproveProcess::getApproveDelete, 0);
+        wrapper.eq(ApproveProcess::getApproveDelete, 0)
+                .ne(ApproveProcess::getApproveStatus, 2);
 
-        Integer statusCode = parseStatus(status);
-        if (statusCode != null) {
-            wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
-        }
         if (approveType != null) {
             wrapper.eq(ApproveProcess::getApproveType, approveType);
         }
-        if (StringUtils.hasText(keyword)) {
-            wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
-                    .or().like(ApproveProcess::getApproveReason, keyword)
-                    .or().like(ApproveProcess::getApproveUserName, keyword)
-                    .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
+//        if (StringUtils.hasText(keyword)) {
+//            wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
+//                    .or().like(ApproveProcess::getApproveReason, keyword)
+//                    .or().like(ApproveProcess::getApproveUserName, keyword)
+//                    .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
+//        }
+        if (statusCode != null && (statusCode == 0 || statusCode == 1)) {
+            wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId);
+        } else {
+            wrapper.and(w -> w.eq(ApproveProcess::getApproveUser, userId)
+                    .or().eq(ApproveProcess::getApproveUserCurrentId, userId)
+                    .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId));
         }
 
-        wrapper.orderByDesc(ApproveProcess::getCreateTime);
-        wrapper.last("limit " + normalizeLimit(limit));
+        wrapper.orderByDesc(ApproveProcess::getCreateTime)
+                .last("limit " + normalizeLimit(limit));
 
-        List<ApproveProcess> processes = approveProcessMapper.selectList(wrapper);
-        if (processes == null || processes.isEmpty()) {
-            return jsonResponse(
-                    true,
-                    "todo_list",
-                    "鏈煡璇㈠埌绗﹀悎鏉′欢鐨勫鎵瑰緟鍔炪��",
+        List<ApproveProcess> processes = defaultList(approveProcessMapper.selectList(wrapper));
+        if (processes.isEmpty()) {
+            return jsonResponse(true, "todo_list", "鏈煡璇㈠埌褰撳墠鐢ㄦ埛绗﹀悎鏉′欢鐨勫鎵瑰緟鍔炪��",
                     Map.of("count", 0),
-                    Map.of(
-                            "columns", List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName", "approveReason", "approveStatus", "createTime"),
-                            "items", List.of()
-                    ),
-                    Map.of()
-            );
+                    Map.of("columns", todoColumns(), "items", List.of()),
+                    Map.of());
         }
 
-        List<Map<String, Object>> items = processes.stream().map(process -> {
-            Map<String, Object> item = new LinkedHashMap<>();
-            item.put("approveId", process.getApproveId());
-            item.put("approveType", approveTypeName(process.getApproveType()));
-            item.put("approveUserName", process.getApproveUserName());
-            item.put("approveUserCurrentName", process.getApproveUserCurrentName());
-            item.put("approveReason", process.getApproveReason());
-            item.put("approveStatus", approveStatusName(process.getApproveStatus()));
-            item.put("createTime", formatDateTime(process.getCreateTime()));
-            return item;
-        }).collect(Collectors.toList());
+        List<Map<String, Object>> items = processes.stream()
+                .filter(process -> canView(process, userId))
+                .sorted(Comparator
+                        .comparing((ApproveProcess process) -> !Objects.equals(process.getApproveUserCurrentId(), userId))
+                        .thenComparing(ApproveProcess::getCreateTime, Comparator.nullsLast(Comparator.reverseOrder())))
+                .map(process -> {
+                    Map<String, Object> item = new LinkedHashMap<>();
+                    item.put("approveId", process.getApproveId());
+                    item.put("approveType", approveTypeName(process.getApproveType()));
+                    item.put("approveUserName", safe(process.getApproveUserName()));
+                    item.put("approveUserCurrentName", safe(process.getApproveUserCurrentName()));
+                    item.put("approveReason", safe(process.getApproveReason()));
+                    item.put("approveStatus", approveStatusName(process.getApproveStatus()));
+                    item.put("createTime", formatDateTime(process.getCreateTime()));
+                    item.put("relation", relationName(process, userId));
+                    return item;
+                })
+                .collect(Collectors.toList());
 
-        return jsonResponse(
-                true,
-                "todo_list",
-                "宸茶繑鍥炲鎵瑰緟鍔炲垪琛紝鍙洿鎺ユ覆鏌撹〃鏍兼垨鍗$墖銆�",
+        return jsonResponse(true, "todo_list", "宸茶繑鍥炲綋鍓嶇敤鎴风浉鍏冲鎵瑰垪琛ㄣ��",
                 Map.of(
                         "count", items.size(),
-                        "statusFilter", status == null ? "all" : status,
+                        "statusFilter", StringUtils.hasText(status) ? status : "all",
                         "approveType", approveType == null ? "" : approveType,
                         "keyword", keyword == null ? "" : keyword
                 ),
-                Map.of(
-                        "columns", List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName", "approveReason", "approveStatus", "createTime"),
-                        "items", items
-                ),
-                Map.of()
-        );
+                Map.of("columns", todoColumns(), "items", items),
+                Map.of());
     }
 
-    @Tool(name = "鏌ヨ瀹℃壒寰呭姙璇︽儏", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ鍗曟潯瀹℃壒寰呭姙璇︽儏锛岃繑鍥炵粨鏋勫寲鏂囨湰銆�")
-    public String getTodoDetail(@P("娴佺▼缂栧彿 approveId") String approveId) {
-        ApproveProcess process = getProcessByApproveId(approveId);
+    @Tool(name = "鏌ヨ瀹℃壒寰呭姙璇︽儏", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ褰撳墠鐧诲綍浜哄彲瑙佺殑瀹℃壒璇︽儏銆�")
+    public String getTodoDetail(@ToolMemoryId String memoryId,
+                                @P("娴佺▼缂栧彿 approveId") String approveId) {
+        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
         if (process == null) {
-            return "鏈壘鍒板搴旂殑瀹℃壒娴佺▼锛岃纭娴佺▼缂栧彿鏄惁姝g‘銆�";
+            return "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冩煡鐪嬭娴佺▼銆�";
         }
 
         StringJoiner detail = new StringJoiner("\n");
@@ -139,255 +167,231 @@
         detail.add("閲戦: " + (process.getPrice() == null ? "" : process.getPrice().toPlainString()));
         detail.add("澶囨敞: " + safe(process.getApproveRemark()));
         detail.add("鍒涘缓鏃堕棿: " + formatDateTime(process.getCreateTime()));
+        detail.add("涓庡綋鍓嶇敤鎴峰叧绯�: " + relationName(process, currentUserId(memoryId)));
         return detail.toString();
     }
 
-    @Tool(name = "鏌ヨ瀹℃壒娴佽浆璁板綍", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ瀹℃壒鑺傜偣鍜屽鎵规棩蹇楋紝閫傚悎鍥炵瓟杩涘害銆佸崱鍦ㄥ摢涓妭鐐广�佽皝澶勭悊杩囥��")
-    public String getTodoProgress(@P("娴佺▼缂栧彿 approveId") String approveId) {
-        ApproveProcess process = getProcessByApproveId(approveId);
+    @Tool(name = "鏌ヨ瀹℃壒娴佽浆璁板綍", value = "鏍规嵁娴佺▼缂栧彿鏌ヨ瀹℃壒鑺傜偣鍜屽鎵规棩蹇楋紝鐢ㄤ簬鍥炵瓟杩涘害銆佸綋鍓嶅崱鐐瑰拰鍘嗗彶澶勭悊璁板綍銆�")
+    public String getTodoProgress(@ToolMemoryId String memoryId,
+                                  @P("娴佺▼缂栧彿 approveId") String approveId) {
+        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
         if (process == null) {
-            return jsonResponse(
-                    false,
-                    "todo_progress",
-                    "鏈壘鍒板搴旂殑瀹℃壒娴佺▼锛岃纭娴佺▼缂栧彿鏄惁姝g‘銆�",
-                    Map.of("approveId", approveId == null ? "" : approveId),
+            return jsonResponse(false, "todo_progress", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冩煡鐪嬭娴佺▼銆�",
+                    Map.of("approveId", safe(approveId)),
                     Map.of(),
-                    Map.of()
-            );
+                    Map.of());
         }
 
         List<ApproveNode> nodes = listNodes(process);
         List<ApproveLog> logs = listLogs(process.getId());
+        ApproveNode currentNode = findCurrentNode(nodes);
+
         List<Map<String, Object>> nodeItems = nodes.stream().map(node -> {
             Map<String, Object> item = new LinkedHashMap<>();
             item.put("approveNodeOrder", node.getApproveNodeOrder());
-            item.put("approveNodeUser", node.getApproveNodeUser());
+            item.put("approveNodeUser", safe(node.getApproveNodeUser()));
+            item.put("approveNodeUserId", node.getApproveNodeUserId());
             item.put("approveNodeStatus", approveNodeStatusName(node.getApproveNodeStatus()));
             item.put("approveNodeTime", formatDate(node.getApproveNodeTime()));
-            item.put("approveNodeReason", node.getApproveNodeReason());
-            item.put("approveNodeRemark", node.getApproveNodeRemark());
+            item.put("approveNodeReason", safe(node.getApproveNodeReason()));
+            item.put("approveNodeRemark", safe(node.getApproveNodeRemark()));
+            item.put("isCurrent", currentNode != null && Objects.equals(currentNode.getId(), node.getId()));
             return item;
         }).collect(Collectors.toList());
+
         List<Map<String, Object>> logItems = logs.stream().map(log -> {
             Map<String, Object> item = new LinkedHashMap<>();
             item.put("approveNodeOrder", log.getApproveNodeOrder());
             item.put("approveUser", log.getApproveUser());
             item.put("approveStatus", approveStatusName(log.getApproveStatus()));
             item.put("approveTime", formatDate(log.getApproveTime()));
-            item.put("approveRemark", log.getApproveRemark());
+            item.put("approveRemark", safe(log.getApproveRemark()));
             return item;
         }).collect(Collectors.toList());
-        return jsonResponse(
-                true,
-                "todo_progress",
-                "宸茶繑鍥炲鎵规祦杞褰曪紝鍙覆鏌撴椂闂寸嚎銆佹楠ゆ潯鍜屾棩蹇楄〃鏍笺��",
+
+        return jsonResponse(true, "todo_progress", "宸茶繑鍥炲鎵规祦杞褰曘��",
                 Map.of(
-                        "approveId", process.getApproveId(),
+                        "approveId", safe(process.getApproveId()),
                         "currentStatus", approveStatusName(process.getApproveStatus()),
                         "currentApprover", safe(process.getApproveUserCurrentName()),
+                        "currentNodeOrder", currentNode == null ? "" : currentNode.getApproveNodeOrder(),
                         "nodeCount", nodeItems.size(),
                         "logCount", logItems.size()
                 ),
-                Map.of(
-                        "nodes", nodeItems,
-                        "logs", logItems
-                ),
-                Map.of()
-        );
+                Map.of("nodes", nodeItems, "logs", logItems),
+                Map.of());
     }
 
-    @Tool(name = "缁熻瀹℃壒寰呭姙鏁版嵁", value = "杩斿洖涓撲笟鍖栫粺璁$粨鏋滐紝鍖呭惈鎽樿鎸囨爣鍜屽彲鐩存帴鐢ㄤ簬 ECharts 鐨勫浘琛� option銆�")
-    public String getTodoStats() {
-        List<ApproveProcess> processes = approveProcessMapper.selectList(new LambdaQueryWrapper<ApproveProcess>()
-                .eq(ApproveProcess::getApproveDelete, 0));
-        if (processes == null || processes.isEmpty()) {
-            return jsonResponse(
-                    true,
-                    "todo_stats",
-                    "褰撳墠娌℃湁瀹℃壒寰呭姙鏁版嵁銆�",
-                    Map.of("total", 0),
+    @Tool(name = "缁熻瀹℃壒寰呭姙鏁版嵁", value = "鎸夌敤鎴锋寚瀹氱殑鏃堕棿鑼冨洿缁熻褰撳墠鐧诲綍浜虹浉鍏冲鎵圭殑鐘舵�佸垎甯冦�佺被鍨嬪垎甯冨拰瓒嬪娍锛涙湭鎸囧畾鏃堕粯璁よ繎7澶┿��")
+    public String getTodoStats(@ToolMemoryId String memoryId,
+                               @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
+                               @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
+                               @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡� 浠婂ぉ銆佹湰鏈堛�佽繎30澶┿��2026-04-01鍒�2026-04-27", required = false) String timeRange) {
+        Long userId = currentUserId(memoryId);
+        List<ApproveProcess> processes = defaultList(approveProcessMapper.selectList(new LambdaQueryWrapper<ApproveProcess>()
+                .eq(ApproveProcess::getApproveDelete, 0)
+                .and(w -> w.eq(ApproveProcess::getApproveUser, userId)
+                        .or().eq(ApproveProcess::getApproveUserCurrentId, userId)
+                        .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId))));
+
+        DateRange dateRange = resolveDateRange(startDate, endDate, timeRange);
+        List<ApproveProcess> filteredProcesses = processes.stream()
+                .filter(process -> withinDateRange(process.getCreateTime(), dateRange))
+                .collect(Collectors.toList());
+
+        if (filteredProcesses.isEmpty()) {
+            return jsonResponse(true, "todo_stats", "褰撳墠鐢ㄦ埛娌℃湁鐩稿叧瀹℃壒鏁版嵁銆�",
+                    Map.of(
+                            "total", 0,
+                            "startDate", dateRange.start().toString(),
+                            "endDate", dateRange.end().toString(),
+                            "timeRange", dateRange.label()
+                    ),
                     Map.of(
                             "statusDistribution", Map.of(),
                             "typeDistribution", Map.of(),
-                            "recent7DayTrend", List.of(),
-                            "tips", List.of()
+                            "trend", List.of()
                     ),
-                    Map.of()
-            );
+                    Map.of());
         }
 
-        Map<String, Long> statusStats = processes.stream()
+        Map<String, Long> statusStats = filteredProcesses.stream()
                 .collect(Collectors.groupingBy(p -> approveStatusName(p.getApproveStatus()), LinkedHashMap::new, Collectors.counting()));
-        Map<String, Long> typeStats = processes.stream()
+        Map<String, Long> typeStats = filteredProcesses.stream()
                 .collect(Collectors.groupingBy(p -> approveTypeName(p.getApproveType()), LinkedHashMap::new, Collectors.counting()));
 
-        long pendingCount = countByStatus(processes, 0);
-        long processingCount = countByStatus(processes, 1);
-        long approvedCount = countByStatus(processes, 2);
-        long rejectedCount = countByStatus(processes, 3);
-        long resubmittedCount = countByStatus(processes, 4);
+        long pendingCount = countByStatus(filteredProcesses, 0);
+        long processingCount = countByStatus(filteredProcesses, 1);
+        long approvedCount = countByStatus(filteredProcesses, 2);
+        long rejectedCount = countByStatus(filteredProcesses, 3);
+        long resubmittedCount = countByStatus(filteredProcesses, 4);
 
-        List<String> recentDates = buildRecentDates(7);
-        List<Long> trendValues = recentDates.stream()
-                .map(date -> processes.stream()
-                        .filter(process -> process.getCreateTime() != null)
-                        .filter(process -> process.getCreateTime().toLocalDate().equals(LocalDate.parse(date)))
-                        .count())
-                .collect(Collectors.toList());
+        TrendRange trendRange = buildTrendRange(dateRange.start(), dateRange.end(), filteredProcesses);
 
         Map<String, Object> summary = new LinkedHashMap<>();
-        summary.put("total", processes.size());
+        summary.put("total", filteredProcesses.size());
         summary.put("pending", pendingCount);
         summary.put("processing", processingCount);
         summary.put("approved", approvedCount);
         summary.put("rejected", rejectedCount);
         summary.put("resubmitted", resubmittedCount);
-        summary.put("approvalCompletionRate", calculateRate(approvedCount, processes.size()));
-        summary.put("rejectionRate", calculateRate(rejectedCount, processes.size()));
+        summary.put("approvalCompletionRate", calculateRate(approvedCount, filteredProcesses.size()));
+        summary.put("rejectionRate", calculateRate(rejectedCount, filteredProcesses.size()));
+        summary.put("startDate", dateRange.start().toString());
+        summary.put("endDate", dateRange.end().toString());
+        summary.put("timeRange", dateRange.label());
+        summary.put("trendGranularity", trendRange.granularity());
 
         Map<String, Object> charts = new LinkedHashMap<>();
         charts.put("statusBarOption", buildStatusBarOption(statusStats));
         charts.put("typePieOption", buildTypePieOption(typeStats));
-        charts.put("recentTrendLineOption", buildRecentTrendLineOption(recentDates, trendValues));
+        charts.put("trendLineOption", buildTrendLineOption(trendRange.labels(), trendRange.values(), trendRange.label()));
 
-        return jsonResponse(
-                true,
-                "todo_stats",
-                "宸茶繑鍥炲鎵圭粺璁℃瑙堜笌鍥捐〃閰嶇疆锛屽墠绔彲鐩存帴浣跨敤 charts 涓殑 ECharts option 娓叉煋銆�",
+        return jsonResponse(true, "todo_stats", "宸茶繑鍥炲綋鍓嶇敤鎴风浉鍏冲鎵圭粺璁°��",
                 summary,
                 Map.of(
                         "statusDistribution", statusStats,
                         "typeDistribution", typeStats,
-                        "recent7DayTrend", toTrendItems(recentDates, trendValues),
-                        "tips", List.of(
-                                "statusBarOption 閫傚悎灞曠ず鍚勫鎵圭姸鎬佹暟閲忓姣�",
-                                "typePieOption 閫傚悎灞曠ず鍚勫鎵圭被鍨嬪崰姣�",
-                                "recentTrendLineOption 閫傚悎灞曠ず鏈�杩�7澶╂柊澧炲鎵硅秼鍔�"
-                        )
+                        "trend", toTrendItems(trendRange.labels(), trendRange.values())
                 ),
-                charts
-        );
+                charts);
     }
 
-    @Transactional
-    @Tool(name = "瀹℃壒寰呭姙", value = "鎵ц瀹℃壒鍔ㄤ綔銆俛ction 鍙敮鎸� approve 鎴� reject銆俛pprove 琛ㄧず閫氳繃锛宺eject 琛ㄧず椹冲洖銆�")
-    public String reviewTodo(
-            @P("娴佺▼缂栧彿 approveId") String approveId,
-            @P("鍔ㄤ綔锛宎pprove=閫氳繃锛宺eject=椹冲洖") String action,
-            @P(value = "瀹℃壒澶囨敞锛屽彲涓嶄紶", required = false) String remark) {
-        ApproveProcess process = getProcessByApproveId(approveId);
+    @Transactional(rollbackFor = Exception.class)
+    @Tool(name = "瀹℃壒寰呭姙", value = "鎵ц瀹℃壒鍔ㄤ綔锛宎ction 浠呮敮鎸� approve 鎴� reject锛屼笖鍙兘澶勭悊褰撳墠鐧诲綍浜鸿嚜宸辩殑寰呭鑺傜偣銆�")
+    public String reviewTodo(@ToolMemoryId String memoryId,
+                             @P("娴佺▼缂栧彿 approveId") String approveId,
+                             @P("鍔ㄤ綔锛宎pprove=閫氳繃锛宺eject=椹冲洖") String action,
+                             @P(value = "瀹℃壒澶囨敞锛屽彲涓嶄紶", required = false) String remark) {
+
+        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
         if (process == null) {
-            return actionResult(false, "review_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+            return actionResult(false, "review_action", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冭闂娴佺▼銆�", approveId, null);
         }
-        if (process.getApproveDelete() != null && process.getApproveDelete() == 1) {
-            return actionResult(false, "review_action", "璇ュ鎵规祦绋嬪凡鍒犻櫎锛屼笉鑳藉啀瀹℃牳銆�", approveId, null);
+        if (!canOperate(process, currentUserId(memoryId))) {
+            return actionResult(false, "review_action", "褰撳墠鐧诲綍浜轰笉鏄瀹℃壒鐨勫綋鍓嶅鐞嗕汉銆�", approveId, null);
         }
         if (process.getApproveStatus() != null && (process.getApproveStatus() == 2 || process.getApproveStatus() == 3)) {
-            return actionResult(false, "review_action", "璇ュ鎵规祦绋嬪凡缁撴潫锛屼笉鑳介噸澶嶅鏍搞��", approveId, null);
+            return actionResult(false, "review_action", "璇ュ鎵瑰凡缁撴潫锛屼笉鑳介噸澶嶅鐞嗐��", approveId, null);
         }
 
         List<ApproveNode> nodes = listNodes(process);
         ApproveNode currentNode = findCurrentNode(nodes);
-        if (currentNode == null) {
-            return actionResult(false, "review_action", "鏈壘鍒板彲瀹℃牳鐨勫綋鍓嶈妭鐐广��", approveId, null);
+        if (currentNode == null || !Objects.equals(currentNode.getApproveNodeUserId(), currentUserId(memoryId))) {
+            return actionResult(false, "review_action", "鏈壘鍒板綋鍓嶇敤鎴峰彲澶勭悊鐨勫鎵硅妭鐐广��", approveId, null);
         }
 
         String normalizedAction = action == null ? "" : action.trim().toLowerCase();
-        Date now = new Date();
-        currentNode.setApproveNodeTime(now);
+        currentNode.setApproveNodeRemark(remark);
+        currentNode.setApproveNodeReason("reject".equals(normalizedAction) ? remark : null);
+        currentNode.setUpdateUser(currentUserId(memoryId));
         currentNode.setUpdateTime(LocalDateTime.now());
+        currentNode.setIsLast(isLastNode(nodes, currentNode));
 
-        ApproveLog log = new ApproveLog();
-        log.setApproveId(process.getId());
-        log.setApproveNodeOrder(currentNode.getApproveNodeOrder());
-        log.setApproveUser(currentNode.getApproveNodeUserId());
-        log.setApproveTime(now);
-        log.setApproveRemark(remark);
-
-        if ("approve".equals(normalizedAction)) {
-            currentNode.setApproveNodeStatus(1);
-            currentNode.setApproveNodeReason(null);
-            approveNodeMapper.updateById(currentNode);
-
-            ApproveNode nextNode = findNextNode(nodes, currentNode.getApproveNodeOrder());
-            if (nextNode == null) {
-                process.setApproveStatus(2);
-                process.setApproveOverTime(now);
-                process.setApproveUserCurrentId(null);
-                process.setApproveUserCurrentName(null);
-            } else {
-                process.setApproveStatus(1);
-                process.setApproveUserCurrentId(nextNode.getApproveNodeUserId());
-                process.setApproveUserCurrentName(nextNode.getApproveNodeUser());
+        try {
+            switch (normalizedAction) {
+                case "approve" -> currentNode.setApproveNodeStatus(1);
+                case "reject" -> currentNode.setApproveNodeStatus(2);
+                default -> {
+                    return actionResult(false, "review_action", "action 鍙敮鎸� approve 鎴� reject銆�", approveId, null);
+                }
             }
-            process.setApproveRemark(remark);
-            approveProcessMapper.updateById(process);
-
-            log.setApproveStatus(nextNode == null ? 2 : 1);
-            approveLogMapper.insert(log);
-            return actionResult(true, "review_action",
-                    nextNode == null ? "瀹℃壒宸查�氳繃锛屼笖璇ユ祦绋嬪凡鍏ㄩ儴瀹屾垚銆�" : "瀹℃壒宸查�氳繃锛屾祦绋嬪凡娴佽浆鍒颁笅涓�瀹℃壒鑺傜偣銆�",
-                    approveId,
-                    Map.of(
-                            "action", "approve",
-                            "currentStatus", approveStatusName(process.getApproveStatus()),
-                            "nextApprover", nextNode == null ? "" : safe(nextNode.getApproveNodeUser()),
-                            "remark", safe(remark)
-                    ));
+            approveNodeService.updateApproveNode(currentNode);
+        } catch (IOException e) {
+            throw new RuntimeException("瀹℃壒澶勭悊澶辫触", e);
         }
 
-        if ("reject".equals(normalizedAction)) {
-            currentNode.setApproveNodeStatus(2);
-            currentNode.setApproveNodeReason(remark);
-            currentNode.setApproveNodeRemark(remark);
-            approveNodeMapper.updateById(currentNode);
+        ApproveProcess refreshed = getProcessByApproveId(approveId);
+        writeApproveLog(memoryId, refreshed, currentNode, remark);
+        ApproveNode nextNode = refreshed == null ? null : findCurrentNode(listNodes(refreshed));
 
-            process.setApproveStatus(3);
-            process.setApproveOverTime(now);
-            process.setApproveUserCurrentId(null);
-            process.setApproveUserCurrentName(null);
-            process.setApproveRemark(remark);
-            approveProcessMapper.updateById(process);
-
-            log.setApproveStatus(3);
-            approveLogMapper.insert(log);
-            return actionResult(true, "review_action", "瀹℃壒宸查┏鍥炪��", approveId, Map.of(
-                    "action", "reject",
-                    "currentStatus", approveStatusName(process.getApproveStatus()),
-                    "remark", safe(remark)
-            ));
-        }
-
-        return actionResult(false, "review_action", "action 鍙敮鎸� approve 鎴� reject銆�", approveId, null);
+        return actionResult(true, "review_action",
+                "approve".equals(normalizedAction) ? "瀹℃壒宸查�氳繃銆�" : "瀹℃壒宸查┏鍥炪��",
+                approveId,
+                Map.of(
+                        "action", normalizedAction,
+                        "currentStatus", refreshed == null ? "" : approveStatusName(refreshed.getApproveStatus()),
+                        "nextApprover", nextNode == null ? "" : safe(nextNode.getApproveNodeUser()),
+                        "remark", safe(remark)
+                ));
     }
 
-    @Transactional
-    @Tool(name = "鍙栨秷瀹℃壒寰呭姙瀹℃牳", value = "鎾ら攢鏈�杩戜竴娆″鏍哥粨鏋滐紝灏嗘渶杩戝鏍歌妭鐐规仮澶嶄负鏈鏍革紝骞跺洖婊氭祦绋嬬姸鎬併��")
-    public String cancelReviewTodo(
-            @P("娴佺▼缂栧彿 approveId") String approveId,
-            @P(value = "鍙栨秷鍘熷洜锛屽彲涓嶄紶", required = false) String reason) {
-        ApproveProcess process = getProcessByApproveId(approveId);
+    @Transactional(rollbackFor = Exception.class)
+    @Tool(name = "鍙栨秷瀹℃壒寰呭姙瀹℃牳", value = "鎾ら攢鏈�杩戜竴娆″鏍哥粨鏋滐紝浠呭厑璁告渶杩戜竴娆″鏍镐汉鎴栫敵璇蜂汉鎿嶄綔銆�")
+    public String cancelReviewTodo(@ToolMemoryId String memoryId,
+                                   @P("娴佺▼缂栧彿 approveId") String approveId,
+                                   @P(value = "鍙栨秷鍘熷洜锛屽彲涓嶄紶", required = false) String reason) {
+
+        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
         if (process == null) {
-            return actionResult(false, "cancel_review_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+            return actionResult(false, "cancel_review_action", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冭闂娴佺▼銆�", approveId, null);
         }
+
         List<ApproveNode> nodes = listNodes(process);
         ApproveNode lastReviewedNode = nodes.stream()
                 .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() != 0)
                 .max(Comparator.comparing(ApproveNode::getApproveNodeOrder))
                 .orElse(null);
         if (lastReviewedNode == null) {
-            return actionResult(false, "cancel_review_action", "褰撳墠娴佺▼娌℃湁鍙彇娑堢殑瀹℃牳璁板綍銆�", approveId, null);
+            return actionResult(false, "cancel_review_action", "褰撳墠娴佺▼娌℃湁鍙挙閿�鐨勫鏍歌褰曘��", approveId, null);
+        }
+
+        Long userId = currentUserId(memoryId);
+        if (!isAdmin(userId)
+                && !Objects.equals(process.getApproveUser(), userId)
+                && !Objects.equals(lastReviewedNode.getApproveNodeUserId(), userId)) {
+            return actionResult(false, "cancel_review_action", "鍙湁鐢宠浜恒�佹渶杩戜竴娆″鏍镐汉鎴栫鐞嗗憳鍙互鎾ら攢銆�", approveId, null);
         }
 
         lastReviewedNode.setApproveNodeStatus(0);
         lastReviewedNode.setApproveNodeTime(null);
         lastReviewedNode.setApproveNodeReason(null);
         lastReviewedNode.setApproveNodeRemark(reason);
+        lastReviewedNode.setUpdateUser(userId);
         lastReviewedNode.setUpdateTime(LocalDateTime.now());
         approveNodeMapper.updateById(lastReviewedNode);
 
-        List<ApproveLog> logs = listLogs(process.getId());
-        ApproveLog latestLog = logs.stream()
+        ApproveLog latestLog = listLogs(process.getId()).stream()
                 .max(Comparator.comparing(ApproveLog::getApproveNodeOrder)
                         .thenComparing(ApproveLog::getApproveTime, Comparator.nullsLast(Date::compareTo)))
                 .orElse(null);
@@ -395,20 +399,14 @@
             approveLogMapper.deleteById(latestLog.getId());
         }
 
-        ApproveNode previousReviewedNode = nodes.stream()
-                .filter(node -> !node.getId().equals(lastReviewedNode.getId()))
-                .filter(node -> node.getApproveNodeStatus() != null && node.getApproveNodeStatus() == 1)
-                .max(Comparator.comparing(ApproveNode::getApproveNodeOrder))
-                .orElse(null);
-
         process.setApproveOverTime(null);
         process.setApproveRemark(reason);
-        process.setApproveStatus(previousReviewedNode == null ? 0 : 1);
+        process.setApproveStatus(lastReviewedNode.getApproveNodeOrder() == null || lastReviewedNode.getApproveNodeOrder() <= 1 ? 0 : 1);
         process.setApproveUserCurrentId(lastReviewedNode.getApproveNodeUserId());
         process.setApproveUserCurrentName(lastReviewedNode.getApproveNodeUser());
         approveProcessMapper.updateById(process);
 
-        return actionResult(true, "cancel_review_action", "鏈�杩戜竴娆″鏍稿凡鍙栨秷锛屾祦绋嬪凡鍥為��鍒板搴斿鎵硅妭鐐广��", approveId, Map.of(
+        return actionResult(true, "cancel_review_action", "鏈�杩戜竴娆″鏍稿凡鎾ら攢銆�", approveId, Map.of(
                 "rollbackNodeOrder", lastReviewedNode.getApproveNodeOrder(),
                 "currentStatus", approveStatusName(process.getApproveStatus()),
                 "currentApprover", safe(process.getApproveUserCurrentName()),
@@ -416,27 +414,36 @@
         ));
     }
 
-    @Transactional
-    @Tool(name = "淇敼瀹℃壒寰呭姙", value = "淇敼瀹℃壒鍗曞熀纭�淇℃伅銆傛敮鎸佷慨鏀规爣棰樸�佸紑濮嬫棩鏈熴�佺粨鏉熸棩鏈熴�侀噾棰濄�佸湴鐐广�佺被鍨嬪拰澶囨敞銆傛棩鏈熸牸寮忓繀椤绘槸 yyyy-MM-dd銆�")
-    public String updateTodo(
-            @P("娴佺▼缂栧彿 approveId") String approveId,
-            @P(value = "鏂扮殑鏍囬锛屽彲涓嶄紶", required = false) String approveReason,
-            @P(value = "鏂扮殑寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
-            @P(value = "鏂扮殑缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
-            @P(value = "鏂扮殑閲戦锛屽彲涓嶄紶", required = false) BigDecimal price,
-            @P(value = "鏂扮殑鍦扮偣锛屽彲涓嶄紶", required = false) String location,
-            @P(value = "鏂扮殑瀹℃壒绫诲瀷锛屽彲涓嶄紶", required = false) Integer approveType,
-            @P(value = "鏂扮殑澶囨敞锛屽彲涓嶄紶", required = false) String approveRemark) {
-        ApproveProcess process = getProcessByApproveId(approveId);
+    @Transactional(rollbackFor = Exception.class)
+    @Tool(name = "淇敼瀹℃壒寰呭姙", value = "淇敼瀹℃壒鍗曞熀纭�淇℃伅锛屼粎鍏佽鐢宠浜轰慨鏀癸紱涓嶆敮鎸侀�氳繃 AI 鍙樻洿瀹℃壒绫诲瀷銆�")
+    public String updateTodo(@ToolMemoryId String memoryId,
+                             @P("娴佺▼缂栧彿 approveId") String approveId,
+                             @P(value = "鏂扮殑鏍囬锛屽彲涓嶄紶", required = false) String approveReason,
+                             @P(value = "鏂扮殑寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
+                             @P(value = "鏂扮殑缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
+                             @P(value = "鏂扮殑閲戦锛屽彲涓嶄紶", required = false) BigDecimal price,
+                             @P(value = "鏂扮殑鍦扮偣锛屽彲涓嶄紶", required = false) String location,
+                             @P(value = "鏂扮殑瀹℃壒绫诲瀷锛屽彲涓嶄紶", required = false) Integer approveType,
+                             @P(value = "鏂扮殑澶囨敞锛屽彲涓嶄紶", required = false) String approveRemark) {
+
+        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
         if (process == null) {
-            return actionResult(false, "update_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+            return actionResult(false, "update_action", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冭闂娴佺▼銆�", approveId, null);
+        }
+        if (!Objects.equals(process.getApproveUser(), currentUserId(memoryId)) && !isAdmin(currentUserId(memoryId))) {
+            return actionResult(false, "update_action", "鍙湁鐢宠浜烘垨绠$悊鍛樺彲浠ヤ慨鏀瑰鎵瑰崟銆�", approveId, null);
+        }
+        if (process.getApproveStatus() != null && (process.getApproveStatus() == 1 || process.getApproveStatus() == 2)) {
+            return actionResult(false, "update_action", "瀹℃壒澶勭悊涓垨宸插畬鎴愭椂锛屼笉鍏佽閫氳繃 AI 淇敼銆�", approveId, null);
+        }
+        if (approveType != null && !Objects.equals(approveType, process.getApproveType())) {
+            return actionResult(false, "update_action", "AI 鍔╂墜鏆備笉鏀寔鐩存帴鍙樻洿瀹℃壒绫诲瀷锛岄伩鍏嶈妭鐐归厤缃け鐪熴��", approveId, null);
         }
         if (!StringUtils.hasText(approveReason)
                 && !StringUtils.hasText(startDate)
                 && !StringUtils.hasText(endDate)
                 && price == null
                 && !StringUtils.hasText(location)
-                && approveType == null
                 && !StringUtils.hasText(approveRemark)) {
             return actionResult(false, "update_action", "娌℃湁妫�娴嬪埌鍙洿鏂扮殑瀛楁銆�", approveId, null);
         }
@@ -456,9 +463,6 @@
         if (StringUtils.hasText(location)) {
             process.setLocation(location);
         }
-        if (approveType != null) {
-            process.setApproveType(approveType);
-        }
         if (StringUtils.hasText(approveRemark)) {
             process.setApproveRemark(approveRemark);
         }
@@ -475,30 +479,34 @@
         ));
     }
 
-    @Transactional
-    @Tool(name = "鍒犻櫎瀹℃壒寰呭姙", value = "鍒犻櫎瀹℃壒娴佺▼銆傞�昏緫鍒犻櫎娴佺▼璁板綍锛屽苟鍚屾閫昏緫鍒犻櫎瀹℃壒鑺傜偣銆�")
-    public String deleteTodo(@P("娴佺▼缂栧彿 approveId") String approveId) {
-        ApproveProcess process = getProcessByApproveId(approveId);
+    @Transactional(rollbackFor = Exception.class)
+    @Tool(name = "鍒犻櫎瀹℃壒寰呭姙", value = "鍒犻櫎瀹℃壒娴佺▼锛屼粎鍏佽鐢宠浜哄垹闄ゆ湭瀹屾垚鐨勬祦绋嬨��")
+    public String deleteTodo(@ToolMemoryId String memoryId,
+                             @P("娴佺▼缂栧彿 approveId") String approveId) {
+        ApproveProcess process = getAccessibleProcess(memoryId, approveId);
         if (process == null) {
-            return actionResult(false, "delete_action", "鏈壘鍒板搴斿鎵规祦绋嬨��", approveId, null);
+            return actionResult(false, "delete_action", "鏈壘鍒板搴斿鎵癸紝鎴栧綋鍓嶇敤鎴锋棤鏉冭闂娴佺▼銆�", approveId, null);
         }
-        if (process.getApproveDelete() != null && process.getApproveDelete() == 1) {
-            return actionResult(false, "delete_action", "璇ュ鎵规祦绋嬪凡缁忔槸鍒犻櫎鐘舵�併��", approveId, null);
+        if (!Objects.equals(process.getApproveUser(), currentUserId(memoryId)) && !isAdmin(currentUserId(memoryId))) {
+            return actionResult(false, "delete_action", "鍙湁鐢宠浜烘垨绠$悊鍛樺彲浠ュ垹闄ゅ鎵瑰崟銆�", approveId, null);
+        }
+        if (process.getApproveStatus() != null && (process.getApproveStatus() == 1 || process.getApproveStatus() == 2)) {
+            return actionResult(false, "delete_action", "瀹℃壒澶勭悊涓垨宸插畬鎴愮殑娴佺▼涓嶅厑璁搁�氳繃 AI 鍒犻櫎銆�", approveId, null);
         }
 
-        process.setApproveDelete(1);
-        approveProcessMapper.updateById(process);
-
-        List<ApproveNode> nodes = listNodes(process);
-        for (ApproveNode node : nodes) {
-            node.setDeleteFlag(1);
-            node.setUpdateTime(LocalDateTime.now());
-            approveNodeMapper.updateById(node);
-        }
+        approveProcessService.delByIds(Collections.singletonList(process.getId()));
         return actionResult(true, "delete_action", "瀹℃壒娴佺▼宸插垹闄ゃ��", approveId, Map.of(
-                "deletedNodeCount", nodes.size(),
+                "deletedProcessId", process.getId(),
                 "approveStatus", approveStatusName(process.getApproveStatus())
         ));
+    }
+
+    private ApproveProcess getAccessibleProcess(String memoryId, String approveId) {
+        ApproveProcess process = getProcessByApproveId(approveId);
+        if (process == null) {
+            return null;
+        }
+        return canView(process, currentUserId(memoryId)) ? process : null;
     }
 
     private ApproveProcess getProcessByApproveId(String approveId) {
@@ -507,29 +515,31 @@
         }
         return approveProcessMapper.selectOne(new LambdaQueryWrapper<ApproveProcess>()
                 .eq(ApproveProcess::getApproveId, approveId)
+                .eq(ApproveProcess::getApproveDelete, 0)
                 .last("limit 1"));
     }
 
     private List<ApproveNode> listNodes(ApproveProcess process) {
-        List<ApproveNode> nodes = approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
+        if (process == null) {
+            return List.of();
+        }
+        List<ApproveNode> nodes = defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
                 .eq(ApproveNode::getDeleteFlag, 0)
                 .eq(ApproveNode::getApproveProcessId, process.getApproveId())
-                .orderByAsc(ApproveNode::getApproveNodeOrder));
-        if (nodes != null && !nodes.isEmpty()) {
+                .orderByAsc(ApproveNode::getApproveNodeOrder)));
+        if (!nodes.isEmpty()) {
             return nodes;
         }
-        List<ApproveNode> fallbackNodes = approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
+        return defaultList(approveNodeMapper.selectList(new LambdaQueryWrapper<ApproveNode>()
                 .eq(ApproveNode::getDeleteFlag, 0)
                 .eq(ApproveNode::getApproveProcessId, String.valueOf(process.getId()))
-                .orderByAsc(ApproveNode::getApproveNodeOrder));
-        return fallbackNodes == null ? List.of() : fallbackNodes;
+                .orderByAsc(ApproveNode::getApproveNodeOrder)));
     }
 
     private List<ApproveLog> listLogs(Long processId) {
-        List<ApproveLog> logs = approveLogMapper.selectList(new LambdaQueryWrapper<ApproveLog>()
+        return defaultList(approveLogMapper.selectList(new LambdaQueryWrapper<ApproveLog>()
                 .eq(ApproveLog::getApproveId, processId)
-                .orderByAsc(ApproveLog::getApproveNodeOrder, ApproveLog::getApproveTime));
-        return logs == null ? List.of() : logs;
+                .orderByAsc(ApproveLog::getApproveNodeOrder, ApproveLog::getApproveTime)));
     }
 
     private ApproveNode findCurrentNode(List<ApproveNode> nodes) {
@@ -539,12 +549,72 @@
                 .orElse(null);
     }
 
-    private ApproveNode findNextNode(List<ApproveNode> nodes, Integer currentOrder) {
-        return nodes.stream()
-                .filter(node -> node.getApproveNodeOrder() != null && currentOrder != null)
-                .filter(node -> node.getApproveNodeOrder() > currentOrder)
-                .min(Comparator.comparing(ApproveNode::getApproveNodeOrder))
+    private boolean isLastNode(List<ApproveNode> nodes, ApproveNode currentNode) {
+        Integer maxOrder = nodes.stream()
+                .map(ApproveNode::getApproveNodeOrder)
+                .filter(Objects::nonNull)
+                .max(Integer::compareTo)
                 .orElse(null);
+        return maxOrder != null && Objects.equals(maxOrder, currentNode.getApproveNodeOrder());
+    }
+
+    private void writeApproveLog(String memoryId, ApproveProcess process, ApproveNode currentNode, String remark) {
+        if (process == null || currentNode == null) {
+            return;
+        }
+        ApproveLog log = new ApproveLog();
+        log.setApproveId(process.getId());
+        log.setApproveNodeOrder(currentNode.getApproveNodeOrder());
+        log.setApproveUser(currentUserId(memoryId));
+        log.setApproveTime(new Date());
+        log.setApproveStatus(process.getApproveStatus());
+        log.setApproveRemark(remark);
+        approveLogMapper.insert(log);
+    }
+
+    private boolean canView(ApproveProcess process, Long userId) {
+        if (process == null || userId == null) {
+            return false;
+        }
+        return isAdmin(userId)
+                || Objects.equals(process.getApproveUser(), userId)
+                || Objects.equals(process.getApproveUserCurrentId(), userId)
+                || containsUserId(process.getApproveUserIds(), userId);
+    }
+
+    private boolean canOperate(ApproveProcess process, Long userId) {
+        return process != null && userId != null && Objects.equals(process.getApproveUserCurrentId(), userId);
+    }
+
+    private boolean containsUserId(String csv, Long userId) {
+        if (!StringUtils.hasText(csv) || userId == null) {
+            return false;
+        }
+        String target = String.valueOf(userId);
+        for (String item : csv.split(",")) {
+            if (target.equals(item.trim())) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private String relationName(ApproveProcess process, Long userId) {
+        if (Objects.equals(process.getApproveUserCurrentId(), userId)) {
+            return "褰撳墠瀹℃壒浜�";
+        }
+        if (Objects.equals(process.getApproveUser(), userId)) {
+            return "鐢宠浜�";
+        }
+        if (containsUserId(process.getApproveUserIds(), userId)) {
+            return "瀹℃壒閾炬垚鍛�";
+        }
+        return "鍙";
+    }
+
+    private List<String> todoColumns() {
+        return List.of("approveId", "approveType", "approveUserName", "approveUserCurrentName",
+                "approveReason", "approveStatus", "createTime", "relation");
     }
 
     private int normalizeLimit(Integer limit) {
@@ -640,17 +710,7 @@
         if (total <= 0) {
             return "0.00%";
         }
-        double rate = part * 100.0 / total;
-        return String.format("%.2f%%", rate);
-    }
-
-    private List<String> buildRecentDates(int days) {
-        List<String> dates = new ArrayList<>();
-        LocalDate today = LocalDate.now();
-        for (int i = days - 1; i >= 0; i--) {
-            dates.add(today.minusDays(i).toString());
-        }
-        return dates;
+        return String.format("%.2f%%", part * 100.0 / total);
     }
 
     private List<Map<String, Object>> toTrendItems(List<String> dates, List<Long> values) {
@@ -702,9 +762,9 @@
         return option;
     }
 
-    private Map<String, Object> buildRecentTrendLineOption(List<String> dates, List<Long> values) {
+    private Map<String, Object> buildTrendLineOption(List<String> dates, List<Long> values, String label) {
         Map<String, Object> option = new LinkedHashMap<>();
-        option.put("title", Map.of("text", "鏈�杩�7澶╁鎵规柊澧炶秼鍔�", "left", "center"));
+        option.put("title", Map.of("text", label + "瀹℃壒鏂板瓒嬪娍", "left", "center"));
         option.put("tooltip", Map.of("trigger", "axis"));
         option.put("xAxis", Map.of("type", "category", "data", dates));
         option.put("yAxis", Map.of("type", "value"));
@@ -726,19 +786,147 @@
         }
     }
 
+    private DateRange resolveDateRange(String startDateText, String endDateText, String timeRange) {
+        LocalDate today = LocalDate.now();
+        LocalDate explicitStart = parseLocalDate(startDateText);
+        LocalDate explicitEnd = parseLocalDate(endDateText);
+        if (explicitStart != null || explicitEnd != null) {
+            LocalDate start = explicitStart != null ? explicitStart : explicitEnd;
+            LocalDate end = explicitEnd != null ? explicitEnd : explicitStart;
+            if (start.isAfter(end)) {
+                LocalDate temp = start;
+                start = end;
+                end = temp;
+            }
+            return new DateRange(start, end, start + "鑷�" + end);
+        }
+        if (!StringUtils.hasText(timeRange)) {
+            return new DateRange(today.minusDays(6), today, "杩�7澶�");
+        }
+
+        String text = timeRange.trim();
+        if (text.contains("浠婂ぉ")) {
+            return new DateRange(today, today, "浠婂ぉ");
+        }
+        if (text.contains("鏄ㄥぉ") || text.contains("鏄ㄦ棩")) {
+            LocalDate day = today.minusDays(1);
+            return new DateRange(day, day, "鏄ㄥぉ");
+        }
+        if (text.contains("鏈懆")) {
+            LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+            return new DateRange(start, today, "鏈懆");
+        }
+        if (text.contains("涓婂懆")) {
+            LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+            LocalDate start = thisWeekStart.minusWeeks(1);
+            LocalDate end = thisWeekStart.minusDays(1);
+            return new DateRange(start, end, "涓婂懆");
+        }
+        if (text.contains("鏈湀")) {
+            LocalDate start = today.withDayOfMonth(1);
+            return new DateRange(start, today, "鏈湀");
+        }
+        if (text.contains("涓婃湀")) {
+            YearMonth lastMonth = YearMonth.from(today).minusMonths(1);
+            return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "涓婃湀");
+        }
+        if (text.contains("鏈勾") || text.contains("浠婂勾")) {
+            LocalDate start = today.withDayOfYear(1);
+            return new DateRange(start, today, "鏈勾");
+        }
+        if (text.contains("鍘诲勾")) {
+            LocalDate start = today.minusYears(1).withDayOfYear(1);
+            LocalDate end = today.minusYears(1).withMonth(12).withDayOfMonth(31);
+            return new DateRange(start, end, "鍘诲勾");
+        }
+
+        Matcher relativeMatcher = java.util.regex.Pattern.compile("(杩憒鏈�杩�)(\\d+)(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text);
+        if (relativeMatcher.find()) {
+            int amount = Integer.parseInt(relativeMatcher.group(2));
+            String unit = relativeMatcher.group(3);
+            LocalDate start = switch (unit) {
+                case "澶�" -> today.minusDays(Math.max(amount - 1L, 0));
+                case "鍛�" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
+                case "涓湀", "鏈�" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
+                case "骞�" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
+                default -> today.minusDays(6);
+            };
+            return new DateRange(start, today, "杩�" + amount + unit);
+        }
+
+        Matcher dateMatcher = java.util.regex.Pattern.compile("(\\d{4}-\\d{2}-\\d{2})").matcher(text);
+        if (dateMatcher.find()) {
+            LocalDate start = LocalDate.parse(dateMatcher.group(1));
+            LocalDate end = dateMatcher.find() ? LocalDate.parse(dateMatcher.group(1)) : start;
+            if (start.isAfter(end)) {
+                LocalDate temp = start;
+                start = end;
+                end = temp;
+            }
+            return new DateRange(start, end, start + "鑷�" + end);
+        }
+
+        return new DateRange(today.minusDays(6), today, "杩�7澶�");
+    }
+
+    private boolean withinDateRange(LocalDateTime createTime, DateRange dateRange) {
+        if (createTime == null) {
+            return false;
+        }
+        LocalDate date = createTime.toLocalDate();
+        return !date.isBefore(dateRange.start()) && !date.isAfter(dateRange.end());
+    }
+
+    private TrendRange buildTrendRange(LocalDate start, LocalDate end, List<ApproveProcess> processes) {
+        long days = ChronoUnit.DAYS.between(start, end) + 1;
+        if (days <= 31) {
+            List<String> labels = new ArrayList<>();
+            List<Long> values = new ArrayList<>();
+            for (LocalDate cursor = start; !cursor.isAfter(end); cursor = cursor.plusDays(1)) {
+                LocalDate current = cursor;
+                labels.add(current.toString());
+                values.add(processes.stream()
+                        .filter(process -> process.getCreateTime() != null)
+                        .filter(process -> process.getCreateTime().toLocalDate().equals(current))
+                        .count());
+            }
+            return new TrendRange(labels, values, "day", start + "鑷�" + end);
+        }
+
+        List<String> labels = new ArrayList<>();
+        List<Long> values = new ArrayList<>();
+        YearMonth startMonth = YearMonth.from(start);
+        YearMonth endMonth = YearMonth.from(end);
+        for (YearMonth cursor = startMonth; !cursor.isAfter(endMonth); cursor = cursor.plusMonths(1)) {
+            YearMonth current = cursor;
+            labels.add(current.toString());
+            values.add(processes.stream()
+                    .filter(process -> process.getCreateTime() != null)
+                    .filter(process -> YearMonth.from(process.getCreateTime()).equals(current))
+                    .count());
+        }
+        return new TrendRange(labels, values, "month", start + "鑷�" + end);
+    }
+
+    private LocalDate parseLocalDate(String text) {
+        if (!StringUtils.hasText(text)) {
+            return null;
+        }
+        return LocalDate.parse(text.trim());
+    }
+
     private String actionResult(boolean success, String type, String description, String approveId, Map<String, Object> data) {
         Map<String, Object> summary = new LinkedHashMap<>();
-        summary.put("approveId", approveId == null ? "" : approveId);
+        summary.put("approveId", safe(approveId));
         return jsonResponse(success, type, description, summary, data == null ? Map.of() : data, Map.of());
     }
 
-    private String jsonResponse(
-            boolean success,
-            String type,
-            String description,
-            Map<String, Object> summary,
-            Map<String, Object> data,
-            Map<String, Object> charts) {
+    private String jsonResponse(boolean success,
+                                String type,
+                                String description,
+                                Map<String, Object> summary,
+                                Map<String, Object> data,
+                                Map<String, Object> charts) {
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("success", success);
         result.put("type", type);
@@ -748,4 +936,30 @@
         result.put("charts", charts == null ? Map.of() : charts);
         return JSON.toJSONString(result);
     }
+
+    private LoginUser currentLoginUser(String memoryId) {
+        LoginUser loginUser = aiSessionUserContext.get(memoryId);
+        if (loginUser != null) {
+            return loginUser;
+        }
+        return SecurityUtils.getLoginUser();
+    }
+
+    private Long currentUserId(String memoryId) {
+        return currentLoginUser(memoryId).getUserId();
+    }
+
+    private boolean isAdmin(Long userId) {
+        return SecurityUtils.isAdmin(userId);
+    }
+
+    private <T> List<T> defaultList(List<T> list) {
+        return list == null ? List.of() : list;
+    }
+
+    private record DateRange(LocalDate start, LocalDate end, String label) {
+    }
+
+    private record TrendRange(List<String> labels, List<Long> values, String granularity, String label) {
+    }
 }
diff --git a/src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java b/src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java
index f2e1d08..9f6a258 100644
--- a/src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java
+++ b/src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java
@@ -37,6 +37,7 @@
         IGNORE_TABLES.add("sys_notice");
         IGNORE_TABLES.add("sys_user_client");
         IGNORE_TABLES.add("product_model");
+        IGNORE_TABLES.add("ai_chat_session");
         IGNORE_TABLES.add("product");
 
     }
diff --git a/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java b/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
index 410d049..93d4c77 100644
--- a/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
+++ b/src/main/java/com/ruoyi/framework/security/filter/JwtAuthenticationTokenFilter.java
@@ -22,23 +22,35 @@
  * @author ruoyi
  */
 @Component
-public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
-{
+public class JwtAuthenticationTokenFilter extends OncePerRequestFilter
+{
     @Autowired
     private TokenService tokenService;
 
     @Override
-    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
-            throws ServletException, IOException
-    {
-        LoginUser loginUser = tokenService.getLoginUser(request);
+    protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain chain)
+            throws ServletException, IOException
+    {
+        LoginUser loginUser = tokenService.getLoginUser(request);
         if (StringUtils.isNotNull(loginUser) && StringUtils.isNull(SecurityUtils.getAuthentication()))
         {
             tokenService.verifyToken(loginUser);
             UsernamePasswordAuthenticationToken authenticationToken = new UsernamePasswordAuthenticationToken(loginUser, null, loginUser.getAuthorities());
             authenticationToken.setDetails(new WebAuthenticationDetailsSource().buildDetails(request));
             SecurityContextHolder.getContext().setAuthentication(authenticationToken);
-        }
-        chain.doFilter(request, response);
-    }
-}
+        }
+        chain.doFilter(request, response);
+    }
+
+    @Override
+    protected boolean shouldNotFilterAsyncDispatch()
+    {
+        return false;
+    }
+
+    @Override
+    protected boolean shouldNotFilterErrorDispatch()
+    {
+        return false;
+    }
+}
diff --git a/src/main/resources/application-dev-pro.yml b/src/main/resources/application-dev-pro.yml
index f94aaa2..1552370 100644
--- a/src/main/resources/application-dev-pro.yml
+++ b/src/main/resources/application-dev-pro.yml
@@ -142,14 +142,15 @@
       enabled: false
   # redis 閰嶇疆
   data:
+    mongodb:
+      uri: mongodb://114.132.189.42:9028/chat_memory_db
+    # redis 閰嶇疆
     redis:
       # 鍦板潃
-  #    host: 127.0.0.1
-      host: 47.114.74.44
+      host: 127.0.0.1
       #    host: 172.17.0.1
       # 绔彛锛岄粯璁や负6379
-  #    port: 6379
-      port: 6399
+      port: 6379
       # 鏁版嵁搴撶储寮�
       database: 0
       # 瀵嗙爜

--
Gitblit v1.9.3