kra/web/src/pages/systemTools/autoCode/mcpTest/index.tsx

236 lines
8.2 KiB
TypeScript

/**
* KRA - MCP Tools测试
* 对应 GVA: view/systemTools/autoCode/mcpTest.vue
*/
import React, { useState, useEffect } from 'react';
import { PageContainer } from '@ant-design/pro-components';
import { Card, Row, Col, Button, Modal, Form, Input, Select, message, Tooltip, Image } from 'antd';
import { PlayCircleOutlined, CopyOutlined } from '@ant-design/icons';
import { mcpList, mcpTest } from '@/services/kratos/autoCode';
interface McpTool {
name: string;
description: string;
inputSchema?: {
properties?: Record<string, {
type: string;
description?: string;
enum?: string[];
default?: any;
}>;
required?: string[];
};
}
interface McpServerConfig {
mcpServers: Record<string, { url: string }>;
}
const MCPTest: React.FC = () => {
const [tools, setTools] = useState<McpTool[]>([]);
const [serverConfig, setServerConfig] = useState<string>('');
const [testModalVisible, setTestModalVisible] = useState(false);
const [currentTool, setCurrentTool] = useState<McpTool | null>(null);
const [testResult, setTestResult] = useState<any>(null);
const [form] = Form.useForm();
const fetchTools = async () => {
const res = await mcpList();
if (res.code === 0 && res.data?.list?.tools) {
setTools(res.data.list.tools);
setServerConfig(JSON.stringify(res.data.mcpServerConfig, null, 2));
}
};
useEffect(() => {
fetchTools();
}, []);
const copyConfig = () => {
navigator.clipboard.writeText(serverConfig);
message.success('配置已复制到剪贴板');
};
const openTestModal = (tool: McpTool) => {
setCurrentTool(tool);
setTestResult(null);
form.resetFields();
// 设置默认值
if (tool.inputSchema?.properties) {
const initialValues: Record<string, any> = {};
Object.entries(tool.inputSchema.properties).forEach(([key, prop]) => {
if (prop.default !== undefined) {
initialValues[key] = prop.default;
} else if (prop.type === 'boolean') {
initialValues[key] = false;
}
});
form.setFieldsValue(initialValues);
}
setTestModalVisible(true);
};
const handleTest = async () => {
if (!currentTool) return;
try {
const values = await form.validateFields();
// 处理 object/array 类型的 JSON 解析
if (currentTool.inputSchema?.properties) {
Object.entries(currentTool.inputSchema.properties).forEach(([key, prop]) => {
if ((prop.type === 'object' || prop.type === 'array') && values[key]) {
try {
values[key] = JSON.parse(values[key]);
} catch (e) {
message.error(`参数 ${key} 的JSON格式无效`);
throw e;
}
}
});
}
const res = await mcpTest({
name: currentTool.name,
arguments: values,
});
setTestResult(res.data);
if (res.code === 0) {
message.success('API调用成功');
}
} catch (error) {
// 验证失败或JSON解析失败
}
};
const renderTestResult = () => {
if (!testResult) return null;
if (typeof testResult === 'string') {
return <pre style={{ background: '#f5f5f5', padding: 10, borderRadius: 4, whiteSpace: 'pre-wrap' }}>{testResult}</pre>;
}
if (testResult.type === 'image' && testResult.content) {
return <Image src={testResult.content} style={{ maxWidth: '100%', maxHeight: 300 }} />;
}
if (testResult.type === 'text' && testResult.content) {
return <pre style={{ background: '#f5f5f5', padding: 10, borderRadius: 4, whiteSpace: 'pre-wrap' }}>{testResult.content}</pre>;
}
return <pre style={{ background: '#f5f5f5', padding: 10, borderRadius: 4, whiteSpace: 'pre-wrap' }}>{JSON.stringify(testResult, null, 2)}</pre>;
};
return (
<PageContainer>
<Card
title="MCP 服务器配置示例"
extra={
<Tooltip title="复制配置">
<Button icon={<CopyOutlined />} onClick={copyConfig} />
</Tooltip>
}
style={{ marginBottom: 16 }}
>
<pre style={{ background: '#f5f5f5', padding: 10, borderRadius: 4, whiteSpace: 'pre-wrap' }}>
{serverConfig}
</pre>
</Card>
<Row gutter={[16, 16]}>
{tools.map((tool) => (
<Col key={tool.name} xs={24} sm={12} lg={8}>
<Card
title={tool.name}
extra={
<Tooltip title="测试工具">
<Button
icon={<PlayCircleOutlined />}
onClick={() => openTestModal(tool)}
/>
</Tooltip>
}
style={{ minHeight: 200 }}
>
<p style={{ marginBottom: 8 }}>{tool.description}</p>
{tool.inputSchema?.properties && Object.keys(tool.inputSchema.properties).length > 0 && (
<div style={{ fontSize: 12, color: '#666', borderTop: '1px solid #f0f0f0', paddingTop: 8 }}>
<strong> ({Object.keys(tool.inputSchema.properties).length})</strong>
<div style={{ maxHeight: 100, overflow: 'auto', marginTop: 8 }}>
{Object.entries(tool.inputSchema.properties).map(([name, prop]) => (
<div key={name} style={{ padding: '4px 0', borderBottom: '1px solid #f5f5f5' }}>
<span style={{ fontWeight: 500 }}>{name}</span>
{tool.inputSchema?.required?.includes(name) && <span style={{ color: 'red' }}>*</span>}
<span style={{ marginLeft: 8, background: '#e6f7ff', padding: '0 4px', borderRadius: 2 }}>{prop.type}</span>
<div style={{ color: '#999', fontSize: 11 }}>{prop.description || '无描述'}</div>
</div>
))}
</div>
</div>
)}
{(!tool.inputSchema?.properties || Object.keys(tool.inputSchema.properties).length === 0) && (
<div style={{ color: '#999', fontStyle: 'italic', textAlign: 'center', padding: 16 }}>
</div>
)}
</Card>
</Col>
))}
</Row>
<Modal
title={currentTool ? `${currentTool.name} - 参数测试` : '参数测试'}
open={testModalVisible}
onCancel={() => setTestModalVisible(false)}
onOk={handleTest}
okText="测试"
width={600}
>
{currentTool && (
<Form form={form} layout="vertical">
{currentTool.inputSchema?.properties && Object.entries(currentTool.inputSchema.properties).map(([name, prop]) => (
<Form.Item
key={name}
label={prop.description || name}
name={name}
rules={currentTool.inputSchema?.required?.includes(name) ? [{ required: true, message: `请输入 ${prop.description || name}` }] : []}
>
{prop.type === 'boolean' ? (
<Select>
<Select.Option value={true}>True</Select.Option>
<Select.Option value={false}>False</Select.Option>
</Select>
) : prop.enum ? (
<Select>
{prop.enum.map((v) => (
<Select.Option key={v} value={v}>{v}</Select.Option>
))}
</Select>
) : prop.type === 'number' ? (
<Input type="number" placeholder={prop.description || `请输入${name}`} />
) : prop.type === 'object' || prop.type === 'array' ? (
<Input.TextArea rows={3} placeholder={`${prop.description || name} (请输入JSON格式)`} />
) : (
<Input placeholder={prop.description || `请输入${name}`} />
)}
</Form.Item>
))}
</Form>
)}
{testResult && (
<div style={{ marginTop: 16, padding: 16, border: '1px solid #f0f0f0', borderRadius: 4 }}>
<h4>API :</h4>
{renderTestResult()}
</div>
)}
</Modal>
</PageContainer>
);
};
export default MCPTest;