Merge pull request #207 from APIParkLab/feature/1.5-cx

Feature/1.5 cx
This commit is contained in:
ningyv
2025-02-19 18:41:45 +08:00
committed by GitHub
21 changed files with 528 additions and 192 deletions
+1 -1
View File
@@ -43,7 +43,7 @@
"react-dom": "^18.2.0",
"react-i18next": "^15.0.1",
"react-joyride": "^2.8.2",
"react-router-dom": "^6.20.0",
"react-router-dom": "6.20.0",
"swagger-ui-react": "^5.17.14",
"tailwindcss": "^3.3.5",
"uuid": "^9.0.1",
@@ -241,6 +241,9 @@ function BasicLayout({ project = 'core' }: { project: string }) {
headerTitleRender={() => (
<div className="w-[192px] flex items-center">
<img className="h-[20px] cursor-pointer " src={Logo} onClick={() => navigator(mainPage)} />
<a className="align-text-top" href="https://github.com/APIParkLab/APIPark" target="_blank" className="ml-[5px] h-[25px] relative">
<img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social" className='absolute top-[6px]' width={75} alt="" />
</a>
</div>
)}
logo={Logo}
@@ -280,9 +283,9 @@ function BasicLayout({ project = 'core' }: { project: string }) {
collapsedButtonRender={false}
>
<div
className={`w-full h-calc-100vh-minus-navbar pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T ${
className={`w-full h-calc-100vh-minus-navbar ${
currentUrl.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden'
}`}
} ${currentUrl.startsWith('/guide/page') ? '' : 'pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T'}`}
>
<Outlet />
</div>
@@ -22,6 +22,7 @@ class InsidePageProps {
headerClassName?: string = ''
/** 整个页面滚动 */
scrollPage?: boolean = true
customPadding?: boolean
customBtn?: ReactNode
}
@@ -41,6 +42,7 @@ const InsidePage: FC<InsidePageProps> = ({
contentClassName = '',
headerClassName = '',
scrollPage = true,
customPadding = false,
customBtn
}) => {
const navigate = useNavigate()
@@ -57,7 +59,7 @@ const InsidePage: FC<InsidePageProps> = ({
{!pageTitle && !description && !backUrl && !customBtn ? (
<></>
) : (
<div className="mb-[30px]">
<div className={customPadding ? '' : 'mb-[30px]'}>
{backUrl && (
<div className="text-[18px] leading-[25px] mb-[12px]">
<Button type="text" onClick={goBack}>
@@ -41,6 +41,7 @@ interface PageListProps<T> extends ProTableProps<T, unknown>, RefAttributes<Acti
beforeSearchNode?: React.ReactNode[]
onSearchWordChange?: (e: ChangeEvent<HTMLInputElement>) => void
afterNewBtn?: React.ReactNode[]
beforeNewBtn?: React.ReactNode[]
dragSortKey?: string
onDragSortEnd?: (beforeIndex: number, afterIndex: number, newDataSource: T[]) => void | Promise<void>
tableTitle?: string
@@ -56,7 +57,8 @@ interface PageListProps<T> extends ProTableProps<T, unknown>, RefAttributes<Acti
delayLoading?: boolean
noScroll?: boolean
/* 前端分页的表格,需要传入该字段以支持后端搜索 */
manualReloadTable?: () => void
manualReloadTable?: () => void,
customEmptyRender?: () => React.ReactNode
}
const PageList = <T extends Record<string, unknown>>(
@@ -80,6 +82,7 @@ const PageList = <T extends Record<string, unknown>>(
onSearchWordChange,
manualReloadTable,
afterNewBtn,
beforeNewBtn,
dragSortKey,
onDragSortEnd,
tableTitle,
@@ -94,7 +97,8 @@ const PageList = <T extends Record<string, unknown>>(
tableTitleClass,
delayLoading = true,
besidesTableHeight,
noScroll
noScroll,
customEmptyRender
} = props
const parentRef = useRef<HTMLDivElement>(null)
const [tableHeight, setTableHeight] = useState(minVirtualHeight || window.innerHeight)
@@ -190,6 +194,7 @@ const PageList = <T extends Record<string, unknown>>(
const headerTitle = () => {
return (
<>
{beforeNewBtn ? (beforeNewBtn as React.ReactNode[]) : undefined}
{tableTitle ? (
<span className={`text-[30px] leading-[42px] my-mbase pl-[20px] ${tableTitleClass}`}>{tableTitle}</span>
) : addNewBtnTitle ? (
@@ -345,6 +350,11 @@ const PageList = <T extends Record<string, unknown>>(
)
onChange?.(pagination, filters, sorter, extra)
}}
locale={{
emptyText: customEmptyRender ? customEmptyRender?.() : undefined
}}
style={customEmptyRender ? { height: '100%' } : undefined}
bodyStyle={customEmptyRender ? { height: '100%' } : undefined}
rowKey={primaryKey}
dataSource={dataSource}
search={false}
@@ -148,7 +148,7 @@ const mockData = [
access: 'system.settings.ai_provider.view'
},
{
name: 'APIKey 资源池',
name: 'API Key 负载',
key: 'aiKeys',
path: '/keysetting',
icon: 'ic:baseline-key',
@@ -162,7 +162,7 @@ const mockData = [
access: 'system.settings.ai_api.view'
},
{
name: '负载均衡',
name: '模型灾备',
key: 'loadBalancing',
path: '/loadBalancing',
icon: 'ph:network-x',
@@ -850,5 +850,25 @@
"Kbe98ba9e": "Private Service",
"K24540de": "Stop",
"Kd85b3f64": "Continue Waiting",
"K1400a1fc": "As a prefix for all APIs within the service, such as host/{service_name}/{api_path}. This has a significant impact, so modify with caution"
"K1400a1fc": "As a prefix for all APIs within the service, such as host/{service_name}/{api_path}. This has a significant impact, so modify with caution",
"Kb8185132": "OR",
"K83829a3b": "Model Exception",
"Kb92fb02b": "Deployment Failed",
"Kcf8e3b18": "Tags",
"K9ae48909": "API Key Balancing",
"K437724fc": "Supports creating multiple API Keys for intelligent load balancing under a single API model provider",
"K7d17707e": "Model Fallback",
"Kc007db4a": "Only .png, .jpg, .jpeg, .svg image files are supported",
"Kacf10c44": "Ollama Endpoint",
"K8d4f5b44": "Input example: https://www.apipark.com",
"K481442d3": "Configure Ollama Service",
"Kf9b341e3": "How to deploy Ollama?",
"K8632bef2": "Model deployment service not configured",
"Kbbd8ce81": "Configure Service",
"K39a8d392": "Import OpenAPI documents to publish existing system APIs to APIPark.",
"Ka742e079": "Add API Key for public cloud AI models to call public cloud AI models via APIPark.",
"K8097d6be": "is an open-source AI Gateway and API Portal that unifies access to OpenAI, DeepSeek, and other AI models. With enterprise-grade security features and real-time monitoring, it helps teams safely manage and share their AI APIs through a unified gateway.",
"Kf1ce5b3": "✨ We'd love your support on Github! Leave us a star or share your feedback. ",
"K3af90490": "⚡ You can quickly open the API for everyone to use via the following methods:",
"K6b99dce8": "address"
}
@@ -872,5 +872,25 @@
"Kbe98ba9e": "プライベートサービス",
"K24540de": "停止",
"Kd85b3f64": "引き続き待機",
"K1400a1fc": "サービス内のすべてのAPIのプレフィックスとして使用されます。例えば host/{service_name}/{api_path} のように、大きな影響を与えるため、慎重に変更してください。"
"K1400a1fc": "サービス内のすべてのAPIのプレフィックスとして使用されます。例えば host/{service_name}/{api_path} のように、大きな影響を与えるため、慎重に変更してください。",
"Kb8185132": "または",
"K83829a3b": "モデル異常",
"Kb92fb02b": "デプロイ失敗",
"Kcf8e3b18": "タグ",
"K9ae48909": "APIキーのペイロード",
"K437724fc": "1つのAPIモデルプロバイダーで複数のAPIキーを作成し、インテリジェント負荷分散をサポート",
"K7d17707e": "モデルフォールバック",
"Kc007db4a": "画像ファイルは .png, .jpg, .jpeg, .svg 形式のみサポートされています",
"Kacf10c44": "Ollama エンドポイント",
"K8d4f5b44": "入力例:https://www.apipark.com",
"K481442d3": "Ollama サービスの設定",
"Kf9b341e3": "Ollamaのデプロイ方法は?",
"K8632bef2": "モデルデプロイサービスが設定されていません",
"Kbbd8ce81": "サービスの設定",
"K39a8d392": "OpenAPIドキュメントをインポートし、既存のシステムのAPIをAPIParkに公開します。",
"Ka742e079": "パブリッククラウドAIモデルのAPIキーを追加し、APIParkを介してパブリッククラウドのAIモデルを統一的に呼び出します。",
"K8097d6be": "OpenAIやDeepSeekなどのさまざまなAIモデルに迅速にアクセスできるオープンソースのワンストップAIゲートウェイおよびAPIポータルです。統一されたリクエスト形式を使用して、モデルの切り替えによるビジネスへの影響を回避し、企業レベルのAPIセキュリティ(認証/レート制限/センシティブワードフィルタリング)とリアルタイムの使用量監視を提供します。チーム内でのAPI共有やコラボレーションをサポートし、インターフェースのサブスクリプション認証を管理してAPIのセキュリティを確保します。",
"Kf1ce5b3": "✨ Githubでスターを付けていただくか、製品フィードバックをお寄せください。",
"K3af90490": "⚡ 以下の方法で、APIをすぐに公開して皆さんに利用してもらえます:",
"K6b99dce8": "Ollama アドレス"
}
@@ -803,5 +803,25 @@
"Kbe98ba9e": "私有服务",
"K24540de": "停止",
"Kd85b3f64": "继续等待",
"K1400a1fc": "作为服务内所有API的前缀,比如host/{service_name}/{api_path},影响较大,谨慎修改"
"K1400a1fc": "作为服务内所有API的前缀,比如host/{service_name}/{api_path},影响较大,谨慎修改",
"Kb8185132": "或",
"K83829a3b": "模型异常",
"Kb92fb02b": "部署失败",
"Kcf8e3b18": "Tags",
"K9ae48909": "API Key 负载",
"K437724fc": "支持单个 API 模型供应商下创建多个 API Key 进行智能负载均衡",
"K7d17707e": "模型灾备",
"Kc007db4a": "仅支持 .png .jpg .jpeg .svg 格式的图片文件",
"Kacf10c44": "Ollama 端点",
"K8d4f5b44": "输入例如:https://www.apipark.com",
"K481442d3": "配置 Ollama 服务",
"Kf9b341e3": "如何部署 Ollama",
"K8632bef2": "模型部署服务未配置",
"Kbbd8ce81": "配置服务",
"K39a8d392": "导入OpenAPI文档,将现有系统的API发布到APIPark。",
"Ka742e079": "添加公有云AI模型的 API Key,通过APIPark 统一调用公有云的AI模型。",
"K8097d6be": "是开源的一站式 AI 网关与 API 门户,可快速接入 OpenAI/DeepSeek 等各类 AI 模型,通过统一请求格式避免模型切换对业务造成影响,提供企业级 API 安全防护(鉴权/限流/敏感词过滤)与实时用量监控,支持团队内 API 共享协作,管理接口订阅授权并保证您的API安全。",
"Kf1ce5b3": "✨ 欢迎在 Github 为我们 Star 或提供产品反馈意见。",
"K3af90490": "⚡您可快速通过以下方式开放API供大家使用:",
"K6b99dce8": "Ollama 地址"
}
@@ -872,5 +872,25 @@
"Kbe98ba9e": "私有服務",
"K24540de": "停止",
"Kd85b3f64": "繼續等待",
"K1400a1fc": "作為服務內所有 API 的前綴,例如 host/{service_name}/{api_path},這會產生較大的影響,請謹慎修改"
"K1400a1fc": "作為服務內所有 API 的前綴,例如 host/{service_name}/{api_path},這會產生較大的影響,請謹慎修改",
"Kb8185132": "或",
"K83829a3b": "模型異常",
"Kb92fb02b": "部署失敗",
"Kcf8e3b18": "標籤",
"K9ae48909": "API Key 負載",
"K437724fc": "支持在單個 API 模型供應商下創建多個 API Key 進行智能負載均衡",
"K7d17707e": "模型災備",
"Kc007db4a": "僅支持 .png, .jpg, .jpeg, .svg 格式的圖片文件",
"Kacf10c44": "Ollama 端點",
"K8d4f5b44": "輸入範例:https://www.apipark.com",
"K481442d3": "配置 Ollama 服務",
"Kf9b341e3": "如何部署 Ollama",
"K8632bef2": "模型部署服務未配置",
"Kbbd8ce81": "配置服務",
"K39a8d392": "導入 OpenAPI 文件,將現有系統的 API 發佈到 APIPark。",
"Ka742e079": "添加公有雲 AI 模型的 API Key,通過 APIPark 統一調用公有雲的 AI 模型。",
"K8097d6be": "是一個開源的一站式 AI 閘道和 API 入口網站,可快速接入 OpenAI/DeepSeek 等各類 AI 模型,通過統一的請求格式避免模型切換對業務造成影響,提供企業級 API 安全防護(鑑權/限流/敏感詞過濾)與實時用量監控,支持團隊內 API 共享協作,管理介面訂閱授權並保證您的 API 安全。",
"Kf1ce5b3": "✨ 歡迎在 Github 為我們 Star 或提供產品反饋意見。",
"K3af90490": "⚡ 您可以快速通過以下方式開放 API 供大家使用:",
"K6b99dce8": "Ollama 地址"
}
@@ -9,6 +9,7 @@ export type AiServiceConfigFieldType = {
name?: string;
id?: string;
provider?:string
model?:string
prefix?:string;
logo?:string;
logoFile?:UploadFile;
+7
View File
@@ -996,6 +996,13 @@ p{
min-width:unset !important;
}
.local-model-list .ant-pro-table .ant-table-body {
overflow: hidden !important;
}
.local-model-list .ant-pro-table td {
border-bottom: none !important;
}
.table-border {
.ant-table:not(.ant-table-bordered){
border:1px solid var(--border-color) !important;
@@ -189,7 +189,7 @@ const AiServiceInsideRouterCreate = () => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setLlmList(data.models)
const localId = id || aiServiceInfo?.id
const localId = id || aiServiceInfo?.model
if (replaceDefaultLlm) {
setDefaultLlm((prev) => {
@@ -0,0 +1,85 @@
import { forwardRef, useEffect, useImperativeHandle } from 'react'
import { App, Divider, Form, Space, Switch, Tag, Input } from 'antd'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { $t } from '@common/locales'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { useFetch } from '@common/hooks/http'
export type ConfigureOllamaServiceHandle = {
save: () => Promise<boolean | string>
}
const ConfigureOllamaService = forwardRef<ConfigureOllamaServiceHandle, any>((props, ref) => {
const { address = '' } = props
const [form] = Form.useForm()
const { fetchData } = useFetch()
const { message } = App.useApp()
useEffect(() => {
form.setFieldsValue({ address })
}, [])
/**
*
* @returns
*/
const save: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
try {
form
.validateFields()
.then((value) => {
fetchData<BasicResponse<null>>('model/local/source/ollama', {
method: 'PUT',
eoBody: { address: value.address }
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
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 (
<WithPermission access="">
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="mx-auto "
name="partitionInsideCert"
autoComplete="off"
>
<Form.Item
name="address"
rules={[{ required: true, whitespace: true }]}
className="p-4 bg-white rounded-lg"
label={$t('Ollama 地址')}
>
<Input
placeholder={$t('输入例如:https://www.apipark.com')}
value={address}
onChange={(e) => form.setFieldValue('address', e.target.value)}
/>
</Form.Item>
</Form>
</WithPermission>
)
})
export default ConfigureOllamaService
@@ -4,13 +4,15 @@ import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPe
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Divider, Form, Space, Switch, Tag } from 'antd'
import { App, Divider, Form, Space, Switch, Tag, Button } from 'antd'
import React, { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { ModelListData } from './types'
import LocalAiDeploy, { LocalAiDeployHandle } from '../guide/LocalAiDeploy'
import { ServiceDeployment } from '../system/serviceDeployment/ServiceDeployment'
import { LogsFooter } from '../system/serviceDeployment/ServiceDeployMentFooter'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { Icon } from '@iconify/react/dist/iconify.js'
import ConfigureOllamaService, { ConfigureOllamaServiceHandle } from './ConfigureOllamaService'
type EditLocalModelModalHandle = {
save: () => Promise<boolean | string>
}
@@ -18,20 +20,21 @@ type EditLocalModelModalProps = {
enable: boolean
modelID?: string
}
const EditLocalModelModal = forwardRef<EditLocalModelModalHandle, EditLocalModelModalProps>((props: EditLocalModelModalProps, ref) => {
const { enable, modelID } = props
const { fetchData } = useFetch()
const { message } = App.useApp()
const [form] = Form.useForm()
const [currentStatus, setCurrentStatus] = useState<boolean>(enable)
const EditLocalModelModal = forwardRef<EditLocalModelModalHandle, EditLocalModelModalProps>(
(props: EditLocalModelModalProps, ref) => {
const { enable, modelID } = props
const { fetchData } = useFetch()
const { message } = App.useApp()
const [form] = Form.useForm()
const [currentStatus, setCurrentStatus] = useState<boolean>(enable)
useEffect(() => {
form.setFieldsValue({ enable })
}, [])
useEffect(() => {
form.setFieldsValue({ enable })
}, [])
/**
*
* @returns
*/
*
* @returns
*/
const save: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
try {
@@ -41,22 +44,23 @@ const EditLocalModelModal = forwardRef<EditLocalModelModalHandle, EditLocalModel
const finalValue = {
disable: !value.enable
}
fetchData<BasicResponse<null>>('model/local/info', {
method: 'PUT',
eoParams: { model: modelID },
eoBody: finalValue,
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
resolve(true)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo) => reject(errorInfo))
fetchData<BasicResponse<null>>('model/local/info', {
method: 'PUT',
eoParams: { model: modelID },
eoBody: finalValue
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
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) {
@@ -68,40 +72,41 @@ const EditLocalModelModal = forwardRef<EditLocalModelModalHandle, EditLocalModel
save
}))
return (
<WithPermission access="">
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="mx-auto "
name="partitionInsideCert"
autoComplete="off"
>
<Form.Item className="p-4 bg-white rounded-lg" label={$t('LLM 状态管理')}>
<div className="flex justify-between items-center">
<div>
<span className="text-gray-600">{$t('当前调用状态:')}</span>
{currentStatus && <Tag color="success">{$t('正常')}</Tag>}
{!currentStatus && <Tag color="warning">{$t('停用')}</Tag>}
</div>
<Form.Item name="enable" valuePropName="checked" noStyle>
<Switch
checkedChildren={$t('启用')}
unCheckedChildren={$t('停用')}
onChange={(checked) => {
form.setFieldsValue({ enable: checked })
setCurrentStatus(checked)
}}
/>
</Form.Item>
</div>
</Form.Item>
</Form>
</WithPermission>
)
})
return (
<WithPermission access="">
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="mx-auto "
name="partitionInsideCert"
autoComplete="off"
>
<Form.Item className="p-4 bg-white rounded-lg" label={$t('LLM 状态管理')}>
<div className="flex justify-between items-center">
<div>
<span className="text-gray-600">{$t('当前调用状态:')}</span>
{currentStatus && <Tag color="success">{$t('正常')}</Tag>}
{!currentStatus && <Tag color="warning">{$t('停用')}</Tag>}
</div>
<Form.Item name="enable" valuePropName="checked" noStyle>
<Switch
checkedChildren={$t('启用')}
unCheckedChildren={$t('停用')}
onChange={(checked) => {
form.setFieldsValue({ enable: checked })
setCurrentStatus(checked)
}}
/>
</Form.Item>
</div>
</Form.Item>
</Form>
</WithPermission>
)
}
)
const LocalModelList: React.FC = () => {
const pageListRef = useRef<ActionType>(null)
@@ -109,6 +114,7 @@ const LocalModelList: React.FC = () => {
const { fetchData } = useFetch()
const [searchWord, setSearchWord] = useState<string>('')
const localAiDeployRef = useRef<LocalAiDeployHandle>()
const ConfigureOllamaServiceRef = useRef<ConfigureOllamaServiceHandle>()
const EditLocalModelModalRef = useRef<EditLocalModelModalHandle>()
const [stateColumnMap] = useState<{ [k: string]: { text: string; className?: string } }>({
normal: { text: '正常' },
@@ -118,10 +124,84 @@ const LocalModelList: React.FC = () => {
deploying_error: { text: '部署失败', className: 'text-[#ff4d4f] cursor-pointer' }
})
const [ollamaAddress, setOllamaAddress] = useState<string>('')
useEffect(() => {
getOllamaData()
}, [])
const configureService = (address?: string) => {
modal.confirm({
title: $t('配置 Ollama 服务'),
content: (
<ConfigureOllamaService ref={ConfigureOllamaServiceRef} address={address}></ConfigureOllamaService>
),
onOk: () => {
return ConfigureOllamaServiceRef.current?.save().then((res) => {
if (res === true) {
getOllamaData()
pageListRef.current?.reload()
}
})
},
footer: (_, { OkBtn, CancelBtn }) => {
return (
<div className="flex justify-between items-center">
<a
target="_blank"
rel="noopener noreferrer"
href="https://ollama.com/download/linux"
className="flex items-center gap-[8px]"
>
<span>{$t('如何部署 Ollama')}</span>
</a>
<div>
<CancelBtn />
<OkBtn />
</div>
</div>
)
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const customEmptyRender = () => {
return (
<>
<div>
<Icon className="align-sub mr-[5px]" icon="ph:hard-drives-light" width="50" height="50" />
<div>{$t('模型部署服务未配置')}</div>
<Button type="primary" className="mt-[10px]" onClick={() => configureService()}>
{$t('配置服务')}
</Button>
</div>
</>
)
}
const getOllamaData = async () => {
const response = await fetchData<BasicResponse<{ data: any[] }>>('model/local/source/ollama', {
method: 'GET'
})
if (response.code === STATUS_CODE.SUCCESS) {
setOllamaAddress(response.data?.config?.address || '')
} else {
message.error(response.msg || $t(RESPONSE_TIPS.error))
}
}
const handleEdit = (record: ModelListData) => {
modal.confirm({
title: $t('模型设置'),
content: <EditLocalModelModal ref={EditLocalModelModalRef} modelID={record.id} enable={record.state !== 'disabled'}/>,
content: (
<EditLocalModelModal ref={EditLocalModelModalRef} modelID={record.id} enable={record.state !== 'disabled'} />
),
onOk: () => {
return EditLocalModelModalRef.current?.save().then((res) => {
if (res === true) {
@@ -243,7 +323,7 @@ const LocalModelList: React.FC = () => {
{
title: '',
key: 'option',
btnNums: 4,
btnNums: 2,
fixed: 'right',
valueType: 'option',
render: (_: React.ReactNode, entity: ModelListData) => [
@@ -283,7 +363,9 @@ const LocalModelList: React.FC = () => {
}
const modalInstance = modal.confirm({
title: $t('部署过程'),
content: <ServiceDeployment record={record} closeModal={closeModal} updateFooter={updateFooter} cancelCb={cancel} />,
content: (
<ServiceDeployment record={record} closeModal={closeModal} updateFooter={updateFooter} cancelCb={cancel} />
),
footer: () => {
return <LogsFooter record={record} closeModal={closeModal} />
},
@@ -319,13 +401,14 @@ const LocalModelList: React.FC = () => {
}
}}
>
{stateColumnMap[entity?.state as string]?.text || '-'}
{$t(stateColumnMap[entity?.state as string]?.text || '-')}
</span>
)
},
{
title: $t('Apis'),
dataIndex: 'apiCount',
width: 100,
render: (dom: React.ReactNode, record: ModelListData) => (
<span className="[&>.key-link]:text-[#2196f3] cursor-pointer">
<a
@@ -351,11 +434,16 @@ const LocalModelList: React.FC = () => {
<PageList
ref={pageListRef}
rowKey="id"
tableClass="local-model-list"
customEmptyRender={customEmptyRender}
request={requestList}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
pageListRef.current?.reload()
}}
beforeNewBtn={
[<Button className="mr-btnbase" key="removeFromDep" onClick={() => configureService(ollamaAddress)}>{$t('配置服务')}</Button>]
}
showPagination={true}
searchPlaceholder={$t('请输入名称搜索')}
columns={columns}
@@ -6,16 +6,18 @@ import { $t } from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js'
import { App } from 'antd'
import { Card } from 'antd'
import { useRef } from 'react'
import { useEffect, useRef, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import AiSettingModalContent, { AiSettingModalContentHandle } from '../aiSetting/AiSettingModal'
import { checkAccess } from '@common/utils/permission'
import LocalAiDeploy, { LocalAiDeployHandle } from './LocalAiDeploy'
import useDeployLocalModel from './deployModelUtil'
import RestAIDeploy, { RestAIDeployHandle } from './RestAIDeploy'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
export const AIModelGuide = () => {
const { modal } = App.useApp()
const { message, modal } = App.useApp()
const entityData = useRef<any>(null)
const navigateTo = useNavigate()
const { accessData } = useGlobalContext()
@@ -23,6 +25,8 @@ export const AIModelGuide = () => {
const localAiDeployRef = useRef<LocalAiDeployHandle>()
const restAiDeployRef = useRef<RestAIDeployHandle>()
const { deployLocalModel } = useDeployLocalModel()
const { fetchData } = useFetch()
const [ollamaAddress, setOllamaAddress] = useState<string>('')
const dumpServerPage = () => {
navigateTo('/service/list')
@@ -105,10 +109,31 @@ export const AIModelGuide = () => {
})
}
const getOllamaData = async () => {
const response = await fetchData<BasicResponse<{ data: any[] }>>('model/local/source/ollama', {
method: 'GET'
})
if (response.code === STATUS_CODE.SUCCESS) {
setOllamaAddress(response.data?.config?.address || '')
} else {
message.error(response.msg || $t(RESPONSE_TIPS.error))
}
}
useEffect(() => {
getOllamaData()
}, [])
/**
* AI API
*/
const localModelCardClick = async () => {
if (!ollamaAddress) {
navigateTo('/aisetting?status=unconfigure')
return
}
const modalInstance = modal.confirm({
title: $t('部署本地模型'),
content: <LocalAiDeploy ref={localAiDeployRef} onClose={() => {
@@ -131,6 +156,10 @@ export const AIModelGuide = () => {
}
const deployDeepSeek = async (e: any) => {
e.stopPropagation()
if (!ollamaAddress) {
navigateTo('/aisetting?status=unconfigure')
return
}
await deployLocalModel({
modelID: 'deepseek-r1'
})
@@ -141,13 +170,13 @@ export const AIModelGuide = () => {
{
imgSrc: restAPIPic,
title: $t('添加 Rest 服务'),
description: $t('支持批量添加现有 API 文档以实现统一的外部访问。'),
description: $t('导入OpenAPI文档,将现有系统的API发布到APIPark。'),
click: restCardClick
},
{
imgSrc: onlineAIPic,
title: $t('添加在线 AI API'),
description: $t('快速调用 AI 模型的云服务 API,方便管理提示词和统一计费。'),
description: $t('添加公有云AI模型的 API Key,通过APIPark 统一调用公有云的AI模型。'),
click: aiCardClick
},
{
@@ -164,7 +193,9 @@ export const AIModelGuide = () => {
}
]
return (
<div className="mb-[30px] pt-[15px] flex justify-between space-x-4">
<>
<p>{$t('⚡您可快速通过以下方式开放API供大家使用:')}</p>
<div className="mb-[30px] pt-[25px] flex justify-between space-x-4">
{cardList.map((item, itemIndex) => (
<Card
key={itemIndex}
@@ -182,5 +213,6 @@ export const AIModelGuide = () => {
</Card>
))}
</div>
</>
)
}
@@ -151,12 +151,13 @@ export default function Guide() {
description={
<div className="flex flex-col gap-[8px]">
<p>
<span className="font-bold">🦄 APIPark </span>
{$t(
'你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。'
'是开源的一站式 AI 网关与 API 门户,可快速接入 OpenAI/DeepSeek 等各类 AI 模型,通过统一请求格式避免模型切换对业务造成影响,提供企业级 API 安全防护(鉴权/限流/敏感词过滤)与实时用量监控,支持团队内 API 共享协作,管理接口订阅授权并保证您的API安全。'
)}
</p>
<p>
{$t('如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。')}
{$t('✨ 欢迎在 Github 为我们 Star 或提供产品反馈意见。')}
<span className="font-bold">
{$t('点击这里')}
<span className="align-middle leading-[16px]">
@@ -184,7 +185,9 @@ export default function Guide() {
}
showBorder={false}
scrollPage={false}
contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B"
customPadding={true}
headerClassName="pt-[30px] pl-[40px]"
contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B pl-[40px]"
>
<AIModelGuide></AIModelGuide>
<div className="flex flex-col gap-[15px]">
@@ -114,7 +114,7 @@ const LocalAiDeploy = forwardRef<LocalAiDeployHandle, any>((props: any, ref: any
name="partitionInsideCert"
autoComplete="off"
>
<Form.Item label={$t('模型供应商')} name="provider" rules={[{ required: true }]}>
<Form.Item label={$t('模型')} name="provider" rules={[{ required: true }]}>
<Select
showSearch
className="w-INPUT_NORMAL"
@@ -158,8 +158,8 @@ const LocalAiDeploy = forwardRef<LocalAiDeployHandle, any>((props: any, ref: any
: null}
</div>
</Form.Item>
<Form.Item label={$t('默认模型')} name="model" className="mt-[16px]" rules={[{ required: true }]}>
<Select
<Form.Item label={$t('Tags')} name="model" className="mt-[16px]" rules={[{ required: true }]}>
<Select
showSearch
className="w-INPUT_NORMAL"
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
@@ -168,7 +168,7 @@ const LocalAiDeploy = forwardRef<LocalAiDeployHandle, any>((props: any, ref: any
label: (
<div className="relative">
<span>{provider.name}</span>
{ provider.size && <span className="absolute right-[10px] text-[#999]">{provider.size}</span> }
{provider.size && <span className="absolute right-[10px] text-[#999]">{provider.size}</span>}
</div>
),
value: provider.id,
@@ -338,10 +338,10 @@ const KeySettings: React.FC = () => {
return (
<InsidePage
className="overflow-y-auto gap-4 pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X"
pageTitle={$t('APIKey 资源池')}
pageTitle={$t('API Key 负载')}
description={
<>
{$t('支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡')}
{$t('支持单个 API 模型供应商下创建多个 API Key 进行智能负载均衡')}
<div className="mt-4">
<AIProviderSelect
value={selectedProvider}
@@ -28,7 +28,7 @@ const LoadBalancingPage = () => {
const { fetchData } = useFetch()
const addModel = () => {
modal.confirm({
title: $t('添加负载均衡'),
title: $t('添加模型'),
content: <AddLoadBalancingModel ref={addModelRef} />,
width: 600,
closable: true,
@@ -267,7 +267,7 @@ const LoadBalancingPage = () => {
return (
<>
<InsidePage
pageTitle={$t('负载均衡')}
pageTitle={$t('模型灾备')}
description={$t(
'系统自动识别异常AI模型后,自动替换成以下优先级最高的可用模型。这将确保您的AI应用保持高可用性和最佳性能,从而防止任何单个LLM异常成为您的性能瓶颈。'
)}
@@ -1,12 +1,6 @@
import { LoadingOutlined } from '@ant-design/icons'
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
import {
BasicResponse,
DELETE_TIPS,
PLACEHOLDER,
RESPONSE_TIPS,
STATUS_CODE
} from '@common/const/const.tsx'
import { BasicResponse, DELETE_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
import { EntityItem, MemberItem, SimpleTeamItem } from '@common/const/type.ts'
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
@@ -50,15 +44,10 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
const [tagOptionList, setTagOptionList] = useState<DefaultOptionType[]>([])
const [serviceClassifyOptionList, setServiceClassifyOptionList] = useState<DefaultOptionType[]>()
const [uploadLoading, setUploadLoading] = useState<boolean>(false)
const {
checkPermission,
accessInit,
getGlobalAccessData,
state,
aiConfigFlushed,
setAiConfigFlushed
} = useGlobalContext()
const { checkPermission, accessInit, getGlobalAccessData, state, aiConfigFlushed, setAiConfigFlushed } =
useGlobalContext()
const [providerOptionList, setProviderOptionList] = useState<DefaultOptionType[]>()
const [modelList, setModelList] = useState<DefaultOptionType[]>()
const location = useLocation()
const currentUrl = location.pathname
@@ -77,18 +66,80 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers/configured', {
method: 'GET',
eoTransformKeys: [],
eoParams: { all: true}
}).then(response => {
eoParams: { all: true }
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const configuredProvider = data.providers
?.filter(x => x.configured)
?.filter((x) => x.configured)
?.map((x: SimpleAiProviderItem) => {
return { ...x, label: x.name, value: x.id }
})
setProviderOptionList(configuredProvider)
if (!serviceId && configuredProvider.length > 0) {
form.setFieldsValue({ provider: configuredProvider[0]?.id })
if (configuredProvider[0]?.type === 'local') {
getLocalModelList()
} else {
getOnlineModelList(configuredProvider[0]?.id)
}
}
if (serviceId && configuredProvider.length > 0) {
const providerID = form.getFieldValue('provider')
const provider = configuredProvider?.find((item: any) => item.id === providerID)
if (provider?.type === 'local') {
getLocalModelList(false)
} else {
getOnlineModelList(provider?.id, false)
}
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const modelProviderChange = (id: string) => {
const provider = providerOptionList?.find((item) => item.id === id)
if (provider?.type === 'local') {
getLocalModelList()
} else {
getOnlineModelList(provider?.id)
}
}
const getLocalModelList = (setDefaultLlm = true) => {
fetchData<BasicResponse<{ providers: any[] }>>('simple/ai/models/local/configured', {
method: 'GET'
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const localModelList = data.models?.map((x: any) => {
return { ...x, label: x.name, value: x.id }
})
setModelList(localModelList)
if (setDefaultLlm && localModelList.length > 0) {
form.setFieldsValue({ model: localModelList[0]?.id })
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const getOnlineModelList = (id: string, setDefaultLlm = true) => {
fetchData<BasicResponse<{ providers: any[] }>>('ai/provider/llms', {
method: 'GET',
eoParams: { provider: id },
eoTransformKeys: ['default_llm']
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const localModelList = data.llms?.map((x: any) => {
return { ...x, label: x.id, value: x.id }
})
setModelList(localModelList)
if (setDefaultLlm && localModelList.length > 0) {
form.setFieldsValue({ model: localModelList[0]?.id })
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
@@ -137,9 +188,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
}
const uploadButton = (
<div>
{uploadLoading ? <LoadingOutlined /> : <Icon icon="ic:baseline-add" width="24" height="24" />}
</div>
<div>{uploadLoading ? <LoadingOutlined /> : <Icon icon="ic:baseline-add" width="24" height="24" />}</div>
)
const getTagAndServiceClassifyList = () => {
@@ -147,7 +196,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
setServiceClassifyOptionList([])
fetchData<BasicResponse<{ catalogues: CategorizesType[]; tags: EntityItem[] }>>('catalogues', {
method: 'GET'
}).then(response => {
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setTagOptionList(
@@ -175,7 +224,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
method: 'GET',
eoParams: { team: teamId, service: serviceId },
eoTransformKeys: ['team_id', 'service_type', 'approval_type', 'service_kind']
}).then(response => {
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setTimeout(() => {
@@ -196,6 +245,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
})
setImageBase64(data.service.logo)
setShowClassify(data.service.serviceType === 'public')
getProviderOptionList()
}, 0)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
@@ -204,21 +254,19 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
}
const onFinish: () => Promise<boolean | string> = () => {
return form.validateFields().then(value => {
return form.validateFields().then((value) => {
return fetchData<BasicResponse<{ service: { id: string } }>>(
serviceId === undefined ? 'team/service' : 'service/info',
{
method: serviceId === undefined ? 'POST' : 'PUT',
eoParams: {
...(serviceId === undefined
? { team: value.team }
: { service: serviceId, team: teamId })
...(serviceId === undefined ? { team: value.team } : { service: serviceId, team: teamId })
},
eoBody: { ...value, prefix: value.prefix?.trim() },
eoTransformKeys: ['serviceType', 'approvalType', 'serviceKind']
}
)
.then(response => {
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
@@ -229,7 +277,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
}
})
.catch(errorInfo => {
.catch((errorInfo) => {
return Promise.reject(errorInfo)
})
})
@@ -241,7 +289,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(
!checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams',
{ method: 'GET', eoTransformKeys: [] }
).then(response => {
).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setTeamOptionList(
@@ -262,7 +310,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
fetchData<BasicResponse<null>>('team/service', {
method: 'DELETE',
eoParams: { team: teamId, service: serviceId }
}).then(response => {
}).then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
@@ -285,7 +333,6 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
getTeamOptionList()
})
}
getProviderOptionList()
getTagAndServiceClassifyList()
if (serviceId !== undefined) {
setOnEdit(true)
@@ -299,6 +346,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
}
])
} else {
getProviderOptionList()
setOnEdit(false)
const id = uuidv4()
form.setFieldValue('id', id)
@@ -333,7 +381,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
// const serviceTypeOptions = useMemo(()=>SERVICE_KIND_OPTIONS.map((x)=>({...x, label:$t(x.label)})),[state.language]);
// const visualizationOptions = useMemo(()=>SERVICE_VISUALIZATION_OPTIONS.map((x)=>({...x, label:$t(x.label)})),[state.language])
const approvalOptions = useMemo(
() => SERVICE_APPROVAL_OPTIONS.map(x => ({ ...x, label: $t(x.label) })),
() => SERVICE_APPROVAL_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })),
[state.language]
)
@@ -364,21 +412,13 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
name="id"
rules={[{ required: true, whitespace: true }]}
>
<Input
className="w-INPUT_NORMAL"
disabled={onEdit}
placeholder={$t(PLACEHOLDER.input)}
/>
<Input className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
{!onEdit && (
<Form.Item<SystemConfigFieldType>
label={$t('服务类型')}
name="serviceKind"
rules={[{ required: true }]}
>
<Form.Item<SystemConfigFieldType> label={$t('服务类型')} name="serviceKind" rules={[{ required: true }]}>
<Radio.Group
disabled={onEdit}
onChange={e => {
onChange={(e) => {
setShowAI(e.target.value === 'ai')
}}
>
@@ -398,39 +438,40 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
</Form.Item>
)}
{showAI && (
<Form.Item<AiServiceConfigFieldType>
label={$t('默认 AI 供应商')}
name="provider"
rules={[{ required: true }]}
extra={
serviceId
? $t('创建 API 时会默认选择该供应商,修改默认供应商不会影响现有 API')
: ''
}
>
{providerOptionList && providerOptionList.length > 0 ? (
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
options={providerOptionList}
></Select>
) : (
<p>
{$t('未配置任何 AI 模型供应商,')}
<a href="/aisetting" target="_blank" onClick={() => setAiConfigFlushed(false)}>
{$t('立即配置')}
</a>
</p>
)}
</Form.Item>
<>
<Form.Item<AiServiceConfigFieldType>
label={$t('默认 AI 供应商')}
name="provider"
rules={[{ required: true }]}
extra={serviceId ? $t('创建 API 时会默认选择该供应商,修改默认供应商不会影响现有 API') : ''}
>
{providerOptionList && providerOptionList.length > 0 ? (
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
options={providerOptionList}
onChange={(e) => {
modelProviderChange(e)
}}
></Select>
) : (
<p>
{$t('未配置任何 AI 模型供应商,')}
<a href="/aisetting" target="_blank" onClick={() => setAiConfigFlushed(false)}>
{$t('立即配置')}
</a>
</p>
)}
</Form.Item>
<Form.Item<AiServiceConfigFieldType> label={$t('默认模型')} name="model" rules={[{ required: true }]}>
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={modelList}></Select>
</Form.Item>
</>
)}
<Form.Item<SystemConfigFieldType>
label={$t('API 调用前缀')}
name="prefix"
extra={$t(
'作为服务内所有API的前缀,比如host/{service_name}/{api_path},影响较大,谨慎修改'
)}
extra={$t('作为服务内所有API的前缀,比如host/{service_name}/{api_path},影响较大,谨慎修改')}
rules={[
{ required: true, whitespace: true },
{
@@ -438,18 +479,10 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
}
]}
>
<Input
prefix={onEdit ? '' : '/'}
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
/>
<Input prefix={onEdit ? '' : '/'} className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
</Form.Item>
{!onEdit && (
<Form.Item<SystemConfigFieldType>
label={$t('所属团队')}
name="team"
rules={[{ required: true }]}
>
<Form.Item<SystemConfigFieldType> label={$t('所属团队')} name="team" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
disabled={onEdit}
@@ -459,11 +492,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
</Form.Item>
)}
<Form.Item<SystemConfigFieldType>
label={$t('订阅审核')}
name="approvalType"
rules={[{ required: true }]}
>
<Form.Item<SystemConfigFieldType> label={$t('订阅审核')} name="approvalType" rules={[{ required: true }]}>
<Radio.Group className="flex flex-col" options={approvalOptions} />
</Form.Item>
@@ -498,7 +527,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
<Form.Item<SystemConfigFieldType>
label={$t('图标')}
name="logoFile"
extra={$t('仅支持 .png .jpg .jpeg .svg 格式的图片文件, 大于 1KB 的文件将被压缩')}
extra={$t('仅支持 .png .jpg .jpeg .svg 格式的图片文件')}
valuePropName="fileList"
getValueFromEvent={normFile}
>
@@ -515,11 +544,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
style={{ marginTop: 8 }}
>
{imageBase64 ? (
<img
src={imageBase64}
alt="Logo"
style={{ maxWidth: '200px', width: '68px', height: '68px' }}
/>
<img src={imageBase64} alt="Logo" style={{ maxWidth: '200px', width: '68px', height: '68px' }} />
) : (
uploadButton
)}
@@ -68,12 +68,12 @@ const Integrate = ({ service }: { service: ServiceDetailType }) => {
<div className="my-[10px]">{$t('可通过以下 URL 或 下载 Json 文件,导入 API 文档数据到 Agent 平台中。')}</div>
<div className="flex w-full items-center gap-[30px]">
<Space.Compact className="w-[700px]">
<Input className="truncate" disabled title={url} value={url} />
<Input className="truncate" readOnly title={url} value={url} />
<Button type="primary" onClick={copyURL}>
{$t('复制 URL')}
</Button>
</Space.Compact>
<span className="text-[14px] font-bold">OR</span>
<span className="text-[14px] font-bold">{$t('或')}</span>
<Button href={`/api/v1/export/openapi/${serviceId}`} target="_blank">
{$t('下载 Json 文件')}
</Button>