From c49bddb9e78e6ce813b8621b9c8cb6bbb6eab7b0 Mon Sep 17 00:00:00 2001 From: ningyv <1793599591@qq.com> Date: Thu, 10 Apr 2025 09:20:28 +0800 Subject: [PATCH] feature/1.7-MCP --- frontend/package.json | 3 +- .../src/components/aoplatform/InsidePage.tsx | 4 +- .../src/contexts/GlobalStateContext.tsx | 14 + .../packages/common/src/hooks/pluginLoader.ts | 20 ++ .../common/src/locales/scan/en-US.json | 4 +- .../common/src/locales/scan/ja-JP.json | 4 +- .../common/src/locales/scan/zh-CN.json | 4 +- .../common/src/locales/scan/zh-TW.json | 4 +- .../core/src/const/ai-service/type.ts | 1 + frontend/packages/core/src/const/const.tsx | 16 ++ .../packages/core/src/const/system/const.tsx | 4 + .../packages/core/src/const/system/type.ts | 1 + .../pages/aiService/AiServiceInsidePage.tsx | 1 + .../core/src/pages/mcpService/AddMcpKey.tsx | 101 +++++++ .../mcpService/IntegrationAIContainer.tsx | 260 ++++++++++++++++++ .../src/pages/mcpService/McpKeyContainer.tsx | 200 ++++++++++++++ .../pages/mcpService/McpServiceContainer.tsx | 29 ++ .../core/src/pages/system/SystemConfig.tsx | 45 ++- .../src/pages/system/SystemInsidePage.tsx | 1 + .../core/src/pages/system/SystemList.tsx | 16 +- 20 files changed, 714 insertions(+), 18 deletions(-) create mode 100644 frontend/packages/core/src/pages/mcpService/AddMcpKey.tsx create mode 100644 frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx create mode 100644 frontend/packages/core/src/pages/mcpService/McpKeyContainer.tsx create mode 100644 frontend/packages/core/src/pages/mcpService/McpServiceContainer.tsx diff --git a/frontend/package.json b/frontend/package.json index 5998344c..9b1c9a92 100644 --- a/frontend/package.json +++ b/frontend/package.json @@ -47,7 +47,8 @@ "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" }, "devDependencies": { "@ant-design/cssinjs": "^1.18.2", diff --git a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx index 484a0814..7d9e7697 100644 --- a/frontend/packages/common/src/components/aoplatform/InsidePage.tsx +++ b/frontend/packages/common/src/components/aoplatform/InsidePage.tsx @@ -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 = ({ tagList?.length > 0 && tagList?.map((tag) => { return ( - + {tag.label} ) diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx index d230cc6d..a56c9448 100644 --- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -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', diff --git a/frontend/packages/common/src/hooks/pluginLoader.ts b/frontend/packages/common/src/hooks/pluginLoader.ts index 84f06bc4..f9ae66b1 100644 --- a/frontend/packages/common/src/hooks/pluginLoader.ts +++ b/frontend/packages/common/src/hooks/pluginLoader.ts @@ -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', diff --git a/frontend/packages/common/src/locales/scan/en-US.json b/frontend/packages/common/src/locales/scan/en-US.json index 57f0ba51..23047b4d 100644 --- a/frontend/packages/common/src/locales/scan/en-US.json +++ b/frontend/packages/common/src/locales/scan/en-US.json @@ -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." } diff --git a/frontend/packages/common/src/locales/scan/ja-JP.json b/frontend/packages/common/src/locales/scan/ja-JP.json index 61ea7256..694a8894 100644 --- a/frontend/packages/common/src/locales/scan/ja-JP.json +++ b/frontend/packages/common/src/locales/scan/ja-JP.json @@ -920,5 +920,7 @@ "Kce2fcdbf": "権限がありません", "K24f6a5b4": "カスタム(空のテンプレート)", "Kea608112": "プリセットテンプレートを読み込む", - "Kee7de862": "サプライヤーを編集( (0) )" + "Kee7de862": "サプライヤーを編集( (0) )", + "Kb0e0aeda": "APIキーを新規作成", + "K9d81999c": "APIキーは、システムレベルのOpen APIおよびMCPの呼び出しに使用できます。" } diff --git a/frontend/packages/common/src/locales/scan/zh-CN.json b/frontend/packages/common/src/locales/scan/zh-CN.json index f009105f..b8d834ae 100644 --- a/frontend/packages/common/src/locales/scan/zh-CN.json +++ b/frontend/packages/common/src/locales/scan/zh-CN.json @@ -851,5 +851,7 @@ "Kce2fcdbf": "暂无权限", "K24f6a5b4": "自定义(空模板)", "Kea608112": "载入预置模板", - "Kee7de862": "编辑供应商( (0) )" + "Kee7de862": "编辑供应商( (0) )", + "Kb0e0aeda": "新增 API Key", + "K9d81999c": "API 密钥可用于调用系统级 Open API 和 MCP。" } diff --git a/frontend/packages/common/src/locales/scan/zh-TW.json b/frontend/packages/common/src/locales/scan/zh-TW.json index fe2aae97..72e7bbfc 100644 --- a/frontend/packages/common/src/locales/scan/zh-TW.json +++ b/frontend/packages/common/src/locales/scan/zh-TW.json @@ -920,5 +920,7 @@ "Kce2fcdbf": "暫無權限", "K24f6a5b4": "自訂(空模板)", "Kea608112": "載入預設模板", - "Kee7de862": "編輯供應商( (0) )" + "Kee7de862": "編輯供應商( (0) )", + "Kb0e0aeda": "新增 API 金鑰", + "K9d81999c": "API 金鑰可用於調用系統級 Open API 和 MCP。" } diff --git a/frontend/packages/core/src/const/ai-service/type.ts b/frontend/packages/core/src/const/ai-service/type.ts index 0c3e2841..82c3825c 100644 --- a/frontend/packages/core/src/const/ai-service/type.ts +++ b/frontend/packages/core/src/const/ai-service/type.ts @@ -21,6 +21,7 @@ export type AiServiceConfigFieldType = { catalogue?:string | string[]; approvalType?:string; providerType?:string + enable_mcp?: boolean }; export type AiServiceSubServiceTableListItem = { diff --git a/frontend/packages/core/src/const/const.tsx b/frontend/packages/core/src/const/const.tsx index 1ee3f57b..f763da82 100644 --- a/frontend/packages/core/src/const/const.tsx +++ b/frontend/packages/core/src/const/const.tsx @@ -800,6 +800,22 @@ export const routerMap: Map = 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', { diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index bfd95cf1..b7dee443 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -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' } diff --git a/frontend/packages/core/src/const/system/type.ts b/frontend/packages/core/src/const/system/type.ts index c5f05336..9d4a82fe 100644 --- a/frontend/packages/core/src/const/system/type.ts +++ b/frontend/packages/core/src/const/system/type.ts @@ -31,6 +31,7 @@ export type SystemConfigFieldType = { catalogue?:string | string[]; approvalType?:string; modelMapping?: string; + enable_mcp?: boolean; }; export type SystemSubServiceTableListItem = { diff --git a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx index c438539f..025ac7fd 100644 --- a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx +++ b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx @@ -228,6 +228,7 @@ const AiServiceInsidePage: FC = () => { diff --git a/frontend/packages/core/src/pages/mcpService/AddMcpKey.tsx b/frontend/packages/core/src/pages/mcpService/AddMcpKey.tsx new file mode 100644 index 00000000..2c786c4a --- /dev/null +++ b/frontend/packages/core/src/pages/mcpService/AddMcpKey.tsx @@ -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 +} + +type addMcpKeysProps = { + name?: string + value?: string + type?: string + apikey?: string +} + +const AddMcpKey = forwardRef((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 = () => { + return new Promise((resolve, reject) => { + try { + form + .validateFields() + .then((value) => { + console.log('value', value) + const finalValue = { + ...value, + value: editValue ? editValue : uuidv4(), + expired: 0 + } + fetchData>('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 ( +
+ label={$t('名称')} name="name" rules={[{ required: true }]}> + + + + ) +}) + +export default AddMcpKey diff --git a/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx new file mode 100644 index 00000000..80b99fdc --- /dev/null +++ b/frontend/packages/core/src/pages/mcpService/IntegrationAIContainer.tsx @@ -0,0 +1,260 @@ +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' + +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('') + const [apiKey, setApiKey] = useState('暂无数据') + const [apiKeyList, setApiKeyList] = useState<{ value: string; label: string }[]>([]) + const [tabContent, setTabContent] = useState({ + 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 => { + 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>('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>(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)) + }) + } + + useEffect(() => { + type === 'global' && getGlobalMcpConfig() + initTabsData() + getKeysList() + }, []) + useEffect(() => { + if (activeTab === 'openApi') { + setConfigContent(tabContent.openApi?.configContent || '') + } else if (activeTab === 'mcp') { + setConfigContent(tabContent.mcp.configContent || '') + } + }, [tabContent, activeTab]) + return ( + <> + +

+ + {$t('AI 代理集成')} +

+
+ {type === 'service' && ( +
+
setActiveTab('openApi')} + > + Open API +
+
setActiveTab('mcp')} + > + MCP +
+
+ )} +
+ {activeTab === 'openApi' ? tabContent.openApi?.title : tabContent.mcp.title} +
+ {/* 标签页内容区域 */} +
+ + handleCopy(configContent)} + sx={{ + position: 'absolute', + top: '5px', + right: '5px', + color: '#999', + transition: 'none', + '&.MuiButtonBase-root:hover': { + background: 'transparent', + color: '#3D46F2', + transition: 'none' + } + }} + > +
+
+ {activeTab === 'mcp' && ( + <> +
API Key
+ diff --git a/frontend/packages/core/src/pages/system/SystemInsidePage.tsx b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx index ef1fee32..d7a174dc 100644 --- a/frontend/packages/core/src/pages/system/SystemInsidePage.tsx +++ b/frontend/packages/core/src/pages/system/SystemInsidePage.tsx @@ -234,6 +234,7 @@ const SystemInsidePage: FC = () => { diff --git a/frontend/packages/core/src/pages/system/SystemList.tsx b/frontend/packages/core/src/pages/system/SystemList.tsx index 4e0f5f12..06b53ab5 100644 --- a/frontend/packages/core/src/pages/system/SystemList.tsx +++ b/frontend/packages/core/src/pages/system/SystemList.tsx @@ -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) => ( + + {$t(SERVICE_KIND_OPTIONS.find((x) => x.value === record.service_kind)?.label || '-')} + {record.enable_mcp && ( + MCP + )} + + ) } if ((x.dataIndex as string) === 'state') { x.render = (dom: React.ReactNode, record: any) => (