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