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