feat(web): add tool management

This commit is contained in:
zhaoying
2025-12-26 11:57:50 +08:00
parent ad2f52c037
commit 44aac44a05
18 changed files with 2193 additions and 19 deletions

31
web/src/api/tools.ts Normal file
View 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)
}

View File

@@ -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',

View File

@@ -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: '核心节点',

View File

@@ -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')),

View File

@@ -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" },

View File

@@ -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,

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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;

View 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'
],
}
}

View 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;

View 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;
}