前端重构
This commit is contained in:
parent
010a96db19
commit
5f66394e7c
|
|
@ -102,8 +102,10 @@ export default [
|
|||
name: '插件系统',
|
||||
hideInMenu: true,
|
||||
routes: [
|
||||
{ path: '/plugin/announcement', name: '公告管理', component: './plugin/announcement' },
|
||||
{ path: '/plugin/email', name: '邮件插件', component: './plugin/email' },
|
||||
{ path: '/plugin/installPlugin', name: '插件安装', component: './systemTools/installPlugin' },
|
||||
{ path: '/plugin/pubPlug', name: '打包插件', component: './systemTools/pubPlug' },
|
||||
{ path: '/plugin/plugin-email', name: '邮件插件', component: './plugin/email' },
|
||||
{ path: '/plugin/anInfo', name: '公告管理', component: './plugin/announcement' },
|
||||
],
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -1,55 +1,57 @@
|
|||
/**
|
||||
* KRA - 服务器状态页面
|
||||
* 对应 GVA: view/systemTools/system/system.vue 中的服务器状态部分
|
||||
* 对应 GVA: view/system/state.vue
|
||||
*/
|
||||
import React, { useState, useEffect } from 'react';
|
||||
import React, { useState, useEffect, useRef } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Card, Row, Col, Progress, Statistic, Spin } from 'antd';
|
||||
import { Card, Row, Col, Progress, Spin } from 'antd';
|
||||
import { getServerInfo } from '@/services/kratos/system';
|
||||
|
||||
interface ServerInfo {
|
||||
cpu: {
|
||||
cpuNum: number;
|
||||
cpuPercent: number[];
|
||||
};
|
||||
mem: {
|
||||
total: number;
|
||||
used: number;
|
||||
usedPercent: number;
|
||||
};
|
||||
disk: {
|
||||
total: number;
|
||||
used: number;
|
||||
usedPercent: number;
|
||||
};
|
||||
os: {
|
||||
goVersion: string;
|
||||
os: string;
|
||||
arch: string;
|
||||
numCpu: number;
|
||||
compiler: string;
|
||||
version: string;
|
||||
numGoroutine: number;
|
||||
};
|
||||
interface DiskInfo {
|
||||
mountPoint: string;
|
||||
totalMb: number;
|
||||
usedMb: number;
|
||||
totalGb: number;
|
||||
usedGb: number;
|
||||
usedPercent: number;
|
||||
}
|
||||
|
||||
const formatBytes = (bytes: number): string => {
|
||||
if (bytes === 0) return '0 B';
|
||||
const k = 1024;
|
||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
||||
interface ServerState {
|
||||
os?: {
|
||||
goos: string;
|
||||
numCpu: number;
|
||||
compiler: string;
|
||||
goVersion: string;
|
||||
numGoroutine: number;
|
||||
};
|
||||
cpu?: {
|
||||
cores: number;
|
||||
cpus: number[];
|
||||
};
|
||||
ram?: {
|
||||
totalMb: number;
|
||||
usedMb: number;
|
||||
usedPercent: number;
|
||||
};
|
||||
disk?: DiskInfo[];
|
||||
}
|
||||
|
||||
const getProgressColor = (percent: number) => {
|
||||
if (percent < 20) return '#5cb87a';
|
||||
if (percent < 40) return '#e6a23c';
|
||||
return '#f56c6c';
|
||||
};
|
||||
|
||||
const State: React.FC = () => {
|
||||
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
|
||||
const [state, setState] = useState<ServerState>({});
|
||||
const [loading, setLoading] = useState(true);
|
||||
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||
|
||||
const fetchServerInfo = async () => {
|
||||
const reload = async () => {
|
||||
try {
|
||||
const res = await getServerInfo();
|
||||
if (res.code === 0) {
|
||||
setServerInfo(res.data);
|
||||
setState(res.data?.server || res.data || {});
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
|
|
@ -57,9 +59,13 @@ const State: React.FC = () => {
|
|||
};
|
||||
|
||||
useEffect(() => {
|
||||
fetchServerInfo();
|
||||
const timer = setInterval(fetchServerInfo, 5000);
|
||||
return () => clearInterval(timer);
|
||||
reload();
|
||||
timerRef.current = setInterval(reload, 10000); // 10秒刷新一次
|
||||
return () => {
|
||||
if (timerRef.current) {
|
||||
clearInterval(timerRef.current);
|
||||
}
|
||||
};
|
||||
}, []);
|
||||
|
||||
if (loading) {
|
||||
|
|
@ -75,56 +81,134 @@ const State: React.FC = () => {
|
|||
return (
|
||||
<PageContainer>
|
||||
<Row gutter={[16, 16]}>
|
||||
{/* CPU 使用率 */}
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card title="CPU 使用率">
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={serverInfo?.cpu?.cpuPercent?.[0] || 0}
|
||||
format={(percent) => `${percent?.toFixed(1)}%`}
|
||||
/>
|
||||
<Statistic title="CPU 核心数" value={serverInfo?.cpu?.cpuNum || 0} />
|
||||
</Card>
|
||||
{/* Runtime */}
|
||||
<Col xs={24} md={12}>
|
||||
{state.os && (
|
||||
<Card title="Runtime" style={{ height: 320 }}>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={12}>os:</Col>
|
||||
<Col span={12}>{state.os.goos}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={12}>cpu nums:</Col>
|
||||
<Col span={12}>{state.os.numCpu}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={12}>compiler:</Col>
|
||||
<Col span={12}>{state.os.compiler}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={12}>go version:</Col>
|
||||
<Col span={12}>{state.os.goVersion}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={12}>goroutine nums:</Col>
|
||||
<Col span={12}>{state.os.numGoroutine}</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{/* 内存使用 */}
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card title="内存使用">
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={serverInfo?.mem?.usedPercent || 0}
|
||||
format={(percent) => `${percent?.toFixed(1)}%`}
|
||||
/>
|
||||
<Statistic
|
||||
title="已用/总量"
|
||||
value={`${formatBytes(serverInfo?.mem?.used || 0)} / ${formatBytes(serverInfo?.mem?.total || 0)}`}
|
||||
/>
|
||||
</Card>
|
||||
{/* Disk */}
|
||||
<Col xs={24} md={12}>
|
||||
{state.disk && (
|
||||
<Card title="Disk" style={{ height: 320, overflow: 'auto' }}>
|
||||
{state.disk.map((item, index) => (
|
||||
<Row key={index} gutter={[8, 8]} style={{ marginBottom: 24 }}>
|
||||
<Col span={12}>
|
||||
<Row gutter={[8, 4]}>
|
||||
<Col span={12}>MountPoint</Col>
|
||||
<Col span={12}>{item.mountPoint}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 4]}>
|
||||
<Col span={12}>total (MB)</Col>
|
||||
<Col span={12}>{item.totalMb}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 4]}>
|
||||
<Col span={12}>used (MB)</Col>
|
||||
<Col span={12}>{item.usedMb}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 4]}>
|
||||
<Col span={12}>total (GB)</Col>
|
||||
<Col span={12}>{item.totalGb}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 4]}>
|
||||
<Col span={12}>used (GB)</Col>
|
||||
<Col span={12}>{item.usedGb}</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={Math.round(item.usedPercent)}
|
||||
strokeColor={getProgressColor(item.usedPercent)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
|
||||
<Row gutter={[16, 16]} style={{ marginTop: 16 }}>
|
||||
{/* CPU */}
|
||||
<Col xs={24} md={12}>
|
||||
{state.cpu && (
|
||||
<Card title="CPU" style={{ height: 320, overflow: 'auto' }}>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={12}>physical number of cores:</Col>
|
||||
<Col span={12}>{state.cpu.cores}</Col>
|
||||
</Row>
|
||||
{state.cpu.cpus?.map((item, index) => (
|
||||
<Row key={index} gutter={[8, 8]}>
|
||||
<Col span={12}>core {index}:</Col>
|
||||
<Col span={12}>
|
||||
<Progress
|
||||
percent={Math.round(item)}
|
||||
strokeColor={getProgressColor(item)}
|
||||
size="small"
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
))}
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
|
||||
{/* 磁盘使用 */}
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card title="磁盘使用">
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={serverInfo?.disk?.usedPercent || 0}
|
||||
format={(percent) => `${percent?.toFixed(1)}%`}
|
||||
/>
|
||||
<Statistic
|
||||
title="已用/总量"
|
||||
value={`${formatBytes(serverInfo?.disk?.used || 0)} / ${formatBytes(serverInfo?.disk?.total || 0)}`}
|
||||
/>
|
||||
</Card>
|
||||
</Col>
|
||||
|
||||
{/* 系统信息 */}
|
||||
<Col xs={24} sm={12} lg={6}>
|
||||
<Card title="系统信息">
|
||||
<p><strong>Go 版本:</strong> {serverInfo?.os?.goVersion}</p>
|
||||
<p><strong>系统:</strong> {serverInfo?.os?.os}</p>
|
||||
<p><strong>架构:</strong> {serverInfo?.os?.arch}</p>
|
||||
<p><strong>Goroutine:</strong> {serverInfo?.os?.numGoroutine}</p>
|
||||
</Card>
|
||||
{/* Ram */}
|
||||
<Col xs={24} md={12}>
|
||||
{state.ram && (
|
||||
<Card title="Ram" style={{ height: 320 }}>
|
||||
<Row gutter={[8, 8]}>
|
||||
<Col span={12}>
|
||||
<Row gutter={[8, 4]}>
|
||||
<Col span={12}>total (MB)</Col>
|
||||
<Col span={12}>{state.ram.totalMb}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 4]}>
|
||||
<Col span={12}>used (MB)</Col>
|
||||
<Col span={12}>{state.ram.usedMb}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 4]}>
|
||||
<Col span={12}>total (GB)</Col>
|
||||
<Col span={12}>{(state.ram.totalMb / 1024).toFixed(2)}</Col>
|
||||
</Row>
|
||||
<Row gutter={[8, 4]}>
|
||||
<Col span={12}>used (GB)</Col>
|
||||
<Col span={12}>{(state.ram.usedMb / 1024).toFixed(2)}</Col>
|
||||
</Row>
|
||||
</Col>
|
||||
<Col span={12}>
|
||||
<Progress
|
||||
type="dashboard"
|
||||
percent={Math.round(state.ram.usedPercent)}
|
||||
strokeColor={getProgressColor(state.ram.usedPercent)}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
</Card>
|
||||
)}
|
||||
</Col>
|
||||
</Row>
|
||||
</PageContainer>
|
||||
|
|
|
|||
|
|
@ -0,0 +1,264 @@
|
|||
/**
|
||||
* KRA - MCP Tools模板
|
||||
* 对应 GVA: view/systemTools/autoCode/mcp.vue
|
||||
*/
|
||||
import React from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Card, Form, Input, Button, Table, Select, Checkbox, message } from 'antd';
|
||||
import { PlusOutlined, DeleteOutlined } from '@ant-design/icons';
|
||||
import { mcp } from '@/services/kratos/autoCode';
|
||||
|
||||
interface ParamItem {
|
||||
key: string;
|
||||
name: string;
|
||||
description: string;
|
||||
type: string;
|
||||
default?: string;
|
||||
required: boolean;
|
||||
}
|
||||
|
||||
interface ResponseItem {
|
||||
key: string;
|
||||
type: string;
|
||||
}
|
||||
|
||||
const MCP: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [params, setParams] = React.useState<ParamItem[]>([]);
|
||||
const [response, setResponse] = React.useState<ResponseItem[]>([]);
|
||||
|
||||
const addParam = () => {
|
||||
setParams([...params, {
|
||||
key: Date.now().toString(),
|
||||
name: '',
|
||||
description: '',
|
||||
type: 'string',
|
||||
default: '',
|
||||
required: false,
|
||||
}]);
|
||||
};
|
||||
|
||||
const removeParam = (key: string) => {
|
||||
setParams(params.filter(p => p.key !== key));
|
||||
};
|
||||
|
||||
const updateParam = (key: string, field: string, value: any) => {
|
||||
setParams(params.map(p => p.key === key ? { ...p, [field]: value } : p));
|
||||
};
|
||||
|
||||
const addResponse = () => {
|
||||
setResponse([...response, { key: Date.now().toString(), type: 'text' }]);
|
||||
};
|
||||
|
||||
const removeResponse = (key: string) => {
|
||||
setResponse(response.filter(r => r.key !== key));
|
||||
};
|
||||
|
||||
const handleSubmit = async () => {
|
||||
try {
|
||||
const values = await form.validateFields();
|
||||
|
||||
// 验证参数
|
||||
for (const p of params) {
|
||||
if (!p.name || !p.description || !p.type) {
|
||||
message.error('请完善所有参数信息');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 验证返回参数
|
||||
for (const r of response) {
|
||||
if (!r.type) {
|
||||
message.error('请完善所有返回参数类型');
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
const res = await mcp({
|
||||
...values,
|
||||
params: params.map(({ key, ...rest }) => rest),
|
||||
response: response.map(({ key, ...rest }) => rest),
|
||||
});
|
||||
|
||||
if (res.code === 0) {
|
||||
message.success(res.msg || '生成成功');
|
||||
}
|
||||
} catch (error) {
|
||||
// 表单验证失败
|
||||
}
|
||||
};
|
||||
|
||||
const paramColumns = [
|
||||
{
|
||||
title: '参数名',
|
||||
dataIndex: 'name',
|
||||
width: 120,
|
||||
render: (_: any, record: ParamItem) => (
|
||||
<Input
|
||||
value={record.name}
|
||||
placeholder="参数名"
|
||||
onChange={(e) => updateParam(record.key, 'name', e.target.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '描述',
|
||||
dataIndex: 'description',
|
||||
render: (_: any, record: ParamItem) => (
|
||||
<Input
|
||||
value={record.description}
|
||||
placeholder="描述"
|
||||
onChange={(e) => updateParam(record.key, 'description', e.target.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
width: 120,
|
||||
render: (_: any, record: ParamItem) => (
|
||||
<Select
|
||||
value={record.type}
|
||||
style={{ width: '100%' }}
|
||||
onChange={(value) => updateParam(record.key, 'type', value)}
|
||||
>
|
||||
<Select.Option value="string">string</Select.Option>
|
||||
<Select.Option value="number">number</Select.Option>
|
||||
<Select.Option value="boolean">boolean</Select.Option>
|
||||
<Select.Option value="object">object</Select.Option>
|
||||
<Select.Option value="array">array</Select.Option>
|
||||
</Select>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '默认值',
|
||||
dataIndex: 'default',
|
||||
width: 150,
|
||||
render: (_: any, record: ParamItem) => (
|
||||
<Input
|
||||
value={record.default}
|
||||
disabled={record.type === 'object'}
|
||||
onChange={(e) => updateParam(record.key, 'default', e.target.value)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '必填',
|
||||
dataIndex: 'required',
|
||||
width: 80,
|
||||
render: (_: any, record: ParamItem) => (
|
||||
<Checkbox
|
||||
checked={record.required}
|
||||
onChange={(e) => updateParam(record.key, 'required', e.target.checked)}
|
||||
/>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
render: (_: any, record: ParamItem) => (
|
||||
<Button type="link" danger onClick={() => removeParam(record.key)}>
|
||||
删除
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
const responseColumns = [
|
||||
{
|
||||
title: '类型',
|
||||
dataIndex: 'type',
|
||||
render: (_: any, record: ResponseItem) => (
|
||||
<Select
|
||||
value={record.type}
|
||||
style={{ width: 200 }}
|
||||
onChange={(value) => {
|
||||
setResponse(response.map(r => r.key === record.key ? { ...r, type: value } : r));
|
||||
}}
|
||||
>
|
||||
<Select.Option value="text">text</Select.Option>
|
||||
<Select.Option value="image">image</Select.Option>
|
||||
</Select>
|
||||
),
|
||||
},
|
||||
{
|
||||
title: '操作',
|
||||
width: 80,
|
||||
render: (_: any, record: ResponseItem) => (
|
||||
<Button type="link" danger onClick={() => removeResponse(record.key)}>
|
||||
删除
|
||||
</Button>
|
||||
),
|
||||
},
|
||||
];
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<Card>
|
||||
<Form form={form} layout="vertical">
|
||||
<Form.Item
|
||||
label="工具名称"
|
||||
name="name"
|
||||
rules={[{ required: true, message: '请输入工具名称' }]}
|
||||
>
|
||||
<Input placeholder="例:CurrentTime" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item
|
||||
label="工具描述"
|
||||
name="description"
|
||||
rules={[{ required: true, message: '请输入工具描述' }]}
|
||||
>
|
||||
<Input.TextArea placeholder="请输入工具描述" />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="参数列表">
|
||||
<Table
|
||||
columns={paramColumns}
|
||||
dataSource={params}
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={addParam}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
添加参数
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="返回参数">
|
||||
<Table
|
||||
columns={responseColumns}
|
||||
dataSource={response}
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
size="small"
|
||||
/>
|
||||
<Button
|
||||
type="primary"
|
||||
icon={<PlusOutlined />}
|
||||
onClick={addResponse}
|
||||
style={{ marginTop: 10 }}
|
||||
>
|
||||
添加返回参数
|
||||
</Button>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<div style={{ textAlign: 'right' }}>
|
||||
<Button type="primary" onClick={handleSubmit}>
|
||||
生成
|
||||
</Button>
|
||||
</div>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default MCP;
|
||||
|
|
@ -0,0 +1,235 @@
|
|||
/**
|
||||
* 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;
|
||||
|
|
@ -0,0 +1,211 @@
|
|||
/**
|
||||
* KRA - AI页面绘制
|
||||
* 对应 GVA: view/systemTools/autoCode/picture.vue
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { PageContainer } from '@ant-design/pro-components';
|
||||
import { Card, Form, Input, Button, Radio, Checkbox, Tabs, Empty, message } from 'antd';
|
||||
import WarningBar from '@/components/WarningBar/WarningBar';
|
||||
import { createWeb } from '@/services/kratos/autoCode';
|
||||
|
||||
const { TextArea } = Input;
|
||||
|
||||
// 页面用途与内容板块的推荐映射
|
||||
const pageTypeContentMap: Record<string, string[]> = {
|
||||
'企业官网': ['Banner轮播图', '产品/服务介绍', '功能特点展示', '客户案例', '联系表单'],
|
||||
'电商页面': ['Banner轮播图', '商品列表', '商品卡片', '购物车', '商品分类', '热门推荐', '限时特惠', '结算页面', '用户评价'],
|
||||
'个人博客': ['Banner轮播图', '新闻/博客列表', '用户评价', '联系表单'],
|
||||
'产品介绍': ['Banner轮播图', '产品/服务介绍', '功能特点展示', '价格表', 'FAQ/常见问题'],
|
||||
'活动落地页': ['Banner轮播图', '功能特点展示', '联系表单', '数据统计'],
|
||||
};
|
||||
|
||||
const contentBlockOptions = [
|
||||
'Banner轮播图', '产品/服务介绍', '功能特点展示', '客户案例', '团队介绍',
|
||||
'联系表单', '新闻/博客列表', '价格表', 'FAQ/常见问题', '用户评价',
|
||||
'数据统计', '商品列表', '商品卡片', '购物车', '结算页面',
|
||||
'订单跟踪', '商品分类', '热门推荐', '限时特惠', '其他',
|
||||
];
|
||||
|
||||
const Picture: React.FC = () => {
|
||||
const [form] = Form.useForm();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [output, setOutput] = useState<string>('');
|
||||
const [pageType, setPageType] = useState('企业官网');
|
||||
const [contentBlocks, setContentBlocks] = useState<string[]>(['Banner轮播图', '产品/服务介绍']);
|
||||
|
||||
const handlePageTypeChange = (value: string) => {
|
||||
setPageType(value);
|
||||
if (value !== '其他' && pageTypeContentMap[value]) {
|
||||
setContentBlocks([...pageTypeContentMap[value]]);
|
||||
form.setFieldsValue({ contentBlocks: pageTypeContentMap[value] });
|
||||
}
|
||||
};
|
||||
|
||||
const handleGenerate = async () => {
|
||||
const values = await form.validateFields();
|
||||
|
||||
let fullPrompt = '';
|
||||
fullPrompt += `页面用途: ${values.pageType === '其他' ? values.pageTypeCustom : values.pageType}\n`;
|
||||
|
||||
const blocks = (values.contentBlocks || []).filter((b: string) => b !== '其他');
|
||||
if (values.contentBlocksCustom) blocks.push(values.contentBlocksCustom);
|
||||
fullPrompt += `主要内容板块: ${blocks.join(', ')}\n`;
|
||||
|
||||
fullPrompt += `风格偏好: ${values.stylePreference === '其他' ? values.stylePreferenceCustom : values.stylePreference}\n`;
|
||||
fullPrompt += `设计布局: ${values.layoutDesign === '其他' ? values.layoutDesignCustom : values.layoutDesign}\n`;
|
||||
fullPrompt += `配色方案: ${values.colorScheme === '其他' ? values.colorSchemeCustom : values.colorScheme}\n`;
|
||||
|
||||
if (values.prompt) {
|
||||
fullPrompt += `\n详细描述: ${values.prompt}`;
|
||||
}
|
||||
|
||||
setLoading(true);
|
||||
try {
|
||||
const res = await createWeb({ web: fullPrompt, command: 'createWeb' });
|
||||
if (res.code === 0) {
|
||||
setOutput(res.data);
|
||||
message.success('生成成功');
|
||||
}
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
const copyCode = () => {
|
||||
navigator.clipboard.writeText(output);
|
||||
message.success('复制成功');
|
||||
};
|
||||
|
||||
return (
|
||||
<PageContainer>
|
||||
<WarningBar
|
||||
title="此功能只针对授权用户开放"
|
||||
href="https://www.gin-vue-admin.com/empower/"
|
||||
/>
|
||||
|
||||
<Card title="AI前端工程师" style={{ marginBottom: 16 }}>
|
||||
<Form form={form} layout="vertical" initialValues={{
|
||||
pageType: '企业官网',
|
||||
contentBlocks: ['Banner轮播图', '产品/服务介绍'],
|
||||
stylePreference: '简约',
|
||||
layoutDesign: '响应式',
|
||||
colorScheme: '蓝色系',
|
||||
}}>
|
||||
<Form.Item label="页面用途" name="pageType">
|
||||
<Radio.Group onChange={(e) => handlePageTypeChange(e.target.value)}>
|
||||
<Radio value="企业官网">企业官网</Radio>
|
||||
<Radio value="电商页面">电商页面</Radio>
|
||||
<Radio value="个人博客">个人博客</Radio>
|
||||
<Radio value="产品介绍">产品介绍</Radio>
|
||||
<Radio value="活动落地页">活动落地页</Radio>
|
||||
<Radio value="其他">其他</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item noStyle shouldUpdate={(prev, cur) => prev.pageType !== cur.pageType}>
|
||||
{({ getFieldValue }) => getFieldValue('pageType') === '其他' && (
|
||||
<Form.Item name="pageTypeCustom">
|
||||
<Input placeholder="请输入页面用途" />
|
||||
</Form.Item>
|
||||
)}
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="主要内容板块" name="contentBlocks">
|
||||
<Checkbox.Group options={contentBlockOptions} />
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="风格偏好" name="stylePreference">
|
||||
<Radio.Group>
|
||||
<Radio value="简约">简约</Radio>
|
||||
<Radio value="科技感">科技感</Radio>
|
||||
<Radio value="温馨">温馨</Radio>
|
||||
<Radio value="专业">专业</Radio>
|
||||
<Radio value="创意">创意</Radio>
|
||||
<Radio value="复古">复古</Radio>
|
||||
<Radio value="奢华">奢华</Radio>
|
||||
<Radio value="其他">其他</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="设计布局" name="layoutDesign">
|
||||
<Radio.Group>
|
||||
<Radio value="单栏布局">单栏布局</Radio>
|
||||
<Radio value="双栏布局">双栏布局</Radio>
|
||||
<Radio value="三栏布局">三栏布局</Radio>
|
||||
<Radio value="网格布局">网格布局</Radio>
|
||||
<Radio value="瀑布流">瀑布流</Radio>
|
||||
<Radio value="卡片式">卡片式</Radio>
|
||||
<Radio value="响应式">响应式</Radio>
|
||||
<Radio value="其他">其他</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="配色方案" name="colorScheme">
|
||||
<Radio.Group>
|
||||
<Radio value="蓝色系">蓝色系</Radio>
|
||||
<Radio value="绿色系">绿色系</Radio>
|
||||
<Radio value="红色系">红色系</Radio>
|
||||
<Radio value="黑白灰">黑白灰</Radio>
|
||||
<Radio value="暖色调">暖色调</Radio>
|
||||
<Radio value="冷色调">冷色调</Radio>
|
||||
<Radio value="其他">其他</Radio>
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item label="详细描述(可选)" name="prompt">
|
||||
<TextArea
|
||||
rows={5}
|
||||
maxLength={2000}
|
||||
placeholder="补充您对页面的其他要求或特殊需求"
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item>
|
||||
<Button type="primary" loading={loading} onClick={handleGenerate}>
|
||||
生成
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</Card>
|
||||
|
||||
{output ? (
|
||||
<Card>
|
||||
<Tabs items={[
|
||||
{
|
||||
key: 'preview',
|
||||
label: '页面预览',
|
||||
children: (
|
||||
<div style={{ height: 500, overflow: 'auto', background: '#f5f5f5', padding: 16 }}>
|
||||
<div dangerouslySetInnerHTML={{ __html: output }} />
|
||||
</div>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'source',
|
||||
label: '源代码',
|
||||
children: (
|
||||
<div style={{ position: 'relative' }}>
|
||||
<Button
|
||||
style={{ position: 'absolute', top: 8, right: 8 }}
|
||||
onClick={copyCode}
|
||||
>
|
||||
复制
|
||||
</Button>
|
||||
<pre style={{ height: 500, overflow: 'auto', background: '#f5f5f5', padding: 16, marginTop: 40 }}>
|
||||
{output}
|
||||
</pre>
|
||||
</div>
|
||||
),
|
||||
},
|
||||
]} />
|
||||
</Card>
|
||||
) : (
|
||||
<Card>
|
||||
<Empty description="暂无生成内容" />
|
||||
</Card>
|
||||
)}
|
||||
</PageContainer>
|
||||
);
|
||||
};
|
||||
|
||||
export default Picture;
|
||||
Loading…
Reference in New Issue