From 5e40dee0e0a4e4cff4a1aafca2444f61c39cbf32 Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 19 三月 2026 11:17:14 +0800
Subject: [PATCH] #AI.会话能力增强

---
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionRenameRequest.java       |    9 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatMemoryService.java          |    8 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/AiChatService.java                |    8 +
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatMemoryServiceImpl.java |  119 +++++++++++++++-
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionDto.java                 |    4 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/entity/AiChatSession.java                 |    3 
 rsf-server/src/main/java/com/vincent/rsf/server/ai/service/impl/AiChatServiceImpl.java       |   16 ++
 rsf-admin/src/api/ai/chat.js                                                                 |   22 ++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/controller/AiChatController.java          |   19 ++
 version/db/ai_feature.sql                                                                    |   17 ++
 rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionPinRequest.java          |    9 +
 rsf-admin/src/layout/AiChatDrawer.jsx                                                        |  132 +++++++++++++++++-
 12 files changed, 342 insertions(+), 24 deletions(-)

diff --git a/rsf-admin/src/api/ai/chat.js b/rsf-admin/src/api/ai/chat.js
index b11da93..939a2d0 100644
--- a/rsf-admin/src/api/ai/chat.js
+++ b/rsf-admin/src/api/ai/chat.js
@@ -13,9 +13,9 @@
     throw new Error(msg || "鑾峰彇 AI 杩愯鏃朵俊鎭け璐�");
 };
 
-export const getAiSessions = async (promptCode = "home.default") => {
+export const getAiSessions = async (promptCode = "home.default", keyword = "") => {
     const res = await request.get("ai/chat/sessions", {
-        params: { promptCode },
+        params: { promptCode, keyword },
     });
     const { code, msg, data } = res.data;
     if (code === 200) {
@@ -33,6 +33,24 @@
     throw new Error(msg || "鍒犻櫎 AI 浼氳瘽澶辫触");
 };
 
+export const renameAiSession = async (sessionId, title) => {
+    const res = await request.post(`ai/chat/session/rename/${sessionId}`, { title });
+    const { code, msg, data } = res.data;
+    if (code === 200) {
+        return data;
+    }
+    throw new Error(msg || "閲嶅懡鍚� AI 浼氳瘽澶辫触");
+};
+
+export const pinAiSession = async (sessionId, pinned) => {
+    const res = await request.post(`ai/chat/session/pin/${sessionId}`, { pinned });
+    const { code, msg, data } = res.data;
+    if (code === 200) {
+        return data;
+    }
+    throw new Error(msg || "鏇存柊 AI 浼氳瘽缃《鐘舵�佸け璐�");
+};
+
 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 c86e221..e8e2a07 100644
--- a/rsf-admin/src/layout/AiChatDrawer.jsx
+++ b/rsf-admin/src/layout/AiChatDrawer.jsx
@@ -6,6 +6,10 @@
     Box,
     Button,
     Chip,
+    Dialog,
+    DialogActions,
+    DialogContent,
+    DialogTitle,
     Divider,
     Drawer,
     IconButton,
@@ -26,7 +30,11 @@
 import CloseIcon from "@mui/icons-material/Close";
 import AddCommentOutlinedIcon from "@mui/icons-material/AddCommentOutlined";
 import DeleteOutlineOutlinedIcon from "@mui/icons-material/DeleteOutlineOutlined";
-import { getAiRuntime, getAiSessions, removeAiSession, streamAiChat } from "@/api/ai/chat";
+import EditOutlinedIcon from "@mui/icons-material/EditOutlined";
+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";
 
 const DEFAULT_PROMPT_CODE = "home.default";
 
@@ -51,6 +59,8 @@
     const [streaming, setStreaming] = useState(false);
     const [usage, setUsage] = useState(null);
     const [drawerError, setDrawerError] = useState("");
+    const [sessionKeyword, setSessionKeyword] = useState("");
+    const [renameDialog, setRenameDialog] = useState({ open: false, sessionId: null, title: "" });
 
     const promptCode = runtime?.promptCode || DEFAULT_PROMPT_CODE;
 
@@ -78,7 +88,7 @@
     const initializeDrawer = async (targetSessionId = null) => {
         await Promise.all([
             loadRuntime(targetSessionId),
-            loadSessions(),
+            loadSessions(sessionKeyword),
         ]);
     };
 
@@ -100,9 +110,9 @@
         }
     };
 
-    const loadSessions = async () => {
+    const loadSessions = async (keyword = sessionKeyword) => {
         try {
-            const data = await getAiSessions(DEFAULT_PROMPT_CODE);
+            const data = await getAiSessions(DEFAULT_PROMPT_CODE, keyword);
             setSessions(data);
         } catch (error) {
             const message = error.message || "鑾峰彇 AI 浼氳瘽鍒楄〃澶辫触";
@@ -120,6 +130,16 @@
         setUsage(null);
         setDrawerError("");
     };
+
+    useEffect(() => {
+        if (!open) {
+            return;
+        }
+        const timer = window.setTimeout(() => {
+            loadSessions(sessionKeyword);
+        }, 250);
+        return () => window.clearTimeout(timer);
+    }, [sessionKeyword, open]);
 
     const handleSwitchSession = async (targetSessionId) => {
         if (streaming || targetSessionId === sessionId) {
@@ -140,9 +160,52 @@
                 startNewSession();
                 await loadRuntime(null);
             }
-            await loadSessions();
+            await loadSessions(sessionKeyword);
         } catch (error) {
             const message = error.message || "鍒犻櫎 AI 浼氳瘽澶辫触";
+            setDrawerError(message);
+            notify(message, { type: "error" });
+        }
+    };
+
+    const handlePinSession = async (targetSessionId, pinned) => {
+        if (streaming || !targetSessionId) {
+            return;
+        }
+        try {
+            await pinAiSession(targetSessionId, pinned);
+            notify(pinned ? "浼氳瘽宸茬疆椤�" : "浼氳瘽宸插彇娑堢疆椤�");
+            await loadSessions(sessionKeyword);
+        } catch (error) {
+            const message = error.message || "鏇存柊浼氳瘽缃《鐘舵�佸け璐�";
+            setDrawerError(message);
+            notify(message, { type: "error" });
+        }
+    };
+
+    const openRenameDialog = (item) => {
+        setRenameDialog({
+            open: true,
+            sessionId: item?.sessionId || null,
+            title: item?.title || "",
+        });
+    };
+
+    const closeRenameDialog = () => {
+        setRenameDialog({ open: false, sessionId: null, title: "" });
+    };
+
+    const handleRenameSubmit = async () => {
+        if (streaming || !renameDialog.sessionId) {
+            return;
+        }
+        try {
+            await renameAiSession(renameDialog.sessionId, renameDialog.title);
+            notify("浼氳瘽宸查噸鍛藉悕");
+            closeRenameDialog();
+            await loadSessions(sessionKeyword);
+        } catch (error) {
+            const message = error.message || "閲嶅懡鍚嶄細璇濆け璐�";
             setDrawerError(message);
             notify(message, { type: "error" });
         }
@@ -254,7 +317,7 @@
             if (completed) {
                 await Promise.all([
                     loadRuntime(completedSessionId),
-                    loadSessions(),
+                    loadSessions(sessionKeyword),
                 ]);
             }
         }
@@ -313,6 +376,17 @@
                                     鏂板缓浼氳瘽
                                 </Button>
                             </Stack>
+                            <TextField
+                                value={sessionKeyword}
+                                onChange={(event) => setSessionKeyword(event.target.value)}
+                                fullWidth
+                                size="small"
+                                placeholder="鎼滅储浼氳瘽鏍囬"
+                                InputProps={{
+                                    startAdornment: <SearchOutlinedIcon fontSize="small" sx={{ mr: 1, color: "text.secondary" }} />,
+                                }}
+                                sx={{ mb: 1.25 }}
+                            />
                             <Paper variant="outlined" sx={{ overflow: "hidden" }}>
                                 {!sessions.length ? (
                                     <Box px={1.5} py={1.25}>
@@ -332,16 +406,41 @@
                                             >
                                                 <ListItemText
                                                     primary={item.title || `浼氳瘽 ${item.sessionId}`}
-                                                    secondary={item.lastMessageTime || `Session ${item.sessionId}`}
+                                                    secondary={item.lastMessagePreview || item.lastMessageTime || `Session ${item.sessionId}`}
                                                     primaryTypographyProps={{
                                                         noWrap: true,
                                                         fontSize: 14,
+                                                        fontWeight: item.pinned ? 700 : 400,
                                                     }}
                                                     secondaryTypographyProps={{
                                                         noWrap: true,
                                                         fontSize: 12,
                                                     }}
                                                 />
+                                                <IconButton
+                                                    size="small"
+                                                    edge="end"
+                                                    disabled={streaming}
+                                                    onClick={(event) => {
+                                                        event.stopPropagation();
+                                                        handlePinSession(item.sessionId, !item.pinned);
+                                                    }}
+                                                    title={item.pinned ? "鍙栨秷缃《" : "缃《浼氳瘽"}
+                                                >
+                                                    {item.pinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
+                                                </IconButton>
+                                                <IconButton
+                                                    size="small"
+                                                    edge="end"
+                                                    disabled={streaming}
+                                                    onClick={(event) => {
+                                                        event.stopPropagation();
+                                                        openRenameDialog(item);
+                                                    }}
+                                                    title="閲嶅懡鍚嶄細璇�"
+                                                >
+                                                    <EditOutlinedIcon fontSize="small" />
+                                                </IconButton>
                                                 <IconButton
                                                     size="small"
                                                     edge="end"
@@ -476,6 +575,25 @@
                     </Box>
                 </Box>
             </Box>
+            <Dialog open={renameDialog.open} onClose={closeRenameDialog} fullWidth maxWidth="xs">
+                <DialogTitle>閲嶅懡鍚嶄細璇�</DialogTitle>
+                <DialogContent>
+                    <TextField
+                        value={renameDialog.title}
+                        onChange={(event) => setRenameDialog((prev) => ({ ...prev, title: event.target.value }))}
+                        autoFocus
+                        margin="dense"
+                        label="浼氳瘽鏍囬"
+                        fullWidth
+                    />
+                </DialogContent>
+                <DialogActions>
+                    <Button onClick={closeRenameDialog}>鍙栨秷</Button>
+                    <Button onClick={handleRenameSubmit} variant="contained" disabled={streaming || !renameDialog.title.trim()}>
+                        淇濆瓨
+                    </Button>
+                </DialogActions>
+            </Dialog>
         </Drawer>
     );
 };
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 64e9f06..d31b241 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
@@ -1,6 +1,8 @@
 package com.vincent.rsf.server.ai.controller;
 
 import com.vincent.rsf.framework.common.R;
+import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
 import com.vincent.rsf.server.ai.dto.AiChatRequest;
 import com.vincent.rsf.server.ai.service.AiChatService;
 import com.vincent.rsf.server.system.controller.BaseController;
@@ -30,8 +32,9 @@
 
     @PreAuthorize("isAuthenticated()")
     @GetMapping("/ai/chat/sessions")
-    public R sessions(@RequestParam(required = false) String promptCode) {
-        return R.ok().add(aiChatService.listSessions(promptCode, getLoginUserId(), getTenantId()));
+    public R sessions(@RequestParam(required = false) String promptCode,
+                      @RequestParam(required = false) String keyword) {
+        return R.ok().add(aiChatService.listSessions(promptCode, keyword, getLoginUserId(), getTenantId()));
     }
 
     @PreAuthorize("isAuthenticated()")
@@ -42,6 +45,18 @@
     }
 
     @PreAuthorize("isAuthenticated()")
+    @PostMapping("/ai/chat/session/rename/{sessionId}")
+    public R renameSession(@PathVariable Long sessionId, @RequestBody AiChatSessionRenameRequest request) {
+        return R.ok("Update Success").add(aiChatService.renameSession(sessionId, request, getLoginUserId(), getTenantId()));
+    }
+
+    @PreAuthorize("isAuthenticated()")
+    @PostMapping("/ai/chat/session/pin/{sessionId}")
+    public R pinSession(@PathVariable Long sessionId, @RequestBody AiChatSessionPinRequest request) {
+        return R.ok("Update Success").add(aiChatService.pinSession(sessionId, request, getLoginUserId(), getTenantId()));
+    }
+
+    @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/AiChatSessionDto.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionDto.java
index a54b23c..0846691 100644
--- a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionDto.java
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionDto.java
@@ -16,6 +16,10 @@
 
     private String promptCode;
 
+    private Boolean pinned;
+
+    private String lastMessagePreview;
+
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date lastMessageTime;
 }
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionPinRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionPinRequest.java
new file mode 100644
index 0000000..7d38ad6
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionPinRequest.java
@@ -0,0 +1,9 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+@Data
+public class AiChatSessionPinRequest {
+
+    private Boolean pinned;
+}
diff --git a/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionRenameRequest.java b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionRenameRequest.java
new file mode 100644
index 0000000..a90addc
--- /dev/null
+++ b/rsf-server/src/main/java/com/vincent/rsf/server/ai/dto/AiChatSessionRenameRequest.java
@@ -0,0 +1,9 @@
+package com.vincent.rsf.server.ai.dto;
+
+import lombok.Data;
+
+@Data
+public class AiChatSessionRenameRequest {
+
+    private String title;
+}
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 18a8bab..67188a8 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,9 @@
     @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
     private Date lastMessageTime;
 
+    @ApiModelProperty(value = "鏄惁缃《")
+    private Integer pinned;
+
     @ApiModelProperty(value = "鐘舵��")
     private Integer status;
 
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 404f30b..db7af54 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
@@ -3,6 +3,8 @@
 import com.vincent.rsf.server.ai.dto.AiChatMemoryDto;
 import com.vincent.rsf.server.ai.dto.AiChatMessageDto;
 import com.vincent.rsf.server.ai.dto.AiChatSessionDto;
+import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
 import com.vincent.rsf.server.ai.entity.AiChatSession;
 
 import java.util.List;
@@ -11,11 +13,15 @@
 
     AiChatMemoryDto getMemory(Long userId, Long tenantId, String promptCode, Long sessionId);
 
-    List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode);
+    List<AiChatSessionDto> listSessions(Long userId, Long tenantId, String promptCode, String keyword);
 
     AiChatSession resolveSession(Long userId, Long tenantId, String promptCode, Long sessionId, String titleSeed);
 
     void saveRound(AiChatSession session, Long userId, Long tenantId, List<AiChatMessageDto> memoryMessages, String assistantContent);
 
     void removeSession(Long userId, Long tenantId, Long sessionId);
+
+    AiChatSessionDto renameSession(Long userId, Long tenantId, Long sessionId, AiChatSessionRenameRequest request);
+
+    AiChatSessionDto pinSession(Long userId, Long tenantId, Long sessionId, AiChatSessionPinRequest request);
 }
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 27ecf4f..ec4d2a9 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
@@ -3,6 +3,8 @@
 import com.vincent.rsf.server.ai.dto.AiChatRequest;
 import com.vincent.rsf.server.ai.dto.AiChatRuntimeDto;
 import com.vincent.rsf.server.ai.dto.AiChatSessionDto;
+import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
 import org.springframework.web.servlet.mvc.method.annotation.SseEmitter;
 
 import java.util.List;
@@ -11,9 +13,13 @@
 
     AiChatRuntimeDto getRuntime(String promptCode, Long sessionId, Long userId, Long tenantId);
 
-    List<AiChatSessionDto> listSessions(String promptCode, Long userId, Long tenantId);
+    List<AiChatSessionDto> listSessions(String promptCode, String keyword, Long userId, Long tenantId);
 
     SseEmitter stream(AiChatRequest request, Long userId, Long tenantId);
 
     void removeSession(Long sessionId, Long userId, Long tenantId);
+
+    AiChatSessionDto renameSession(Long sessionId, AiChatSessionRenameRequest request, Long userId, Long tenantId);
+
+    AiChatSessionDto pinSession(Long sessionId, AiChatSessionPinRequest request, 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 b663a6a..c0a4a61 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
@@ -5,6 +5,8 @@
 import com.vincent.rsf.framework.exception.CoolException;
 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;
@@ -19,6 +21,7 @@
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
+import java.util.Locale;
 
 @Service
 @RequiredArgsConstructor
@@ -47,7 +50,7 @@
     }
 
     @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 +59,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,12 +68,7 @@
         }
         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;
     }
@@ -87,6 +87,7 @@
                 .setUserId(userId)
                 .setTenantId(tenantId)
                 .setLastMessageTime(now)
+                .setPinned(0)
                 .setStatus(StatusType.ENABLE.val)
                 .setDeleted(0)
                 .setCreateBy(userId)
@@ -157,6 +158,40 @@
         }
     }
 
+    @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));
+    }
+
     private AiChatSession findLatestSession(Long userId, Long tenantId, String promptCode) {
         return aiChatSessionMapper.selectOne(new LambdaQueryWrapper<AiChatSession>()
                 .eq(AiChatSession::getUserId, userId)
@@ -175,6 +210,23 @@
                 .eq(AiChatSession::getUserId, userId)
                 .eq(AiChatSession::getTenantId, tenantId)
                 .eq(AiChatSession::getPromptCode, promptCode)
+                .eq(AiChatSession::getDeleted, 0)
+                .eq(AiChatSession::getStatus, StatusType.ENABLE.val)
+                .last("limit 1"));
+        if (session == null) {
+            throw new CoolException("AI 浼氳瘽涓嶅瓨鍦ㄦ垨鏃犳潈璁块棶");
+        }
+        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"));
@@ -266,8 +318,57 @@
         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 ensureIdentity(Long userId, Long tenantId) {
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 05dc09b..280914f 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
@@ -12,6 +12,8 @@
 import com.vincent.rsf.server.ai.dto.AiChatRuntimeDto;
 import com.vincent.rsf.server.ai.dto.AiChatStatusDto;
 import com.vincent.rsf.server.ai.dto.AiChatSessionDto;
+import com.vincent.rsf.server.ai.dto.AiChatSessionPinRequest;
+import com.vincent.rsf.server.ai.dto.AiChatSessionRenameRequest;
 import com.vincent.rsf.server.ai.dto.AiResolvedConfig;
 import com.vincent.rsf.server.ai.entity.AiParam;
 import com.vincent.rsf.server.ai.entity.AiPrompt;
@@ -98,9 +100,9 @@
     }
 
     @Override
-    public List<AiChatSessionDto> listSessions(String promptCode, Long userId, Long tenantId) {
+    public List<AiChatSessionDto> listSessions(String promptCode, String keyword, Long userId, Long tenantId) {
         AiResolvedConfig config = aiConfigResolverService.resolve(promptCode, tenantId);
-        return aiChatMemoryService.listSessions(userId, tenantId, config.getPromptCode());
+        return aiChatMemoryService.listSessions(userId, tenantId, config.getPromptCode(), keyword);
     }
 
     @Override
@@ -109,6 +111,16 @@
     }
 
     @Override
+    public AiChatSessionDto renameSession(Long sessionId, AiChatSessionRenameRequest request, Long userId, Long tenantId) {
+        return aiChatMemoryService.renameSession(userId, tenantId, sessionId, request);
+    }
+
+    @Override
+    public AiChatSessionDto pinSession(Long sessionId, AiChatSessionPinRequest request, Long userId, Long tenantId) {
+        return aiChatMemoryService.pinSession(userId, tenantId, sessionId, request);
+    }
+
+    @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);
diff --git a/version/db/ai_feature.sql b/version/db/ai_feature.sql
index d5cd06f..529ea30 100644
--- a/version/db/ai_feature.sql
+++ b/version/db/ai_feature.sql
@@ -76,6 +76,7 @@
   `user_id` bigint(20) NOT NULL COMMENT '鐢ㄦ埛 ID',
   `tenant_id` bigint(20) DEFAULT NULL COMMENT '绉熸埛',
   `last_message_time` datetime DEFAULT NULL COMMENT '鏈�鍚庢秷鎭椂闂�',
+  `pinned` tinyint(1) DEFAULT '0' COMMENT '鏄惁缃《',
   `status` int(11) DEFAULT '1' COMMENT '鐘舵��',
   `deleted` int(11) DEFAULT '0' COMMENT '鍒犻櫎鏍囪',
   `create_time` datetime DEFAULT NULL COMMENT '鍒涘缓鏃堕棿',
@@ -117,6 +118,22 @@
 EXECUTE builtin_code_stmt;
 DEALLOCATE PREPARE builtin_code_stmt;
 
+SET @chat_session_pinned_exists := (
+  SELECT COUNT(1)
+  FROM `information_schema`.`COLUMNS`
+  WHERE `TABLE_SCHEMA` = DATABASE()
+    AND `TABLE_NAME` = 'sys_ai_chat_session'
+    AND `COLUMN_NAME` = 'pinned'
+);
+SET @chat_session_pinned_sql := IF(
+  @chat_session_pinned_exists = 0,
+  'ALTER TABLE `sys_ai_chat_session` ADD COLUMN `pinned` tinyint(1) DEFAULT ''0'' COMMENT ''鏄惁缃《'' AFTER `last_message_time`',
+  'SELECT 1'
+);
+PREPARE chat_session_pinned_stmt FROM @chat_session_pinned_sql;
+EXECUTE chat_session_pinned_stmt;
+DEALLOCATE PREPARE chat_session_pinned_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