feat(web): add tool management
This commit is contained in:
31
web/src/api/tools.ts
Normal file
31
web/src/api/tools.ts
Normal file
@@ -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<string, any>) => {
|
||||
return request.post(`/tools/parse_schema`, data)
|
||||
|
||||
}
|
||||
@@ -1348,13 +1348,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',
|
||||
@@ -1369,12 +1375,13 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
requestHeader: 'Request Headers',
|
||||
config: 'Configuration',
|
||||
authType: 'Authentication Type',
|
||||
noAuth: 'No Authentication',
|
||||
apiKey: 'API Key',
|
||||
basicAuth: 'Basic Auth',
|
||||
bearerToken: 'Bearer Token',
|
||||
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',
|
||||
@@ -1448,7 +1455,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',
|
||||
@@ -1505,7 +1511,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',
|
||||
|
||||
@@ -1447,12 +1447,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',
|
||||
@@ -1468,12 +1475,13 @@ export const zh = {
|
||||
requestHeader: '请求头',
|
||||
config: '配置',
|
||||
authType: '认证方式',
|
||||
noAuth: '无需认证',
|
||||
apiKey: 'API Key',
|
||||
basicAuth: 'Basic Auth',
|
||||
bearerToken: 'Bearer Token',
|
||||
none: '无需认证',
|
||||
api_key: 'API Key',
|
||||
basic_auth: 'Basic Auth',
|
||||
bearer_token: 'Bearer Token',
|
||||
username: '用户名',
|
||||
password: '密码',
|
||||
key_name: 'Key Name',
|
||||
requestHeaderDesc: '发送到 MCP 服务器的额外 HTTP 请求头',
|
||||
addRequestHeader: '添加请求头',
|
||||
editRequestHeader: '编辑请求头',
|
||||
@@ -1547,7 +1555,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',
|
||||
@@ -1604,7 +1611,9 @@ export const zh = {
|
||||
desc: '描述',
|
||||
method: '方法',
|
||||
path: '路径',
|
||||
viewDetail: '查看详情'
|
||||
viewDetail: '查看详情',
|
||||
textLink: '测试连接',
|
||||
noResult: '处理结果将显示在这里'
|
||||
},
|
||||
workflow: {
|
||||
coreNode: '核心节点',
|
||||
|
||||
@@ -61,6 +61,7 @@ const componentMap: Record<string, LazyExoticComponent<ComponentType<object>>> =
|
||||
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')),
|
||||
|
||||
@@ -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" },
|
||||
|
||||
@@ -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,
|
||||
|
||||
141
web/src/views/ToolManagement/Custom.tsx
Normal file
141
web/src/views/ToolManagement/Custom.tsx
Normal file
@@ -0,0 +1,141 @@
|
||||
import React, { useState, useRef, useEffect, type ReactNode } from 'react';
|
||||
import {
|
||||
Button,
|
||||
Row,
|
||||
Col,
|
||||
App,
|
||||
List,
|
||||
Space
|
||||
} from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import dayjs from 'dayjs';
|
||||
|
||||
import type { ToolItem, Query, CustomToolModalRef } from './types';
|
||||
import CustomToolModal from './components/CustomToolModal';
|
||||
import SearchInput from '@/components/SearchInput'
|
||||
import BodyWrapper from '@/components/Empty/BodyWrapper'
|
||||
import RbCard from '@/components/RbCard'
|
||||
import { getTools, deleteTool } from '@/api/tools'
|
||||
|
||||
const Custom: React.FC<{ getStatusTag: (status: string) => ReactNode }> = ({ getStatusTag }) => {
|
||||
const { t } = useTranslation();
|
||||
const { message, modal } = App.useApp()
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [data, setData] = useState<ToolItem[]>([]);
|
||||
const [query, setQuery] = useState<Query>({ name: undefined, tool_type: 'custom' });
|
||||
const customToolModalRef = useRef<CustomToolModalRef>(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 (
|
||||
<div>
|
||||
<Row gutter={16} className='rb:mb-4 rb:w-full'>
|
||||
<Col span={8}>
|
||||
<SearchInput
|
||||
placeholder={t('tool.customSearchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={16} className="rb:text-right">
|
||||
<Button type="primary" onClick={() => {handleEdit()}}>{t('tool.addCustom')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<BodyWrapper loading={loading} empty={data.length === 0}>
|
||||
<List
|
||||
grid={{ gutter: 16, column: 2 }}
|
||||
dataSource={data}
|
||||
renderItem={(item) => (
|
||||
<List.Item key={item.id}>
|
||||
<RbCard
|
||||
// avatar={
|
||||
// <div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
// {item.name[0]}
|
||||
// </div>
|
||||
// }
|
||||
title={
|
||||
<div>
|
||||
{item.name}<br/>
|
||||
{/* <div className="rb:mt-1 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167]">xx个工具</div> */}
|
||||
</div>
|
||||
}
|
||||
extra={getStatusTag(item.status)}
|
||||
>
|
||||
<div>
|
||||
{['auth_type', 'tag', 'created_at'].map(key => (
|
||||
<div
|
||||
key={key}
|
||||
className="rb:flex rb:gap-4 rb:justify-start rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
|
||||
>
|
||||
<div className="rb:whitespace-nowrap rb:w-27.5">{t(`tool.${key}`)}</div>
|
||||
<div className='rb:flex-1 rb:text-ellipsis rb:overflow-hidden rb:whitespace-nowrap rb:flex-inline rb:text-left rb:py-px rb:rounded rb:font-medium'>
|
||||
{key === 'created_at' && item[key] ? dayjs(item[key]).format('YYYY-MM-DD HH:mm:ss') : (item.config_data as any)?.[key] || '-'}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
|
||||
<Space size={16}>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEdit(item)}
|
||||
></div>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDeleteService(item)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</RbCard>
|
||||
</List.Item>
|
||||
)}
|
||||
className="rb:h-[calc(100vh-178px)] rb:overflow-y-auto rb:overflow-x-hidden"
|
||||
/>
|
||||
</BodyWrapper>
|
||||
|
||||
{/* 添加服务弹窗组件 */}
|
||||
<CustomToolModal
|
||||
ref={customToolModalRef}
|
||||
refresh={getData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Custom;
|
||||
168
web/src/views/ToolManagement/Inner.tsx
Normal file
168
web/src/views/ToolManagement/Inner.tsx
Normal file
@@ -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<ToolItem[]>([]);
|
||||
const [query, setQuery] = useState<Query>({ name: undefined, tool_type: 'builtin' });
|
||||
const [curTime, setCurTime] = useState<Dayjs>(dayjs())
|
||||
const timeToolModalRef = useRef<TimeToolModalRef>(null)
|
||||
const jsonToolModalRef = useRef<JsonToolModalRef>(null)
|
||||
const innerToolModalRef = useRef<InnerToolModalRef>(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 (
|
||||
<div>
|
||||
<Row gutter={16} className='rb:mb-4 rb:w-full'>
|
||||
<Col span={8}>
|
||||
<SearchInput
|
||||
placeholder={t('tool.innerSearchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Col>
|
||||
</Row>
|
||||
<BodyWrapper loading={loading} empty={data.length === 0}>
|
||||
<List
|
||||
grid={{ gutter: 16, column: 2 }}
|
||||
dataSource={data}
|
||||
renderItem={(item) => (
|
||||
<List.Item key={item.id}>
|
||||
<RbCard
|
||||
className={clsx({
|
||||
'rb:h-82.5!': item.config_data.tool_class === 'DateTimeTool' || item.config_data.tool_class === 'JsonTool'
|
||||
})}
|
||||
// avatar={
|
||||
// <div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
// {item.name[0]}
|
||||
// </div>
|
||||
// }
|
||||
title={item.name}
|
||||
extra={getStatusTag(item.status)}
|
||||
bodyClassName='rb:h-[calc(100%-40px)]'
|
||||
>
|
||||
<div className="rb:h-full rb:flex rb:flex-col rb:justify-between">
|
||||
<div className="rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167]">
|
||||
{t(`tool.${item.config_data.tool_class}_features`)} <br />
|
||||
<Space size={4} className="rb:mt-2">
|
||||
{InnerConfigData[item.config_data.tool_class].features.map(vo => <Tag key={vo} color="default">{ t(`tool.${vo}`) }</Tag>) }
|
||||
</Space>
|
||||
|
||||
{item.config_data.tool_class === 'DateTimeTool'
|
||||
? <div className="rb:mt-3 rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
{t('tool.currentTime')}
|
||||
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:my-2">
|
||||
{curTime.format('YYYY-MM-DD HH:mm:ss')}
|
||||
</div>
|
||||
{t('tool.timestamp')}
|
||||
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:mt-2">
|
||||
{curTime.unix()}
|
||||
</div>
|
||||
</div>
|
||||
:item.config_data.tool_class === 'JsonTool'
|
||||
? <div className="rb:mt-3 rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
{t('tool.jsonEg')}
|
||||
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:my-2">
|
||||
{InnerConfigData[item.config_data.tool_class].eg}
|
||||
</div>
|
||||
</div>
|
||||
: <div className="rb:mt-3 rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
{t('tool.configStatus')}
|
||||
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:my-2">
|
||||
{t(`tool.${item.status}_desc`)}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</div>
|
||||
|
||||
<div className="rb:mt-4 rb:flex rb:items-center rb:justify-end">
|
||||
{item.config_data.tool_class === 'DateTimeTool' || item.config_data.tool_class === 'JsonTool' ?
|
||||
<EyeOutlined className="rb:text-5 rb:text-[#5B6167]! rb:hover:text-[#212332]!" onClick={() => handleEdit(item)} />
|
||||
: <div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEdit(item)}
|
||||
></div>
|
||||
}
|
||||
</div>
|
||||
</div>
|
||||
</RbCard>
|
||||
</List.Item>
|
||||
)}
|
||||
className="rb:h-[calc(100vh-178px)] rb:overflow-y-auto rb:overflow-x-hidden"
|
||||
/>
|
||||
</BodyWrapper>
|
||||
|
||||
<TimeToolModal
|
||||
ref={timeToolModalRef}
|
||||
/>
|
||||
<JsonToolModal
|
||||
ref={jsonToolModalRef}
|
||||
/>
|
||||
<InnerToolModal
|
||||
ref={innerToolModalRef}
|
||||
refreshTable={getData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Inner;
|
||||
167
web/src/views/ToolManagement/Mcp.tsx
Normal file
167
web/src/views/ToolManagement/Mcp.tsx
Normal file
@@ -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<ToolItem[]>([]);
|
||||
const [query, setQuery] = useState<Query>({ name: undefined, tool_type: 'mcp' });
|
||||
const addServiceModalRef = useRef<McpServiceModalRef>(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 (
|
||||
<div>
|
||||
<Row gutter={16} className='rb:mb-4 rb:w-full'>
|
||||
<Col span={8}>
|
||||
<SearchInput
|
||||
placeholder={t('tool.mcpSearchPlaceholder')}
|
||||
onSearch={handleSearch}
|
||||
style={{width: '100%'}}
|
||||
/>
|
||||
</Col>
|
||||
<Col span={16} className="rb:text-right">
|
||||
<Button type="primary" onClick={() => {handleEdit()}}>{t('tool.addService')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
<BodyWrapper loading={loading} empty={data?.length === 0}>
|
||||
<List
|
||||
grid={{ gutter: 16, column: 3 }}
|
||||
dataSource={data}
|
||||
renderItem={(item) => (
|
||||
<List.Item key={item.id}>
|
||||
<RbCard
|
||||
// avatar={
|
||||
// <div className="rb:w-12 rb:h-12 rb:rounded-lg rb:mr-3.25 rb:bg-[#155eef] rb:flex rb:items-center rb:justify-center rb:text-[28px] rb:text-[#ffffff]">
|
||||
// {item.name[0]}
|
||||
// </div>
|
||||
// }
|
||||
title={item.name}
|
||||
extra={getStatusTag(item.status)}
|
||||
>
|
||||
<div>
|
||||
{[
|
||||
'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 (
|
||||
<div
|
||||
key={key}
|
||||
className="rb:flex rb:gap-4 rb:justify-start rb:text-[#5B6167] rb:text-[14px] rb:leading-5 rb:mb-3"
|
||||
>
|
||||
<div className="rb:whitespace-nowrap rb:w-27.5">{t(`tool.${key}`)}</div>
|
||||
{displayValue}
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
<div className="rb:mt-4 rb:text-[12px] rb:leading-4 rb:font-regular rb:text-[#5B6167] rb:flex rb:items-center rb:justify-end">
|
||||
<Space size={16}>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/edit.svg')] rb:hover:bg-[url('@/assets/images/edit_hover.svg')]"
|
||||
onClick={() => handleEdit(item)}
|
||||
></div>
|
||||
<Button type="text" icon={<LinkOutlined />} onClick={() => handleTestConnection(item)}></Button>
|
||||
<div
|
||||
className="rb:w-5 rb:h-5 rb:cursor-pointer rb:bg-cover rb:bg-[url('@/assets/images/delete.svg')] rb:hover:bg-[url('@/assets/images/delete_hover.svg')]"
|
||||
onClick={() => handleDeleteService(item)}
|
||||
></div>
|
||||
</Space>
|
||||
</div>
|
||||
</div>
|
||||
</RbCard>
|
||||
</List.Item>
|
||||
)}
|
||||
className="rb:h-[calc(100vh-178px)] rb:overflow-y-auto rb:overflow-x-hidden"
|
||||
/>
|
||||
</BodyWrapper>
|
||||
|
||||
{/* 添加服务弹窗组件 */}
|
||||
<McpServiceModal
|
||||
ref={addServiceModalRef}
|
||||
refresh={getData}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default Mcp;
|
||||
278
web/src/views/ToolManagement/components/CustomToolModal.tsx
Normal file
278
web/src/views/ToolManagement/components/CustomToolModal.tsx
Normal file
@@ -0,0 +1,278 @@
|
||||
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<string, Record<string, string | null>>
|
||||
request_body: null | string;
|
||||
responses: Record<string, Record<string, string | null>>
|
||||
tags: string[]
|
||||
}>
|
||||
}
|
||||
const authTypeList = ['none', 'api_key', 'basic_auth']
|
||||
const CustomToolModal = forwardRef<CustomToolModalRef, CustomToolModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<CustomToolItem>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editVo, setEditVo] = useState<ToolItem | null>(null)
|
||||
const values = Form.useWatch<CustomToolItem>([], form)
|
||||
const [parseSchemaData, setParseSchemaData] = useState<ParseSchemaData>({} 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 (
|
||||
<RbModal
|
||||
title={editVo?.id ? t('tool.editCustom') : t('tool.addCustom')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.save')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
width={1000}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
config: {
|
||||
auth_type: 'none'
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 名称和图标 */}
|
||||
<Form.Item label={t('tool.nameAndIcon')} required>
|
||||
<Row gutter={8}>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
noStyle
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.pleaseEnter')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button>icon</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name={['config', 'schema_content']}
|
||||
label={t('tool.schema')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input.TextArea rows={10} placeholder={t('tool.schemaPlaceholder')} onBlur={(e) => formatSchema(e.target.value)} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
label={t('tool.availableTools')}
|
||||
>
|
||||
<Table
|
||||
rowKey="summary"
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: t('tool.name'),
|
||||
dataIndex: 'summary',
|
||||
key: 'summary',
|
||||
render: (summary) => (
|
||||
<span>{summary ?? parseSchemaData.title}</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
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}
|
||||
/>
|
||||
</Form.Item>
|
||||
|
||||
<>
|
||||
{/* 认证方式 */}
|
||||
<FormItem
|
||||
name={['config', 'auth_type']}
|
||||
label={t('tool.authType')}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={authTypeList.map(value => ({
|
||||
label: t(`tool.${value}`),
|
||||
value
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
{/* API Key: 认证方式 = api_key 展示 */}
|
||||
{values?.config?.auth_type === 'api_key' && <>
|
||||
<FormItem
|
||||
name={['config', 'auth_config', "key_name"]}
|
||||
label={t('tool.key_name')}
|
||||
>
|
||||
<Input placeholder={t('common.inputPlaceholder', { title: t('tool.key_name') })} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name={['config', 'auth_config', "api_key"]}
|
||||
label={t('tool.api_key')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.inputPlaceholder', { title: t('tool.api_key') })} />
|
||||
</FormItem>
|
||||
</>}
|
||||
|
||||
{/* API Key: 认证方式 = bearer_token 展示 */}
|
||||
{values?.config?.auth_type === 'bearer_token' &&
|
||||
<FormItem
|
||||
name={['config', 'auth_config', "token"]}
|
||||
label={t('tool.bearer_token')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.inputPlaceholder', { title: t('tool.bearer_token') })} />
|
||||
</FormItem>
|
||||
}
|
||||
|
||||
{/* API Key: 认证方式 = basic_auth 展示 */}
|
||||
{values?.config?.auth_type === 'basic_auth' &&
|
||||
<>
|
||||
<FormItem
|
||||
name={['config', 'auth_config', "username"]}
|
||||
label={t('tool.username')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.inputPlaceholder', { title: t('tool.username') })} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name={['config', 'auth_config', "password"]}
|
||||
label={t('tool.password')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.inputPlaceholder', { title: t('tool.password') })} />
|
||||
</FormItem>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
<FormItem
|
||||
name="tags"
|
||||
label={t('tool.tag')}
|
||||
extra={t('tool.tagDesc')}
|
||||
>
|
||||
<Select
|
||||
mode="tags"
|
||||
style={{ width: '100%' }}
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
/>
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default CustomToolModal;
|
||||
155
web/src/views/ToolManagement/components/InnerToolModal.tsx
Normal file
155
web/src/views/ToolManagement/components/InnerToolModal.tsx
Normal file
@@ -0,0 +1,155 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Select, Checkbox, InputNumber, Button, App } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { InnerToolModalRef, ToolItem, InnerConfigItem, InnerToolItem } from '../types'
|
||||
import RbModal from '@/components/RbModal'
|
||||
import { InnerConfigData } from '../constant'
|
||||
import RbAlert from '@/components/RbAlert';
|
||||
import { updateTool, testConnection } from '@/api/tools'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface InnerToolModalProps {
|
||||
refreshTable: () => void;
|
||||
}
|
||||
|
||||
const InnerToolModal = forwardRef<InnerToolModalRef, InnerToolModalProps>(({
|
||||
refreshTable
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<InnerToolItem>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editVo, setEditVo] = useState<ToolItem>({} as ToolItem)
|
||||
const [config, setConfig] = useState<InnerConfigItem['config']>({});
|
||||
const search_type = Form.useWatch(['config', 'parameters', 'search_type'], form)
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false)
|
||||
setConfig({})
|
||||
};
|
||||
|
||||
const handleOpen = (data: ToolItem) => {
|
||||
setEditVo(data)
|
||||
const { config_data } = data
|
||||
form.setFieldsValue({
|
||||
config: {
|
||||
...config_data,
|
||||
parameters: {
|
||||
search_type: 'web',
|
||||
...(config_data as any).parameters
|
||||
},
|
||||
}
|
||||
})
|
||||
setConfig(InnerConfigData[config_data.tool_class].config)
|
||||
setVisible(true)
|
||||
};
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
updateTool(editVo.id, {
|
||||
config: {
|
||||
...editVo.config_data,
|
||||
...values.config,
|
||||
}
|
||||
} as any)
|
||||
.then(() => {
|
||||
handleClose()
|
||||
message.success(t('common.saveSuccess'))
|
||||
refreshTable()
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('err', err)
|
||||
});
|
||||
}
|
||||
const handleTestConnection = () => {
|
||||
testConnection(editVo.id)
|
||||
.then(() => {
|
||||
message.success(t('tool.testConnectionSuccess'));
|
||||
})
|
||||
};
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={`${editVo.name} ${t('tool.config')}`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
confirmLoading={loading}
|
||||
footer={[
|
||||
<Button onClick={handleClose}>{t('common.cancel')}</Button>,
|
||||
<Button onClick={handleTestConnection}>{t('tool.textLink')}</Button>,
|
||||
<Button type="primary" loading={loading} onClick={handleSave}>{t('common.save')}</Button>,
|
||||
]}
|
||||
>
|
||||
{editVo?.config_data?.tool_class && config && <>
|
||||
<RbAlert className="rb:mb-3">
|
||||
<div>
|
||||
<div className="rb:text-[14px] rb:font-medium">{t('tool.configDesc')}</div>
|
||||
<div className="rb:mt-2">{t(`tool.${editVo?.config_data?.tool_class}_config_desc`)}</div>
|
||||
<div className="rb:font-medium">{t('tool.link')}: <Button size="small" type="link">{InnerConfigData[editVo?.config_data?.tool_class].link}</Button></div>
|
||||
</div>
|
||||
</RbAlert>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
{Object.keys(config).map((key) => {
|
||||
const range = key === 'pagesize' && search_type ? config[key].range?.[search_type] ?? [] : [ config[key].min, config[key].max ]
|
||||
return (
|
||||
<FormItem
|
||||
key={key}
|
||||
label={config[key].type === 'checkbox' ? null : t(`tool.${key}`)}
|
||||
name={config[key].name}
|
||||
extra={config[key].desc ? t(`tool.${config[key].desc}`, { count1: range[0], count2: range[1] }) : null}
|
||||
valuePropName={config[key].type === 'checkbox' ? 'checked' : 'value'}
|
||||
rules={config[key].rules ? config[key].rules.map(vo => ({
|
||||
...vo,
|
||||
message: t(vo.message)
|
||||
})) : []}
|
||||
>
|
||||
{config[key].type === 'input'
|
||||
? <Input placeholder={t('common.inputPlaceholder', { title: t(`tool.${key}`) })} />
|
||||
: config[key].type === 'number'
|
||||
? <InputNumber
|
||||
placeholder={t('common.pleaseEnter')}
|
||||
min={range[0]}
|
||||
max={range[1]}
|
||||
step={config[key].step}
|
||||
className="rb:w-full!"
|
||||
/>
|
||||
: config[key].type === 'checkbox'
|
||||
? <Checkbox>{t(`tool.${key}`)}</Checkbox>
|
||||
: config[key].type === 'select' && config[key].options
|
||||
? <Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={config[key].options.map(vo => ({
|
||||
...vo,
|
||||
label: t(`tool.${vo.label}`)
|
||||
}))}
|
||||
/>
|
||||
: null
|
||||
}
|
||||
</FormItem>
|
||||
)
|
||||
})}
|
||||
</Form>
|
||||
</>}
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default InnerToolModal;
|
||||
161
web/src/views/ToolManagement/components/JsonToolModal.tsx
Normal file
161
web/src/views/ToolManagement/components/JsonToolModal.tsx
Normal file
@@ -0,0 +1,161 @@
|
||||
import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Button, Space, Tree } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import type { TreeDataNode } from 'antd';
|
||||
|
||||
import type { ToolItem, JsonToolModalRef, ExecuteData } from '../types'
|
||||
import RbModal from '@/components/RbModal';
|
||||
import FormItem from 'antd/es/form/FormItem';
|
||||
import CodeBlock from '@/components/Markdown/CodeBlock';
|
||||
import { execute } from '@/api/tools';
|
||||
|
||||
const JsonToolModal = forwardRef<JsonToolModalRef>((_props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{ json: string; }>();
|
||||
const [data, setData] = useState<ToolItem>({} as ToolItem)
|
||||
const [formatValue, setFormatValue] = useState<string | Record<string, any> | null>(null)
|
||||
|
||||
// 转换数据结构为Tree组件需要的格式
|
||||
const convertToTreeData = (data: Record<string, any>, parentKey = ''): TreeDataNode[] => {
|
||||
if (data.children) {
|
||||
return convertToTreeData(data.children, parentKey);
|
||||
}
|
||||
|
||||
return Object.entries(data).map(([key, item]) => {
|
||||
const nodeKey = parentKey ? `${parentKey}-${key}` : key;
|
||||
const title = `${key}: ${item.value || ''}`;
|
||||
|
||||
const node: TreeDataNode = {
|
||||
key: nodeKey,
|
||||
title,
|
||||
};
|
||||
|
||||
if (item.children) {
|
||||
node.children = convertToTreeData(item.children, nodeKey);
|
||||
}
|
||||
|
||||
return node;
|
||||
});
|
||||
};
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setData({} as ToolItem)
|
||||
};
|
||||
|
||||
const handleOpen = (data: ToolItem) => {
|
||||
setData(data)
|
||||
setVisible(true);
|
||||
};
|
||||
const handleParse = async () => {
|
||||
try {
|
||||
const text = await navigator.clipboard.readText();
|
||||
form.setFieldValue('json', text);
|
||||
} catch (err) {
|
||||
console.error('Failed to read clipboard:', err);
|
||||
}
|
||||
}
|
||||
const handleOperate = (type: string) => {
|
||||
const json = form.getFieldValue('json')
|
||||
if (!json || !data.id) return
|
||||
let params: ExecuteData = {
|
||||
tool_id: data.id,
|
||||
parameters: {
|
||||
operation: type,
|
||||
input_data: json
|
||||
}
|
||||
}
|
||||
if (type === 'format') {
|
||||
params = {
|
||||
...params,
|
||||
parameters: {
|
||||
...params.parameters,
|
||||
indent: 2,
|
||||
ensure_ascii: false,
|
||||
sort_keys: false
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
execute(params)
|
||||
.then(res => {
|
||||
const { data } = res as {data: {
|
||||
formatted_json: string;
|
||||
minified_json: string;
|
||||
is_valid: boolean;
|
||||
converted_json: string;
|
||||
error: string;
|
||||
structure: Record<string, string | number>
|
||||
}}
|
||||
switch (type) {
|
||||
case 'format':
|
||||
setFormatValue(data.formatted_json);
|
||||
break
|
||||
case 'minify':
|
||||
setFormatValue(data.minified_json)
|
||||
break
|
||||
case 'validate':
|
||||
setFormatValue(data.structure)
|
||||
break
|
||||
case 'convert':
|
||||
setFormatValue(data.converted_json)
|
||||
break
|
||||
}
|
||||
})
|
||||
}
|
||||
const clear = () => {
|
||||
form.setFieldValue('json', undefined)
|
||||
setFormatValue(null)
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
return (
|
||||
<RbModal
|
||||
title={data.name}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
<FormItem
|
||||
name="json"
|
||||
label={<Space size={8}>
|
||||
{t('tool.enterJson')}
|
||||
<Button onClick={clear}>{t('tool.clear')}</Button>
|
||||
<Button onClick={handleParse}>{t('tool.parse')}</Button>
|
||||
</Space>}
|
||||
>
|
||||
<Input.TextArea rows={10} placeholder={t('tool.jsonPlaceholder')} />
|
||||
</FormItem>
|
||||
|
||||
<Space size={8} className="rb:mb-3">
|
||||
<Button onClick={() => handleOperate('format')}>{t('tool.format')}</Button>
|
||||
<Button onClick={() => handleOperate('minify')}>{t('tool.minify')}</Button>
|
||||
<Button onClick={() => handleOperate('validate')}>{t('tool.validate')}</Button>
|
||||
</Space>
|
||||
<FormItem
|
||||
label={t('tool.outputResult')}
|
||||
>
|
||||
{typeof formatValue === "string" && formatValue
|
||||
? <CodeBlock value={formatValue} />
|
||||
: formatValue && typeof formatValue === "object"
|
||||
? <div className="rb:bg-[#F0F3F8] rb:p-[16px_20px_16px_24px] rb:rounded-lg"><Tree showLine treeData={convertToTreeData(formatValue)} /></div>
|
||||
: <div className="rb:bg-[#F0F3F8] rb:p-[16px_20px_16px_24px] rb:rounded-lg rb:text-[#A8A9AA]">{t('tool.noResult')}</div>
|
||||
}
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default JsonToolModal;
|
||||
355
web/src/views/ToolManagement/components/McpServiceModal.tsx
Normal file
355
web/src/views/ToolManagement/components/McpServiceModal.tsx
Normal file
@@ -0,0 +1,355 @@
|
||||
import { forwardRef, useImperativeHandle, useState, useRef } from 'react';
|
||||
import { Form, Input, Select, App, Button, Tabs, Space } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import type { MCPToolItem, ToolItem } from '../types'
|
||||
import RbModal from '@/components/RbModal';
|
||||
import Empty from '@/components/Empty';
|
||||
import RequestHeaderModal from './RequestHeaderModal';
|
||||
import Table from '@/components/Table';
|
||||
import { addTool, updateTool, testConnection } from '@/api/tools'
|
||||
import type { McpServiceModalRef } from '../types'
|
||||
|
||||
const FormItem = Form.Item;
|
||||
|
||||
interface McpServiceModalProps {
|
||||
refresh: () => void;
|
||||
}
|
||||
|
||||
export interface RequestHeader {
|
||||
key: string;
|
||||
value: string;
|
||||
[key: string]: string | undefined;
|
||||
}
|
||||
export interface RequestHeaderModalRef {
|
||||
handleOpen: (index?: number, data?: RequestHeader) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
const authTypeList = ['none', 'api_key', 'basic_auth']
|
||||
const tabKeys = ['auth', 'requestHeader', 'config']
|
||||
const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
|
||||
refresh
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<MCPToolItem>();
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [editVo, setEditVo] = useState<ToolItem | null>(null)
|
||||
const [activeTab, setActiveTab] = useState('auth');
|
||||
const values = Form.useWatch<MCPToolItem>([], form)
|
||||
const requestHeaderModalRef = useRef<RequestHeaderModalRef>(null)
|
||||
const [requestHeaderList, setRequestHeaderList] = useState<RequestHeader[]>([])
|
||||
|
||||
const formatTabItems = () => {
|
||||
return tabKeys.map(key => ({
|
||||
key,
|
||||
label: t(`tool.${key}`),
|
||||
}))
|
||||
}
|
||||
const handleChangeTab = (key: string) => {
|
||||
setActiveTab(key);
|
||||
}
|
||||
|
||||
// 封装取消方法,添加关闭弹窗逻辑
|
||||
const handleClose = () => {
|
||||
setVisible(false);
|
||||
form.resetFields();
|
||||
setLoading(false);
|
||||
setEditVo(null)
|
||||
setActiveTab('auth')
|
||||
setRequestHeaderList([])
|
||||
};
|
||||
|
||||
const handleOpen = (data?: ToolItem) => {
|
||||
if (data?.id) {
|
||||
const { config_data, name, description, icon } = data
|
||||
form.setFieldsValue({
|
||||
name, description, icon,
|
||||
config: { ...config_data }
|
||||
})
|
||||
|
||||
if (config_data.connection_config.headers) {
|
||||
console.log(Object.keys(config_data.connection_config.headers).map(key => ({
|
||||
key,
|
||||
value: config_data.connection_config.headers[key]
|
||||
})))
|
||||
setRequestHeaderList(Object.keys(config_data.connection_config.headers).map(key => ({
|
||||
key,
|
||||
value: config_data.connection_config.headers[key]
|
||||
})))
|
||||
}
|
||||
setEditVo(data)
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
setVisible(true);
|
||||
};
|
||||
|
||||
// 封装保存方法,添加提交逻辑
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then(() => {
|
||||
setLoading(true);
|
||||
// 创建新服务对象
|
||||
const { config, ...rest } = values
|
||||
|
||||
const newService: MCPToolItem = {
|
||||
...rest,
|
||||
tool_type: 'mcp',
|
||||
config: {
|
||||
...config,
|
||||
connection_config: {
|
||||
...config.connection_config,
|
||||
headers: requestHeaderList.reduce((acc: Record<string, string>, cur) => {
|
||||
acc[cur.key] = cur.value
|
||||
return acc
|
||||
}, {})
|
||||
}
|
||||
}
|
||||
}
|
||||
const request = editVo?.id ? updateTool(editVo.id, newService) : addTool(newService)
|
||||
request.then((res: any) => {
|
||||
message.success(t('common.saveSuccess'));
|
||||
testConnection(res.tool_id || editVo?.id)
|
||||
.then(() => {
|
||||
handleClose();
|
||||
refresh()
|
||||
})
|
||||
.finally(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
})
|
||||
.catch(() => {
|
||||
setLoading(false);
|
||||
})
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('表单验证失败:', err);
|
||||
setLoading(false);
|
||||
});
|
||||
};
|
||||
const handleEditRequestHeader = (index?: number, data?: RequestHeader) => {
|
||||
requestHeaderModalRef.current?.handleOpen(index, data)
|
||||
}
|
||||
const handleDeleteRequestHeader = (index: number) => {
|
||||
const list = requestHeaderList.filter((_item, idx) => idx !== index)
|
||||
setRequestHeaderList([...list])
|
||||
}
|
||||
|
||||
// 暴露给父组件的方法
|
||||
useImperativeHandle(ref, () => ({
|
||||
handleOpen,
|
||||
handleClose
|
||||
}));
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={editVo?.id ? t('tool.editService') : `${t('tool.addService')} (HTTP)`}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('tool.saveAndTest')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
config: {
|
||||
connection_config: {
|
||||
auth_type: 'none',
|
||||
timeout: 30,
|
||||
},
|
||||
}
|
||||
}}
|
||||
>
|
||||
{/* 服务端点 URL */}
|
||||
<FormItem
|
||||
name={['config', "server_url"]}
|
||||
label={t('tool.serviceEndpoint')}
|
||||
extra={t('tool.serviceEndpointExtra')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('tool.serviceEndpointPlaceholder')} />
|
||||
</FormItem>
|
||||
<Form.Item
|
||||
name="name"
|
||||
label={t('tool.name')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('tool.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
{/* 名称和图标 */}
|
||||
{/* <Form.Item label={t('tool.nameAndIcon')} required>
|
||||
<Row gutter={8}>
|
||||
<Col span={16}>
|
||||
<Form.Item
|
||||
name="name"
|
||||
noStyle
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('tool.namePlaceholder')} />
|
||||
</Form.Item>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button>icon</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</Form.Item> */}
|
||||
|
||||
{/* 描述 */}
|
||||
<FormItem
|
||||
name="description"
|
||||
label={t('tool.description')}
|
||||
>
|
||||
<Input.TextArea rows={3} placeholder={t('common.inputPlaceholder', { title: t('tool.description') })}/>
|
||||
</FormItem>
|
||||
|
||||
|
||||
{/* 认证、请求头、配置 */}
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={formatTabItems()}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
{/* 认证模块 */}
|
||||
<>
|
||||
{/* 认证方式 */}
|
||||
<FormItem
|
||||
name={['config', 'connection_config', 'auth_type']}
|
||||
label={t('tool.authType')}
|
||||
hidden={activeTab !== 'auth'}
|
||||
>
|
||||
<Select
|
||||
placeholder={t('common.pleaseSelect')}
|
||||
options={authTypeList.map(value => ({
|
||||
label: t(`tool.${value}`),
|
||||
value
|
||||
}))}
|
||||
/>
|
||||
</FormItem>
|
||||
|
||||
{/* API Key: 认证方式 = api_key 展示 */}
|
||||
{values?.config?.connection_config?.auth_type === 'api_key' && <>
|
||||
<FormItem
|
||||
name={['config', 'connection_config', 'auth_config', "key_name"]}
|
||||
label={t('tool.key_name')}
|
||||
hidden={activeTab !== 'auth'}
|
||||
>
|
||||
<Input placeholder={t('common.inputPlaceholder', { title: t('tool.key_name') })} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name={['config', 'connection_config', 'auth_config', "api_key"]}
|
||||
label={t('tool.api_key')}
|
||||
hidden={activeTab !== 'auth'}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.inputPlaceholder', { title: t('tool.api_key') })} />
|
||||
</FormItem>
|
||||
</>}
|
||||
|
||||
{/* API Key: 认证方式 = bearer_token 展示 */}
|
||||
{values?.config?.connection_config?.auth_type === 'bearer_token' &&
|
||||
<FormItem
|
||||
name={['config', 'connection_config', 'auth_config', "token"]}
|
||||
label={t('tool.bearer_token')}
|
||||
hidden={activeTab !== 'auth'}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.inputPlaceholder', { title: t('tool.bearer_token') })} />
|
||||
</FormItem>
|
||||
}
|
||||
|
||||
{/* API Key: 认证方式 = basic_auth 展示 */}
|
||||
{values?.config?.connection_config?.auth_type === 'basic_auth' &&
|
||||
<>
|
||||
<FormItem
|
||||
name={['config', 'connection_config', 'auth_config', "username"]}
|
||||
label={t('tool.username')}
|
||||
hidden={activeTab !== 'auth'}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.inputPlaceholder', { title: t('tool.username') })} />
|
||||
</FormItem>
|
||||
<FormItem
|
||||
name={['config', 'connection_config', 'auth_config', "password"]}
|
||||
label={t('tool.password')}
|
||||
hidden={activeTab !== 'auth'}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input.Password placeholder={t('common.inputPlaceholder', { title: t('tool.password') })} />
|
||||
</FormItem>
|
||||
</>
|
||||
}
|
||||
</>
|
||||
{/* 请求头模块 */}
|
||||
<div className={activeTab !== 'requestHeader' ? 'rb:hidden' : ''}>
|
||||
<div className="rb:flex rb:items-center rb:justify-between rb:mb-1 rb:w-full">
|
||||
<div className="rb:font-medium rb:leading-5">{t('tool.requestHeader')}</div>
|
||||
<Button style={{padding: '0 8px', height: '24px'}} onClick={() => handleEditRequestHeader()}>+{t('tool.addRequestHeader')}</Button>
|
||||
</div>
|
||||
<div className="rb:text-[12px] rb:text-[#5B6167] rb:leading-4 rb:mb-3">{t('tool.requestHeaderDesc')}</div>
|
||||
|
||||
{requestHeaderList.length === 0
|
||||
? <Empty size={88} />
|
||||
:
|
||||
<Table
|
||||
rowKey="key"
|
||||
pagination={false}
|
||||
columns={[
|
||||
{
|
||||
title: t('tool.requestHeaderName'),
|
||||
dataIndex: 'key',
|
||||
key: 'key',
|
||||
},
|
||||
{
|
||||
title: t('tool.requestHeaderValue'),
|
||||
dataIndex: 'value',
|
||||
key: 'value',
|
||||
},
|
||||
{
|
||||
title: t('common.operation'),
|
||||
key: 'action',
|
||||
render: (_, record, index: number) => (
|
||||
<Space size="middle">
|
||||
<Button
|
||||
type="link"
|
||||
onClick={() => handleEditRequestHeader(index, record as RequestHeader)}
|
||||
>
|
||||
{t('common.edit')}
|
||||
</Button>
|
||||
<Button type="link" danger onClick={() => handleDeleteRequestHeader(index)}>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
</Space>
|
||||
),
|
||||
},
|
||||
]}
|
||||
initialData={requestHeaderList}
|
||||
emptySize={88}
|
||||
/>
|
||||
}
|
||||
</div>
|
||||
{/* 配置模块 */}
|
||||
<>
|
||||
<FormItem
|
||||
name={['config', 'connection_config', "timeout"]}
|
||||
label={t('tool.timeout')}
|
||||
hidden={activeTab !== 'config'}
|
||||
>
|
||||
<Input type="number" min={5} max={300} placeholder={t('common.pleaseEnter')} />
|
||||
</FormItem>
|
||||
</>
|
||||
</Form>
|
||||
|
||||
<RequestHeaderModal
|
||||
ref={requestHeaderModalRef}
|
||||
refreshTable={setRequestHeaderList}
|
||||
/>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default McpServiceModal;
|
||||
102
web/src/views/ToolManagement/components/RequestHeaderModal.tsx
Normal file
102
web/src/views/ToolManagement/components/RequestHeaderModal.tsx
Normal file
@@ -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<SetStateAction<RequestHeader[]>>;
|
||||
}
|
||||
|
||||
const RequestHeaderModal = forwardRef<RequestHeaderModalRef, RequestHeaderModalProps>(({
|
||||
refreshTable
|
||||
}, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<RequestHeader>();
|
||||
const [loading, setLoading] = useState(false)
|
||||
const [editIndex, setEditIndex] = useState<number | undefined>(-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 (
|
||||
<RbModal
|
||||
title={typeof editIndex === 'number' ? t('tool.editRequestHeader') : t('tool.addRequestHeader')}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText={t('common.create')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
{/* 请求头名称 */}
|
||||
<FormItem
|
||||
name="key"
|
||||
label={t('tool.requestHeaderName')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter')} />
|
||||
</FormItem>
|
||||
{/* 请求头值 */}
|
||||
<FormItem
|
||||
name="value"
|
||||
label={t('tool.requestHeaderValue')}
|
||||
rules={[{ required: true, message: t('common.pleaseEnter') }]}
|
||||
>
|
||||
<Input placeholder={t('common.enter',)} />
|
||||
</FormItem>
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default RequestHeaderModal;
|
||||
205
web/src/views/ToolManagement/components/TimeToolModal.tsx
Normal file
205
web/src/views/ToolManagement/components/TimeToolModal.tsx
Normal file
@@ -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<TimeToolModalRef>((_props, ref) => {
|
||||
const { t } = useTranslation();
|
||||
const [visible, setVisible] = useState(false);
|
||||
const [form] = Form.useForm<{ timestamp: string; formatType: string; }>();
|
||||
const [data, setData] = useState<ToolItem>({} as ToolItem)
|
||||
const [timeFormat, setTimeFormat] = useState<string | undefined>(undefined)
|
||||
const [activeTab, setActiveTab] = useState('currentTime');
|
||||
const values = Form.useWatch([], form)
|
||||
const [currentTime, setCurrentTime] = useState<CurrentTimeObj>({} as CurrentTimeObj)
|
||||
const [timestampFormat, setTimestampFormat] = useState<string | null>(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 (
|
||||
<RbModal
|
||||
title={data.name}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
footer={null}
|
||||
>
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
initialValues={{
|
||||
formatType: formatList[0].value
|
||||
}}
|
||||
>
|
||||
{/* 当前时间、时间戳转换、时间格式化 */}
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={formatTabItems()}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
|
||||
{/* 当前时间 */}
|
||||
{activeTab === 'currentTime' &&
|
||||
<>
|
||||
<FormItem label={t('tool.currentTime')} >
|
||||
<Input disabled value={currentTime?.datetime} />
|
||||
</FormItem>
|
||||
<FormItem label={t('tool.utcTime')} >
|
||||
<Input disabled value={currentTime?.iso_format} />
|
||||
</FormItem>
|
||||
<FormItem label={t('tool.secondsTimestamp')} >
|
||||
<Input disabled value={currentTime?.timestamp} />
|
||||
</FormItem>
|
||||
<FormItem label={t('tool.millisecondsTimestamp')} >
|
||||
<Input disabled value={currentTime?.timestamp_ms} />
|
||||
</FormItem>
|
||||
</>
|
||||
}
|
||||
{/* 时间戳转换 */}
|
||||
{activeTab === 'timestampConversion' &&
|
||||
<>
|
||||
<FormItem label={t('tool.enterTimestamp')} >
|
||||
<Row gutter={24}>
|
||||
<Col span={16}>
|
||||
<FormItem name="timestamp">
|
||||
<Input />
|
||||
</FormItem>
|
||||
</Col>
|
||||
<Col span={8}>
|
||||
<Button onClick={handleFormat}>{t('tool.conversion')}</Button>
|
||||
</Col>
|
||||
</Row>
|
||||
</FormItem>
|
||||
{timestampFormat &&
|
||||
<div className="rb:mt-3 rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
{t('tool.conversionResult')}
|
||||
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:mt-2">
|
||||
{timestampFormat}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
</>
|
||||
}
|
||||
{/* 时间格式化 */}
|
||||
{activeTab === 'timeFormat' &&
|
||||
<>
|
||||
<FormItem label={t('tool.chooseFormatType')} name="formatType">
|
||||
<Select
|
||||
options={formatList}
|
||||
onChange={handleChangeFormatType}
|
||||
/>
|
||||
</FormItem>
|
||||
<div className="rb:mt-3 rb:bg-[#F0F3F8] rb:px-3 rb:py-2.5 rb:rounded-md">
|
||||
{t('tool.conversionResult')}
|
||||
<div className="rb:font-medium rb:bg-white rb:px-3 rb:py-2.5 rb:rounded-md rb:mt-2">
|
||||
{timeFormat}
|
||||
</div>
|
||||
</div>
|
||||
</>
|
||||
}
|
||||
</Form>
|
||||
</RbModal>
|
||||
);
|
||||
});
|
||||
|
||||
export default TimeToolModal;
|
||||
190
web/src/views/ToolManagement/constant.ts
Normal file
190
web/src/views/ToolManagement/constant.ts
Normal file
@@ -0,0 +1,190 @@
|
||||
import type { InnerConfigItem } from './types';
|
||||
export const InnerConfigData: Record<string, InnerConfigItem> = {
|
||||
DateTimeTool: {
|
||||
features: [
|
||||
'timeFormat',
|
||||
'timeZoneConversion',
|
||||
'timestampConversion',
|
||||
'timeCalculation'
|
||||
],
|
||||
},
|
||||
JsonTool: {
|
||||
features: [
|
||||
'jsonFormat',
|
||||
'jsonGzip',
|
||||
'jsonCheck',
|
||||
'jsonConversion'
|
||||
],
|
||||
eg: '{"name":"工具","tool_class":"内置"}'
|
||||
},
|
||||
BaiduSearchTool: {
|
||||
link: 'https://ai.baidu.com/',
|
||||
config: {
|
||||
api_key: {
|
||||
name: ['config', 'parameters', 'api_key'],
|
||||
type: 'input',
|
||||
desc: 'BaiduSearchTool_api_key_desc',
|
||||
rules: [
|
||||
{ required: true, message: 'common.pleaseEnter' }
|
||||
]
|
||||
},
|
||||
type: {
|
||||
name: ['config', 'parameters', 'search_type'],
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'webSearch', value: 'web' },
|
||||
{ label: 'newsSearch', value: 'news' },
|
||||
{ label: 'imageSearch', value: 'image' },
|
||||
],
|
||||
defaultValue: 'webSearch'
|
||||
},
|
||||
pagesize: {
|
||||
name: ['config', 'parameters', 'pagesize'],
|
||||
type: 'number',
|
||||
range: {
|
||||
web: [1, 50],
|
||||
news: [1, 30],
|
||||
image: [1, 10],
|
||||
},
|
||||
step: 1,
|
||||
defaultValue: 10,
|
||||
desc: 'pagesize_desc'
|
||||
},
|
||||
BaiduSearchTool_enable: {
|
||||
name: ['config', 'is_enabled'],
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
},
|
||||
features: [
|
||||
'webSearch',
|
||||
'newsSearch',
|
||||
'imageSearch',
|
||||
'realTimeResults'
|
||||
],
|
||||
},
|
||||
MinerUTool: {
|
||||
link: 'https://MinerUTool.ai/',
|
||||
config: {
|
||||
api_key: {
|
||||
name: ['config', 'parameters', 'api_key'],
|
||||
type: 'input',
|
||||
desc: 'MinerUTool_api_key_desc',
|
||||
rules: [
|
||||
{ required: true, message: 'common.pleaseEnter' }
|
||||
]
|
||||
},
|
||||
api_address: {
|
||||
name: ['config', 'parameters', 'api_address'],
|
||||
type: 'input',
|
||||
desc: 'MinerUTool_api_address_desc',
|
||||
defaultValue: 'https://api.MinerUTool.ai/v1'
|
||||
},
|
||||
parsing_mode: {
|
||||
name: ['config', 'parameters', 'parsing_mode'],
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'auto_recognition', value: 'auto_recognition' },
|
||||
{ label: 'pure_text_mode', value: 'pure_text_mode' },
|
||||
{ label: 'table_priority', value: 'table_priority' },
|
||||
{ label: 'image_priority', value: 'image_priority' },
|
||||
],
|
||||
defaultValue: 'auto_recognition'
|
||||
},
|
||||
timeout: {
|
||||
name: ['config', 'parameters', 'timeout'],
|
||||
type: 'number',
|
||||
min: 10,
|
||||
max: 300,
|
||||
step: 1,
|
||||
defaultValue: 60,
|
||||
desc: 'MinerUTool_timeout_desc'
|
||||
},
|
||||
MinerUTool_enable: {
|
||||
name: ['config', 'is_enabled'],
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
MinerUTool_extract_images_enable: {
|
||||
name: ['config', 'images_enable'],
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
desc: 'MinerUTool_extract_images_enable_desc'
|
||||
}
|
||||
},
|
||||
features: [
|
||||
'pdfParser',
|
||||
'tableExtraction',
|
||||
'imageRecognition',
|
||||
'textExtraction'
|
||||
],
|
||||
},
|
||||
TextInTool: {
|
||||
link: 'https://www.TextInTool.com/',
|
||||
config: {
|
||||
app_id: {
|
||||
name: ['config', 'parameters', 'app_id'],
|
||||
type: 'input',
|
||||
desc: 'TextInTool_app_id_desc',
|
||||
rules: [
|
||||
{ required: true, message: 'common.pleaseEnter' }
|
||||
]
|
||||
},
|
||||
secret_key: {
|
||||
name: ['config', 'parameters', 'secret_key'],
|
||||
type: 'input',
|
||||
desc: 'TextInTool_secret_key_desc',
|
||||
rules: [
|
||||
{ required: true, message: 'common.pleaseEnter' }
|
||||
]
|
||||
},
|
||||
api_address: {
|
||||
name: ['config', 'parameters', 'api_address'],
|
||||
type: 'input',
|
||||
desc: 'TextInTool_api_address_desc',
|
||||
defaultValue: 'https://api.MinerUTool.ai/v1'
|
||||
},
|
||||
language_identification: {
|
||||
name: ['config', 'parameters', 'language_identification'],
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'automatic_detection', value: 'automatic_detection' },
|
||||
{ label: 'simplified_chinese', value: 'simplified_chinese' },
|
||||
{ label: 'traditional_chinese', value: 'traditional_chinese' },
|
||||
{ label: 'english', value: 'english' },
|
||||
{ label: 'japanese', value: 'japanese' },
|
||||
{ label: 'korean_language', value: 'korean_language' },
|
||||
],
|
||||
defaultValue: 'automatic_detection'
|
||||
},
|
||||
pattern_recognition: {
|
||||
name: ['config', 'parameters', 'pattern_recognition'],
|
||||
type: 'select',
|
||||
options: [
|
||||
{ label: 'universal_identification', value: 'universal_identification' },
|
||||
{ label: 'high_precision_identification', value: 'high_precision_identification' },
|
||||
{ label: 'handwriting_recognition', value: 'handwriting_recognition' },
|
||||
{ label: 'formula_recognition', value: 'formula_recognition' },
|
||||
],
|
||||
defaultValue: 'universal_identification'
|
||||
},
|
||||
TextInTool_enable: {
|
||||
name: ['config', 'is_enabled'],
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
},
|
||||
return_text_position_enable: {
|
||||
name: ['config', 'position_enable'],
|
||||
type: 'checkbox',
|
||||
defaultValue: true,
|
||||
desc: 'return_text_position_enable_desc'
|
||||
},
|
||||
},
|
||||
features: [
|
||||
'universalOCR',
|
||||
'handwritingRecognition',
|
||||
'multilingualSupport',
|
||||
'highPrecisionRecognition'
|
||||
],
|
||||
}
|
||||
}
|
||||
52
web/src/views/ToolManagement/index.tsx
Normal file
52
web/src/views/ToolManagement/index.tsx
Normal file
@@ -0,0 +1,52 @@
|
||||
import React, { useState } from 'react';
|
||||
import { Tabs } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
|
||||
import Mcp from './Mcp';
|
||||
import Inner from './Inner';
|
||||
import Custom from './Custom';
|
||||
import Tag from '@/components/Tag'
|
||||
|
||||
const tabKeys = ['mcp', 'inner', 'custom']
|
||||
const ToolManagement: React.FC = () => {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('mcp');
|
||||
|
||||
const formatTabItems = () => {
|
||||
return tabKeys.map(key => ({
|
||||
key,
|
||||
label: t(`tool.${key}`),
|
||||
}))
|
||||
}
|
||||
const handleChangeTab = (key: string) => {
|
||||
setActiveTab(key);
|
||||
}
|
||||
// 获取状态标签
|
||||
const getStatusTag = (status: string) => {
|
||||
switch (status) {
|
||||
case 'available':
|
||||
return <Tag color="processing">{t('tool.status.available')}</Tag>;
|
||||
case 'unconfigured':
|
||||
return <Tag color="default">{t('tool.status.unconfigured')}</Tag>;
|
||||
case 'configured_disabled':
|
||||
return <Tag color="warning">{t('tool.status.configured_disabled')}</Tag>;
|
||||
case 'error':
|
||||
return <Tag color="error">{t('tool.status.error')}</Tag>;
|
||||
}
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rb:-mt-4">
|
||||
<Tabs
|
||||
activeKey={activeTab}
|
||||
items={formatTabItems()}
|
||||
onChange={handleChangeTab}
|
||||
/>
|
||||
{activeTab === 'mcp' && <Mcp getStatusTag={getStatusTag} />}
|
||||
{activeTab === 'inner' && <Inner getStatusTag={getStatusTag} />}
|
||||
{activeTab === 'custom' && <Custom getStatusTag={getStatusTag} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
export default ToolManagement;
|
||||
138
web/src/views/ToolManagement/types.ts
Normal file
138
web/src/views/ToolManagement/types.ts
Normal file
@@ -0,0 +1,138 @@
|
||||
type ToolType = 'mcp' | 'builtin' | 'custom'
|
||||
export interface Query {
|
||||
name?: string;
|
||||
tool_type: ToolType
|
||||
}
|
||||
|
||||
interface ApiKeyAuth {
|
||||
key_name: string;
|
||||
api_key: string;
|
||||
}
|
||||
interface BasicAuth {
|
||||
username: string;
|
||||
password: string;
|
||||
}
|
||||
interface BearerTokenAuth {
|
||||
token: string;
|
||||
}
|
||||
export interface MCPToolItem {
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string | null;
|
||||
tool_type: ToolType;
|
||||
config: {
|
||||
server_url: string;
|
||||
connection_config: {
|
||||
auth_type: 'none' | 'api_key' | 'basic_auth' | 'bearer_token';
|
||||
auth_config: ApiKeyAuth | BasicAuth | BearerTokenAuth;
|
||||
timeout: number;
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
}
|
||||
}
|
||||
export interface InnerToolItem {
|
||||
config: {
|
||||
tool_class: string;
|
||||
requires_config: boolean;
|
||||
is_enabled: boolean;
|
||||
parameters: {
|
||||
api_key: string;
|
||||
}
|
||||
}
|
||||
}
|
||||
export interface CustomToolItem {
|
||||
name: string;
|
||||
description: string;
|
||||
icon?: string | null;
|
||||
tool_type: string;
|
||||
config: {
|
||||
auth_type: 'none' | 'api_key' | 'basic_auth' | 'bearer_token';
|
||||
auth_config: ApiKeyAuth | BasicAuth | BearerTokenAuth;
|
||||
schema_content: string;
|
||||
schema_url: null;
|
||||
}
|
||||
}
|
||||
export interface ToolItem {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
icon: string | null;
|
||||
tool_type: ToolType;
|
||||
version: string;
|
||||
parameters: any[];
|
||||
config_data: {
|
||||
server_url: string;
|
||||
connection_config: {
|
||||
auth_type: 'none' | 'api_key' | 'basic_auth' | 'bearer_token';
|
||||
auth_config: ApiKeyAuth | BasicAuth | BearerTokenAuth;
|
||||
timeout: number;
|
||||
headers: Record<string, string>;
|
||||
};
|
||||
last_health_check: number | null;
|
||||
health_status: 'unknown' | 'healthy' | 'unhealthy';
|
||||
available_tools: any[];
|
||||
|
||||
tool_class: string;
|
||||
|
||||
schema_content: string;
|
||||
};
|
||||
status: 'available' | 'unavailable';
|
||||
tags: string[];
|
||||
tenant_id: string;
|
||||
created_at: number;
|
||||
}
|
||||
|
||||
|
||||
export interface McpServiceModalRef {
|
||||
handleOpen: (data?: ToolItem) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
export interface TimeToolModalRef {
|
||||
handleOpen: (data: ToolItem) => void;
|
||||
}
|
||||
export interface JsonToolModalRef {
|
||||
handleOpen: (data: ToolItem) => void;
|
||||
}
|
||||
export interface InnerToolModalRef {
|
||||
handleOpen: (data: ToolItem) => void;
|
||||
}
|
||||
|
||||
export interface ConfigItem {
|
||||
name: string | string[];
|
||||
type: 'input' | 'select' | 'checkbox' | 'number';
|
||||
desc?: string;
|
||||
rules?: any[];
|
||||
options?: { label: string; value: string }[];
|
||||
range?: { [key: string]: number[]}
|
||||
min?: number;
|
||||
max?: number;
|
||||
step?: number;
|
||||
defaultValue?: any;
|
||||
}
|
||||
export interface InnerConfigItem {
|
||||
link?: string;
|
||||
config?: Record<string, ConfigItem>
|
||||
features: string[];
|
||||
eg?: string;
|
||||
}
|
||||
|
||||
export interface ExecuteData {
|
||||
tool_id: string;
|
||||
parameters: {
|
||||
operation: string;
|
||||
// 时间戳转换日期时间
|
||||
input_value?: string;
|
||||
output_format?: string;
|
||||
to_timezone?: string;
|
||||
input_format?: string;
|
||||
from_timezone?: string;
|
||||
indent?: number;
|
||||
ensure_ascii?: boolean;
|
||||
sort_keys?: boolean;
|
||||
input_data?: string;
|
||||
}
|
||||
}
|
||||
export interface CustomToolModalRef {
|
||||
handleOpen: (data?: ToolItem) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
Reference in New Issue
Block a user