diff --git a/frontend/packages/core/src/pages/common/ApiRequestSetting.tsx b/frontend/packages/core/src/pages/common/ApiRequestSetting.tsx index 3ecce6c9..975a7809 100644 --- a/frontend/packages/core/src/pages/common/ApiRequestSetting.tsx +++ b/frontend/packages/core/src/pages/common/ApiRequestSetting.tsx @@ -86,9 +86,9 @@ export default function ApiRequestSetting() { - label={$t('集成地址')} + label={$t('OpenAPI & MCP 调用地址')} name="sitePrefix" - rules={[{ whitespace: true }]} + rules={[{ required: true, whitespace: true }]} extra={$t('与外部平台集成时,获取 API 市场中文档信息的域名')} > diff --git a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx index 7e42524c..6a6e3f9c 100644 --- a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx +++ b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx @@ -10,6 +10,7 @@ import { useConnection } from './hook/useConnection' import { ClientRequest, Tool, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js' import { z } from 'zod' import { useNavigate } from 'react-router-dom' +import { ServiceDetailType } from '@market/const/serviceHub/type' import useCopyToClipboard from '@common/hooks/copy' type ConfigList = { @@ -34,12 +35,20 @@ type ApiKeyItem = { const IntegrationAIContainer = ({ type, - handleToolsChange + handleToolsChange, + customClassName, + service, + serviceId, + currentTab }: { type: 'global' | 'service' handleToolsChange: (value: Tool[]) => void + customClassName?: string + service?: ServiceDetailType + serviceId?: string + currentTab?: string }) => { - const [activeTab, setActiveTab] = useState('mcp') + const [activeTab, setActiveTab] = useState(type === 'service' ? 'openApi' : 'mcp') const { message } = App.useApp() const [configContent, setConfigContent] = useState('') const [apiKey, setApiKey] = useState('') @@ -66,14 +75,14 @@ const IntegrationAIContainer = ({ const params: ConfigList = { mcp: { title: $t('MCP 配置'), - configContent: '', + configContent: service?.mcpAccessConfig || '', apiKeys: [] } } - if (type === 'global') { + if (type === 'service') { params.openApi = { title: $t('Open API 文档'), - configContent: '', + configContent: service?.openapiAddress || '', apiKeys: [] } } @@ -131,7 +140,7 @@ const IntegrationAIContainer = ({ * 获取 API Key 列表 */ const getKeysList = () => { - fetchData>(type === 'global' ? 'simple/system/apikeys' : '', { + fetchData>(type === 'global' ? 'simple/system/apikeys' : 'my/apikeys', { method: 'GET' }) .then((response) => { @@ -212,28 +221,34 @@ const IntegrationAIContainer = ({ // 使用 useRef 保存最新的连接状态和断开函数 const connectionStatusRef = useRef(connectionStatus) const disconnectFnRef = useRef(disconnectMcpServer) - + // 当连接状态或断开函数变化时更新 ref useEffect(() => { connectionStatusRef.current = connectionStatus disconnectFnRef.current = disconnectMcpServer }, [connectionStatus, disconnectMcpServer]) - + + const setupComponent = () => { + initTabsData() + if (type === 'global') { + getGlobalMcpConfig() + setMcpServerUrl('mcp/global/sse') + } else { + service?.basic.enableMcp && setMcpServerUrl(`mcp/service/${serviceId}/sse`) + } + getKeysList() + } + + useEffect(() => { + setupComponent() + }, [service]) + useEffect(() => { + if (type === 'service') { + currentTab === 'MCP' ? setActiveTab('mcp') : setActiveTab('openApi') + } + }, [currentTab]) // 仅在组件加载时执行初始化逻辑 useEffect(() => { - // 局部函数,仅在此 effect 执行期间存在 - const setupComponent = () => { - if (type === 'global') { - getGlobalMcpConfig() - setMcpServerUrl('mcp/global/sse') - } - initTabsData() - getKeysList() - } - - // 执行初始化 - setupComponent() - // 返回清理函数,只会在组件卸载时执行 return () => { try { @@ -242,7 +257,6 @@ const IntegrationAIContainer = ({ if (disconnectFn) { disconnectFn() } - } catch (err) { console.error('断开连接时出错:', err) } @@ -250,11 +264,11 @@ const IntegrationAIContainer = ({ }, [type]) useEffect(() => { if (activeTab === 'openApi' && tabContent?.openApi?.configContent) { - setConfigContent(tabContent?.openApi?.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}')) + setConfigContent(tabContent?.openApi?.configContent) } else if (activeTab === 'mcp' && tabContent?.mcp?.configContent) { setConfigContent(tabContent.mcp.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}')) } - }, [apiKey, activeTab, tabContent]) + }, [service, apiKey, activeTab, tabContent]) useEffect(() => { if (mcpServerUrl) { @@ -274,7 +288,7 @@ const IntegrationAIContainer = ({ <>
- {type === 'service' && ( + {type === 'service' && service?.basic.enableMcp && (
{/* 标签页内容区域 */}
- + {activeTab === 'mcp' ? ( + { + try { + return JSON.parse(configContent); + } catch (e) { + return {}; + } + })() + : configContent + : {} + } + theme="monokai" + indentWidth={2} + displayDataTypes={false} + displayObjectSize={false} + name={false} + collapsed={false} + enableClipboard={false} + style={{ + backgroundColor: 'transparent', + wordBreak: 'break-word', + whiteSpace: 'normal' + }} + /> + ) : ( + <> +
{configContent || ''}
+ + )} handleCopy(configContent)} diff --git a/frontend/packages/core/src/pages/mcpService/McpToolsContainer.tsx b/frontend/packages/core/src/pages/mcpService/McpToolsContainer.tsx index b3a98fc8..41bbf025 100644 --- a/frontend/packages/core/src/pages/mcpService/McpToolsContainer.tsx +++ b/frontend/packages/core/src/pages/mcpService/McpToolsContainer.tsx @@ -2,12 +2,12 @@ import { Icon } from '@iconify/react/dist/iconify.js' import { Tool } from '@modelcontextprotocol/sdk/types.js' import { Card } from 'antd' -const McpToolsContainer = ({ tools = [] }: { tools: Tool[] }) => { +const McpToolsContainer = ({ tools = [], customClassName }: { tools: Tool[]; customClassName?: string }) => { return ( <> { const { serviceId } = useParams() @@ -28,6 +39,14 @@ const ServiceHubDetail = () => { const { modal, message } = App.useApp() const [mySystemOptionList, setMySystemOptionList] = useState() const [service, setService] = useState() + const [serviceMetrics, setServiceMetrics] = useState<{ title: string; icon: React.ReactNode; value: string }[]>([]) + const [serviceTags, setServiceTags] = useState< + { color: string; textColor: string; title: string; content: React.ReactNode }[] + >([]) + const [tools, setTools] = useState([]) + const [tabItem, setTabItem] = useState([]) + const [currentTab, setCurrentTab] = useState('') + const { state } = useGlobalContext() const navigate = useNavigate() const modifyApiDoc = (apiDoc: string, apiPrefix: string) => { @@ -60,7 +79,11 @@ const ServiceHubDetail = () => { 'invoke_address', 'approval_type', 'service_kind', - 'site_prefix' + 'site_prefix', + 'enable_mcp', + 'mcp_server_address', + 'mcp_access_config', + 'openapi_address' ] }).then((response) => { const { code, data, msg } = response @@ -73,12 +96,66 @@ const ServiceHubDetail = () => { setServiceName(data.service.name) setServiceDesc(data.service.description) setServiceDoc(DOMPurify.sanitize(data.service.document)) + setServiceMetricsList(data.service.basic) + setTabItemList(data.service.basic) } else { message.error(msg || $t(RESPONSE_TIPS.error)) } }) } + const handleTabChange = (value: any) => { + setCurrentTab(value) + } + + const setServiceMetricsList = (serviceBasicInfo: ServiceBasicInfoType) => { + // 设置服务指标数据 + setServiceMetrics([ + { + title: 'API 数量', + icon: , + value: serviceBasicInfo.apiNum.toString() + }, + { + title: '接入消费者数量', + icon: , + value: serviceBasicInfo.appNum.toString() + }, + { + title: '30天内调用次数', + icon: , + value: formatInvokeCount(serviceBasicInfo.invokeCount ?? 0) + } + ]) + // 设置服务标签数据 + const tags = [ + { + color: '#7371fc1b', + textColor: 'text-theme', + title: serviceBasicInfo?.catalogue?.name || '-', + content: serviceBasicInfo?.catalogue?.name || '-' + }, + { + color: '#fbe5e5', + textColor: 'text-[#000]', + title: serviceBasicInfo?.serviceKind || '-', + content: SERVICE_KIND_OPTIONS.find((x) => x.value === serviceBasicInfo?.serviceKind)?.label || '-' + } + ] + + // 如果启用了MCP,添加MCP标签 + if (serviceBasicInfo?.enableMcp) { + tags.push({ + color: '#ffc107', + textColor: 'text-[#000]', + title: 'MCP', + content: 'MCP' + }) + } + + setServiceTags(tags) + } + useEffect(() => { if (!serviceId) { console.warn('缺少serviceId') @@ -134,120 +211,234 @@ const ServiceHubDetail = () => { }) } - const items = [ - { - key: 'introduction', - label: $t('介绍'), - children: ( - <> -
- - ), - icon: - }, - { - key: 'api-document', - label: $t('API 文档'), - children: ( -
- -
- ), - icon: - }, - { - key: 'api-integrate', - label: $t('集成'), - children: ( -
- -
- ), - icon: + const handleToolsChange = (value: Tool[]) => { + setTools(value) + } + // 格式化调用次数,添加K和M单位 + const formatInvokeCount = (count: number | null | undefined): string => { + if (count === null || count === undefined) return '-' + if (count >= 1000000) { + const value = Math.floor(count / 100000) / 10 + return `${value}M` } - ] + if (count >= 1000) { + const value = Math.floor(count / 100) / 10 + return `${value}K` + } + return count.toString() + } + + /** + * 定义一个更新标签项的函数,在serviceBasicInfo或tools变化时调用 + */ + const updateTabItems = useCallback(() => { + if (!serviceBasicInfo) return + const descriptionItem = [ + { + label: $t('供应方'), + value: serviceBasicInfo?.team?.name || '-', + className: 'pb-[10px]' + }, + { + label: $t('版本'), + value: serviceBasicInfo?.version || '-', + className: 'pb-[10px]' + }, + { + label: $t('更新时间'), + value: serviceBasicInfo?.updateTime || '-', + className: 'pb-[10px]', + isTimeString: true + }, + { + label: $t('审核'), + value: serviceBasicInfo?.approvalType ? $t(approvalTypeTranslate[serviceBasicInfo?.approvalType] || '-') : '-', + className: 'pb-[0px]' + } + ] + const items: TabItemType[] = [ + { + key: 'introduction', + label: $t('介绍'), + children: ( + <> + + + + {descriptionItem.map((item, index) => ( + + {item.isTimeString ? ( + + {item.value} + + ) : ( + item.value + )} + + ))} + + +
+
+ + ), + icon: + }, + { + key: 'api-document', + label: $t('API'), + children: ( + + + + ), + icon: + } + ] + if (serviceBasicInfo.enableMcp) { + items.push({ + key: 'MCP', + label: 'MCP', + children: , + icon: + }) + } + setTabItem(items) + }, [serviceBasicInfo, serviceDoc, service, tools, state.language]) + + /** + * 当初始化serviceBasicInfo时调用的函数 + * @param _serviceBasicInfo + */ + const setTabItemList = (_serviceBasicInfo: ServiceBasicInfoType) => { + // 只调用更新函数,更新将由useEffect处理 + updateTabItems() + } + useEffect(() => { + if (serviceBasicInfo) { + updateTabItems() + } + }, [tools, updateTabItems, serviceBasicInfo]) return ( -
-
-
-
- -
-
- {/* {service?.name?.substring(0,1)} */} - - ) : undefined - } - icon={serviceBasicInfo?.logo ? '' : } - > - {' '} - - -
-

+

+
+ +
+ +
+
+
+ + ) : undefined + } + icon={serviceBasicInfo?.logo ? '' : } + > + {' '} + +
+
+

{serviceName}

-
-

{serviceDesc || '-'}

-

- - {$t('Base URL')}: {serviceBasicInfo?.invokeAddress || '-'} -

-
- -
+
+ {serviceTags.map((tag, index) => ( + + {tag.content} + + ))} + {serviceMetrics.map((item, index) => ( + + + {item.icon} + {item.value} + + + ))}
-
- -
-
- - {serviceBasicInfo?.appNum ?? '-'} - {serviceBasicInfo?.team?.name || '-'} - - {serviceBasicInfo?.approvalType ? $t(approvalTypeTranslate[serviceBasicInfo?.approvalType] || '-') : '-'} - - {serviceBasicInfo?.catalogue?.name || '-'} - - {serviceBasicInfo?.tags?.map((x) => x.name)?.join(',') || '-'} - - - - - {serviceBasicInfo?.version || '-'} - - - {serviceBasicInfo?.updateTime || '-'} - - - -
-
+ + {serviceDesc || $t('暂无服务描述')} + +
+
+ +
+ +
+ + +
+
) } diff --git a/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx b/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx index 1c9e9f3e..d8eca82e 100644 --- a/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx +++ b/frontend/packages/market/src/pages/serviceHub/ServiceHubList.tsx @@ -160,7 +160,7 @@ const ServiceHubList: FC = () => { classNames={{ header: 'border-b-[0px] p-[20px] ', body: 'pt-0 h-[110px]' }} onClick={() => showDocumentDetail(item)} > - + {item.description || $t('暂无服务描述')} @@ -283,7 +283,7 @@ const CardAction = (props: { service: ServiceHubTableListItem }) => { - + {service.subscriberNum ?? '-'} @@ -293,7 +293,7 @@ const CardAction = (props: { service: ServiceHubTableListItem }) => { - {formatInvokeCount(service.invokeCount)} + {formatInvokeCount(service.invokeCount ?? 0)}