feature/1.7-MCP

This commit is contained in:
ningyv
2025-04-10 09:20:28 +08:00
parent 9617ddc02c
commit c49bddb9e7
20 changed files with 714 additions and 18 deletions
+2 -1
View File
@@ -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) => (