| | |
| | | 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 |