mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-07-06 16:11:56 +08:00
Merge pull request #279 from APIParkLab/feature/1.7-cxx
Feature/1.7 cxx
This commit is contained in:
@@ -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.
|
||||
|
||||
Reference in New Issue
Block a user