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:
@@ -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>
|
||||
|
||||
Reference in New Issue
Block a user