zhou zhou
5 天以前 40905cbd04c2e332cd4bc2b9e0c5b3e1da9cccfa
rsf-admin/src/layout/AiChatDrawer.jsx
@@ -1,22 +1,32 @@
import React, { useEffect, useMemo, useRef, useState } from "react";
import { useLocation, useNavigate } from "react-router-dom";
import { useNotify } from "react-admin";
import { useNotify, useTranslate } from "react-admin";
import ReactMarkdown from "react-markdown";
import remarkGfm from "remark-gfm";
import {
    Alert,
    Box,
    Button,
    Chip,
    Collapse,
    Dialog,
    DialogActions,
    DialogContent,
    DialogTitle,
    Divider,
    Drawer,
    IconButton,
    List,
    ListItemButton,
    ListItemText,
    MenuItem,
    Paper,
    Stack,
    TextField,
    Typography,
} from "@mui/material";
import { Light as SyntaxHighlighter } from "react-syntax-highlighter";
import { atomOneLight } from "react-syntax-highlighter/dist/esm/styles/hljs";
import SmartToyOutlinedIcon from "@mui/icons-material/SmartToyOutlined";
import SendRoundedIcon from "@mui/icons-material/SendRounded";
import StopCircleOutlinedIcon from "@mui/icons-material/StopCircleOutlined";
@@ -26,44 +36,252 @@
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 ExpandMoreOutlinedIcon from "@mui/icons-material/ExpandMoreOutlined";
import ExpandLessOutlinedIcon from "@mui/icons-material/ExpandLessOutlined";
import { clearAiSessionMemory, getAiRuntime, getAiSessions, pinAiSession, removeAiSession, renameAiSession, retainAiSessionLatestRound, streamAiChat } from "@/api/ai/chat";
const DEFAULT_PROMPT_CODE = "home.default";
const AI_CHAT_DRAWER_Z_INDEX = 1400;
const AI_CHAT_DIALOG_Z_INDEX = AI_CHAT_DRAWER_Z_INDEX + 20;
const quickLinks = [
    { label: "AI 参数", path: "/aiParam", icon: <SettingsSuggestOutlinedIcon fontSize="small" /> },
    { label: "Prompt", path: "/aiPrompt", icon: <PsychologyAltOutlinedIcon fontSize="small" /> },
    { label: "MCP", path: "/aiMcpMount", icon: <CableOutlinedIcon fontSize="small" /> },
];
const normalizeMarkdownContent = (content) => {
    if (!content) {
        return "";
    }
    return content
        .replace(/\r\n/g, "\n")
        .replace(/(\n[-*] .+)\n{2,}(?=[-*] )/g, "$1\n")
        .replace(/(\n\d+\. .+)\n{2,}(?=\d+\. )/g, "$1\n")
        .replace(/([^\n])\n{3,}/g, "$1\n\n");
};
const markdownSx = {
    width: "100%",
    fontSize: "0.84rem",
    "& > *:first-of-type": {
        mt: 0,
    },
    "& > *:last-child": {
        mb: 0,
    },
    "& p": {
        m: 0,
        lineHeight: 1.28,
    },
    "& p + p": {
        mt: 0.1,
    },
    "& h1, & h2, & h3, & h4, & h5, & h6": {
        mt: 0.25,
        mb: 0.04,
        lineHeight: 1.16,
        fontWeight: 700,
    },
    "& h1": {
        fontSize: "0.96rem",
    },
    "& h2": {
        fontSize: "0.92rem",
    },
    "& h3": {
        fontSize: "0.89rem",
    },
    "& ul, & ol": {
        my: 0.02,
        pl: 1.3,
    },
    "& ul > li, & ol > li": {
        lineHeight: 1.2,
    },
    "& li + li": {
        mt: 0,
    },
    "& li > p": {
        display: "inline",
        m: 0,
        lineHeight: "inherit",
    },
    "& li::marker": {
        fontSize: "0.78rem",
    },
    "& blockquote": {
        m: 0,
        mt: 0.18,
        px: 0.7,
        py: 0.25,
        borderLeft: "3px solid rgba(25, 118, 210, 0.35)",
        bgcolor: "rgba(25, 118, 210, 0.06)",
    },
    "& hr": {
        my: 0.25,
        border: 0,
        borderTop: "1px solid rgba(0, 0, 0, 0.12)",
    },
    "& table": {
        width: "100%",
        borderCollapse: "collapse",
        mt: 0.18,
        mb: 0.04,
        fontSize: "0.78rem",
    },
    "& th, & td": {
        border: "1px solid rgba(0, 0, 0, 0.12)",
        px: 0.4,
        py: 0.22,
        textAlign: "left",
        verticalAlign: "top",
    },
    "& th": {
        bgcolor: "rgba(0, 0, 0, 0.04)",
        fontWeight: 700,
    },
    "& a": {
        color: "primary.main",
        textDecoration: "underline",
        wordBreak: "break-all",
    },
    "& img": {
        maxWidth: "100%",
        borderRadius: 1.5,
    },
    "& code": {
        fontFamily: "'Consolas', 'Monaco', monospace",
    },
};
const AiMarkdownContent = ({ content }) => (
    <Box sx={markdownSx}>
        <ReactMarkdown
            remarkPlugins={[remarkGfm]}
            components={{
                p: ({ children }) => <Typography variant="body2">{children}</Typography>,
                li: ({ children }) => <Box component="li" sx={{ fontSize: "0.875rem" }}>{children}</Box>,
                blockquote: ({ children }) => <Box component="blockquote">{children}</Box>,
                a: ({ href, children }) => (
                    <Box
                        component="a"
                        href={href}
                        target="_blank"
                        rel="noreferrer"
                    >
                        {children}
                    </Box>
                ),
                code({ inline, className, children, ...props }) {
                    const match = /language-(\w+)/.exec(className || "");
                    const code = String(children).replace(/\n$/, "");
                    if (!inline) {
                        return (
                            <Box sx={{ mt: 0.7, mb: 0.2, borderRadius: 1.5, overflow: "hidden" }}>
                                <SyntaxHighlighter
                                    language={match?.[1]}
                                    style={atomOneLight}
                                    customStyle={{
                                        margin: 0,
                                        padding: "6px 8px",
                                        borderRadius: 12,
                                        fontSize: "0.74rem",
                                    }}
                                    wrapLongLines
                                    PreTag="div"
                                    {...props}
                                >
                                    {code}
                                </SyntaxHighlighter>
                            </Box>
                        );
                    }
                    return (
                        <Box
                            component="code"
                            sx={{
                                px: 0.45,
                                py: "1px",
                                borderRadius: 0.75,
                                bgcolor: "rgba(0, 0, 0, 0.08)",
                                fontSize: "0.74em",
                            }}
                            {...props}
                        >
                            {children}
                        </Box>
                    );
                },
            }}
        >
            {normalizeMarkdownContent(content)}
        </ReactMarkdown>
    </Box>
);
const AiChatDrawer = ({ open, onClose }) => {
    const navigate = useNavigate();
    const location = useLocation();
    const notify = useNotify();
    const translate = useTranslate();
    const abortRef = useRef(null);
    const messagesContainerRef = useRef(null);
    const messagesBottomRef = useRef(null);
    const [runtime, setRuntime] = useState(null);
    const [selectedAiParamId, setSelectedAiParamId] = useState(null);
    const [sessionId, setSessionId] = useState(null);
    const [sessions, setSessions] = useState([]);
    const [persistedMessages, setPersistedMessages] = useState([]);
    const [messages, setMessages] = useState([]);
    const [traceEvents, setTraceEvents] = useState([]);
    const [expandedTraceIds, setExpandedTraceIds] = useState([]);
    const [input, setInput] = useState("");
    const [loadingRuntime, setLoadingRuntime] = useState(false);
    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 [runtimePanelExpanded, setRuntimePanelExpanded] = useState(false);
    const quickLinks = useMemo(() => ([
        { label: translate("menu.aiParam"), path: "/aiParam", icon: <SettingsSuggestOutlinedIcon fontSize="small" /> },
        { label: translate("menu.aiPrompt"), path: "/aiPrompt", icon: <PsychologyAltOutlinedIcon fontSize="small" /> },
        { label: translate("menu.aiMcpMount"), path: "/aiMcpMount", icon: <CableOutlinedIcon fontSize="small" /> },
    ]), [translate]);
    const promptCode = runtime?.promptCode || DEFAULT_PROMPT_CODE;
    const selectableModelOptions = useMemo(() => {
        if (runtime?.modelOptions?.length) {
            return runtime.modelOptions;
        }
        if (runtime?.model) {
            return [{
                aiParamId: runtime?.aiParamId ?? "CURRENT_MODEL",
                name: runtime.model,
                model: runtime.model,
                active: true,
            }];
        }
        return [];
    }, [runtime]);
    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]);
    useEffect(() => {
        if (open) {
            setRuntimePanelExpanded(false);
            initializeDrawer();
        } else {
            stopStream(false);
@@ -74,37 +292,52 @@
        stopStream(false);
    }, []);
    useEffect(() => {
        if (!open) {
            return;
        }
        const timer = window.requestAnimationFrame(() => {
            scrollMessagesToBottom();
        });
        return () => window.cancelAnimationFrame(timer);
    }, [open, messages, streaming]);
    const initializeDrawer = async (targetSessionId = null) => {
        setTraceEvents([]);
        setExpandedTraceIds([]);
        await Promise.all([
            loadRuntime(targetSessionId),
            loadSessions(),
            loadSessions(sessionKeyword),
        ]);
    };
    const loadRuntime = async (targetSessionId = null) => {
    const loadRuntime = async (targetSessionId = null, targetAiParamId = selectedAiParamId) => {
        setLoadingRuntime(true);
        setDrawerError("");
        try {
            const data = await getAiRuntime(DEFAULT_PROMPT_CODE, targetSessionId);
            const data = await getAiRuntime(DEFAULT_PROMPT_CODE, targetSessionId, targetAiParamId);
            const historyMessages = data?.persistedMessages || [];
            setRuntime(data);
            setSelectedAiParamId(data?.aiParamId ?? null);
            setSessionId(data?.sessionId || null);
            setPersistedMessages(historyMessages);
            setMessages(historyMessages);
            return data;
        } catch (error) {
            const message = error.message || "获取 AI 运行时失败";
            const message = error.message || translate("ai.drawer.runtimeFailed");
            setDrawerError(message);
            return null;
        } finally {
            setLoadingRuntime(false);
        }
    };
    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 会话列表失败";
            const message = error.message || translate("ai.drawer.sessionListFailed");
            setDrawerError(message);
        }
    };
@@ -116,16 +349,48 @@
        setSessionId(null);
        setPersistedMessages([]);
        setMessages([]);
        setTraceEvents([]);
        setExpandedTraceIds([]);
        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) {
            return;
        }
        setUsage(null);
        setTraceEvents([]);
        setExpandedTraceIds([]);
        await loadRuntime(targetSessionId);
    };
    const handleModelChange = async (event) => {
        if (streaming) {
            return;
        }
        const rawValue = event.target.value;
        const nextAiParamId = rawValue === "" ? null : Number(rawValue);
        if (nextAiParamId === selectedAiParamId) {
            return;
        }
        const previousAiParamId = selectedAiParamId;
        setSelectedAiParamId(nextAiParamId);
        const data = await loadRuntime(sessionId, nextAiParamId);
        if (!data) {
            setSelectedAiParamId(previousAiParamId);
            notify(translate("ai.drawer.modelSwitchFailed"), { type: "error" });
        }
    };
    const handleDeleteSession = async (targetSessionId) => {
@@ -134,14 +399,93 @@
        }
        try {
            await removeAiSession(targetSessionId);
            notify("会话已删除");
            notify(translate("ai.drawer.sessionDeleted"));
            if (targetSessionId === sessionId) {
                startNewSession();
                await loadRuntime(null);
            }
            await loadSessions();
            await loadSessions(sessionKeyword);
        } catch (error) {
            const message = error.message || "删除 AI 会话失败";
            const message = error.message || translate("ai.drawer.deleteSessionFailed");
            setDrawerError(message);
            notify(message, { type: "error" });
        }
    };
    const handlePinSession = async (targetSessionId, pinned) => {
        if (streaming || !targetSessionId) {
            return;
        }
        try {
            await pinAiSession(targetSessionId, pinned);
            notify(translate(pinned ? "ai.drawer.pinned" : "ai.drawer.unpinned"));
            await loadSessions(sessionKeyword);
        } catch (error) {
            const message = error.message || translate("ai.drawer.pinFailed");
            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(translate("ai.drawer.renamed"));
            closeRenameDialog();
            await loadSessions(sessionKeyword);
        } catch (error) {
            const message = error.message || translate("ai.drawer.renameFailed");
            setDrawerError(message);
            notify(message, { type: "error" });
        }
    };
    const handleClearMemory = async () => {
        if (streaming || !sessionId) {
            return;
        }
        try {
            await clearAiSessionMemory(sessionId);
            notify(translate("ai.drawer.memoryCleared"));
            await Promise.all([
                loadRuntime(sessionId),
                loadSessions(sessionKeyword),
            ]);
        } catch (error) {
            const message = error.message || translate("ai.drawer.clearMemoryFailed");
            setDrawerError(message);
            notify(message, { type: "error" });
        }
    };
    const handleRetainLatestRound = async () => {
        if (streaming || !sessionId) {
            return;
        }
        try {
            await retainAiSessionLatestRound(sessionId);
            notify(translate("ai.drawer.retainLatestRoundSuccess"));
            await Promise.all([
                loadRuntime(sessionId),
                loadSessions(sessionKeyword),
            ]);
        } catch (error) {
            const message = error.message || translate("ai.drawer.retainLatestRoundFailed");
            setDrawerError(message);
            notify(message, { type: "error" });
        }
@@ -153,8 +497,18 @@
            abortRef.current = null;
            setStreaming(false);
            if (showTip) {
                notify("已停止当前对话输出");
                notify(translate("ai.drawer.stopSuccess"));
            }
        }
    };
    const scrollMessagesToBottom = () => {
        if (messagesBottomRef.current) {
            messagesBottomRef.current.scrollIntoView({ block: "end" });
            return;
        }
        if (messagesContainerRef.current) {
            messagesContainerRef.current.scrollTop = messagesContainerRef.current.scrollHeight;
        }
    };
@@ -183,6 +537,60 @@
        return next;
    };
    const appendTraceEvent = (payload) => {
        if (!payload?.traceId) {
            return;
        }
        setTraceEvents((prev) => {
            const index = prev.findIndex((item) => item.traceId === payload.traceId);
            if (index < 0) {
                return [...prev, payload].sort((left, right) => (
                    (left?.sequence ?? 0) - (right?.sequence ?? 0)
                ));
            }
            const next = [...prev];
            next[index] = { ...next[index], ...payload };
            return next;
        });
    };
    const toggleTraceEventExpanded = (traceId) => {
        if (!traceId) {
            return;
        }
        setExpandedTraceIds((prev) => (
            prev.includes(traceId)
                ? prev.filter((item) => item !== traceId)
                : [...prev, traceId]
        ));
    };
    const getThinkingStatusLabel = (status) => {
        if (status === "COMPLETED") {
            return translate("ai.drawer.thinkingStatusCompleted");
        }
        if (status === "FAILED") {
            return translate("ai.drawer.thinkingStatusFailed");
        }
        if (status === "ABORTED") {
            return translate("ai.drawer.thinkingStatusAborted");
        }
        if (status === "UPDATED") {
            return translate("ai.drawer.thinkingStatusUpdated");
        }
        return translate("ai.drawer.thinkingStatusStarted");
    };
    const getToolStatusLabel = (status) => {
        if (status === "FAILED") {
            return translate("ai.drawer.toolStatusFailed");
        }
        if (status === "COMPLETED") {
            return translate("ai.drawer.toolStatusCompleted");
        }
        return translate("ai.drawer.toolStatusRunning");
    };
    const handleSend = async () => {
        const content = input.trim();
        if (!content || streaming) {
@@ -193,6 +601,8 @@
        setInput("");
        setUsage(null);
        setDrawerError("");
        setTraceEvents([]);
        setExpandedTraceIds([]);
        setMessages(ensureAssistantPlaceholder(nextMessages));
        setStreaming(true);
@@ -201,11 +611,13 @@
        let completed = false;
        let completedSessionId = sessionId;
        let completedAiParamId = selectedAiParamId;
        try {
            await streamAiChat(
                {
                    sessionId,
                    aiParamId: selectedAiParamId,
                    promptCode,
                    messages: memoryMessages,
                    metadata: {
@@ -217,13 +629,18 @@
                    onEvent: (eventName, payload) => {
                        if (eventName === "start") {
                            setRuntime(payload);
                            setSelectedAiParamId(payload?.aiParamId ?? null);
                            if (payload?.sessionId) {
                                setSessionId(payload.sessionId);
                                completedSessionId = payload.sessionId;
                            }
                            completedAiParamId = payload?.aiParamId ?? completedAiParamId;
                        }
                        if (eventName === "delta") {
                            appendAssistantDelta(payload?.content || "");
                        }
                        if (eventName === "trace") {
                            appendTraceEvent(payload);
                        }
                        if (eventName === "done") {
                            setUsage(payload);
@@ -233,16 +650,17 @@
                            }
                        }
                        if (eventName === "error") {
                            const message = payload?.message || "AI 对话失败";
                            setDrawerError(message);
                            notify(message, { type: "error" });
                            const message = payload?.message || translate("ai.drawer.chatFailed");
                            const displayMessage = payload?.requestId ? `${message} [${payload.requestId}]` : message;
                            setDrawerError(displayMessage);
                            notify(displayMessage, { type: "error" });
                        }
                    },
                }
            );
        } catch (error) {
            if (error?.name !== "AbortError") {
                const message = error.message || "AI 对话失败";
                const message = error.message || translate("ai.drawer.chatFailed");
                setDrawerError(message);
                notify(message, { type: "error" });
            }
@@ -251,8 +669,8 @@
            setStreaming(false);
            if (completed) {
                await Promise.all([
                    loadRuntime(completedSessionId),
                    loadSessions(),
                    loadRuntime(completedSessionId, completedAiParamId),
                    loadSessions(sessionKeyword),
                ]);
            }
        }
@@ -272,11 +690,11 @@
            onClose={onClose}
            ModalProps={{ keepMounted: true }}
            sx={{
                zIndex: 1400,
                zIndex: AI_CHAT_DRAWER_Z_INDEX,
                "& .MuiDrawer-paper": {
                    top: 0,
                    height: "100vh",
                    width: { xs: "100vw", md: "50vw" },
                    width: { xs: "100vw", md: "70vw" },
                },
            }}
        >
@@ -284,12 +702,12 @@
                <Stack direction="row" alignItems="center" spacing={1} px={2} py={1.5}>
                    <SmartToyOutlinedIcon color="primary" />
                    <Typography variant="h6" flex={1}>
                        AI 对话
                        {translate("ai.drawer.title")}
                    </Typography>
                    <IconButton size="small" onClick={startNewSession} title="新建会话" disabled={streaming}>
                    <IconButton size="small" onClick={startNewSession} title={translate("ai.drawer.newSession")} disabled={streaming}>
                        <AddCommentOutlinedIcon fontSize="small" />
                    </IconButton>
                    <IconButton size="small" onClick={onClose} title="关闭">
                    <IconButton size="small" onClick={onClose} title={translate("ai.common.close")}>
                        <CloseIcon fontSize="small" />
                    </IconButton>
                </Stack>
@@ -306,16 +724,27 @@
                    >
                        <Box px={2} py={1.5}>
                            <Stack direction="row" alignItems="center" justifyContent="space-between" mb={1}>
                                <Typography variant="subtitle2">会话列表</Typography>
                                <Typography variant="subtitle2">{translate("ai.drawer.sessionList")}</Typography>
                                <Button size="small" onClick={startNewSession} disabled={streaming}>
                                    新建会话
                                    {translate("ai.drawer.newSession")}
                                </Button>
                            </Stack>
                            <TextField
                                value={sessionKeyword}
                                onChange={(event) => setSessionKeyword(event.target.value)}
                                fullWidth
                                size="small"
                                placeholder={translate("ai.drawer.searchPlaceholder")}
                                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}>
                                        <Typography variant="body2" color="text.secondary">
                                            暂无历史会话
                                            {translate("ai.drawer.noSessions")}
                                        </Typography>
                                    </Box>
                                ) : (
@@ -329,11 +758,12 @@
                                                alignItems="flex-start"
                                            >
                                                <ListItemText
                                                    primary={item.title || `会话 ${item.sessionId}`}
                                                    secondary={item.lastMessageTime || `Session ${item.sessionId}`}
                                                    primary={item.title || translate("ai.drawer.sessionTitle", { id: item.sessionId })}
                                                    secondary={item.lastMessagePreview || item.lastMessageTime || translate("ai.drawer.sessionMetric", { id: item.sessionId })}
                                                    primaryTypographyProps={{
                                                        noWrap: true,
                                                        fontSize: 14,
                                                        fontWeight: item.pinned ? 700 : 400,
                                                    }}
                                                    secondaryTypographyProps={{
                                                        noWrap: true,
@@ -346,9 +776,33 @@
                                                    disabled={streaming}
                                                    onClick={(event) => {
                                                        event.stopPropagation();
                                                        handlePinSession(item.sessionId, !item.pinned);
                                                    }}
                                                    title={translate(item.pinned ? "ai.drawer.unpinAction" : "ai.drawer.pinAction")}
                                                >
                                                    {item.pinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
                                                </IconButton>
                                                <IconButton
                                                    size="small"
                                                    edge="end"
                                                    disabled={streaming}
                                                    onClick={(event) => {
                                                        event.stopPropagation();
                                                        openRenameDialog(item);
                                                    }}
                                                    title={translate("ai.drawer.renameAction")}
                                                >
                                                    <EditOutlinedIcon fontSize="small" />
                                                </IconButton>
                                                <IconButton
                                                    size="small"
                                                    edge="end"
                                                    disabled={streaming}
                                                    onClick={(event) => {
                                                        event.stopPropagation();
                                                        handleDeleteSession(item.sessionId);
                                                    }}
                                                    title="删除会话"
                                                    title={translate("ai.drawer.deleteAction")}
                                                >
                                                    <DeleteOutlineOutlinedIcon fontSize="small" />
                                                </IconButton>
@@ -360,31 +814,203 @@
                        </Box>
                    </Box>
                    <Box
                        width={{ xs: "100%", md: 280 }}
                        borderRight={{ xs: "none", md: "1px solid rgba(224, 224, 224, 1)" }}
                        borderBottom={{ xs: "1px solid rgba(224, 224, 224, 1)", md: "none" }}
                        display="flex"
                        flexDirection="column"
                        minHeight={0}
                    >
                        <Box px={2} py={1.5} display="flex" flexDirection="column" minHeight={0}>
                            <Typography variant="subtitle2" mb={1}>
                                {translate("ai.drawer.activityTrace")}
                            </Typography>
                            <Paper variant="outlined" sx={{ flex: 1, minHeight: { xs: 140, md: 0 }, overflow: "hidden", bgcolor: "grey.50" }}>
                                {!traceEvents.length ? (
                                    <Box px={1.5} py={1.25}>
                                        <Typography variant="body2" color="text.secondary">
                                            {translate("ai.drawer.noActivityTrace")}
                                        </Typography>
                                    </Box>
                                ) : (
                                    <Stack spacing={1} sx={{ p: 1.25, maxHeight: { xs: 220, md: "calc(100vh - 180px)" }, overflow: "auto" }}>
                                        {traceEvents.map((item) => (
                                            <Paper
                                                key={item.traceId}
                                                variant="outlined"
                                                sx={{
                                                    p: 1.25,
                                                    bgcolor: item.status === "FAILED"
                                                        ? "error.lighter"
                                                        : item.traceType === "thinking"
                                                            ? "info.lighter"
                                                            : "common.white",
                                                    borderColor: item.status === "FAILED"
                                                        ? "error.light"
                                                        : item.traceType === "thinking"
                                                            ? "info.light"
                                                            : "divider",
                                                }}
                                            >
                                                <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
                                                    <Chip
                                                        size="small"
                                                        variant="outlined"
                                                        color={item.traceType === "thinking" ? "info" : "primary"}
                                                        label={translate(item.traceType === "thinking" ? "ai.drawer.traceTypeThinking" : "ai.drawer.traceTypeTool")}
                                                    />
                                                    <Typography variant="body2" fontWeight={700}>
                                                        {item.traceType === "thinking"
                                                            ? (item.title || translate("ai.drawer.thinkingProcess"))
                                                            : (item.toolName || item.title || translate("ai.drawer.unknownTool"))}
                                                    </Typography>
                                                    <Chip
                                                        size="small"
                                                        color={item.status === "FAILED"
                                                            ? "error"
                                                            : item.status === "COMPLETED"
                                                                ? "success"
                                                                : item.status === "ABORTED"
                                                                    ? "warning"
                                                                    : "info"}
                                                        label={item.traceType === "thinking"
                                                            ? getThinkingStatusLabel(item.status)
                                                            : getToolStatusLabel(item.status)}
                                                    />
                                                    {item.durationMs != null && (
                                                        <Typography variant="caption" color="text.secondary">
                                                            {item.durationMs} ms
                                                        </Typography>
                                                    )}
                                                    {item.traceType === "tool" && (item.inputSummary || item.outputSummary || item.errorMessage) && (
                                                        <Button
                                                            size="small"
                                                            onClick={() => toggleTraceEventExpanded(item.traceId)}
                                                            endIcon={expandedTraceIds.includes(item.traceId)
                                                                ? <ExpandLessOutlinedIcon fontSize="small" />
                                                                : <ExpandMoreOutlinedIcon fontSize="small" />}
                                                            sx={{ ml: "auto", minWidth: "auto", px: 0.5 }}
                                                        >
                                                            {expandedTraceIds.includes(item.traceId) ? translate("ai.drawer.collapseDetail") : translate("ai.drawer.viewDetail")}
                                                        </Button>
                                                    )}
                                                </Stack>
                                                {item.traceType === "thinking" ? (
                                                    <Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}>
                                                        {item.content || translate("ai.drawer.thinkingEmpty")}
                                                    </Typography>
                                                ) : (
                                                    <Collapse in={expandedTraceIds.includes(item.traceId)} timeout="auto" unmountOnExit>
                                                        {!!item.title && (
                                                            <Typography variant="caption" display="block" color="text.secondary" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}>
                                                                {item.title}
                                                            </Typography>
                                                        )}
                                                        {!!item.inputSummary && (
                                                            <Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}>
                                                                {translate("ai.drawer.toolInput", { value: item.inputSummary })}
                                                            </Typography>
                                                        )}
                                                        {!!item.outputSummary && (
                                                            <Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}>
                                                                {translate("ai.drawer.toolOutput", { value: item.outputSummary })}
                                                            </Typography>
                                                        )}
                                                        {!!item.errorMessage && (
                                                            <Typography variant="caption" color="error.main" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}>
                                                                {translate("ai.drawer.toolError", { value: item.errorMessage })}
                                                            </Typography>
                                                        )}
                                                    </Collapse>
                                                )}
                                            </Paper>
                                        ))}
                                    </Stack>
                                )}
                            </Paper>
                        </Box>
                    </Box>
                    <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={`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}`} />
                            <Stack direction="row" alignItems="center" justifyContent="space-between" spacing={1}>
                                <Typography variant="subtitle2">
                                    {translate("ai.drawer.runtimeOverview")}
                                </Typography>
                                <Button
                                    size="small"
                                    onClick={() => setRuntimePanelExpanded((prev) => !prev)}
                                    endIcon={runtimePanelExpanded
                                        ? <ExpandLessOutlinedIcon fontSize="small" />
                                        : <ExpandMoreOutlinedIcon fontSize="small" />}
                                    sx={{ minWidth: "auto", px: 1 }}
                                >
                                    {translate(runtimePanelExpanded ? "ai.drawer.runtimeCollapse" : "ai.drawer.runtimeExpand")}
                                </Button>
                            </Stack>
                            <Stack direction="row" spacing={1} mt={1.5} flexWrap="wrap" useFlexGap>
                                {quickLinks.map((item) => (
                            <Collapse in={runtimePanelExpanded} timeout="auto" unmountOnExit>
                                <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap sx={{ mt: 1.5 }}>
                                    <Chip size="small" label={translate("ai.drawer.requestMetric", { value: runtimeSummary.requestId })} />
                                    <Chip size="small" label={translate("ai.drawer.sessionMetric", { id: sessionId || "--" })} />
                                    <Chip size="small" label={translate("ai.drawer.promptMetric", { value: runtimeSummary.promptName })} />
                                    <Chip size="small" label={translate("ai.drawer.modelMetric", { value: runtimeSummary.model })} />
                                    <Chip size="small" label={translate("ai.drawer.mcpMetric", { value: runtimeSummary.mountedMcpCount })} />
                                    <Chip size="small" label={translate("ai.drawer.historyMetric", { value: persistedMessages.length })} />
                                    <Chip size="small" label={translate("ai.drawer.recentMetric", { value: runtimeSummary.recentMessageCount })} />
                                    <Chip size="small" color={runtimeSummary.hasSummary ? "success" : "default"} label={translate(runtimeSummary.hasSummary ? "ai.drawer.hasSummary" : "ai.drawer.noSummary")} />
                                    <Chip size="small" color={runtimeSummary.hasFacts ? "info" : "default"} label={translate(runtimeSummary.hasFacts ? "ai.drawer.hasFacts" : "ai.drawer.noFacts")} />
                                </Stack>
                                <Stack direction="row" spacing={1} mt={1.5} flexWrap="wrap" useFlexGap>
                                    {quickLinks.map((item) => (
                                        <Button
                                            key={item.path}
                                            size="small"
                                            variant="outlined"
                                            startIcon={item.icon}
                                            onClick={() => navigate(item.path)}
                                        >
                                            {item.label}
                                        </Button>
                                    ))}
                                    <Button
                                        key={item.path}
                                        size="small"
                                        variant="outlined"
                                        startIcon={item.icon}
                                        onClick={() => navigate(item.path)}
                                        startIcon={<HistoryOutlinedIcon />}
                                        onClick={handleRetainLatestRound}
                                        disabled={!sessionId || streaming}
                                    >
                                        {item.label}
                                        {translate("ai.drawer.retainLatestRound")}
                                    </Button>
                                ))}
                            </Stack>
                                    <Button
                                        size="small"
                                        variant="outlined"
                                        color="warning"
                                        startIcon={<AutoDeleteOutlinedIcon />}
                                        onClick={handleClearMemory}
                                        disabled={!sessionId || streaming}
                                    >
                                        {translate("ai.drawer.clearMemory")}
                                    </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>
                                )}
                            </Collapse>
                            {loadingRuntime && (
                                <Typography variant="body2" color="text.secondary" mt={1}>
                                    正在加载 AI 运行时信息...
                                    {translate("ai.drawer.loadingRuntime")}
                                </Typography>
                            )}
                            {!!drawerError && (
@@ -396,11 +1022,20 @@
                        <Divider />
                        <Box flex={1} overflow="auto" px={2} py={2} display="flex" flexDirection="column" gap={1.5}>
                        <Box
                            ref={messagesContainerRef}
                            flex={1}
                            overflow="auto"
                            px={2}
                            py={2}
                            display="flex"
                            flexDirection="column"
                            gap={1.5}
                        >
                            {!messages.length && (
                                <Paper variant="outlined" sx={{ p: 2, bgcolor: "grey.50" }}>
                                    <Typography variant="body2" color="text.secondary">
                                        这里会通过 SSE 流式返回 AI 回复。你也可以先去上面的快捷入口维护参数、Prompt 和 MCP 挂载。
                                        {translate("ai.drawer.emptyHint")}
                                    </Typography>
                                </Paper>
                            )}
@@ -410,36 +1045,58 @@
                                    display="flex"
                                    justifyContent={message.role === "user" ? "flex-end" : "flex-start"}
                                >
                                    <Paper
                                        elevation={0}
                                        sx={{
                                            px: 1.5,
                                            py: 1.25,
                                            maxWidth: "85%",
                                            borderRadius: 2,
                                            bgcolor: message.role === "user" ? "primary.main" : "grey.100",
                                            color: message.role === "user" ? "primary.contrastText" : "text.primary",
                                            whiteSpace: "pre-wrap",
                                            wordBreak: "break-word",
                                        }}
                                    >
                                        <Typography variant="caption" display="block" sx={{ opacity: 0.72, mb: 0.5 }}>
                                            {message.role === "user" ? "你" : "AI"}
                                        </Typography>
                                        <Typography variant="body2">
                                            {message.content || (streaming && index === messages.length - 1 ? "思考中..." : "")}
                                        </Typography>
                                    </Paper>
                                    <Stack spacing={1} sx={{ maxWidth: "85%", width: "100%" }} alignItems={message.role === "user" ? "flex-end" : "flex-start"}>
                                        <Paper
                                            elevation={0}
                                            sx={{
                                                px: 1.5,
                                                py: 1.25,
                                                width: "fit-content",
                                                maxWidth: "100%",
                                                borderRadius: 2,
                                                bgcolor: message.role === "user" ? "primary.main" : "grey.100",
                                                color: message.role === "user" ? "primary.contrastText" : "text.primary",
                                                whiteSpace: "pre-wrap",
                                                wordBreak: "break-word",
                                            }}
                                        >
                                            <Typography variant="caption" display="block" sx={{ opacity: 0.72, mb: 0.5 }}>
                                                {message.role === "user" ? translate("ai.drawer.userRole") : translate("ai.drawer.assistantRole")}
                                            </Typography>
                                            {message.role === "assistant" ? (
                                                <AiMarkdownContent
                                                    content={message.content || (streaming && index === messages.length - 1
                                                        ? translate("ai.drawer.thinking")
                                                        : "")}
                                                />
                                            ) : (
                                                <Typography variant="body2" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
                                                    {message.content || ""}
                                                </Typography>
                                            )}
                                        </Paper>
                                    </Stack>
                                </Box>
                            ))}
                            <Box ref={messagesBottomRef} sx={{ height: 1 }} />
                        </Box>
                        <Divider />
                        <Box px={2} py={1.5}>
                            {usage?.elapsedMs != null && (
                                <Typography variant="caption" color="text.secondary" display="block" mb={0.5}>
                                    {translate("ai.drawer.elapsedMetric", { value: usage.elapsedMs })}
                                    {usage?.firstTokenLatencyMs != null ? ` / ${translate("ai.drawer.firstTokenMetric", { value: usage.firstTokenLatencyMs })}` : ""}
                                </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}
                                    {translate("ai.drawer.tokenMetric", {
                                        prompt: usage?.promptTokens ?? 0,
                                        completion: usage?.completionTokens ?? 0,
                                        total: usage?.totalTokens ?? 0,
                                    })}
                                </Typography>
                            )}
                            <TextField
@@ -450,24 +1107,92 @@
                                multiline
                                minRows={3}
                                maxRows={6}
                                placeholder="输入你的问题,按 Enter 发送,Shift + Enter 换行"
                                placeholder={translate("ai.drawer.inputPlaceholder")}
                            />
                            <Stack direction="row" spacing={1} justifyContent="flex-end" mt={1.25}>
                                <Button onClick={() => setInput("")}>清空输入</Button>
                            <Stack
                                direction={{ xs: "column", sm: "row" }}
                                spacing={1}
                                justifyContent="space-between"
                                alignItems={{ xs: "stretch", sm: "center" }}
                                mt={1.25}
                            >
                                {!!selectableModelOptions.length && (
                                    <TextField
                                        select
                                        size="small"
                                        label={translate("ai.drawer.modelSelectorLabel")}
                                        value={selectedAiParamId ?? runtime?.aiParamId ?? selectableModelOptions[0]?.aiParamId ?? ""}
                                        onChange={handleModelChange}
                                        disabled={streaming || loadingRuntime || selectableModelOptions.length <= 1}
                                        SelectProps={{
                                            MenuProps: {
                                                disableScrollLock: true,
                                                sx: {
                                                    zIndex: 1605,
                                                },
                                                PaperProps: {
                                                    sx: {
                                                        zIndex: 1606,
                                                    },
                                                },
                                            },
                                        }}
                                        sx={{
                                            minWidth: { xs: "100%", sm: 260 },
                                            maxWidth: { xs: "100%", sm: 320 },
                                        }}
                                    >
                                        {selectableModelOptions.map((item) => (
                                            <MenuItem key={String(item.aiParamId)} value={item.aiParamId}>
                                                {`${item.name || item.model || "--"}${item.model && item.name !== item.model ? ` / ${item.model}` : ""}${item.active ? ` ${translate("ai.drawer.defaultModelSuffix")}` : ""}`}
                                            </MenuItem>
                                        ))}
                                    </TextField>
                                )}
                                <Stack direction="row" spacing={1} justifyContent="flex-end">
                                <Button onClick={() => setInput("")}>{translate("ai.drawer.clearInput")}</Button>
                                {streaming ? (
                                    <Button variant="outlined" color="warning" startIcon={<StopCircleOutlinedIcon />} onClick={() => stopStream(true)}>
                                        停止
                                        {translate("ai.drawer.stop")}
                                    </Button>
                                ) : (
                                    <Button variant="contained" startIcon={<SendRoundedIcon />} onClick={handleSend}>
                                        发送
                                        {translate("ai.drawer.send")}
                                    </Button>
                                )}
                                </Stack>
                            </Stack>
                        </Box>
                    </Box>
                </Box>
            </Box>
            <Dialog
                open={renameDialog.open}
                onClose={closeRenameDialog}
                fullWidth
                maxWidth="xs"
                sx={{
                    zIndex: AI_CHAT_DIALOG_Z_INDEX,
                }}
            >
                <DialogTitle>{translate("ai.drawer.renameDialogTitle")}</DialogTitle>
                <DialogContent>
                    <TextField
                        value={renameDialog.title}
                        onChange={(event) => setRenameDialog((prev) => ({ ...prev, title: event.target.value }))}
                        autoFocus
                        margin="dense"
                        label={translate("ai.drawer.sessionTitleField")}
                        fullWidth
                    />
                </DialogContent>
                <DialogActions>
                    <Button onClick={closeRenameDialog}>{translate("ai.common.cancel")}</Button>
                    <Button onClick={handleRenameSubmit} variant="contained" disabled={streaming || !renameDialog.title.trim()}>
                        {translate("ai.common.save")}
                    </Button>
                </DialogActions>
            </Dialog>
        </Drawer>
    );
};