From 5d16d9a0e7240ff4e6346bfee4890159da5a764e Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 19 三月 2026 11:40:51 +0800
Subject: [PATCH] #AI.记忆治理

---
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java          |    4 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatMemoryDto.java                  |    8 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java |  181 ++++++++++++++++++++++++-
 rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java                 |    6 
 rsf-admin/src/api/ai/chat.js                                                                 |   18 ++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java                 |    6 
 version/db/ai_feature.sql                                                                    |   51 +++++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java                |    4 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatMessage.java                 |    3 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java       |   28 +++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java          |   14 ++
 rsf-admin/src/layout/AiChatDrawer.jsx                                                        |   79 +++++++++++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java                    |    4 
 13 files changed, 395 insertions(+), 11 deletions(-)

diff --git a/rsf-admin/src/api/ai/chat.js b/rsf-admin/src/api/ai/chat.js
index 939a2d0..1b2117b 100644
--- a/rsf-admin/src/api/ai/chat.js
+++ b/rsf-admin/src/api/ai/chat.js
@@ -51,6 +51,24 @@
     throw new Error(msg || "鏇存柊 AI 浼氳瘽缃《鐘舵�佸け璐�");
 };
 
+export const clearAiSessionMemory = async (sessionId) => {
+    const res = await request.post(`ai/chat/session/memory/clear/${sessionId}`);
+    const { code, msg, data } = res.data;
+    if (code === 200) {
+        return data;
+    }
+    throw new Error(msg || "娓呯┖ AI 浼氳瘽璁板繂澶辫触");
+};
+
+export const retainAiSessionLatestRound = async (sessionId) => {
+    const res = await request.post(`ai/chat/session/memory/retain-latest/${sessionId}`);
+    const { code, msg, data } = res.data;
+    if (code === 200) {
+        return data;
+    }
+    throw new Error(msg || "浠呬繚鐣欏綋鍓嶈疆璁板繂澶辫触");
+};
+
 export const streamAiChat = async (payload, { signal, onEvent } = {}) => {
     const token = getToken();
     const response = await fetch(`${PREFIX_BASE_URL}ai/chat/stream`, {
diff --git a/rsf-admin/src/layout/AiChatDrawer.jsx b/rsf-admin/src/layout/AiChatDrawer.jsx
index e8e2a07..4b9a69e 100644
--- a/rsf-admin/src/layout/AiChatDrawer.jsx
+++ b/rsf-admin/src/layout/AiChatDrawer.jsx
@@ -31,10 +31,12 @@
 import AddCommentOutlinedIcon from "@mui/icons-material/AddCommentOutlined";
 import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined";
 import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
+import AutoDeleteOutlinedIcon from "@mui/icons-material/AutoDeleteOutlined";
+import HistoryOutlinedIcon from "@mui/icons-material/HistoryOutlined";
 import PushPinOutlinedIcon from "@mui/icons-material/PushPinOutlined";
 import PushPinIcon from "@mui/icons-material/PushPin";
 import SearchOutlinedIcon from "@mui/icons-material/SearchOutlined";
-import { getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, streamAiChat } from "@/api/ai/chat";
+import { clearAiSessionMemory, getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, retainAiSessionLatestRound, streamAiChat } from "@/api/ai/chat";
 
 const DEFAULT_PROMPT_CODE = "home.default";
 
@@ -70,6 +72,9 @@
             promptName: runtime?.promptName || "--",
             model: runtime?.model || "--",
             mountedMcpCount: runtime?.mountedMcpCount ?? 0,
+            recentMessageCount: runtime?.recentMessageCount ?? 0,
+            hasSummary: !!runtime?.memorySummary,
+            hasFacts: !!runtime?.memoryFacts,
         };
     }, [runtime]);
 
@@ -206,6 +211,42 @@
             await loadSessions(sessionKeyword);
         } catch (error) {
             const message = error.message || "閲嶅懡鍚嶄細璇濆け璐�";
+            setDrawerError(message);
+            notify(message, { type: "error" });
+        }
+    };
+
+    const handleClearMemory = async () => {
+        if (streaming || !sessionId) {
+            return;
+        }
+        try {
+            await clearAiSessionMemory(sessionId);
+            notify("浼氳瘽璁板繂宸叉竻绌�");
+            await Promise.all([
+                loadRuntime(sessionId),
+                loadSessions(sessionKeyword),
+            ]);
+        } catch (error) {
+            const message = error.message || "娓呯┖浼氳瘽璁板繂澶辫触";
+            setDrawerError(message);
+            notify(message, { type: "error" });
+        }
+    };
+
+    const handleRetainLatestRound = async () => {
+        if (streaming || !sessionId) {
+            return;
+        }
+        try {
+            await retainAiSessionLatestRound(sessionId);
+            notify("宸蹭粎淇濈暀褰撳墠杞蹇�");
+            await Promise.all([
+                loadRuntime(sessionId),
+                loadSessions(sessionKeyword),
+            ]);
+        } catch (error) {
+            const message = error.message || "淇濈暀褰撳墠杞蹇嗗け璐�";
             setDrawerError(message);
             notify(message, { type: "error" });
         }
@@ -470,6 +511,9 @@
                                 <Chip size="small" label={`Model: ${runtimeSummary.model}`} />
                                 <Chip size="small" label={`MCP: ${runtimeSummary.mountedMcpCount}`} />
                                 <Chip size="small" label={`History: ${persistedMessages.length}`} />
+                                <Chip size="small" label={`Recent: ${runtimeSummary.recentMessageCount}`} />
+                                <Chip size="small" color={runtimeSummary.hasSummary ? "success" : "default"} label={runtimeSummary.hasSummary ? "鏈夋憳瑕�" : "鏃犳憳瑕�"} />
+                                <Chip size="small" color={runtimeSummary.hasFacts ? "info" : "default"} label={runtimeSummary.hasFacts ? "鏈変簨瀹�" : "鏃犱簨瀹�"} />
                             </Stack>
                             <Stack direction="row" spacing={1} mt={1.5} flexWrap="wrap" useFlexGap>
                                 {quickLinks.map((item) => (
@@ -483,7 +527,40 @@
                                         {item.label}
                                     </Button>
                                 ))}
+                                <Button
+                                    size="small"
+                                    variant="outlined"
+                                    startIcon={<HistoryOutlinedIcon />}
+                                    onClick={handleRetainLatestRound}
+                                    disabled={!sessionId || streaming}
+                                >
+                                    浠呬繚鐣欏綋鍓嶈疆
+                                </Button>
+                                <Button
+                                    size="small"
+                                    variant="outlined"
+                                    color="warning"
+                                    startIcon={<AutoDeleteOutlinedIcon />}
+                                    onClick={handleClearMemory}
+                                    disabled={!sessionId || streaming}
+                                >
+                                    娓呯┖璁板繂
+                                </Button>
                             </Stack>
+                            {!!runtime?.memorySummary && (
+                                <Alert severity="info" sx={{ mt: 1.5 }}>
+                                    <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
+                                        {runtime.memorySummary}
+                                    </Typography>
+                                </Alert>
+                            )}
+                            {!!runtime?.memoryFacts && (
+                                <Alert severity="success" sx={{ mt: 1.5 }}>
+                                    <Typography variant="body2" sx={{ whiteSpace: "pre-wrap" }}>
+                                        {runtime.memoryFacts}
+                                    </Typography>
+                                </Alert>
+                            )}
                             {loadingRuntime && (
                                 <Typography variant="body2" color="text.secondary" mt={1}>
                                     姝e湪鍔犺浇 AI 杩愯鏃朵俊鎭�...
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java
index 37e43df..7cc00eb 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/config/AiDefaults.java
@@ -18,4 +18,8 @@
     public static final int DEFAULT_TIMEOUT_MS = 60000;
     public static final double DEFAULT_TEMPERATURE = 0.7D;
     public static final double DEFAULT_TOP_P = 1.0D;
+    public static final int MEMORY_RECENT_ROUNDS = 6;
+    public static final int MEMORY_SUMMARY_TRIGGER_MESSAGES = 12;
+    public static final int MEMORY_SUMMARY_MAX_LENGTH = 1200;
+    public static final int MEMORY_FACTS_MAX_LENGTH = 600;
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java
index d31b241..613d629 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java
@@ -57,6 +57,20 @@
     }
 
     @PreAuthorize("isAuthenticated()")
+    @PostMapping("/ai/chat/session/memory/clear/{sessionId}")
+    public R clearSessionMemory(@PathVariable Long sessionId) {
+        aiChatService.clearSessionMemory(sessionId, getLoginUserId(), getTenantId());
+        return R.ok("Clear Success").add(sessionId);
+    }
+
+    @PreAuthorize("isAuthenticated()")
+    @PostMapping("/ai/chat/session/memory/retain-latest/{sessionId}")
+    public R retainLatestRound(@PathVariable Long sessionId) {
+        aiChatService.retainLatestRound(sessionId, getLoginUserId(), getTenantId());
+        return R.ok("Retain Success").add(sessionId);
+    }
+
+    @PreAuthorize("isAuthenticated()")
     @PostMapping(value = "/ai/chat/stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
     public SseEmitter stream(@RequestBody AiChatRequest request) {
         String requestId = StringUtils.hasText(request.getRequestId())
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatMemoryDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatMemoryDto.java
index afd6421..9791d9a 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatMemoryDto.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatMemoryDto.java
@@ -11,5 +11,13 @@
 
     private Long sessionId;
 
+    private String memorySummary;
+
+    private String memoryFacts;
+
+    private Integer recentMessageCount;
+
     private List<AiChatMessageDto> persistedMessages;
+
+    private List<AiChatMessageDto> shortMemoryMessages;
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java
index 9856335..0a6db57 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatRuntimeDto.java
@@ -27,5 +27,11 @@
 
     private List<String> mountErrors;
 
+    private String memorySummary;
+
+    private String memoryFacts;
+
+    private Integer recentMessageCount;
+
     private List<AiChatMessageDto> persistedMessages;
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatMessage.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatMessage.java
index 41753d0..849de05 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatMessage.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatMessage.java
@@ -35,6 +35,9 @@
     @ApiModelProperty(value = "娑堟伅鍐呭")
     private String content;
 
+    @ApiModelProperty(value = "鍐呭闀垮害")
+    private Integer contentLength;
+
     @ApiModelProperty(value = "鐢ㄦ埛 ID")
     private Long userId;
 
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java
index 67188a8..b475c95 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java
@@ -40,6 +40,12 @@
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date lastMessageTime;
 
+    @ApiModelProperty(value = "璁板繂鎽樿")
+    private String memorySummary;
+
+    @ApiModelProperty(value = "鍏抽敭浜嬪疄")
+    private String memoryFacts;
+
     @ApiModelProperty(value = "鏄惁缃《")
     private Integer pinned;
 
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java
index db7af54..2c8339c 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java
@@ -24,4 +24,8 @@
     AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request);
 
     AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request);
+
+    void clearSessionMemory(Long userId, Long tenantId, Long sessionId);
+
+    void retainLatestRound(Long userId, Long tenantId, Long sessionId);
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java
index ec4d2a9..940f251 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java
@@ -22,4 +22,8 @@
     AiChatSessionDto renameSession(Long sessionId, AiChatSessionRenameRequest request, Long userId, Long tenantId);
 
     AiChatSessionDto pinSession(Long sessionId, AiChatSessionPinRequest request, Long userId, Long tenantId);
+
+    void clearSessionMemory(Long sessionId, Long userId, Long tenantId);
+
+    void retainLatestRound(Long sessionId, Long userId, Long tenantId);
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java
index c0a4a61..5f1c1f3 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java
@@ -3,6 +3,7 @@
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.vincent.rsf.framework.common.Cools;
 import com.vincent.rsf.framework.exception.CoolException;
+import com.vincent.rsf.server.ai.config.AiDefaults;
 import com.vincent.rsf.server.ai.dto.AiChatMemoryDto;
 import com.vincent.rsf.server.ai.dto.AiChatMessageDto;
 import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
@@ -21,7 +22,6 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
-import java.util.Locale;
 
 @Service
 @RequiredArgsConstructor
@@ -40,12 +40,22 @@
         if (session == null) {
             return AiChatMemoryDto.builder()
                     .sessionId(null)
+                    .memorySummary(null)
+                    .memoryFacts(null)
+                    .recentMessageCount(0)
                     .persistedMessages(List.of())
+                    .shortMemoryMessages(List.of())
                     .build();
         }
+        List<AiChatMessageDto> persistedMessages = listMessages(session.getId());
+        List<AiChatMessageDto> shortMemoryMessages = tailMessagesByRounds(persistedMessages, AiDefaults.MEMORY_RECENT_ROUNDS);
         return AiChatMemoryDto.builder()
                 .sessionId(session.getId())
-                .persistedMessages(listMessages(session.getId()))
+                .memorySummary(session.getMemorySummary())
+                .memoryFacts(session.getMemoryFacts())
+                .recentMessageCount(shortMemoryMessages.size())
+                .persistedMessages(persistedMessages)
+                .shortMemoryMessages(shortMemoryMessages)
                 .build();
     }
 
@@ -123,6 +133,7 @@
                 .setUpdateBy(userId)
                 .setUpdateTime(now);
         aiChatSessionMapper.updateById(update);
+        refreshMemoryProfile(session.getId(), userId);
     }
 
     @Override
@@ -192,6 +203,47 @@
         return buildSessionDto(requireOwnedSession(sessionId, userId, tenantId));
     }
 
+    @Override
+    public void clearSessionMemory(Long userId, Long tenantId, Long sessionId) {
+        ensureIdentity(userId, tenantId);
+        AiChatSession session = requireOwnedSession(sessionId, userId, tenantId);
+        List<AiChatMessage> messages = aiChatMessageMapper.selectList(new LambdaQueryWrapper<AiChatMessage>()
+                .eq(AiChatMessage::getSessionId, sessionId)
+                .eq(AiChatMessage::getDeleted, 0));
+        for (AiChatMessage message : messages) {
+            aiChatMessageMapper.updateById(new AiChatMessage()
+                    .setId(message.getId())
+                    .setDeleted(1));
+        }
+        aiChatSessionMapper.updateById(new AiChatSession()
+                .setId(sessionId)
+                .setMemorySummary(null)
+                .setMemoryFacts(null)
+                .setUpdateBy(userId)
+                .setUpdateTime(new Date())
+                .setLastMessageTime(session.getCreateTime()));
+    }
+
+    @Override
+    public void retainLatestRound(Long userId, Long tenantId, Long sessionId) {
+        ensureIdentity(userId, tenantId);
+        requireOwnedSession(sessionId, userId, tenantId);
+        List<AiChatMessage> records = listMessageRecords(sessionId);
+        if (records.isEmpty()) {
+            return;
+        }
+        List<AiChatMessage> retained = tailMessageRecordsByRounds(records, 1);
+        for (AiChatMessage message : records) {
+            boolean shouldKeep = retained.stream().anyMatch(item -> item.getId().equals(message.getId()));
+            if (!shouldKeep) {
+                aiChatMessageMapper.updateById(new AiChatMessage()
+                        .setId(message.getId())
+                        .setDeleted(1));
+            }
+        }
+        refreshMemoryProfile(sessionId, userId);
+    }
+
     private AiChatSession findLatestSession(Long userId, Long tenantId, String promptCode) {
         return aiChatSessionMapper.selectOne(new LambdaQueryWrapper<AiChatSession>()
                 .eq(AiChatSession::getUserId, userId)
@@ -237,11 +289,7 @@
     }
 
     private List<AiChatMessageDto> listMessages(Long sessionId) {
-        List<AiChatMessage> records = aiChatMessageMapper.selectList(new LambdaQueryWrapper<AiChatMessage>()
-                .eq(AiChatMessage::getSessionId, sessionId)
-                .eq(AiChatMessage::getDeleted, 0)
-                .orderByAsc(AiChatMessage::getSeqNo)
-                .orderByAsc(AiChatMessage::getId));
+        List<AiChatMessage> records = listMessageRecords(sessionId);
         if (Cools.isEmpty(records)) {
             return List.of();
         }
@@ -279,6 +327,14 @@
         return normalized;
     }
 
+    private List<AiChatMessage> listMessageRecords(Long sessionId) {
+        return aiChatMessageMapper.selectList(new LambdaQueryWrapper<AiChatMessage>()
+                .eq(AiChatMessage::getSessionId, sessionId)
+                .eq(AiChatMessage::getDeleted, 0)
+                .orderByAsc(AiChatMessage::getSeqNo)
+                .orderByAsc(AiChatMessage::getId));
+    }
+
     private int findNextSeqNo(Long sessionId) {
         AiChatMessage lastMessage = aiChatMessageMapper.selectOne(new LambdaQueryWrapper<AiChatMessage>()
                 .eq(AiChatMessage::getSessionId, sessionId)
@@ -295,6 +351,7 @@
                 .setSeqNo(seqNo)
                 .setRole(role)
                 .setContent(content)
+                .setContentLength(content == null ? 0 : content.length())
                 .setUserId(userId)
                 .setTenantId(tenantId)
                 .setDeleted(0)
@@ -371,6 +428,116 @@
         return normalized.length() > 80 ? normalized.substring(0, 80) : normalized;
     }
 
+    private void refreshMemoryProfile(Long sessionId, Long userId) {
+        List<AiChatMessageDto> messages = listMessages(sessionId);
+        List<AiChatMessageDto> shortMemoryMessages = tailMessagesByRounds(messages, AiDefaults.MEMORY_RECENT_ROUNDS);
+        List<AiChatMessageDto> historyMessages = messages.size() > shortMemoryMessages.size()
+                ? messages.subList(0, messages.size() - shortMemoryMessages.size())
+                : List.of();
+        String memorySummary = historyMessages.size() >= AiDefaults.MEMORY_SUMMARY_TRIGGER_MESSAGES
+                ? buildMemorySummary(historyMessages)
+                : null;
+        String memoryFacts = buildMemoryFacts(messages);
+        AiChatMessage lastMessage = aiChatMessageMapper.selectOne(new LambdaQueryWrapper<AiChatMessage>()
+                .eq(AiChatMessage::getSessionId, sessionId)
+                .eq(AiChatMessage::getDeleted, 0)
+                .orderByDesc(AiChatMessage::getSeqNo)
+                .orderByDesc(AiChatMessage::getId)
+                .last("limit 1"));
+        aiChatSessionMapper.updateById(new AiChatSession()
+                .setId(sessionId)
+                .setMemorySummary(memorySummary)
+                .setMemoryFacts(memoryFacts)
+                .setLastMessageTime(lastMessage == null ? null : lastMessage.getCreateTime())
+                .setUpdateBy(userId)
+                .setUpdateTime(new Date()));
+    }
+
+    private List<AiChatMessageDto> tailMessagesByRounds(List<AiChatMessageDto> source, int rounds) {
+        if (Cools.isEmpty(source) || rounds <= 0) {
+            return List.of();
+        }
+        int userCount = 0;
+        int startIndex = source.size();
+        for (int i = source.size() - 1; i >= 0; i--) {
+            AiChatMessageDto item = source.get(i);
+            startIndex = i;
+            if (item != null && "user".equalsIgnoreCase(item.getRole())) {
+                userCount++;
+                if (userCount >= rounds) {
+                    break;
+                }
+            }
+        }
+        return new ArrayList<>(source.subList(Math.max(0, startIndex), source.size()));
+    }
+
+    private List<AiChatMessage> tailMessageRecordsByRounds(List<AiChatMessage> source, int rounds) {
+        if (Cools.isEmpty(source) || rounds <= 0) {
+            return List.of();
+        }
+        int userCount = 0;
+        int startIndex = source.size();
+        for (int i = source.size() - 1; i >= 0; i--) {
+            AiChatMessage item = source.get(i);
+            startIndex = i;
+            if (item != null && "user".equalsIgnoreCase(item.getRole())) {
+                userCount++;
+                if (userCount >= rounds) {
+                    break;
+                }
+            }
+        }
+        return new ArrayList<>(source.subList(Math.max(0, startIndex), source.size()));
+    }
+
+    private String buildMemorySummary(List<AiChatMessageDto> historyMessages) {
+        StringBuilder builder = new StringBuilder("杈冩棭瀵硅瘽鎽樿:\n");
+        for (AiChatMessageDto item : historyMessages) {
+            if (item == null || !StringUtils.hasText(item.getContent())) {
+                continue;
+            }
+            String prefix = "assistant".equalsIgnoreCase(item.getRole()) ? "- AI: " : "- 鐢ㄦ埛: ";
+            String content = compactText(item.getContent(), 120);
+            if (!StringUtils.hasText(content)) {
+                continue;
+            }
+            builder.append(prefix).append(content).append("\n");
+            if (builder.length() >= AiDefaults.MEMORY_SUMMARY_MAX_LENGTH) {
+                break;
+            }
+        }
+        return compactText(builder.toString(), AiDefaults.MEMORY_SUMMARY_MAX_LENGTH);
+    }
+
+    private String buildMemoryFacts(List<AiChatMessageDto> messages) {
+        if (Cools.isEmpty(messages)) {
+            return null;
+        }
+        StringBuilder builder = new StringBuilder("鍏抽敭浜嬪疄:\n");
+        int userFacts = 0;
+        for (int i = messages.size() - 1; i >= 0 && userFacts < 4; i--) {
+            AiChatMessageDto item = messages.get(i);
+            if (item == null || !"user".equalsIgnoreCase(item.getRole()) || !StringUtils.hasText(item.getContent())) {
+                continue;
+            }
+            builder.append("- 鐢ㄦ埛鍏虫敞: ").append(compactText(item.getContent(), 100)).append("\n");
+            userFacts++;
+        }
+        return userFacts == 0 ? null : compactText(builder.toString(), AiDefaults.MEMORY_FACTS_MAX_LENGTH);
+    }
+
+    private String compactText(String content, int maxLength) {
+        if (!StringUtils.hasText(content)) {
+            return null;
+        }
+        String normalized = content.trim()
+                .replace("\r", " ")
+                .replace("\n", " ")
+                .replaceAll("\\s+", " ");
+        return normalized.length() > maxLength ? normalized.substring(0, maxLength) : normalized;
+    }
+
     private void ensureIdentity(Long userId, Long tenantId) {
         if (userId == null) {
             throw new CoolException("褰撳墠鐧诲綍鐢ㄦ埛涓嶅瓨鍦�");
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java
index 280914f..1ce1279 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java
@@ -95,6 +95,9 @@
                 .mountedMcpCount(config.getMcpMounts().size())
                 .mountedMcpNames(config.getMcpMounts().stream().map(item -> item.getName()).toList())
                 .mountErrors(List.of())
+                .memorySummary(memory.getMemorySummary())
+                .memoryFacts(memory.getMemoryFacts())
+                .recentMessageCount(memory.getRecentMessageCount())
                 .persistedMessages(memory.getPersistedMessages())
                 .build();
     }
@@ -121,6 +124,16 @@
     }
 
     @Override
+    public void clearSessionMemory(Long sessionId, Long userId, Long tenantId) {
+        aiChatMemoryService.clearSessionMemory(userId, tenantId, sessionId);
+    }
+
+    @Override
+    public void retainLatestRound(Long sessionId, Long userId, Long tenantId) {
+        aiChatMemoryService.retainLatestRound(userId, tenantId, sessionId);
+    }
+
+    @Override
     public SseEmitter stream(AiChatRequest request, Long userId, Long tenantId) {
         SseEmitter emitter = new SseEmitter(AiDefaults.SSE_TIMEOUT_MS);
         CompletableFuture.runAsync(() -> doStream(request, userId, tenantId, emitter), aiChatTaskExecutor);
@@ -141,7 +154,7 @@
             AiChatSession session = resolveSession(request, userId, tenantId, config.getPromptCode());
             sessionId = session.getId();
             AiChatMemoryDto memory = loadMemory(userId, tenantId, config.getPromptCode(), session.getId());
-            List<AiChatMessageDto> mergedMessages = mergeMessages(memory.getPersistedMessages(), request.getMessages());
+            List<AiChatMessageDto> mergedMessages = mergeMessages(memory.getShortMemoryMessages(), request.getMessages());
             try (McpMountRuntimeFactory.McpMountRuntime runtime = createRuntime(config, userId)) {
                 emitStrict(emitter, "start", AiChatRuntimeDto.builder()
                         .requestId(requestId)
@@ -153,6 +166,9 @@
                         .mountedMcpCount(runtime.getMountedCount())
                         .mountedMcpNames(runtime.getMountedNames())
                         .mountErrors(runtime.getErrors())
+                        .memorySummary(memory.getMemorySummary())
+                        .memoryFacts(memory.getMemoryFacts())
+                        .recentMessageCount(memory.getRecentMessageCount())
                         .persistedMessages(memory.getPersistedMessages())
                         .build());
                 emitSafely(emitter, "status", AiChatStatusDto.builder()
@@ -167,7 +183,7 @@
                         requestId, userId, tenantId, session.getId(), resolvedModel);
 
                 Prompt prompt = new Prompt(
-                        buildPromptMessages(mergedMessages, config.getPrompt(), request.getMetadata()),
+                        buildPromptMessages(memory, mergedMessages, config.getPrompt(), request.getMetadata()),
                         buildChatOptions(config.getAiParam(), runtime.getToolCallbacks(), userId, request.getMetadata())
                 );
                 OpenAiChatModel chatModel = createChatModel(config.getAiParam());
@@ -400,7 +416,7 @@
         return builder.build();
     }
 
-    private List<Message> buildPromptMessages(List<AiChatMessageDto> sourceMessages, AiPrompt aiPrompt, Map<String, Object> metadata) {
+    private List<Message> buildPromptMessages(AiChatMemoryDto memory, List<AiChatMessageDto> sourceMessages, AiPrompt aiPrompt, Map<String, Object> metadata) {
         if (Cools.isEmpty(sourceMessages)) {
             throw new CoolException("瀵硅瘽娑堟伅涓嶈兘涓虹┖");
         }
@@ -408,6 +424,12 @@
         if (StringUtils.hasText(aiPrompt.getSystemPrompt())) {
             messages.add(new SystemMessage(aiPrompt.getSystemPrompt()));
         }
+        if (memory != null && StringUtils.hasText(memory.getMemorySummary())) {
+            messages.add(new SystemMessage("鍘嗗彶鎽樿:\n" + memory.getMemorySummary()));
+        }
+        if (memory != null && StringUtils.hasText(memory.getMemoryFacts())) {
+            messages.add(new SystemMessage("鍏抽敭浜嬪疄:\n" + memory.getMemoryFacts()));
+        }
         int lastUserIndex = -1;
         for (int i = 0; i < sourceMessages.size(); i++) {
             AiChatMessageDto item = sourceMessages.get(i);
diff --git a/version/db/ai_feature.sql b/version/db/ai_feature.sql
index 529ea30..898732a 100644
--- a/version/db/ai_feature.sql
+++ b/version/db/ai_feature.sql
@@ -76,6 +76,8 @@
   `user_id` bigint(20) NOT NULL COMMENT '鐢ㄦ埛 ID',
   `tenant_id` bigint(20) DEFAULT NULL COMMENT '绉熸埛',
   `last_message_time` datetime DEFAULT NULL COMMENT '鏈�鍚庢秷鎭椂闂�',
+  `memory_summary` longtext COMMENT '璁板繂鎽樿',
+  `memory_facts` text COMMENT '鍏抽敭浜嬪疄',
   `pinned` tinyint(1) DEFAULT '0' COMMENT '鏄惁缃《',
   `status` int(11) DEFAULT '1' COMMENT '鐘舵��',
   `deleted` int(11) DEFAULT '0' COMMENT '鍒犻櫎鏍囪',
@@ -93,6 +95,7 @@
   `seq_no` int(11) NOT NULL COMMENT '娑堟伅搴忓彿',
   `role` varchar(32) NOT NULL COMMENT '娑堟伅瑙掕壊',
   `content` longtext COMMENT '娑堟伅鍐呭',
+  `content_length` int(11) DEFAULT NULL COMMENT '鍐呭闀垮害',
   `user_id` bigint(20) NOT NULL COMMENT '鐢ㄦ埛 ID',
   `tenant_id` bigint(20) DEFAULT NULL COMMENT '绉熸埛',
   `deleted` int(11) DEFAULT '0' COMMENT '鍒犻櫎鏍囪',
@@ -134,6 +137,54 @@
 EXECUTE chat_session_pinned_stmt;
 DEALLOCATE PREPARE chat_session_pinned_stmt;
 
+SET @chat_session_summary_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_chat_session'
+    AND `COLUMN_NAME` = 'memory_summary'
+);
+SET @chat_session_summary_sql := IF(
+  @chat_session_summary_exists = 0,
+  'ALTER TABLE `sys_ai_chat_session` ADD COLUMN `memory_summary` longtext COMMENT ''璁板繂鎽樿'' AFTER `last_message_time`',
+  'SELECT 1'
+);
+PREPARE chat_session_summary_stmt FROM @chat_session_summary_sql;
+EXECUTE chat_session_summary_stmt;
+DEALLOCATE PREPARE chat_session_summary_stmt;
+
+SET @chat_session_facts_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_chat_session'
+    AND `COLUMN_NAME` = 'memory_facts'
+);
+SET @chat_session_facts_sql := IF(
+  @chat_session_facts_exists = 0,
+  'ALTER TABLE `sys_ai_chat_session` ADD COLUMN `memory_facts` text COMMENT ''鍏抽敭浜嬪疄'' AFTER `memory_summary`',
+  'SELECT 1'
+);
+PREPARE chat_session_facts_stmt FROM @chat_session_facts_sql;
+EXECUTE chat_session_facts_stmt;
+DEALLOCATE PREPARE chat_session_facts_stmt;
+
+SET @chat_message_length_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_chat_message'
+    AND `COLUMN_NAME` = 'content_length'
+);
+SET @chat_message_length_sql := IF(
+  @chat_message_length_exists = 0,
+  'ALTER TABLE `sys_ai_chat_message` ADD COLUMN `content_length` int(11) DEFAULT NULL COMMENT ''鍐呭闀垮害'' AFTER `content`',
+  'SELECT 1'
+);
+PREPARE chat_message_length_stmt FROM @chat_message_length_sql;
+EXECUTE chat_message_length_stmt;
+DEALLOCATE PREPARE chat_message_length_stmt;
+
 BEGIN;
 INSERT INTO `sys_ai_prompt`
 (`id`, `name`, `code`, `scene`, `system_prompt`, `user_prompt_template`, `tenant_id`, `status`, `deleted`, `create_time`, `create_by`, `update_time`, `update_by`, `memo`)

--
Gitblit v1.9.1