diff --git a/web/package.json b/web/package.json index d6642ac8..e6c7483d 100644 --- a/web/package.json +++ b/web/package.json @@ -17,7 +17,11 @@ "@dnd-kit/modifiers": "^9.0.0", "@dnd-kit/sortable": "^10.0.0", "@dnd-kit/utilities": "^3.2.2", + "@lexical/code": "^0.39.0", + "@lexical/link": "^0.39.0", + "@lexical/list": "^0.39.0", "@lexical/react": "^0.39.0", + "@lexical/rich-text": "^0.39.0", "antd": "^5.27.4", "axios": "^1.12.2", "clsx": "^2.1.1", diff --git a/web/src/api/tools.ts b/web/src/api/tools.ts new file mode 100644 index 00000000..142d4c41 --- /dev/null +++ b/web/src/api/tools.ts @@ -0,0 +1,31 @@ +import { request } from '@/utils/request' +import type { Query, CustomToolItem, ExecuteData, MCPToolItem, InnerToolItem } from '@/views/ToolManagement/types' + +// 工具列表 +export const getTools = (data: Query) => { + return request.get('/tools', data) +} +// 创建MCP工具 +export const addTool = (values: MCPToolItem | CustomToolItem) => { + return request.post('/tools', values) +} +// 更新工具 +export const updateTool = (tool_id: string, data: MCPToolItem | InnerToolItem | CustomToolItem) => { + return request.put(`/tools/${tool_id}`, data) +} +// 删除工具 +export const deleteTool = (tool_id: string) => { + return request.delete(`/tools/${tool_id}`) +} +// MCP 测试连接 +export const testConnection = (tool_id: string) => { + return request.post(`/tools/${tool_id}/test`) +} +// 工具测试 +export const execute = (data: ExecuteData) => { + return request.post(`/tools/execution/execute`, data) +} +export const parseSchema = (data: Record) => { + return request.post(`/tools/parse_schema`, data) + +} \ No newline at end of file diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index 3843a760..b6795623 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1372,13 +1372,19 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re addService: 'Add MCP Service', addServiceSuccess: 'Service added successfully', server_url: 'Service URL', - lastConnection: 'Last Connection', + last_health_check: 'Last Connection', responseTime: 'Response Time', status: { - active: 'Active', - inactive: 'Inactive', + available: '可用', + unconfigured: '未配置', + configured_disabled: '已配置未启用', + error: '链接异常' }, - testConnectionSuccess: 'Connection test successful', + available_desc: 'API 已配置并启用', + unconfigured_desc: '需要配置 API Key', + configured_disabled_desc: 'API 已配置但未启用', + error_desc: 'API 已配置但链接异常', + serviceEndpoint: 'Service Endpoint URL', serviceEndpointPlaceholder: 'URL of the service endpoint', serviceEndpointExtra: 'Complete access address of the MCP service', @@ -1392,13 +1398,14 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re auth: 'Authentication', requestHeader: 'Request Headers', config: 'Configuration', - authType: 'Authentication Type', - noAuth: 'No Authentication', - apiKey: 'API Key', - basicAuth: 'Basic Auth', - bearerToken: 'Bearer Token', + auth_type: 'Authentication Type', + none: 'No Authentication', + api_key: 'API Key', + basic_auth: 'Basic Auth', + bearer_token: 'Bearer Token', username: 'Username', password: 'Password', + key_name: 'Key Name', requestHeaderDesc: 'Additional HTTP request headers sent to MCP server', addRequestHeader: 'Add Request Header', editRequestHeader: 'Edit Request Header', @@ -1472,7 +1479,6 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re MinerUTool_config_desc: 'MinerU is a high-precision PDF document parsing tool that requires an API Key to use.', TextInTool_config_desc: 'TextIn provides intelligent OCR text recognition service with multi-language support.', link: 'Application URL', - api_key: 'API Key', BaiduSearchTool_api_key_desc: 'API Key obtained from Baidu Open Platform', MinerUTool_api_key_desc: 'API Key obtained from MinerU platform', secret_key: 'Secret Key', @@ -1529,7 +1535,8 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re desc: 'Description', method: 'Method', path: 'Path', - viewDetail: 'View Details' + viewDetail: 'View Details', + noResult: 'Processing results will be displayed here' }, workflow: { coreNode: 'Core Nodes', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index d83607f3..bc67c463 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1360,6 +1360,8 @@ export const zh = { startANewConversation: '开始新对话', normalReply: '正常回复', quickReply: '快速回复', + web_search: '联网搜索', + memory: '记忆', }, login: { title: '红熊记忆科学', @@ -1470,12 +1472,19 @@ export const zh = { addService: '添加MCP服务', addServiceSuccess: '服务添加成功', server_url: '服务地址', - lastConnection: '最后连接', + last_health_check: '最后连接', responseTime: '响应时间', status: { - active: '活跃', - inactive: '不活跃', + available: '可用', + unconfigured: '未配置', + configured_disabled: '已配置未启用', + error: '链接异常' }, + available_desc: 'API 已配置并启用', + unconfigured_desc: '需要配置 API Key', + configured_disabled_desc: 'API 已配置但未启用', + error_desc: 'API 已配置但链接异常', + testConnectionSuccess: '测试连接成功', serviceEndpoint: '服务端点 URL', serviceEndpointPlaceholder: '服务端点的 URL', @@ -1490,13 +1499,14 @@ export const zh = { auth: '认证', requestHeader: '请求头', config: '配置', - authType: '认证方式', - noAuth: '无需认证', - apiKey: 'API Key', - basicAuth: 'Basic Auth', - bearerToken: 'Bearer Token', + auth_Type: '认证方式', + none: '无需认证', + api_key: 'API Key', + basic_auth: 'Basic Auth', + bearer_token: 'Bearer Token', username: '用户名', password: '密码', + key_name: 'Key Name', requestHeaderDesc: '发送到 MCP 服务器的额外 HTTP 请求头', addRequestHeader: '添加请求头', editRequestHeader: '编辑请求头', @@ -1570,7 +1580,6 @@ export const zh = { MinerUTool_config_desc: 'MinerU是高精度PDF文档解析工具,需要API Key才能使用。', TextInTool_config_desc: 'TextIn提供智能OCR文字识别服务,支持多语言识别。', link: '申请地址', - api_key: 'API Key', BaiduSearchTool_api_key_desc: '从百度开放平台获取的API Key', MinerUTool_api_key_desc: '从MinerU平台获取的API Key', secret_key: 'Secret Key', @@ -1627,7 +1636,9 @@ export const zh = { desc: '描述', method: '方法', path: '路径', - viewDetail: '查看详情' + viewDetail: '查看详情', + textLink: '测试连接', + noResult: '处理结果将显示在这里' }, workflow: { coreNode: '核心节点', diff --git a/web/src/routes/index.tsx b/web/src/routes/index.tsx index e7624994..a4c20163 100644 --- a/web/src/routes/index.tsx +++ b/web/src/routes/index.tsx @@ -61,6 +61,7 @@ const componentMap: Record>> = OrderPayment: lazy(() => import('@/views/OrderPayment')), OrderHistory: lazy(() => import('@/views/OrderHistory')), Pricing: lazy(() => import('@/views/Pricing')), + ToolManagement: lazy(() => import('@/views/ToolManagement')), Login: lazy(() => import('@/views/Login')), InviteRegister: lazy(() => import('@/views/InviteRegister')), NoPermission: lazy(() => import('@/views/NoPermission')), diff --git a/web/src/routes/routes.json b/web/src/routes/routes.json index 45e187d6..a414185d 100644 --- a/web/src/routes/routes.json +++ b/web/src/routes/routes.json @@ -5,6 +5,7 @@ { "path": "/user-management", "element": "UserManagement" }, { "path": "/model", "element": "ModelManagement" }, { "path": "/space", "element": "SpaceManagement" }, + { "path": "/tool", "element": "ToolManagement" }, { "path": "/pricing", "element": "Pricing" }, { "path": "/order-pay", "element": "OrderPayment" }, { "path": "/orders", "element": "OrderHistory" }, diff --git a/web/src/store/menu.json b/web/src/store/menu.json index a8232531..1c4cec9d 100644 --- a/web/src/store/menu.json +++ b/web/src/store/menu.json @@ -26,6 +26,19 @@ "sort": 0, "subs": [] }, + { + "id": 7, + "parent": 0, + "code": "tool", + "label": "工具管理", + "i18nKey": "menu.toolManagement", + "path": "/tool", + "enable": true, + "display": true, + "level": 1, + "sort": 0, + "subs": [] + }, { "id": 6, "parent": 0, diff --git a/web/src/utils/request.ts b/web/src/utils/request.ts index 647b30f6..447ff88c 100644 --- a/web/src/utils/request.ts +++ b/web/src/utils/request.ts @@ -302,6 +302,10 @@ export const request = { // 获取父级域名 const getParentDomain = () => { const hostname = window.location.hostname + // 检查是否为IP地址 + if (/^\d+\.\d+\.\d+\.\d+$/.test(hostname)) { + return hostname + } const parts = hostname.split('.') return parts.length > 2 ? `.${parts.slice(-2).join('.')}` : hostname } diff --git a/web/src/views/Conversation/index.tsx b/web/src/views/Conversation/index.tsx index e19f4f06..d791bf2d 100644 --- a/web/src/views/Conversation/index.tsx +++ b/web/src/views/Conversation/index.tsx @@ -208,7 +208,7 @@ const Conversation: FC = () => { return ( -
+
handleChangeHistory(null)} > @@ -250,7 +250,7 @@ const Conversation: FC = () => {
} - +
@@ -264,7 +264,7 @@ const Conversation: FC = () => { onSend={handleSend} labelFormat={(item) => dayjs(item.created_at).locale('en').format('MMMM D, YYYY [at] h:mm A')} > -
+ ReactNode }> = ({ getStatusTag }) => { + const { t } = useTranslation(); + const { message, modal } = App.useApp() + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + const [query, setQuery] = useState({ name: undefined, tool_type: 'custom' }); + const customToolModalRef = useRef(null); + + useEffect(() => { + getData() + }, [query.name]) + + const getData = () => { + setLoading(true) + getTools(query) + .then((res) => { + setData(res as ToolItem[]) + }) + .finally(() => { + setLoading(false) + }) + } + const handleSearch = (value?: string) => { + setQuery(prev => ({ ...prev, name: value })) + } + + // 打开添加服务弹窗 + const handleEdit = (data?: ToolItem) => { + customToolModalRef.current?.handleOpen(data); + }; + + // 删除服务 + const handleDeleteService = (item: ToolItem) => { + modal.confirm({ + title: t('common.confirmDeleteDesc', { name: item.name }), + okText: t('common.delete'), + okType: 'danger', + onOk: () => { + deleteTool(item.id).then(() => { + message.success(t('common.deleteSuccess')); + getData() + }) + } + }) + }; + + return ( +
+ + + + + + + + + + ( + + + // {item.name[0]} + //
+ // } + title={ +
+ {item.name}
+ {/*
xx个工具
*/} +
+ } + extra={getStatusTag(item.status)} + > +
+ {['auth_type', 'tag', 'created_at'].map(key => ( +
+
{t(`tool.${key}`)}
+
+ {key === 'created_at' && item[key] + ? dayjs(item[key]).format('YYYY-MM-DD HH:mm:ss') + : key === 'auth_type' + ? t(`tool.${(item.config_data as any)?.[key]}`) + : (item.config_data as any)?.[key] || '-' + } +
+
+ ))} +
+ +
handleEdit(item)} + >
+
handleDeleteService(item)} + >
+
+
+
+ + + )} + className="rb:h-[calc(100vh-178px)] rb:overflow-y-auto rb:overflow-x-hidden" + /> + + + {/* 添加服务弹窗组件 */} + +
+ ); +}; + +export default Custom; \ No newline at end of file diff --git a/web/src/views/ToolManagement/Inner.tsx b/web/src/views/ToolManagement/Inner.tsx new file mode 100644 index 00000000..8c507a46 --- /dev/null +++ b/web/src/views/ToolManagement/Inner.tsx @@ -0,0 +1,168 @@ +import React, { useState, useRef, useEffect, type ReactNode } from 'react'; +import { + Row, + Col, + Tag, + List, + Space +} from 'antd'; +import { EyeOutlined } from '@ant-design/icons'; +import clsx from 'clsx' +import { useTranslation } from 'react-i18next'; +import dayjs, { type Dayjs } from 'dayjs' + +import type { Query, ToolItem, TimeToolModalRef, JsonToolModalRef, InnerToolModalRef } from './types'; +import SearchInput from '@/components/SearchInput' +import BodyWrapper from '@/components/Empty/BodyWrapper' +import RbCard from '@/components/RbCard/Card' +import TimeToolModal from './components/TimeToolModal' +import JsonToolModal from './components/JsonToolModal' +import InnerToolModal from './components/InnerToolModal' +import { getTools } from '@/api/tools' +import { InnerConfigData } from './constant' + +const Inner: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getStatusTag }) => { + const { t } = useTranslation(); + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + const [query, setQuery] = useState({ name: undefined, tool_type: 'builtin' }); + const [curTime, setCurTime] = useState(dayjs()) + const timeToolModalRef = useRef(null) + const jsonToolModalRef = useRef(null) + const innerToolModalRef = useRef(null) + + useEffect(() => { + getData() + const timer = setInterval(() => { + setCurTime(dayjs()) + }, 1000) + return () => { + clearInterval(timer) + } + }, [query.name]) + + const getData = () => { + setLoading(true) + getTools(query) + .then((res) => { + setData(res as ToolItem[]) + }) + .finally(() => { + setLoading(false) + }) + } + const handleSearch = (value?: string) => { + setQuery(prev => ({ ...prev, name: value })) + } + + // 打开添加服务弹窗 + const handleEdit = (data: ToolItem) => { + switch (data.config_data.tool_class) { + case 'DateTimeTool': + timeToolModalRef.current?.handleOpen(data); + break + case 'JsonTool': + jsonToolModalRef.current?.handleOpen(data); + break + default: + innerToolModalRef.current?.handleOpen(data); + break; + } + } + + return ( +
+ + + + + + + ( + + + // {item.name[0]} + //
+ // } + title={item.name} + extra={getStatusTag(item.status)} + bodyClassName='rb:h-[calc(100%-40px)]' + > +
+
+ {t(`tool.${item.config_data.tool_class}_features`)}
+ + {InnerConfigData[item.config_data.tool_class].features.map(vo => { t(`tool.${vo}`) }) } + + + {item.config_data.tool_class === 'DateTimeTool' + ?
+ {t('tool.currentTime')} +
+ {curTime.format('YYYY-MM-DD HH:mm:ss')} +
+ {t('tool.timestamp')} +
+ {curTime.unix()} +
+
+ :item.config_data.tool_class === 'JsonTool' + ?
+ {t('tool.jsonEg')} +
+ {InnerConfigData[item.config_data.tool_class].eg} +
+
+ :
+ {t('tool.configStatus')} +
+ {t(`tool.${item.status}_desc`)} +
+
+ } +
+ +
+ {item.config_data.tool_class === 'DateTimeTool' || item.config_data.tool_class === 'JsonTool' ? + handleEdit(item)} /> + :
handleEdit(item)} + >
+ } +
+
+ + + )} + className="rb:h-[calc(100vh-178px)] rb:overflow-y-auto rb:overflow-x-hidden" + /> + + + + + +
+ ); +}; + +export default Inner; \ No newline at end of file diff --git a/web/src/views/ToolManagement/Mcp.tsx b/web/src/views/ToolManagement/Mcp.tsx new file mode 100644 index 00000000..30e2614f --- /dev/null +++ b/web/src/views/ToolManagement/Mcp.tsx @@ -0,0 +1,167 @@ +import React, { useState, useRef, useEffect, type ReactNode } from 'react'; +import { + Button, + Row, + Col, + App, + List, + Space, +} from 'antd'; +import { LinkOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; + +import type { ToolItem, Query, McpServiceModalRef } from './types'; +import McpServiceModal from './components/McpServiceModal'; +import SearchInput from '@/components/SearchInput' +import BodyWrapper from '@/components/Empty/BodyWrapper' +import RbCard from '@/components/RbCard/Card' +import { getTools, deleteTool, testConnection } from '@/api/tools' + +const Mcp: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getStatusTag }) => { + const { t } = useTranslation(); + const { message, modal } = App.useApp() + const [loading, setLoading] = useState(false); + const [data, setData] = useState([]); + const [query, setQuery] = useState({ name: undefined, tool_type: 'mcp' }); + const addServiceModalRef = useRef(null); + + useEffect(() => { + getData() + }, [query.name]) + + const getData = () => { + setLoading(true) + getTools(query) + .then((res) => { + setData(res as ToolItem[]) + }) + .finally(() => { + setLoading(false) + }) + } + const handleSearch = (value?: string) => { + setQuery(prev => ({ ...prev, name: value })) + } + + // 打开添加服务弹窗 + const handleEdit = (data?: ToolItem) => { + addServiceModalRef.current?.handleOpen(data); + }; + + // 测试连接 + const handleTestConnection = (item: ToolItem) => { + if (!item.id) { + return + } + testConnection(item.id) + .then(() => { + message.success(t('tool.testConnectionSuccess')); + getData() + }) + }; + + // 删除服务 + const handleDeleteService = (item: ToolItem) => { + if (!item.id) { + return + } + modal.confirm({ + title: t('common.confirmDeleteDesc', { name: item.name }), + okText: t('common.delete'), + okType: 'danger', + onOk: () => { + deleteTool(item.id as string) + .then(() => { + message.success(t('common.deleteSuccess')); + getData() + }) + } + }) + }; + + return ( +
+ + + + + + + + + + ( + + + // {item.name[0]} + //
+ // } + title={item.name} + extra={getStatusTag(item.status)} + > +
+ {[ + 'server_url', + 'last_health_check', + ].map(key => { + const value = item.config_data?.[key as keyof typeof item.config_data]; + let displayValue: React.ReactNode; + + if (key === 'last_health_check') { + displayValue = value ? new Date(value as number).toLocaleString() : '-'; + } else if (typeof value === 'string' || typeof value === 'number') { + displayValue = value; + } else { + displayValue = '-'; + } + + return ( +
+
{t(`tool.${key}`)}
+ {displayValue} +
+ ); + })} +
+ +
handleEdit(item)} + >
+ +
handleDeleteService(item)} + >
+
+
+
+ + + )} + className="rb:h-[calc(100vh-178px)] rb:overflow-y-auto rb:overflow-x-hidden" + /> + + + {/* 添加服务弹窗组件 */} + + + ); +}; + +export default Mcp; \ No newline at end of file diff --git a/web/src/views/ToolManagement/components/CustomToolModal.tsx b/web/src/views/ToolManagement/components/CustomToolModal.tsx new file mode 100644 index 00000000..cb4f036c --- /dev/null +++ b/web/src/views/ToolManagement/components/CustomToolModal.tsx @@ -0,0 +1,285 @@ +import { forwardRef, useImperativeHandle, useState } from 'react'; +import { Form, Input, Select, Row, Col, App, Button } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { CustomToolItem, CustomToolModalRef, ToolItem } from '../types' +import RbModal from '@/components/RbModal'; +import { parseSchema, addTool, updateTool } from '@/api/tools'; +import Table from '@/components/Table'; +const FormItem = Form.Item; + +interface CustomToolModalProps { + refresh: () => void; +} +interface ParseSchemaData { + title: string; + description: string; + version: string; + base_url: string; + operations: Array<{ + method: string; + path: string; + summary: string; + description: string; + parameters: Record> + request_body: null | string; + responses: Record> + tags: string[] + }> +} +const authTypeList = ['none', 'api_key', 'basic_auth'] +const CustomToolModal = forwardRef(({ + refresh +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [editVo, setEditVo] = useState(null) + const values = Form.useWatch([], form) + const [parseSchemaData, setParseSchemaData] = useState({} as ParseSchemaData) + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false); + setEditVo(null) + setParseSchemaData({} as ParseSchemaData) + }; + + const handleOpen = (data?: ToolItem) => { + if (data?.id) { + const { config_data, ...rest } = data + form.setFieldsValue({ + ...rest, + config: {...config_data} + }) + setEditVo(data) + formatSchema(config_data.schema_content) + } else { + form.resetFields(); + } + setVisible(true); + }; + + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + form + .validateFields() + .then(() => { + setLoading(true); + // 创建新服务对象 + const { config, ...reset } = values + const request = editVo?.id ? updateTool(editVo?.id, { + ...editVo, + ...reset, + config: { + ...editVo.config_data, + ...config + } + }) : addTool({ + ...values, + tool_type: 'custom' + }) + request.then(() => { + message.success(t('tool.addServiceSuccess')); + handleClose(); + refresh() + }) + .finally(() => { + setLoading(false); + }) + }) + .catch((err) => { + console.log('表单验证失败:', err); + setLoading(false); + }); + }; + const formatSchema = (value: string) => { + setParseSchemaData({} as ParseSchemaData) + try { + const json = JSON.parse(value) + parseSchema({ schema_content: json }) + .then(res => { + const response = res as { data: ParseSchemaData } + setParseSchemaData(response.data) + }) + } catch (error) { + console.log('error', error) + } + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + + + + + + {/* 名称和图标 */} + {/* + + + + + + + + + + + */} + + formatSchema(e.target.value)} /> + + + ( + {summary ?? parseSchemaData.title} + ) + }, + { + title: t('tool.desc'), + dataIndex: 'description', + key: 'description', + }, + { + title: t('tool.method'), + dataIndex: 'method', + key: 'method', + }, + { + title: t('tool.path'), + dataIndex: 'path', + key: 'path', + }, + ]} + initialData={parseSchemaData.operations || []} + emptySize={88} + /> + + + <> + {/* 认证方式 */} + + + + + + + } + + {/* API Key: 认证方式 = bearer_token 展示 */} + {values?.config?.auth_type === 'bearer_token' && + + + + } + + {/* API Key: 认证方式 = basic_auth 展示 */} + {values?.config?.auth_type === 'basic_auth' && + <> + + + + + + + + } + + + + : config[key].type === 'number' + ? + : config[key].type === 'checkbox' + ? {t(`tool.${key}`)} + : config[key].type === 'select' && config[key].options + ? + + + + + {/* 名称和图标 */} + {/* + + + + + + + + + + + */} + + {/* 描述 */} + + + + + + {/* 认证、请求头、配置 */} + + {/* 认证模块 */} + <> + {/* 认证方式 */} + + + } + + {/* API Key: 认证方式 = bearer_token 展示 */} + {values?.config?.connection_config?.auth_type === 'bearer_token' && + + } + + {/* API Key: 认证方式 = basic_auth 展示 */} + {values?.config?.connection_config?.auth_type === 'basic_auth' && + <> + + + + } + + {/* 请求头模块 */} +
+
+
{t('tool.requestHeader')}
+ +
+
{t('tool.requestHeaderDesc')}
+ + {requestHeaderList.length === 0 + ? + : +
( + + + + + ), + }, + ]} + initialData={requestHeaderList} + emptySize={88} + /> + } + + {/* 配置模块 */} + <> + + + + + + + ); +}); + +export default McpServiceModal; diff --git a/web/src/views/ToolManagement/components/RequestHeaderModal.tsx b/web/src/views/ToolManagement/components/RequestHeaderModal.tsx new file mode 100644 index 00000000..5e20120d --- /dev/null +++ b/web/src/views/ToolManagement/components/RequestHeaderModal.tsx @@ -0,0 +1,102 @@ +import { forwardRef, useImperativeHandle, useState, type SetStateAction, type Dispatch } from 'react'; +import { Form, Input } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { RequestHeader, RequestHeaderModalRef } from './McpServiceModal' +import RbModal from '@/components/RbModal' + +const FormItem = Form.Item; + +interface RequestHeaderModalProps { + refreshTable: Dispatch>; +} + +const RequestHeaderModal = forwardRef(({ + refreshTable +}, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false) + const [editIndex, setEditIndex] = useState(-1) + + const values = Form.useWatch([], form); + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false) + }; + + const handleOpen = (index?: number, data?: RequestHeader) => { + if (data) { + setEditIndex(index) + form.setFieldsValue(data) + } else { + form.resetFields(); + } + setVisible(true); + }; + // 封装保存方法,添加提交逻辑 + const handleSave = () => { + form + .validateFields() + .then(() => { + if (typeof editIndex === 'number' && editIndex > -1) { + refreshTable(prev => { + const newList = [...prev] + newList[editIndex] = values + return newList + }) + } else { + refreshTable(prev => [...prev, values]) + } + handleClose() + }) + .catch((err) => { + console.log('err', err) + }); + } + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + return ( + +
+ {/* 请求头名称 */} + + + + {/* 请求头值 */} + + + + +
+ ); +}); + +export default RequestHeaderModal; \ No newline at end of file diff --git a/web/src/views/ToolManagement/components/TimeToolModal.tsx b/web/src/views/ToolManagement/components/TimeToolModal.tsx new file mode 100644 index 00000000..af64a222 --- /dev/null +++ b/web/src/views/ToolManagement/components/TimeToolModal.tsx @@ -0,0 +1,205 @@ +import { forwardRef, useImperativeHandle, useState, useEffect } from 'react'; +import { Form, Input, Select, Row, Col, Button, Tabs } from 'antd'; +import { useTranslation } from 'react-i18next'; + +import type { ToolItem, TimeToolModalRef } from '../types' +import RbModal from '@/components/RbModal'; +import { execute } from '@/api/tools'; +const FormItem = Form.Item; + +const tabKeys = ['currentTime', 'timestampConversion', 'timeFormat'] +const formatList = [ + { label: '%Y-%m-%d %H:%M:%S', value: '%Y-%m-%d %H:%M:%S' }, + { label: '%Y%m%d_%H%M%S', value: '%Y%m%d_%H%M%S' }, + { label: '%Y年%m月%d日 %H:%M', value: '%Y年%m月%d日 %H:%M' }, + { label: '%Y-%m-%d %H:%M:%S.%f', value: '%Y-%m-%d %H:%M:%S.%f' }, + { label: '%d/%m/%Y', value: '%d/%m/%Y' }, + { label: '%m/%d/%Y', value: '%m/%d/%Y' }, +] +interface CurrentTimeObj { + datetime: string; + iso_format: string; + timestamp: string; + timestamp_ms: string; +} +const TimeToolModal = forwardRef((_props, ref) => { + const { t } = useTranslation(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm<{ timestamp: string; formatType: string; }>(); + const [data, setData] = useState({} as ToolItem) + const [timeFormat, setTimeFormat] = useState(undefined) + const [activeTab, setActiveTab] = useState('currentTime'); + const values = Form.useWatch([], form) + const [currentTime, setCurrentTime] = useState({} as CurrentTimeObj) + const [timestampFormat, setTimestampFormat] = useState(null) + + const formatTabItems = () => { + return tabKeys.map(key => ({ + key, + label: t(`tool.${key}`), + })) + } + const handleChangeTab = (key: string) => { + setActiveTab(key); + setTimestampFormat(null) + setTimeFormat(undefined) + } + + // 封装取消方法,添加关闭弹窗逻辑 + const handleClose = () => { + setVisible(false); + form.resetFields(); + setData({} as ToolItem) + setActiveTab('currentTime') + }; + + const handleOpen = (vo: ToolItem) => { + setData(vo) + setVisible(true); + getCurrentTime(vo) + }; + + const getCurrentTime = (vo: ToolItem) => { + if (!vo.id) return + execute({ + tool_id: vo.id, + parameters: { + operation: 'now', + output_format: '%Y-%m-%d %H:%M:%S', + } + }).then(res => { + const response = res as { data: CurrentTimeObj} + setCurrentTime(response.data) + }) + } + + const handleFormat = () => { + const timestamp = form.getFieldValue('timestamp') + if (!timestamp || !data.id) return + execute({ + "tool_id": data.id, + "parameters": { + "operation": "now", + "input_value": timestamp, + } + }) + .then(res => { + const response = res as { data: CurrentTimeObj } + setTimestampFormat(response.data.datetime) + }) + } + const handleChangeFormatType = () => { + if (!data.id) return + execute({ + tool_id: data.id, + parameters: { + operation: 'now', + output_format: values.formatType, + to_timezone: 'UTC' + } + }).then(res => { + const response = res as { data: CurrentTimeObj } + console.log('timeFormat', response.data.datetime) + setTimeFormat(response.data.datetime) + }) + } + + useEffect(() => { + if (values?.formatType) { + handleChangeFormatType() + } + }, [values?.formatType]) + + // 暴露给父组件的方法 + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + + return ( + +
+ {/* 当前时间、时间戳转换、时间格式化 */} + + + {/* 当前时间 */} + {activeTab === 'currentTime' && + <> + + + + + + + + + + + + + + } + {/* 时间戳转换 */} + {activeTab === 'timestampConversion' && + <> + + +
+ + + + + + + + + + {timestampFormat && +
+ {t('tool.conversionResult')} +
+ {timestampFormat} +
+
+ } + + } + {/* 时间格式化 */} + {activeTab === 'timeFormat' && + <> + +