feat: feature/1.5-Data Integration

This commit is contained in:
ningyv
2025-02-18 19:51:28 +08:00
parent 4390e9767c
commit 66d0653dc6
16 changed files with 411 additions and 114 deletions
@@ -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,24 @@
"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:"
}
@@ -872,5 +872,24 @@
"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をすぐに公開して皆さんに利用してもらえます:"
}
@@ -803,5 +803,24 @@
"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供大家使用:"
}
@@ -872,5 +872,24 @@
"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 供大家使用:"
}
+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;
@@ -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',
eoParams: { 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,19 +114,94 @@ 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: '正常' },
deploying: { text: '部署中', className: 'text-[#2196f3] cursor-pointer' },
error: { text: '模型异常', className: 'text-[#ff4d4f]' },
disabled: { text: '停用' },
deploying_error: { text: '部署失败', className: 'text-[#ff4d4f] cursor-pointer' }
normal: { text: $t('正常') },
deploying: { text: $t('部署中'), className: 'text-[#2196f3] cursor-pointer' },
error: { text: $t('模型异常'), className: 'text-[#ff4d4f]' },
disabled: { text: $t('停用') },
deploying_error: { text: $t('部署失败'), 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} />
},
@@ -326,6 +408,7 @@ const LocalModelList: React.FC = () => {
{
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
},
{
@@ -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]">
@@ -180,6 +181,7 @@ export default function Guide() {
Star
</span>
</p>
<p>{$t('⚡您可快速通过以下方式开放API供大家使用:')}</p>
</div>
}
showBorder={false}
@@ -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"
@@ -135,27 +135,6 @@ const LocalAiDeploy = forwardRef<LocalAiDeployHandle, any>((props: any, ref: any
getLocalModelList(value)
}}
></Select>
</Form.Item>
<Form.Item label={$t('模型')} name="model" className="mt-[16px]" rules={[{ required: true }]}>
<Select
showSearch
className="w-INPUT_NORMAL"
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
placeholder={$t(PLACEHOLDER.input)}
options={tagList.map((provider) => ({
label: (
<div className="relative">
<span>{provider.name}</span>
{provider.size && <span className="absolute right-[10px] text-[#999]">{provider.size}</span>}
</div>
),
value: provider.id,
searchText: provider.name.toLowerCase()
}))}
onChange={(value) => {
form.setFieldValue('model', value)
}}
></Select>
<div className="mt-[10px] mb-[5px]">
<Icon className="align-text-top" icon="noto-v1:fire" width="17" height="17" />
{$t('热点模型')}
@@ -179,6 +158,27 @@ const LocalAiDeploy = forwardRef<LocalAiDeployHandle, any>((props: any, ref: any
: null}
</div>
</Form.Item>
<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())}
placeholder={$t(PLACEHOLDER.input)}
options={tagList.map((provider) => ({
label: (
<div className="relative">
<span>{provider.name}</span>
{provider.size && <span className="absolute right-[10px] text-[#999]">{provider.size}</span>}
</div>
),
value: provider.id,
searchText: provider.name.toLowerCase()
}))}
onChange={(value) => {
form.setFieldValue('model', value)
}}
></Select>
</Form.Item>
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
@@ -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异常成为您的性能瓶颈。'
)}
@@ -498,7 +498,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}
>
@@ -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>