zhou zhou
21 小时以前 287a666e1b2bb155e86aa88ebace201d1e8a51f6
#AI.国际化
13个文件已修改
1295 ■■■■ 已修改文件
rsf-admin/src/i18n/en.js 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/zh.js 304 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/layout/AiChatDrawer.jsx 174 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiCallLog/AiCallLogList.jsx 104 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx 73 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx 55 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiParam/AiParamForm.jsx 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiParam/AiParamList.jsx 41 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiPrompt/AiPromptForm.jsx 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiPrompt/AiPromptList.jsx 43 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiShared/AiConfigDialog.jsx 14 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/page/system/aiShared/AiRuntimeSummary.jsx 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
rsf-admin/src/i18n/en.js
@@ -232,6 +232,310 @@
        taskPathTemplateMerge: 'TaskPathTemplateMerge',
        missionFlowStepInstance: 'Mission Flow Steps',
    },
    ai: {
        common: {
            cancel: "Cancel",
            close: "Close",
            save: "Save",
            new: "New",
            detail: "Details",
            delete: "Delete",
            enabled: "Enabled",
            disabled: "Disabled",
            saveSuccess: "Saved successfully",
            updateSuccess: "Updated successfully",
            deleteSuccess: "Deleted successfully",
            deleteFailed: "Delete failed",
            operationFailed: "Operation failed",
            confirmDelete: "Delete \"%{name}\"?",
            none: "None",
            notValidated: "Not validated",
            notTested: "Not tested",
            lastUpdatedBy: "Last updated by",
            lastUpdatedAt: "Last updated at",
            target: "Target",
            lastTest: "Last test",
            prompt: "Prompt",
            model: "Model",
            error: "Error",
            testing: "Testing...",
        },
        runtimeSummary: {
            fetchFailed: "Failed to load runtime summary",
            title: "Current Runtime",
            description: "Shows the active model, prompt, and MCP mounts.",
            currentModel: "Current Model",
            validateStatus: "Validate %{status}",
            currentPrompt: "Current Prompt",
            lastUpdated: "Last updated: %{time} / %{user}",
            enabledMcp: "Enabled MCP",
            enabledMcpCount: "%{count} enabled",
        },
        param: {
            fields: {
                providerType: "Provider Type",
                model: "Model",
                baseUrl: "Base URL",
                apiKey: "API Key",
                temperature: "Temperature",
                topP: "Top P",
                maxTokens: "Max Tokens",
                timeoutMs: "Timeout(ms)",
                streamingEnabled: "Enable Streaming",
                validateStatus: "Latest Validate Status",
                lastValidateElapsedMs: "Latest Validate Elapsed(ms)",
                lastValidateTime: "Latest Validate Time",
                lastValidateMessage: "Latest Validate Result",
            },
            list: {
                emptyTitle: "No AI parameter configs",
                emptyDescription: "Create an OpenAI-compatible model card first.",
                streaming: "Streaming",
                nonStreaming: "Non-streaming",
            },
            dialog: {
                create: "New AI Parameter",
                edit: "Edit AI Parameter",
                show: "View AI Parameter Details",
            },
            validate: {
                success: "AI parameter validation succeeded",
                failed: "AI parameter validation failed",
                loading: "Validating...",
                beforeSave: "Validate Before Save",
                description: "Validates the current Base URL, API Key, and model directly.",
            },
            form: {
                sections: {
                    main: "Main Configuration",
                    runtime: "Runtime and Audit",
                },
            },
        },
        prompt: {
            fields: {
                code: "Code",
                scene: "Scene",
                systemPrompt: "System Prompt",
                userPromptTemplate: "User Prompt Template",
            },
            list: {
                emptyTitle: "No prompt configs",
                emptyDescription: "Once you create a prompt card, AI chat will load it dynamically.",
                sceneValue: "Scene: %{value}",
            },
            dialog: {
                create: "New Prompt",
                edit: "Edit Prompt",
                show: "View Prompt Details",
            },
            preview: {
                defaultInput: "Please summarize what this page can do",
                success: "Prompt preview generated",
                failed: "Prompt preview failed",
                title: "Prompt Preview",
                input: "Sample Input",
                metadata: "Sample Metadata JSON",
                loading: "Rendering...",
                render: "Render Preview",
                description: "Render the System Prompt and User Prompt with the current form values.",
                resolvedVariables: "Resolved variables: %{value}",
                renderedSystemPrompt: "Rendered System Prompt",
                renderedUserPrompt: "Rendered User Prompt",
            },
            form: {
                sections: {
                    main: "Prompt Configuration",
                    runtime: "Runtime and Audit",
                },
            },
        },
        mcp: {
            fields: {
                transportType: "Transport Type",
                builtinCode: "Built-in MCP",
                serverUrl: "Server URL",
                command: "Command",
                sort: "Sort",
                healthStatus: "Health Status",
                lastInitElapsedMs: "Last Init Elapsed(ms)",
                lastTestTime: "Last Test Time",
                lastTestMessage: "Last Test Result",
            },
            groups: {
                builtin: {
                    title: "Built-in MCP",
                    description: "Built-in tool mounts for exposing platform capabilities directly.",
                },
                sse: {
                    title: "Remote SSE MCP",
                    description: "Mount external tools through a remote MCP server.",
                },
                stdio: {
                    title: "Local STDIO MCP",
                    description: "Mount external MCP tools through a local command process.",
                },
            },
            health: {
                healthy: "Healthy",
                unhealthy: "Failed",
            },
            list: {
                emptyTitle: "No MCP mounts",
                emptyDescription: "Create a built-in MCP, remote SSE mount, or local STDIO mount.",
                sortValue: "Sort %{value}",
                noConnectivityTest: "No connectivity test has been run yet",
                connectivityTest: "Connectivity Test",
            },
            dialog: {
                create: "New MCP Mount",
                edit: "Edit MCP Mount",
                show: "View MCP Mount Details",
            },
            connectivity: {
                success: "Connectivity test completed",
                failed: "Connectivity test failed",
            },
            form: {
                testBeforeSave: "Test Before Save",
                testDescription: "Validate connectivity directly with the current draft without saving.",
                sections: {
                    main: "MCP Mount Configuration",
                    runtime: "Runtime Info",
                },
            },
            tools: {
                schemaParseFailed: "Failed to parse input schema: %{message}",
                loadFailed: "Failed to load tools",
                inputRequired: "Please enter tool test JSON",
                testSuccess: "Tool %{name} test completed",
                testFailed: "Tool test failed",
                saveBeforePreview: "Save the mount before previewing tools and running tests.",
                title: "Tool Preview and Test",
                description: "Supports connectivity checks, structured schema preview, and generated test forms from input parameters.",
                refresh: "Refresh Tools",
                noTools: "No tools were resolved for this mount.",
                purpose: "Purpose: %{value}",
                fieldCount: "%{count} fields",
                queryBoundary: "Query boundary: %{value}",
                exampleQuestions: "Example Questions",
                formattedSchema: "Formatted Input Schema",
                testInput: "Test Input JSON",
                testInputPlaceholder: "Example: {\"code\":\"A01\"}",
                executeTest: "Run Test",
                testResult: "Test Result",
            },
        },
        observe: {
            fields: {
                requestId: "Request ID",
                promptCode: "Prompt Code",
                userId: "User ID",
                sessionId: "Session ID",
                mountedMcp: "MCP Mounts",
            },
            summary: {
                fetchFailed: "Failed to load AI observe stats",
                title: "Observe Overview",
                description: "Statistics for AI chats and MCP tool calls in the current tenant.",
                callCount: "AI Calls",
                successFailure: "Success %{success} / Failed %{failure}",
                avgElapsed: "Average Elapsed",
                firstToken: "First token %{value} ms",
                tokenUsage: "Token Usage",
                avgToken: "Average %{value}",
                toolSuccessRate: "Tool Success Rate",
                toolCallFailure: "Calls %{call} / Failed %{failure}",
            },
            status: {
                completed: "Success",
                failed: "Failed",
                aborted: "Aborted",
            },
            detail: {
                mcpLogsFailed: "Failed to load MCP call logs",
                title: "AI Call Details",
                mcpLogs: "MCP Tool Call Logs",
                noMcpLogs: "No MCP tool logs were generated for this call.",
                inputSummary: "Input Summary",
                outputSummary: "Output Summary / Error",
            },
            list: {
                emptyTitle: "No AI call logs",
                emptyDescription: "After starting AI chats, statistics and audit records will appear here.",
                userValue: "User %{value}",
                elapsedValue: "Elapsed %{value} ms",
                tokenValue: "Token %{value}",
                mcpToolCalls: "MCP / Tool Calls",
                mcpToolSummary: "%{mcp} mounts, %{success} tool successes, %{failure} failures",
            },
        },
        drawer: {
            title: "AI Chat",
            runtimeFailed: "Failed to load AI runtime",
            sessionListFailed: "Failed to load AI sessions",
            sessionDeleted: "Session deleted",
            deleteSessionFailed: "Failed to delete AI session",
            pinned: "Session pinned",
            unpinned: "Session unpinned",
            pinFailed: "Failed to update session pin state",
            renamed: "Session renamed",
            renameFailed: "Failed to rename session",
            memoryCleared: "Session memory cleared",
            clearMemoryFailed: "Failed to clear session memory",
            retainLatestRoundSuccess: "Only the latest round was kept",
            retainLatestRoundFailed: "Failed to keep only the latest round",
            stopSuccess: "Current output stopped",
            chatFailed: "AI chat failed",
            newSession: "New Session",
            sessionList: "Sessions",
            searchPlaceholder: "Search session titles",
            noSessions: "No history sessions",
            sessionTitle: "Session %{id}",
            pinAction: "Pin session",
            unpinAction: "Unpin session",
            renameAction: "Rename session",
            deleteAction: "Delete session",
            toolTrace: "Tool Trace",
            noToolTrace: "No tool call was triggered in this round",
            unknownTool: "Unknown tool",
            toolStatusFailed: "Failed",
            toolStatusCompleted: "Completed",
            toolStatusRunning: "Running",
            collapseDetail: "Hide Details",
            viewDetail: "View Details",
            toolInput: "Input: %{value}",
            toolOutput: "Output summary: %{value}",
            toolError: "Error: %{value}",
            hasSummary: "Summary",
            noSummary: "No Summary",
            hasFacts: "Facts",
            noFacts: "No Facts",
            retainLatestRound: "Keep Latest Round",
            clearMemory: "Clear Memory",
            loadingRuntime: "Loading AI runtime info...",
            emptyHint: "AI responses stream back through SSE here. You can also maintain parameters, prompts, and MCP mounts from the quick links above.",
            userRole: "You",
            assistantRole: "AI",
            thinking: "Thinking...",
            inputPlaceholder: "Type your question. Press Enter to send, Shift + Enter for a new line",
            clearInput: "Clear Input",
            stop: "Stop",
            send: "Send",
            renameDialogTitle: "Rename Session",
            sessionTitleField: "Session Title",
            requestMetric: "Req: %{value}",
            sessionMetric: "Session: %{id}",
            promptMetric: "Prompt: %{value}",
            modelMetric: "Model: %{value}",
            mcpMetric: "MCP: %{value}",
            historyMetric: "History: %{value}",
            recentMetric: "Recent: %{value}",
            elapsedMetric: "Elapsed: %{value} ms",
            firstTokenMetric: "First token: %{value} ms",
            tokenMetric: "Tokens: prompt %{prompt} / completion %{completion} / total %{total}",
        },
    },
    table: {
        field: {
            basStationArea: {
rsf-admin/src/i18n/zh.js
@@ -248,6 +248,310 @@
        taskPathTemplateMerge: '任务路径模板合并',
        missionFlowStepInstance: '任务流程步骤',
    },
    ai: {
        common: {
            cancel: "取消",
            close: "关闭",
            save: "保存",
            new: "新建",
            detail: "详情",
            delete: "删除",
            enabled: "启用",
            disabled: "停用",
            saveSuccess: "保存成功",
            updateSuccess: "更新成功",
            deleteSuccess: "删除成功",
            deleteFailed: "删除失败",
            operationFailed: "操作失败",
            confirmDelete: "确认删除“%{name}”吗?",
            none: "无",
            notValidated: "未校验",
            notTested: "未测试",
            lastUpdatedBy: "最近更新人",
            lastUpdatedAt: "最近更新时间",
            target: "目标",
            lastTest: "最近测试",
            prompt: "Prompt",
            model: "模型",
            error: "错误",
            testing: "测试中...",
        },
        runtimeSummary: {
            fetchFailed: "获取运行态摘要失败",
            title: "当前运行态",
            description: "展示当前生效的模型、Prompt 与 MCP 挂载信息。",
            currentModel: "当前模型",
            validateStatus: "校验 %{status}",
            currentPrompt: "当前 Prompt",
            lastUpdated: "最近更新:%{time} / %{user}",
            enabledMcp: "已启用 MCP",
            enabledMcpCount: "%{count} 个",
        },
        param: {
            fields: {
                providerType: "提供方类型",
                model: "模型",
                baseUrl: "Base URL",
                apiKey: "API Key",
                temperature: "Temperature",
                topP: "Top P",
                maxTokens: "Max Tokens",
                timeoutMs: "Timeout(ms)",
                streamingEnabled: "启用流式响应",
                validateStatus: "最近校验状态",
                lastValidateElapsedMs: "最近校验耗时(ms)",
                lastValidateTime: "最近校验时间",
                lastValidateMessage: "最近校验结果",
            },
            list: {
                emptyTitle: "暂无 AI 参数配置",
                emptyDescription: "可以先新建一个 OpenAI 兼容模型参数卡片。",
                streaming: "流式响应",
                nonStreaming: "非流式",
            },
            dialog: {
                create: "新建 AI 参数",
                edit: "编辑 AI 参数",
                show: "查看 AI 参数详情",
            },
            validate: {
                success: "AI 参数验证成功",
                failed: "AI 参数验证失败",
                loading: "验证中...",
                beforeSave: "保存前验证",
                description: "会直接校验当前 Base URL、API Key 与模型是否可调用。",
            },
            form: {
                sections: {
                    main: "主要配置",
                    runtime: "运行与审计信息",
                },
            },
        },
        prompt: {
            fields: {
                code: "编码",
                scene: "场景",
                systemPrompt: "System Prompt",
                userPromptTemplate: "User Prompt Template",
            },
            list: {
                emptyTitle: "暂无 Prompt 配置",
                emptyDescription: "新建一张 Prompt 卡片后,AI 对话会动态加载这里的内容。",
                sceneValue: "场景: %{value}",
            },
            dialog: {
                create: "新建 Prompt",
                edit: "编辑 Prompt",
                show: "查看 Prompt 详情",
            },
            preview: {
                defaultInput: "请帮我总结当前页面能做什么",
                success: "Prompt 预览完成",
                failed: "Prompt 预览失败",
                title: "Prompt 预览",
                input: "示例输入",
                metadata: "示例元数据 JSON",
                loading: "预览中...",
                render: "预览渲染",
                description: "用当前表单内容渲染 System Prompt 和 User Prompt。",
                resolvedVariables: "已解析变量:%{value}",
                renderedSystemPrompt: "渲染后的 System Prompt",
                renderedUserPrompt: "渲染后的 User Prompt",
            },
            form: {
                sections: {
                    main: "Prompt 配置",
                    runtime: "运行与审计信息",
                },
            },
        },
        mcp: {
            fields: {
                transportType: "传输类型",
                builtinCode: "内置 MCP",
                serverUrl: "服务地址",
                command: "命令",
                sort: "排序",
                healthStatus: "健康状态",
                lastInitElapsedMs: "最近初始化耗时(ms)",
                lastTestTime: "最近测试时间",
                lastTestMessage: "最近测试结果",
            },
            groups: {
                builtin: {
                    title: "内置 MCP",
                    description: "系统内置工具挂载,适合直接暴露平台能力。",
                },
                sse: {
                    title: "远程 SSE MCP",
                    description: "通过远程 MCP Server 挂载外部工具。",
                },
                stdio: {
                    title: "本地 STDIO MCP",
                    description: "通过本地命令进程挂载外部 MCP。",
                },
            },
            health: {
                healthy: "正常",
                unhealthy: "失败",
            },
            list: {
                emptyTitle: "暂无 MCP 挂载",
                emptyDescription: "可以新建内置 MCP、远程 SSE 挂载或本地 STDIO 挂载。",
                sortValue: "排序 %{value}",
                noConnectivityTest: "尚未执行连通性测试",
                connectivityTest: "连通测试",
            },
            dialog: {
                create: "新建 MCP 挂载",
                edit: "编辑 MCP 挂载",
                show: "查看 MCP 挂载详情",
            },
            connectivity: {
                success: "连通性测试完成",
                failed: "连通性测试失败",
            },
            form: {
                testBeforeSave: "保存前测试",
                testDescription: "用当前草稿配置直接校验连通性,不会落库。",
                sections: {
                    main: "MCP 挂载配置",
                    runtime: "运行态信息",
                },
            },
            tools: {
                schemaParseFailed: "Input Schema 解析失败: %{message}",
                loadFailed: "获取工具列表失败",
                inputRequired: "请输入工具测试 JSON",
                testSuccess: "工具 %{name} 测试完成",
                testFailed: "工具测试失败",
                saveBeforePreview: "保存挂载后即可预览工具并执行测试。",
                title: "工具预览与测试",
                description: "支持连通性测试、结构化 Schema 预览和按输入参数自动生成测试表单。",
                refresh: "刷新工具",
                noTools: "当前挂载未解析出任何工具。",
                purpose: "用途: %{value}",
                fieldCount: "%{count} 个参数",
                queryBoundary: "查询边界: %{value}",
                exampleQuestions: "示例提问",
                formattedSchema: "格式化 Input Schema",
                testInput: "测试输入 JSON",
                testInputPlaceholder: "例如:{\"code\":\"A01\"}",
                executeTest: "执行测试",
                testResult: "测试结果",
            },
        },
        observe: {
            fields: {
                requestId: "请求ID",
                promptCode: "Prompt 编码",
                userId: "用户ID",
                sessionId: "会话ID",
                mountedMcp: "MCP 挂载",
            },
            summary: {
                fetchFailed: "获取 AI 观测统计失败",
                title: "观测总览",
                description: "当前租户下的 AI 对话调用与 MCP 工具调用统计。",
                callCount: "AI 调用量",
                successFailure: "成功 %{success} / 失败 %{failure}",
                avgElapsed: "平均耗时",
                firstToken: "首包 %{value} ms",
                tokenUsage: "Token 使用",
                avgToken: "平均 %{value}",
                toolSuccessRate: "工具成功率",
                toolCallFailure: "调用 %{call} / 失败 %{failure}",
            },
            status: {
                completed: "成功",
                failed: "失败",
                aborted: "中断",
            },
            detail: {
                mcpLogsFailed: "获取 MCP 调用日志失败",
                title: "AI 调用详情",
                mcpLogs: "MCP 工具调用日志",
                noMcpLogs: "当前调用没有产生 MCP 工具日志。",
                inputSummary: "输入摘要",
                outputSummary: "输出摘要 / 错误",
            },
            list: {
                emptyTitle: "暂无 AI 调用日志",
                emptyDescription: "发起 AI 对话后,这里会展示调用统计和审计记录。",
                userValue: "用户 %{value}",
                elapsedValue: "耗时 %{value} ms",
                tokenValue: "Token %{value}",
                mcpToolCalls: "MCP / 工具调用",
                mcpToolSummary: "挂载 %{mcp} 个,工具成功 %{success},失败 %{failure}",
            },
        },
        drawer: {
            title: "AI 对话",
            runtimeFailed: "获取 AI 运行时失败",
            sessionListFailed: "获取 AI 会话列表失败",
            sessionDeleted: "会话已删除",
            deleteSessionFailed: "删除 AI 会话失败",
            pinned: "会话已置顶",
            unpinned: "会话已取消置顶",
            pinFailed: "更新会话置顶状态失败",
            renamed: "会话已重命名",
            renameFailed: "重命名会话失败",
            memoryCleared: "会话记忆已清空",
            clearMemoryFailed: "清空会话记忆失败",
            retainLatestRoundSuccess: "已仅保留当前轮记忆",
            retainLatestRoundFailed: "保留当前轮记忆失败",
            stopSuccess: "已停止当前对话输出",
            chatFailed: "AI 对话失败",
            newSession: "新建会话",
            sessionList: "会话列表",
            searchPlaceholder: "搜索会话标题",
            noSessions: "暂无历史会话",
            sessionTitle: "会话 %{id}",
            pinAction: "置顶会话",
            unpinAction: "取消置顶",
            renameAction: "重命名会话",
            deleteAction: "删除会话",
            toolTrace: "工具调用轨迹",
            noToolTrace: "当前轮未触发工具调用",
            unknownTool: "未知工具",
            toolStatusFailed: "失败",
            toolStatusCompleted: "完成",
            toolStatusRunning: "执行中",
            collapseDetail: "收起详情",
            viewDetail: "查看详情",
            toolInput: "入参: %{value}",
            toolOutput: "结果摘要: %{value}",
            toolError: "错误: %{value}",
            hasSummary: "有摘要",
            noSummary: "无摘要",
            hasFacts: "有事实",
            noFacts: "无事实",
            retainLatestRound: "仅保留当前轮",
            clearMemory: "清空记忆",
            loadingRuntime: "正在加载 AI 运行时信息...",
            emptyHint: "这里会通过 SSE 流式返回 AI 回复。你也可以先去上面的快捷入口维护参数、Prompt 和 MCP 挂载。",
            userRole: "你",
            assistantRole: "AI",
            thinking: "思考中...",
            inputPlaceholder: "输入你的问题,按 Enter 发送,Shift + Enter 换行",
            clearInput: "清空输入",
            stop: "停止",
            send: "发送",
            renameDialogTitle: "重命名会话",
            sessionTitleField: "会话标题",
            requestMetric: "Req: %{value}",
            sessionMetric: "Session: %{id}",
            promptMetric: "Prompt: %{value}",
            modelMetric: "Model: %{value}",
            mcpMetric: "MCP: %{value}",
            historyMetric: "History: %{value}",
            recentMetric: "Recent: %{value}",
            elapsedMetric: "耗时: %{value} ms",
            firstTokenMetric: "首包: %{value} ms",
            tokenMetric: "Tokens: prompt %{prompt} / completion %{completion} / total %{total}",
        },
    },
    table: {
        field: {            
            flowStepInstance: { 
rsf-admin/src/layout/AiChatDrawer.jsx
@@ -1,6 +1,6 @@
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 {
    Alert,
    Box,
@@ -43,17 +43,14 @@
const DEFAULT_PROMPT_CODE = "home.default";
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 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 [sessionId, setSessionId] = useState(null);
    const [sessions, setSessions] = useState([]);
@@ -68,6 +65,12 @@
    const [drawerError, setDrawerError] = useState("");
    const [sessionKeyword, setSessionKeyword] = useState("");
    const [renameDialog, setRenameDialog] = useState({ open: false, sessionId: null, title: "" });
    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;
@@ -95,6 +98,16 @@
        stopStream(false);
    }, []);
    useEffect(() => {
        if (!open) {
            return;
        }
        const timer = window.requestAnimationFrame(() => {
            scrollMessagesToBottom();
        });
        return () => window.cancelAnimationFrame(timer);
    }, [open, messages, streaming]);
    const initializeDrawer = async (targetSessionId = null) => {
        setToolEvents([]);
        setExpandedToolIds([]);
@@ -115,7 +128,7 @@
            setPersistedMessages(historyMessages);
            setMessages(historyMessages);
        } catch (error) {
            const message = error.message || "获取 AI 运行时失败";
            const message = error.message || translate("ai.drawer.runtimeFailed");
            setDrawerError(message);
        } finally {
            setLoadingRuntime(false);
@@ -127,7 +140,7 @@
            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);
        }
    };
@@ -171,14 +184,14 @@
        }
        try {
            await removeAiSession(targetSessionId);
            notify("会话已删除");
            notify(translate("ai.drawer.sessionDeleted"));
            if (targetSessionId === sessionId) {
                startNewSession();
                await loadRuntime(null);
            }
            await loadSessions(sessionKeyword);
        } catch (error) {
            const message = error.message || "删除 AI 会话失败";
            const message = error.message || translate("ai.drawer.deleteSessionFailed");
            setDrawerError(message);
            notify(message, { type: "error" });
        }
@@ -190,10 +203,10 @@
        }
        try {
            await pinAiSession(targetSessionId, pinned);
            notify(pinned ? "会话已置顶" : "会话已取消置顶");
            notify(translate(pinned ? "ai.drawer.pinned" : "ai.drawer.unpinned"));
            await loadSessions(sessionKeyword);
        } catch (error) {
            const message = error.message || "更新会话置顶状态失败";
            const message = error.message || translate("ai.drawer.pinFailed");
            setDrawerError(message);
            notify(message, { type: "error" });
        }
@@ -217,11 +230,11 @@
        }
        try {
            await renameAiSession(renameDialog.sessionId, renameDialog.title);
            notify("会话已重命名");
            notify(translate("ai.drawer.renamed"));
            closeRenameDialog();
            await loadSessions(sessionKeyword);
        } catch (error) {
            const message = error.message || "重命名会话失败";
            const message = error.message || translate("ai.drawer.renameFailed");
            setDrawerError(message);
            notify(message, { type: "error" });
        }
@@ -233,13 +246,13 @@
        }
        try {
            await clearAiSessionMemory(sessionId);
            notify("会话记忆已清空");
            notify(translate("ai.drawer.memoryCleared"));
            await Promise.all([
                loadRuntime(sessionId),
                loadSessions(sessionKeyword),
            ]);
        } catch (error) {
            const message = error.message || "清空会话记忆失败";
            const message = error.message || translate("ai.drawer.clearMemoryFailed");
            setDrawerError(message);
            notify(message, { type: "error" });
        }
@@ -251,13 +264,13 @@
        }
        try {
            await retainAiSessionLatestRound(sessionId);
            notify("已仅保留当前轮记忆");
            notify(translate("ai.drawer.retainLatestRoundSuccess"));
            await Promise.all([
                loadRuntime(sessionId),
                loadSessions(sessionKeyword),
            ]);
        } catch (error) {
            const message = error.message || "保留当前轮记忆失败";
            const message = error.message || translate("ai.drawer.retainLatestRoundFailed");
            setDrawerError(message);
            notify(message, { type: "error" });
        }
@@ -269,8 +282,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;
        }
    };
@@ -380,7 +403,7 @@
                            }
                        }
                        if (eventName === "error") {
                            const message = payload?.message || "AI 对话失败";
                            const message = payload?.message || translate("ai.drawer.chatFailed");
                            const displayMessage = payload?.requestId ? `${message} [${payload.requestId}]` : message;
                            setDrawerError(displayMessage);
                            notify(displayMessage, { type: "error" });
@@ -390,7 +413,7 @@
            );
        } 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" });
            }
@@ -432,12 +455,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>
@@ -454,9 +477,9 @@
                    >
                        <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
@@ -464,7 +487,7 @@
                                onChange={(event) => setSessionKeyword(event.target.value)}
                                fullWidth
                                size="small"
                                placeholder="搜索会话标题"
                                placeholder={translate("ai.drawer.searchPlaceholder")}
                                InputProps={{
                                    startAdornment: <SearchOutlinedIcon fontSize="small" sx={{ mr: 1, color: "text.secondary" }} />,
                                }}
@@ -474,7 +497,7 @@
                                {!sessions.length ? (
                                    <Box px={1.5} py={1.25}>
                                        <Typography variant="body2" color="text.secondary">
                                            暂无历史会话
                                            {translate("ai.drawer.noSessions")}
                                        </Typography>
                                    </Box>
                                ) : (
@@ -488,8 +511,8 @@
                                                alignItems="flex-start"
                                            >
                                                <ListItemText
                                                    primary={item.title || `会话 ${item.sessionId}`}
                                                    secondary={item.lastMessagePreview || 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,
@@ -508,7 +531,7 @@
                                                        event.stopPropagation();
                                                        handlePinSession(item.sessionId, !item.pinned);
                                                    }}
                                                    title={item.pinned ? "取消置顶" : "置顶会话"}
                                                    title={translate(item.pinned ? "ai.drawer.unpinAction" : "ai.drawer.pinAction")}
                                                >
                                                    {item.pinned ? <PushPinIcon fontSize="small" /> : <PushPinOutlinedIcon fontSize="small" />}
                                                </IconButton>
@@ -520,7 +543,7 @@
                                                        event.stopPropagation();
                                                        openRenameDialog(item);
                                                    }}
                                                    title="重命名会话"
                                                    title={translate("ai.drawer.renameAction")}
                                                >
                                                    <EditOutlinedIcon fontSize="small" />
                                                </IconButton>
@@ -532,7 +555,7 @@
                                                        event.stopPropagation();
                                                        handleDeleteSession(item.sessionId);
                                                    }}
                                                    title="删除会话"
                                                    title={translate("ai.drawer.deleteAction")}
                                                >
                                                    <DeleteOutlineOutlinedIcon fontSize="small" />
                                                </IconButton>
@@ -554,13 +577,13 @@
                    >
                        <Box px={2} py={1.5} display="flex" flexDirection="column" minHeight={0}>
                            <Typography variant="subtitle2" mb={1}>
                                工具调用轨迹
                                {translate("ai.drawer.toolTrace")}
                            </Typography>
                            <Paper variant="outlined" sx={{ flex: 1, minHeight: { xs: 140, md: 0 }, overflow: "hidden", bgcolor: "grey.50" }}>
                                {!toolEvents.length ? (
                                    <Box px={1.5} py={1.25}>
                                        <Typography variant="body2" color="text.secondary">
                                            当前轮未触发工具调用
                                            {translate("ai.drawer.noToolTrace")}
                                        </Typography>
                                    </Box>
                                ) : (
@@ -577,12 +600,12 @@
                                            >
                                                <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
                                                    <Typography variant="body2" fontWeight={700}>
                                                        {item.toolName || "未知工具"}
                                                        {item.toolName || translate("ai.drawer.unknownTool")}
                                                    </Typography>
                                                    <Chip
                                                        size="small"
                                                        color={item.status === "FAILED" ? "error" : item.status === "COMPLETED" ? "success" : "info"}
                                                        label={item.status === "FAILED" ? "失败" : item.status === "COMPLETED" ? "完成" : "执行中"}
                                                        label={translate(item.status === "FAILED" ? "ai.drawer.toolStatusFailed" : item.status === "COMPLETED" ? "ai.drawer.toolStatusCompleted" : "ai.drawer.toolStatusRunning")}
                                                    />
                                                    {item.durationMs != null && (
                                                        <Typography variant="caption" color="text.secondary">
@@ -598,24 +621,24 @@
                                                                : <ExpandMoreOutlinedIcon fontSize="small" />}
                                                            sx={{ ml: "auto", minWidth: "auto", px: 0.5 }}
                                                        >
                                                            {expandedToolIds.includes(item.toolCallId) ? "收起详情" : "查看详情"}
                                                            {expandedToolIds.includes(item.toolCallId) ? translate("ai.drawer.collapseDetail") : translate("ai.drawer.viewDetail")}
                                                        </Button>
                                                    )}
                                                </Stack>
                                                <Collapse in={expandedToolIds.includes(item.toolCallId)} timeout="auto" unmountOnExit>
                                                    {!!item.inputSummary && (
                                                        <Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}>
                                                            入参: {item.inputSummary}
                                                            {translate("ai.drawer.toolInput", { value: item.inputSummary })}
                                                        </Typography>
                                                    )}
                                                    {!!item.outputSummary && (
                                                        <Typography variant="caption" display="block" sx={{ mt: 0.75, whiteSpace: "pre-wrap" }}>
                                                            结果摘要: {item.outputSummary}
                                                            {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" }}>
                                                            错误: {item.errorMessage}
                                                            {translate("ai.drawer.toolError", { value: item.errorMessage })}
                                                        </Typography>
                                                    )}
                                                </Collapse>
@@ -630,15 +653,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 ? "有事实" : "无事实"} />
                                <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) => (
@@ -659,7 +682,7 @@
                                    onClick={handleRetainLatestRound}
                                    disabled={!sessionId || streaming}
                                >
                                    仅保留当前轮
                                    {translate("ai.drawer.retainLatestRound")}
                                </Button>
                                <Button
                                    size="small"
@@ -669,7 +692,7 @@
                                    onClick={handleClearMemory}
                                    disabled={!sessionId || streaming}
                                >
                                    清空记忆
                                    {translate("ai.drawer.clearMemory")}
                                </Button>
                            </Stack>
                            {!!runtime?.memorySummary && (
@@ -688,7 +711,7 @@
                            )}
                            {loadingRuntime && (
                                <Typography variant="body2" color="text.secondary" mt={1}>
                                    正在加载 AI 运行时信息...
                                    {translate("ai.drawer.loadingRuntime")}
                                </Typography>
                            )}
                            {!!drawerError && (
@@ -700,11 +723,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>
                            )}
@@ -728,14 +760,15 @@
                                        }}
                                    >
                                        <Typography variant="caption" display="block" sx={{ opacity: 0.72, mb: 0.5 }}>
                                            {message.role === "user" ? "你" : "AI"}
                                            {message.role === "user" ? translate("ai.drawer.userRole") : translate("ai.drawer.assistantRole")}
                                        </Typography>
                                        <Typography variant="body2">
                                            {message.content || (streaming && index === messages.length - 1 ? "思考中..." : "")}
                                            {message.content || (streaming && index === messages.length - 1 ? translate("ai.drawer.thinking") : "")}
                                        </Typography>
                                    </Paper>
                                </Box>
                            ))}
                            <Box ref={messagesBottomRef} sx={{ height: 1 }} />
                        </Box>
                        <Divider />
@@ -743,12 +776,17 @@
                        <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` : ""}
                                    {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
@@ -759,17 +797,17 @@
                                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>
                                <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>
@@ -778,21 +816,21 @@
                </Box>
            </Box>
            <Dialog open={renameDialog.open} onClose={closeRenameDialog} fullWidth maxWidth="xs">
                <DialogTitle>重命名会话</DialogTitle>
                <DialogTitle>{translate("ai.drawer.renameDialogTitle")}</DialogTitle>
                <DialogContent>
                    <TextField
                        value={renameDialog.title}
                        onChange={(event) => setRenameDialog((prev) => ({ ...prev, title: event.target.value }))}
                        autoFocus
                        margin="dense"
                        label="会话标题"
                        label={translate("ai.drawer.sessionTitleField")}
                        fullWidth
                    />
                </DialogContent>
                <DialogActions>
                    <Button onClick={closeRenameDialog}>取消</Button>
                    <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>
rsf-admin/src/page/system/aiCallLog/AiCallLogList.jsx
@@ -8,6 +8,7 @@
    TopToolbar,
    useListContext,
    useNotify,
    useTranslate,
} from "react-admin";
import {
    Alert,
@@ -32,12 +33,12 @@
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <TextInput source="requestId" label="请求ID" />,
    <TextInput source="promptCode" label="Prompt 编码" />,
    <TextInput source="userId" label="用户ID" />,
    <TextInput source="requestId" label="ai.observe.fields.requestId" />,
    <TextInput source="promptCode" label="ai.observe.fields.promptCode" />,
    <TextInput source="userId" label="ai.observe.fields.userId" />,
    <SelectInput
        source="status"
        label="状态"
        label="common.field.status"
        choices={[
            { id: "RUNNING", name: "RUNNING" },
            { id: "COMPLETED", name: "COMPLETED" },
@@ -48,6 +49,7 @@
];
const ObserveSummary = () => {
    const translate = useTranslate();
    const [stats, setStats] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState("");
@@ -64,7 +66,7 @@
            })
            .catch((err) => {
                if (active) {
                    setError(err?.message || "获取 AI 观测统计失败");
                    setError(err?.message || translate("ai.observe.summary.fetchFailed"));
                }
            })
            .finally(() => {
@@ -83,9 +85,9 @@
                <CardContent>
                    <Stack direction="row" justifyContent="space-between" alignItems="center" mb={2}>
                        <Box>
                            <Typography variant="h6">观测总览</Typography>
                            <Typography variant="h6">{translate("ai.observe.summary.title")}</Typography>
                            <Typography variant="body2" color="text.secondary">
                                当前租户下的 AI 对话调用与 MCP 工具调用统计。
                                {translate("ai.observe.summary.description")}
                            </Typography>
                        </Box>
                        {loading && <CircularProgress size={24} />}
@@ -94,31 +96,31 @@
                    {!loading && !error && stats && (
                        <Grid container spacing={2}>
                            <Grid item xs={12} md={3}>
                                <Typography variant="caption" color="text.secondary">AI 调用量</Typography>
                                <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.callCount")}</Typography>
                                <Typography variant="h5">{stats.callCount ?? 0}</Typography>
                                <Typography variant="body2" color="text.secondary">
                                    成功 {stats.successCount ?? 0} / 失败 {stats.failureCount ?? 0}
                                    {translate("ai.observe.summary.successFailure", { success: stats.successCount ?? 0, failure: stats.failureCount ?? 0 })}
                                </Typography>
                            </Grid>
                            <Grid item xs={12} md={3}>
                                <Typography variant="caption" color="text.secondary">平均耗时</Typography>
                                <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.avgElapsed")}</Typography>
                                <Typography variant="h5">{stats.avgElapsedMs ?? 0} ms</Typography>
                                <Typography variant="body2" color="text.secondary">
                                    首包 {stats.avgFirstTokenLatencyMs ?? 0} ms
                                    {translate("ai.observe.summary.firstToken", { value: stats.avgFirstTokenLatencyMs ?? 0 })}
                                </Typography>
                            </Grid>
                            <Grid item xs={12} md={3}>
                                <Typography variant="caption" color="text.secondary">Token 使用</Typography>
                                <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.tokenUsage")}</Typography>
                                <Typography variant="h5">{stats.totalTokens ?? 0}</Typography>
                                <Typography variant="body2" color="text.secondary">
                                    平均 {stats.avgTotalTokens ?? 0}
                                    {translate("ai.observe.summary.avgToken", { value: stats.avgTotalTokens ?? 0 })}
                                </Typography>
                            </Grid>
                            <Grid item xs={12} md={3}>
                                <Typography variant="caption" color="text.secondary">工具成功率</Typography>
                                <Typography variant="caption" color="text.secondary">{translate("ai.observe.summary.toolSuccessRate")}</Typography>
                                <Typography variant="h5">{Number(stats.toolSuccessRate || 0).toFixed(2)}%</Typography>
                                <Typography variant="body2" color="text.secondary">
                                    调用 {stats.toolCallCount ?? 0} / 失败 {stats.toolFailureCount ?? 0}
                                    {translate("ai.observe.summary.toolCallFailure", { call: stats.toolCallCount ?? 0, failure: stats.toolFailureCount ?? 0 })}
                                </Typography>
                            </Grid>
                        </Grid>
@@ -129,21 +131,22 @@
    );
};
const resolveStatusChip = (status) => {
const resolveStatusChip = (status, translate) => {
    if (status === "COMPLETED") {
        return { color: "success", label: "成功" };
        return { color: "success", label: translate("ai.observe.status.completed") };
    }
    if (status === "FAILED") {
        return { color: "error", label: "失败" };
        return { color: "error", label: translate("ai.observe.status.failed") };
    }
    if (status === "ABORTED") {
        return { color: "warning", label: "中断" };
        return { color: "warning", label: translate("ai.observe.status.aborted") };
    }
    return { color: "default", label: status || "--" };
};
const AiCallLogDetailDialog = ({ record, open, onClose }) => {
    const notify = useNotify();
    const translate = useTranslate();
    const [logs, setLogs] = useState([]);
    const [loading, setLoading] = useState(false);
@@ -161,7 +164,7 @@
            })
            .catch((error) => {
                if (active) {
                    notify(error?.message || "获取 MCP 调用日志失败", { type: "error" });
                    notify(error?.message || translate("ai.observe.detail.mcpLogsFailed"), { type: "error" });
                }
            })
            .finally(() => {
@@ -180,35 +183,35 @@
    return (
        <Dialog open={open} onClose={onClose} fullWidth maxWidth="lg">
            <DialogTitle>AI 调用详情</DialogTitle>
            <DialogTitle>{translate("ai.observe.detail.title")}</DialogTitle>
            <DialogContent dividers>
                <Grid container spacing={2}>
                    <Grid item xs={12} md={6}>
                        <Typography variant="caption" color="text.secondary">请求ID</Typography>
                        <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.requestId")}</Typography>
                        <Typography variant="body2">{record.requestId || "--"}</Typography>
                    </Grid>
                    <Grid item xs={12} md={3}>
                        <Typography variant="caption" color="text.secondary">用户ID</Typography>
                        <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.userId")}</Typography>
                        <Typography variant="body2">{record.userId || "--"}</Typography>
                    </Grid>
                    <Grid item xs={12} md={3}>
                        <Typography variant="caption" color="text.secondary">会话ID</Typography>
                        <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.sessionId")}</Typography>
                        <Typography variant="body2">{record.sessionId || "--"}</Typography>
                    </Grid>
                    <Grid item xs={12} md={4}>
                        <Typography variant="caption" color="text.secondary">Prompt</Typography>
                        <Typography variant="caption" color="text.secondary">{translate("ai.common.prompt")}</Typography>
                        <Typography variant="body2">{record.promptName || "--"} / {record.promptCode || "--"}</Typography>
                    </Grid>
                    <Grid item xs={12} md={4}>
                        <Typography variant="caption" color="text.secondary">模型</Typography>
                        <Typography variant="caption" color="text.secondary">{translate("ai.common.model")}</Typography>
                        <Typography variant="body2">{record.model || "--"}</Typography>
                    </Grid>
                    <Grid item xs={12} md={4}>
                        <Typography variant="caption" color="text.secondary">状态</Typography>
                        <Typography variant="caption" color="text.secondary">{translate("common.field.status")}</Typography>
                        <Typography variant="body2">{record.status || "--"}</Typography>
                    </Grid>
                    <Grid item xs={12}>
                        <Typography variant="caption" color="text.secondary">MCP 挂载</Typography>
                        <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.mountedMcp")}</Typography>
                        <Typography variant="body2">{record.mountedMcpNames || "--"}</Typography>
                    </Grid>
                    {record.errorMessage && (
@@ -219,12 +222,12 @@
                    <Grid item xs={12}>
                        <Divider sx={{ my: 1 }} />
                        <Stack direction="row" justifyContent="space-between" alignItems="center" mb={1}>
                            <Typography variant="h6">MCP 工具调用日志</Typography>
                            <Typography variant="h6">{translate("ai.observe.detail.mcpLogs")}</Typography>
                            {loading && <CircularProgress size={20} />}
                        </Stack>
                        {!loading && !logs.length && (
                            <Typography variant="body2" color="text.secondary">
                                当前调用没有产生 MCP 工具日志。
                                {translate("ai.observe.detail.noMcpLogs")}
                            </Typography>
                        )}
                        <Stack spacing={1.5}>
@@ -244,13 +247,11 @@
                                                label={item.status || "--"}
                                            />
                                        </Stack>
                                        <Typography variant="caption" color="text.secondary">输入摘要</Typography>
                                        <Typography variant="caption" color="text.secondary">{translate("ai.observe.detail.inputSummary")}</Typography>
                                        <Typography variant="body2" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
                                            {item.inputSummary || "--"}
                                        </Typography>
                                        <Typography variant="caption" color="text.secondary" display="block" mt={1}>
                                            输出摘要 / 错误
                                        </Typography>
                                        <Typography variant="caption" color="text.secondary" display="block" mt={1}>{translate("ai.observe.detail.outputSummary")}</Typography>
                                        <Typography variant="body2" sx={{ whiteSpace: "pre-wrap", wordBreak: "break-word" }}>
                                            {item.outputSummary || item.errorMessage || "--"}
                                        </Typography>
@@ -262,13 +263,14 @@
                </Grid>
            </DialogContent>
            <DialogActions>
                <Button onClick={onClose}>关闭</Button>
                <Button onClick={onClose}>{translate("ai.common.close")}</Button>
            </DialogActions>
        </Dialog>
    );
};
const AiCallLogCards = ({ onView }) => {
    const translate = useTranslate();
    const { data, isLoading } = useListContext();
    const records = useMemo(() => (Array.isArray(data) ? data : []), [data]);
@@ -284,9 +286,9 @@
        return (
            <Box px={2} py={6}>
                <Card variant="outlined" sx={{ p: 3, textAlign: "center", borderStyle: "dashed" }}>
                    <Typography variant="subtitle1">暂无 AI 调用日志</Typography>
                    <Typography variant="subtitle1">{translate("ai.observe.list.emptyTitle")}</Typography>
                    <Typography variant="body2" color="text.secondary" mt={1}>
                        发起 AI 对话后,这里会展示调用统计和审计记录。
                        {translate("ai.observe.list.emptyDescription")}
                    </Typography>
                </Card>
            </Box>
@@ -297,14 +299,14 @@
        <Box px={2} py={2}>
            <Grid container spacing={2}>
                {records.map((record) => {
                    const statusMeta = resolveStatusChip(record.status);
                    const statusMeta = resolveStatusChip(record.status, translate);
                    return (
                        <Grid item xs={12} md={6} xl={4} key={record.id}>
                            <Card variant="outlined" sx={{ height: "100%", borderRadius: 3, boxShadow: "0 8px 24px rgba(15, 23, 42, 0.06)" }}>
                                <CardContent sx={{ pb: 1.5 }}>
                                    <Stack direction="row" justifyContent="space-between" alignItems="flex-start" spacing={1}>
                                        <Box>
                                            <Typography variant="h6">{record.promptName || "AI 对话"}</Typography>
                                            <Typography variant="h6">{record.promptName || translate("ai.drawer.title")}</Typography>
                                            <Typography variant="body2" color="text.secondary">
                                                {record.promptCode || "--"} / {record.model || "--"}
                                            </Typography>
@@ -312,31 +314,31 @@
                                        <Chip size="small" color={statusMeta.color} label={statusMeta.label} />
                                    </Stack>
                                    <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}>
                                        <Chip size="small" variant="outlined" label={`用户 ${record.userId || "--"}`} />
                                        <Chip size="small" variant="outlined" label={`耗时 ${record.elapsedMs ?? 0} ms`} />
                                        <Chip size="small" variant="outlined" label={`Token ${record.totalTokens ?? 0}`} />
                                        <Chip size="small" variant="outlined" label={translate("ai.observe.list.userValue", { value: record.userId || "--" })} />
                                        <Chip size="small" variant="outlined" label={translate("ai.observe.list.elapsedValue", { value: record.elapsedMs ?? 0 })} />
                                        <Chip size="small" variant="outlined" label={translate("ai.observe.list.tokenValue", { value: record.totalTokens ?? 0 })} />
                                    </Stack>
                                    <Divider sx={{ my: 1.5 }} />
                                    <Typography variant="caption" color="text.secondary">请求ID</Typography>
                                    <Typography variant="caption" color="text.secondary">{translate("ai.observe.fields.requestId")}</Typography>
                                    <Typography variant="body2" sx={{ wordBreak: "break-all" }}>{record.requestId || "--"}</Typography>
                                    <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>
                                        MCP / 工具调用
                                    </Typography>
                                    <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("ai.observe.list.mcpToolCalls")}</Typography>
                                    <Typography variant="body2">
                                        挂载 {record.mountedMcpCount ?? 0} 个,工具成功 {record.toolSuccessCount ?? 0},失败 {record.toolFailureCount ?? 0}
                                        {translate("ai.observe.list.mcpToolSummary", {
                                            mcp: record.mountedMcpCount ?? 0,
                                            success: record.toolSuccessCount ?? 0,
                                            failure: record.toolFailureCount ?? 0,
                                        })}
                                    </Typography>
                                    {record.errorMessage && (
                                        <>
                                            <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>
                                                错误
                                            </Typography>
                                            <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("ai.common.error")}</Typography>
                                            <Typography variant="body2">{record.errorMessage}</Typography>
                                        </>
                                    )}
                                </CardContent>
                                <CardActions sx={{ px: 2, pb: 2, pt: 0 }}>
                                    <Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => onView(record)}>
                                        详情
                                        {translate("ai.common.detail")}
                                    </Button>
                                </CardActions>
                            </Card>
rsf-admin/src/page/system/aiMcpMount/AiMcpMountForm.jsx
@@ -5,6 +5,7 @@
    SelectInput,
    TextInput,
    useNotify,
    useTranslate,
} from "react-admin";
import { Alert, Button, Grid, Stack, Typography } from "@mui/material";
import StatusSelectInput from "@/page/components/StatusSelectInput";
@@ -18,6 +19,7 @@
const AiMcpDraftTestSection = ({ formData, readOnly }) => {
    const notify = useNotify();
    const translate = useTranslate();
    const [loading, setLoading] = useState(false);
    const [result, setResult] = useState(null);
@@ -26,11 +28,11 @@
        try {
            const data = await validateDraftMcpConnectivity(formData);
            setResult(data);
            notify(data?.message || "草稿连通性测试完成");
            notify(data?.message || translate("ai.mcp.connectivity.success"));
        } catch (error) {
            const nextResult = {
                healthStatus: "UNHEALTHY",
                message: error?.message || "草稿连通性测试失败",
                message: error?.message || translate("ai.mcp.connectivity.failed"),
            };
            setResult(nextResult);
            notify(nextResult.message, { type: "error" });
@@ -48,10 +50,10 @@
            <Grid item xs={12}>
                <Stack direction="row" spacing={1} alignItems="center">
                    <Button variant="outlined" onClick={handleValidate} disabled={loading}>
                        {loading ? "测试中..." : "保存前测试"}
                        {loading ? translate("ai.common.testing") : translate("ai.mcp.form.testBeforeSave")}
                    </Button>
                    <Typography variant="body2" color="text.secondary">
                        用当前草稿配置直接校验连通性,不会落库。
                        {translate("ai.mcp.form.testDescription")}
                    </Typography>
                </Stack>
            </Grid>
@@ -68,16 +70,19 @@
    );
};
const AiMcpMountForm = ({ readOnly = false }) => (
const AiMcpMountForm = ({ readOnly = false }) => {
    const translate = useTranslate();
    return (
    <Grid container spacing={2} width={{ xs: "100%", xl: "80%" }}>
        <Grid item xs={12}>
            <Typography variant="h6">MCP 挂载配置</Typography>
            <Typography variant="h6">{translate("ai.mcp.form.sections.main")}</Typography>
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="name" label="名称" fullWidth disabled={readOnly} />
            <TextInput source="name" label="common.field.name" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={6}>
            <SelectInput source="transportType" label="传输类型" choices={transportChoices} fullWidth disabled={readOnly} />
            <SelectInput source="transportType" label="ai.mcp.fields.transportType" choices={transportChoices} fullWidth disabled={readOnly} />
        </Grid>
        <FormDataConsumer>
            {({ formData }) => (
@@ -87,7 +92,7 @@
                            <Grid item xs={12}>
                                <SelectInput
                                    source="builtinCode"
                                    label="内置 MCP"
                                    label="ai.mcp.fields.builtinCode"
                                    choices={[
                                        { id: "RSF_WMS", name: "RSF_WMS" },
                                        { id: "RSF_WMS_STOCK", name: "RSF_WMS_STOCK" },
@@ -103,7 +108,7 @@
                    {formData.transportType === "SSE_HTTP" && (
                        <>
                            <Grid item xs={12}>
                                <TextInput source="serverUrl" label="服务地址" fullWidth disabled={readOnly} />
                                <TextInput source="serverUrl" label="ai.mcp.fields.serverUrl" fullWidth disabled={readOnly} />
                            </Grid>
                            <Grid item xs={12}>
                                <TextInput source="endpoint" label="SSE Endpoint" fullWidth disabled={readOnly} />
@@ -116,7 +121,7 @@
                    {formData.transportType === "STDIO" && (
                        <>
                            <Grid item xs={12}>
                                <TextInput source="command" label="命令" fullWidth disabled={readOnly} />
                                <TextInput source="command" label="ai.mcp.fields.command" fullWidth disabled={readOnly} />
                            </Grid>
                            <Grid item xs={12}>
                                <TextInput source="argsJson" label="Args JSON" fullWidth multiline minRows={4} disabled={readOnly} />
@@ -133,39 +138,40 @@
            <NumberInput source="requestTimeoutMs" label="Timeout(ms)" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={4}>
            <NumberInput source="sort" label="排序" fullWidth disabled={readOnly} />
            <NumberInput source="sort" label="ai.mcp.fields.sort" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={4}>
            <StatusSelectInput disabled={readOnly} />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="memo" label="备注" fullWidth multiline minRows={3} disabled={readOnly} />
            <TextInput source="memo" label="common.field.memo" fullWidth multiline minRows={3} disabled={readOnly} />
        </Grid>
        <FormDataConsumer>
            {({ formData }) => <AiMcpDraftTestSection formData={formData} readOnly={readOnly} />}
        </FormDataConsumer>
        <Grid item xs={12}>
            <Typography variant="h6">运行态信息</Typography>
            <Typography variant="h6">{translate("ai.mcp.form.sections.runtime")}</Typography>
        </Grid>
        <Grid item xs={12} md={4}>
            <TextInput source="healthStatus" label="健康状态" fullWidth disabled />
            <TextInput source="healthStatus" label="ai.mcp.fields.healthStatus" fullWidth disabled />
        </Grid>
        <Grid item xs={12} md={4}>
            <TextInput source="lastInitElapsedMs" label="最近初始化耗时(ms)" fullWidth disabled />
            <TextInput source="lastInitElapsedMs" label="ai.mcp.fields.lastInitElapsedMs" fullWidth disabled />
        </Grid>
        <Grid item xs={12} md={4}>
            <TextInput source="lastTestTime$" label="最近测试时间" fullWidth disabled />
            <TextInput source="lastTestTime$" label="ai.mcp.fields.lastTestTime" fullWidth disabled />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="lastTestMessage" label="最近测试结果" fullWidth multiline minRows={3} disabled />
            <TextInput source="lastTestMessage" label="ai.mcp.fields.lastTestMessage" fullWidth multiline minRows={3} disabled />
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="updateBy" label="最近更新人" fullWidth disabled />
            <TextInput source="updateBy" label="ai.common.lastUpdatedBy" fullWidth disabled />
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="updateTime$" label="最近更新时间" fullWidth disabled />
            <TextInput source="updateTime$" label="ai.common.lastUpdatedAt" fullWidth disabled />
        </Grid>
    </Grid>
);
    );
};
export default AiMcpMountForm;
rsf-admin/src/page/system/aiMcpMount/AiMcpMountList.jsx
@@ -9,6 +9,7 @@
    useListContext,
    useNotify,
    useRefresh,
    useTranslate,
} from "react-admin";
import {
    Box,
@@ -39,7 +40,7 @@
    <SearchInput source="condition" alwaysOn />,
    <SelectInput
        source="transportType"
        label="传输类型"
        label="ai.mcp.fields.transportType"
        choices={[
            { id: "SSE_HTTP", name: "SSE_HTTP" },
            { id: "STDIO", name: "STDIO" },
@@ -48,7 +49,7 @@
    />,
    <SelectInput
        source="status"
        label="状态"
        label="common.field.status"
        choices={[
            { id: "1", name: "common.enums.statusTrue" },
            { id: "0", name: "common.enums.statusFalse" },
@@ -83,22 +84,23 @@
};
const transportGroups = [
    { key: "BUILTIN", title: "内置 MCP", description: "系统内置工具挂载,适合直接暴露平台能力。" },
    { key: "SSE_HTTP", title: "远程 SSE MCP", description: "通过远程 MCP Server 挂载外部工具。" },
    { key: "STDIO", title: "本地 STDIO MCP", description: "通过本地命令进程挂载外部 MCP。" },
    { key: "BUILTIN", titleKey: "ai.mcp.groups.builtin.title", descriptionKey: "ai.mcp.groups.builtin.description" },
    { key: "SSE_HTTP", titleKey: "ai.mcp.groups.sse.title", descriptionKey: "ai.mcp.groups.sse.description" },
    { key: "STDIO", titleKey: "ai.mcp.groups.stdio.title", descriptionKey: "ai.mcp.groups.stdio.description" },
];
const resolveHealthMeta = (record) => {
const resolveHealthMeta = (record, translate) => {
    if (record.healthStatus === "HEALTHY") {
        return { color: "success", label: "正常" };
        return { color: "success", label: translate("ai.mcp.health.healthy") };
    }
    if (record.healthStatus === "UNHEALTHY") {
        return { color: "error", label: "失败" };
        return { color: "error", label: translate("ai.mcp.health.unhealthy") };
    }
    return { color: "default", label: "未测试" };
    return { color: "default", label: translate("ai.common.notTested") };
};
const AiMcpMountCards = ({ onView, onEdit, onDelete, onConnectivityTest, deleting, testingConnectivityId }) => {
    const translate = useTranslate();
    const { data, isLoading } = useListContext();
    const records = useMemo(() => (Array.isArray(data) ? data : []), [data]);
    const groupedRecords = useMemo(() => {
@@ -120,9 +122,9 @@
        return (
            <Box px={2} py={6}>
                <Card variant="outlined" sx={{ p: 3, textAlign: "center", borderStyle: "dashed" }}>
                    <Typography variant="subtitle1">暂无 MCP 挂载</Typography>
                    <Typography variant="subtitle1">{translate("ai.mcp.list.emptyTitle")}</Typography>
                    <Typography variant="body2" color="text.secondary" mt={1}>
                        可以新建内置 MCP、远程 SSE 挂载或本地 STDIO 挂载。
                        {translate("ai.mcp.list.emptyDescription")}
                    </Typography>
                </Card>
            </Box>
@@ -135,14 +137,14 @@
                {groupedRecords.map((group) => (
                    <Box key={group.key}>
                        <Box mb={1.5}>
                            <Typography variant="h6">{group.title}</Typography>
                            <Typography variant="h6">{translate(group.titleKey)}</Typography>
                            <Typography variant="body2" color="text.secondary">
                                {group.description}
                                {translate(group.descriptionKey)}
                            </Typography>
                        </Box>
                        <Grid container spacing={2}>
                            {group.records.map((record) => {
                                const healthMeta = resolveHealthMeta(record);
                                const healthMeta = resolveHealthMeta(record, translate);
                                return (
                                    <Grid item xs={12} md={6} xl={4} key={record.id}>
                                        <Card
@@ -167,39 +169,35 @@
                                                        <Chip
                                                            size="small"
                                                            color={record.statusBool ? "success" : "default"}
                                                            label={record.statusBool ? "启用" : "停用"}
                                                            label={translate(record.statusBool ? "ai.common.enabled" : "ai.common.disabled")}
                                                        />
                                                        <Chip size="small" color={healthMeta.color} label={healthMeta.label} />
                                                    </Stack>
                                                </Stack>
                                                <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}>
                                                    <Chip size="small" variant="outlined" label={`排序 ${record.sort ?? 0}`} />
                                                    <Chip size="small" variant="outlined" label={translate("ai.mcp.list.sortValue", { value: record.sort ?? 0 })} />
                                                    <Chip size="small" variant="outlined" label={`${record.requestTimeoutMs ?? "--"} ms`} />
                                                    <Chip size="small" variant="outlined" label={`Init ${record.lastInitElapsedMs ?? "--"} ms`} />
                                                </Stack>
                                                <Divider sx={{ my: 1.5 }} />
                                                <Typography variant="caption" color="text.secondary">目标</Typography>
                                                <Typography variant="caption" color="text.secondary">{translate("ai.common.target")}</Typography>
                                                <Typography variant="body2" sx={{ mt: 0.5, wordBreak: "break-all" }}>
                                                    {truncateText(resolveTargetLabel(record), 120)}
                                                </Typography>
                                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>
                                                    最近测试
                                                </Typography>
                                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("ai.common.lastTest")}</Typography>
                                                <Typography variant="body2">
                                                    {record.lastTestTime$ ? `${record.lastTestTime$} · ${truncateText(record.lastTestMessage, 72)}` : "尚未执行连通性测试"}
                                                    {record.lastTestTime$ ? `${record.lastTestTime$} · ${truncateText(record.lastTestMessage, 72)}` : translate("ai.mcp.list.noConnectivityTest")}
                                                </Typography>
                                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>
                                                    备注
                                                </Typography>
                                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("common.field.memo")}</Typography>
                                                <Typography variant="body2">{truncateText(record.memo)}</Typography>
                                            </CardContent>
                                            <CardActions sx={{ px: 2, pb: 2, pt: 0, justifyContent: "space-between", alignItems: "flex-start" }}>
                                                <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap>
                                                    <Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => onView(record.id)}>
                                                        详情
                                                        {translate("ai.common.detail")}
                                                    </Button>
                                                    <Button size="small" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(record.id)}>
                                                        编辑
                                                        {translate("common.button.edit")}
                                                    </Button>
                                                    <Button
                                                        size="small"
@@ -207,7 +205,7 @@
                                                        onClick={() => onConnectivityTest(record)}
                                                        disabled={testingConnectivityId === record.id}
                                                    >
                                                        {testingConnectivityId === record.id ? "测试中..." : "连通测试"}
                                                        {testingConnectivityId === record.id ? translate("ai.common.testing") : translate("ai.mcp.list.connectivityTest")}
                                                    </Button>
                                                </Stack>
                                                <Button
@@ -217,7 +215,7 @@
                                                    onClick={() => onDelete(record)}
                                                    disabled={deleting}
                                                >
                                                    删除
                                                    {translate("ai.common.delete")}
                                                </Button>
                                            </CardActions>
                                        </Card>
@@ -233,6 +231,7 @@
};
const AiMcpMountList = () => {
    const translate = useTranslate();
    const notify = useNotify();
    const refresh = useRefresh();
    const [deleteOne, { isPending: deleting }] = useDelete();
@@ -243,7 +242,7 @@
    const closeDialog = () => setDialogState({ open: false, mode: "create", recordId: null });
    const handleDelete = (record) => {
        if (!record?.id || !window.confirm(`确认删除“${record.name}”吗?`)) {
        if (!record?.id || !window.confirm(translate("ai.common.confirmDelete", { name: record.name }))) {
            return;
        }
        deleteOne(
@@ -251,11 +250,11 @@
            { id: record.id },
            {
                onSuccess: () => {
                    notify("删除成功");
                    notify(translate("ai.common.deleteSuccess"));
                    refresh();
                },
                onError: (error) => {
                    notify(error?.message || "删除失败", { type: "error" });
                    notify(error?.message || translate("ai.common.deleteFailed"), { type: "error" });
                },
            }
        );
@@ -268,19 +267,19 @@
        setTestingConnectivityId(record.id);
        try {
            const result = await testMcpConnectivity(record.id);
            notify(result?.message || "连通性测试完成");
            notify(result?.message || translate("ai.mcp.connectivity.success"));
            refresh();
        } catch (error) {
            notify(error?.message || "连通性测试失败", { type: "error" });
            notify(error?.message || translate("ai.mcp.connectivity.failed"), { type: "error" });
        } finally {
            setTestingConnectivityId(null);
        }
    };
    const dialogTitle = {
        create: "新建 MCP 挂载",
        edit: "编辑 MCP 挂载",
        show: "查看 MCP 挂载详情",
        create: translate("ai.mcp.dialog.create"),
        edit: translate("ai.mcp.dialog.edit"),
        show: translate("ai.mcp.dialog.show"),
    }[dialogState.mode];
    return (
@@ -293,7 +292,7 @@
                    <TopToolbar>
                        <FilterButton />
                        <Button variant="contained" startIcon={<AddRoundedIcon />} onClick={() => openDialog("create")}>
                            新建
                            {translate("ai.common.new")}
                        </Button>
                        <MyExportButton />
                    </TopToolbar>
rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx
@@ -1,4 +1,5 @@
import React, { useEffect, useMemo, useState } from "react";
import { useTranslate, useNotify } from "react-admin";
import {
    Accordion,
    AccordionDetails,
@@ -18,10 +19,9 @@
import PlayCircleOutlineOutlinedIcon from "@mui/icons-material/PlayCircleOutlineOutlined";
import PreviewOutlinedIcon from "@mui/icons-material/PreviewOutlined";
import ExpandMoreOutlinedIcon from "@mui/icons-material/ExpandMoreOutlined";
import { useNotify } from "react-admin";
import { previewMcpTools, testMcpConnectivity, testMcpTool } from "@/api/ai/mcpMount";
const parseInputSchema = (inputSchema) => {
const parseInputSchema = (inputSchema, translate) => {
    if (!inputSchema) {
        return { pretty: "", fields: [], required: [], error: "" };
    }
@@ -46,7 +46,7 @@
            pretty: inputSchema,
            fields: [],
            required: [],
            error: `Input Schema 解析失败: ${error.message}`,
            error: translate("ai.mcp.tools.schemaParseFailed", { message: error.message }),
        };
    }
};
@@ -118,6 +118,7 @@
const AiMcpMountToolsPanel = ({ mountId }) => {
    const notify = useNotify();
    const translate = useTranslate();
    const [loading, setLoading] = useState(false);
    const [tools, setTools] = useState([]);
    const [error, setError] = useState("");
@@ -130,10 +131,10 @@
    const schemaInfoMap = useMemo(() => {
        return tools.reduce((result, tool) => {
            result[tool.name] = parseInputSchema(tool.inputSchema);
            result[tool.name] = parseInputSchema(tool.inputSchema, translate);
            return result;
        }, {});
    }, [tools]);
    }, [tools, translate]);
    useEffect(() => {
        if (!mountId) {
@@ -158,7 +159,7 @@
            setInputs({});
            setStructuredInputs({});
        } catch (requestError) {
            setError(requestError.message || "获取工具列表失败");
            setError(requestError.message || translate("ai.mcp.tools.loadFailed"));
        } finally {
            setLoading(false);
        }
@@ -169,9 +170,9 @@
        try {
            const result = await testMcpConnectivity(mountId);
            setConnectivity(result);
            notify(result?.message || "连通性测试完成");
            notify(result?.message || translate("ai.mcp.connectivity.success"));
        } catch (requestError) {
            const message = requestError.message || "连通性测试失败";
            const message = requestError.message || translate("ai.mcp.connectivity.failed");
            notify(message, { type: "error" });
        } finally {
            setTestingConnectivity(false);
@@ -210,7 +211,7 @@
    const handleTest = async (toolName) => {
        const inputJson = inputs[toolName];
        if (!inputJson || !inputJson.trim()) {
            notify("请输入工具测试 JSON", { type: "warning" });
            notify(translate("ai.mcp.tools.inputRequired"), { type: "warning" });
            return;
        }
        setTestingToolName(toolName);
@@ -223,9 +224,9 @@
                ...prev,
                [toolName]: result?.output || "",
            }));
            notify(`工具 ${toolName} 测试完成`);
            notify(translate("ai.mcp.tools.testSuccess", { name: toolName }));
        } catch (requestError) {
            const message = requestError.message || "工具测试失败";
            const message = requestError.message || translate("ai.mcp.tools.testFailed");
            setOutputs((prev) => ({
                ...prev,
                [toolName]: message,
@@ -239,7 +240,7 @@
    if (!mountId) {
        return (
            <Alert severity="info" sx={{ mt: 2 }}>
                保存挂载后即可预览工具并执行测试。
                {translate("ai.mcp.tools.saveBeforePreview")}
            </Alert>
        );
    }
@@ -249,16 +250,16 @@
            <Accordion defaultExpanded={false} sx={{ borderRadius: 3, overflow: "hidden" }}>
                <AccordionSummary expandIcon={<ExpandMoreOutlinedIcon />}>
                    <Box flex={1}>
                        <Typography variant="h6">工具预览与测试</Typography>
                        <Typography variant="h6">{translate("ai.mcp.tools.title")}</Typography>
                        <Typography variant="body2" color="text.secondary">
                            支持连通性测试、结构化 Schema 预览和按输入参数自动生成测试表单。
                            {translate("ai.mcp.tools.description")}
                        </Typography>
                    </Box>
                </AccordionSummary>
                <AccordionDetails>
                    <Stack direction="row" justifyContent="space-between" alignItems="center" mb={1.5} flexWrap="wrap" useFlexGap>
                        <Button size="small" startIcon={<PreviewOutlinedIcon />} onClick={loadTools} disabled={loading}>
                            刷新工具
                            {translate("ai.mcp.tools.refresh")}
                        </Button>
                        <Button
                            size="small"
@@ -267,7 +268,7 @@
                            onClick={handleConnectivityTest}
                            disabled={testingConnectivity}
                        >
                            {testingConnectivity ? "测试中..." : "连通性测试"}
                            {testingConnectivity ? translate("ai.common.testing") : translate("ai.mcp.list.connectivityTest")}
                        </Button>
                    </Stack>
                    {!!connectivity && (
@@ -289,7 +290,7 @@
                        </Alert>
                    )}
                    {!loading && !error && !tools.length && (
                        <Alert severity="info">当前挂载未解析出任何工具。</Alert>
                        <Alert severity="info">{translate("ai.mcp.tools.noTools")}</Alert>
                    )}
                    <Grid container spacing={2}>
                        {tools.map((tool) => {
@@ -303,11 +304,11 @@
                                                <Box>
                                                    <Typography variant="subtitle1">{tool.name}</Typography>
                                                    <Typography variant="body2" color="text.secondary">
                                                        {tool.description || "暂无描述"}
                                                        {tool.description || translate("ai.common.none")}
                                                    </Typography>
                                                    {!!tool.toolPurpose && (
                                                        <Typography variant="caption" color="text.secondary" display="block" mt={0.5}>
                                                            用途: {tool.toolPurpose}
                                                            {translate("ai.mcp.tools.purpose", { value: tool.toolPurpose })}
                                                        </Typography>
                                                    )}
                                                </Box>
@@ -318,7 +319,7 @@
                                                        </Typography>
                                                    )}
                                                    <Typography variant="caption" color="text.secondary">
                                                        {schemaInfo.fields.length} 个参数
                                                        {translate("ai.mcp.tools.fieldCount", { count: schemaInfo.fields.length })}
                                                    </Typography>
                                                    <Typography variant="caption" color="text.secondary">
                                                        {tool.returnDirect ? "returnDirect" : "normal"}
@@ -331,13 +332,13 @@
                                                <CardContent>
                                                    {!!tool.queryBoundary && (
                                                        <Alert severity="info" sx={{ mb: 2 }}>
                                                            查询边界: {tool.queryBoundary}
                                                            {translate("ai.mcp.tools.queryBoundary", { value: tool.queryBoundary })}
                                                        </Alert>
                                                    )}
                                                    {!!tool.exampleQuestions?.length && (
                                                        <Alert severity="success" sx={{ mb: 2 }}>
                                                            <Typography variant="body2" fontWeight={700} mb={0.5}>
                                                                示例提问
                                                                {translate("ai.mcp.tools.exampleQuestions")}
                                                            </Typography>
                                                            {tool.exampleQuestions.map((question) => (
                                                                <Typography key={question} variant="body2">
@@ -352,7 +353,7 @@
                                                        </Alert>
                                                    )}
                                                    <TextField
                                                        label="格式化 Input Schema"
                                                        label={translate("ai.mcp.tools.formattedSchema")}
                                                        value={schemaInfo.pretty || tool.inputSchema || ""}
                                                        fullWidth
                                                        multiline
@@ -391,7 +392,7 @@
                                                        </Grid>
                                                    )}
                                                    <TextField
                                                        label="测试输入 JSON"
                                                        label={translate("ai.mcp.tools.testInput")}
                                                        value={inputs[tool.name] || ""}
                                                        onChange={(event) => handleInputChange(tool.name, event.target.value)}
                                                        fullWidth
@@ -399,7 +400,7 @@
                                                        minRows={5}
                                                        maxRows={12}
                                                        sx={{ mt: 2 }}
                                                        placeholder='例如:{"code":"A01"}'
                                                        placeholder={translate("ai.mcp.tools.testInputPlaceholder")}
                                                    />
                                                    <Stack direction="row" justifyContent="flex-end" mt={1.5}>
                                                        <Button
@@ -408,11 +409,11 @@
                                                            onClick={() => handleTest(tool.name)}
                                                            disabled={testingToolName === tool.name}
                                                        >
                                                            {testingToolName === tool.name ? "测试中..." : "执行测试"}
                                                            {testingToolName === tool.name ? translate("ai.common.testing") : translate("ai.mcp.tools.executeTest")}
                                                        </Button>
                                                    </Stack>
                                                    <TextField
                                                        label="测试结果"
                                                        label={translate("ai.mcp.tools.testResult")}
                                                        value={outputs[tool.name] || ""}
                                                        fullWidth
                                                        multiline
rsf-admin/src/page/system/aiParam/AiParamForm.jsx
@@ -6,6 +6,7 @@
    SelectInput,
    TextInput,
    useNotify,
    useTranslate,
} from "react-admin";
import { Alert, Button, Grid, Stack, Typography } from "@mui/material";
import StatusSelectInput from "@/page/components/StatusSelectInput";
@@ -17,6 +18,7 @@
const AiParamValidateSection = ({ formData, readOnly }) => {
    const notify = useNotify();
    const translate = useTranslate();
    const [loading, setLoading] = useState(false);
    const [result, setResult] = useState(null);
@@ -25,11 +27,11 @@
        try {
            const data = await validateAiParamDraft(formData);
            setResult(data);
            notify(data?.message || "AI 参数验证成功");
            notify(data?.message || translate("ai.param.validate.success"));
        } catch (error) {
            const nextResult = {
                status: "INVALID",
                message: error?.message || "AI 参数验证失败",
                message: error?.message || translate("ai.param.validate.failed"),
            };
            setResult(nextResult);
            notify(nextResult.message, { type: "error" });
@@ -44,10 +46,10 @@
                <Grid item xs={12}>
                    <Stack direction="row" spacing={1} alignItems="center">
                        <Button variant="outlined" onClick={handleValidate} disabled={loading}>
                            {loading ? "验证中..." : "保存前验证"}
                            {loading ? translate("ai.param.validate.loading") : translate("ai.param.validate.beforeSave")}
                        </Button>
                        <Typography variant="body2" color="text.secondary">
                            会直接校验当前 Base URL、API Key 与模型是否可调用。
                            {translate("ai.param.validate.description")}
                        </Typography>
                    </Stack>
                </Grid>
@@ -65,72 +67,76 @@
    );
};
const AiParamForm = ({ readOnly = false }) => (
const AiParamForm = ({ readOnly = false }) => {
    const translate = useTranslate();
    return (
    <Grid container spacing={2} width={{ xs: "100%", xl: "80%" }}>
        <Grid item xs={12}>
            <Typography variant="h6">主要配置</Typography>
            <Typography variant="h6">{translate("ai.param.form.sections.main")}</Typography>
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="name" label="名称" fullWidth disabled={readOnly} />
            <TextInput source="name" label="common.field.name" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={6}>
            <SelectInput source="providerType" label="提供方类型" choices={providerChoices} fullWidth disabled={readOnly} />
            <SelectInput source="providerType" label="ai.param.fields.providerType" choices={providerChoices} fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="baseUrl" label="Base URL" fullWidth disabled={readOnly} />
            <TextInput source="baseUrl" label="ai.param.fields.baseUrl" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="apiKey" label="API Key" fullWidth disabled={readOnly} />
            <TextInput source="apiKey" label="ai.param.fields.apiKey" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="model" label="模型" fullWidth disabled={readOnly} />
            <TextInput source="model" label="ai.param.fields.model" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={3}>
            <NumberInput source="temperature" label="Temperature" fullWidth disabled={readOnly} />
            <NumberInput source="temperature" label="ai.param.fields.temperature" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={3}>
            <NumberInput source="topP" label="Top P" fullWidth disabled={readOnly} />
            <NumberInput source="topP" label="ai.param.fields.topP" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={3}>
            <NumberInput source="maxTokens" label="Max Tokens" fullWidth disabled={readOnly} />
            <NumberInput source="maxTokens" label="ai.param.fields.maxTokens" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={3}>
            <NumberInput source="timeoutMs" label="Timeout(ms)" fullWidth disabled={readOnly} />
            <NumberInput source="timeoutMs" label="ai.param.fields.timeoutMs" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={6}>
            <BooleanInput source="streamingEnabled" label="启用流式响应" disabled={readOnly} />
            <BooleanInput source="streamingEnabled" label="ai.param.fields.streamingEnabled" disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={6}>
            <StatusSelectInput disabled={readOnly} />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="memo" label="备注" fullWidth multiline minRows={3} disabled={readOnly} />
            <TextInput source="memo" label="common.field.memo" fullWidth multiline minRows={3} disabled={readOnly} />
        </Grid>
        <FormDataConsumer>
            {({ formData }) => <AiParamValidateSection formData={formData} readOnly={readOnly} />}
        </FormDataConsumer>
        <Grid item xs={12}>
            <Typography variant="h6">运行与审计信息</Typography>
            <Typography variant="h6">{translate("ai.param.form.sections.runtime")}</Typography>
        </Grid>
        <Grid item xs={12} md={3}>
            <TextInput source="validateStatus" label="最近校验状态" fullWidth disabled />
            <TextInput source="validateStatus" label="ai.param.fields.validateStatus" fullWidth disabled />
        </Grid>
        <Grid item xs={12} md={3}>
            <TextInput source="lastValidateElapsedMs" label="最近校验耗时(ms)" fullWidth disabled />
            <TextInput source="lastValidateElapsedMs" label="ai.param.fields.lastValidateElapsedMs" fullWidth disabled />
        </Grid>
        <Grid item xs={12} md={3}>
            <TextInput source="lastValidateTime$" label="最近校验时间" fullWidth disabled />
            <TextInput source="lastValidateTime$" label="ai.param.fields.lastValidateTime" fullWidth disabled />
        </Grid>
        <Grid item xs={12} md={3}>
            <TextInput source="updateBy" label="最近更新人" fullWidth disabled />
            <TextInput source="updateBy" label="ai.common.lastUpdatedBy" fullWidth disabled />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="lastValidateMessage" label="最近校验结果" fullWidth multiline minRows={3} disabled />
            <TextInput source="lastValidateMessage" label="ai.param.fields.lastValidateMessage" fullWidth multiline minRows={3} disabled />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="updateTime$" label="最近更新时间" fullWidth disabled />
            <TextInput source="updateTime$" label="ai.common.lastUpdatedAt" fullWidth disabled />
        </Grid>
    </Grid>
);
    );
};
export default AiParamForm;
rsf-admin/src/page/system/aiParam/AiParamList.jsx
@@ -10,6 +10,7 @@
    useListContext,
    useNotify,
    useRefresh,
    useTranslate,
} from "react-admin";
import {
    Box,
@@ -35,11 +36,11 @@
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <TextInput source="providerType" label="提供方类型" />,
    <TextInput source="model" label="模型" />,
    <TextInput source="providerType" label="ai.param.fields.providerType" />,
    <TextInput source="model" label="ai.param.fields.model" />,
    <SelectInput
        source="status"
        label="状态"
        label="common.field.status"
        choices={[
            { id: "1", name: "common.enums.statusTrue" },
            { id: "0", name: "common.enums.statusFalse" },
@@ -64,6 +65,7 @@
};
const AiParamCards = ({ onView, onEdit, onDelete, deleting }) => {
    const translate = useTranslate();
    const { data, isLoading } = useListContext();
    const records = useMemo(() => (Array.isArray(data) ? data : []), [data]);
@@ -79,9 +81,9 @@
        return (
            <Box px={2} py={6}>
                <Card variant="outlined" sx={{ p: 3, textAlign: "center", borderStyle: "dashed" }}>
                    <Typography variant="subtitle1">暂无 AI 参数配置</Typography>
                    <Typography variant="subtitle1">{translate("ai.param.list.emptyTitle")}</Typography>
                    <Typography variant="body2" color="text.secondary" mt={1}>
                        可以先新建一个 OpenAI 兼容模型参数卡片。
                        {translate("ai.param.list.emptyDescription")}
                    </Typography>
                </Card>
            </Box>
@@ -114,7 +116,7 @@
                                    <Chip
                                        size="small"
                                        color={record.statusBool ? "success" : "default"}
                                        label={record.statusBool ? "启用" : "停用"}
                                        label={translate(record.statusBool ? "ai.common.enabled" : "ai.common.disabled")}
                                    />
                                </Stack>
                                <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}>
@@ -123,7 +125,7 @@
                                        size="small"
                                        variant="outlined"
                                        color={record.streamingEnabled ? "info" : "default"}
                                        label={record.streamingEnabled ? "流式响应" : "非流式"}
                                        label={translate(record.streamingEnabled ? "ai.param.list.streaming" : "ai.param.list.nonStreaming")}
                                    />
                                </Stack>
                                <Divider sx={{ my: 1.5 }} />
@@ -151,18 +153,16 @@
                                        <Typography variant="body2">{record.timeoutMs ?? "--"} ms</Typography>
                                    </Grid>
                                </Grid>
                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>
                                    备注
                                </Typography>
                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("common.field.memo")}</Typography>
                                <Typography variant="body2">{truncateText(record.memo)}</Typography>
                            </CardContent>
                            <CardActions sx={{ px: 2, pb: 2, pt: 0, justifyContent: "space-between" }}>
                                <Stack direction="row" spacing={1}>
                                    <Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => onView(record.id)}>
                                        详情
                                        {translate("ai.common.detail")}
                                    </Button>
                                    <Button size="small" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(record.id)}>
                                        编辑
                                        {translate("common.button.edit")}
                                    </Button>
                                </Stack>
                                <Button
@@ -172,7 +172,7 @@
                                    onClick={() => onDelete(record)}
                                    disabled={deleting}
                                >
                                    删除
                                    {translate("ai.common.delete")}
                                </Button>
                            </CardActions>
                        </Card>
@@ -184,6 +184,7 @@
};
const AiParamList = () => {
    const translate = useTranslate();
    const notify = useNotify();
    const refresh = useRefresh();
    const [deleteOne, { isPending: deleting }] = useDelete();
@@ -193,7 +194,7 @@
    const closeDialog = () => setDialogState({ open: false, mode: "create", recordId: null });
    const handleDelete = (record) => {
        if (!record?.id || !window.confirm(`确认删除“${record.name}”吗?`)) {
        if (!record?.id || !window.confirm(translate("ai.common.confirmDelete", { name: record.name }))) {
            return;
        }
        deleteOne(
@@ -201,20 +202,20 @@
            { id: record.id },
            {
                onSuccess: () => {
                    notify("删除成功");
                    notify(translate("ai.common.deleteSuccess"));
                    refresh();
                },
                onError: (error) => {
                    notify(error?.message || "删除失败", { type: "error" });
                    notify(error?.message || translate("ai.common.deleteFailed"), { type: "error" });
                },
            }
        );
    };
    const dialogTitle = {
        create: "新建 AI 参数",
        edit: "编辑 AI 参数",
        show: "查看 AI 参数详情",
        create: translate("ai.param.dialog.create"),
        edit: translate("ai.param.dialog.edit"),
        show: translate("ai.param.dialog.show"),
    }[dialogState.mode];
    return (
@@ -227,7 +228,7 @@
                    <TopToolbar>
                        <FilterButton />
                        <Button variant="contained" startIcon={<AddRoundedIcon />} onClick={() => openDialog("create")}>
                            新建
                            {translate("ai.common.new")}
                        </Button>
                        <MyExportButton />
                    </TopToolbar>
rsf-admin/src/page/system/aiPrompt/AiPromptForm.jsx
@@ -3,6 +3,7 @@
    FormDataConsumer,
    TextInput,
    useNotify,
    useTranslate,
} from "react-admin";
import { Alert, Button, Grid, Stack, TextField, Typography } from "@mui/material";
import StatusSelectInput from "@/page/components/StatusSelectInput";
@@ -10,7 +11,8 @@
const AiPromptPreviewSection = ({ formData }) => {
    const notify = useNotify();
    const [input, setInput] = useState("请帮我总结当前页面能做什么");
    const translate = useTranslate();
    const [input, setInput] = useState(() => translate("ai.prompt.preview.defaultInput"));
    const [metadataText, setMetadataText] = useState("{\"path\":\"/system/aiPrompt\"}");
    const [preview, setPreview] = useState(null);
    const [loading, setLoading] = useState(false);
@@ -25,10 +27,10 @@
                metadata,
            });
            setPreview(data);
            notify("Prompt 预览完成");
            notify(translate("ai.prompt.preview.success"));
        } catch (error) {
            setPreview(null);
            notify(error?.message || "Prompt 预览失败", { type: "error" });
            notify(error?.message || translate("ai.prompt.preview.failed"), { type: "error" });
        } finally {
            setLoading(false);
        }
@@ -37,11 +39,11 @@
    return (
        <>
            <Grid item xs={12}>
                <Typography variant="h6">Prompt 预览</Typography>
                <Typography variant="h6">{translate("ai.prompt.preview.title")}</Typography>
            </Grid>
            <Grid item xs={12}>
                <TextField
                    label="示例输入"
                    label={translate("ai.prompt.preview.input")}
                    value={input}
                    onChange={(event) => setInput(event.target.value)}
                    fullWidth
@@ -51,7 +53,7 @@
            </Grid>
            <Grid item xs={12}>
                <TextField
                    label="示例元数据 JSON"
                    label={translate("ai.prompt.preview.metadata")}
                    value={metadataText}
                    onChange={(event) => setMetadataText(event.target.value)}
                    fullWidth
@@ -62,10 +64,10 @@
            <Grid item xs={12}>
                <Stack direction="row" spacing={1} alignItems="center">
                    <Button variant="outlined" onClick={handlePreview} disabled={loading}>
                        {loading ? "预览中..." : "预览渲染"}
                        {loading ? translate("ai.prompt.preview.loading") : translate("ai.prompt.preview.render")}
                    </Button>
                    <Typography variant="body2" color="text.secondary">
                        用当前表单内容渲染 System Prompt 和 User Prompt。
                        {translate("ai.prompt.preview.description")}
                    </Typography>
                </Stack>
            </Grid>
@@ -73,12 +75,14 @@
                <>
                    <Grid item xs={12}>
                        <Alert severity="success">
                            已解析变量:{(preview.resolvedVariables || []).join(", ") || "无"}
                            {translate("ai.prompt.preview.resolvedVariables", {
                                value: (preview.resolvedVariables || []).join(", ") || translate("ai.common.none"),
                            })}
                        </Alert>
                    </Grid>
                    <Grid item xs={12}>
                        <TextField
                            label="渲染后的 System Prompt"
                            label={translate("ai.prompt.preview.renderedSystemPrompt")}
                            value={preview.renderedSystemPrompt || ""}
                            fullWidth
                            multiline
@@ -88,7 +92,7 @@
                    </Grid>
                    <Grid item xs={12}>
                        <TextField
                            label="渲染后的 User Prompt"
                            label={translate("ai.prompt.preview.renderedUserPrompt")}
                            value={preview.renderedUserPrompt || ""}
                            fullWidth
                            multiline
@@ -102,45 +106,49 @@
    );
};
const AiPromptForm = ({ readOnly = false }) => (
const AiPromptForm = ({ readOnly = false }) => {
    const translate = useTranslate();
    return (
    <Grid container spacing={2} width={{ xs: "100%", xl: "80%" }}>
        <Grid item xs={12}>
            <Typography variant="h6">Prompt 配置</Typography>
            <Typography variant="h6">{translate("ai.prompt.form.sections.main")}</Typography>
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="name" label="名称" fullWidth disabled={readOnly} />
            <TextInput source="name" label="common.field.name" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="code" label="编码" fullWidth disabled={readOnly} />
            <TextInput source="code" label="ai.prompt.fields.code" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="scene" label="场景" fullWidth disabled={readOnly} />
            <TextInput source="scene" label="ai.prompt.fields.scene" fullWidth disabled={readOnly} />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="systemPrompt" label="System Prompt" fullWidth multiline minRows={6} disabled={readOnly} />
            <TextInput source="systemPrompt" label="ai.prompt.fields.systemPrompt" fullWidth multiline minRows={6} disabled={readOnly} />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="userPromptTemplate" label="User Prompt Template" fullWidth multiline minRows={5} disabled={readOnly} />
            <TextInput source="userPromptTemplate" label="ai.prompt.fields.userPromptTemplate" fullWidth multiline minRows={5} disabled={readOnly} />
        </Grid>
        <Grid item xs={12} md={6}>
            <StatusSelectInput disabled={readOnly} />
        </Grid>
        <Grid item xs={12}>
            <TextInput source="memo" label="备注" fullWidth multiline minRows={3} disabled={readOnly} />
            <TextInput source="memo" label="common.field.memo" fullWidth multiline minRows={3} disabled={readOnly} />
        </Grid>
        <FormDataConsumer>
            {({ formData }) => <AiPromptPreviewSection formData={formData} />}
        </FormDataConsumer>
        <Grid item xs={12}>
            <Typography variant="h6">运行与审计信息</Typography>
            <Typography variant="h6">{translate("ai.prompt.form.sections.runtime")}</Typography>
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="updateBy" label="最近更新人" fullWidth disabled />
            <TextInput source="updateBy" label="ai.common.lastUpdatedBy" fullWidth disabled />
        </Grid>
        <Grid item xs={12} md={6}>
            <TextInput source="updateTime$" label="最近更新时间" fullWidth disabled />
            <TextInput source="updateTime$" label="ai.common.lastUpdatedAt" fullWidth disabled />
        </Grid>
    </Grid>
);
    );
};
export default AiPromptForm;
rsf-admin/src/page/system/aiPrompt/AiPromptList.jsx
@@ -10,6 +10,7 @@
    useListContext,
    useNotify,
    useRefresh,
    useTranslate,
} from "react-admin";
import {
    Box,
@@ -35,11 +36,11 @@
const filters = [
    <SearchInput source="condition" alwaysOn />,
    <TextInput source="code" label="编码" />,
    <TextInput source="scene" label="场景" />,
    <TextInput source="code" label="ai.prompt.fields.code" />,
    <TextInput source="scene" label="ai.prompt.fields.scene" />,
    <SelectInput
        source="status"
        label="状态"
        label="common.field.status"
        choices={[
            { id: "1", name: "common.enums.statusTrue" },
            { id: "0", name: "common.enums.statusFalse" },
@@ -61,6 +62,7 @@
};
const AiPromptCards = ({ onView, onEdit, onDelete, deleting }) => {
    const translate = useTranslate();
    const { data, isLoading } = useListContext();
    const records = useMemo(() => (Array.isArray(data) ? data : []), [data]);
@@ -76,9 +78,9 @@
        return (
            <Box px={2} py={6}>
                <Card variant="outlined" sx={{ p: 3, textAlign: "center", borderStyle: "dashed" }}>
                    <Typography variant="subtitle1">暂无 Prompt 配置</Typography>
                    <Typography variant="subtitle1">{translate("ai.prompt.list.emptyTitle")}</Typography>
                    <Typography variant="body2" color="text.secondary" mt={1}>
                        新建一张 Prompt 卡片后,AI 对话会动态加载这里的内容。
                        {translate("ai.prompt.list.emptyDescription")}
                    </Typography>
                </Card>
            </Box>
@@ -111,29 +113,27 @@
                                    <Chip
                                        size="small"
                                        color={record.statusBool ? "success" : "default"}
                                        label={record.statusBool ? "启用" : "停用"}
                                        label={translate(record.statusBool ? "ai.common.enabled" : "ai.common.disabled")}
                                    />
                                </Stack>
                                <Stack direction="row" spacing={1} flexWrap="wrap" useFlexGap mt={1.5}>
                                    <Chip size="small" variant="outlined" label={`Scene: ${record.scene || "--"}`} />
                                    <Chip size="small" variant="outlined" label={translate("ai.prompt.list.sceneValue", { value: record.scene || "--" })} />
                                </Stack>
                                <Divider sx={{ my: 1.5 }} />
                                <Typography variant="caption" color="text.secondary">System Prompt</Typography>
                                <Typography variant="caption" color="text.secondary">{translate("ai.prompt.fields.systemPrompt")}</Typography>
                                <Typography variant="body2" sx={{ mt: 0.5 }}>
                                    {truncateText(record.systemPrompt)}
                                </Typography>
                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>
                                    User Prompt Template
                                </Typography>
                                <Typography variant="caption" color="text.secondary" display="block" mt={1.5}>{translate("ai.prompt.fields.userPromptTemplate")}</Typography>
                                <Typography variant="body2">{truncateText(record.userPromptTemplate, 100)}</Typography>
                            </CardContent>
                            <CardActions sx={{ px: 2, pb: 2, pt: 0, justifyContent: "space-between" }}>
                                <Stack direction="row" spacing={1}>
                                    <Button size="small" startIcon={<VisibilityOutlinedIcon />} onClick={() => onView(record.id)}>
                                        详情
                                        {translate("ai.common.detail")}
                                    </Button>
                                    <Button size="small" startIcon={<EditOutlinedIcon />} onClick={() => onEdit(record.id)}>
                                        编辑
                                        {translate("common.button.edit")}
                                    </Button>
                                </Stack>
                                <Button
@@ -143,7 +143,7 @@
                                    onClick={() => onDelete(record)}
                                    disabled={deleting}
                                >
                                    删除
                                    {translate("ai.common.delete")}
                                </Button>
                            </CardActions>
                        </Card>
@@ -155,6 +155,7 @@
};
const AiPromptList = () => {
    const translate = useTranslate();
    const notify = useNotify();
    const refresh = useRefresh();
    const [deleteOne, { isPending: deleting }] = useDelete();
@@ -164,7 +165,7 @@
    const closeDialog = () => setDialogState({ open: false, mode: "create", recordId: null });
    const handleDelete = (record) => {
        if (!record?.id || !window.confirm(`确认删除“${record.name}”吗?`)) {
        if (!record?.id || !window.confirm(translate("ai.common.confirmDelete", { name: record.name }))) {
            return;
        }
        deleteOne(
@@ -172,20 +173,20 @@
            { id: record.id },
            {
                onSuccess: () => {
                    notify("删除成功");
                    notify(translate("ai.common.deleteSuccess"));
                    refresh();
                },
                onError: (error) => {
                    notify(error?.message || "删除失败", { type: "error" });
                    notify(error?.message || translate("ai.common.deleteFailed"), { type: "error" });
                },
            }
        );
    };
    const dialogTitle = {
        create: "新建 Prompt",
        edit: "编辑 Prompt",
        show: "查看 Prompt 详情",
        create: translate("ai.prompt.dialog.create"),
        edit: translate("ai.prompt.dialog.edit"),
        show: translate("ai.prompt.dialog.show"),
    }[dialogState.mode];
    return (
@@ -198,7 +199,7 @@
                    <TopToolbar>
                        <FilterButton />
                        <Button variant="contained" startIcon={<AddRoundedIcon />} onClick={() => openDialog("create")}>
                            新建
                            {translate("ai.common.new")}
                        </Button>
                        <MyExportButton />
                    </TopToolbar>
rsf-admin/src/page/system/aiShared/AiConfigDialog.jsx
@@ -7,6 +7,7 @@
    Toolbar,
    useNotify,
    useRefresh,
    useTranslate,
} from "react-admin";
import {
    Button,
@@ -16,9 +17,9 @@
    DialogTitle,
} from "@mui/material";
const DialogFormToolbar = ({ onClose }) => (
const DialogFormToolbar = ({ onClose, translate }) => (
    <Toolbar sx={{ justifyContent: "space-between", px: 0 }}>
        <Button onClick={onClose}>取消</Button>
        <Button onClick={onClose}>{translate("ai.common.cancel")}</Button>
        <SaveButton />
    </Toolbar>
);
@@ -36,25 +37,26 @@
}) => {
    const notify = useNotify();
    const refresh = useRefresh();
    const translate = useTranslate();
    if (!open) {
        return null;
    }
    const handleSuccess = () => {
        notify(mode === "create" ? "保存成功" : "更新成功");
        notify(translate(mode === "create" ? "ai.common.saveSuccess" : "ai.common.updateSuccess"));
        refresh();
        onClose();
    };
    const handleError = (error) => {
        notify(error?.message || "操作失败", { type: "error" });
        notify(error?.message || translate("ai.common.operationFailed"), { type: "error" });
    };
    const formContent = (
        <SimpleForm
            defaultValues={mode === "create" ? defaultValues : undefined}
            toolbar={mode === "show" ? false : <DialogFormToolbar onClose={onClose} />}
            toolbar={mode === "show" ? false : <DialogFormToolbar onClose={onClose} translate={translate} />}
            sx={{
                "& .RaSimpleForm-form": {
                    maxWidth: "100%",
@@ -89,7 +91,7 @@
            </DialogContent>
            {mode === "show" && (
                <DialogActions>
                    <Button onClick={onClose}>关闭</Button>
                    <Button onClick={onClose}>{translate("ai.common.close")}</Button>
                </DialogActions>
            )}
        </Dialog>
rsf-admin/src/page/system/aiShared/AiRuntimeSummary.jsx
@@ -1,8 +1,10 @@
import React, { useEffect, useState } from "react";
import { useTranslate } from "react-admin";
import { Alert, Box, Card, CardContent, Chip, CircularProgress, Grid, Stack, Typography } from "@mui/material";
import { getAiConfigSummary } from "@/api/ai/configCenter";
const AiRuntimeSummary = ({ promptCode = "home.default" }) => {
    const translate = useTranslate();
    const [summary, setSummary] = useState(null);
    const [loading, setLoading] = useState(true);
    const [error, setError] = useState("");
@@ -22,7 +24,7 @@
                if (!active) {
                    return;
                }
                setError(err?.message || "获取运行态摘要失败");
                setError(err?.message || translate("ai.runtimeSummary.fetchFailed"));
            })
            .finally(() => {
                if (active) {
@@ -46,9 +48,9 @@
                <CardContent>
                    <Stack direction="row" justifyContent="space-between" alignItems="center" mb={2}>
                        <Box>
                            <Typography variant="h6">当前运行态</Typography>
                            <Typography variant="h6">{translate("ai.runtimeSummary.title")}</Typography>
                            <Typography variant="body2" color="text.secondary">
                                展示当前生效的模型、Prompt 与 MCP 挂载信息。
                                {translate("ai.runtimeSummary.description")}
                            </Typography>
                        </Box>
                        {loading && <CircularProgress size={24} />}
@@ -57,29 +59,32 @@
                    {!loading && !error && summary && (
                        <Grid container spacing={2}>
                            <Grid item xs={12} md={4}>
                                <Typography variant="caption" color="text.secondary">当前模型</Typography>
                                <Typography variant="caption" color="text.secondary">{translate("ai.runtimeSummary.currentModel")}</Typography>
                                <Typography variant="body1">{summary.activeModel || "--"}</Typography>
                                <Typography variant="body2" color="text.secondary">
                                    {summary.activeParamName || "--"}
                                </Typography>
                                <Stack direction="row" spacing={1} mt={1} flexWrap="wrap" useFlexGap>
                                    <Chip size="small" label={`校验 ${summary.activeParamValidateStatus || "--"}`} />
                                    <Chip size="small" variant="outlined" label={summary.activeParamValidatedAt || "未校验"} />
                                    <Chip size="small" label={translate("ai.runtimeSummary.validateStatus", { status: summary.activeParamValidateStatus || "--" })} />
                                    <Chip size="small" variant="outlined" label={summary.activeParamValidatedAt || translate("ai.common.notValidated")} />
                                </Stack>
                            </Grid>
                            <Grid item xs={12} md={4}>
                                <Typography variant="caption" color="text.secondary">当前 Prompt</Typography>
                                <Typography variant="caption" color="text.secondary">{translate("ai.runtimeSummary.currentPrompt")}</Typography>
                                <Typography variant="body1">{summary.promptName || "--"}</Typography>
                                <Typography variant="body2" color="text.secondary">
                                    {summary.promptCode || "--"} / {summary.promptScene || "--"}
                                </Typography>
                                <Typography variant="body2" color="text.secondary" mt={1}>
                                    最近更新:{summary.activePromptUpdatedAt || "--"} / {summary.activePromptUpdatedBy || "--"}
                                    {translate("ai.runtimeSummary.lastUpdated", {
                                        time: summary.activePromptUpdatedAt || "--",
                                        user: summary.activePromptUpdatedBy || "--",
                                    })}
                                </Typography>
                            </Grid>
                            <Grid item xs={12} md={4}>
                                <Typography variant="caption" color="text.secondary">已启用 MCP</Typography>
                                <Typography variant="body1">{summary.enabledMcpCount ?? 0} 个</Typography>
                                <Typography variant="caption" color="text.secondary">{translate("ai.runtimeSummary.enabledMcp")}</Typography>
                                <Typography variant="body1">{translate("ai.runtimeSummary.enabledMcpCount", { count: summary.enabledMcpCount ?? 0 })}</Typography>
                                <Stack direction="row" spacing={1} mt={1} flexWrap="wrap" useFlexGap>
                                    {(summary.enabledMcpNames || []).map((name) => (
                                        <Chip key={name} size="small" variant="outlined" label={name} />