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