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' import { stringRegExp } from '@/utils/validator'; 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', 'bearer_token'] const tabKeys = ['auth', 'requestHeader', 'config'] const McpServiceModal = forwardRef(({ refresh }, ref) => { const { t } = useTranslation(); const { message } = App.useApp(); const [visible, setVisible] = useState(false); const [form] = Form.useForm(); const [loading, setLoading] = useState(false); const [editVo, setEditVo] = useState(null) const [activeTab, setActiveTab] = useState('auth'); const values = Form.useWatch([], form) const requestHeaderModalRef = useRef(null) const [requestHeaderList, setRequestHeaderList] = useState([]) const abortControllerRef = useRef(null) const formatTabItems = () => { return tabKeys.map(key => ({ key, label: t(`tool.${key}`), })) } const handleChangeTab = (key: string) => { setActiveTab(key); } // 封装取消方法,添加关闭弹窗逻辑 const handleClose = () => { // 如果有正在进行的请求,取消它 if (abortControllerRef.current) { abortControllerRef.current.abort(); abortControllerRef.current = null; } 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 if (data) { const { config_data, name, description, icon } = data form.setFieldsValue({ name, description, icon, ...(config_data ? { config: { ...config_data } } : {}) }) // 如果是从 Market 组件传来的数据(包含 market_id),保存完整的 data 用于后续提交 if ((data as any).market_id) { setEditVo(data) } } else { form.resetFields(); } setVisible(true); }; // 封装保存方法,添加提交逻辑 const handleSave = () => { form .validateFields() .then(() => { setLoading(true); // 创建 AbortController 用于取消请求 abortControllerRef.current = new AbortController(); // 创建新服务对象 const { config, ...rest } = values const newService: MCPToolItem = { ...rest, tool_type: 'mcp', config: { ...config, connection_config: { ...config.connection_config, headers: requestHeaderList.reduce((acc: Record, cur) => { acc[cur.key] = cur.value return acc }, {}) } } } // 如果是从 Market 组件传来的数据,添加市场相关字段 if ((editVo as any)?.market_id) { (newService.config as any).source_channel = (editVo as any).source_channel; (newService.config as any).market_id = (editVo as any).market_id; (newService.config as any).market_config_id = (editVo as any).market_config_id; (newService.config as any).mcp_service_id = (editVo as any).mcp_service_id; } const request = editVo?.id ? updateTool(editVo.id, newService, { signal: abortControllerRef.current.signal }) : addTool(newService, { signal: abortControllerRef.current.signal }) request.then((res: any) => { // 清除 AbortController abortControllerRef.current = null; message.success(t('common.saveSuccess')); setLoading(false); handleClose(); refresh(); // 在后台测试连接,不阻塞用户操作 testConnection(res.tool_id || editVo?.id).catch((err) => { console.error('测试连接失败:', err); }); }) .catch((error) => { // 清除 AbortController abortControllerRef.current = null; // 如果是用户主动取消,不显示错误提示 if (error.name === 'AbortError' || error.code === 'ERR_CANCELED') { console.log('请求已取消'); } else { message.error(t('common.saveFailed')); } 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 ( ( <> )} >
{/* 服务端点 URL */} {/* 名称和图标 */} {/* */} {/* 描述 */} {/* 认证、请求头、配置 */} {/* 认证模块 */} <> {/* 认证方式 */} } {/* API Key: 认证方式 = bearer_token 展示 */} {values?.config?.connection_config?.auth_type === 'bearer_token' && } {/* API Key: 认证方式 = basic_auth 展示 */} {values?.config?.connection_config?.auth_type === 'basic_auth' && <> } {/* 请求头模块 */}
{t('tool.requestHeader')}
{t('tool.requestHeaderDesc')}
{requestHeaderList.length === 0 ? : { return
{value}
} }, { title: t('common.operation'), key: 'action', width: 80, render: (_, record, index: number) => ( ), }, ]} initialData={requestHeaderList} emptySize={88} scroll={{ x: 'max-content' }} /> } {/* 配置模块 */} <> ); }); export default McpServiceModal;