Merge pull request #279 from APIParkLab/feature/1.7-cxx

Feature/1.7 cxx
This commit is contained in:
ningyv
2025-04-10 16:23:15 +08:00
committed by GitHub
6 changed files with 223 additions and 128 deletions
@@ -204,7 +204,7 @@ const mockData = [
access: ''
},
{
name: 'MCP Key',
name: 'API Key',
key: 'mcpKey',
path: '/mcpKey',
icon: 'material-symbols:key',
@@ -1,4 +1,4 @@
import { App, Card, Select } from 'antd'
import { App, Button, Card, Empty, Select } from 'antd'
import { $t } from '@common/locales/index.ts'
import { Icon } from '@iconify/react/dist/iconify.js'
import { useEffect, useState } from 'react'
@@ -7,6 +7,9 @@ import { IconButton } from '@common/components/postcat/api/IconButton'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { useConnection } from './hook/useConnection'
import { ClientRequest, Tool, ListToolsResultSchema } from '@modelcontextprotocol/sdk/types.js'
import { z } from 'zod'
import { useNavigate } from 'react-router-dom'
type ConfigList = {
openApi?: {
@@ -28,13 +31,26 @@ type ApiKeyItem = {
value: string
}
const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | 'service'; handleApiKeyChange: (value: string) => void }) => {
const IntegrationAIContainer = ({
type,
handleToolsChange
}: {
type: 'global' | 'service'
handleToolsChange: (value: Tool[]) => void
}) => {
const [activeTab, setActiveTab] = useState('mcp')
const { message } = App.useApp()
const [configContent, setConfigContent] = useState<string>('')
const [apiKey, setApiKey] = useState<string>('')
const [apiKeyList, setApiKeyList] = useState<{ value: string; label: string }[]>([])
const [mcpServerUrl, setMcpServerUrl] = useState<string>('')
const navigator = useNavigate()
const [errors, setErrors] = useState<Record<string, string | null>>({
resources: null,
prompts: null,
tools: null
})
const [tabContent, setTabContent] = useState<ConfigList>({
mcp: {
title: $t('MCP 配置'),
@@ -76,7 +92,6 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' |
const handleChange = (value: string) => {
setApiKey(value)
handleApiKeyChange(value)
}
/**
@@ -106,6 +121,10 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' |
})
}
const addKey = () => {
navigator('/mcpKey')
}
/**
* 获取 API Key 列表
*/
@@ -136,6 +155,41 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' |
})
}
const clearError = (tabKey: keyof typeof errors) => {
setErrors((prev) => ({ ...prev, [tabKey]: null }))
}
const makeRequest = async <T extends z.ZodType>(request: ClientRequest, schema: T, tabKey?: keyof typeof errors) => {
try {
const response = await makeConnectionRequest(request, schema)
if (tabKey !== undefined) {
clearError(tabKey)
}
return response
} catch (e) {
const errorString = (e as Error).message ?? String(e)
if (tabKey !== undefined) {
setErrors((prev) => ({
...prev,
[tabKey]: errorString
}))
}
throw e
}
}
const listTools = async () => {
const response = await makeRequest(
{
method: 'tools/list' as const,
params: {}
},
ListToolsResultSchema,
'tools'
)
handleToolsChange(response.tools)
}
const {
connectionStatus,
serverCapabilities,
@@ -146,72 +200,54 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' |
handleCompletion,
completionsSupported,
connect: connectMcpServer,
disconnect: disconnectMcpServer,
disconnect: disconnectMcpServer
} = useConnection({
transportType: 'sse',
sseUrl: mcpServerUrl,
proxyServerUrl: 'mcp/global/sse',
requestTimeout: 1000,
});
console.log('connectionStatus==================', connectionStatus);
// console.log('serverCapabilities==================', serverCapabilities);
// console.log('mcpClient==================', mcpClient);
// console.log('requestHistory==================', requestHistory);
// console.log('makeConnectionRequest==================', makeConnectionRequest);
// console.log('sendNotification==================', sendNotification);
// console.log('handleCompletion==================', handleCompletion);
// console.log('completionsSupported==================', completionsSupported);
// console.log('connectMcpServer==================', connectMcpServer);
// console.log('disconnectMcpServer==================', disconnectMcpServer);
// const useConnectAIagent = () => {
// connectMcpServer()
// }
sseUrl: '',
proxyServerUrl: mcpServerUrl,
requestTimeout: 1000
})
useEffect(() => {
type === 'global' && getGlobalMcpConfig()
if (type === 'global') {
getGlobalMcpConfig()
setMcpServerUrl('mcp/global/sse')
}
initTabsData()
getKeysList()
}, [])
useEffect(() => {
if (activeTab === 'openApi') {
setConfigContent(tabContent.openApi?.configContent || '')
} else if (activeTab === 'mcp') {
setConfigContent(tabContent.mcp.configContent || '')
if (activeTab === 'openApi' && tabContent?.openApi?.configContent) {
setConfigContent(tabContent?.openApi?.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}'))
} else if (activeTab === 'mcp' && tabContent?.mcp?.configContent) {
setConfigContent(tabContent.mcp.configContent?.replace('{your_api_key}', apiKey || '{your_api_key}'))
}
}, [tabContent, activeTab])
}, [apiKey, activeTab, tabContent])
useEffect(() => {
if (configContent && apiKey) {
const parsedConfig = JSON.parse(configContent)
console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊parsedConfig', parsedConfig, apiKey)
let baseUrl = ''
if (parsedConfig?.mcpServers) {
// 获取 mcpServers 对象中的第一个键
const serverKey = Object.keys(parsedConfig.mcpServers)[0]
baseUrl = parsedConfig.mcpServers[serverKey]?.url
}
baseUrl = baseUrl.replace('{your_api_key}', apiKey)
if (mcpServerUrl === baseUrl) {
return
}
setMcpServerUrl(baseUrl)
console.log('啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊啊', mcpServerUrl)
if (mcpServerUrl) {
if (connectionStatus === 'connected') {
disconnectMcpServer()
}
connectMcpServer()
}
}, [apiKey, configContent, connectMcpServer])
}, [mcpServerUrl])
useEffect(() => {
if (connectionStatus === 'connected') {
listTools()
}
}, [connectionStatus])
return (
<>
<Card
style={{ borderRadius: '10px' }}
className="w-[400px]"
className="w-[400px] h-fit"
classNames={{
body: 'p-[10px]'
}}
>
<p>
<p onClick={listTools}>
<Icon
icon="icon-park-solid:connection-point-two"
className="align-text-bottom mr-[5px]"
@@ -278,34 +314,42 @@ const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' |
{activeTab === 'mcp' && (
<>
<div className="tab-content font-semibold my-[10px]">API Key</div>
<Select value={apiKey} className="w-full" onChange={handleChange} options={apiKeyList} />
<Card
style={{ borderRadius: '5px' }}
className="w-full mt-[5px] "
classNames={{
body: 'p-[5px]'
}}
>
<div className="relative h-[25px]">
{apiKey}
<IconButton
name="copy"
onClick={() => handleCopy(configContent)}
sx={{
position: 'absolute',
top: '0px',
right: '5px',
color: '#999',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
{apiKeyList.length ? (
<>
<Select value={apiKey} className="w-full" onChange={handleChange} options={apiKeyList} />
<Card
style={{ borderRadius: '5px' }}
className="w-full mt-[5px] "
classNames={{
body: 'p-[5px]'
}}
></IconButton>
</div>
</Card>
>
<div className="relative h-[25px]">
{apiKey}
<IconButton
name="copy"
onClick={() => handleCopy(configContent)}
sx={{
position: 'absolute',
top: '0px',
right: '5px',
color: '#999',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
}}
></IconButton>
</div>
</Card>
</>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} description={""}>
<Button onClick={addKey} type="primary">{$t('新增 API Key')}</Button>
</Empty>
)}
</>
)}
</Card>
@@ -1,7 +1,7 @@
import InsidePage from '@common/components/aoplatform/InsidePage'
import { IconButton } from '@common/components/postcat/api/IconButton'
import { $t } from '@common/locales/index.ts'
import { Button, Card, App } from 'antd'
import { Button, Card, App, Empty } from 'antd'
import { useFetch } from '@common/hooks/http'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useEffect, useRef, useState } from 'react'
@@ -101,7 +101,6 @@ const McpKeyContainer = () => {
* 编辑 API Key
*/
const editKey = (key: any) => {
console.log('any', key)
modal.confirm({
title: $t('编辑'),
content: (
@@ -140,17 +139,36 @@ const McpKeyContainer = () => {
{$t('新增 API Key')}
</Button>
<div className="api-key-container mt-[20px]">
{keys.map((key, index) => (
<Card style={{ width: 600, borderRadius: '10px' }} key={index} className="mt-[10px]">
<div className="flex">
<div className="flex-1">
<p className="text-[14px] font-bold">{key.name}</p>
<p className="flex">
<span className="h-[26px] leading-[28px]">{key.value}</span>
{keys.length ? (
keys.map((key, index) => (
<Card style={{ width: 600, borderRadius: '10px' }} key={index} className="mt-[10px]">
<div className="flex">
<div className="flex-1">
<p className="text-[14px] font-bold">{key.name}</p>
<div className="flex">
<span className="h-[26px] leading-[28px]">{key.value}</span>
<IconButton
name="copy"
onClick={() => {
copyCode(key?.value)
}}
sx={{
color: '#333',
transition: 'none',
'&.MuiButtonBase-root:hover': {
background: 'transparent',
color: '#3D46F2',
transition: 'none'
}
}}
></IconButton>
</div>
</div>
<div className="w-[30px] flex justify-center items-center">
<IconButton
name="copy"
name="edit"
onClick={() => {
copyCode(key?.value)
editKey(key)
}}
sx={{
color: '#333',
@@ -162,35 +180,26 @@ const McpKeyContainer = () => {
}
}}
></IconButton>
</p>
<IconButton
name="delete"
onClick={() => {
deleteKey(key.id)
}}
sx={{
color: '#333',
transition: 'none',
'&.MuiButtonBase-root:hover': { background: 'transparent', color: 'red', transition: 'none' }
}}
></IconButton>
</div>
</div>
<div className="w-[30px] flex justify-center items-center">
<IconButton
name="edit"
onClick={() => {
editKey(key)
}}
sx={{
color: '#333',
transition: 'none',
'&.MuiButtonBase-root:hover': { background: 'transparent', color: '#3D46F2', transition: 'none' }
}}
></IconButton>
<IconButton
name="delete"
onClick={() => {
deleteKey(key.id)
}}
sx={{
color: '#333',
transition: 'none',
'&.MuiButtonBase-root:hover': { background: 'transparent', color: 'red', transition: 'none' }
}}
></IconButton>
</div>
</div>
</Card>
))}
</Card>
))
) : (
<>
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE}/>
</>
)}
</div>
</InsidePage>
</>
@@ -1,11 +1,14 @@
import InsidePage from "@common/components/aoplatform/InsidePage"
import { $t } from '@common/locales/index.ts'
import { Card } from "antd"
import IntegrationAIContainer from "./IntegrationAIContainer"
import { Tool } from "@modelcontextprotocol/sdk/types.js"
import { useState } from "react"
import McpToolsContainer from "./McpToolsContainer"
const McpServiceContainer = () => {
const handleApiKeyChange = (value: string) => {
console.log(value)
const [tools, setTools] = useState<Tool[]>([]);
const handleToolsChange = (value: Tool[]) => {
setTools(value)
}
return (
<>
@@ -15,11 +18,10 @@ const McpServiceContainer = () => {
showBorder={false}
scrollPage={false}
>
<div className="flex mt-[10px] pr-[40px]">
<Card style={{ borderRadius: '10px' }} className="flex-1 w-[400px] mr-[10px]">
444
</Card>
<IntegrationAIContainer type={'global'} handleApiKeyChange={handleApiKeyChange}></IntegrationAIContainer>
<McpToolsContainer tools={tools} />
<IntegrationAIContainer type={'global'} handleToolsChange={handleToolsChange}></IntegrationAIContainer>
</div>
</InsidePage>
</>
@@ -0,0 +1,30 @@
import { Icon } from '@iconify/react/dist/iconify.js'
import { Tool } from '@modelcontextprotocol/sdk/types.js'
import { Card } from 'antd'
const McpToolsContainer = ({ tools = [] }: { tools: Tool[] }) => {
return (
<>
<Card
style={{ borderRadius: '10px' }}
className={`w-full flex-1 mr-[10px]`}
classNames={{
body: 'p-[10px]'
}}
>
<div className="mb-[10px]">
<Icon icon="gravity-ui:plug-connection" className="align-text-bottom mr-[5px]" width="16" height="16" />
<span className="text-[14px] font-bold align-middle">Tools</span>
</div>
{tools.map((tool, index) => (
<Card style={{ borderRadius: '10px' }} key={index} className={`w-full ${index > 0 ? 'mt-[10px]' : ''}`}>
<p className="text-[14px] font-bold">{tool.name}</p>
<div className="leading-[28px]">{tool.description}</div>
</Card>
))}
</Card>
</>
)
}
export default McpToolsContainer
@@ -254,16 +254,26 @@ export function useConnection({
const apiPrefix = '/api/v1/';
fullUrl = `${baseUrl}${apiPrefix}${proxyServerUrl}`;
}
// let newSseUrl = ''
// if (sseUrl.startsWith('http://') || sseUrl.startsWith('https://')) {
// // 如果是完整URL,直接使用
// newSseUrl = sseUrl
// } else {
// // 如果是相对路径,添加基础URL和API前缀
// const baseUrl = window.location.origin;
// const apiPrefix = '/api/v1/';
// newSseUrl = `${baseUrl}${apiPrefix}${sseUrl}`;
// }
const mcpProxyServerUrl = new URL(fullUrl);
mcpProxyServerUrl.searchParams.append("transportType", transportType);
if (transportType === "stdio") {
mcpProxyServerUrl.searchParams.append("command", command || '');
mcpProxyServerUrl.searchParams.append("args", args || '');
mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env || {}));
} else {
mcpProxyServerUrl.searchParams.append("url", sseUrl);
}
console.log('sseUrl===', sseUrl)
// mcpProxyServerUrl.searchParams.append("transportType", transportType);
// if (transportType === "stdio") {
// mcpProxyServerUrl.searchParams.append("command", command || '');
// mcpProxyServerUrl.searchParams.append("args", args || '');
// mcpProxyServerUrl.searchParams.append("env", JSON.stringify(env || {}));
// } else {
// mcpProxyServerUrl.searchParams.append("url", newSseUrl);
// }
// console.log('sseUrl===', newSseUrl)
try {
// Inject auth manually instead of using SSEClientTransport, because we're
// proxying through the inspector server first.