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-admin/src/layout/AiChatDrawer.jsx |  221 +++++++++++++++++++++++++++++++++++++++++++++++++++++--
 1 files changed, 212 insertions(+), 9 deletions(-)

diff --git a/rsf-admin/src/layout/AiChatDrawer.jsx b/rsf-admin/src/layout/AiChatDrawer.jsx
index d837069..4b9a69e 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,13 @@
 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 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 { clearAiSessionMemory, getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, retainAiSessionLatestRound, streamAiChat } from "@/api/ai/chat";
 
 const DEFAULT_PROMPT_CODE = "home.default";
 
@@ -51,14 +61,20 @@
     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;
 
     const runtimeSummary = useMemo(() => {
         return {
+            requestId: runtime?.requestId || "--",
             promptName: runtime?.promptName || "--",
             model: runtime?.model || "--",
             mountedMcpCount: runtime?.mountedMcpCount ?? 0,
+            recentMessageCount: runtime?.recentMessageCount ?? 0,
+            hasSummary: !!runtime?.memorySummary,
+            hasFacts: !!runtime?.memoryFacts,
         };
     }, [runtime]);
 
@@ -77,7 +93,7 @@
     const initializeDrawer = async (targetSessionId = null) => {
         await Promise.all([
             loadRuntime(targetSessionId),
-            loadSessions(),
+            loadSessions(sessionKeyword),
         ]);
     };
 
@@ -99,9 +115,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 浼氳瘽鍒楄〃澶辫触";
@@ -119,6 +135,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) {
@@ -139,9 +165,88 @@
                 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" });
+        }
+    };
+
+    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" });
         }
@@ -234,8 +339,9 @@
                         }
                         if (eventName === "error") {
                             const message = payload?.message || "AI 瀵硅瘽澶辫触";
-                            setDrawerError(message);
-                            notify(message, { type: "error" });
+                            const displayMessage = payload?.requestId ? `${message} [${payload.requestId}]` : message;
+                            setDrawerError(displayMessage);
+                            notify(displayMessage, { type: "error" });
                         }
                     },
                 }
@@ -252,7 +358,7 @@
             if (completed) {
                 await Promise.all([
                     loadRuntime(completedSessionId),
-                    loadSessions(),
+                    loadSessions(sessionKeyword),
                 ]);
             }
         }
@@ -311,6 +417,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}>
@@ -330,16 +447,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"
@@ -363,11 +505,15 @@
                     <Box flex={1} display="flex" flexDirection="column" minHeight={0}>
                         <Box px={2} py={1.5}>
                             <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
+                                <Chip size="small" label={`Req: ${runtimeSummary.requestId}`} />
                                 <Chip size="small" label={`Session: ${sessionId || "--"}`} />
                                 <Chip size="small" label={`Prompt: ${runtimeSummary.promptName}`} />
                                 <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) => (
@@ -381,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 杩愯鏃朵俊鎭�...
@@ -437,6 +616,11 @@
                         <Divider />
 
                         <Box px={2} py={1.5}>
+                            {usage?.elapsedMs != null && (
+                                <Typography variant="caption" color="text.secondary" display="block" mb={0.5}>
+                                    Elapsed: {usage.elapsedMs} ms{usage?.firstTokenLatencyMs != null ? ` / First token: ${usage.firstTokenLatencyMs} ms` : ""}
+                                </Typography>
+                            )}
                             {usage?.totalTokens != null && (
                                 <Typography variant="caption" color="text.secondary" display="block" mb={1}>
                                     Tokens: prompt {usage?.promptTokens ?? 0} / completion {usage?.completionTokens ?? 0} / total {usage?.totalTokens ?? 0}
@@ -468,6 +652,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>
     );
 };

--
Gitblit v1.9.1