fix: mcp refactor

This commit is contained in:
wangcham
2025-10-23 15:47:44 +00:00
parent d0a3dee083
commit 075091ed06
7 changed files with 577 additions and 437 deletions
+24 -5
View File
@@ -25,23 +25,42 @@ class MCPRouterGroup(group.RouterGroup):
result = await self.ap.persistence_mgr.execute_async( result = await self.ap.persistence_mgr.execute_async(
sqlalchemy.select(MCPServer).order_by(MCPServer.created_at.desc()) 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 = [] servers_with_status = []
for server in servers: for server in servers:
if servers['enable']: # 设置状态
if server['enable']:
status = 'enabled' status = 'enabled'
else: else:
status = 'disabled' 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 = { server_info = {
'name': server['name'], 'name': server['name'],
'mode': server['mode'], 'mode': server['mode'],
'enable': server['enable'], 'enable': server['enable'],
'description': server.get('description',''),
'extra_args': server.get('extra_args',{}),
'status': status, 'status': status,
'tools': [], # 暂时返回空数组,需要连接到MCP服务器才能获取工具列表
'config': config,
} }
servers_with_status.append(server_info) servers_with_status.append(server_info)
@@ -1,47 +1,51 @@
'use client'; 'use client';
import { useEffect, useState, useRef } from 'react'; import { useEffect, useState } from 'react';
import styles from '@/app/home/plugins/plugins.module.css'; import styles from '@/app/home/plugins/plugins.module.css';
import { MCPMarketCardVO } from '@/app/home/plugins/mcp-market/mcp-market-card/MCPMarketCardVO'; // 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 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 { spaceClient } from '@/app/infra/http/HttpClient';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { Input } from '@/components/ui/input'; // import { Input } from '@/components/ui/input';
import { // import {
Pagination, // Pagination,
PaginationContent, // PaginationContent,
PaginationItem, // PaginationItem,
PaginationLink, // PaginationLink,
PaginationNext, // PaginationNext,
PaginationPrevious, // PaginationPrevious,
} from '@/components/ui/pagination'; // } from '@/components/ui/pagination';
import { // import {
Select, // Select,
SelectContent, // SelectContent,
SelectItem, // SelectItem,
SelectTrigger, // SelectTrigger,
SelectValue, // SelectValue,
} from '@/components/ui/select'; // } from '@/components/ui/select';
import { httpClient, HttpClient } from '@/app/infra/http/HttpClient'; import { httpClient } from '@/app/infra/http/HttpClient';
export default function MCPMarketComponent({ export default function MCPMarketComponent({
askInstallServer, onEditServer,
}: { }: {
askInstallServer: (githubURL: string) => void; askInstallServer?: (githubURL: string) => void;
onEditServer?: (serverName: string) => void;
}) { }) {
const { t } = useTranslation(); const { t } = useTranslation();
const [marketServerList, setMarketServerList] = useState<MCPMarketCardVO[]>( // const [marketServerList, setMarketServerList] = useState<MCPMarketCardVO[]>(
[], // [],
); // );
const [totalCount, setTotalCount] = useState(0); const [installedServers, setInstalledServers] = useState<MCPCardVO[]>([]);
const [nowPage, setNowPage] = useState(1); // const [totalCount, setTotalCount] = useState(0);
const [searchKeyword, setSearchKeyword] = useState(''); // const [nowPage, setNowPage] = useState(1);
// const [searchKeyword, setSearchKeyword] = useState('');
const [loading, setLoading] = useState(false); const [loading, setLoading] = useState(false);
const [sortByValue, setSortByValue] = useState<string>('pushed_at'); // const [sortByValue, setSortByValue] = useState<string>('pushed_at');
const [sortOrderValue, setSortOrderValue] = useState<string>('DESC'); // const [sortOrderValue, setSortOrderValue] = useState<string>('DESC');
const searchTimeout = useRef<NodeJS.Timeout | null>(null); // const searchTimeout = useRef<NodeJS.Timeout | null>(null);
const pageSize = 12; // const pageSize = 12;
useEffect(() => { useEffect(() => {
initData(); initData();
@@ -49,95 +53,131 @@ export default function MCPMarketComponent({
}, []); }, []);
function initData() { function initData() {
getServerList(); fetchInstalledServers();
// getServerList(); // GitHub 市场功能暂时注释
} }
function onInputSearchKeyword(keyword: string) { function fetchInstalledServers() {
setSearchKeyword(keyword); setLoading(true);
httpClient
// 清除之前的定时器 .getMCPServers()
if (searchTimeout.current) { .then((resp) => {
clearTimeout(searchTimeout.current); 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);
});
} }
// 设置新的定时器 // GitHub 市场功能暂时注释
searchTimeout.current = setTimeout(() => { // function onInputSearchKeyword(keyword: string) {
setNowPage(1); // setSearchKeyword(keyword);
getServerList(1, keyword); // if (searchTimeout.current) {
}, 500); // clearTimeout(searchTimeout.current);
} // }
// searchTimeout.current = setTimeout(() => {
function getServerList( // setNowPage(1);
page: number = nowPage, // getServerList(1, keyword);
keyword: string = searchKeyword, // }, 500);
sortBy: string = sortByValue,
sortOrder: string = sortOrderValue,
) {
// setLoading(true);
// 获取后端的 MCP Market 服务器列表
httpClient.getMCPServers().then(
);
// 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/')) { // function getServerList(
// repository = repository.replace('github.com/', ''); // 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);
// // });
// } // }
// const author = repository.split('/')[0]; // function handlePageChange(page: number) {
// const name = repository.split('/')[1]; // setNowPage(page);
// return new MCPMarketCardVO({ // getServerList(page);
// 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) { // function handleSortChange(value: string) {
setNowPage(page); // const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim());
getServerList(page); // setSortByValue(newSortBy);
} // setSortOrderValue(newSortOrder);
// setNowPage(1);
function handleSortChange(value: string) { // getServerList(1, searchKeyword, newSortBy, newSortOrder);
const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim()); // }
setSortByValue(newSortBy);
setSortOrderValue(newSortOrder);
setNowPage(1);
getServerList(1, searchKeyword, newSortBy, newSortOrder);
}
return ( return (
<div className={`${styles.marketComponentBody}`}> <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 <Input
style={{ style={{
width: '300px', width: '300px',
@@ -178,7 +218,6 @@ export default function MCPMarketComponent({
/> />
</PaginationItem> </PaginationItem>
{/* 如果总页数大于5,则只显示5页,如果总页数小于5,则显示所有页 */}
{(() => { {(() => {
const totalPages = Math.ceil(totalCount / pageSize); const totalPages = Math.ceil(totalCount / pageSize);
const maxVisiblePages = 5; const maxVisiblePages = 5;
@@ -255,7 +294,7 @@ export default function MCPMarketComponent({
</div> </div>
)) ))
)} )}
</div> </div> */}
</div> </div>
); );
} }
+182 -109
View File
@@ -4,7 +4,6 @@ import PluginInstalledComponent, {
} from '@/app/home/plugins/plugin-installed/PluginInstalledComponent'; } from '@/app/home/plugins/plugin-installed/PluginInstalledComponent';
import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent'; import MarketPage from '@/app/home/plugins/plugin-market/PluginMarketComponent';
// import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog'; // import PluginSortDialog from '@/app/home/plugins/plugin-sort/PluginSortDialog';
import PluginMarketComponent from '@/app/home/plugins/plugin-market/PluginMarketComponent';
import MCPComponent, { import MCPComponent, {
MCPComponentRef, MCPComponentRef,
} from '@/app/home/plugins/mcp/MCPComponent'; } from '@/app/home/plugins/mcp/MCPComponent';
@@ -34,19 +33,23 @@ import {
DialogFooter, DialogFooter,
} from '@/components/ui/dialog'; } from '@/components/ui/dialog';
import { Input } from '@/components/ui/input'; 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 { httpClient } from '@/app/infra/http/HttpClient';
import { toast } from 'sonner'; import { toast } from 'sonner';
import { useTranslation } from 'react-i18next'; import { useTranslation } from 'react-i18next';
import { PluginV4 } from '@/app/infra/entities/plugin'; import { PluginV4 } from '@/app/infra/entities/plugin';
import { systemInfo } from '@/app/infra/http/HttpClient'; import { systemInfo } from '@/app/infra/http/HttpClient';
import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api'; import { ApiRespPluginSystemStatus } from '@/app/infra/entities/api';
import { set } from 'lodash'; import {
import { passiveEventSupported } from '@tanstack/react-table'; Select,
import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue } from '@radix-ui/react-select'; SelectContent,
SelectItem,
SelectTrigger,
SelectValue,
} from '@radix-ui/react-select';
import { useForm } from 'react-hook-form'; import { useForm } from 'react-hook-form';
import { zodResolver } from '@hookform/resolvers/zod'; import { zodResolver } from '@hookform/resolvers/zod';
import { number, z } from 'zod'; import { z } from 'zod';
import { DialogDescription } from '@radix-ui/react-dialog'; import { DialogDescription } from '@radix-ui/react-dialog';
import { import {
Form, Form,
@@ -58,7 +61,6 @@ import {
FormMessage, FormMessage,
} from '@/components/ui/form'; } from '@/components/ui/form';
enum PluginInstallStatus { enum PluginInstallStatus {
WAIT_INPUT = 'wait_input', WAIT_INPUT = 'wait_input',
ASK_CONFIRM = 'ask_confirm', ASK_CONFIRM = 'ask_confirm',
@@ -66,38 +68,16 @@ enum PluginInstallStatus {
ERROR = 'error', ERROR = 'error',
} }
export default function PluginConfigPage( export default function PluginConfigPage() {
{
editMode = false,
initMCPId,
onFormSubmit,
onFormCancel,
onMcpDeleted,
}:
{
editMode?: boolean;
initMCPId?: string;
onFormSubmit?: () => void;
onFormCancel?: () => void;
onMcpDeleted?: () => void;
} = {}
) {
const { t } = useTranslation(); const { t } = useTranslation();
const [activeTab, setActiveTab] = useState('installed'); const [activeTab, setActiveTab] = useState('installed');
const [modalOpen, setModalOpen] = useState(false); const [modalOpen, setModalOpen] = useState(false);
// const [sortModalOpen, setSortModalOpen] = useState(false);
const [installSource, setInstallSource] = useState<string>('local'); const [installSource, setInstallSource] = useState<string>('local');
const [installInfo, setInstallInfo] = useState<Record<string, any>>({}); // eslint-disable-line @typescript-eslint/no-explicit-any 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 [mcpSSEModalOpen, setMcpSSEModalOpen] = useState(false);
const [pluginInstallStatus, setPluginInstallStatus] = const [pluginInstallStatus, setPluginInstallStatus] =
useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT); useState<PluginInstallStatus>(PluginInstallStatus.WAIT_INPUT);
const [installError, setInstallError] = useState<string | null>(null); const [installError, setInstallError] = useState<string | null>(null);
const [mcpInstallError, setMcpInstallError] = useState<string | null>(null);
const [githubURL, setGithubURL] = useState(''); const [githubURL, setGithubURL] = useState('');
const [isDragOver, setIsDragOver] = useState(false); const [isDragOver, setIsDragOver] = useState(false);
const [pluginSystemStatus, setPluginSystemStatus] = const [pluginSystemStatus, setPluginSystemStatus] =
@@ -143,7 +123,9 @@ export default function PluginConfigPage(
z.object({ z.object({
name: z.string().min(1, { message: t('mcp.nameRequired') }), name: z.string().min(1, { message: t('mcp.nameRequired') }),
timeout: z.number().min(30, { message: t('mcp.timeoutMin30') }), 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') }), url: z.string().min(1, { message: t('mcp.requestURLRequired') }),
extra_args: z.array(getExtraArgSchema(t)).optional(), extra_args: z.array(getExtraArgSchema(t)).optional(),
}); });
@@ -219,22 +201,35 @@ export default function PluginConfigPage(
}); });
}, 1000); }, 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 pluginInstalledRef = useRef<PluginInstalledComponentRef>(null);
const mcpComponentRef = useRef<MCPComponentRef>(null); const mcpComponentRef = useRef<MCPComponentRef>(null);
const [mcpTesting, setMcpTesting] = useState(false); const [mcpTesting, setMcpTesting] = useState(false);
const [editingServerName, setEditingServerName] = useState<string | null>(
null,
);
const [isEditMode, setIsEditMode] = useState(false);
const [refreshKey, setRefreshKey] = useState(0);
// 强制清理 body 样式以修复 Dialog 关闭后点击失效的问题 // 强制清理 body 样式以修复 Dialog 关闭后点击失效的问题
useEffect(() => { useEffect(() => {
console.log('[Dialog Debug] States:', { mcpSSEModalOpen, modalOpen, showDeleteConfirmModal }); console.log('[Dialog Debug] States:', {
mcpSSEModalOpen,
modalOpen,
showDeleteConfirmModal,
});
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
console.log('[Dialog Debug] All dialogs closed, cleaning up body styles...'); console.log(
console.log('[Dialog Debug] Before cleanup - body.style.pointerEvents:', document.body.style.pointerEvents); '[Dialog Debug] All dialogs closed, cleaning up body styles...',
console.log('[Dialog Debug] Before cleanup - body.style.overflow:', document.body.style.overflow); );
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 = () => { const cleanup = () => {
// 强制移除 body 上可能残留的样式 // 强制移除 body 上可能残留的样式
@@ -249,12 +244,21 @@ export default function PluginConfigPage(
document.body.style.overflow = ''; document.body.style.overflow = '';
} }
console.log('[Dialog Debug] After cleanup - body.style.pointerEvents:', document.body.style.pointerEvents); console.log(
console.log('[Dialog Debug] After cleanup - body.style.overflow:', document.body.style.overflow); '[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); const computedStyle = window.getComputedStyle(document.body);
console.log('[Dialog Debug] Computed pointerEvents:', computedStyle.pointerEvents); console.log(
'[Dialog Debug] Computed pointerEvents:',
computedStyle.pointerEvents,
);
}; };
// 多次清理以确保覆盖 Radix 的设置 // 多次清理以确保覆盖 Radix 的设置
@@ -280,7 +284,9 @@ export default function PluginConfigPage(
const interval = setInterval(() => { const interval = setInterval(() => {
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
if (document.body.style.pointerEvents === 'none') { 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.removeProperty('pointer-events');
document.body.style.pointerEvents = ''; document.body.style.pointerEvents = '';
} }
@@ -294,10 +300,15 @@ export default function PluginConfigPage(
useEffect(() => { useEffect(() => {
const observer = new MutationObserver((mutations) => { const observer = new MutationObserver((mutations) => {
mutations.forEach((mutation) => { mutations.forEach((mutation) => {
if (mutation.type === 'attributes' && mutation.attributeName === 'style') { if (
mutation.type === 'attributes' &&
mutation.attributeName === 'style'
) {
if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) { if (!mcpSSEModalOpen && !modalOpen && !showDeleteConfirmModal) {
if (document.body.style.pointerEvents === 'none') { 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.removeProperty('pointer-events');
document.body.style.pointerEvents = ''; 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>) { async function handleFormSubmit(value: z.infer<typeof formSchema>) {
@@ -389,22 +455,30 @@ export default function PluginConfigPage(
timeout: value.timeout, timeout: value.timeout,
}; };
if (isEditMode && editingServerName) {
// 编辑模式:更新服务器
await httpClient.updateMCPServer(editingServerName, serverConfig);
toast.success(t('mcp.updateSuccess'));
} else {
// 创建模式:新建服务器
await httpClient.createMCPServer(serverConfig); await httpClient.createMCPServer(serverConfig);
toast.success(t('mcp.createSuccess')); toast.success(t('mcp.createSuccess'));
}
// 只有在异步操作成功后才关闭对话框 // 只有在异步操作成功后才关闭对话框
setMcpSSEModalOpen(false); setMcpSSEModalOpen(false);
// 重置表单 // 重置表单和状态
form.reset(); form.reset();
setExtraArgs([]); setExtraArgs([]);
setEditingServerName(null);
setIsEditMode(false);
// 调用回调通知父组件刷新 // 刷新服务器列表
onFormSubmit?.(); setRefreshKey((prev) => prev + 1);
} catch (error) { } catch (error) {
console.error('Failed to create MCP server:', error); console.error('Failed to save MCP server:', error);
toast.error(t('mcp.createFailed')); toast.error(isEditMode ? t('mcp.updateFailed') : t('mcp.createFailed'));
} }
} }
@@ -422,9 +496,9 @@ export default function PluginConfigPage(
extraArgsObj[arg.key] = arg.value; extraArgsObj[arg.key] = arg.value;
} }
}); });
httpClient.testMCPServer( httpClient
form.getValues('name'), .testMCPServer(form.getValues('name'))
).then((res) => { .then((res) => {
console.log(res); console.log(res);
toast.success(t('models.testSuccess')); toast.success(t('models.testSuccess'));
}) })
@@ -573,7 +647,6 @@ export default function PluginConfigPage(
return renderPluginConnectionErrorState(); return renderPluginConnectionErrorState();
} }
return ( return (
<div <div
className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`} className={`${styles.pageContainer} ${isDragOver ? 'bg-blue-50' : ''}`}
@@ -599,7 +672,10 @@ export default function PluginConfigPage(
{t('plugins.marketplace')} {t('plugins.marketplace')}
</TabsTrigger> </TabsTrigger>
)} )}
<TabsTrigger value="mcp-servers" className="px-6 py-4 cursor-pointer"> <TabsTrigger
value="mcp-servers"
className="px-6 py-4 cursor-pointer"
>
{t('mcp.title')} {t('mcp.title')}
</TabsTrigger> </TabsTrigger>
</TabsList> </TabsList>
@@ -618,7 +694,9 @@ export default function PluginConfigPage(
<DropdownMenuTrigger asChild> <DropdownMenuTrigger asChild>
<Button variant="default" className="px-6 py-4 cursor-pointer"> <Button variant="default" className="px-6 py-4 cursor-pointer">
<PlusIcon className="w-4 h-4" /> <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" /> <ChevronDownIcon className="ml-2 w-4 h-4" />
</Button> </Button>
</DropdownMenuTrigger> </DropdownMenuTrigger>
@@ -640,6 +718,10 @@ export default function PluginConfigPage(
<DropdownMenuItem <DropdownMenuItem
onClick={() => { onClick={() => {
setActiveTab('mcp-servers'); setActiveTab('mcp-servers');
setIsEditMode(false);
setEditingServerName(null);
form.reset();
setExtraArgs([]);
setMcpSSEModalOpen(true); setMcpSSEModalOpen(true);
}} }}
> >
@@ -691,11 +773,9 @@ export default function PluginConfigPage(
</TabsContent> </TabsContent>
<TabsContent value="mcp-servers"> <TabsContent value="mcp-servers">
<MCPMarketComponent <MCPMarketComponent
askInstallServer={(githubURL) => { key={refreshKey}
setMcpGithubURL(githubURL); onEditServer={(serverName) => {
setMcpMarketInstallModalOpen(true); loadServerForEdit(serverName);
// setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT);
setMcpInstallError(null);
}} }}
/> />
</TabsContent> </TabsContent>
@@ -790,7 +870,7 @@ export default function PluginConfigPage(
</DialogDescription> </DialogDescription>
<DialogFooter> <DialogFooter>
<Button <Button
variant='destructive' variant="destructive"
onClick={() => { onClick={() => {
deleteMCPServer(); deleteMCPServer();
setShowDeleteConfirmModal(false); setShowDeleteConfirmModal(false);
@@ -804,30 +884,37 @@ export default function PluginConfigPage(
<Dialog <Dialog
open={mcpSSEModalOpen} open={mcpSSEModalOpen}
onOpenChange={setMcpSSEModalOpen} onOpenChange={(open) => {
setMcpSSEModalOpen(open);
if (!open) {
// 关闭对话框时重置编辑状态
setIsEditMode(false);
setEditingServerName(null);
form.reset();
setExtraArgs([]);
}
}}
> >
<DialogContent> <DialogContent>
<DialogHeader> <DialogHeader>
<DialogTitle> <DialogTitle>
{t('mcp.createServer')} {isEditMode ? t('mcp.editServer') : t('mcp.createServer')}
</DialogTitle> </DialogTitle>
</DialogHeader> </DialogHeader>
<Form {...form}> <Form {...form}>
<form <form
onSubmit={form.handleSubmit(handleFormSubmit)} onSubmit={form.handleSubmit(handleFormSubmit)}
className='space-y-4' className="space-y-4"
> >
<div className='space-y-4'> <div className="space-y-4">
<FormField <FormField
control={form.control} control={form.control}
name='name' name="name"
render={({ field }) => ( render={({ field }) => (
<FormItem> <FormItem>
<FormLabel>{t('mcp.name')}</FormLabel> <FormLabel>{t('mcp.name')}</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -836,17 +923,12 @@ export default function PluginConfigPage(
<FormField <FormField
control={form.control} control={form.control}
name = 'url' name="url"
render={ render={({ field }) => (
({field}) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>{t('mcp.url')}</FormLabel>
{t('mcp.url')}
</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
@@ -855,44 +937,30 @@ export default function PluginConfigPage(
<FormField <FormField
control={form.control} control={form.control}
name='timeout' name="timeout"
render = { render={({ field }) => (
({field}) => (
<FormItem> <FormItem>
<FormLabel> <FormLabel>{t('mcp.timeout')}</FormLabel>
{t('mcp.timeout')}
</FormLabel>
<FormControl> <FormControl>
<Input <Input {...field} />
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
) )}
}
/> />
<FormField <FormField
control={form.control} control={form.control}
name='ssereadtimeout' name="ssereadtimeout"
render = { render={(field) => (
(field) =>
(
<FormItem> <FormItem>
<FormLabel> <FormLabel>{t('mcp.ssereadtimeout')}</FormLabel>
{t('mcp.ssereadtimeout')}
</FormLabel>
<FormControl> <FormControl>
<Input <Input placeholder={t('mcp.sseTimeout')} {...field} />
placeholder={t('mcp.sseTimeout')}
{...field}
/>
</FormControl> </FormControl>
<FormMessage /> <FormMessage />
</FormItem> </FormItem>
) )}
}
/> />
<FormItem> <FormItem>
@@ -951,7 +1019,11 @@ export default function PluginConfigPage(
</button> </button>
</div> </div>
))} ))}
<Button type="button" variant="outline" onClick={addExtraArg}> <Button
type="button"
variant="outline"
onClick={addExtraArg}
>
{t('models.addParameter')} {t('models.addParameter')}
</Button> </Button>
</div> </div>
@@ -962,7 +1034,7 @@ export default function PluginConfigPage(
</FormItem> </FormItem>
<DialogFooter> <DialogFooter>
{editMode && ( {isEditMode && (
<Button <Button
type="button" type="button"
variant="destructive" variant="destructive"
@@ -973,7 +1045,7 @@ export default function PluginConfigPage(
)} )}
<Button type="submit"> <Button type="submit">
{editMode ? t('common.save') : t('common.submit')} {isEditMode ? t('common.save') : t('common.submit')}
</Button> </Button>
<Button <Button
@@ -992,7 +1064,8 @@ export default function PluginConfigPage(
setMcpSSEModalOpen(false); setMcpSSEModalOpen(false);
form.reset(); form.reset();
setExtraArgs([]); setExtraArgs([]);
onFormCancel?.(); setIsEditMode(false);
setEditingServerName(null);
}} }}
> >
{t('common.cancel')} {t('common.cancel')}
+1 -1
View File
@@ -553,7 +553,7 @@ export class BackendClient extends BaseHttpClient {
} }
public installMCPServerFromSSE( public installMCPServerFromSSE(
source: {}, source: object,
): Promise<AsyncTaskCreatedResp> { ): Promise<AsyncTaskCreatedResp> {
return this.post('/api/v1/mcp/servers', { source }); return this.post('/api/v1/mcp/servers', { source });
} }
+13 -3
View File
@@ -11,7 +11,8 @@ function Dialog({
open, open,
...props ...props
}: React.ComponentProps<typeof DialogPrimitive.Root>) { }: React.ComponentProps<typeof DialogPrimitive.Root>) {
const handleOpenChange = React.useCallback((isOpen: boolean) => { const handleOpenChange = React.useCallback(
(isOpen: boolean) => {
onOpenChange?.(isOpen); onOpenChange?.(isOpen);
// 当对话框关闭时,确保清理 body 样式 // 当对话框关闭时,确保清理 body 样式
@@ -36,7 +37,9 @@ function Dialog({
document.body.style.removeProperty('overflow'); document.body.style.removeProperty('overflow');
}, 150); }, 150);
} }
}, [onOpenChange]); },
[onOpenChange],
);
// 使用 effect 监控 open 状态变化 // 使用 effect 监控 open 状态变化
React.useEffect(() => { React.useEffect(() => {
@@ -61,7 +64,14 @@ function Dialog({
} }
}, [open]); }, [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({ function DialogTrigger({
+1 -2
View File
@@ -1,4 +1,3 @@
const zhHans = { const zhHans = {
common: { common: {
login: '登录', login: '登录',
@@ -348,7 +347,7 @@ const zhHans = {
headersExample: '示例: Authorization: Bearer token123', headersExample: '示例: Authorization: Bearer token123',
enterTimeout: '输入超时时间,单位为毫秒', enterTimeout: '输入超时时间,单位为毫秒',
installFromSSE: '从SSE安装', installFromSSE: '从SSE安装',
sseTimeout:'SSE超时时间' sseTimeout: 'SSE超时时间',
}, },
pipelines: { pipelines: {
title: '流水线', title: '流水线',