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:
yujiangping
2026-03-06 14:55:45 +08:00
parent 85aea97c21
commit 0ea83b4364
8 changed files with 515 additions and 207 deletions

View File

@@ -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>