diff --git a/web/src/i18n/en.ts b/web/src/i18n/en.ts index d404dd6e..b9ab09b0 100644 --- a/web/src/i18n/en.ts +++ b/web/src/i18n/en.ts @@ -1774,6 +1774,7 @@ Memory Bear: After the rebellion, regional warlordism intensified for several re mcp: 'MCP Services', inner: 'Built-in Tools', custom: 'Custom Tools', + market: 'Tool Market', mcpSearchPlaceholder: 'Search MCP Services...', innerSearchPlaceholder: 'Search Tools...', customSearchPlaceholder: 'Search Custom Tools...', diff --git a/web/src/i18n/zh.ts b/web/src/i18n/zh.ts index fc6bb822..71a20207 100644 --- a/web/src/i18n/zh.ts +++ b/web/src/i18n/zh.ts @@ -1771,6 +1771,7 @@ export const zh = { mcp: 'MCP 服务', inner: '内置工具', custom: '自定义工具', + market: '工具市场', mcpSearchPlaceholder: '搜索MCP服务...', innerSearchPlaceholder: '搜索工具...', customSearchPlaceholder: '搜索自定义工具...', diff --git a/web/src/views/ToolManagement/Market.tsx b/web/src/views/ToolManagement/Market.tsx new file mode 100644 index 00000000..59fbddcc --- /dev/null +++ b/web/src/views/ToolManagement/Market.tsx @@ -0,0 +1,315 @@ +import React, { useState, useRef, type ReactNode } from 'react'; +import { Input, Button, Spin, App } from 'antd'; +import { SearchOutlined, SettingOutlined, GlobalOutlined, SyncOutlined } from '@ant-design/icons'; +import { useTranslation } from 'react-i18next'; +import MarketConfigModal, { type MarketConfigModalRef } from './components/MarketConfigModal'; + +interface MarketSource { + id: string; + name: string; + category: string; + icon: string; + url: string; + desc: string; + apiKey: string; + connected: boolean; + mcpCount: number; +} + +interface MarketMcp { + id: string; + name: string; + provider: string; + type: string; + desc: string; + downloads?: string; + stars?: string; + icon: string; + configTemplate: any; +} + +interface MarketCategory { + id: string; + name: string; + icon: string; +} + +const Market: React.FC<{ getStatusTag?: (status: string) => ReactNode }> = () => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [loading, setLoading] = useState(false); + const [selectedSource, setSelectedSource] = useState(null); + const marketConfigModalRef = useRef(null); + const [marketSources, setMarketSources] = useState([ + { 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([ + { id: 'official', name: '官方/综合', icon: '🌐' }, + { id: 'china-cloud', name: '国内云', icon: '☁️' }, + { id: 'community', name: '社区/垂直', icon: '👥' } + ]); + + const [mcpCache, setMcpCache] = useState>({ + '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 [searchKeyword, setSearchKeyword] = useState(''); + + 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} 列表已刷新`); + } + setLoading(false); + }, 600); + }; + + const handleOpenConfig = (sourceId: string) => { + const source = marketSources.find(s => s.id === sourceId); + if (source) { + marketConfigModalRef.current?.handleOpen(source); + } + }; + + const handleConnect = (sourceId: string, apiKey: string) => { + // 更新市场源状态 + setMarketSources(prev => prev.map(source => { + if (source.id === sourceId) { + return { + ...source, + apiKey, + connected: true + }; + } + return source; + })); + + // 模拟获取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 + })); + } + message.success(`已连接 ${source?.name}`); + }, 800); + }; + + const renderSourceDetail = () => { + if (!selectedSource) { + return ( +
+
🏪
+

选择一个 MCP 市场

+

从左侧选择一个市场源,配置连接后即可浏览该市场的 MCP 服务

+
+ ); + } + + const source = marketSources.find(s => s.id === selectedSource); + 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()) + ); + + return ( + <> +
+
+
+ {source.icon} +
+
+

{source.name}

+

{source.desc}

+
+
+
+ + +
+
+ +
+
+

+ 可用 MCP 服务 ({mcpList.length}) +

+
+ {source.connected && ( + + )} + {mcpList.length > 0 && ( + } + placeholder="搜索服务..." + value={searchKeyword} + onChange={(e) => setSearchKeyword(e.target.value)} + style={{ width: 200 }} + /> + )} +
+
+ + {mcpList.length > 0 ? ( + +
+ {filteredList.map(mcp => ( +
+
+
+ {mcp.icon} +
+ + {mcp.type} + +
+

{mcp.name}

+ {mcp.provider && ( +
+ @ {mcp.provider} +
+ )} +

{mcp.desc}

+
+ {mcp.downloads && ( + + {mcp.downloads} + + )} + {mcp.stars && ( + + ⭐ {mcp.stars} + + )} +
+
+ +
+
+ ))} +
+
+ ) : ( +
+
{source.connected ? '📭' : '🔌'}
+

+ {source.connected ? '暂无可用的 MCP 服务' : '尚未连接此市场'} +

+

+ {source.connected ? '该市场暂时没有可用的服务' : '点击右上角"配置"按钮设置连接信息'} +

+ {!source.connected && ( + + )} +
+ )} +
+ + ); + }; + + return ( +
+ {/* 左侧市场源列表 */} +
+
+ MCP 市场 +
+ {categories.map(cat => ( +
+
+ {cat.icon} + {cat.name} +
+
+ {marketSources + .filter(s => s.category === cat.id) + .map(source => ( +
handleSelectSource(source.id)} + > + {source.icon} + + {source.name} + + + {source.mcpCount} + + {source.connected && ( + + )} +
+ ))} +
+
+ ))} +
+ + {/* 右侧内容区 */} +
+
+ {renderSourceDetail()} +
+
+ + {/* 配置弹窗 */} + +
+ ); +}; + +export default Market; diff --git a/web/src/views/ToolManagement/components/MarketConfigModal.tsx b/web/src/views/ToolManagement/components/MarketConfigModal.tsx new file mode 100644 index 00000000..d1d87563 --- /dev/null +++ b/web/src/views/ToolManagement/components/MarketConfigModal.tsx @@ -0,0 +1,173 @@ +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 RbModal from '@/components/RbModal'; + +const FormItem = Form.Item; + +interface MarketSource { + id: string; + name: string; + icon: string; + url: string; + desc: string; + apiKey: string; + connected: boolean; +} + +interface MarketConfigModalProps { + onConnect: (sourceId: string, apiKey: string) => void; +} + +export interface MarketConfigModalRef { + handleOpen: (source: MarketSource) => void; + handleClose: () => void; +} + +const MarketConfigModal = forwardRef(({ + onConnect +}, ref) => { + const { t } = useTranslation(); + const { message } = App.useApp(); + const [visible, setVisible] = useState(false); + const [form] = Form.useForm(); + const [loading, setLoading] = useState(false); + const [currentSource, setCurrentSource] = useState(null); + const [showApiKey, setShowApiKey] = useState(false); + + const handleClose = () => { + setVisible(false); + form.resetFields(); + setLoading(false); + setCurrentSource(null); + setShowApiKey(false); + }; + + const handleOpen = (source: MarketSource) => { + setCurrentSource(source); + form.setFieldsValue({ + url: source.url, + apiKey: source.apiKey, + }); + setVisible(true); + }; + + const handleSave = () => { + form + .validateFields() + .then((values) => { + if (!currentSource) return; + + setLoading(true); + + // 模拟连接延迟 + setTimeout(() => { + onConnect(currentSource.id, values.apiKey || ''); + message.success(`正在连接 ${currentSource.name}...`); + setLoading(false); + handleClose(); + }, 500); + }) + .catch((err) => { + console.log('表单验证失败:', err); + }); + }; + + const handleCopyUrl = () => { + if (currentSource?.url) { + navigator.clipboard.writeText(currentSource.url).then(() => { + message.success(t('common.copySuccess')); + }); + } + }; + + useImperativeHandle(ref, () => ({ + handleOpen, + handleClose + })); + + if (!currentSource) return null; + + return ( + +
+ {/* 市场源信息头部 */} +
+
+ {currentSource.icon} +
+
+

{currentSource.name}

+

{currentSource.desc}

+
+
+ +
+ {/* 市场地址 */} + + + + + + + + {/* API Key */} + + API Key (可选) + + } + extra="部分市场需要 API Key 才能获取完整的服务列表" + > + + +
+
+ ); +}); + +export default MarketConfigModal; diff --git a/web/src/views/ToolManagement/index.tsx b/web/src/views/ToolManagement/index.tsx index 9fa73067..d684ebdd 100644 --- a/web/src/views/ToolManagement/index.tsx +++ b/web/src/views/ToolManagement/index.tsx @@ -1,3 +1,11 @@ +/* + * @Description: + * @Version: 0.0.1 + * @Author: yujiangping + * @Date: 2026-01-05 17:22:23 + * @LastEditors: yujiangping + * @LastEditTime: 2026-03-04 12:24:01 + */ import React, { useState } from 'react'; import { Tabs } from 'antd'; import { useTranslation } from 'react-i18next'; @@ -5,9 +13,10 @@ import { useTranslation } from 'react-i18next'; import Mcp from './Mcp'; import Inner from './Inner'; import Custom from './Custom'; +import Market from './Market'; import Tag from '@/components/Tag' -const tabKeys = ['mcp', 'inner', 'custom'] +const tabKeys = ['mcp', 'inner', 'custom', 'market'] const ToolManagement: React.FC = () => { const { t } = useTranslation(); const [activeTab, setActiveTab] = useState('mcp'); @@ -45,6 +54,7 @@ const ToolManagement: React.FC = () => { {activeTab === 'mcp' && } {activeTab === 'inner' && } {activeTab === 'custom' && } + {activeTab === 'market' && } ); };