mirror of
https://github.com/langbot-app/LangBot.git
synced 2026-06-12 16:56:02 +00: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(
|
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)
|
||||||
|
|
||||||
|
|||||||
@@ -304,7 +304,7 @@ export default function LLMForm({
|
|||||||
onLLMDeleted();
|
onLLMDeleted();
|
||||||
toast.success(t('models.deleteSuccess'));
|
toast.success(t('models.deleteSuccess'));
|
||||||
})
|
})
|
||||||
.catch ((err) => {
|
.catch((err) => {
|
||||||
toast.error(t('models.deleteError') + err.message);
|
toast.error(t('models.deleteError') + err.message);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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);
|
||||||
// 设置新的定时器
|
})
|
||||||
searchTimeout.current = setTimeout(() => {
|
.catch((error) => {
|
||||||
setNowPage(1);
|
console.error('Failed to fetch MCP servers:', error);
|
||||||
getServerList(1, keyword);
|
setLoading(false);
|
||||||
}, 500);
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
function getServerList(
|
// GitHub 市场功能暂时注释
|
||||||
page: number = nowPage,
|
// function onInputSearchKeyword(keyword: string) {
|
||||||
keyword: string = searchKeyword,
|
// setSearchKeyword(keyword);
|
||||||
sortBy: string = sortByValue,
|
// if (searchTimeout.current) {
|
||||||
sortOrder: string = sortOrderValue,
|
// clearTimeout(searchTimeout.current);
|
||||||
) {
|
// }
|
||||||
// setLoading(true);
|
// searchTimeout.current = setTimeout(() => {
|
||||||
|
// setNowPage(1);
|
||||||
|
// getServerList(1, keyword);
|
||||||
|
// }, 500);
|
||||||
|
// }
|
||||||
|
|
||||||
// 获取后端的 MCP Market 服务器列表
|
// function getServerList(
|
||||||
httpClient.getMCPServers().then(
|
// 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);
|
||||||
|
// // });
|
||||||
|
// }
|
||||||
|
|
||||||
|
// function handlePageChange(page: number) {
|
||||||
// spaceClient
|
// setNowPage(page);
|
||||||
// .getMCPMarketServers(page, pageSize, keyword, sortBy, sortOrder)
|
// getServerList(page);
|
||||||
// .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 handleSortChange(value: string) {
|
||||||
// repository = repository.replace('github.com/', '');
|
// const [newSortBy, newSortOrder] = value.split(',').map((s) => s.trim());
|
||||||
// }
|
// setSortByValue(newSortBy);
|
||||||
|
// setSortOrderValue(newSortOrder);
|
||||||
// const author = repository.split('/')[0];
|
// setNowPage(1);
|
||||||
// const name = repository.split('/')[1];
|
// getServerList(1, searchKeyword, newSortBy, newSortOrder);
|
||||||
// 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);
|
|
||||||
}
|
|
||||||
|
|
||||||
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>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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,
|
||||||
import { useForm } from 'react-hook-form';
|
SelectItem,
|
||||||
|
SelectTrigger,
|
||||||
|
SelectValue,
|
||||||
|
} from '@radix-ui/react-select';
|
||||||
|
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] =
|
||||||
@@ -134,7 +114,7 @@ export default function PluginConfigPage(
|
|||||||
});
|
});
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
const removeExtraArg = (index: number) => {
|
const removeExtraArg = (index: number) => {
|
||||||
const newArgs = extraArgs.filter((_, i) => i !== index);
|
const newArgs = extraArgs.filter((_, i) => i !== index);
|
||||||
setExtraArgs(newArgs);
|
setExtraArgs(newArgs);
|
||||||
form.setValue('extra_args', newArgs);
|
form.setValue('extra_args', newArgs);
|
||||||
@@ -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,
|
||||||
};
|
};
|
||||||
|
|
||||||
await httpClient.createMCPServer(serverConfig);
|
if (isEditMode && editingServerName) {
|
||||||
|
// 编辑模式:更新服务器
|
||||||
toast.success(t('mcp.createSuccess'));
|
await httpClient.updateMCPServer(editingServerName, serverConfig);
|
||||||
|
toast.success(t('mcp.updateSuccess'));
|
||||||
|
} else {
|
||||||
|
// 创建模式:新建服务器
|
||||||
|
await httpClient.createMCPServer(serverConfig);
|
||||||
|
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,9 +672,12 @@ export default function PluginConfigPage(
|
|||||||
{t('plugins.marketplace')}
|
{t('plugins.marketplace')}
|
||||||
</TabsTrigger>
|
</TabsTrigger>
|
||||||
)}
|
)}
|
||||||
<TabsTrigger value="mcp-servers" className="px-6 py-4 cursor-pointer">
|
<TabsTrigger
|
||||||
{t('mcp.title')}
|
value="mcp-servers"
|
||||||
</TabsTrigger>
|
className="px-6 py-4 cursor-pointer"
|
||||||
|
>
|
||||||
|
{t('mcp.title')}
|
||||||
|
</TabsTrigger>
|
||||||
</TabsList>
|
</TabsList>
|
||||||
|
|
||||||
<div className="flex flex-row justify-end items-center">
|
<div className="flex flex-row justify-end items-center">
|
||||||
@@ -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>
|
||||||
@@ -637,9 +715,13 @@ export default function PluginConfigPage(
|
|||||||
<PlusIcon className="w-4 h-4" />
|
<PlusIcon className="w-4 h-4" />
|
||||||
{t('mcp.installFromGithub')}
|
{t('mcp.installFromGithub')}
|
||||||
</DropdownMenuItem> */}
|
</DropdownMenuItem> */}
|
||||||
<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>
|
||||||
@@ -781,229 +861,222 @@ export default function PluginConfigPage(
|
|||||||
open={showDeleteConfirmModal}
|
open={showDeleteConfirmModal}
|
||||||
onOpenChange={setShowDeleteConfirmModal}
|
onOpenChange={setShowDeleteConfirmModal}
|
||||||
>
|
>
|
||||||
<DialogContent>
|
<DialogContent>
|
||||||
<DialogHeader>
|
<DialogHeader>
|
||||||
<DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle>
|
<DialogTitle>{t('plugins.confirmDeleteTitle')}</DialogTitle>
|
||||||
</DialogHeader>
|
</DialogHeader>
|
||||||
<DialogDescription>
|
<DialogDescription>
|
||||||
{t('plugins.deleteConfirmation')}
|
{t('plugins.deleteConfirmation')}
|
||||||
</DialogDescription>
|
</DialogDescription>
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
<Button
|
<Button
|
||||||
variant='destructive'
|
variant="destructive"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
deleteMCPServer();
|
deleteMCPServer();
|
||||||
setShowDeleteConfirmModal(false);
|
setShowDeleteConfirmModal(false);
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
{t('common.confirm')}
|
{t('common.confirm')}
|
||||||
</Button>
|
</Button>
|
||||||
</DialogFooter>
|
</DialogFooter>
|
||||||
</DialogContent>
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
|
|
||||||
<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>
|
|
||||||
<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) =>
|
|
||||||
(
|
|
||||||
<FormItem>
|
<FormItem>
|
||||||
<FormLabel>
|
<FormLabel>{t('mcp.name')}</FormLabel>
|
||||||
{t('mcp.ssereadtimeout')}
|
|
||||||
</FormLabel>
|
|
||||||
<FormControl>
|
<FormControl>
|
||||||
<Input
|
<Input {...field} />
|
||||||
placeholder={t('mcp.sseTimeout')}
|
|
||||||
{...field}
|
|
||||||
/>
|
|
||||||
</FormControl>
|
</FormControl>
|
||||||
<FormMessage/>
|
<FormMessage />
|
||||||
</FormItem>
|
</FormItem>
|
||||||
)
|
)}
|
||||||
}
|
|
||||||
/>
|
/>
|
||||||
|
|
||||||
<FormItem>
|
<FormField
|
||||||
<FormLabel>{t('models.extraParameters')}</FormLabel>
|
control={form.control}
|
||||||
<div className="space-y-2">
|
name="url"
|
||||||
{extraArgs.map((arg, index) => (
|
render={({ field }) => (
|
||||||
<div key={index} className="flex gap-2">
|
<FormItem>
|
||||||
<Input
|
<FormLabel>{t('mcp.url')}</FormLabel>
|
||||||
placeholder={t('models.keyName')}
|
<FormControl>
|
||||||
value={arg.key}
|
<Input {...field} />
|
||||||
onChange={(e) =>
|
</FormControl>
|
||||||
updateExtraArg(index, 'key', e.target.value)
|
<FormMessage />
|
||||||
}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<Select
|
/>
|
||||||
value={arg.type}
|
|
||||||
onValueChange={(value) =>
|
<FormField
|
||||||
updateExtraArg(index, 'type', value)
|
control={form.control}
|
||||||
}
|
name="timeout"
|
||||||
>
|
render={({ field }) => (
|
||||||
<SelectTrigger className="w-[120px] bg-[#ffffff] dark:bg-[#2a2a2e]">
|
<FormItem>
|
||||||
<SelectValue placeholder={t('models.type')} />
|
<FormLabel>{t('mcp.timeout')}</FormLabel>
|
||||||
</SelectTrigger>
|
<FormControl>
|
||||||
<SelectContent>
|
<Input {...field} />
|
||||||
<SelectItem value="string">
|
</FormControl>
|
||||||
{t('models.string')}
|
<FormMessage />
|
||||||
</SelectItem>
|
</FormItem>
|
||||||
<SelectItem value="number">
|
)}
|
||||||
{t('models.number')}
|
/>
|
||||||
</SelectItem>
|
|
||||||
<SelectItem value="boolean">
|
<FormField
|
||||||
{t('models.boolean')}
|
control={form.control}
|
||||||
</SelectItem>
|
name="ssereadtimeout"
|
||||||
</SelectContent>
|
render={(field) => (
|
||||||
</Select>
|
<FormItem>
|
||||||
<Input
|
<FormLabel>{t('mcp.ssereadtimeout')}</FormLabel>
|
||||||
placeholder={t('models.value')}
|
<FormControl>
|
||||||
value={arg.value}
|
<Input placeholder={t('mcp.sseTimeout')} {...field} />
|
||||||
onChange={(e) =>
|
</FormControl>
|
||||||
updateExtraArg(index, 'value', e.target.value)
|
<FormMessage />
|
||||||
}
|
</FormItem>
|
||||||
/>
|
)}
|
||||||
<button
|
/>
|
||||||
type="button"
|
|
||||||
className="p-2 hover:bg-gray-100 rounded"
|
<FormItem>
|
||||||
onClick={() => removeExtraArg(index)}
|
<FormLabel>{t('models.extraParameters')}</FormLabel>
|
||||||
>
|
<div className="space-y-2">
|
||||||
<svg
|
{extraArgs.map((arg, index) => (
|
||||||
xmlns="http://www.w3.org/2000/svg"
|
<div key={index} className="flex gap-2">
|
||||||
viewBox="0 0 24 24"
|
<Input
|
||||||
fill="currentColor"
|
placeholder={t('models.keyName')}
|
||||||
className="w-5 h-5 text-red-500"
|
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>
|
{t('models.addParameter')}
|
||||||
</svg>
|
</Button>
|
||||||
</button>
|
</div>
|
||||||
</div>
|
<FormDescription>
|
||||||
))}
|
{t('llm.extraParametersDescription')}
|
||||||
<Button type="button" variant="outline" onClick={addExtraArg}>
|
</FormDescription>
|
||||||
{t('models.addParameter')}
|
<FormMessage />
|
||||||
</Button>
|
</FormItem>
|
||||||
</div>
|
|
||||||
<FormDescription>
|
|
||||||
{t('llm.extraParametersDescription')}
|
|
||||||
</FormDescription>
|
|
||||||
<FormMessage />
|
|
||||||
</FormItem>
|
|
||||||
|
|
||||||
<DialogFooter>
|
<DialogFooter>
|
||||||
{editMode && (
|
{isEditMode && (
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="destructive"
|
variant="destructive"
|
||||||
onClick={() => setShowDeleteConfirmModal(true)}
|
onClick={() => setShowDeleteConfirmModal(true)}
|
||||||
>
|
>
|
||||||
{t('common.delete')}
|
{t('common.delete')}
|
||||||
</Button>
|
</Button>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
<Button type="submit">
|
<Button type="submit">
|
||||||
{editMode ? t('common.save') : t('common.submit')}
|
{isEditMode ? t('common.save') : t('common.submit')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => testMcp()}
|
onClick={() => testMcp()}
|
||||||
disabled={mcpTesting}
|
disabled={mcpTesting}
|
||||||
>
|
>
|
||||||
{t('common.test')}
|
{t('common.test')}
|
||||||
</Button>
|
</Button>
|
||||||
|
|
||||||
<Button
|
<Button
|
||||||
type="button"
|
type="button"
|
||||||
variant="outline"
|
variant="outline"
|
||||||
onClick={() => {
|
onClick={() => {
|
||||||
setMcpSSEModalOpen(false);
|
setMcpSSEModalOpen(false);
|
||||||
form.reset();
|
form.reset();
|
||||||
setExtraArgs([]);
|
setExtraArgs([]);
|
||||||
onFormCancel?.();
|
setIsEditMode(false);
|
||||||
}}
|
setEditingServerName(null);
|
||||||
>
|
}}
|
||||||
{t('common.cancel')}
|
>
|
||||||
</Button>
|
{t('common.cancel')}
|
||||||
</DialogFooter>
|
</Button>
|
||||||
</div>
|
</DialogFooter>
|
||||||
</form>
|
</div>
|
||||||
</Form>
|
</form>
|
||||||
</DialogContent>
|
</Form>
|
||||||
|
</DialogContent>
|
||||||
</Dialog>
|
</Dialog>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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 });
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -11,32 +11,35 @@ 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(
|
||||||
onOpenChange?.(isOpen);
|
(isOpen: boolean) => {
|
||||||
|
onOpenChange?.(isOpen);
|
||||||
|
|
||||||
// 当对话框关闭时,确保清理 body 样式
|
// 当对话框关闭时,确保清理 body 样式
|
||||||
if (!isOpen) {
|
if (!isOpen) {
|
||||||
// 立即清理
|
// 立即清理
|
||||||
document.body.style.removeProperty('pointer-events');
|
|
||||||
document.body.style.removeProperty('overflow');
|
|
||||||
|
|
||||||
// 延迟再次清理,确保覆盖 Radix 的设置
|
|
||||||
setTimeout(() => {
|
|
||||||
document.body.style.removeProperty('pointer-events');
|
document.body.style.removeProperty('pointer-events');
|
||||||
document.body.style.removeProperty('overflow');
|
document.body.style.removeProperty('overflow');
|
||||||
}, 0);
|
|
||||||
|
|
||||||
setTimeout(() => {
|
// 延迟再次清理,确保覆盖 Radix 的设置
|
||||||
document.body.style.removeProperty('pointer-events');
|
setTimeout(() => {
|
||||||
document.body.style.removeProperty('overflow');
|
document.body.style.removeProperty('pointer-events');
|
||||||
}, 50);
|
document.body.style.removeProperty('overflow');
|
||||||
|
}, 0);
|
||||||
|
|
||||||
setTimeout(() => {
|
setTimeout(() => {
|
||||||
document.body.style.removeProperty('pointer-events');
|
document.body.style.removeProperty('pointer-events');
|
||||||
document.body.style.removeProperty('overflow');
|
document.body.style.removeProperty('overflow');
|
||||||
}, 150);
|
}, 50);
|
||||||
}
|
|
||||||
}, [onOpenChange]);
|
setTimeout(() => {
|
||||||
|
document.body.style.removeProperty('pointer-events');
|
||||||
|
document.body.style.removeProperty('overflow');
|
||||||
|
}, 150);
|
||||||
|
}
|
||||||
|
},
|
||||||
|
[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,4 +1,3 @@
|
|||||||
|
|
||||||
const zhHans = {
|
const zhHans = {
|
||||||
common: {
|
common: {
|
||||||
login: '登录',
|
login: '登录',
|
||||||
@@ -335,20 +334,20 @@ const zhHans = {
|
|||||||
onlySupportGithub: '目前仅支持从Github安装MCP服务器',
|
onlySupportGithub: '目前仅支持从Github安装MCP服务器',
|
||||||
enterGithubLink: '输入Github仓库链接',
|
enterGithubLink: '输入Github仓库链接',
|
||||||
add: '添加',
|
add: '添加',
|
||||||
name:'名称',
|
name: '名称',
|
||||||
nameExplained:'用于区分不同的MCP服务器实例',
|
nameExplained: '用于区分不同的MCP服务器实例',
|
||||||
mcpDescription:'描述',
|
mcpDescription: '描述',
|
||||||
descriptionExplained:'简要描述这个MCP服务器的功能或用途',
|
descriptionExplained: '简要描述这个MCP服务器的功能或用途',
|
||||||
sseURL:'SSE URL',
|
sseURL: 'SSE URL',
|
||||||
sseHeaders:'SSE Headers',
|
sseHeaders: 'SSE Headers',
|
||||||
nameRequired:'名称不能为空',
|
nameRequired: '名称不能为空',
|
||||||
sseURLRequired:'SSE URL不能为空',
|
sseURLRequired: 'SSE URL不能为空',
|
||||||
enterSSELink:'输入SSE URL',
|
enterSSELink: '输入SSE URL',
|
||||||
timeoutRequired:'超时时间不能为空',
|
timeoutRequired: '超时时间不能为空',
|
||||||
headersExample:'示例: Authorization: Bearer token123',
|
headersExample: '示例: Authorization: Bearer token123',
|
||||||
enterTimeout:'输入超时时间,单位为毫秒',
|
enterTimeout: '输入超时时间,单位为毫秒',
|
||||||
installFromSSE:'从SSE安装',
|
installFromSSE: '从SSE安装',
|
||||||
sseTimeout:'SSE超时时间'
|
sseTimeout: 'SSE超时时间',
|
||||||
},
|
},
|
||||||
pipelines: {
|
pipelines: {
|
||||||
title: '流水线',
|
title: '流水线',
|
||||||
|
|||||||
Reference in New Issue
Block a user