feat(web): enable MCP market configuration and service management
- Add market configuration API endpoints for creating, updating, and retrieving market configs - Add market MCP listing and detail endpoints with support for activated services - Implement MarketConfigModal component for configuring market connections with URL and API key - Implement McpServiceModal component for viewing and managing MCP services from markets - Add infinite scroll pagination for market sources and MCP services - Add market connection status indicators (connected/disconnected/connecting states) - Add i18n translations for market configuration UI (en and zh) - Update Market component to display market sources with connection management - Add MarketQuery type for market-specific API queries - Refactor market data structure to match backend API response format
This commit is contained in:
@@ -1,5 +1,5 @@
|
||||
import { request } from '@/utils/request'
|
||||
import type { Query, CustomToolItem, ExecuteData, MCPToolItem, InnerToolItem } from '@/views/ToolManagement/types'
|
||||
import type { Query, MarketQuery, CustomToolItem, ExecuteData, MCPToolItem, InnerToolItem } from '@/views/ToolManagement/types'
|
||||
|
||||
// 工具列表
|
||||
export const getTools = (data: Query) => {
|
||||
@@ -33,4 +33,44 @@ export const getToolDetail = (tool_id: string) => {
|
||||
}
|
||||
export const getToolMethods = (tool_id: string) => {
|
||||
return request.get(`/tools/${tool_id}/methods`)
|
||||
}
|
||||
|
||||
// MCP市场列表
|
||||
export const getMarketTools = (data: Query) => {
|
||||
return request.get('/mcp_markets/mcp_markets', data)
|
||||
}
|
||||
// 市场配置创建
|
||||
export const createMarketConfig = (values: {
|
||||
mcp_market_id: string;
|
||||
token: string;
|
||||
status: number;
|
||||
}) => {
|
||||
return request.post('/mcp_market_configs/mcp_market_config', values)
|
||||
}
|
||||
// 市场配置更新
|
||||
export const updateMarketConfig = (values: {
|
||||
mcp_market_config_id: string;
|
||||
token: string;
|
||||
status: number;
|
||||
}) => {
|
||||
return request.put(`/mcp_market_configs/${values.mcp_market_config_id}`, values)
|
||||
}
|
||||
// 市场根据id获取配置
|
||||
export const getMarketConfig = (mcp_market_id: string) => {
|
||||
return request.get(`/mcp_market_configs/mcp_market_id/${mcp_market_id}`)
|
||||
}
|
||||
// 市场MCP列表
|
||||
export const getMarketMCPs = (data: MarketQuery) => {
|
||||
return request.get('/mcp_market_configs/mcp_servers', data)
|
||||
}
|
||||
// 根据配置ID serverId 获取MCP服务详情
|
||||
export const getMarketMCPDetail = (data:{
|
||||
mcp_market_config_id: string;
|
||||
server_id: string;
|
||||
}) => {
|
||||
return request.get(`/mcp_market_configs/mcp_server`,data)
|
||||
}
|
||||
// 市场已激活MCP列表
|
||||
export const getMarketMCPsActivated = (data: MarketQuery) => {
|
||||
return request.get('/mcp_market_configs/operational_mcp_servers', data)
|
||||
}
|
||||
@@ -1948,7 +1948,20 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re
|
||||
path: 'Path',
|
||||
viewDetail: 'View Details',
|
||||
textLink: 'Test Connection',
|
||||
noResult: 'Processing results will be displayed here'
|
||||
noResult: 'Processing results will be displayed here',
|
||||
|
||||
marketConfig: 'Configure {{name}}',
|
||||
marketSaveAndConnect: 'Save & Connect',
|
||||
marketUrl: 'Market URL',
|
||||
marketUrlPlaceholder: 'Market URL',
|
||||
marketCopy: 'Copy',
|
||||
marketApiKeyOptional: 'Optional',
|
||||
marketApiKeyExtra: 'Some markets require an API Key to access the full service list',
|
||||
marketApiKeyPlaceholder: 'Enter API Key to access more services',
|
||||
marketConnectionStatus: 'Connection Status',
|
||||
marketConnected: '● Connected',
|
||||
marketDisconnected: '○ Disconnected',
|
||||
marketConnecting: 'Connecting to {{name}}...',
|
||||
},
|
||||
workflow: {
|
||||
coreNode: 'Core Nodes',
|
||||
|
||||
@@ -1945,7 +1945,20 @@ export const zh = {
|
||||
path: '路径',
|
||||
viewDetail: '查看详情',
|
||||
textLink: '测试连接',
|
||||
noResult: '处理结果将显示在这里'
|
||||
noResult: '处理结果将显示在这里',
|
||||
|
||||
marketConfig: '配置 {{name}}',
|
||||
marketSaveAndConnect: '保存并连接',
|
||||
marketUrl: '市场地址',
|
||||
marketUrlPlaceholder: '市场地址',
|
||||
marketCopy: '复制',
|
||||
marketApiKeyOptional: '可选',
|
||||
marketApiKeyExtra: '部分市场需要 API Key 才能获取完整的服务列表',
|
||||
marketApiKeyPlaceholder: '输入 API Key 以获取更多服务',
|
||||
marketConnectionStatus: '连接状态',
|
||||
marketConnected: '● 已连接',
|
||||
marketDisconnected: '○ 未连接',
|
||||
marketConnecting: '正在连接 {{name}}...',
|
||||
},
|
||||
workflow: {
|
||||
coreNode: '核心节点',
|
||||
|
||||
@@ -1,122 +1,259 @@
|
||||
import React, { useState, useRef, type ReactNode } from 'react';
|
||||
import { Input, Button, Spin, App } from 'antd';
|
||||
import React, { useState, useRef, useEffect, useCallback, type ReactNode } from 'react';
|
||||
import { Input, Button, App, Card, Space, Skeleton, Tag } from 'antd';
|
||||
import { SearchOutlined, SettingOutlined, GlobalOutlined, SyncOutlined } from '@ant-design/icons';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import InfiniteScroll from 'react-infinite-scroll-component';
|
||||
import MarketConfigModal, { type MarketConfigModalRef } from './components/MarketConfigModal';
|
||||
|
||||
import McpServiceModal from './components/McpServiceModal';
|
||||
import type { McpServiceModalRef } from './types';
|
||||
import { getMarketTools, getMarketConfig, getMarketMCPs, getMarketMCPDetail, getMarketMCPsActivated } from '@/api/tools';
|
||||
interface MarketSource {
|
||||
id: string;
|
||||
name: string;
|
||||
category: string;
|
||||
icon: string;
|
||||
logo_url: string;
|
||||
url: string;
|
||||
desc: string;
|
||||
apiKey: string;
|
||||
description: string;
|
||||
api_key?: string;
|
||||
connected: boolean;
|
||||
mcpCount: number;
|
||||
mcp_count: number;
|
||||
created_at?: number;
|
||||
created_by?: string;
|
||||
}
|
||||
|
||||
interface MarketMcp {
|
||||
id: string;
|
||||
name: string;
|
||||
provider: string;
|
||||
type: string;
|
||||
desc: string;
|
||||
downloads?: string;
|
||||
stars?: string;
|
||||
icon: string;
|
||||
configTemplate: any;
|
||||
chinese_name?: string;
|
||||
description: string;
|
||||
logo_url: string;
|
||||
publisher: string;
|
||||
categories?: string[];
|
||||
tags?: string[];
|
||||
view_count?: number;
|
||||
activated?: boolean;
|
||||
locales?: {
|
||||
[lang: string]: {
|
||||
name: string;
|
||||
description: string;
|
||||
};
|
||||
};
|
||||
}
|
||||
|
||||
interface MarketCategory {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
}
|
||||
|
||||
interface MarketApiResponse {
|
||||
items: MarketSource[];
|
||||
}
|
||||
|
||||
const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () => {
|
||||
const { t } = useTranslation();
|
||||
const { t, i18n } = useTranslation();
|
||||
const { message } = App.useApp();
|
||||
|
||||
const getLocaleField = (mcp: MarketMcp, field: 'name' | 'description') => {
|
||||
const lang = i18n.language?.startsWith('zh') ? 'zh' : 'en';
|
||||
return mcp.locales?.[lang]?.[field] || mcp[field] || '';
|
||||
};
|
||||
const [loading, setLoading] = useState(false);
|
||||
const [selectedSource, setSelectedSource] = useState<string | null>(null);
|
||||
const marketConfigModalRef = useRef<MarketConfigModalRef>(null);
|
||||
const [marketSources, setMarketSources] = useState<MarketSource[]>([
|
||||
{ id: 'smithery', name: 'Smithery', category: 'official', icon: '🔧', url: 'https://mcp.smithery.ai', desc: '官方 MCP 服务市场,提供丰富的 MCP 服务', apiKey: '', connected: false, mcpCount: 2847 },
|
||||
{ id: 'mcpmarket', name: 'MCP Market', category: 'official', icon: '🏪', url: 'https://mcpmarket.com', desc: '综合性 MCP 市场平台', apiKey: '', connected: false, mcpCount: 1523 },
|
||||
{ id: 'glama', name: 'Glama.ai MCP', category: 'official', icon: '✨', url: 'https://glama.ai/mcp', desc: 'Glama AI 提供的 MCP 服务集合', apiKey: '', connected: false, mcpCount: 892 },
|
||||
{ id: 'github-mcp', name: 'modelcontextprotocol/servers', category: 'official', icon: '🐙', url: 'https://github.com/modelcontextprotocol/servers', desc: 'GitHub 官方 MCP 服务器仓库', apiKey: '', connected: true, mcpCount: 156 },
|
||||
{ id: 'aliyun-bailian', name: '阿里云百炼 MCP', category: 'china-cloud', icon: '☁️', url: 'https://bailian.console.aliyun.com/mcp', desc: '阿里云百炼平台 MCP 市场', apiKey: '', connected: false, mcpCount: 423 },
|
||||
{ id: 'modelscope', name: '魔搭社区 MCP', category: 'china-cloud', icon: '🎭', url: 'https://modelscope.cn/mcp', desc: '阿里达摩院魔搭社区 MCP 市场', apiKey: '', connected: false, mcpCount: 312 },
|
||||
]);
|
||||
|
||||
const [categories] = useState<MarketCategory[]>([
|
||||
{ id: 'official', name: '官方/综合', icon: '🌐' },
|
||||
{ id: 'china-cloud', name: '国内云', icon: '☁️' },
|
||||
{ id: 'community', name: '社区/垂直', icon: '👥' }
|
||||
]);
|
||||
|
||||
const [mcpCache, setMcpCache] = useState<Record<string, MarketMcp[]>>({
|
||||
'github-mcp': [
|
||||
{ id: 'gh-1', name: 'Fetch', provider: 'modelcontextprotocol', type: 'Hosted', desc: '使用浏览器模拟大型语言模型检索和处理网页内容', downloads: '203.7m', stars: '308.2k', icon: '🌐', configTemplate: {} },
|
||||
{ id: 'gh-2', name: 'Filesystem', provider: 'modelcontextprotocol', type: 'Local', desc: '安全的文件系统操作,支持读写文件和目录管理', downloads: '156.2m', stars: '245.1k', icon: '📁', configTemplate: {} },
|
||||
{ id: 'gh-3', name: 'GitHub', provider: 'modelcontextprotocol', type: 'Hosted', desc: 'GitHub API 集成,支持仓库、Issue、PR 等操作', downloads: '89.4m', stars: '178.3k', icon: '🐙', configTemplate: {} },
|
||||
]
|
||||
});
|
||||
|
||||
const mcpServiceModalRef = useRef<McpServiceModalRef>(null);
|
||||
const [marketSources, setMarketSources] = useState<MarketSource[]>([]);
|
||||
const [categories, setCategories] = useState<MarketCategory[]>([]);
|
||||
const [mcpCache, setMcpCache] = useState<Record<string, MarketMcp[]>>({});
|
||||
const [mcpTotal, setMcpTotal] = useState(0);
|
||||
const [searchKeyword, setSearchKeyword] = useState('');
|
||||
const [configIdMap, setConfigIdMap] = useState<Record<string, string>>({});
|
||||
const [hasMore, setHasMore] = useState(false);
|
||||
const [activatedMcps, setActivatedMcps] = useState<string[]>([]);
|
||||
const [currentPage, setCurrentPage] = useState(1);
|
||||
const pageSize = 20;
|
||||
|
||||
const handleSelectSource = (sourceId: string) => {
|
||||
setSelectedSource(sourceId);
|
||||
};
|
||||
|
||||
const handleRefresh = (sourceId: string) => {
|
||||
setLoading(true);
|
||||
setTimeout(() => {
|
||||
// 模拟刷新数据
|
||||
const source = marketSources.find(s => s.id === sourceId);
|
||||
if (source) {
|
||||
message.success(`${source.name} 列表已刷新`);
|
||||
// 获取市场数据
|
||||
useEffect(() => {
|
||||
const fetchMarketData = async () => {
|
||||
setLoading(true);
|
||||
try {
|
||||
const response = await getMarketTools({}) as MarketApiResponse;
|
||||
if (response?.items && Array.isArray(response.items)) {
|
||||
setMarketSources(response.items);
|
||||
|
||||
// 根据 category 字段分组
|
||||
const categoryMap = new Map<string, MarketCategory>();
|
||||
response.items.forEach(item => {
|
||||
if (item.category && !categoryMap.has(item.category)) {
|
||||
categoryMap.set(item.category, {
|
||||
id: item.category,
|
||||
name: item.category
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
setCategories(Array.from(categoryMap.values()));
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取市场数据失败:', error);
|
||||
message.error('获取市场数据失败');
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
};
|
||||
|
||||
fetchMarketData();
|
||||
}, [message]);
|
||||
|
||||
const fetchMcpList = async (sourceId: string, page = 1, append = false) => {
|
||||
setLoading(true);
|
||||
try {
|
||||
let configId = configIdMap[sourceId];
|
||||
|
||||
// 如果没有缓存 configId,先获取配置
|
||||
if (!configId) {
|
||||
const config: any = await getMarketConfig(sourceId);
|
||||
if (config?.id) {
|
||||
configId = config.id;
|
||||
setConfigIdMap(prev => ({ ...prev, [sourceId]: configId }));
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
// 第一次加载时获取已激活列表
|
||||
let activatedIds: string[] = activatedMcps;
|
||||
if (page === 1 && !append) {
|
||||
const activatedRes: any = await getMarketMCPsActivated({ mcp_market_config_id: configId });
|
||||
if (activatedRes && Array.isArray(activatedRes)) {
|
||||
activatedIds = activatedRes.map((item: any) => item.id);
|
||||
setActivatedMcps(activatedIds);
|
||||
}
|
||||
}
|
||||
|
||||
const res: any = await getMarketMCPs({ mcp_market_config_id: configId, page, pagesize: pageSize });
|
||||
if (res?.items && Array.isArray(res.items)) {
|
||||
// 标记已激活的 MCP
|
||||
const mcpsWithActivated = res.items.map((item: MarketMcp) => ({
|
||||
...item,
|
||||
activated: activatedIds.includes(item.id)
|
||||
}));
|
||||
|
||||
setMcpCache(prev => ({
|
||||
...prev,
|
||||
[sourceId]: append ? [...(prev[sourceId] || []), ...mcpsWithActivated] : mcpsWithActivated
|
||||
}));
|
||||
}
|
||||
if (res?.page) {
|
||||
setMcpTotal(res.page.total || 0);
|
||||
setHasMore(!!res.page.has_next);
|
||||
setCurrentPage(res.page.page || page);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取 MCP 列表失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}, 600);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenConfig = (sourceId: string) => {
|
||||
const loadMore = useCallback(() => {
|
||||
if (!selectedSource || loading) return;
|
||||
fetchMcpList(selectedSource, currentPage + 1, true);
|
||||
}, [selectedSource, currentPage, loading]);
|
||||
|
||||
const handleSelectSource = async (sourceId: string) => {
|
||||
setSelectedSource(sourceId);
|
||||
setSearchKeyword('');
|
||||
setCurrentPage(1);
|
||||
setHasMore(false);
|
||||
setMcpTotal(0);
|
||||
|
||||
// 如果缓存中已有数据,直接使用
|
||||
if (mcpCache[sourceId]) return;
|
||||
|
||||
await fetchMcpList(sourceId, 1);
|
||||
};
|
||||
|
||||
const handleRefresh = async (sourceId: string) => {
|
||||
// 清除缓存,重新从第一页加载
|
||||
setMcpCache(prev => {
|
||||
const next = { ...prev };
|
||||
delete next[sourceId];
|
||||
return next;
|
||||
});
|
||||
setCurrentPage(1);
|
||||
await fetchMcpList(sourceId, 1);
|
||||
const source = marketSources.find(s => s.id === sourceId);
|
||||
if (source) {
|
||||
message.success(`${source.name} 列表已刷新`);
|
||||
}
|
||||
};
|
||||
|
||||
const handleOpenConfig = async (sourceId: string) => {
|
||||
const source = marketSources.find(s => s.id === sourceId);
|
||||
if (!source) return;
|
||||
try {
|
||||
const config: any = await getMarketConfig(sourceId);
|
||||
marketConfigModalRef.current?.handleOpen({
|
||||
...source,
|
||||
connected: config?.status === 1,
|
||||
token: config?.token || '',
|
||||
configId: config?.id || '',
|
||||
});
|
||||
} catch {
|
||||
marketConfigModalRef.current?.handleOpen(source);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnect = (sourceId: string, apiKey: string) => {
|
||||
// 更新市场源状态
|
||||
const handleOpenMcpServiceModal = async (mcp: MarketMcp) => {
|
||||
if (!selectedSource || !configIdMap[selectedSource]) return;
|
||||
try {
|
||||
const detail: any = await getMarketMCPDetail({
|
||||
mcp_market_config_id: configIdMap[selectedSource],
|
||||
server_id: mcp.id,
|
||||
});
|
||||
const toolItem = {
|
||||
name: detail.name,
|
||||
description: detail.description,
|
||||
config_data: {
|
||||
server_url: detail.servers?.[0]?.url || '',
|
||||
connection_config: {
|
||||
auth_type: 'none',
|
||||
timeout: 30,
|
||||
headers: {},
|
||||
},
|
||||
},
|
||||
};
|
||||
mcpServiceModalRef.current?.handleOpen(toolItem as any);
|
||||
} catch (error) {
|
||||
console.error('获取 MCP 服务详情失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const handleConnect = async (sourceId: string, configId: string) => {
|
||||
// 更新市场源状态,缓存 configId
|
||||
setMarketSources(prev => prev.map(source => {
|
||||
if (source.id === sourceId) {
|
||||
return {
|
||||
...source,
|
||||
apiKey,
|
||||
connected: true
|
||||
};
|
||||
return { ...source, connected: true };
|
||||
}
|
||||
return source;
|
||||
}));
|
||||
setConfigIdMap(prev => ({ ...prev, [sourceId]: configId }));
|
||||
|
||||
// 模拟获取MCP列表
|
||||
setTimeout(() => {
|
||||
const source = marketSources.find(s => s.id === sourceId);
|
||||
if (source && !mcpCache[sourceId]) {
|
||||
// 生成模拟数据
|
||||
const mockData: MarketMcp[] = [
|
||||
{ id: `${sourceId}-1`, name: `${source.name} 服务 1`, provider: source.name, type: 'Hosted', desc: `来自 ${source.name} 的 MCP 服务`, downloads: '10.2m', stars: '23.4k', icon: '🔧', configTemplate: {} },
|
||||
{ id: `${sourceId}-2`, name: `${source.name} 服务 2`, provider: source.name, type: 'Local', desc: `来自 ${source.name} 的本地 MCP 服务`, downloads: '8.5m', stars: '18.7k', icon: '⚙️', configTemplate: {} }
|
||||
];
|
||||
setMcpCache(prev => ({
|
||||
...prev,
|
||||
[sourceId]: mockData
|
||||
}));
|
||||
// 用 configId 获取第一页 MCP 列表
|
||||
try {
|
||||
const res: any = await getMarketMCPs({ mcp_market_config_id: configId, page: 1, pagesize: pageSize });
|
||||
if (res?.items && Array.isArray(res.items)) {
|
||||
setMcpCache(prev => ({ ...prev, [sourceId]: res.items }));
|
||||
}
|
||||
message.success(`已连接 ${source?.name}`);
|
||||
}, 800);
|
||||
if (res?.page) {
|
||||
setMcpTotal(res.page.total || 0);
|
||||
setHasMore(!!res.page.has_next);
|
||||
setCurrentPage(1);
|
||||
}
|
||||
} catch (error) {
|
||||
console.error('获取 MCP 列表失败:', error);
|
||||
}
|
||||
};
|
||||
|
||||
const renderSourceDetail = () => {
|
||||
@@ -134,38 +271,45 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
|
||||
if (!source) return null;
|
||||
|
||||
const mcpList = mcpCache[selectedSource] || [];
|
||||
const filteredList = mcpList.filter(mcp =>
|
||||
mcp.name.toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
||||
mcp.desc.toLowerCase().includes(searchKeyword.toLowerCase())
|
||||
);
|
||||
const filteredList = mcpList.filter(mcp => {
|
||||
const name = getLocaleField(mcp, 'name');
|
||||
const desc = getLocaleField(mcp, 'description');
|
||||
return name.toLowerCase().includes(searchKeyword.toLowerCase()) ||
|
||||
desc.toLowerCase().includes(searchKeyword.toLowerCase());
|
||||
});
|
||||
|
||||
return (
|
||||
<>
|
||||
<div className="rb:flex rb:justify-between rb:items-start rb:pb-6 rb:border-b rb:border-gray-200 rb:mb-6">
|
||||
<div className="rb:flex rb:gap-4">
|
||||
<div className="rb:text-5xl rb:w-16 rb:h-16 rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:rounded-xl rb:flex-shrink-0">
|
||||
{source.icon}
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:pb-0">
|
||||
<div className="rb:flex rb:items-center rb:gap-4">
|
||||
<div className="rb:w-10 rb:h-10 rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:rounded-xl rb:flex-shrink-0 rb:overflow-hidden">
|
||||
{source.logo_url ? (
|
||||
<img
|
||||
src={source.logo_url}
|
||||
alt={source.name}
|
||||
className="rb:w-full rb:h-full rb:object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
const parent = e.currentTarget.parentElement;
|
||||
if (parent) {
|
||||
parent.innerHTML = '🏪';
|
||||
parent.style.fontSize = '48px';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="rb:text-5xl">🏪</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="rb:flex-1">
|
||||
<h2 className="rb:text-xl rb:font-semibold rb:text-gray-900 rb:mb-2">{source.name}</h2>
|
||||
<p className="rb:text-sm rb:text-gray-600 rb:leading-relaxed">{source.desc}</p>
|
||||
<div className="rb:flex rb:items-center rb:flex-1">
|
||||
<h2 className="rb:text-xl rb:font-semibold rb:text-gray-900 rb:mb-2 rb:mr-2">{source.name}</h2>
|
||||
可用 MCP 服务 <span className="rb:text-gray-600 rb:font-normal">({mcpTotal})</span>
|
||||
{/* <p className="rb:text-sm rb:text-gray-600 rb:leading-relaxed">{source.description}</p> */}
|
||||
</div>
|
||||
</div>
|
||||
<div className="rb:flex rb:gap-3">
|
||||
<Button icon={<SettingOutlined />} onClick={() => handleOpenConfig(selectedSource)}>
|
||||
配置
|
||||
</Button>
|
||||
<Button type="primary" icon={<GlobalOutlined />} onClick={() => window.open(source.url, '_blank')}>
|
||||
前往市场
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:mt-6">
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:mb-5">
|
||||
<h3 className="rb:text-base rb:font-semibold rb:text-gray-900 rb:m-0">
|
||||
可用 MCP 服务 <span className="rb:text-gray-600 rb:font-normal">({mcpList.length})</span>
|
||||
</h3>
|
||||
<div className="rb:flex rb:gap-3">
|
||||
<div className="rb:flex rb:gap-3 rb:items-center">
|
||||
{source.connected && (
|
||||
<Button size="small" icon={<SyncOutlined />} onClick={() => handleRefresh(selectedSource)}>
|
||||
@@ -182,56 +326,83 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
|
||||
/>
|
||||
)}
|
||||
</div>
|
||||
<Button icon={<SettingOutlined />} onClick={() => handleOpenConfig(selectedSource)}>
|
||||
配置
|
||||
</Button>
|
||||
<Button type="primary" icon={<GlobalOutlined />} onClick={() => window.open(source.url, '_blank')}>
|
||||
前往市场
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div className="rb:mt-6">
|
||||
{mcpList.length > 0 ? (
|
||||
<Spin spinning={loading}>
|
||||
<div className="rb:grid rb:grid-cols-1 md:rb:grid-cols-2 lg:rb:grid-cols-3 rb:gap-4">
|
||||
{filteredList.map(mcp => (
|
||||
<div id="mcpScrollableDiv" className="rb:overflow-y-auto rb:h-[calc(100vh-260px)]">
|
||||
<InfiniteScroll
|
||||
dataLength={filteredList.length}
|
||||
next={loadMore}
|
||||
hasMore={hasMore}
|
||||
loader={<Skeleton active paragraph={{ rows: 2 }} className="rb:mt-4" />}
|
||||
scrollableTarget="mcpScrollableDiv"
|
||||
>
|
||||
<div className="rb:grid rb:grid-cols-3 rb:gap-4">
|
||||
{filteredList.map(mcp => (
|
||||
<div
|
||||
key={mcp.id}
|
||||
className="rb:bg-white rb:border rb:border-gray-200 rb:rounded-lg rb:p-4 rb:transition-all rb:duration-200 hover:rb:shadow-lg hover:rb:border-gray-300"
|
||||
className="rb:bg-white rb:border rb:border-gray-200 rb:rounded-lg rb:p-4 rb:pb-2 rb:transition-all rb:duration-200 hover:rb:shadow-lg hover:rb:border-gray-300"
|
||||
>
|
||||
<div className="rb:flex rb:justify-between rb:items-center rb:mb-3">
|
||||
<div className="rb:text-3xl rb:w-12 rb:h-12 rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:rounded-lg">
|
||||
{mcp.icon}
|
||||
<div className="rb:w-12 rb:h-12 rb:flex rb:items-center rb:justify-center rb:bg-gray-50 rb:rounded-lg rb:overflow-hidden">
|
||||
{mcp.logo_url ? (
|
||||
<img
|
||||
src={mcp.logo_url}
|
||||
alt={getLocaleField(mcp, 'name')}
|
||||
className="rb:w-full rb:h-full rb:object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
const parent = e.currentTarget.parentElement;
|
||||
if (parent) {
|
||||
parent.innerHTML = '🔧';
|
||||
parent.style.fontSize = '24px';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="rb:text-3xl">🔧</span>
|
||||
)}
|
||||
</div>
|
||||
<span className={`rb:px-2 rb:py-1 rb:rounded rb:text-xs rb:font-medium ${
|
||||
mcp.type === 'Hosted'
|
||||
? 'rb:bg-blue-50 rb:text-blue-700'
|
||||
: 'rb:bg-gray-100 rb:text-gray-600'
|
||||
}`}>
|
||||
{mcp.type}
|
||||
</span>
|
||||
{mcp.categories?.[0] && (
|
||||
<span className="rb:px-2 rb:py-1 rb:rounded rb:text-xs rb:font-medium rb:bg-blue-50 rb:text-blue-700">
|
||||
{mcp.categories[0]}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<h3 className="rb:text-base rb:font-semibold rb:text-gray-900 rb:mb-1">{mcp.name}</h3>
|
||||
{mcp.provider && (
|
||||
<h3 className="rb:text-base rb:font-semibold rb:text-gray-900 rb:mb-1">{getLocaleField(mcp, 'name')}</h3>
|
||||
{mcp.publisher && (
|
||||
<div className="rb:mb-2">
|
||||
<span className="rb:text-xs rb:text-gray-500">@ {mcp.provider}</span>
|
||||
<span className="rb:text-xs rb:text-gray-500">{mcp.publisher.startsWith('@') ? mcp.publisher : `@${mcp.publisher}`}</span>
|
||||
</div>
|
||||
)}
|
||||
<p className="rb:text-sm rb:text-gray-600 rb:leading-relaxed rb:mb-3 rb:min-h-[42px]">{mcp.desc}</p>
|
||||
<p className="rb:text-sm rb:text-gray-600 rb:line-clamp-2 rb:mb-3 rb:min-h-10">{getLocaleField(mcp, 'description')}</p>
|
||||
<div className="rb:flex rb:gap-4 rb:mb-3 rb:pt-3 rb:border-t rb:border-gray-100">
|
||||
{mcp.downloads && (
|
||||
{mcp.view_count != null && (
|
||||
<span className="rb:flex rb:items-center rb:gap-1 rb:text-xs rb:text-gray-500">
|
||||
<GlobalOutlined /> {mcp.downloads}
|
||||
</span>
|
||||
)}
|
||||
{mcp.stars && (
|
||||
<span className="rb:flex rb:items-center rb:gap-1 rb:text-xs rb:text-gray-500">
|
||||
⭐ {mcp.stars}
|
||||
<GlobalOutlined /> {mcp.view_count.toLocaleString()}
|
||||
</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="rb:flex rb:justify-end">
|
||||
<Button type="primary" size="small">
|
||||
<div className={`rb:flex rb:items-center ${mcp.activated ? 'rb:justify-between' : 'rb:justify-end'}`}>
|
||||
{mcp.activated && <Tag color="success">已激活</Tag>}
|
||||
<Button type="primary" size="small" onClick={() => handleOpenMcpServiceModal(mcp)}>
|
||||
+ 添加
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</Spin>
|
||||
</div>
|
||||
</InfiniteScroll>
|
||||
</div>
|
||||
) : (
|
||||
<div className="rb:flex rb:flex-col rb:items-center rb:justify-center rb:py-16 rb:text-center">
|
||||
<div className="rb:text-6xl rb:mb-4">{source.connected ? '📭' : '🔌'}</div>
|
||||
@@ -254,50 +425,76 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
|
||||
};
|
||||
|
||||
return (
|
||||
<div className="rb:flex rb:gap-4 rb:h-[calc(100vh-178px)]">
|
||||
<div className="rb:flex rb:gap-4 rb:h-[calc(100vh-138px)]">
|
||||
{/* 左侧市场源列表 */}
|
||||
<div className="rb:w-70 rb:bg-white rb:rounded-lg rb:border rb:border-gray-200 rb:overflow-y-auto rb:flex-shrink-0">
|
||||
<div className="rb:p-4 rb:border-b rb:border-gray-200">
|
||||
<span className="rb:text-base rb:font-semibold rb:text-gray-900">MCP 市场</span>
|
||||
</div>
|
||||
{categories.map(cat => (
|
||||
<div key={cat.id} className="rb:py-3 rb:border-b rb:border-gray-100 last:rb:border-b-0">
|
||||
<div className="rb:flex rb:items-center rb:gap-2 rb:px-4 rb:py-2 rb:text-xs rb:font-medium rb:text-gray-500 rb:uppercase">
|
||||
<span className="rb:text-sm">{cat.icon}</span>
|
||||
<span>{cat.name}</span>
|
||||
</div>
|
||||
<div className="rb:px-2 rb:py-1">
|
||||
{marketSources
|
||||
.filter(s => s.category === cat.id)
|
||||
.map(source => (
|
||||
<div
|
||||
key={source.id}
|
||||
className={`rb:flex rb:items-center rb:gap-2 rb:px-3 rb:py-2.5 rb:rounded-md rb:cursor-pointer rb:transition-all rb:relative ${
|
||||
selectedSource === source.id
|
||||
? 'rb:bg-blue-50 rb:text-blue-600'
|
||||
: 'hover:rb:bg-gray-50'
|
||||
}`}
|
||||
onClick={() => handleSelectSource(source.id)}
|
||||
>
|
||||
<span className="rb:text-lg rb:flex-shrink-0">{source.icon}</span>
|
||||
<span className="rb:flex-1 rb:text-sm rb:font-medium rb:overflow-hidden rb:text-ellipsis rb:whitespace-nowrap">
|
||||
{source.name}
|
||||
</span>
|
||||
<span className="rb:text-xs rb:text-gray-500 rb:px-1.5 rb:py-0.5 rb:bg-gray-100 rb:rounded-full">
|
||||
{source.mcpCount}
|
||||
</span>
|
||||
{source.connected && (
|
||||
<span className="rb:text-green-500 rb:text-[8px] rb:ml-1">●</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</div>
|
||||
</div>
|
||||
))}
|
||||
<div className="rb:w-80 rb:h-full rb:overflow-y-auto">
|
||||
<Space size={12} direction="vertical" className="rb:w-full">
|
||||
{categories.map(cat => (
|
||||
<Card
|
||||
key={cat.id}
|
||||
type="inner"
|
||||
title={
|
||||
<div className="rb:flex rb:items-center rb:gap-2">
|
||||
<span>{cat.name}</span>
|
||||
</div>
|
||||
}
|
||||
classNames={{
|
||||
body: "rb:p-[10px]!",
|
||||
header: "rb:bg-[#F6F8FC]!"
|
||||
}}
|
||||
>
|
||||
<Space size={8} direction="vertical" className="rb:w-full">
|
||||
{marketSources
|
||||
.filter(s => s.category === cat.id)
|
||||
.map(source => (
|
||||
<div
|
||||
key={source.id}
|
||||
className={`rb:bg-white rb:rounded-lg rb:p-2 rb:border rb:cursor-pointer rb:flex rb:items-center rb:gap-2 rb:transition-all ${
|
||||
selectedSource === source.id
|
||||
? 'rb:border-[#155EEF] rb:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.15)]'
|
||||
: 'rb:border-[#DFE4ED] rb:hover:border-[#155EEF] rb:hover:shadow-[0px_2px_4px_0px_rgba(33,35,50,0.15)]'
|
||||
}`}
|
||||
onClick={() => handleSelectSource(source.id)}
|
||||
>
|
||||
<div className="rb:w-5 rb:h-5 rb:flex-shrink-0 rb:flex rb:items-center rb:justify-center rb:overflow-hidden rb:rounded rb:bg-gray-100">
|
||||
{source.logo_url ? (
|
||||
<img
|
||||
src={source.logo_url}
|
||||
alt={source.name}
|
||||
className="rb:w-full rb:h-full rb:object-cover"
|
||||
referrerPolicy="no-referrer"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
const parent = e.currentTarget.parentElement;
|
||||
if (parent) {
|
||||
parent.innerHTML = '🏪';
|
||||
parent.style.fontSize = '16px';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="rb:text-base">🏪</span>
|
||||
)}
|
||||
</div>
|
||||
<span className="rb:flex-1 rb:font-medium rb:text-[12px] rb:overflow-hidden rb:text-ellipsis rb:whitespace-nowrap">
|
||||
{source.name}
|
||||
</span>
|
||||
<span className="rb:text-xs rb:text-gray-500 rb:px-1.5 rb:py-0.5 rb:bg-gray-100 rb:rounded-full rb:flex-shrink-0">
|
||||
{source.mcp_count}
|
||||
</span>
|
||||
{source.connected && (
|
||||
<span className="rb:text-green-500 rb:text-[8px] rb:flex-shrink-0">●</span>
|
||||
)}
|
||||
</div>
|
||||
))}
|
||||
</Space>
|
||||
</Card>
|
||||
))}
|
||||
</Space>
|
||||
</div>
|
||||
|
||||
{/* 右侧内容区 */}
|
||||
<div className="rb:flex-1 rb:bg-white rb:rounded-lg rb:border rb:border-gray-200 rb:overflow-hidden">
|
||||
<div className="rb:flex-1 rb:border-l rb:border-gray-200 rb:overflow-hidden">
|
||||
<div className="rb:h-full rb:overflow-y-auto rb:p-6">
|
||||
{renderSourceDetail()}
|
||||
</div>
|
||||
@@ -308,6 +505,10 @@ const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () =>
|
||||
ref={marketConfigModalRef}
|
||||
onConnect={handleConnect}
|
||||
/>
|
||||
<McpServiceModal
|
||||
ref={mcpServiceModalRef}
|
||||
refresh={() => {}}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -2,6 +2,7 @@ import { forwardRef, useImperativeHandle, useState } from 'react';
|
||||
import { Form, Input, Button, App, Space } from 'antd';
|
||||
import { useTranslation } from 'react-i18next';
|
||||
import { CopyOutlined, EyeInvisibleOutlined, EyeOutlined } from '@ant-design/icons';
|
||||
import { createMarketConfig,updateMarketConfig } from '@/api/tools';
|
||||
import RbModal from '@/components/RbModal';
|
||||
|
||||
const FormItem = Form.Item;
|
||||
@@ -9,15 +10,16 @@ const FormItem = Form.Item;
|
||||
interface MarketSource {
|
||||
id: string;
|
||||
name: string;
|
||||
icon: string;
|
||||
logo_url: string;
|
||||
url: string;
|
||||
desc: string;
|
||||
apiKey: string;
|
||||
description: string;
|
||||
token?: string;
|
||||
connected: boolean;
|
||||
configId?: string;
|
||||
}
|
||||
|
||||
interface MarketConfigModalProps {
|
||||
onConnect: (sourceId: string, apiKey: string) => void;
|
||||
onConnect: (sourceId: string, configId: string) => void;
|
||||
}
|
||||
|
||||
export interface MarketConfigModalRef {
|
||||
@@ -47,8 +49,7 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
|
||||
const handleOpen = (source: MarketSource) => {
|
||||
setCurrentSource(source);
|
||||
form.setFieldsValue({
|
||||
url: source.url,
|
||||
apiKey: source.apiKey,
|
||||
token: source.token || '',
|
||||
});
|
||||
setVisible(true);
|
||||
};
|
||||
@@ -56,18 +57,36 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
|
||||
const handleSave = () => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((values) => {
|
||||
.then(async (values) => {
|
||||
if (!currentSource) return;
|
||||
|
||||
setLoading(true);
|
||||
|
||||
// 模拟连接延迟
|
||||
setTimeout(() => {
|
||||
onConnect(currentSource.id, values.apiKey || '');
|
||||
message.success(`正在连接 ${currentSource.name}...`);
|
||||
setLoading(false);
|
||||
try {
|
||||
let res: any;
|
||||
if (currentSource.configId) {
|
||||
// 更新配置
|
||||
res = await updateMarketConfig({
|
||||
mcp_market_config_id: currentSource.configId,
|
||||
token: values.token || '',
|
||||
status: 1,
|
||||
});
|
||||
message.success(t('tool.marketConfigUpdated', { name: currentSource.name }));
|
||||
} else {
|
||||
// 创建配置
|
||||
res = await createMarketConfig({
|
||||
mcp_market_id: currentSource.id || '',
|
||||
token: values.token || '',
|
||||
status: 1,
|
||||
});
|
||||
message.success(t('tool.marketConnecting', { name: currentSource.name }));
|
||||
}
|
||||
onConnect(currentSource.id, res.id || currentSource.configId);
|
||||
handleClose();
|
||||
}, 500);
|
||||
} catch (error) {
|
||||
console.error('保存配置失败:', error);
|
||||
} finally {
|
||||
setLoading(false);
|
||||
}
|
||||
})
|
||||
.catch((err) => {
|
||||
console.log('表单验证失败:', err);
|
||||
@@ -91,10 +110,10 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
|
||||
|
||||
return (
|
||||
<RbModal
|
||||
title={`配置 ${currentSource.name}`}
|
||||
title={t('tool.marketConfig', { name: currentSource.name })}
|
||||
open={visible}
|
||||
onCancel={handleClose}
|
||||
okText="保存并连接"
|
||||
okText={t('tool.marketSaveAndConnect')}
|
||||
onOk={handleSave}
|
||||
confirmLoading={loading}
|
||||
width={600}
|
||||
@@ -102,12 +121,28 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
|
||||
<div>
|
||||
{/* 市场源信息头部 */}
|
||||
<div className="rb:flex rb:gap-4 rb:mb-6 rb:p-4 rb:bg-gray-50 rb:rounded-lg">
|
||||
<div className="rb:text-4xl rb:w-16 rb:h-16 rb:flex rb:items-center rb:justify-center rb:bg-white rb:rounded-lg rb:flex-shrink-0">
|
||||
{currentSource.icon}
|
||||
<div className="rb:w-16 rb:h-16 rb:flex rb:items-center rb:justify-center rb:bg-white rb:rounded-lg rb:flex-shrink-0 rb:overflow-hidden">
|
||||
{currentSource.logo_url ? (
|
||||
<img
|
||||
src={currentSource.logo_url}
|
||||
alt={currentSource.name}
|
||||
className="rb:w-full rb:h-full rb:object-cover"
|
||||
onError={(e) => {
|
||||
e.currentTarget.style.display = 'none';
|
||||
const parent = e.currentTarget.parentElement;
|
||||
if (parent) {
|
||||
parent.innerHTML = '🏪';
|
||||
parent.style.fontSize = '32px';
|
||||
}
|
||||
}}
|
||||
/>
|
||||
) : (
|
||||
<span className="rb:text-4xl">🏪</span>
|
||||
)}
|
||||
</div>
|
||||
<div className="rb:flex-1">
|
||||
<h3 className="rb:text-base rb:font-semibold rb:mb-1 rb:text-gray-900">{currentSource.name}</h3>
|
||||
<p className="rb:text-sm rb:text-gray-600 rb:leading-relaxed">{currentSource.desc}</p>
|
||||
<p className="rb:text-sm rb:text-gray-600 rb:leading-relaxed">{currentSource.description}</p>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -115,39 +150,34 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
|
||||
form={form}
|
||||
layout="vertical"
|
||||
>
|
||||
{/* 市场地址 */}
|
||||
<FormItem
|
||||
name="url"
|
||||
label="市场地址"
|
||||
>
|
||||
<FormItem label={t('tool.marketUrl')}>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
readOnly
|
||||
placeholder="市场地址"
|
||||
value={currentSource.url}
|
||||
/>
|
||||
<Button
|
||||
icon={<CopyOutlined />}
|
||||
onClick={handleCopyUrl}
|
||||
>
|
||||
复制
|
||||
{t('tool.marketCopy')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
</FormItem>
|
||||
|
||||
{/* API Key */}
|
||||
<FormItem
|
||||
name="apiKey"
|
||||
name="token"
|
||||
label={
|
||||
<span>
|
||||
API Key <span className="rb:text-gray-400 rb:font-normal">(可选)</span>
|
||||
API Key <span className="rb:text-gray-400 rb:font-normal">({t('tool.marketApiKeyOptional')})</span>
|
||||
</span>
|
||||
}
|
||||
extra="部分市场需要 API Key 才能获取完整的服务列表"
|
||||
extra={<span style={{ display: 'inline-block', marginTop: 8 }}>{t('tool.marketApiKeyExtra')}</span>}
|
||||
>
|
||||
<Space.Compact style={{ width: '100%' }}>
|
||||
<Input
|
||||
type={showApiKey ? 'text' : 'password'}
|
||||
placeholder="输入 API Key 以获取更多服务"
|
||||
placeholder={t('tool.marketApiKeyPlaceholder')}
|
||||
autoComplete="off"
|
||||
/>
|
||||
<Button
|
||||
@@ -157,11 +187,10 @@ const MarketConfigModal = forwardRef<MarketConfigModalRef, MarketConfigModalProp
|
||||
</Space.Compact>
|
||||
</FormItem>
|
||||
|
||||
{/* 连接状态 */}
|
||||
<div className="rb:flex rb:items-center rb:gap-2 rb:p-3 rb:bg-gray-50 rb:rounded rb:text-sm">
|
||||
<span className="rb:text-gray-600">连接状态:</span>
|
||||
<span className="rb:text-gray-600">{t('tool.marketConnectionStatus')}:</span>
|
||||
<span className={`rb:font-medium ${currentSource.connected ? 'rb:text-green-600' : 'rb:text-gray-400'}`}>
|
||||
{currentSource.connected ? '● 已连接' : '○ 未连接'}
|
||||
{currentSource.connected ? t('tool.marketConnected') : t('tool.marketDisconnected')}
|
||||
</span>
|
||||
</div>
|
||||
</Form>
|
||||
|
||||
@@ -69,7 +69,7 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
|
||||
config: { ...config_data }
|
||||
})
|
||||
|
||||
if (config_data.connection_config.headers) {
|
||||
if (config_data?.connection_config?.headers) {
|
||||
console.log(Object.keys(config_data.connection_config.headers).map(key => ({
|
||||
key,
|
||||
value: config_data.connection_config.headers[key]
|
||||
@@ -80,6 +80,12 @@ const McpServiceModal = forwardRef<McpServiceModalRef, McpServiceModalProps>(({
|
||||
})))
|
||||
}
|
||||
setEditVo(data)
|
||||
} else if (data) {
|
||||
const { config_data, name, description, icon } = data
|
||||
form.setFieldsValue({
|
||||
name, description, icon,
|
||||
...(config_data ? { config: { ...config_data } } : {})
|
||||
})
|
||||
} else {
|
||||
form.resetFields();
|
||||
}
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
* @Author: yujiangping
|
||||
* @Date: 2026-01-05 17:22:23
|
||||
* @LastEditors: yujiangping
|
||||
* @LastEditTime: 2026-03-04 15:12:48
|
||||
* @LastEditTime: 2026-03-04 15:34:50
|
||||
*/
|
||||
import React, { useState } from 'react';
|
||||
import { Tabs } from 'antd';
|
||||
@@ -54,7 +54,7 @@ const ToolManagement: React.FC = () => {
|
||||
{activeTab === 'mcp' && <Mcp getStatusTag={getStatusTag} />}
|
||||
{activeTab === 'inner' && <Inner getStatusTag={getStatusTag} />}
|
||||
{activeTab === 'custom' && <Custom getStatusTag={getStatusTag} />}
|
||||
{/* {activeTab === 'market' && <Market getStatusTag={getStatusTag} />} */}
|
||||
{activeTab === 'market' && <Market getStatusTag={getStatusTag} />}
|
||||
</div>
|
||||
);
|
||||
};
|
||||
|
||||
@@ -136,4 +136,10 @@ export interface ExecuteData {
|
||||
export interface CustomToolModalRef {
|
||||
handleOpen: (data?: ToolItem) => void;
|
||||
handleClose: () => void;
|
||||
}
|
||||
|
||||
export interface MarketQuery {
|
||||
mcp_market_config_id?: string;
|
||||
page?: number;
|
||||
pagesize?: number;
|
||||
}
|
||||
Reference in New Issue
Block a user