mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
feature/1.7-MCP
This commit is contained in:
@@ -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",
|
||||
|
||||
@@ -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 = {
|
||||
|
||||
@@ -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}>
|
||||
|
||||
@@ -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,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<string>('')
|
||||
const [apiKey, setApiKey] = useState<string>('暂无数据')
|
||||
const [apiKeyList, setApiKeyList] = useState<{ value: string; label: 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))
|
||||
})
|
||||
}
|
||||
|
||||
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 (
|
||||
<>
|
||||
<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
|
||||
@@ -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'
|
||||
@@ -359,10 +359,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 +412,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 +466,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 +506,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