From 80a6d9236ade191a5de0975abe4de5a6e7e63915 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 19 三月 2026 14:03:10 +0800
Subject: [PATCH] #AI.注释

---
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java |  332 ++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 317 insertions(+), 15 deletions(-)

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 b663a6a..d531a27 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,8 +3,11 @@
 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;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
 import com.vincent.rsf.server.ai.dto.AiChatSessionDto;
 import com.vincent.rsf.server.ai.entity.AiChatMessage;
 import com.vincent.rsf.server.ai.entity.AiChatSession;
@@ -27,6 +30,11 @@
     private final AiChatSessionMapper aiChatSessionMapper;
     private final AiChatMessageMapper aiChatMessageMapper;
 
+    /**
+     * 璇诲彇浼氳瘽璁板繂蹇収銆�
+     * 杩斿洖缁撴灉鍚屾椂鍖呭惈瀹屾暣钀藉簱鍘嗗彶銆佺煭鏈熻蹇嗙獥鍙d互鍙婃憳瑕�/浜嬪疄璁板繂锛�
+     * 渚夸簬璋冪敤鏂规寜涓嶅悓鐢ㄩ�旈�夋嫨鏁版嵁绮掑害銆�
+     */
     @Override
     public AiChatMemoryDto getMemory(Long userId, Long tenantId, String promptCode, Long sessionId) {
         ensureIdentity(userId, tenantId);
@@ -37,17 +45,31 @@
         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();
     }
 
+    /**
+     * 鏌ヨ褰撳墠鐢ㄦ埛鍦ㄦ煇涓� Prompt 涓嬬殑浼氳瘽鍒楄〃銆�
+     * 鍒楄〃鍙繑鍥炵敤浜庝晶杈规爮灞曠ず鐨勬憳瑕佷俊鎭紝涓嶈繑鍥炲畬鏁村璇濆唴瀹广��
+     */
     @Override
-    public List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode) {
+    public List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode, String keyword) {
         ensureIdentity(userId, tenantId);
         String resolvedPromptCode = requirePromptCode(promptCode);
         List<AiChatSession> sessions = aiChatSessionMapper.selectList(new LambdaQueryWrapper<AiChatSession>()
@@ -56,6 +78,8 @@
                 .eq(AiChatSession::getPromptCode, resolvedPromptCode)
                 .eq(AiChatSession::getDeleted, 0)
                 .eq(AiChatSession::getStatus, StatusType.ENABLE.val)
+                .like(StringUtils.hasText(keyword), AiChatSession::getTitle, keyword == null ? null : keyword.trim())
+                .orderByDesc(AiChatSession::getPinned)
                 .orderByDesc(AiChatSession::getLastMessageTime)
                 .orderByDesc(AiChatSession::getId));
         if (Cools.isEmpty(sessions)) {
@@ -63,16 +87,15 @@
         }
         List<AiChatSessionDto> result = new ArrayList<>();
         for (AiChatSession session : sessions) {
-            result.add(AiChatSessionDto.builder()
-                    .sessionId(session.getId())
-                    .title(session.getTitle())
-                    .promptCode(session.getPromptCode())
-                    .lastMessageTime(session.getLastMessageTime())
-                    .build());
+            result.add(buildSessionDto(session));
         }
         return result;
     }
 
+    /**
+     * 瑙f瀽鏈疆璇锋眰搴旇钀藉埌鍝釜浼氳瘽銆�
+     * 濡傛灉鍓嶇甯︿簡 sessionId 鍒欏仛褰掑睘鏍¢獙骞跺鐢紱鍚﹀垯鑷姩鍒涘缓鏂颁細璇濄��
+     */
     @Override
     public AiChatSession resolveSession(Long userId, Long tenantId, String promptCode, Long sessionId, String titleSeed) {
         ensureIdentity(userId, tenantId);
@@ -87,6 +110,7 @@
                 .setUserId(userId)
                 .setTenantId(tenantId)
                 .setLastMessageTime(now)
+                .setPinned(0)
                 .setStatus(StatusType.ENABLE.val)
                 .setDeleted(0)
                 .setCreateBy(userId)
@@ -97,6 +121,10 @@
         return session;
     }
 
+    /**
+     * 钀藉簱淇濆瓨涓�鏁磋疆瀵硅瘽銆�
+     * 杩欓噷浼氶『搴忓啓鍏ユ湰杞敤鎴锋秷鎭拰妯″瀷鍥炲锛屽苟鍦ㄦ渶鍚庡埛鏂颁細璇濇爣棰樸�佹渶鍚庢椿璺冩椂闂村拰璁板繂鐢诲儚銆�
+     */
     @Override
     public void saveRound(AiChatSession session, Long userId, Long tenantId, List<AiChatMessageDto> memoryMessages, String assistantContent) {
         if (session == null || session.getId() == null) {
@@ -122,8 +150,10 @@
                 .setUpdateBy(userId)
                 .setUpdateTime(now);
         aiChatSessionMapper.updateById(update);
+        refreshMemoryProfile(session.getId(), userId);
     }
 
+    /** 鍒犻櫎鏁翠釜浼氳瘽鍙婂叾娑堟伅銆� */
     @Override
     public void removeSession(Long userId, Long tenantId, Long sessionId) {
         ensureIdentity(userId, tenantId);
@@ -157,6 +187,85 @@
         }
     }
 
+    /** 鏇存柊浼氳瘽鏍囬骞惰繑鍥炴渶鏂颁細璇濇憳瑕併�� */
+    @Override
+    public AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request) {
+        ensureIdentity(userId, tenantId);
+        if (request == null || !StringUtils.hasText(request.getTitle())) {
+            throw new CoolException("浼氳瘽鏍囬涓嶈兘涓虹┖");
+        }
+        AiChatSession session = requireOwnedSession(sessionId, userId, tenantId);
+        Date now = new Date();
+        AiChatSession update = new AiChatSession()
+                .setId(sessionId)
+                .setTitle(buildSessionTitle(request.getTitle()))
+                .setUpdateBy(userId)
+                .setUpdateTime(now);
+        aiChatSessionMapper.updateById(update);
+        return buildSessionDto(requireOwnedSession(sessionId, userId, tenantId));
+    }
+
+    /** 鏇存柊浼氳瘽缃《鐘舵�併�� */
+    @Override
+    public AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request) {
+        ensureIdentity(userId, tenantId);
+        if (request == null || request.getPinned() == null) {
+            throw new CoolException("缃《鐘舵�佷笉鑳戒负绌�");
+        }
+        AiChatSession session = requireOwnedSession(sessionId, userId, tenantId);
+        Date now = new Date();
+        AiChatSession update = new AiChatSession()
+                .setId(sessionId)
+                .setPinned(Boolean.TRUE.equals(request.getPinned()) ? 1 : 0)
+                .setUpdateBy(userId)
+                .setUpdateTime(now);
+        aiChatSessionMapper.updateById(update);
+        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)
@@ -184,12 +293,25 @@
         return session;
     }
 
+    private AiChatSession requireOwnedSession(Long sessionId, Long userId, Long tenantId) {
+        if (sessionId == null) {
+            throw new CoolException("AI 浼氳瘽 ID 涓嶈兘涓虹┖");
+        }
+        AiChatSession session = aiChatSessionMapper.selectOne(new LambdaQueryWrapper<AiChatSession>()
+                .eq(AiChatSession::getId, sessionId)
+                .eq(AiChatSession::getUserId, userId)
+                .eq(AiChatSession::getTenantId, tenantId)
+                .eq(AiChatSession::getDeleted, 0)
+                .eq(AiChatSession::getStatus, StatusType.ENABLE.val)
+                .last("limit 1"));
+        if (session == null) {
+            throw new CoolException("AI 浼氳瘽涓嶅瓨鍦ㄦ垨鏃犳潈璁块棶");
+        }
+        return session;
+    }
+
     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();
         }
@@ -207,6 +329,7 @@
     }
 
     private List<AiChatMessageDto> normalizeMessages(List<AiChatMessageDto> memoryMessages) {
+        /** 娓呮礂鍓嶇涓婁紶鐨勫唴瀛樻秷鎭紝鍙厑璁� user/assistant 涓ょ被瑙掕壊钀藉簱銆� */
         List<AiChatMessageDto> normalized = new ArrayList<>();
         if (Cools.isEmpty(memoryMessages)) {
             return normalized;
@@ -227,6 +350,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)
@@ -243,6 +374,7 @@
                 .setSeqNo(seqNo)
                 .setRole(role)
                 .setContent(content)
+                .setContentLength(content == null ? 0 : content.length())
                 .setUserId(userId)
                 .setTenantId(tenantId)
                 .setDeleted(0)
@@ -263,11 +395,181 @@
     }
 
     private String buildSessionTitle(String titleSeed) {
+        /**
+         * 鎶婇杞敤鎴烽棶棰樺帇缂╂垚閫傚悎浣滀负浼氳瘽鏍囬鐨勭煭鎽樿銆�
+         * 杩欓噷浼氬幓鎺夋崲琛屻�佽繛缁┖鐧斤紝骞朵紭鍏堝湪鑷劧璇箟鏂偣澶勬埅鏂��
+         */
         if (!StringUtils.hasText(titleSeed)) {
             throw new CoolException("AI 浼氳瘽鏍囬涓嶈兘涓虹┖");
         }
-        String title = titleSeed.trim().replace("\r", " ").replace("\n", " ");
-        return title.length() > 60 ? title.substring(0, 60) : title;
+        String title = titleSeed.trim()
+                .replace("\r", " ")
+                .replace("\n", " ")
+                .replaceAll("\\s+", " ");
+        int punctuationIndex = findSummaryBreakIndex(title);
+        if (punctuationIndex > 0) {
+            title = title.substring(0, punctuationIndex).trim();
+        }
+        return title.length() > 48 ? title.substring(0, 48) : title;
+    }
+
+    private int findSummaryBreakIndex(String title) {
+        String[] separators = {"銆�", "锛�", "锛�", ".", "!", "?"};
+        int result = -1;
+        for (String separator : separators) {
+            int index = title.indexOf(separator);
+            if (index > 0 && (result < 0 || index < result)) {
+                result = index;
+            }
+        }
+        return result;
+    }
+
+    private AiChatSessionDto buildSessionDto(AiChatSession session) {
+        AiChatMessage lastMessage = aiChatMessageMapper.selectOne(new LambdaQueryWrapper<AiChatMessage>()
+                .eq(AiChatMessage::getSessionId, session.getId())
+                .eq(AiChatMessage::getDeleted, 0)
+                .orderByDesc(AiChatMessage::getSeqNo)
+                .orderByDesc(AiChatMessage::getId)
+                .last("limit 1"));
+        return AiChatSessionDto.builder()
+                .sessionId(session.getId())
+                .title(session.getTitle())
+                .promptCode(session.getPromptCode())
+                .pinned(session.getPinned() != null && session.getPinned() == 1)
+                .lastMessagePreview(buildLastMessagePreview(lastMessage))
+                .lastMessageTime(session.getLastMessageTime())
+                .build();
+    }
+
+    private String buildLastMessagePreview(AiChatMessage message) {
+        if (message == null || !StringUtils.hasText(message.getContent())) {
+            return null;
+        }
+        String preview = message.getContent().trim()
+                .replace("\r", " ")
+                .replace("\n", " ")
+                .replaceAll("\\s+", " ");
+        String prefix = "assistant".equalsIgnoreCase(message.getRole()) ? "AI: " : "浣�: ";
+        String normalized = prefix + preview;
+        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) {

--
Gitblit v1.9.1