mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
Compare commits
15 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 29c567977a | |||
| 2e32c57648 | |||
| c49bddb9e7 | |||
| 9617ddc02c | |||
| 60296bf3c9 | |||
| 6e0c66d982 | |||
| 07b49030a2 | |||
| 203c3e4c4b | |||
| f3059f8df8 | |||
| 0b0cd25d2c | |||
| 4e0239d282 | |||
| 92e47812bd | |||
| 2e867da093 | |||
| 7eca309eac | |||
| 31abd609e6 |
@@ -47,7 +47,10 @@
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"uuid": "^9.0.1",
|
||||
"vite-tsconfig-paths": "^4.3.2"
|
||||
"vite-tsconfig-paths": "^4.3.2",
|
||||
"react-json-view": "^1.21.3",
|
||||
"zod": "^3.23.8",
|
||||
"@modelcontextprotocol/sdk": "^1.9.0"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@ant-design/cssinjs": "^1.18.2",
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useNavigate } from 'react-router-dom'
|
||||
class InsidePageProps {
|
||||
showBanner?: boolean = true
|
||||
pageTitle: string | React.ReactNode = ''
|
||||
tagList?: Array<{ label: string | ReactNode }> = []
|
||||
tagList?: Array<{ label: string | ReactNode; className?: string; color?: string }> = []
|
||||
children: React.ReactNode
|
||||
showBtn?: boolean = false
|
||||
btnTitle?: string = ''
|
||||
@@ -79,7 +79,7 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
tagList?.length > 0 &&
|
||||
tagList?.map((tag) => {
|
||||
return (
|
||||
<Tag key={tag.label as string} bordered={false}>
|
||||
<Tag key={tag.label as string} bordered={false} color={tag.color} className={tag.className}>
|
||||
{tag.label}
|
||||
</Tag>
|
||||
)
|
||||
|
||||
@@ -196,6 +196,20 @@ const mockData = [
|
||||
key: 'maintenanceCenter',
|
||||
path: '/datasourcing',
|
||||
children: [
|
||||
{
|
||||
name: 'MCP 服务',
|
||||
key: 'mcpService',
|
||||
path: '/mcpService',
|
||||
icon: 'ph:network-x',
|
||||
access: ''
|
||||
},
|
||||
{
|
||||
name: 'MCP Key',
|
||||
key: 'mcpKey',
|
||||
path: '/mcpKey',
|
||||
icon: 'material-symbols:key',
|
||||
access: ''
|
||||
},
|
||||
{
|
||||
name: '数据源',
|
||||
key: 'datasourcing',
|
||||
|
||||
@@ -220,6 +220,26 @@ const mockData = {
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'mcpService',
|
||||
router: [
|
||||
{
|
||||
path: 'mcpService',
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'mcpKey',
|
||||
router: [
|
||||
{
|
||||
path: 'mcpKey',
|
||||
type: 'normal'
|
||||
}
|
||||
]
|
||||
},
|
||||
{
|
||||
driver: 'apipark.builtIn.component',
|
||||
name: 'loadBalancing',
|
||||
|
||||
@@ -898,5 +898,7 @@
|
||||
"Kce2fcdbf": "No Permission",
|
||||
"K24f6a5b4": "Custom (Empty Template)",
|
||||
"Kea608112": "Load Preset Template",
|
||||
"Kee7de862": "Edit Provider( (0) )"
|
||||
"Kee7de862": "Edit Provider( (0) )",
|
||||
"Kb0e0aeda": "New API Key",
|
||||
"K9d81999c": "The API Key can be used to call system-level Open API and MCP."
|
||||
}
|
||||
|
||||
@@ -920,5 +920,7 @@
|
||||
"Kce2fcdbf": "権限がありません",
|
||||
"K24f6a5b4": "カスタム(空のテンプレート)",
|
||||
"Kea608112": "プリセットテンプレートを読み込む",
|
||||
"Kee7de862": "サプライヤーを編集( (0) )"
|
||||
"Kee7de862": "サプライヤーを編集( (0) )",
|
||||
"Kb0e0aeda": "APIキーを新規作成",
|
||||
"K9d81999c": "APIキーは、システムレベルのOpen APIおよびMCPの呼び出しに使用できます。"
|
||||
}
|
||||
|
||||
@@ -851,5 +851,7 @@
|
||||
"Kce2fcdbf": "暂无权限",
|
||||
"K24f6a5b4": "自定义(空模板)",
|
||||
"Kea608112": "载入预置模板",
|
||||
"Kee7de862": "编辑供应商( (0) )"
|
||||
"Kee7de862": "编辑供应商( (0) )",
|
||||
"Kb0e0aeda": "新增 API Key",
|
||||
"K9d81999c": "API 密钥可用于调用系统级 Open API 和 MCP。"
|
||||
}
|
||||
|
||||
@@ -920,5 +920,7 @@
|
||||
"Kce2fcdbf": "暫無權限",
|
||||
"K24f6a5b4": "自訂(空模板)",
|
||||
"Kea608112": "載入預設模板",
|
||||
"Kee7de862": "編輯供應商( (0) )"
|
||||
"Kee7de862": "編輯供應商( (0) )",
|
||||
"Kb0e0aeda": "新增 API 金鑰",
|
||||
"K9d81999c": "API 金鑰可用於調用系統級 Open API 和 MCP。"
|
||||
}
|
||||
|
||||
@@ -21,6 +21,7 @@ export type AiServiceConfigFieldType = {
|
||||
catalogue?:string | string[];
|
||||
approvalType?:string;
|
||||
providerType?:string
|
||||
enable_mcp?: boolean
|
||||
};
|
||||
|
||||
export type AiServiceSubServiceTableListItem = {
|
||||
|
||||
@@ -800,6 +800,22 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
'mcpService',
|
||||
{
|
||||
type: 'module',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpServiceContainer')),
|
||||
key: 'mcpService'
|
||||
}
|
||||
],
|
||||
[
|
||||
'mcpKey',
|
||||
{
|
||||
type: 'module',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/mcpService/McpKeyContainer')),
|
||||
key: 'mcpKey'
|
||||
}
|
||||
],
|
||||
[
|
||||
'loadBalancing',
|
||||
{
|
||||
|
||||
@@ -362,6 +362,10 @@ export const SERVICE_APPROVAL_OPTIONS = [
|
||||
{ label: '无需审核:允许任何消费者调用该服务', value: 'auto' },
|
||||
{ label: '人工审核:仅允许通过人工审核的消费者调用该服务', value: 'manual' }
|
||||
]
|
||||
export const MCP_OPTIONS = [
|
||||
{ label: '关闭', value: false },
|
||||
{ label: '开启:AI Agent 等产品能够通过 MCP 方式调用服务', value: true }
|
||||
]
|
||||
export const SERVICE_KIND_OPTIONS = [
|
||||
{ label: 'REST', value: 'rest' },
|
||||
{ label: 'AI', value: 'ai' }
|
||||
|
||||
@@ -31,6 +31,7 @@ export type SystemConfigFieldType = {
|
||||
catalogue?:string | string[];
|
||||
approvalType?:string;
|
||||
modelMapping?: string;
|
||||
enable_mcp?: boolean;
|
||||
};
|
||||
|
||||
export type SystemSubServiceTableListItem = {
|
||||
|
||||
@@ -666,6 +666,15 @@ p{
|
||||
display: none;
|
||||
}
|
||||
|
||||
.apipark-layout-base-menu-horizontal-menu-item {
|
||||
padding-right: 0px !important;
|
||||
padding-left: 0px !important;
|
||||
}
|
||||
.apipark-layout-base-menu-horizontal-item-title {
|
||||
padding-left: 16px;
|
||||
padding-right: 16px;
|
||||
}
|
||||
|
||||
.ant-pro-table-list-toolbar-setting-items{
|
||||
position:absolute;
|
||||
top:18px;
|
||||
|
||||
@@ -228,6 +228,7 @@ const AiServiceInsidePage: FC = () => {
|
||||
<InsidePage
|
||||
pageTitle={aiServiceInfo?.name || '-'}
|
||||
tagList={[
|
||||
...(aiServiceInfo?.enable_mcp ? [{ label: 'MCP', color: '#ffc107', className: 'text-[#000]' }] : []),
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
|
||||
@@ -283,8 +283,9 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
),
|
||||
onOk: () => {
|
||||
return addModelModalRef.current?.save().then((res) => {
|
||||
if (res === true) {
|
||||
if (res) {
|
||||
getLlmList(lastLlmID)
|
||||
form.setFieldValue('defaultLlm', res)
|
||||
}
|
||||
})
|
||||
},
|
||||
|
||||
@@ -102,7 +102,7 @@ const AddModels = forwardRef<addModelsContentHandle, addModelContentProps>((prop
|
||||
...value,
|
||||
id: modelID
|
||||
}
|
||||
fetchData<BasicResponse<null>>('ai/provider/model', {
|
||||
fetchData<BasicResponse<{ model: { id: string, name: string } }>>('ai/provider/model', {
|
||||
method: type === 'edit' ? 'PUT' : 'POST',
|
||||
eoParams: { provider: providerID },
|
||||
eoBody: finalValue,
|
||||
@@ -112,7 +112,8 @@ const AddModels = forwardRef<addModelsContentHandle, addModelContentProps>((prop
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t(RESPONSE_TIPS.success) || msg)
|
||||
resolve(true)
|
||||
const llmId = response.data?.model?.id
|
||||
resolve(llmId)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
|
||||
@@ -40,7 +40,7 @@ const LogSettings = () => {
|
||||
const menuData = useMemo(() => {
|
||||
const newMenu = menuItems?.map((x: DynamicMenuItem) => {
|
||||
return getItem(
|
||||
<Link to={`template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
<Link to={`/logsettings/template/${x.name}`}>{$t(x.title)}</Link>,
|
||||
x.name,
|
||||
undefined,
|
||||
undefined,
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
import { App, Form, Input } from 'antd'
|
||||
import { $t } from '@common/locales'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
|
||||
import { forwardRef, useEffect, useImperativeHandle } from 'react'
|
||||
type modelFieldType = {
|
||||
name: string
|
||||
type: string
|
||||
model_parameters: string
|
||||
access_configuration: string
|
||||
}
|
||||
|
||||
export type addMcpKeysHandle = {
|
||||
save: () => Promise<boolean | string>
|
||||
}
|
||||
|
||||
type addMcpKeysProps = {
|
||||
name?: string
|
||||
value?: string
|
||||
type?: string
|
||||
apikey?: string
|
||||
}
|
||||
|
||||
const AddMcpKey = forwardRef<addMcpKeysHandle, addMcpKeysProps>((props, ref) => {
|
||||
const { name = '', value: editValue = '', type = 'new', apikey = '' } = props
|
||||
const [form] = Form.useForm()
|
||||
const { message } = App.useApp()
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({
|
||||
name,
|
||||
value: editValue
|
||||
})
|
||||
}, [])
|
||||
/**
|
||||
* 保存
|
||||
* @returns
|
||||
*/
|
||||
const save: () => Promise<boolean | string> = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
form
|
||||
.validateFields()
|
||||
.then((value) => {
|
||||
console.log('value', value)
|
||||
const finalValue = {
|
||||
...value,
|
||||
value: editValue ? editValue : uuidv4(),
|
||||
expired: 0
|
||||
}
|
||||
fetchData<BasicResponse<any>>('system/apikey', {
|
||||
method: type === 'new' ? 'POST' : 'PUT',
|
||||
eoBody: finalValue,
|
||||
...(type === 'edit' ? {
|
||||
eoParams: { apikey }
|
||||
} : {})
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t(RESPONSE_TIPS.success) || msg)
|
||||
resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
})
|
||||
.catch((errorInfo) => reject(errorInfo))
|
||||
} catch (error) {
|
||||
reject(error)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
save
|
||||
}))
|
||||
|
||||
return (
|
||||
<Form
|
||||
form={form}
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
className="flex flex-col mx-auto h-full"
|
||||
name="mcpKeyModalConfig"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<modelFieldType> label={$t('名称')} name="name" rules={[{ required: true }]}>
|
||||
<Input autoFocus className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
|
||||
export default AddMcpKey
|
||||
@@ -0,0 +1,316 @@
|
||||
import { App, Card, Select } from 'antd'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { useEffect, useState } from 'react'
|
||||
import ReactJson from 'react-json-view'
|
||||
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'
|
||||
|
||||
type ConfigList = {
|
||||
openApi?: {
|
||||
title: string
|
||||
configContent: string
|
||||
apiKeys: string[]
|
||||
}
|
||||
mcp: {
|
||||
title: string
|
||||
configContent: string
|
||||
apiKeys: string[]
|
||||
}
|
||||
}
|
||||
|
||||
type ApiKeyItem = {
|
||||
expired: number
|
||||
id: string
|
||||
name: string
|
||||
value: string
|
||||
}
|
||||
|
||||
const IntegrationAIContainer = ({ type, handleApiKeyChange }: { type: 'global' | 'service'; handleApiKeyChange: (value: string) => 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 [tabContent, setTabContent] = useState<ConfigList>({
|
||||
mcp: {
|
||||
title: $t('MCP 配置'),
|
||||
configContent: '',
|
||||
apiKeys: []
|
||||
}
|
||||
})
|
||||
const { fetchData } = useFetch()
|
||||
|
||||
const initTabsData = () => {
|
||||
const params: ConfigList = {
|
||||
mcp: {
|
||||
title: $t('MCP 配置'),
|
||||
configContent: '',
|
||||
apiKeys: []
|
||||
}
|
||||
}
|
||||
if (type === 'global') {
|
||||
params.openApi = {
|
||||
title: $t('Open API 文档'),
|
||||
configContent: '',
|
||||
apiKeys: []
|
||||
}
|
||||
}
|
||||
setTabContent(params)
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制
|
||||
* @param value
|
||||
* @returns
|
||||
*/
|
||||
const handleCopy = async (value: string): Promise<void> => {
|
||||
if (value) {
|
||||
await navigator.clipboard.writeText(value)
|
||||
message.success($t(RESPONSE_TIPS.copySuccess))
|
||||
}
|
||||
}
|
||||
|
||||
const handleChange = (value: string) => {
|
||||
setApiKey(value)
|
||||
handleApiKeyChange(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取全局 MCP 配置
|
||||
* @returns
|
||||
*/
|
||||
const getGlobalMcpConfig = () => {
|
||||
fetchData<BasicResponse<null>>('global/mcp/config', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTabContent((prevTabContent) => ({
|
||||
...prevTabContent,
|
||||
mcp: {
|
||||
...prevTabContent.mcp,
|
||||
configContent: data.config || ''
|
||||
}
|
||||
}))
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 API Key 列表
|
||||
*/
|
||||
const getKeysList = () => {
|
||||
fetchData<BasicResponse<null>>(type === 'global' ? 'simple/system/apikeys' : '', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
if (data.apikeys && data.apikeys.length > 0) {
|
||||
setApiKeyList(
|
||||
data.apikeys.map((item: ApiKeyItem) => {
|
||||
return {
|
||||
label: item.name,
|
||||
value: item.value
|
||||
}
|
||||
})
|
||||
)
|
||||
setApiKey(data.apikeys[0].value)
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
const {
|
||||
connectionStatus,
|
||||
serverCapabilities,
|
||||
mcpClient,
|
||||
requestHistory,
|
||||
makeRequest: makeConnectionRequest,
|
||||
sendNotification,
|
||||
handleCompletion,
|
||||
completionsSupported,
|
||||
connect: connectMcpServer,
|
||||
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()
|
||||
// }
|
||||
|
||||
useEffect(() => {
|
||||
type === 'global' && getGlobalMcpConfig()
|
||||
initTabsData()
|
||||
getKeysList()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
if (activeTab === 'openApi') {
|
||||
setConfigContent(tabContent.openApi?.configContent || '')
|
||||
} else if (activeTab === 'mcp') {
|
||||
setConfigContent(tabContent.mcp.configContent || '')
|
||||
}
|
||||
}, [tabContent, activeTab])
|
||||
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 (connectionStatus === 'connected') {
|
||||
disconnectMcpServer()
|
||||
}
|
||||
connectMcpServer()
|
||||
}
|
||||
}, [apiKey, configContent, connectMcpServer])
|
||||
|
||||
return (
|
||||
<>
|
||||
<Card
|
||||
style={{ borderRadius: '10px' }}
|
||||
className="w-[400px]"
|
||||
classNames={{
|
||||
body: 'p-[10px]'
|
||||
}}
|
||||
>
|
||||
<p>
|
||||
<Icon
|
||||
icon="icon-park-solid:connection-point-two"
|
||||
className="align-text-bottom mr-[5px]"
|
||||
width="16"
|
||||
height="16"
|
||||
/>
|
||||
{$t('AI 代理集成')}
|
||||
</p>
|
||||
<div className="tab-container mt-3">
|
||||
{type === 'service' && (
|
||||
<div className="tab-nav flex rounded-md overflow-hidden border border-solid border-[#3D46F2] w-fit">
|
||||
<div
|
||||
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'openApi' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
|
||||
onClick={() => setActiveTab('openApi')}
|
||||
>
|
||||
Open API
|
||||
</div>
|
||||
<div
|
||||
className={`tab-item px-5 py-1.5 cursor-pointer text-sm transition-colors ${activeTab === 'mcp' ? 'bg-[#3D46F2] text-white' : 'bg-white text-[#3D46F2]'}`}
|
||||
onClick={() => setActiveTab('mcp')}
|
||||
>
|
||||
MCP
|
||||
</div>
|
||||
</div>
|
||||
)}
|
||||
<div className="tab-content font-semibold mt-[10px]">
|
||||
{activeTab === 'openApi' ? tabContent.openApi?.title : tabContent.mcp.title}
|
||||
</div>
|
||||
{/* 标签页内容区域 */}
|
||||
<div className="bg-[#0a0b21] text-white p-4 rounded-md my-2 font-mono text-sm overflow-auto relative">
|
||||
<ReactJson
|
||||
src={configContent ? JSON.parse(configContent) : {}}
|
||||
theme="monokai"
|
||||
indentWidth={2}
|
||||
displayDataTypes={false}
|
||||
displayObjectSize={false}
|
||||
name={false}
|
||||
collapsed={false}
|
||||
enableClipboard={false}
|
||||
style={{
|
||||
backgroundColor: 'transparent',
|
||||
wordBreak: 'break-word',
|
||||
whiteSpace: 'normal'
|
||||
}}
|
||||
/>
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => handleCopy(configContent)}
|
||||
sx={{
|
||||
position: 'absolute',
|
||||
top: '5px',
|
||||
right: '5px',
|
||||
color: '#999',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</div>
|
||||
{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'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</div>
|
||||
</Card>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default IntegrationAIContainer
|
||||
@@ -0,0 +1,200 @@
|
||||
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 { useFetch } from '@common/hooks/http'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useEffect, useRef, useState } from 'react'
|
||||
import AddMcpKey, { addMcpKeysHandle } from './AddMcpKey'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||
|
||||
const McpKeyContainer = () => {
|
||||
const { fetchData } = useFetch()
|
||||
const { message, modal } = App.useApp()
|
||||
const [keys, setKeys] = useState<any[]>([])
|
||||
const [, forceUpdate] = useState<unknown>(null)
|
||||
const { state } = useGlobalContext()
|
||||
const addMcpKeyModalRef = useRef<addMcpKeysHandle>(null)
|
||||
|
||||
/**
|
||||
* 新增 API Key
|
||||
*/
|
||||
const addKey = () => {
|
||||
modal.confirm({
|
||||
title: $t('新增 API Key'),
|
||||
content: <AddMcpKey ref={addMcpKeyModalRef}></AddMcpKey>,
|
||||
onOk: () => {
|
||||
return addMcpKeyModalRef.current?.save().then((res) => {
|
||||
if (res) {
|
||||
getKeysList()
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取 API Key 列表
|
||||
*/
|
||||
const getKeysList = () => {
|
||||
fetchData<BasicResponse<null>>('system/apikeys', {
|
||||
method: 'GET'
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, msg, data } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setKeys(data.apikeys || [])
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => {
|
||||
message.error(errorInfo || $t(RESPONSE_TIPS.error))
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 复制 API Key
|
||||
*/
|
||||
const copyCode = async (value: string): Promise<void> => {
|
||||
if (value) {
|
||||
await navigator.clipboard.writeText(value)
|
||||
message.success($t(RESPONSE_TIPS.copySuccess))
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 删除 API Key
|
||||
*/
|
||||
const deleteKey = (id: string) => {
|
||||
modal.confirm({
|
||||
title: $t('删除'),
|
||||
content: $t('确定删除吗?'),
|
||||
onOk: async () => {
|
||||
try {
|
||||
const response = await fetchData<BasicResponse<'success'>>('system/apikey', {
|
||||
method: 'DELETE',
|
||||
eoParams: { apikey: id }
|
||||
})
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
message.success($t('删除成功'))
|
||||
getKeysList()
|
||||
}
|
||||
} catch (error) {
|
||||
message.error($t('删除失败'))
|
||||
}
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 编辑 API Key
|
||||
*/
|
||||
const editKey = (key: any) => {
|
||||
console.log('any', key)
|
||||
modal.confirm({
|
||||
title: $t('编辑'),
|
||||
content: (
|
||||
<AddMcpKey ref={addMcpKeyModalRef} name={key.name} value={key.value} apikey={key.id} type={'edit'}></AddMcpKey>
|
||||
),
|
||||
onOk: () => {
|
||||
return addMcpKeyModalRef.current?.save().then((res) => {
|
||||
if (res) {
|
||||
getKeysList()
|
||||
}
|
||||
})
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getKeysList()
|
||||
}, [])
|
||||
useEffect(() => {
|
||||
forceUpdate({})
|
||||
}, [state.language])
|
||||
return (
|
||||
<>
|
||||
<InsidePage
|
||||
pageTitle={$t('API Key')}
|
||||
description={$t('API 密钥可用于调用系统级 Open API 和 MCP。')}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
>
|
||||
<Button type="primary" onClick={addKey}>
|
||||
{$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>
|
||||
<IconButton
|
||||
name="copy"
|
||||
onClick={() => {
|
||||
copyCode(key?.value)
|
||||
}}
|
||||
sx={{
|
||||
color: '#333',
|
||||
transition: 'none',
|
||||
'&.MuiButtonBase-root:hover': {
|
||||
background: 'transparent',
|
||||
color: '#3D46F2',
|
||||
transition: 'none'
|
||||
}
|
||||
}}
|
||||
></IconButton>
|
||||
</p>
|
||||
</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>
|
||||
))}
|
||||
</div>
|
||||
</InsidePage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpKeyContainer
|
||||
@@ -0,0 +1,29 @@
|
||||
import InsidePage from "@common/components/aoplatform/InsidePage"
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { Card } from "antd"
|
||||
import IntegrationAIContainer from "./IntegrationAIContainer"
|
||||
|
||||
const McpServiceContainer = () => {
|
||||
const handleApiKeyChange = (value: string) => {
|
||||
console.log(value)
|
||||
}
|
||||
return (
|
||||
<>
|
||||
<InsidePage
|
||||
pageTitle={$t('MCP 服务')}
|
||||
description={$t('MCP Service 充当 AI 模型与 API 之间的桥梁,允许智能助手(如 Claude)动态发现和调用 Gateway 上的 API,无需繁琐的手动配置或自定义集成。')}
|
||||
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>
|
||||
</div>
|
||||
</InsidePage>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
export default McpServiceContainer
|
||||
@@ -0,0 +1,33 @@
|
||||
// import { InspectorConfig } from "./configurationTypes";
|
||||
|
||||
// OAuth-related session storage keys
|
||||
export const SESSION_KEYS = {
|
||||
CODE_VERIFIER: "mcp_code_verifier",
|
||||
SERVER_URL: "mcp_server_url",
|
||||
TOKENS: "mcp_tokens",
|
||||
CLIENT_INFORMATION: "mcp_client_information",
|
||||
} as const;
|
||||
|
||||
export type ConnectionStatus =
|
||||
| "disconnected"
|
||||
| "connected"
|
||||
| "error"
|
||||
| "error-connecting-to-proxy";
|
||||
|
||||
export const DEFAULT_MCP_PROXY_LISTEN_PORT = "6277";
|
||||
|
||||
/**
|
||||
* Default configuration for the MCP Inspector, Currently persisted in local_storage in the Browser.
|
||||
* Future plans: Provide json config file + Browser local_storage to override default values
|
||||
**/
|
||||
export const DEFAULT_INSPECTOR_CONFIG: any = {
|
||||
MCP_SERVER_REQUEST_TIMEOUT: {
|
||||
description: "Timeout for requests to the MCP server (ms)",
|
||||
value: 10000,
|
||||
},
|
||||
MCP_PROXY_FULL_ADDRESS: {
|
||||
description:
|
||||
"Set this if you are running the MCP Inspector Proxy on a non-default address. Example: http://10.1.1.22:5577",
|
||||
value: "",
|
||||
},
|
||||
} as const;
|
||||
@@ -0,0 +1,22 @@
|
||||
import {
|
||||
NotificationSchema as BaseNotificationSchema,
|
||||
ClientNotificationSchema,
|
||||
ServerNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { z } from "zod";
|
||||
|
||||
export const StdErrNotificationSchema = BaseNotificationSchema.extend({
|
||||
method: z.literal("notifications/stderr"),
|
||||
params: z.object({
|
||||
content: z.string(),
|
||||
}),
|
||||
});
|
||||
|
||||
export const NotificationSchema = ClientNotificationSchema.or(
|
||||
StdErrNotificationSchema,
|
||||
)
|
||||
.or(ServerNotificationSchema)
|
||||
.or(BaseNotificationSchema);
|
||||
|
||||
export type StdErrNotification = z.infer<typeof StdErrNotificationSchema>;
|
||||
export type Notification = z.infer<typeof NotificationSchema>;
|
||||
@@ -0,0 +1,382 @@
|
||||
import { Client } from "@modelcontextprotocol/sdk/client/index.js";
|
||||
import {
|
||||
SSEClientTransport,
|
||||
SseError,
|
||||
} from "@modelcontextprotocol/sdk/client/sse.js";
|
||||
import { App } from 'antd'
|
||||
import {
|
||||
ClientNotification,
|
||||
ClientRequest,
|
||||
CreateMessageRequestSchema,
|
||||
ListRootsRequestSchema,
|
||||
ProgressNotificationSchema,
|
||||
ResourceUpdatedNotificationSchema,
|
||||
LoggingMessageNotificationSchema,
|
||||
Request,
|
||||
Result,
|
||||
ServerCapabilities,
|
||||
PromptReference,
|
||||
ResourceReference,
|
||||
McpError,
|
||||
CompleteResultSchema,
|
||||
ErrorCode,
|
||||
CancelledNotificationSchema,
|
||||
ResourceListChangedNotificationSchema,
|
||||
ToolListChangedNotificationSchema,
|
||||
PromptListChangedNotificationSchema,
|
||||
} from "@modelcontextprotocol/sdk/types.js";
|
||||
import { useState } from "react";
|
||||
import { z } from "zod";
|
||||
import { ConnectionStatus, SESSION_KEYS } from "./constants";
|
||||
import { Notification, StdErrNotificationSchema } from "./notificationTypes";
|
||||
// import { auth } from "@modelcontextprotocol/sdk/client/auth.js";
|
||||
// import { authProvider } from "../auth";
|
||||
// import packageJson from "../../../package.json";
|
||||
|
||||
|
||||
interface UseConnectionOptions {
|
||||
transportType: "stdio" | "sse";
|
||||
command?: string;
|
||||
args?: string;
|
||||
sseUrl: string;
|
||||
env?: Record<string, string>;
|
||||
proxyServerUrl: string;
|
||||
bearerToken?: string;
|
||||
requestTimeout?: number;
|
||||
onNotification?: (notification: Notification) => void;
|
||||
onStdErrNotification?: (notification: Notification) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
onPendingRequest?: (request: any, resolve: any, reject: any) => void;
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
getRoots?: () => any[];
|
||||
}
|
||||
|
||||
interface RequestOptions {
|
||||
signal?: AbortSignal;
|
||||
timeout?: number;
|
||||
suppressToast?: boolean;
|
||||
}
|
||||
|
||||
export function useConnection({
|
||||
transportType,
|
||||
command,
|
||||
args,
|
||||
sseUrl,
|
||||
env,
|
||||
proxyServerUrl,
|
||||
bearerToken,
|
||||
requestTimeout,
|
||||
onNotification,
|
||||
onStdErrNotification,
|
||||
onPendingRequest,
|
||||
getRoots,
|
||||
}: UseConnectionOptions) {
|
||||
const [connectionStatus, setConnectionStatus] =
|
||||
useState<ConnectionStatus>("disconnected");
|
||||
const { message } = App.useApp()
|
||||
const [serverCapabilities, setServerCapabilities] =
|
||||
useState<ServerCapabilities | null>(null);
|
||||
const [mcpClient, setMcpClient] = useState<Client | null>(null);
|
||||
const [requestHistory, setRequestHistory] = useState<
|
||||
{ request: string; response?: string }[]
|
||||
>([]);
|
||||
const [completionsSupported, setCompletionsSupported] = useState(true);
|
||||
|
||||
const pushHistory = (request: object, response?: object) => {
|
||||
setRequestHistory((prev) => [
|
||||
...prev,
|
||||
{
|
||||
request: JSON.stringify(request),
|
||||
response: response !== undefined ? JSON.stringify(response) : undefined,
|
||||
},
|
||||
]);
|
||||
};
|
||||
|
||||
const makeRequest = async <T extends z.ZodType>(
|
||||
request: ClientRequest,
|
||||
schema: T,
|
||||
options?: RequestOptions,
|
||||
): Promise<z.output<T>> => {
|
||||
if (!mcpClient) {
|
||||
throw new Error("MCP client not connected");
|
||||
}
|
||||
|
||||
try {
|
||||
const abortController = new AbortController();
|
||||
const timeoutId = setTimeout(() => {
|
||||
abortController.abort("Request timed out");
|
||||
}, options?.timeout ?? requestTimeout);
|
||||
|
||||
let response;
|
||||
try {
|
||||
response = await mcpClient.request(request, schema, {
|
||||
signal: options?.signal ?? abortController.signal,
|
||||
});
|
||||
pushHistory(request, response);
|
||||
} catch (error) {
|
||||
const errorMessage =
|
||||
error instanceof Error ? error.message : String(error);
|
||||
pushHistory(request, { error: errorMessage });
|
||||
throw error;
|
||||
} finally {
|
||||
clearTimeout(timeoutId);
|
||||
}
|
||||
|
||||
return response;
|
||||
} catch (e: unknown) {
|
||||
if (!options?.suppressToast) {
|
||||
const errorString = (e as Error).message ?? String(e);
|
||||
message.error(errorString)
|
||||
}
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const handleCompletion = async (
|
||||
ref: ResourceReference | PromptReference,
|
||||
argName: string,
|
||||
value: string,
|
||||
signal?: AbortSignal,
|
||||
): Promise<string[]> => {
|
||||
if (!mcpClient || !completionsSupported) {
|
||||
return [];
|
||||
}
|
||||
|
||||
const request: ClientRequest = {
|
||||
method: "completion/complete",
|
||||
params: {
|
||||
argument: {
|
||||
name: argName,
|
||||
value,
|
||||
},
|
||||
ref,
|
||||
},
|
||||
};
|
||||
|
||||
try {
|
||||
const response = await makeRequest(request, CompleteResultSchema, {
|
||||
signal,
|
||||
suppressToast: true,
|
||||
});
|
||||
return response?.completion.values || [];
|
||||
} catch (e: unknown) {
|
||||
// Disable completions silently if the server doesn't support them.
|
||||
// See https://github.com/modelcontextprotocol/specification/discussions/122
|
||||
if (e instanceof McpError && e.code === ErrorCode.MethodNotFound) {
|
||||
setCompletionsSupported(false);
|
||||
return [];
|
||||
}
|
||||
|
||||
// Unexpected errors - show toast and rethrow
|
||||
message.error(e instanceof Error ? e.message : String(e))
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
|
||||
const sendNotification = async (notification: ClientNotification) => {
|
||||
if (!mcpClient) {
|
||||
const error = new Error("MCP client not connected");
|
||||
message.error(error.message)
|
||||
throw error;
|
||||
}
|
||||
|
||||
try {
|
||||
await mcpClient.notification(notification);
|
||||
// Log successful notifications
|
||||
pushHistory(notification);
|
||||
} catch (e: unknown) {
|
||||
if (e instanceof McpError) {
|
||||
// Log MCP protocol errors
|
||||
pushHistory(notification, { error: e.message });
|
||||
}
|
||||
message.error(e instanceof Error ? e.message : String(e))
|
||||
throw e;
|
||||
}
|
||||
};
|
||||
// TODO_先屏蔽,暂时不需要
|
||||
// const checkProxyHealth = async () => {
|
||||
// try {
|
||||
// const proxyHealthUrl = new URL(`${proxyServerUrl}/health`);
|
||||
// const proxyHealthResponse = await fetch(proxyHealthUrl);
|
||||
// const proxyHealth = await proxyHealthResponse.json();
|
||||
// if (proxyHealth?.status !== "ok") {
|
||||
// throw new Error("MCP Proxy Server is not healthy");
|
||||
// }
|
||||
// } catch (e) {
|
||||
// console.error("Couldn't connect to MCP Proxy Server", e);
|
||||
// throw e;
|
||||
// }
|
||||
// };
|
||||
// TODO_先屏蔽,暂时不需要
|
||||
// const handleAuthError = async (error: unknown) => {
|
||||
// if (error instanceof SseError && error.code === 401) {
|
||||
// sessionStorage.setItem(SESSION_KEYS.SERVER_URL, sseUrl);
|
||||
|
||||
// const result = await auth(authProvider, { serverUrl: sseUrl });
|
||||
// return result === "AUTHORIZED";
|
||||
// }
|
||||
|
||||
// return false;
|
||||
// };
|
||||
|
||||
const connect = async (_e?: unknown, retryCount: number = 0) => {
|
||||
const client = new Client<Request, Notification, Result>(
|
||||
{
|
||||
name: "mcp-inspector",
|
||||
version: '0.0.1',
|
||||
},
|
||||
{
|
||||
capabilities: {
|
||||
sampling: {},
|
||||
roots: {
|
||||
listChanged: true,
|
||||
},
|
||||
},
|
||||
},
|
||||
);
|
||||
// TODO_暂时不需要
|
||||
// try {
|
||||
// await checkProxyHealth();
|
||||
// } catch {
|
||||
// setConnectionStatus("error-connecting-to-proxy");
|
||||
// return;
|
||||
// }
|
||||
// 使用与http.ts一致的方式处理URL
|
||||
// 注意:proxyServerUrl应该是完整URL,或者我们需要为其添加基础URL
|
||||
// 处理两种情况:完整URL或相对路径
|
||||
let fullUrl;
|
||||
if (proxyServerUrl.startsWith('http://') || proxyServerUrl.startsWith('https://')) {
|
||||
// 如果是完整URL,直接使用
|
||||
fullUrl = `${proxyServerUrl}/sse`;
|
||||
} else {
|
||||
// 如果是相对路径,添加基础URL和API前缀
|
||||
const baseUrl = window.location.origin;
|
||||
const apiPrefix = '/api/v1/';
|
||||
fullUrl = `${baseUrl}${apiPrefix}${proxyServerUrl}`;
|
||||
}
|
||||
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)
|
||||
try {
|
||||
// Inject auth manually instead of using SSEClientTransport, because we're
|
||||
// proxying through the inspector server first.
|
||||
const headers: HeadersInit = {};
|
||||
|
||||
// TODO_暂时不需要。Use manually provided bearer token if available, otherwise use OAuth tokens
|
||||
// const token = bearerToken || (await authProvider.tokens())?.access_token;
|
||||
// if (token) {
|
||||
// headers["Authorization"] = `Bearer ${token}`;
|
||||
// }
|
||||
|
||||
// 创建SSE客户端传输层
|
||||
const clientTransport = new SSEClientTransport(mcpProxyServerUrl, {
|
||||
eventSourceInit: {
|
||||
fetch: (url, init) => fetch(url, { ...init, headers }),
|
||||
},
|
||||
requestInit: {
|
||||
headers,
|
||||
},
|
||||
});
|
||||
// TODO_暂时不需要
|
||||
// if (onNotification) {
|
||||
// [
|
||||
// CancelledNotificationSchema,
|
||||
// ProgressNotificationSchema,
|
||||
// LoggingMessageNotificationSchema,
|
||||
// ResourceUpdatedNotificationSchema,
|
||||
// ResourceListChangedNotificationSchema,
|
||||
// ToolListChangedNotificationSchema,
|
||||
// PromptListChangedNotificationSchema,
|
||||
// ].forEach((notificationSchema) => {
|
||||
// client.setNotificationHandler(notificationSchema, onNotification);
|
||||
// });
|
||||
|
||||
// client.fallbackNotificationHandler = (
|
||||
// notification: Notification,
|
||||
// ): Promise<void> => {
|
||||
// onNotification(notification);
|
||||
// return Promise.resolve();
|
||||
// };
|
||||
// }
|
||||
|
||||
// if (onStdErrNotification) {
|
||||
// client.setNotificationHandler(
|
||||
// StdErrNotificationSchema,
|
||||
// onStdErrNotification,
|
||||
// );
|
||||
// }
|
||||
|
||||
try {
|
||||
await client.connect(clientTransport);
|
||||
} catch (error) {
|
||||
console.error(
|
||||
`Failed to connect to MCP Server via the MCP Inspector Proxy: ${mcpProxyServerUrl}:`,
|
||||
error,
|
||||
);
|
||||
// TODO_先屏蔽,后续如果需要再处理
|
||||
// const shouldRetry = await handleAuthError(error);
|
||||
// if (shouldRetry) {
|
||||
// return connect(undefined, retryCount + 1);
|
||||
// }
|
||||
|
||||
if (error instanceof SseError && error.code === 401) {
|
||||
// Don't set error state if we're about to redirect for auth
|
||||
return;
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
|
||||
const capabilities = client.getServerCapabilities();
|
||||
setServerCapabilities(capabilities ?? null);
|
||||
setCompletionsSupported(true); // Reset completions support on new connection
|
||||
// TODO_暂时不需要
|
||||
// if (onPendingRequest) {
|
||||
// client.setRequestHandler(CreateMessageRequestSchema, (request) => {
|
||||
// return new Promise((resolve, reject) => {
|
||||
// onPendingRequest(request, resolve, reject);
|
||||
// });
|
||||
// });
|
||||
// }
|
||||
|
||||
// if (getRoots) {
|
||||
// client.setRequestHandler(ListRootsRequestSchema, async () => {
|
||||
// return { roots: getRoots() };
|
||||
// });
|
||||
// }
|
||||
|
||||
setMcpClient(client);
|
||||
setConnectionStatus("connected");
|
||||
} catch (e) {
|
||||
console.error(e);
|
||||
setConnectionStatus("error");
|
||||
}
|
||||
};
|
||||
|
||||
const disconnect = async () => {
|
||||
await mcpClient?.close();
|
||||
setMcpClient(null);
|
||||
setConnectionStatus("disconnected");
|
||||
setCompletionsSupported(false);
|
||||
setServerCapabilities(null);
|
||||
};
|
||||
|
||||
return {
|
||||
connectionStatus,
|
||||
serverCapabilities,
|
||||
mcpClient,
|
||||
requestHistory,
|
||||
makeRequest,
|
||||
sendNotification,
|
||||
handleCompletion,
|
||||
completionsSupported,
|
||||
connect,
|
||||
disconnect,
|
||||
};
|
||||
}
|
||||
@@ -11,10 +11,10 @@ import { normFile } from '@common/utils/uploadPic.ts'
|
||||
import { validateUrlSlash } from '@common/utils/validate.ts'
|
||||
import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx'
|
||||
import { AiServiceConfigFieldType } from '@core/const/ai-service/type.ts'
|
||||
import { SERVICE_APPROVAL_OPTIONS } from '@core/const/system/const.tsx'
|
||||
import { MCP_OPTIONS, SERVICE_APPROVAL_OPTIONS } from '@core/const/system/const.tsx'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import { CategorizesType } from '@market/const/serviceHub/type.ts'
|
||||
import { App, Button, Form, Input, Radio, Row, Select, Tooltip, TreeSelect, Upload } from 'antd'
|
||||
import { App, Button, Form, Input, Radio, RadioChangeEvent, Row, Select, Switch, Tooltip, TreeSelect, Upload } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/cascader'
|
||||
import { RcFile, UploadChangeParam, UploadFile, UploadProps } from 'antd/es/upload/interface'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useMemo, useState } from 'react'
|
||||
@@ -23,6 +23,7 @@ import { v4 as uuidv4 } from 'uuid'
|
||||
import { SystemConfigFieldType, SystemConfigHandle } from '../../const/system/type.ts'
|
||||
import { useSystemContext } from '../../contexts/SystemContext.tsx'
|
||||
import { Codebox } from '@common/components/postcat/api/Codebox/index.tsx'
|
||||
import { useAiServiceContext } from '@core/contexts/AiServiceContext.tsx'
|
||||
|
||||
export type SimpleAiProviderItem = EntityItem & {
|
||||
configured: boolean
|
||||
@@ -39,6 +40,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
const navigate = useNavigate()
|
||||
const { setBreadcrumb } = useBreadcrumb()
|
||||
const { setSystemInfo } = useSystemContext()
|
||||
const { setAiServiceInfo } = useAiServiceContext()
|
||||
const [showClassify, setShowClassify] = useState<boolean>(true)
|
||||
const [showAI, setShowAI] = useState<boolean>(false)
|
||||
const [imageBase64, setImageBase64] = useState<string | null>(null)
|
||||
@@ -274,6 +276,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
setSystemInfo(data.service)
|
||||
setAiServiceInfo(data.service)
|
||||
return Promise.resolve(true)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -359,10 +362,35 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
form.setFieldValue('team', teamId)
|
||||
form.setFieldValue('serviceType', 'public')
|
||||
form.setFieldValue('approvalType', 'auto')
|
||||
form.setFieldValue('enable_mcp', false)
|
||||
}
|
||||
return form.setFieldsValue({})
|
||||
}, [serviceId])
|
||||
|
||||
const handleMcpChange = (e: RadioChangeEvent) => {
|
||||
if (e.target.value) {
|
||||
return
|
||||
}
|
||||
modal.confirm({
|
||||
title: $t('关闭 MCP'),
|
||||
content: $t('关闭后将无法通过MCP方式调用服务'),
|
||||
onOk: () => {
|
||||
form.setFieldValue('enable_mcp', false)
|
||||
},
|
||||
onCancel: () => {
|
||||
form.setFieldValue('enable_mcp', true)
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
okButtonProps: {
|
||||
danger: true
|
||||
},
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const deleteSystemModal = async () => {
|
||||
modal.confirm({
|
||||
title: $t('删除'),
|
||||
@@ -387,6 +415,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
() => SERVICE_APPROVAL_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })),
|
||||
[state.language]
|
||||
)
|
||||
const mcpOptions = useMemo(() => MCP_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })), [state.language])
|
||||
|
||||
return (
|
||||
<>
|
||||
@@ -440,6 +469,9 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
</Radio.Group>
|
||||
</Form.Item>
|
||||
)}
|
||||
<Form.Item<AiServiceConfigFieldType> label={$t('MCP')} name="enable_mcp" rules={[{ required: true }]}>
|
||||
<Radio.Group className="flex flex-col" options={mcpOptions} onChange={serviceId ? handleMcpChange : undefined}/>
|
||||
</Form.Item>
|
||||
{showAI && (
|
||||
<>
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
@@ -477,10 +509,14 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={modelList ? modelList.map((x) => ({
|
||||
...x,
|
||||
searchText: x.name.toLowerCase()
|
||||
})) : []}
|
||||
options={
|
||||
modelList
|
||||
? modelList.map((x) => ({
|
||||
...x,
|
||||
searchText: x.name.toLowerCase()
|
||||
}))
|
||||
: []
|
||||
}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
</>
|
||||
|
||||
@@ -234,6 +234,7 @@ const SystemInsidePage: FC = () => {
|
||||
<InsidePage
|
||||
pageTitle={systemInfo?.name || '-'}
|
||||
tagList={[
|
||||
...(systemInfo?.enable_mcp ? [{ label: 'MCP', color: '#ffc107', className: 'text-[#000]' }] : []),
|
||||
{
|
||||
label: (
|
||||
<Paragraph className="mb-0" copyable={serviceId ? { text: serviceId } : false}>
|
||||
|
||||
@@ -8,7 +8,7 @@ import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
import { useFetch } from '@common/hooks/http.ts'
|
||||
import { $t } from '@common/locales/index.ts'
|
||||
import { App } from 'antd'
|
||||
import { App, Tag } from 'antd'
|
||||
import { FC, useEffect, useMemo, useRef, useState } from 'react'
|
||||
import { useNavigate } from 'react-router-dom'
|
||||
import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/const.tsx'
|
||||
@@ -176,10 +176,16 @@ const SystemList: FC = () => {
|
||||
x.valueEnum = teamList
|
||||
}
|
||||
if ((x.dataIndex as string) === 'service_kind') {
|
||||
x.valueEnum = {}
|
||||
SERVICE_KIND_OPTIONS.forEach((option) => {
|
||||
;(x.valueEnum as any)[option.value] = { text: $t(option.label) }
|
||||
})
|
||||
x.render = (dom: React.ReactNode, record: any) => (
|
||||
<span
|
||||
className={`text-[13px] `}
|
||||
>
|
||||
{$t(SERVICE_KIND_OPTIONS.find((x) => x.value === record.service_kind)?.label || '-')}
|
||||
{record.enable_mcp && (
|
||||
<Tag color="#ffc107" className="text-[#000] ml-[5px]">MCP</Tag>
|
||||
)}
|
||||
</span>
|
||||
)
|
||||
}
|
||||
if ((x.dataIndex as string) === 'state') {
|
||||
x.render = (dom: React.ReactNode, record: any) => (
|
||||
|
||||
Reference in New Issue
Block a user