236 lines
8.2 KiB
TypeScript
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;
|