diff --git a/pkg/api/http/controller/groups/mcp.py b/pkg/api/http/controller/groups/mcp.py index b92267e9..01f91f1d 100644 --- a/pkg/api/http/controller/groups/mcp.py +++ b/pkg/api/http/controller/groups/mcp.py @@ -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) diff --git a/web/src/app/home/models/component/llm-form/LLMForm.tsx b/web/src/app/home/models/component/llm-form/LLMForm.tsx index 2b663fa5..6d46da6d 100644 --- a/web/src/app/home/models/component/llm-form/LLMForm.tsx +++ b/web/src/app/home/models/component/llm-form/LLMForm.tsx @@ -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); }); } diff --git a/web/src/app/home/plugins/mcp-market/MCPMarketComponent.tsx b/web/src/app/home/plugins/mcp-market/MCPMarketComponent.tsx index 441f1853..a529d67f 100644 --- a/web/src/app/home/plugins/mcp-market/MCPMarketComponent.tsx +++ b/web/src/app/home/plugins/mcp-market/MCPMarketComponent.tsx @@ -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( - [], - ); - const [totalCount, setTotalCount] = useState(0); - const [nowPage, setNowPage] = useState(1); - const [searchKeyword, setSearchKeyword] = useState(''); + // const [marketServerList, setMarketServerList] = useState( + // [], + // ); + const [installedServers, setInstalledServers] = useState([]); + // const [totalCount, setTotalCount] = useState(0); + // const [nowPage, setNowPage] = useState(1); + // const [searchKeyword, setSearchKeyword] = useState(''); const [loading, setLoading] = useState(false); - const [sortByValue, setSortByValue] = useState('pushed_at'); - const [sortOrderValue, setSortOrderValue] = useState('DESC'); - const searchTimeout = useRef(null); - const pageSize = 12; + // const [sortByValue, setSortByValue] = useState('pushed_at'); + // const [sortOrderValue, setSortOrderValue] = useState('DESC'); + // const searchTimeout = useRef(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 (
-
+ {/* 已安装的服务器列表 */} +
+

+ {t('mcp.installedServers')} +

+
+ {loading ? ( +
+ {t('mcp.loading')} +
+ ) : installedServers.length === 0 ? ( +
+ {t('mcp.noInstalledServers')} +
+ ) : ( + installedServers.map((server, index) => ( +
+ { + if (onEditServer) { + onEditServer(server.name); + } + }} + onRefresh={fetchInstalledServers} + /> +
+ )) + )} +
+
+ + {/* GitHub 市场功能暂时注释 */} + {/*
- {/* 如果总页数大于5,则只显示5页,如果总页数小于5,则显示所有页 */} {(() => { const totalPages = Math.ceil(totalCount / pageSize); const maxVisiblePages = 5; @@ -255,7 +294,7 @@ export default function MCPMarketComponent({
)) )} -
+
*/} ); } diff --git a/web/src/app/home/plugins/page.tsx b/web/src/app/home/plugins/page.tsx index 29768703..38f70b85 100644 --- a/web/src/app/home/plugins/page.tsx +++ b/web/src/app/home/plugins/page.tsx @@ -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('local'); const [installInfo, setInstallInfo] = useState>({}); // 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.WAIT_INPUT); const [installError, setInstallError] = useState(null); - const [mcpInstallError, setMcpInstallError] = useState(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 | null>(null); - const [mcpInstallConfig, setMcpInstallConfig] = useState | null>(null); const pluginInstalledRef = useRef(null); const mcpComponentRef = useRef(null); const [mcpTesting, setMcpTesting] = useState(false); + const [editingServerName, setEditingServerName] = useState( + 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) { @@ -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 (
)} - - {t('mcp.title')} - + + {t('mcp.title')} +
@@ -618,7 +694,9 @@ export default function PluginConfigPage( @@ -637,9 +715,13 @@ export default function PluginConfigPage( {t('mcp.installFromGithub')} */} - { setActiveTab('mcp-servers'); + setIsEditMode(false); + setEditingServerName(null); + form.reset(); + setExtraArgs([]); setMcpSSEModalOpen(true); }} > @@ -691,11 +773,9 @@ export default function PluginConfigPage( { - setMcpGithubURL(githubURL); - setMcpMarketInstallModalOpen(true); - // setMcpInstallStatus(PluginInstallStatus.WAIT_INPUT); - setMcpInstallError(null); + key={refreshKey} + onEditServer={(serverName) => { + loadServerForEdit(serverName); }} /> @@ -781,229 +861,222 @@ export default function PluginConfigPage( open={showDeleteConfirmModal} onOpenChange={setShowDeleteConfirmModal} > - - - {t('plugins.confirmDeleteTitle')} - - - {t('plugins.deleteConfirmation')} - - - - - + + + {t('plugins.confirmDeleteTitle')} + + + {t('plugins.deleteConfirmation')} + + + + + - + { + setMcpSSEModalOpen(open); + if (!open) { + // 关闭对话框时重置编辑状态 + setIsEditMode(false); + setEditingServerName(null); + form.reset(); + setExtraArgs([]); + } + }} > - - - - {t('mcp.createServer')} - - -
- -
- ( - - {t('mcp.name')} - - - - - - )} - /> - - ( - - - {t('mcp.url')} - - - - - - - )} - /> - - ( - - - {t('mcp.timeout')} - - - - - - - ) - } - /> - - - ( + + + + {isEditMode ? t('mcp.editServer') : t('mcp.createServer')} + + + + +
+ ( - - {t('mcp.ssereadtimeout')} - + {t('mcp.name')} - + - + - ) - } + )} /> - - {t('models.extraParameters')} -
- {extraArgs.map((arg, index) => ( -
- - updateExtraArg(index, 'key', e.target.value) - } - /> - - - updateExtraArg(index, 'value', e.target.value) - } - /> - +
+ ))} + -
- ))} - -
- - {t('llm.extraParametersDescription')} - - - + {t('models.addParameter')} + +
+ + {t('llm.extraParametersDescription')} + + + - - {editMode && ( - - )} + + {isEditMode && ( + + )} - + - + - - -
- - - + + +
+ + + - - + + ); } diff --git a/web/src/app/infra/http/BackendClient.ts b/web/src/app/infra/http/BackendClient.ts index 8c533ab2..6c106a29 100644 --- a/web/src/app/infra/http/BackendClient.ts +++ b/web/src/app/infra/http/BackendClient.ts @@ -553,7 +553,7 @@ export class BackendClient extends BaseHttpClient { } public installMCPServerFromSSE( - source: {}, + source: object, ): Promise { return this.post('/api/v1/mcp/servers', { source }); } diff --git a/web/src/components/ui/dialog.tsx b/web/src/components/ui/dialog.tsx index 91823fd2..743c4b87 100644 --- a/web/src/components/ui/dialog.tsx +++ b/web/src/components/ui/dialog.tsx @@ -11,32 +11,35 @@ function Dialog({ open, ...props }: React.ComponentProps) { - 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 ; + return ( + + ); } function DialogTrigger({ diff --git a/web/src/i18n/locales/zh-Hans.ts b/web/src/i18n/locales/zh-Hans.ts index cac04c99..d4932e26 100644 --- a/web/src/i18n/locales/zh-Hans.ts +++ b/web/src/i18n/locales/zh-Hans.ts @@ -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: '流水线',