From 3d81df739dc45599c257d8cdefe0996f66ccdeae Mon Sep 17 00:00:00 2001
From: zhou zhou <3272660260@qq.com>
Date: 星期四, 19 三月 2026 12:18:14 +0800
Subject: [PATCH] #AI.MCP 管理增强

---
 rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx |  344 +++++++++++++++++++++++++++++++++++++++++++++-----------
 1 files changed, 275 insertions(+), 69 deletions(-)

diff --git a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx
index 1901478..2a333e9 100644
--- a/rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx
+++ b/rsf-admin/src/page/system/aiMcpMount/AiMcpMountToolsPanel.jsx
@@ -1,4 +1,4 @@
-import React, { useEffect, useState } from "react";
+import React, { useEffect, useMemo, useState } from "react";
 import {
     Accordion,
     AccordionDetails,
@@ -9,8 +9,8 @@
     Card,
     CardContent,
     CircularProgress,
-    Divider,
     Grid,
+    MenuItem,
     Stack,
     TextField,
     Typography,
@@ -19,7 +19,102 @@
 import PreviewOutlinedIcon from "@mui/icons-material/PreviewOutlined";
 import ExpandMoreOutlinedIcon from "@mui/icons-material/ExpandMoreOutlined";
 import { useNotify } from "react-admin";
-import { previewMcpTools, testMcpTool } from "@/api/ai/mcpMount";
+import { previewMcpTools, testMcpConnectivity, testMcpTool } from "@/api/ai/mcpMount";
+
+const parseInputSchema = (inputSchema) => {
+    if (!inputSchema) {
+        return { pretty: "", fields: [], required: [], error: "" };
+    }
+    try {
+        const schema = JSON.parse(inputSchema);
+        const properties = schema?.properties || {};
+        const required = Array.isArray(schema?.required) ? schema.required : [];
+        return {
+            pretty: JSON.stringify(schema, null, 2),
+            required,
+            error: "",
+            fields: Object.entries(properties).map(([name, definition]) => ({
+                name,
+                title: definition?.title || name,
+                description: definition?.description || "",
+                type: definition?.type || "string",
+                enumValues: Array.isArray(definition?.enum) ? definition.enum : [],
+            })),
+        };
+    } catch (error) {
+        return {
+            pretty: inputSchema,
+            fields: [],
+            required: [],
+            error: `Input Schema 瑙f瀽澶辫触: ${error.message}`,
+        };
+    }
+};
+
+const normalizeFieldValue = (field, rawValue) => {
+    if (rawValue === "" || rawValue == null) {
+        return undefined;
+    }
+    if (field.type === "integer") {
+        const parsed = Number.parseInt(rawValue, 10);
+        return Number.isNaN(parsed) ? rawValue : parsed;
+    }
+    if (field.type === "number") {
+        const parsed = Number(rawValue);
+        return Number.isNaN(parsed) ? rawValue : parsed;
+    }
+    if (field.type === "boolean") {
+        return rawValue === true || rawValue === "true";
+    }
+    return rawValue;
+};
+
+const buildInputJson = (schemaInfo, fieldValues) => {
+    if (!schemaInfo?.fields?.length) {
+        return "";
+    }
+    const payload = {};
+    schemaInfo.fields.forEach((field) => {
+        const normalized = normalizeFieldValue(field, fieldValues?.[field.name]);
+        if (normalized !== undefined) {
+            payload[field.name] = normalized;
+        }
+    });
+    return JSON.stringify(payload, null, 2);
+};
+
+const readStructuredValues = (schemaInfo, inputJson) => {
+    if (!schemaInfo?.fields?.length || !inputJson || !inputJson.trim()) {
+        return {};
+    }
+    try {
+        const parsed = JSON.parse(inputJson);
+        if (!parsed || typeof parsed !== "object" || Array.isArray(parsed)) {
+            return {};
+        }
+        const values = {};
+        schemaInfo.fields.forEach((field) => {
+            const value = parsed[field.name];
+            if (value === undefined || value === null) {
+                return;
+            }
+            values[field.name] = typeof value === "boolean" ? String(value) : String(value);
+        });
+        return values;
+    } catch (error) {
+        return {};
+    }
+};
+
+const resolveConnectivitySeverity = (healthStatus) => {
+    if (healthStatus === "HEALTHY") {
+        return "success";
+    }
+    if (healthStatus === "UNHEALTHY") {
+        return "error";
+    }
+    return "info";
+};
 
 const AiMcpMountToolsPanel = ({ mountId }) => {
     const notify = useNotify();
@@ -27,14 +122,26 @@
     const [tools, setTools] = useState([]);
     const [error, setError] = useState("");
     const [inputs, setInputs] = useState({});
+    const [structuredInputs, setStructuredInputs] = useState({});
     const [outputs, setOutputs] = useState({});
     const [testingToolName, setTestingToolName] = useState("");
+    const [testingConnectivity, setTestingConnectivity] = useState(false);
+    const [connectivity, setConnectivity] = useState(null);
+
+    const schemaInfoMap = useMemo(() => {
+        return tools.reduce((result, tool) => {
+            result[tool.name] = parseInputSchema(tool.inputSchema);
+            return result;
+        }, {});
+    }, [tools]);
 
     useEffect(() => {
         if (!mountId) {
             setTools([]);
             setInputs({});
+            setStructuredInputs({});
             setOutputs({});
+            setConnectivity(null);
             setError("");
             return;
         }
@@ -48,10 +155,26 @@
             const data = await previewMcpTools(mountId);
             setTools(data);
             setOutputs({});
+            setInputs({});
+            setStructuredInputs({});
         } catch (requestError) {
             setError(requestError.message || "鑾峰彇宸ュ叿鍒楄〃澶辫触");
         } finally {
             setLoading(false);
+        }
+    };
+
+    const handleConnectivityTest = async () => {
+        setTestingConnectivity(true);
+        try {
+            const result = await testMcpConnectivity(mountId);
+            setConnectivity(result);
+            notify(result?.message || "杩為�氭�ф祴璇曞畬鎴�");
+        } catch (requestError) {
+            const message = requestError.message || "杩為�氭�ф祴璇曞け璐�";
+            notify(message, { type: "error" });
+        } finally {
+            setTestingConnectivity(false);
         }
     };
 
@@ -60,6 +183,28 @@
             ...prev,
             [toolName]: value,
         }));
+        setStructuredInputs((prev) => ({
+            ...prev,
+            [toolName]: readStructuredValues(schemaInfoMap[toolName], value),
+        }));
+    };
+
+    const handleStructuredFieldChange = (toolName, fieldName, value) => {
+        const schemaInfo = schemaInfoMap[toolName];
+        setStructuredInputs((prev) => {
+            const nextToolValues = {
+                ...(prev[toolName] || {}),
+                [fieldName]: value,
+            };
+            setInputs((prevInputs) => ({
+                ...prevInputs,
+                [toolName]: buildInputJson(schemaInfo, nextToolValues),
+            }));
+            return {
+                ...prev,
+                [toolName]: nextToolValues,
+            };
+        });
     };
 
     const handleTest = async (toolName) => {
@@ -106,16 +251,33 @@
                     <Box flex={1}>
                         <Typography variant="h6">宸ュ叿棰勮涓庢祴璇�</Typography>
                         <Typography variant="body2" color="text.secondary">
-                            褰撳墠鎸傝浇瑙f瀽鍑虹殑鍏ㄩ儴宸ュ叿閮芥樉绀哄湪杩欓噷锛屽彲鐩存帴杈撳叆 JSON 鍋氭祴璇曘��
+                            鏀寔杩為�氭�ф祴璇曘�佺粨鏋勫寲 Schema 棰勮鍜屾寜杈撳叆鍙傛暟鑷姩鐢熸垚娴嬭瘯琛ㄥ崟銆�
                         </Typography>
                     </Box>
                 </AccordionSummary>
                 <AccordionDetails>
-                    <Stack direction="row" justifyContent="flex-end" alignItems="center" mb={1.5}>
+                    <Stack direction="row" justifyContent="space-between" alignItems="center" mb={1.5} flexWrap="wrap" useFlexGap>
                         <Button size="small" startIcon={<PreviewOutlinedIcon />} onClick={loadTools} disabled={loading}>
                             鍒锋柊宸ュ叿
                         </Button>
+                        <Button
+                            size="small"
+                            variant="outlined"
+                            startIcon={<PlayCircleOutlineOutlinedIcon />}
+                            onClick={handleConnectivityTest}
+                            disabled={testingConnectivity}
+                        >
+                            {testingConnectivity ? "娴嬭瘯涓�..." : "杩為�氭�ф祴璇�"}
+                        </Button>
                     </Stack>
+                    {!!connectivity && (
+                        <Alert severity={resolveConnectivitySeverity(connectivity.healthStatus)} sx={{ mb: 2 }}>
+                            {connectivity.message}
+                            {connectivity.initElapsedMs != null && ` 路 Init ${connectivity.initElapsedMs} ms`}
+                            {connectivity.toolCount != null && ` 路 Tools ${connectivity.toolCount}`}
+                            {connectivity.testedAt && ` 路 ${connectivity.testedAt}`}
+                        </Alert>
+                    )}
                     {loading && (
                         <Box display="flex" justifyContent="center" py={4}>
                             <CircularProgress size={28} />
@@ -130,71 +292,115 @@
                         <Alert severity="info">褰撳墠鎸傝浇鏈В鏋愬嚭浠讳綍宸ュ叿銆�</Alert>
                     )}
                     <Grid container spacing={2}>
-                        {tools.map((tool) => (
-                            <Grid item xs={12} key={tool.name}>
-                                <Accordion defaultExpanded={false} sx={{ borderRadius: 3, overflow: "hidden" }}>
-                                    <AccordionSummary expandIcon={<ExpandMoreOutlinedIcon />}>
-                                        <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2} width="100%" pr={1}>
-                                            <Box>
-                                                <Typography variant="subtitle1">{tool.name}</Typography>
-                                                <Typography variant="body2" color="text.secondary">
-                                                    {tool.description || "鏆傛棤鎻忚堪"}
-                                                </Typography>
-                                            </Box>
-                                            <Typography variant="caption" color="text.secondary">
-                                                {tool.returnDirect ? "returnDirect" : "normal"}
-                                            </Typography>
-                                        </Stack>
-                                    </AccordionSummary>
-                                    <AccordionDetails>
-                                        <Card variant="outlined" sx={{ borderRadius: 3 }}>
-                                            <CardContent>
-                                                <TextField
-                                                    label="Input Schema"
-                                                    value={tool.inputSchema || ""}
-                                                    fullWidth
-                                                    multiline
-                                                    minRows={5}
-                                                    maxRows={12}
-                                                    InputProps={{ readOnly: true }}
-                                                />
-                                                <TextField
-                                                    label="娴嬭瘯杈撳叆 JSON"
-                                                    value={inputs[tool.name] || ""}
-                                                    onChange={(event) => handleInputChange(tool.name, event.target.value)}
-                                                    fullWidth
-                                                    multiline
-                                                    minRows={5}
-                                                    maxRows={12}
-                                                    sx={{ mt: 2 }}
-                                                    placeholder='渚嬪锛歿"code":"A01"}'
-                                                />
-                                                <Stack direction="row" justifyContent="flex-end" mt={1.5}>
-                                                    <Button
-                                                        variant="contained"
-                                                        startIcon={<PlayCircleOutlineOutlinedIcon />}
-                                                        onClick={() => handleTest(tool.name)}
-                                                        disabled={testingToolName === tool.name}
-                                                    >
-                                                        {testingToolName === tool.name ? "娴嬭瘯涓�..." : "鎵ц娴嬭瘯"}
-                                                    </Button>
+                        {tools.map((tool) => {
+                            const schemaInfo = schemaInfoMap[tool.name] || { pretty: "", fields: [], required: [], error: "" };
+                            const structuredValues = structuredInputs[tool.name] || {};
+                            return (
+                                <Grid item xs={12} key={tool.name}>
+                                    <Accordion defaultExpanded={false} sx={{ borderRadius: 3, overflow: "hidden" }}>
+                                        <AccordionSummary expandIcon={<ExpandMoreOutlinedIcon />}>
+                                            <Stack direction="row" justifyContent="space-between" alignItems="center" spacing={2} width="100%" pr={1}>
+                                                <Box>
+                                                    <Typography variant="subtitle1">{tool.name}</Typography>
+                                                    <Typography variant="body2" color="text.secondary">
+                                                        {tool.description || "鏆傛棤鎻忚堪"}
+                                                    </Typography>
+                                                </Box>
+                                                <Stack direction="row" spacing={1} alignItems="center" flexWrap="wrap" useFlexGap>
+                                                    <Typography variant="caption" color="text.secondary">
+                                                        {schemaInfo.fields.length} 涓弬鏁�
+                                                    </Typography>
+                                                    <Typography variant="caption" color="text.secondary">
+                                                        {tool.returnDirect ? "returnDirect" : "normal"}
+                                                    </Typography>
                                                 </Stack>
-                                                <TextField
-                                                    label="娴嬭瘯缁撴灉"
-                                                    value={outputs[tool.name] || ""}
-                                                    fullWidth
-                                                    multiline
-                                                    minRows={5}
-                                                    maxRows={16}
-                                                    sx={{ mt: 2 }}
-                                                    InputProps={{ readOnly: true }}
-                                                />
-                                            </CardContent>
-                                        </Card>
-                                    </AccordionDetails>
-                                </Accordion>
-                            </Grid>
-                        ))}
+                                            </Stack>
+                                        </AccordionSummary>
+                                        <AccordionDetails>
+                                            <Card variant="outlined" sx={{ borderRadius: 3 }}>
+                                                <CardContent>
+                                                    {!!schemaInfo.error && (
+                                                        <Alert severity="warning" sx={{ mb: 2 }}>
+                                                            {schemaInfo.error}
+                                                        </Alert>
+                                                    )}
+                                                    <TextField
+                                                        label="鏍煎紡鍖� Input Schema"
+                                                        value={schemaInfo.pretty || tool.inputSchema || ""}
+                                                        fullWidth
+                                                        multiline
+                                                        minRows={6}
+                                                        maxRows={16}
+                                                        InputProps={{ readOnly: true }}
+                                                    />
+                                                    {!!schemaInfo.fields.length && (
+                                                        <Grid container spacing={2} sx={{ mt: 0.5 }}>
+                                                            {schemaInfo.fields.map((field) => (
+                                                                <Grid item xs={12} md={field.type === "boolean" ? 6 : 12} key={`${tool.name}-${field.name}`}>
+                                                                    <TextField
+                                                                        select={field.type === "boolean" || field.enumValues.length > 0}
+                                                                        type={field.type === "integer" || field.type === "number" ? "number" : "text"}
+                                                                        label={`${field.title}${schemaInfo.required.includes(field.name) ? " *" : ""}`}
+                                                                        value={structuredValues[field.name] ?? ""}
+                                                                        onChange={(event) => handleStructuredFieldChange(tool.name, field.name, event.target.value)}
+                                                                        fullWidth
+                                                                        helperText={field.description || field.type}
+                                                                        sx={{ mt: 2 }}
+                                                                    >
+                                                                        {field.type === "boolean" && (
+                                                                            [
+                                                                                <MenuItem key="true" value="true">true</MenuItem>,
+                                                                                <MenuItem key="false" value="false">false</MenuItem>,
+                                                                            ]
+                                                                        )}
+                                                                        {field.type !== "boolean" && field.enumValues.map((value) => (
+                                                                            <MenuItem key={value} value={String(value)}>
+                                                                                {String(value)}
+                                                                            </MenuItem>
+                                                                        ))}
+                                                                    </TextField>
+                                                                </Grid>
+                                                            ))}
+                                                        </Grid>
+                                                    )}
+                                                    <TextField
+                                                        label="娴嬭瘯杈撳叆 JSON"
+                                                        value={inputs[tool.name] || ""}
+                                                        onChange={(event) => handleInputChange(tool.name, event.target.value)}
+                                                        fullWidth
+                                                        multiline
+                                                        minRows={5}
+                                                        maxRows={12}
+                                                        sx={{ mt: 2 }}
+                                                        placeholder='渚嬪锛歿"code":"A01"}'
+                                                    />
+                                                    <Stack direction="row" justifyContent="flex-end" mt={1.5}>
+                                                        <Button
+                                                            variant="contained"
+                                                            startIcon={<PlayCircleOutlineOutlinedIcon />}
+                                                            onClick={() => handleTest(tool.name)}
+                                                            disabled={testingToolName === tool.name}
+                                                        >
+                                                            {testingToolName === tool.name ? "娴嬭瘯涓�..." : "鎵ц娴嬭瘯"}
+                                                        </Button>
+                                                    </Stack>
+                                                    <TextField
+                                                        label="娴嬭瘯缁撴灉"
+                                                        value={outputs[tool.name] || ""}
+                                                        fullWidth
+                                                        multiline
+                                                        minRows={5}
+                                                        maxRows={16}
+                                                        sx={{ mt: 2 }}
+                                                        InputProps={{ readOnly: true }}
+                                                    />
+                                                </CardContent>
+                                            </Card>
+                                        </AccordionDetails>
+                                    </Accordion>
+                                </Grid>
+                            );
+                        })}
                     </Grid>
                 </AccordionDetails>
             </Accordion>

--
Gitblit v1.9.1