diff --git a/web/config/routes.ts b/web/config/routes.ts index 4a3a7a7..86af8d0 100644 --- a/web/config/routes.ts +++ b/web/config/routes.ts @@ -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' }, ], }, diff --git a/web/src/pages/system/state/index.tsx b/web/src/pages/system/state/index.tsx index 278a151..14ceefd 100644 --- a/web/src/pages/system/state/index.tsx +++ b/web/src/pages/system/state/index.tsx @@ -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(null); + const [state, setState] = useState({}); const [loading, setLoading] = useState(true); + const timerRef = useRef(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 ( - {/* CPU 使用率 */} - - - `${percent?.toFixed(1)}%`} - /> - - + {/* Runtime */} + + {state.os && ( + + + os: + {state.os.goos} + + + cpu nums: + {state.os.numCpu} + + + compiler: + {state.os.compiler} + + + go version: + {state.os.goVersion} + + + goroutine nums: + {state.os.numGoroutine} + + + )} - {/* 内存使用 */} - - - `${percent?.toFixed(1)}%`} - /> - - + {/* Disk */} + + {state.disk && ( + + {state.disk.map((item, index) => ( + + + + MountPoint + {item.mountPoint} + + + total (MB) + {item.totalMb} + + + used (MB) + {item.usedMb} + + + total (GB) + {item.totalGb} + + + used (GB) + {item.usedGb} + + + + + + + ))} + + )} + + + + + {/* CPU */} + + {state.cpu && ( + + + physical number of cores: + {state.cpu.cores} + + {state.cpu.cpus?.map((item, index) => ( + + core {index}: + + + + + ))} + + )} - {/* 磁盘使用 */} - - - `${percent?.toFixed(1)}%`} - /> - - - - - {/* 系统信息 */} - - -

Go 版本: {serverInfo?.os?.goVersion}

-

系统: {serverInfo?.os?.os}

-

架构: {serverInfo?.os?.arch}

-

Goroutine: {serverInfo?.os?.numGoroutine}

-
+ {/* Ram */} + + {state.ram && ( + + + + + total (MB) + {state.ram.totalMb} + + + used (MB) + {state.ram.usedMb} + + + total (GB) + {(state.ram.totalMb / 1024).toFixed(2)} + + + used (GB) + {(state.ram.usedMb / 1024).toFixed(2)} + + + + + + + + )}
diff --git a/web/src/pages/systemTools/autoCode/mcp/index.tsx b/web/src/pages/systemTools/autoCode/mcp/index.tsx new file mode 100644 index 0000000..e5574c7 --- /dev/null +++ b/web/src/pages/systemTools/autoCode/mcp/index.tsx @@ -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([]); + const [response, setResponse] = React.useState([]); + + 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) => ( + updateParam(record.key, 'name', e.target.value)} + /> + ), + }, + { + title: '描述', + dataIndex: 'description', + render: (_: any, record: ParamItem) => ( + updateParam(record.key, 'description', e.target.value)} + /> + ), + }, + { + title: '类型', + dataIndex: 'type', + width: 120, + render: (_: any, record: ParamItem) => ( + + ), + }, + { + title: '默认值', + dataIndex: 'default', + width: 150, + render: (_: any, record: ParamItem) => ( + updateParam(record.key, 'default', e.target.value)} + /> + ), + }, + { + title: '必填', + dataIndex: 'required', + width: 80, + render: (_: any, record: ParamItem) => ( + updateParam(record.key, 'required', e.target.checked)} + /> + ), + }, + { + title: '操作', + width: 80, + render: (_: any, record: ParamItem) => ( + + ), + }, + ]; + + const responseColumns = [ + { + title: '类型', + dataIndex: 'type', + render: (_: any, record: ResponseItem) => ( + + ), + }, + { + title: '操作', + width: 80, + render: (_: any, record: ResponseItem) => ( + + ), + }, + ]; + + return ( + + +
+ + + + + + + + + + + + + + +
+ + + + +
+ +
+
+ + + + ); +}; + +export default MCP; diff --git a/web/src/pages/systemTools/autoCode/mcpTest/index.tsx b/web/src/pages/systemTools/autoCode/mcpTest/index.tsx new file mode 100644 index 0000000..5487acd --- /dev/null +++ b/web/src/pages/systemTools/autoCode/mcpTest/index.tsx @@ -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; + required?: string[]; + }; +} + +interface McpServerConfig { + mcpServers: Record; +} + +const MCPTest: React.FC = () => { + const [tools, setTools] = useState([]); + const [serverConfig, setServerConfig] = useState(''); + const [testModalVisible, setTestModalVisible] = useState(false); + const [currentTool, setCurrentTool] = useState(null); + const [testResult, setTestResult] = useState(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 = {}; + 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
{testResult}
; + } + + if (testResult.type === 'image' && testResult.content) { + return ; + } + + if (testResult.type === 'text' && testResult.content) { + return
{testResult.content}
; + } + + return
{JSON.stringify(testResult, null, 2)}
; + }; + + return ( + + +
+ +