| | |
| | | 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: { |
| | |
| | | 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: { |
| | |
| | | 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, |
| | |
| | | |
| | | 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([]); |
| | |
| | | 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; |
| | | |
| | |
| | | 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([]); |
| | |
| | | setPersistedMessages(historyMessages); |
| | | setMessages(historyMessages); |
| | | } catch (error) { |
| | | const message = error.message || "获取 AI 运行时失败"; |
| | | const message = error.message || translate("ai.drawer.runtimeFailed"); |
| | | setDrawerError(message); |
| | | } finally { |
| | | setLoadingRuntime(false); |
| | |
| | | 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); |
| | | } |
| | | }; |
| | |
| | | } |
| | | 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" }); |
| | | } |
| | |
| | | } |
| | | 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" }); |
| | | } |
| | |
| | | } |
| | | 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" }); |
| | | } |
| | |
| | | } |
| | | 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" }); |
| | | } |
| | |
| | | } |
| | | 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" }); |
| | | } |
| | |
| | | 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; |
| | | } |
| | | }; |
| | | |
| | |
| | | } |
| | | } |
| | | 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" }); |
| | |
| | | ); |
| | | } 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" }); |
| | | } |
| | |
| | | <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> |
| | |
| | | > |
| | | <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 |
| | |
| | | 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" }} />, |
| | | }} |
| | |
| | | {!sessions.length ? ( |
| | | <Box px={1.5} py={1.25}> |
| | | <Typography variant="body2" color="text.secondary"> |
| | | 暂无历史会话 |
| | | {translate("ai.drawer.noSessions")} |
| | | </Typography> |
| | | </Box> |
| | | ) : ( |
| | |
| | | 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, |
| | |
| | | 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> |
| | |
| | | event.stopPropagation(); |
| | | openRenameDialog(item); |
| | | }} |
| | | title="重命名会话" |
| | | title={translate("ai.drawer.renameAction")} |
| | | > |
| | | <EditOutlinedIcon fontSize="small" /> |
| | | </IconButton> |
| | |
| | | event.stopPropagation(); |
| | | handleDeleteSession(item.sessionId); |
| | | }} |
| | | title="删除会话" |
| | | title={translate("ai.drawer.deleteAction")} |
| | | > |
| | | <DeleteOutlineOutlinedIcon fontSize="small" /> |
| | | </IconButton> |
| | |
| | | > |
| | | <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> |
| | | ) : ( |
| | |
| | | > |
| | | <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"> |
| | |
| | | : <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> |
| | |
| | | <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) => ( |
| | |
| | | onClick={handleRetainLatestRound} |
| | | disabled={!sessionId || streaming} |
| | | > |
| | | 仅保留当前轮 |
| | | {translate("ai.drawer.retainLatestRound")} |
| | | </Button> |
| | | <Button |
| | | size="small" |
| | |
| | | onClick={handleClearMemory} |
| | | disabled={!sessionId || streaming} |
| | | > |
| | | 清空记忆 |
| | | {translate("ai.drawer.clearMemory")} |
| | | </Button> |
| | | </Stack> |
| | | {!!runtime?.memorySummary && ( |
| | |
| | | )} |
| | | {loadingRuntime && ( |
| | | <Typography variant="body2" color="text.secondary" mt={1}> |
| | | 正在加载 AI 运行时信息... |
| | | {translate("ai.drawer.loadingRuntime")} |
| | | </Typography> |
| | | )} |
| | | {!!drawerError && ( |
| | |
| | | |
| | | <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> |
| | | )} |
| | |
| | | }} |
| | | > |
| | | <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 /> |
| | |
| | | <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 |
| | |
| | | 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> |
| | |
| | | </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> |
| | |
| | | TopToolbar, |
| | | useListContext, |
| | | useNotify, |
| | | useTranslate, |
| | | } from "react-admin"; |
| | | import { |
| | | Alert, |
| | |
| | | |
| | | 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" }, |
| | |
| | | ]; |
| | | |
| | | const ObserveSummary = () => { |
| | | const translate = useTranslate(); |
| | | const [stats, setStats] = useState(null); |
| | | const [loading, setLoading] = useState(true); |
| | | const [error, setError] = useState(""); |
| | |
| | | }) |
| | | .catch((err) => { |
| | | if (active) { |
| | | setError(err?.message || "获取 AI 观测统计失败"); |
| | | setError(err?.message || translate("ai.observe.summary.fetchFailed")); |
| | | } |
| | | }) |
| | | .finally(() => { |
| | |
| | | <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} />} |
| | |
| | | {!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> |
| | |
| | | ); |
| | | }; |
| | | |
| | | 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); |
| | | |
| | |
| | | }) |
| | | .catch((error) => { |
| | | if (active) { |
| | | notify(error?.message || "获取 MCP 调用日志失败", { type: "error" }); |
| | | notify(error?.message || translate("ai.observe.detail.mcpLogsFailed"), { type: "error" }); |
| | | } |
| | | }) |
| | | .finally(() => { |
| | |
| | | |
| | | 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 && ( |
| | |
| | | <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}> |
| | |
| | | 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> |
| | |
| | | </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]); |
| | | |
| | |
| | | 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> |
| | |
| | | <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> |
| | |
| | | <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> |
| | |
| | | SelectInput, |
| | | TextInput, |
| | | useNotify, |
| | | useTranslate, |
| | | } from "react-admin"; |
| | | import { Alert, Button, Grid, Stack, Typography } from "@mui/material"; |
| | | import StatusSelectInput from "@/page/components/StatusSelectInput"; |
| | |
| | | |
| | | const AiMcpDraftTestSection = ({ formData, readOnly }) => { |
| | | const notify = useNotify(); |
| | | const translate = useTranslate(); |
| | | const [loading, setLoading] = useState(false); |
| | | const [result, setResult] = useState(null); |
| | | |
| | |
| | | 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" }); |
| | |
| | | <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> |
| | |
| | | ); |
| | | }; |
| | | |
| | | 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 }) => ( |
| | |
| | | <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" }, |
| | |
| | | {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} /> |
| | |
| | | {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} /> |
| | |
| | | <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; |
| | |
| | | useListContext, |
| | | useNotify, |
| | | useRefresh, |
| | | useTranslate, |
| | | } from "react-admin"; |
| | | import { |
| | | Box, |
| | |
| | | <SearchInput source="condition" alwaysOn />, |
| | | <SelectInput |
| | | source="transportType" |
| | | label="传输类型" |
| | | label="ai.mcp.fields.transportType" |
| | | choices={[ |
| | | { id: "SSE_HTTP", name: "SSE_HTTP" }, |
| | | { id: "STDIO", name: "STDIO" }, |
| | |
| | | />, |
| | | <SelectInput |
| | | source="status" |
| | | label="状态" |
| | | label="common.field.status" |
| | | choices={[ |
| | | { id: "1", name: "common.enums.statusTrue" }, |
| | | { id: "0", name: "common.enums.statusFalse" }, |
| | |
| | | }; |
| | | |
| | | 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(() => { |
| | |
| | | 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> |
| | |
| | | {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 |
| | |
| | | <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" |
| | |
| | | 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 |
| | |
| | | onClick={() => onDelete(record)} |
| | | disabled={deleting} |
| | | > |
| | | 删除 |
| | | {translate("ai.common.delete")} |
| | | </Button> |
| | | </CardActions> |
| | | </Card> |
| | |
| | | }; |
| | | |
| | | const AiMcpMountList = () => { |
| | | const translate = useTranslate(); |
| | | const notify = useNotify(); |
| | | const refresh = useRefresh(); |
| | | const [deleteOne, { isPending: deleting }] = useDelete(); |
| | |
| | | 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( |
| | |
| | | { 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" }); |
| | | }, |
| | | } |
| | | ); |
| | |
| | | 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 ( |
| | |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <Button variant="contained" startIcon={<AddRoundedIcon />} onClick={() => openDialog("create")}> |
| | | 新建 |
| | | {translate("ai.common.new")} |
| | | </Button> |
| | | <MyExportButton /> |
| | | </TopToolbar> |
| | |
| | | import React, { useEffect, useMemo, useState } from "react"; |
| | | import { useTranslate, useNotify } from "react-admin"; |
| | | import { |
| | | Accordion, |
| | | AccordionDetails, |
| | |
| | | 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: "" }; |
| | | } |
| | |
| | | pretty: inputSchema, |
| | | fields: [], |
| | | required: [], |
| | | error: `Input Schema 解析失败: ${error.message}`, |
| | | error: translate("ai.mcp.tools.schemaParseFailed", { message: error.message }), |
| | | }; |
| | | } |
| | | }; |
| | |
| | | |
| | | const AiMcpMountToolsPanel = ({ mountId }) => { |
| | | const notify = useNotify(); |
| | | const translate = useTranslate(); |
| | | const [loading, setLoading] = useState(false); |
| | | const [tools, setTools] = useState([]); |
| | | const [error, setError] = useState(""); |
| | |
| | | |
| | | 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) { |
| | |
| | | setInputs({}); |
| | | setStructuredInputs({}); |
| | | } catch (requestError) { |
| | | setError(requestError.message || "获取工具列表失败"); |
| | | setError(requestError.message || translate("ai.mcp.tools.loadFailed")); |
| | | } finally { |
| | | setLoading(false); |
| | | } |
| | |
| | | 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); |
| | |
| | | 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); |
| | |
| | | ...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, |
| | |
| | | if (!mountId) { |
| | | return ( |
| | | <Alert severity="info" sx={{ mt: 2 }}> |
| | | 保存挂载后即可预览工具并执行测试。 |
| | | {translate("ai.mcp.tools.saveBeforePreview")} |
| | | </Alert> |
| | | ); |
| | | } |
| | |
| | | <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" |
| | |
| | | onClick={handleConnectivityTest} |
| | | disabled={testingConnectivity} |
| | | > |
| | | {testingConnectivity ? "测试中..." : "连通性测试"} |
| | | {testingConnectivity ? translate("ai.common.testing") : translate("ai.mcp.list.connectivityTest")} |
| | | </Button> |
| | | </Stack> |
| | | {!!connectivity && ( |
| | |
| | | </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) => { |
| | |
| | | <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> |
| | |
| | | </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"} |
| | |
| | | <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"> |
| | |
| | | </Alert> |
| | | )} |
| | | <TextField |
| | | label="格式化 Input Schema" |
| | | label={translate("ai.mcp.tools.formattedSchema")} |
| | | value={schemaInfo.pretty || tool.inputSchema || ""} |
| | | fullWidth |
| | | multiline |
| | |
| | | </Grid> |
| | | )} |
| | | <TextField |
| | | label="测试输入 JSON" |
| | | label={translate("ai.mcp.tools.testInput")} |
| | | value={inputs[tool.name] || ""} |
| | | onChange={(event) => handleInputChange(tool.name, event.target.value)} |
| | | fullWidth |
| | |
| | | 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 |
| | |
| | | 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 |
| | |
| | | SelectInput, |
| | | TextInput, |
| | | useNotify, |
| | | useTranslate, |
| | | } from "react-admin"; |
| | | import { Alert, Button, Grid, Stack, Typography } from "@mui/material"; |
| | | import StatusSelectInput from "@/page/components/StatusSelectInput"; |
| | |
| | | |
| | | const AiParamValidateSection = ({ formData, readOnly }) => { |
| | | const notify = useNotify(); |
| | | const translate = useTranslate(); |
| | | const [loading, setLoading] = useState(false); |
| | | const [result, setResult] = useState(null); |
| | | |
| | |
| | | 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" }); |
| | |
| | | <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> |
| | |
| | | ); |
| | | }; |
| | | |
| | | 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; |
| | |
| | | useListContext, |
| | | useNotify, |
| | | useRefresh, |
| | | useTranslate, |
| | | } from "react-admin"; |
| | | import { |
| | | Box, |
| | |
| | | |
| | | 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" }, |
| | |
| | | }; |
| | | |
| | | const AiParamCards = ({ onView, onEdit, onDelete, deleting }) => { |
| | | const translate = useTranslate(); |
| | | const { data, isLoading } = useListContext(); |
| | | const records = useMemo(() => (Array.isArray(data) ? data : []), [data]); |
| | | |
| | |
| | | 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> |
| | |
| | | <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}> |
| | |
| | | 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 }} /> |
| | |
| | | <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 |
| | |
| | | onClick={() => onDelete(record)} |
| | | disabled={deleting} |
| | | > |
| | | 删除 |
| | | {translate("ai.common.delete")} |
| | | </Button> |
| | | </CardActions> |
| | | </Card> |
| | |
| | | }; |
| | | |
| | | const AiParamList = () => { |
| | | const translate = useTranslate(); |
| | | const notify = useNotify(); |
| | | const refresh = useRefresh(); |
| | | const [deleteOne, { isPending: deleting }] = useDelete(); |
| | |
| | | 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( |
| | |
| | | { 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 ( |
| | |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <Button variant="contained" startIcon={<AddRoundedIcon />} onClick={() => openDialog("create")}> |
| | | 新建 |
| | | {translate("ai.common.new")} |
| | | </Button> |
| | | <MyExportButton /> |
| | | </TopToolbar> |
| | |
| | | FormDataConsumer, |
| | | TextInput, |
| | | useNotify, |
| | | useTranslate, |
| | | } from "react-admin"; |
| | | import { Alert, Button, Grid, Stack, TextField, Typography } from "@mui/material"; |
| | | import StatusSelectInput from "@/page/components/StatusSelectInput"; |
| | |
| | | |
| | | 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); |
| | |
| | | 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); |
| | | } |
| | |
| | | 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 |
| | |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label="示例元数据 JSON" |
| | | label={translate("ai.prompt.preview.metadata")} |
| | | value={metadataText} |
| | | onChange={(event) => setMetadataText(event.target.value)} |
| | | fullWidth |
| | |
| | | <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> |
| | |
| | | <> |
| | | <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 |
| | |
| | | </Grid> |
| | | <Grid item xs={12}> |
| | | <TextField |
| | | label="渲染后的 User Prompt" |
| | | label={translate("ai.prompt.preview.renderedUserPrompt")} |
| | | value={preview.renderedUserPrompt || ""} |
| | | fullWidth |
| | | multiline |
| | |
| | | ); |
| | | }; |
| | | |
| | | 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; |
| | |
| | | useListContext, |
| | | useNotify, |
| | | useRefresh, |
| | | useTranslate, |
| | | } from "react-admin"; |
| | | import { |
| | | Box, |
| | |
| | | |
| | | 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" }, |
| | |
| | | }; |
| | | |
| | | const AiPromptCards = ({ onView, onEdit, onDelete, deleting }) => { |
| | | const translate = useTranslate(); |
| | | const { data, isLoading } = useListContext(); |
| | | const records = useMemo(() => (Array.isArray(data) ? data : []), [data]); |
| | | |
| | |
| | | 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> |
| | |
| | | <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 |
| | |
| | | onClick={() => onDelete(record)} |
| | | disabled={deleting} |
| | | > |
| | | 删除 |
| | | {translate("ai.common.delete")} |
| | | </Button> |
| | | </CardActions> |
| | | </Card> |
| | |
| | | }; |
| | | |
| | | const AiPromptList = () => { |
| | | const translate = useTranslate(); |
| | | const notify = useNotify(); |
| | | const refresh = useRefresh(); |
| | | const [deleteOne, { isPending: deleting }] = useDelete(); |
| | |
| | | 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( |
| | |
| | | { 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 ( |
| | |
| | | <TopToolbar> |
| | | <FilterButton /> |
| | | <Button variant="contained" startIcon={<AddRoundedIcon />} onClick={() => openDialog("create")}> |
| | | 新建 |
| | | {translate("ai.common.new")} |
| | | </Button> |
| | | <MyExportButton /> |
| | | </TopToolbar> |
| | |
| | | Toolbar, |
| | | useNotify, |
| | | useRefresh, |
| | | useTranslate, |
| | | } from "react-admin"; |
| | | import { |
| | | Button, |
| | |
| | | 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> |
| | | ); |
| | |
| | | }) => { |
| | | 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%", |
| | |
| | | </DialogContent> |
| | | {mode === "show" && ( |
| | | <DialogActions> |
| | | <Button onClick={onClose}>关闭</Button> |
| | | <Button onClick={onClose}>{translate("ai.common.close")}</Button> |
| | | </DialogActions> |
| | | )} |
| | | </Dialog> |
| | |
| | | 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(""); |
| | |
| | | if (!active) { |
| | | return; |
| | | } |
| | | setError(err?.message || "获取运行态摘要失败"); |
| | | setError(err?.message || translate("ai.runtimeSummary.fetchFailed")); |
| | | }) |
| | | .finally(() => { |
| | | if (active) { |
| | |
| | | <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} />} |
| | |
| | | {!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} /> |