mirror of
https://github.com/langbot-app/LangBot.git
synced 2025-11-25 19:37:36 +08:00
fix: mcp refactor
This commit is contained in:
@@ -25,23 +25,42 @@ class MCPRouterGroup(group.RouterGroup):
|
||||
result = await self.ap.persistence_mgr.execute_async(
|
||||
sqlalchemy.select(MCPServer).order_by(MCPServer.created_at.desc())
|
||||
)
|
||||
servers = [self.ap.persistence_mgr.serialize_model(MCPServer, row) for row in result.scalars().all()]
|
||||
|
||||
raw_results = result.all()
|
||||
servers = [self.ap.persistence_mgr.serialize_model(MCPServer, row) for row in raw_results]
|
||||
|
||||
servers_with_status = []
|
||||
for server in servers:
|
||||
if servers['enable']:
|
||||
# 设置状态
|
||||
if server['enable']:
|
||||
status = 'enabled'
|
||||
else:
|
||||
status = 'disabled'
|
||||
|
||||
# 这里先写成开关状态,先不写连接状态
|
||||
# 构建 config 对象 (前端期望的格式)
|
||||
extra_args = server.get('extra_args', {})
|
||||
config = {
|
||||
'name': server['name'],
|
||||
'mode': server['mode'],
|
||||
'enable': server['enable'],
|
||||
}
|
||||
|
||||
# 根据模式添加相应的配置
|
||||
if server['mode'] == 'sse':
|
||||
config['url'] = extra_args.get('url', '')
|
||||
config['headers'] = extra_args.get('headers', {})
|
||||
config['timeout'] = extra_args.get('timeout', 60)
|
||||
elif server['mode'] == 'stdio':
|
||||
config['command'] = extra_args.get('command', '')
|
||||
config['args'] = extra_args.get('args', [])
|
||||
config['env'] = extra_args.get('env', {})
|
||||
|
||||
server_info = {
|
||||
'name': server['name'],
|
||||
'mode': server['mode'],
|
||||
'enable': server['enable'],
|
||||
'description': server.get('description',''),
|
||||
'extra_args': server.get('extra_args',{}),
|
||||
'status': status,
|
||||
'tools': [], # 暂时返回空数组,需要连接到MCP服务器才能获取工具列表
|
||||
'config': config,
|
||||
}
|
||||
servers_with_status.append(server_info)
|
||||
|
||||
|
||||
@@ -304,7 +304,7 @@ export default function LLMForm({
|
||||
onLLMDeleted();
|
||||
toast.success(t('models.deleteSuccess'));
|
||||
})
|
||||
.catch ((err) => {
|
||||
.catch((err) => {
|
||||
toast.error(t('models.deleteError') + err.message);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1,47 +1,51 @@
|
||||
'use client';
|
||||
|
||||
import { useEffect, useState, useRef } from 'react';
|
||||
import { useEffect, useState } from 'react';
|
||||
import styles from '@/app/home/plugins/plugins.module.css';
|
||||
import { MCPMarketCardVO } from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardVO';
|
||||
import MCPMarketCardComponent from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardComponent';
|
||||
// import { MCPMarketCardVO } from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardVO';
|
||||
// import MCPMarketCardComponent from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardComponent';
|
||||
import MCPCardComponent from '@/app/home/plugins/mcp/mcp-card/MCPCardComponent';
|
||||
import { MCPCardVO } from '@/app/home/plugins/mcp/MCPCardVO';
|
||||
// import { spaceClient } from '@/app/infra/http/HttpClient';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import {
|
||||
Pagination,
|
||||
PaginationContent,
|
||||
PaginationItem,
|
||||
PaginationLink,
|
||||
PaginationNext,
|
||||
PaginationPrevious,
|
||||
} from '@/components/ui/pagination';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@/components/ui/select';
|
||||
// import { Input } from '@/components/ui/input';
|
||||
// import {
|
||||
// Pagination,
|
||||
// PaginationContent,
|
||||
// PaginationItem,
|
||||
// PaginationLink,
|
||||
// PaginationNext,
|
||||
// PaginationPrevious,
|
||||
// } from '@/components/ui/pagination';
|
||||
// import {
|
||||
// Select,
|
||||
// SelectContent,
|
||||
// SelectItem,
|
||||
// SelectTrigger,
|
||||
// SelectValue,
|
||||
// } from '@/components/ui/select';
|
||||
|
||||
import { httpClient, HttpClient } from '@/app/infra/http/HttpClient';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
|
||||
export default function MCPMarketComponent({
|
||||
askInstallServer,
|
||||
onEditServer,
|
||||
}: {
|
||||
askInstallServer: (githubURL: string) => void;
|
||||
askInstallServer?: (githubURL: string) => void;
|
||||
onEditServer?: (serverName: string) => void;
|
||||
}) {
|
||||
const { t } = useTranslation();
|
||||
const [marketServerList, setMarketServerList] = useState<MCPMarketCardVO[]>(
|
||||
[],
|
||||
);
|
||||
const [totalCount, setTotalCount] = useState(0);
|
||||
const [nowPage, setNowPage] = useState(1);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
// const [marketServerList, setMarketServerList] = useState<MCPMarketCardVO[]>(
|
||||
// [],
|
||||
// );
|
||||
const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
|
||||
// const [totalCount, setTotalCount] = useState(0);
|
||||
// const [nowPage, setNowPage] = useState(1);
|
||||
// const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [sortByValue, setSortByValue] = useState<string>('pushed_at');
|
||||
const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
|
||||
const searchTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
const pageSize = 12;
|
||||
// const [sortByValue, setSortByValue] = useState<string>('pushed_at');
|
||||
// const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
|
||||
// const searchTimeout = useRef<NodeJS.Timeout | null>(null);
|
||||
// const pageSize = 12;
|
||||
|
||||
useEffect(() => {
|
||||
initData();
|
||||
@@ -49,95 +53,131 @@ export default function MCPMarketComponent({
|
||||
}, []);
|
||||
|
||||
function initData() {
|
||||
getServerList();
|
||||
fetchInstalledServers();
|
||||
// getServerList(); // GitHub 市场功能暂时注释
|
||||
}
|
||||
|
||||
function onInputSearchKeyword(keyword: string) {
|
||||
setSearchKeyword(keyword);
|
||||
|
||||
// 清除之前的定时器
|
||||
if (searchTimeout.current) {
|
||||
clearTimeout(searchTimeout.current);
|
||||
}
|
||||
|
||||
// 设置新的定时器
|
||||
searchTimeout.current = setTimeout(() => {
|
||||
setNowPage(1);
|
||||
getServerList(1, keyword);
|
||||
}, 500);
|
||||
function fetchInstalledServers() {
|
||||
setLoading(true);
|
||||
httpClient
|
||||
.getMCPServers()
|
||||
.then((resp) => {
|
||||
const servers = resp.servers.map((server) => new MCPCardVO(server));
|
||||
setInstalledServers(servers);
|
||||
setLoading(false);
|
||||
})
|
||||
.catch((error) => {
|
||||
console.error('Failed to fetch MCP servers:', error);
|
||||
setLoading(false);
|
||||
});
|
||||
}
|
||||
|
||||
function getServerList(
|
||||
page: number = nowPage,
|
||||
keyword: string = searchKeyword,
|
||||
sortBy: string = sortByValue,
|
||||
sortOrder: string = sortOrderValue,
|
||||
) {
|
||||
// setLoading(true);
|
||||
// GitHub 市场功能暂时注释
|
||||
// function onInputSearchKeyword(keyword: string) {
|
||||
// setSearchKeyword(keyword);
|
||||
// if (searchTimeout.current) {
|
||||
// clearTimeout(searchTimeout.current);
|
||||
// }
|
||||
// searchTimeout.current = setTimeout(() => {
|
||||
// setNowPage(1);
|
||||
// getServerList(1, keyword);
|
||||
// }, 500);
|
||||
// }
|
||||
|
||||
// 获取后端的 MCP Market 服务器列表
|
||||
httpClient.getMCPServers().then(
|
||||
);
|
||||
|
||||
|
||||
// function getServerList(
|
||||
// page: number = nowPage,
|
||||
// keyword: string = searchKeyword,
|
||||
// sortBy: string = sortByValue,
|
||||
// sortOrder: string = sortOrderValue,
|
||||
// ) {
|
||||
// // GitHub 安装功能暂时注释
|
||||
// // spaceClient
|
||||
// // .getMCPMarketServers(page, pageSize, keyword, sortBy, sortOrder)
|
||||
// // .then((res) => {
|
||||
// // setMarketServerList(
|
||||
// // res.servers.map((marketServer) => {
|
||||
// // let repository = marketServer.repository;
|
||||
// // if (repository.startsWith('https://github.com/')) {
|
||||
// // repository = repository.replace('https://github.com/', '');
|
||||
// // }
|
||||
// // if (repository.startsWith('github.com/')) {
|
||||
// // repository = repository.replace('github.com/', '');
|
||||
// // }
|
||||
// // const author = repository.split('/')[0];
|
||||
// // const name = repository.split('/')[1];
|
||||
// // return new MCPMarketCardVO({
|
||||
// // author: author,
|
||||
// // description: marketServer.description,
|
||||
// // githubURL: `https://github.com/${repository}`,
|
||||
// // name: name,
|
||||
// // serverId: String(marketServer.ID),
|
||||
// // starCount: marketServer.stars,
|
||||
// // version:
|
||||
// // 'version' in marketServer
|
||||
// // ? String(marketServer.version)
|
||||
// // : '1.0.0',
|
||||
// // });
|
||||
// // }),
|
||||
// // );
|
||||
// // setTotalCount(res.total);
|
||||
// // setLoading(false);
|
||||
// // console.log('market servers:', res);
|
||||
// // })
|
||||
// // .catch((error) => {
|
||||
// // console.error(t('mcp.getServerListError'), error);
|
||||
// // setLoading(false);
|
||||
// // });
|
||||
// }
|
||||
|
||||
|
||||
// spaceClient
|
||||
// .getMCPMarketServers(page, pageSize, keyword, sortBy, sortOrder)
|
||||
// .then((res) => {
|
||||
// setMarketServerList(
|
||||
// res.servers.map((marketServer) => {
|
||||
// let repository = marketServer.repository;
|
||||
// if (repository.startsWith('https://github.com/')) {
|
||||
// repository = repository.replace('https://github.com/', '');
|
||||
// }
|
||||
// function handlePageChange(page: number) {
|
||||
// setNowPage(page);
|
||||
// getServerList(page);
|
||||
// }
|
||||
|
||||
// if (repository.startsWith('github.com/')) {
|
||||
// repository = repository.replace('github.com/', '');
|
||||
// }
|
||||
|
||||
// const author = repository.split('/')[0];
|
||||
// const name = repository.split('/')[1];
|
||||
// return new MCPMarketCardVO({
|
||||
// author: author,
|
||||
// description: marketServer.description,
|
||||
// githubURL: `https://github.com/${repository}`,
|
||||
// name: name,
|
||||
// serverId: String(marketServer.ID),
|
||||
// starCount: marketServer.stars,
|
||||
// version:
|
||||
// 'version' in marketServer
|
||||
// ? String(marketServer.version)
|
||||
// : '1.0.0', // 如果没有提供版本,则默认为1.0.0
|
||||
// });
|
||||
// }),
|
||||
// );
|
||||
// setTotalCount(res.total);
|
||||
// setLoading(false);
|
||||
// console.log('market servers:', res);
|
||||
// })
|
||||
// .catch((error) => {
|
||||
// console.error(t('mcp.getServerListError'), error);
|
||||
// setLoading(false);
|
||||
// });
|
||||
}
|
||||
|
||||
function handlePageChange(page: number) {
|
||||
setNowPage(page);
|
||||
getServerList(page);
|
||||
}
|
||||
|
||||
function handleSortChange(value: string) {
|
||||
const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim());
|
||||
setSortByValue(newSortBy);
|
||||
setSortOrderValue(newSortOrder);
|
||||
setNowPage(1);
|
||||
getServerList(1, searchKeyword, newSortBy, newSortOrder);
|
||||
}
|
||||
// function handleSortChange(value: string) {
|
||||
// const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim());
|
||||
// setSortByValue(newSortBy);
|
||||
// setSortOrderValue(newSortOrder);
|
||||
// setNowPage(1);
|
||||
// getServerList(1, searchKeyword, newSortBy, newSortOrder);
|
||||
// }
|
||||
|
||||
return (
|
||||
<div className={`${styles.marketComponentBody}`}>
|
||||
<div className="flex items-center justify-start mb-2 mt-2 pl-[0.8rem] pr-[0.8rem]">
|
||||
{/* 已安装的服务器列表 */}
|
||||
<div className="mb-6">
|
||||
<h2 className="text-xl font-semibold mb-4 pl-[0.8rem] pt-4">
|
||||
{t('mcp.installedServers')}
|
||||
</h2>
|
||||
<div className={`${styles.pluginListContainer}`}>
|
||||
{loading ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{t('mcp.loading')}
|
||||
</div>
|
||||
) : installedServers.length === 0 ? (
|
||||
<div style={{ textAlign: 'center', padding: '20px' }}>
|
||||
{t('mcp.noInstalledServers')}
|
||||
</div>
|
||||
) : (
|
||||
installedServers.map((server, index) => (
|
||||
<div key={`${server.name}-${index}`}>
|
||||
<MCPCardComponent
|
||||
cardVO={server}
|
||||
onCardClick={() => {
|
||||
if (onEditServer) {
|
||||
onEditServer(server.name);
|
||||
}
|
||||
}}
|
||||
onRefresh={fetchInstalledServers}
|
||||
/>
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* GitHub 市场功能暂时注释 */}
|
||||
{/* <div className="flex items-center justify-start mb-2 mt-2 pl-[0.8rem] pr-[0.8rem]">
|
||||
<Input
|
||||
style={{
|
||||
width: '300px',
|
||||
@@ -178,7 +218,6 @@ export default function MCPMarketComponent({
|
||||
/>
|
||||
</PaginationItem>
|
||||
|
||||
{/* 如果总页数大于5,则只显示5页,如果总页数小于5,则显示所有页 */}
|
||||
{(() => {
|
||||
const totalPages = Math.ceil(totalCount / pageSize);
|
||||
const maxVisiblePages = 5;
|
||||
@@ -255,7 +294,7 @@ export default function MCPMarketComponent({
|
||||
</div>
|
||||
))
|
||||
)}
|
||||
</div>
|
||||
</div> */}
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -4,7 +4,6 @@ import PluginInstalledComponent, {
|
||||
} from '@/app/home/plugins/plugin-installed/PluginInstalledComponent';
|
||||
import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent';
|
||||
// import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog';
|
||||
import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent';
|
||||
import MCPComponent, {
|
||||
MCPComponentRef,
|
||||
} from '@/app/home/plugins/mcp/MCPComponent';
|
||||
@@ -34,19 +33,23 @@ import {
|
||||
DialogFooter,
|
||||
} from '@/components/ui/dialog';
|
||||
import { Input } from '@/components/ui/input';
|
||||
import React, { useState, useRef, useCallback, useEffect, use } from 'react';
|
||||
import React, { useState, useRef, useCallback, useEffect } from 'react';
|
||||
import { httpClient } from '@/app/infra/http/HttpClient';
|
||||
import { toast } from 'sonner';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { PluginV4 } from '@/app/infra/entities/plugin';
|
||||
import { systemInfo } from '@/app/infra/http/HttpClient';
|
||||
import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
|
||||
import { set } from 'lodash';
|
||||
import { passiveEventSupported } from '@tanstack/react-table';
|
||||
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@radix-ui/react-select';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import {
|
||||
Select,
|
||||
SelectContent,
|
||||
SelectItem,
|
||||
SelectTrigger,
|
||||
SelectValue,
|
||||
} from '@radix-ui/react-select';
|
||||
import { useForm } from 'react-hook-form';
|
||||
import { zodResolver } from '@hookform/resolvers/zod';
|
||||
import { number, z } from 'zod';
|
||||
import { z } from 'zod';
|
||||
import { DialogDescription } from '@radix-ui/react-dialog';
|
||||
import {
|
||||
Form,
|
||||
@@ -58,7 +61,6 @@ import {
|
||||
FormMessage,
|
||||
} from '@/components/ui/form';
|
||||
|
||||
|
||||
enum PluginInstallStatus {
|
||||
WAIT_INPUT = 'wait_input',
|
||||
ASK_CONFIRM = 'ask_confirm',
|
||||
@@ -66,38 +68,16 @@ enum PluginInstallStatus {
|
||||
ERROR = 'error',
|
||||
}
|
||||
|
||||
export default function PluginConfigPage(
|
||||
{
|
||||
editMode = false,
|
||||
initMCPId,
|
||||
onFormSubmit,
|
||||
onFormCancel,
|
||||
onMcpDeleted,
|
||||
}:
|
||||
{
|
||||
editMode?: boolean;
|
||||
initMCPId?: string;
|
||||
onFormSubmit?: () => void;
|
||||
onFormCancel?: () => void;
|
||||
onMcpDeleted?: () => void;
|
||||
} = {}
|
||||
) {
|
||||
|
||||
export default function PluginConfigPage() {
|
||||
const { t } = useTranslation();
|
||||
const [activeTab, setActiveTab] = useState('installed');
|
||||
const [modalOpen, setModalOpen] = useState(false);
|
||||
// const [sortModalOpen, setSortModalOpen] = useState(false);
|
||||
const [installSource, setInstallSource] = useState<string>('local');
|
||||
const [installInfo, setInstallInfo] = useState<Record<string, any>>({}); // eslint-disable-line @typescript-eslint/no-explicit-any
|
||||
const [sortModalOpen, setSortModalOpen] = useState(false);
|
||||
// const [mcpModalOpen, setMcpModalOpen] = useState(false);
|
||||
const [mcpMarketInstallModalOpen, setMcpMarketInstallModalOpen] =
|
||||
useState(false);
|
||||
const [mcpSSEModalOpen, setMcpSSEModalOpen] = useState(false);
|
||||
const [pluginInstallStatus, setPluginInstallStatus] =
|
||||
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
|
||||
const [installError, setInstallError] = useState<string | null>(null);
|
||||
const [mcpInstallError, setMcpInstallError] = useState<string | null>(null);
|
||||
const [githubURL, setGithubURL] = useState('');
|
||||
const [isDragOver, setIsDragOver] = useState(false);
|
||||
const [pluginSystemStatus, setPluginSystemStatus] =
|
||||
@@ -134,7 +114,7 @@ export default function PluginConfigPage(
|
||||
});
|
||||
}
|
||||
});
|
||||
const removeExtraArg = (index: number) => {
|
||||
const removeExtraArg = (index: number) => {
|
||||
const newArgs = extraArgs.filter((_, i) => i !== index);
|
||||
setExtraArgs(newArgs);
|
||||
form.setValue('extra_args', newArgs);
|
||||
@@ -143,7 +123,9 @@ export default function PluginConfigPage(
|
||||
z.object({
|
||||
name: z.string().min(1, { message: t('mcp.nameRequired') }),
|
||||
timeout: z.number().min(30, { message: t('mcp.timeoutMin30') }),
|
||||
ssereadtimeout: z.number().min(300, { message: t('mcp.sseTimeoutMin300') }),
|
||||
ssereadtimeout: z
|
||||
.number()
|
||||
.min(300, { message: t('mcp.sseTimeoutMin300') }),
|
||||
url: z.string().min(1, { message: t('mcp.requestURLRequired') }),
|
||||
extra_args: z.array(getExtraArgSchema(t)).optional(),
|
||||
});
|
||||
@@ -219,22 +201,35 @@ export default function PluginConfigPage(
|
||||
});
|
||||
}, 1000);
|
||||
}
|
||||
const [mcpGithubURL, setMcpGithubURL] = useState('');
|
||||
const [mcpSSEURL, setMcpSSEURL] = useState('');
|
||||
const [mcpSSEConfig, setMcpSSEConfig] = useState<Record<string, any> | null>(null);
|
||||
const [mcpInstallConfig, setMcpInstallConfig] = useState<Record<string, any> | null>(null);
|
||||
const pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
|
||||
const mcpComponentRef = useRef<MCPComponentRef>(null);
|
||||
const [mcpTesting, setMcpTesting] = useState(false);
|
||||
const [editingServerName, setEditingServerName] = useState<string | null>(
|
||||
null,
|
||||
);
|
||||
const [isEditMode, setIsEditMode] = useState(false);
|
||||
const [refreshKey, setRefreshKey] = useState(0);
|
||||
|
||||
// 强制清理 body 样式以修复 Dialog 关闭后点击失效的问题
|
||||
useEffect(() => {
|
||||
console.log('[Dialog Debug] States:', { mcpSSEModalOpen, modalOpen, showDeleteConfirmModal });
|
||||
console.log('[Dialog Debug] States:', {
|
||||
mcpSSEModalOpen,
|
||||
modalOpen,
|
||||
showDeleteConfirmModal,
|
||||
});
|
||||
|
||||
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
|
||||
console.log('[Dialog Debug] All dialogs closed, cleaning up body styles...');
|
||||
console.log('[Dialog Debug] Before cleanup - body.style.pointerEvents:', document.body.style.pointerEvents);
|
||||
console.log('[Dialog Debug] Before cleanup - body.style.overflow:', document.body.style.overflow);
|
||||
console.log(
|
||||
'[Dialog Debug] All dialogs closed, cleaning up body styles...',
|
||||
);
|
||||
console.log(
|
||||
'[Dialog Debug] Before cleanup - body.style.pointerEvents:',
|
||||
document.body.style.pointerEvents,
|
||||
);
|
||||
console.log(
|
||||
'[Dialog Debug] Before cleanup - body.style.overflow:',
|
||||
document.body.style.overflow,
|
||||
);
|
||||
|
||||
const cleanup = () => {
|
||||
// 强制移除 body 上可能残留的样式
|
||||
@@ -249,12 +244,21 @@ export default function PluginConfigPage(
|
||||
document.body.style.overflow = '';
|
||||
}
|
||||
|
||||
console.log('[Dialog Debug] After cleanup - body.style.pointerEvents:', document.body.style.pointerEvents);
|
||||
console.log('[Dialog Debug] After cleanup - body.style.overflow:', document.body.style.overflow);
|
||||
console.log(
|
||||
'[Dialog Debug] After cleanup - body.style.pointerEvents:',
|
||||
document.body.style.pointerEvents,
|
||||
);
|
||||
console.log(
|
||||
'[Dialog Debug] After cleanup - body.style.overflow:',
|
||||
document.body.style.overflow,
|
||||
);
|
||||
|
||||
// 检查计算后的样式
|
||||
const computedStyle = window.getComputedStyle(document.body);
|
||||
console.log('[Dialog Debug] Computed pointerEvents:', computedStyle.pointerEvents);
|
||||
console.log(
|
||||
'[Dialog Debug] Computed pointerEvents:',
|
||||
computedStyle.pointerEvents,
|
||||
);
|
||||
};
|
||||
|
||||
// 多次清理以确保覆盖 Radix 的设置
|
||||
@@ -280,7 +284,9 @@ export default function PluginConfigPage(
|
||||
const interval = setInterval(() => {
|
||||
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
|
||||
if (document.body.style.pointerEvents === 'none') {
|
||||
console.log('[Global Cleanup] Found stale pointer-events, cleaning...');
|
||||
console.log(
|
||||
'[Global Cleanup] Found stale pointer-events, cleaning...',
|
||||
);
|
||||
document.body.style.removeProperty('pointer-events');
|
||||
document.body.style.pointerEvents = '';
|
||||
}
|
||||
@@ -294,10 +300,15 @@ export default function PluginConfigPage(
|
||||
useEffect(() => {
|
||||
const observer = new MutationObserver((mutations) => {
|
||||
mutations.forEach((mutation) => {
|
||||
if (mutation.type === 'attributes' && mutation.attributeName === 'style') {
|
||||
if (
|
||||
mutation.type === 'attributes' &&
|
||||
mutation.attributeName === 'style'
|
||||
) {
|
||||
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
|
||||
if (document.body.style.pointerEvents === 'none') {
|
||||
console.log('[MutationObserver] Detected pointer-events being set to none, reverting...');
|
||||
console.log(
|
||||
'[MutationObserver] Detected pointer-events being set to none, reverting...',
|
||||
);
|
||||
document.body.style.removeProperty('pointer-events');
|
||||
document.body.style.pointerEvents = '';
|
||||
}
|
||||
@@ -360,8 +371,63 @@ export default function PluginConfigPage(
|
||||
}
|
||||
}
|
||||
|
||||
function deleteMCPServer() {
|
||||
|
||||
async function deleteMCPServer() {
|
||||
if (!editingServerName) return;
|
||||
|
||||
try {
|
||||
await httpClient.deleteMCPServer(editingServerName);
|
||||
toast.success(t('mcp.deleteSuccess'));
|
||||
|
||||
// 关闭所有对话框
|
||||
setShowDeleteConfirmModal(false);
|
||||
setMcpSSEModalOpen(false);
|
||||
|
||||
// 重置状态
|
||||
form.reset();
|
||||
setExtraArgs([]);
|
||||
setEditingServerName(null);
|
||||
setIsEditMode(false);
|
||||
|
||||
// 刷新服务器列表
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
} catch (error) {
|
||||
console.error('Failed to delete server:', error);
|
||||
toast.error(t('mcp.deleteFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
// 加载服务器数据用于编辑
|
||||
async function loadServerForEdit(serverName: string) {
|
||||
try {
|
||||
const resp = await httpClient.getMCPServer(serverName);
|
||||
const server = resp.server;
|
||||
|
||||
// 填充表单数据
|
||||
form.setValue('name', server.name);
|
||||
form.setValue('url', server.config.url || '');
|
||||
form.setValue('timeout', server.config.timeout || 30);
|
||||
form.setValue('ssereadtimeout', 300); // 默认值,如果后端有返回则使用后端的
|
||||
|
||||
// 填充 headers 作为 extra_args
|
||||
if (server.config.headers) {
|
||||
const headers = Object.entries(server.config.headers).map(
|
||||
([key, value]) => ({
|
||||
key,
|
||||
type: 'string' as const,
|
||||
value: String(value),
|
||||
}),
|
||||
);
|
||||
setExtraArgs(headers);
|
||||
form.setValue('extra_args', headers);
|
||||
}
|
||||
|
||||
setEditingServerName(serverName);
|
||||
setIsEditMode(true);
|
||||
setMcpSSEModalOpen(true);
|
||||
} catch (error) {
|
||||
console.error('Failed to load server:', error);
|
||||
toast.error(t('mcp.loadFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
async function handleFormSubmit(value: z.infer<typeof formSchema>) {
|
||||
@@ -389,22 +455,30 @@ export default function PluginConfigPage(
|
||||
timeout: value.timeout,
|
||||
};
|
||||
|
||||
await httpClient.createMCPServer(serverConfig);
|
||||
|
||||
toast.success(t('mcp.createSuccess'));
|
||||
if (isEditMode && editingServerName) {
|
||||
// 编辑模式:更新服务器
|
||||
await httpClient.updateMCPServer(editingServerName, serverConfig);
|
||||
toast.success(t('mcp.updateSuccess'));
|
||||
} else {
|
||||
// 创建模式:新建服务器
|
||||
await httpClient.createMCPServer(serverConfig);
|
||||
toast.success(t('mcp.createSuccess'));
|
||||
}
|
||||
|
||||
// 只有在异步操作成功后才关闭对话框
|
||||
setMcpSSEModalOpen(false);
|
||||
|
||||
// 重置表单
|
||||
// 重置表单和状态
|
||||
form.reset();
|
||||
setExtraArgs([]);
|
||||
setEditingServerName(null);
|
||||
setIsEditMode(false);
|
||||
|
||||
// 调用回调通知父组件刷新
|
||||
onFormSubmit?.();
|
||||
// 刷新服务器列表
|
||||
setRefreshKey((prev) => prev + 1);
|
||||
} catch (error) {
|
||||
console.error('Failed to create MCP server:', error);
|
||||
toast.error(t('mcp.createFailed'));
|
||||
console.error('Failed to save MCP server:', error);
|
||||
toast.error(isEditMode ? t('mcp.updateFailed') : t('mcp.createFailed'));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -422,9 +496,9 @@ export default function PluginConfigPage(
|
||||
extraArgsObj[arg.key] = arg.value;
|
||||
}
|
||||
});
|
||||
httpClient.testMCPServer(
|
||||
form.getValues('name'),
|
||||
).then((res) => {
|
||||
httpClient
|
||||
.testMCPServer(form.getValues('name'))
|
||||
.then((res) => {
|
||||
console.log(res);
|
||||
toast.success(t('models.testSuccess'));
|
||||
})
|
||||
@@ -573,7 +647,6 @@ export default function PluginConfigPage(
|
||||
return renderPluginConnectionErrorState();
|
||||
}
|
||||
|
||||
|
||||
return (
|
||||
<div
|
||||
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}
|
||||
@@ -599,9 +672,12 @@ export default function PluginConfigPage(
|
||||
{t('plugins.marketplace')}
|
||||
</TabsTrigger>
|
||||
)}
|
||||
<TabsTrigger value="mcp-servers" className="px-6 py-4 cursor-pointer">
|
||||
{t('mcp.title')}
|
||||
</TabsTrigger>
|
||||
<TabsTrigger
|
||||
value="mcp-servers"
|
||||
className="px-6 py-4 cursor-pointer"
|
||||
>
|
||||
{t('mcp.title')}
|
||||
</TabsTrigger>
|
||||
</TabsList>
|
||||
|
||||
<div className="flex flex-row justify-end items-center">
|
||||
@@ -618,7 +694,9 @@ export default function PluginConfigPage(
|
||||
<DropdownMenuTrigger asChild>
|
||||
<Button variant="default" className="px-6 py-4 cursor-pointer">
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
{activeTab === 'mcp-servers' ? t('mcp.add') : t('plugins.install')}
|
||||
{activeTab === 'mcp-servers'
|
||||
? t('mcp.add')
|
||||
: t('plugins.install')}
|
||||
<ChevronDownIcon className="ml-2 w-4 h-4" />
|
||||
</Button>
|
||||
</DropdownMenuTrigger>
|
||||
@@ -637,9 +715,13 @@ export default function PluginConfigPage(
|
||||
<PlusIcon className="w-4 h-4" />
|
||||
{t('mcp.installFromGithub')}
|
||||
</DropdownMenuItem> */}
|
||||
<DropdownMenuItem
|
||||
<DropdownMenuItem
|
||||
onClick={() => {
|
||||
setActiveTab('mcp-servers');
|
||||
setIsEditMode(false);
|
||||
setEditingServerName(null);
|
||||
form.reset();
|
||||
setExtraArgs([]);
|
||||
setMcpSSEModalOpen(true);
|
||||
}}
|
||||
>
|
||||
@@ -691,11 +773,9 @@ export default function PluginConfigPage(
|
||||
</TabsContent>
|
||||
<TabsContent value="mcp-servers">
|
||||
<MCPMarketComponent
|
||||
askInstallServer={(githubURL) => {
|
||||
setMcpGithubURL(githubURL);
|
||||
setMcpMarketInstallModalOpen(true);
|
||||
// setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT);
|
||||
setMcpInstallError(null);
|
||||
key={refreshKey}
|
||||
onEditServer={(serverName) => {
|
||||
loadServerForEdit(serverName);
|
||||
}}
|
||||
/>
|
||||
</TabsContent>
|
||||
@@ -781,229 +861,222 @@ export default function PluginConfigPage(
|
||||
open={showDeleteConfirmModal}
|
||||
onOpenChange={setShowDeleteConfirmModal}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{t('plugins.deleteConfirmation')}
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant='destructive'
|
||||
onClick={() => {
|
||||
deleteMCPServer();
|
||||
setShowDeleteConfirmModal(false);
|
||||
}}
|
||||
>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle>
|
||||
</DialogHeader>
|
||||
<DialogDescription>
|
||||
{t('plugins.deleteConfirmation')}
|
||||
</DialogDescription>
|
||||
<DialogFooter>
|
||||
<Button
|
||||
variant="destructive"
|
||||
onClick={() => {
|
||||
deleteMCPServer();
|
||||
setShowDeleteConfirmModal(false);
|
||||
}}
|
||||
>
|
||||
{t('common.confirm')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
|
||||
|
||||
<Dialog
|
||||
open={mcpSSEModalOpen}
|
||||
onOpenChange={setMcpSSEModalOpen}
|
||||
onOpenChange={(open) => {
|
||||
setMcpSSEModalOpen(open);
|
||||
if (!open) {
|
||||
// 关闭对话框时重置编辑状态
|
||||
setIsEditMode(false);
|
||||
setEditingServerName(null);
|
||||
form.reset();
|
||||
setExtraArgs([]);
|
||||
}
|
||||
}}
|
||||
>
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{t('mcp.createServer')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleFormSubmit)}
|
||||
className='space-y-4'
|
||||
>
|
||||
<div className='space-y-4'>
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='name'
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control = {form.control}
|
||||
name = 'url'
|
||||
render={
|
||||
({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('mcp.url')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='timeout'
|
||||
render = {
|
||||
({field}) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('mcp.timeout')}
|
||||
</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
{...field}
|
||||
/>
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
</FormItem>
|
||||
)
|
||||
}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name='ssereadtimeout'
|
||||
render = {
|
||||
(field) =>
|
||||
(
|
||||
<DialogContent>
|
||||
<DialogHeader>
|
||||
<DialogTitle>
|
||||
{isEditMode ? t('mcp.editServer') : t('mcp.createServer')}
|
||||
</DialogTitle>
|
||||
</DialogHeader>
|
||||
<Form {...form}>
|
||||
<form
|
||||
onSubmit={form.handleSubmit(handleFormSubmit)}
|
||||
className="space-y-4"
|
||||
>
|
||||
<div className="space-y-4">
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="name"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>
|
||||
{t('mcp.ssereadtimeout')}
|
||||
</FormLabel>
|
||||
<FormLabel>{t('mcp.name')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input
|
||||
placeholder={t('mcp.sseTimeout')}
|
||||
{...field}
|
||||
/>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage/>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)
|
||||
}
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>{t('models.extraParameters')}</FormLabel>
|
||||
<div className="space-y-2">
|
||||
{extraArgs.map((arg, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('models.keyName')}
|
||||
value={arg.key}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'key', e.target.value)
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={arg.type}
|
||||
onValueChange={(value) =>
|
||||
updateExtraArg(index, 'type', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('models.type')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">
|
||||
{t('models.string')}
|
||||
</SelectItem>
|
||||
<SelectItem value="number">
|
||||
{t('models.number')}
|
||||
</SelectItem>
|
||||
<SelectItem value="boolean">
|
||||
{t('models.boolean')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder={t('models.value')}
|
||||
value={arg.value}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'value', e.target.value)
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 hover:bg-gray-100 rounded"
|
||||
onClick={() => removeExtraArg(index)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="w-5 h-5 text-red-500"
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="url"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.url')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="timeout"
|
||||
render={({ field }) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.timeout')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormField
|
||||
control={form.control}
|
||||
name="ssereadtimeout"
|
||||
render={(field) => (
|
||||
<FormItem>
|
||||
<FormLabel>{t('mcp.ssereadtimeout')}</FormLabel>
|
||||
<FormControl>
|
||||
<Input placeholder={t('mcp.sseTimeout')} {...field} />
|
||||
</FormControl>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
)}
|
||||
/>
|
||||
|
||||
<FormItem>
|
||||
<FormLabel>{t('models.extraParameters')}</FormLabel>
|
||||
<div className="space-y-2">
|
||||
{extraArgs.map((arg, index) => (
|
||||
<div key={index} className="flex gap-2">
|
||||
<Input
|
||||
placeholder={t('models.keyName')}
|
||||
value={arg.key}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'key', e.target.value)
|
||||
}
|
||||
/>
|
||||
<Select
|
||||
value={arg.type}
|
||||
onValueChange={(value) =>
|
||||
updateExtraArg(index, 'type', value)
|
||||
}
|
||||
>
|
||||
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
||||
<SelectValue placeholder={t('models.type')} />
|
||||
</SelectTrigger>
|
||||
<SelectContent>
|
||||
<SelectItem value="string">
|
||||
{t('models.string')}
|
||||
</SelectItem>
|
||||
<SelectItem value="number">
|
||||
{t('models.number')}
|
||||
</SelectItem>
|
||||
<SelectItem value="boolean">
|
||||
{t('models.boolean')}
|
||||
</SelectItem>
|
||||
</SelectContent>
|
||||
</Select>
|
||||
<Input
|
||||
placeholder={t('models.value')}
|
||||
value={arg.value}
|
||||
onChange={(e) =>
|
||||
updateExtraArg(index, 'value', e.target.value)
|
||||
}
|
||||
/>
|
||||
<button
|
||||
type="button"
|
||||
className="p-2 hover:bg-gray-100 rounded"
|
||||
onClick={() => removeExtraArg(index)}
|
||||
>
|
||||
<svg
|
||||
xmlns="http://www.w3.org/2000/svg"
|
||||
viewBox="0 0 24 24"
|
||||
fill="currentColor"
|
||||
className="w-5 h-5 text-red-500"
|
||||
>
|
||||
<path d="M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={addExtraArg}
|
||||
>
|
||||
<path d="M7 4V2H17V4H22V6H20V21C20 21.5523 19.5523 22 19 22H5C4.44772 22 4 21.5523 4 21V6H2V4H7ZM6 6V20H18V6H6ZM9 9H11V17H9V9ZM13 9H15V17H13V9Z"></path>
|
||||
</svg>
|
||||
</button>
|
||||
</div>
|
||||
))}
|
||||
<Button type="button" variant="outline" onClick={addExtraArg}>
|
||||
{t('models.addParameter')}
|
||||
</Button>
|
||||
</div>
|
||||
<FormDescription>
|
||||
{t('llm.extraParametersDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
{t('models.addParameter')}
|
||||
</Button>
|
||||
</div>
|
||||
<FormDescription>
|
||||
{t('llm.extraParametersDescription')}
|
||||
</FormDescription>
|
||||
<FormMessage />
|
||||
</FormItem>
|
||||
|
||||
<DialogFooter>
|
||||
{editMode && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteConfirmModal(true)}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
)}
|
||||
<DialogFooter>
|
||||
{isEditMode && (
|
||||
<Button
|
||||
type="button"
|
||||
variant="destructive"
|
||||
onClick={() => setShowDeleteConfirmModal(true)}
|
||||
>
|
||||
{t('common.delete')}
|
||||
</Button>
|
||||
)}
|
||||
|
||||
<Button type="submit">
|
||||
{editMode ? t('common.save') : t('common.submit')}
|
||||
</Button>
|
||||
<Button type="submit">
|
||||
{isEditMode ? t('common.save') : t('common.submit')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => testMcp()}
|
||||
disabled={mcpTesting}
|
||||
>
|
||||
{t('common.test')}
|
||||
</Button>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => testMcp()}
|
||||
disabled={mcpTesting}
|
||||
>
|
||||
{t('common.test')}
|
||||
</Button>
|
||||
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setMcpSSEModalOpen(false);
|
||||
form.reset();
|
||||
setExtraArgs([]);
|
||||
onFormCancel?.();
|
||||
}}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
<Button
|
||||
type="button"
|
||||
variant="outline"
|
||||
onClick={() => {
|
||||
setMcpSSEModalOpen(false);
|
||||
form.reset();
|
||||
setExtraArgs([]);
|
||||
setIsEditMode(false);
|
||||
setEditingServerName(null);
|
||||
}}
|
||||
>
|
||||
{t('common.cancel')}
|
||||
</Button>
|
||||
</DialogFooter>
|
||||
</div>
|
||||
</form>
|
||||
</Form>
|
||||
</DialogContent>
|
||||
</Dialog>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -553,7 +553,7 @@ export class BackendClient extends BaseHttpClient {
|
||||
}
|
||||
|
||||
public installMCPServerFromSSE(
|
||||
source: {},
|
||||
source: object,
|
||||
): Promise<AsyncTaskCreatedResp> {
|
||||
return this.post('/api/v1/mcp/servers', { source });
|
||||
}
|
||||
|
||||
@@ -11,32 +11,35 @@ function Dialog({
|
||||
open,
|
||||
...props
|
||||
}: React.ComponentProps<typeof DialogPrimitive.Root>) {
|
||||
const handleOpenChange = React.useCallback((isOpen: boolean) => {
|
||||
onOpenChange?.(isOpen);
|
||||
const handleOpenChange = React.useCallback(
|
||||
(isOpen: boolean) => {
|
||||
onOpenChange?.(isOpen);
|
||||
|
||||
// 当对话框关闭时,确保清理 body 样式
|
||||
if (!isOpen) {
|
||||
// 立即清理
|
||||
document.body.style.removeProperty('pointer-events');
|
||||
document.body.style.removeProperty('overflow');
|
||||
|
||||
// 延迟再次清理,确保覆盖 Radix 的设置
|
||||
setTimeout(() => {
|
||||
// 当对话框关闭时,确保清理 body 样式
|
||||
if (!isOpen) {
|
||||
// 立即清理
|
||||
document.body.style.removeProperty('pointer-events');
|
||||
document.body.style.removeProperty('overflow');
|
||||
}, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.style.removeProperty('pointer-events');
|
||||
document.body.style.removeProperty('overflow');
|
||||
}, 50);
|
||||
// 延迟再次清理,确保覆盖 Radix 的设置
|
||||
setTimeout(() => {
|
||||
document.body.style.removeProperty('pointer-events');
|
||||
document.body.style.removeProperty('overflow');
|
||||
}, 0);
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.style.removeProperty('pointer-events');
|
||||
document.body.style.removeProperty('overflow');
|
||||
}, 150);
|
||||
}
|
||||
}, [onOpenChange]);
|
||||
setTimeout(() => {
|
||||
document.body.style.removeProperty('pointer-events');
|
||||
document.body.style.removeProperty('overflow');
|
||||
}, 50);
|
||||
|
||||
setTimeout(() => {
|
||||
document.body.style.removeProperty('pointer-events');
|
||||
document.body.style.removeProperty('overflow');
|
||||
}, 150);
|
||||
}
|
||||
},
|
||||
[onOpenChange],
|
||||
);
|
||||
|
||||
// 使用 effect 监控 open 状态变化
|
||||
React.useEffect(() => {
|
||||
@@ -61,7 +64,14 @@ function Dialog({
|
||||
}
|
||||
}, [open]);
|
||||
|
||||
return <DialogPrimitive.Root data-slot="dialog" open={open} {...props} onOpenChange={handleOpenChange} />;
|
||||
return (
|
||||
<DialogPrimitive.Root
|
||||
data-slot="dialog"
|
||||
open={open}
|
||||
{...props}
|
||||
onOpenChange={handleOpenChange}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
function DialogTrigger({
|
||||
|
||||
@@ -1,4 +1,3 @@
|
||||
|
||||
const zhHans = {
|
||||
common: {
|
||||
login: '登录',
|
||||
@@ -335,20 +334,20 @@ const zhHans = {
|
||||
onlySupportGithub: '目前仅支持从Github安装MCP服务器',
|
||||
enterGithubLink: '输入Github仓库链接',
|
||||
add: '添加',
|
||||
name:'名称',
|
||||
nameExplained:'用于区分不同的MCP服务器实例',
|
||||
mcpDescription:'描述',
|
||||
descriptionExplained:'简要描述这个MCP服务器的功能或用途',
|
||||
sseURL:'SSE URL',
|
||||
sseHeaders:'SSE Headers',
|
||||
nameRequired:'名称不能为空',
|
||||
sseURLRequired:'SSE URL不能为空',
|
||||
enterSSELink:'输入SSE URL',
|
||||
timeoutRequired:'超时时间不能为空',
|
||||
headersExample:'示例: Authorization: Bearer token123',
|
||||
enterTimeout:'输入超时时间,单位为毫秒',
|
||||
installFromSSE:'从SSE安装',
|
||||
sseTimeout:'SSE超时时间'
|
||||
name: '名称',
|
||||
nameExplained: '用于区分不同的MCP服务器实例',
|
||||
mcpDescription: '描述',
|
||||
descriptionExplained: '简要描述这个MCP服务器的功能或用途',
|
||||
sseURL: 'SSE URL',
|
||||
sseHeaders: 'SSE Headers',
|
||||
nameRequired: '名称不能为空',
|
||||
sseURLRequired: 'SSE URL不能为空',
|
||||
enterSSELink: '输入SSE URL',
|
||||
timeoutRequired: '超时时间不能为空',
|
||||
headersExample: '示例: Authorization: Bearer token123',
|
||||
enterTimeout: '输入超时时间,单位为毫秒',
|
||||
installFromSSE: '从SSE安装',
|
||||
sseTimeout: 'SSE超时时间',
|
||||
},
|
||||
pipelines: {
|
||||
title: '流水线',
|
||||
|
||||
Reference in New Issue
Block a user