前端重构
This commit is contained in:
parent
010a96db19
commit
5f66394e7c
|
|
@ -102,8 +102,10 @@ export default [
|
||||||
name: '插件系统',
|
name: '插件系统',
|
||||||
hideInMenu: true,
|
hideInMenu: true,
|
||||||
routes: [
|
routes: [
|
||||||
{ path: '/plugin/announcement', name: '公告管理', component: './plugin/announcement' },
|
{ path: '/plugin/installPlugin', name: '插件安装', component: './systemTools/installPlugin' },
|
||||||
{ path: '/plugin/email', name: '邮件插件', component: './plugin/email' },
|
{ 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 - 服务器状态页面
|
* 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 { 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';
|
import { getServerInfo } from '@/services/kratos/system';
|
||||||
|
|
||||||
interface ServerInfo {
|
interface DiskInfo {
|
||||||
cpu: {
|
mountPoint: string;
|
||||||
cpuNum: number;
|
totalMb: number;
|
||||||
cpuPercent: number[];
|
usedMb: number;
|
||||||
};
|
totalGb: number;
|
||||||
mem: {
|
usedGb: number;
|
||||||
total: number;
|
|
||||||
used: number;
|
|
||||||
usedPercent: 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;
|
|
||||||
};
|
|
||||||
}
|
}
|
||||||
|
|
||||||
const formatBytes = (bytes: number): string => {
|
interface ServerState {
|
||||||
if (bytes === 0) return '0 B';
|
os?: {
|
||||||
const k = 1024;
|
goos: string;
|
||||||
const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
|
numCpu: number;
|
||||||
const i = Math.floor(Math.log(bytes) / Math.log(k));
|
compiler: string;
|
||||||
return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
|
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 State: React.FC = () => {
|
||||||
const [serverInfo, setServerInfo] = useState<ServerInfo | null>(null);
|
const [state, setState] = useState<ServerState>({});
|
||||||
const [loading, setLoading] = useState(true);
|
const [loading, setLoading] = useState(true);
|
||||||
|
const timerRef = useRef<NodeJS.Timeout | null>(null);
|
||||||
|
|
||||||
const fetchServerInfo = async () => {
|
const reload = async () => {
|
||||||
try {
|
try {
|
||||||
const res = await getServerInfo();
|
const res = await getServerInfo();
|
||||||
if (res.code === 0) {
|
if (res.code === 0) {
|
||||||
setServerInfo(res.data);
|
setState(res.data?.server || res.data || {});
|
||||||
}
|
}
|
||||||
} finally {
|
} finally {
|
||||||
setLoading(false);
|
setLoading(false);
|
||||||
|
|
@ -57,9 +59,13 @@ const State: React.FC = () => {
|
||||||
};
|
};
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
fetchServerInfo();
|
reload();
|
||||||
const timer = setInterval(fetchServerInfo, 5000);
|
timerRef.current = setInterval(reload, 10000); // 10秒刷新一次
|
||||||
return () => clearInterval(timer);
|
return () => {
|
||||||
|
if (timerRef.current) {
|
||||||
|
clearInterval(timerRef.current);
|
||||||
|
}
|
||||||
|
};
|
||||||
}, []);
|
}, []);
|
||||||
|
|
||||||
if (loading) {
|
if (loading) {
|
||||||
|
|
@ -75,56 +81,134 @@ const State: React.FC = () => {
|
||||||
return (
|
return (
|
||||||
<PageContainer>
|
<PageContainer>
|
||||||
<Row gutter={[16, 16]}>
|
<Row gutter={[16, 16]}>
|
||||||
{/* CPU 使用率 */}
|
{/* Runtime */}
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} md={12}>
|
||||||
<Card title="CPU 使用率">
|
{state.os && (
|
||||||
<Progress
|
<Card title="Runtime" style={{ height: 320 }}>
|
||||||
type="dashboard"
|
<Row gutter={[8, 8]}>
|
||||||
percent={serverInfo?.cpu?.cpuPercent?.[0] || 0}
|
<Col span={12}>os:</Col>
|
||||||
format={(percent) => `${percent?.toFixed(1)}%`}
|
<Col span={12}>{state.os.goos}</Col>
|
||||||
/>
|
</Row>
|
||||||
<Statistic title="CPU 核心数" value={serverInfo?.cpu?.cpuNum || 0} />
|
<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>
|
</Card>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
|
|
||||||
{/* 内存使用 */}
|
{/* Disk */}
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} md={12}>
|
||||||
<Card title="内存使用">
|
{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
|
<Progress
|
||||||
type="dashboard"
|
type="dashboard"
|
||||||
percent={serverInfo?.mem?.usedPercent || 0}
|
percent={Math.round(item.usedPercent)}
|
||||||
format={(percent) => `${percent?.toFixed(1)}%`}
|
strokeColor={getProgressColor(item.usedPercent)}
|
||||||
/>
|
|
||||||
<Statistic
|
|
||||||
title="已用/总量"
|
|
||||||
value={`${formatBytes(serverInfo?.mem?.used || 0)} / ${formatBytes(serverInfo?.mem?.total || 0)}`}
|
|
||||||
/>
|
/>
|
||||||
|
</Col>
|
||||||
|
</Row>
|
||||||
|
))}
|
||||||
</Card>
|
</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>
|
||||||
|
|
||||||
{/* 磁盘使用 */}
|
{/* Ram */}
|
||||||
<Col xs={24} sm={12} lg={6}>
|
<Col xs={24} md={12}>
|
||||||
<Card title="磁盘使用">
|
{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
|
<Progress
|
||||||
type="dashboard"
|
type="dashboard"
|
||||||
percent={serverInfo?.disk?.usedPercent || 0}
|
percent={Math.round(state.ram.usedPercent)}
|
||||||
format={(percent) => `${percent?.toFixed(1)}%`}
|
strokeColor={getProgressColor(state.ram.usedPercent)}
|
||||||
/>
|
/>
|
||||||
<Statistic
|
|
||||||
title="已用/总量"
|
|
||||||
value={`${formatBytes(serverInfo?.disk?.used || 0)} / ${formatBytes(serverInfo?.disk?.total || 0)}`}
|
|
||||||
/>
|
|
||||||
</Card>
|
|
||||||
</Col>
|
</Col>
|
||||||
|
</Row>
|
||||||
{/* 系统信息 */}
|
|
||||||
<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>
|
</Card>
|
||||||
|
)}
|
||||||
</Col>
|
</Col>
|
||||||
</Row>
|
</Row>
|
||||||
</PageContainer>
|
</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