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

feat:Feature/1.5
This commit is contained in:
ningyv
2025-02-17 13:38:22 +08:00
committed by GitHub
58 changed files with 3275 additions and 1147 deletions
+3 -3
View File
@@ -25,7 +25,7 @@ jobs:
echo "Build frontend..."
cd ./frontend && pnpm run build
- name: upload frontend release
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: frontend-package
path: frontend/dist
@@ -41,7 +41,7 @@ jobs:
- name: Checkout #Checkout代码
uses: actions/checkout@v3
- name: download frontend release
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: frontend-package
path: frontend/dist
@@ -71,7 +71,7 @@ jobs:
- uses: actions/checkout@v3
- name: download frontend release
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: frontend-package
path: frontend/dist
+1 -1
View File
@@ -210,7 +210,7 @@ APIPark uses the Apache 2.0 License. For more details, please refer to the LICEN
For enterprise-level features and professional technical support, contact our pre-sales experts for personalized demos, customized solutions, and pricing.
- Website: https://apipark.com
- Email: dev@apipark.com
- Email: contact@apipark.com
<br>
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<image id="icons8-server-100" y="11" width="99" height="78" xlink:href="data:img/png;base64,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"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<image id="icons8-ai-cloud-100" y="16" width="100" height="68" xlink:href="data:img/png;base64,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"/>
</svg>

After

Width:  |  Height:  |  Size: 4.5 KiB

@@ -0,0 +1,14 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<defs>
<style>
.cls-1 {
font-size: 40px;
fill: #339af0;
font-family: Montserrat;
font-weight: 600;
}
</style>
</defs>
<image id="icons8-api-100" width="100" height="100" xlink:href="data:img/png;base64,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"/>
<text id="REST" class="cls-1" transform="translate(20.904 48.641) scale(0.553)">REST</text>
</svg>

After

Width:  |  Height:  |  Size: 4.1 KiB

@@ -116,6 +116,10 @@ function BasicLayout({ project = 'core' }: { project: string }) {
getGlobalAccessData()
}, [])
useEffect(() => {
setPathname(location.pathname)
}, [location.pathname])
const logOut = () => {
fetchData<BasicResponse<null>>('account/logout', { method: 'GET' }).then((response) => {
const { code, msg } = response
@@ -182,7 +186,7 @@ function BasicLayout({ project = 'core' }: { project: string }) {
</Button>,
...((pluginSlotHub.getSlot('basicLayoutAfterBtns') as unknown[]) || [])
]
}, [pluginSlotHub.getSlot('basicLayoutAfterBtns')])
}, [state.language, pluginSlotHub.getSlot('basicLayoutAfterBtns')])
return (
<div
@@ -26,7 +26,8 @@ interface CodeboxProps {
language?: codeBoxLanguagesType
extraContent?: React.ReactNode
sx?: Record<string, unknown>
editorTheme?: 'vs' | 'vs-dark' | 'hc-black'
editorTheme?: 'vs' | 'vs-dark' | 'hc-black',
autoScrollToEnd?: boolean
}
export const Codebox = memo((props: CodeboxProps) => {
@@ -41,7 +42,8 @@ export const Codebox = memo((props: CodeboxProps) => {
readOnly = false,
language = 'plaintext',
extraContent,
editorTheme = 'vs'
editorTheme = 'vs',
autoScrollToEnd = false
} = props
const [code, setCode] = useState<string>(``)
@@ -120,6 +122,11 @@ export const Codebox = memo((props: CodeboxProps) => {
const editorDidMount = (editor: MonacoEditor.IStandaloneCodeEditor): void => {
editorRef.current = editor
autoScrollToEnd && editor.onDidChangeModelContent(() => {
const model = editor.getModel()
const lineCount = model.getLineCount()
editor.revealLine(lineCount)
})
}
const formatCode = async (): Promise<void> => {
@@ -239,6 +239,21 @@ export const PERMISSION_DEFINITION = [
anyOf: [{ backend: ['system.settings.log_configuration.manager'] }]
}
},
'system.settings.ai_balance.view': {
granted: {
anyOf: [{ backend: ['system.settings.ai_balance.view'] }]
}
},
'system.settings.ai_balance.delete': {
granted: {
anyOf: [{ backend: ['system.settings.ai_balance.manager'] }]
}
},
'system.settings.ai_balance.add': {
granted: {
anyOf: [{ backend: ['system.settings.ai_balance.manager'] }]
}
},
'system.devops.policy.view': {
granted: {
anyOf: [{ backend: ['system.settings.strategy.view'] }]
@@ -63,6 +63,12 @@ export type SimpleTeamItem = {
description: string
appNum: number
}
export type LocalModelItem = {
id: string
isPopular: boolean
name: string
size: string
}
export type MatchItem = {
position: typeof MatchPositionEnum
@@ -160,6 +160,13 @@ const mockData = [
path: '/aiApis',
icon: 'ic:baseline-api',
access: 'system.settings.ai_api.view'
},
{
name: '负载均衡',
key: 'loadBalancing',
path: '/loadBalancing',
icon: 'ph:network-x',
access: 'system.settings.ai_balance.view'
}
]
},
+31 -7
View File
@@ -134,6 +134,8 @@ type EoRequest = RequestInit & {
eoTransformKeys?: string[]
eoApiPrefix?: string
eoBody?: { [k: string]: unknown } | Array<unknown> | string
isStream?: boolean
handleStream?: (line: any) => void
}
type EoHeaders = Headers | { [k: string]: string }
@@ -186,14 +188,36 @@ export function useFetch() {
throw new Error(`HTTP error! status: ${response.status}`)
}
// 如果响应体为JSON且指定了转换键,则转换响应数据
if (options?.eoApiPrefix||isJsonHttp(response.headers)) {
const data = await response.json()
const newData = (await pluginEventHub.emit('httpResponse', { data, continue: true })) as Response
return shouldTransformKeys ? (keysToCamel(newData, options.eoTransformKeys as string[]) as T) : data
}
if (options?.isStream) {
const reader = response.body?.getReader()
const decoder = new TextDecoder('utf-8')
let buffer = ''
if (reader) {
while (true) {
const { done, value } = await reader.read()
if (done) break
buffer += decoder.decode(value, { stream: true })
const lines = buffer.split('\n')
buffer = lines.pop() || ''
for (const line of lines) {
options?.handleStream?.(line)
}
}
return response
if (buffer) {
options?.handleStream?.(buffer)
}
}
} else {
// 如果响应体为JSON且指定了转换键,则转换响应数据
if (options?.eoApiPrefix || isJsonHttp(response.headers)) {
const data = await response.json()
const newData = (await pluginEventHub.emit('httpResponse', { data, continue: true })) as Response
return shouldTransformKeys ? (keysToCamel(newData, options.eoTransformKeys as string[]) as T) : data
}
return response
}
})
.catch((error) => {
// 全局错误处理
@@ -219,6 +219,16 @@ const mockData = {
type: 'normal'
}
]
},
{
driver: 'apipark.builtIn.component',
name: 'loadBalancing',
router: [
{
path: 'loadBalancing',
type: 'normal'
}
]
}
// {
// "driver": "apipark.remote.normal",
@@ -808,5 +808,47 @@
"Ke32702ac": "After saving, the supplier status will become [Disabled]. APIs using this supplier will temporarily use the normal supplier with the highest load priority.",
"Ka08c28d4": "After saving, the supplier status will become [Normal], restoring the AI capabilities of this supplier.",
"Kab8fe398": "Current Call Status:",
"K4880fd04": "Add (0) APIKey"
"K4880fd04": "Add (0) APIKey",
"Kf553a17e": "View",
"K84b2cf2d": "Online Model",
"Kdbf37ece": "Local Model",
"Kc7f7aa98": "Model Type",
"K42213ffa": "Online Model",
"K15e69f64": "Model Settings",
"K68f1c446": "Deploy AI Model",
"K953bbe54": "Delete Model",
"K1bbe8b92": "There are",
"Kca29bf8b": "APIs using the current model. After deleting the current model configuration, the related APIs will switch to the highest-priority available model in the load balancing system. All API keys and related data under the current model will be cleared. Are you sure you want to delete the current model?",
"Kf02ec68c": "The current model is the last one and cannot be deleted",
"Kf63cb5b4": "Deployment Process",
"K2b2e787c": "Apis",
"K11372aaf": "Deploy Model",
"K14bcebd2": "Keys",
"K663648ae": "Add Model",
"K2c93168c": "Add REST API",
"K31086771": "Supports batch addition of existing API documents for unified external access",
"K68932d54": "Add Online AI API",
"K659140c3": "Quickly call cloud service API of AI model, conveniently manage prompt and unified billing",
"K8341389c": "Deploy AI Locally & Generate API",
"Kf4e629f9": "Quickly deploy open-source models locally and automatically generate APIs",
"K26b9d431": "Deploy",
"K8facd134": "Click here",
"K96871eb8": "Click",
"K1fd51aaa": "Model Name",
"K40c527de": "Hot Model",
"Kcdb675ed": "Select OpenAPI File (.json / .yaml)",
"Kbb028f95": "Add Load Balancing",
"Kfac16394": "When an AI model anomaly is detected, the system will automatically replace it with the highest-priority available model below. This ensures your AI application maintains high availability and optimal performance, preventing any single LLM anomaly from becoming a performance bottleneck.",
"K769d59d": "Please enter...",
"K65b21404": "Download",
"K7cc5269": "Initializing",
"Kf9308d46": "Stop Deployment",
"K3de04ec6": "Are you sure you want to stop the deployment?",
"K881fef4c": "Are you sure you want to delete the service?",
"Ka791de39": "Deploying",
"Kf7056787": "Public Service",
"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"
}
@@ -830,5 +830,47 @@
"Ke32702ac": "保存後、サプライヤーのステータスは【無効】となり、このサプライヤーのAPIは一時的に負荷優先度が最も高い正常なサプライヤーを使用します。",
"Ka08c28d4": "保存後、サプライヤーのステータスは【正常】となり、このサプライヤーのAI機能が復元されます。",
"Kab8fe398": "現在の呼び出し状態:",
"K4880fd04": "APIKeyを追加 (0)"
"K4880fd04": "APIKeyを追加 (0)",
"Kf553a17e": "表示",
"K84b2cf2d": "オンラインモデル",
"Kdbf37ece": "ローカルモデル",
"Kc7f7aa98": "モデルタイプ",
"K42213ffa": "オンラインモデル",
"K15e69f64": "モデル設定",
"K68f1c446": "ローカルモデルをデプロイ",
"K953bbe54": "モデルを削除",
"K1bbe8b92": "現在",
"Kca29bf8b": "個の API がこのモデルを使用しています。このモデル設定を削除すると、関連する API はロードバランシング内で最優先の利用可能なモデルに切り替わります。また、このモデルに関連するすべての API キーとデータが削除されます。本当にこのモデルを削除しますか?",
"Kf02ec68c": "現在のモデルは最後のモデルであり、削除できません。",
"Kf63cb5b4": "デプロイプロセス",
"K2b2e787c": "Apis",
"K11372aaf": "モデルをデプロイ",
"K14bcebd2": "Keys",
"K663648ae": "モデルを追加",
"K2c93168c": "REST サービスを追加",
"K31086771": "既存の API ドキュメントを一括追加し、統一された外部アクセスを実現できます。",
"K68932d54": "オンライン AI API を追加",
"K659140c3": "AI モデルのクラウド API を素早く呼び出し、プロンプト管理や一元的な課金管理を簡単にします。",
"K8341389c": "ローカルに AI をデプロイし API を生成",
"Kf4e629f9": "オープンソースモデルをローカルに素早くデプロイし、自動的に API を生成します。",
"K26b9d431": "デプロイ",
"K8facd134": "ここをクリック",
"K96871eb8": "クリック",
"K1fd51aaa": "モデル名",
"K40c527de": "人気モデル",
"Kcdb675ed": "OpenAPI ファイル (.json / .yaml) を選択",
"Kbb028f95": "ロードバランシングを追加",
"Kfac16394": "システムが AI モデルの異常を検知した場合、自動的に以下の最優先の利用可能なモデルに置き換えます。これにより、AI アプリの高可用性と最適なパフォーマンスを維持し、単一の LLM の異常がボトルネックになるのを防ぎます。",
"K769d59d": "入力してください...",
"K65b21404": "ダウンロード",
"K7cc5269": "初期化",
"Kf9308d46": "デプロイを停止",
"K3de04ec6": "本当にデプロイを停止しますか?",
"K881fef4c": "本当にサービスを削除しますか?",
"Ka791de39": "デプロイ中",
"Kf7056787": "パブリックサービス",
"Kbe98ba9e": "プライベートサービス",
"K24540de": "停止",
"Kd85b3f64": "引き続き待機",
"K1400a1fc": "サービス内のすべてのAPIのプレフィックスとして使用されます。例えば host/{service_name}/{api_path} のように、大きな影響を与えるため、慎重に変更してください。"
}
@@ -761,5 +761,47 @@
"Ke32702ac": "保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。",
"Ka08c28d4": "保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。",
"Kab8fe398": "当前调用状态:",
"K4880fd04": "添加 (0) APIKey"
"K4880fd04": "添加 (0) APIKey",
"Kf553a17e": "查看 ",
"K84b2cf2d": "线上模型",
"Kdbf37ece": "本地模型",
"Kc7f7aa98": "模型类型",
"K42213ffa": "在线模型",
"K15e69f64": "模型设置",
"K68f1c446": "部署本地模型",
"K953bbe54": "删除模型",
"K1bbe8b92": "有",
"Kca29bf8b": "个API使用当前模型,删除当前的模型配置后,该模型相关的API将会切换为使用负载均衡中优先级最高的可用模型。并且当前模型下的所有API KEY和相关数据将会被清空,是否确认删除当前模型?",
"Kf02ec68c": "当前模型为最后一个模型,不支持删除",
"Kf63cb5b4": "部署过程",
"K2b2e787c": "Apis",
"K11372aaf": "部署模型",
"K14bcebd2": "Keys",
"K663648ae": "添加模型",
"K2c93168c": "添加 Rest 服务",
"K31086771": "支持批量添加现有 API 文档以实现统一的外部访问。",
"K68932d54": "添加在线 AI API",
"K659140c3": "快速调用 AI 模型的云服务 API,方便管理提示词和统一计费。",
"K8341389c": "本地部署 AI 并生成 API",
"Kf4e629f9": "快速在本地部署开源模型并自动生成 API。",
"K26b9d431": "部署",
"K8facd134": "点击这里",
"K96871eb8": "点击",
"K1fd51aaa": "模型名称",
"K40c527de": "热点模型",
"Kcdb675ed": "选择 OpenAPI 文件 (.json / .yaml)",
"Kbb028f95": "添加负载均衡",
"Kfac16394": "系统自动识别异常AI模型后,自动替换成以下优先级最高的可用模型。这将确保您的AI应用保持高可用性和最佳性能,从而防止任何单个LLM异常成为您的性能瓶颈。",
"K769d59d": "请输入...",
"K65b21404": "下载",
"K7cc5269": "初始化",
"Kf9308d46": "停止部署",
"K3de04ec6": "确定停止部署吗?",
"K881fef4c": "确定删除服务吗?",
"Ka791de39": "部署中",
"Kf7056787": "公共服务",
"Kbe98ba9e": "私有服务",
"K24540de": "停止",
"Kd85b3f64": "继续等待",
"K1400a1fc": "作为服务内所有API的前缀,比如host/{service_name}/{api_path},影响较大,谨慎修改"
}
@@ -830,5 +830,47 @@
"Ke32702ac": "儲存後供應商狀態變為【停用】,使用本供應商的 API 將暫時使用負載優先級最高的正常供應商。",
"Ka08c28d4": "儲存後供應商狀態變為【正常】,恢復調用本供應商的 AI 能力。",
"Kab8fe398": "目前調用狀態:",
"K4880fd04": "新增 (0) APIKey"
"K4880fd04": "新增 (0) APIKey",
"Kf553a17e": "查看",
"K84b2cf2d": "線上模型",
"Kdbf37ece": "本地模型",
"Kc7f7aa98": "模型類型",
"K42213ffa": "線上模型",
"K15e69f64": "模型設置",
"K68f1c446": "部署本地模型",
"K953bbe54": "刪除模型",
"K1bbe8b92": "有",
"Kca29bf8b": "個 API 使用當前模型,刪除當前的模型配置後,該模型相關的 API 將會切換為使用負載均衡中優先級最高的可用模型。此外,當前模型下的所有 API KEY 和相關數據將會被清空,是否確認刪除當前模型?",
"Kf02ec68c": "當前模型為最後一個模型,不支持刪除",
"Kf63cb5b4": "部署過程",
"K2b2e787c": "Apis",
"K11372aaf": "部署模型",
"K14bcebd2": "Keys",
"K663648ae": "添加模型",
"K2c93168c": "添加 REST 服務",
"K31086771": "支持批量添加現有 API 文檔,以實現統一的外部訪問。",
"K68932d54": "添加線上 AI API",
"K659140c3": "快速調用 AI 模型的雲端 API,方便管理提示詞和統一計費。",
"K8341389c": "本地部署 AI 並生成 API",
"Kf4e629f9": "快速在本地部署開源模型並自動生成 API。",
"K26b9d431": "部署",
"K8facd134": "點擊這裡",
"K96871eb8": "點擊",
"K1fd51aaa": "模型名稱",
"K40c527de": "熱門模型",
"Kcdb675ed": "選擇 OpenAPI 文件 (.json / .yaml)",
"Kbb028f95": "添加負載均衡",
"Kfac16394": "當系統自動檢測到 AI 模型異常時,會自動替換為以下優先級最高的可用模型。這將確保您的 AI 應用保持高可用性和最佳性能,防止任何單個 LLM 異常成為性能瓶頸。",
"K769d59d": "請輸入...",
"K65b21404": "下載",
"K7cc5269": "初始化",
"Kf9308d46": "停止部署",
"K3de04ec6": "確定停止部署嗎?",
"K881fef4c": "確定刪除服務嗎?",
"Ka791de39": "部署中",
"Kf7056787": "公共服務",
"Kbe98ba9e": "私有服務",
"K24540de": "停止",
"Kd85b3f64": "繼續等待",
"K1400a1fc": "作為服務內所有 API 的前綴,例如 host/{service_name}/{api_path},這會產生較大的影響,請謹慎修改"
}
@@ -25,10 +25,11 @@ interface AIProviderResponse {
interface AIProviderSelectProps {
value?: string
onChange?: (value: string, provider: AIProvider) => void
style?: React.CSSProperties
style?: React.CSSProperties,
source?: 'ai_api' | 'ai_keys'
}
const AIProviderSelect: React.FC<AIProviderSelectProps> = ({ value, onChange, style = { width: 200 } }) => {
const AIProviderSelect: React.FC<AIProviderSelectProps> = ({ value, onChange, source = 'ai', style = { width: 200 } }) => {
const { t } = useTranslation()
const [providers, setProviders] = useState<AIProvider[]>([])
const [loading, setLoading] = useState(false)
@@ -40,7 +41,7 @@ const AIProviderSelect: React.FC<AIProviderSelectProps> = ({ value, onChange, st
if (isMounted) setLoading(true)
try {
const endpoint = 'simple/ai/providers/configured'
const response = await fetchData<AIProviderResponse>(endpoint, { method: 'GET' })
const response = await fetchData<AIProviderResponse>(endpoint, { method: 'GET', ...(source === 'ai_api' ? { eoParams: { all: true } } : {}) })
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const providers = data.providers.map((val) => ({
@@ -19,6 +19,7 @@ export type AiServiceConfigFieldType = {
serviceType?:'public'|'inner';
catalogue?:string | string[];
approvalType?:string;
providerType?:string
};
export type AiServiceSubServiceTableListItem = {
@@ -799,5 +799,20 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
}
]
}
],
[
'loadBalancing',
{
type: 'module',
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/loadBalancingLayout.tsx')),
key: 'loadBalancing',
children: [
{
path: 'list',
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/index.tsx')),
key: 'loadBalancingList'
}
]
}
]
])
@@ -108,6 +108,12 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns<SystemTableListItem>[] = [
dataIndex: ['team', 'name'],
ellipsis: true
},
{
title: '状态',
width: 140,
dataIndex: 'state',
ellipsis: true
},
{
title: 'API 数量',
dataIndex: 'apiNum',
@@ -11,6 +11,7 @@ export type SystemTableListItem = {
serviceNum: number,
description:string;
master:EntityItem;
state: string
service_kind:'ai'|'rest',
createTime:string;
};
+9
View File
@@ -746,6 +746,15 @@ p{
padding:16px 20px !important
}
}
.custom-steps .ant-steps-icon span {
width: auto !important;
}
.custom-steps .ant-steps-item-content {
margin-top: 0 !important;
}
.custom-steps .ant-steps-item-content .ant-steps-item-description {
width: 138px !important;
}
.ant-modal-body .pr-PAGE_INSIDE_X{
@@ -255,6 +255,7 @@ const ApiSettings: React.FC = () => {
<div className="flex gap-2 items-center">
<AIProviderSelect
value={selectedProvider}
source="ai_api"
onChange={(value, option) => {
setSelectedProvider(value)
setProvider(option)
@@ -31,7 +31,8 @@ const AiServiceInsidePage: FC = () => {
const getAiServiceInfo = () => {
fetchData<BasicResponse<{ service: AiServiceConfigFieldType }>>('service/info', {
method: 'GET',
eoParams: { team: teamId, service: serviceId }
eoParams: { team: teamId, service: serviceId },
eoTransformKeys: ['provider_type']
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
@@ -15,12 +15,12 @@ import { AI_SERVICE_VARIABLES_TABLE_COLUMNS } from '@core/const/ai-service/const
import { VariableItems } from '@core/const/ai-service/type.ts'
import { API_PATH_MATCH_RULES } from '@core/const/system/const'
import { useAiServiceContext } from '@core/contexts/AiServiceContext.tsx'
import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/AiSettingList'
import { Icon } from '@iconify/react/dist/iconify.js'
import { App, Button, Form, Input, InputNumber, Row, Space, Spin, Switch, Tag } from 'antd'
import { MutableRefObject, useEffect, useMemo, useRef, useState } from 'react'
import { useNavigate, useParams } from 'react-router-dom'
import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from './AiServiceInsideRouterModelConfig'
import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/types'
type AiServiceRouterField = {
name: string
@@ -79,7 +79,7 @@ const AiServiceInsideRouterCreate = () => {
timeout,
retry,
aiPrompt: { variables: variables, prompt: prompt },
aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config },
aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config, type: defaultLlm?.type },
disabled
}
return fetchData<BasicResponse<null>>('service/ai-router', {
@@ -147,10 +147,17 @@ const AiServiceInsideRouterCreate = () => {
...prev,
provider: aiModel?.provider,
id: aiModel?.id,
config: aiModel.config
config: aiModel.config,
type: aiModel?.type
}) as AiProviderDefaultConfig & { config: string }
)
getDefaultModelConfig(aiModel?.provider)
getDefaultModelConfig({
provider: aiModel?.provider,
id: aiModel?.id,
replaceDefaultLlm: false,
setIcon: true,
type: aiModel?.type
})
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
@@ -159,34 +166,109 @@ const AiServiceInsideRouterCreate = () => {
.finally(() => setLoading(false))
}
const getDefaultModelConfig = (provider?: string) => {
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[]; provider: AiProviderDefaultConfig }>>('ai/provider/llms', {
method: 'GET',
eoParams: { provider: provider ?? aiServiceInfo?.provider?.id },
eoTransformKeys: ['default_llm']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setLlmList(data.llms)
setDefaultLlm((prev) => {
const llmSetting = data.llms?.find(
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? data.provider.defaultLlm)
)
return {
...prev,
defaultLlm: data.provider.defaultLlm,
provider: data.provider.id,
name: data.provider.name,
config: llmSetting?.config || '',
...(llmSetting ?? {})
} as AiProviderDefaultConfig & { config: string }
})
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
const getDefaultModelConfig = ({
provider,
id,
replaceDefaultLlm = true,
setIcon = true,
type
}: {
provider?: string
id?: string
replaceDefaultLlm?: boolean
setIcon?: boolean
type?: string
} = {}) => {
// 如果编辑状态下 是本地 或者,新增状态下是本地
if (type === 'local' || (!type && aiServiceInfo?.providerType === 'local')) {
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[]; provider: AiProviderDefaultConfig }>>('simple/ai/models/local/configured', {
method: 'GET',
eoTransformKeys: ['default_config']
})
.catch((errorInfo) => console.error(errorInfo))
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setLlmList(data.models)
const localId = id || aiServiceInfo?.id
if (replaceDefaultLlm) {
setDefaultLlm((prev) => {
const llmSetting = data.models?.find(
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? localId)
)
return {
...prev,
defaultLlm: localId,
provider: localId,
name: aiServiceInfo?.name,
config: llmSetting?.defaultConfig || '',
type: 'local',
...(llmSetting ?? {})
} as AiProviderDefaultConfig & { config: string }
})
}
if (setIcon) {
setDefaultLlm((prev) => {
const llmSetting = data.models?.find(
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? localId)
)
return {
...prev,
logo: llmSetting?.logo,
scopes: llmSetting?.scopes
} as AiProviderDefaultConfig & { config: string }
})
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => console.error(errorInfo))
} else {
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[]; provider: AiProviderDefaultConfig }>>('ai/provider/llms', {
method: 'GET',
eoParams: { provider: provider ?? aiServiceInfo?.provider?.id },
eoTransformKeys: ['default_llm']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setLlmList(data.llms)
if (replaceDefaultLlm) {
setDefaultLlm((prev) => {
const llmSetting = data.llms?.find(
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? data.provider.defaultLlm)
)
return {
...prev,
defaultLlm: data.provider.defaultLlm,
provider: data.provider.id,
name: data.provider.name,
config: llmSetting?.config || '',
type: 'online',
...(llmSetting ?? {})
} as AiProviderDefaultConfig & { config: string }
})
}
if (setIcon) {
setDefaultLlm((prev) => {
const llmSetting = data.llms?.find(
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? data.provider.defaultLlm)
)
return {
...prev,
logo: llmSetting?.logo,
scopes: llmSetting?.scopes
} as AiProviderDefaultConfig & { config: string }
})
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => console.error(errorInfo))
}
}
useEffect(() => {
@@ -237,13 +319,21 @@ const AiServiceInsideRouterCreate = () => {
}
const handlerSubmit: () => Promise<boolean> | undefined = () => {
return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string }) => {
return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string, type: string, provider: string }) => {
getDefaultModelConfig({
provider: res.provider,
id: res.id,
type: res.type,
replaceDefaultLlm: false,
setIcon: true
})
setDefaultLlm(
(prev) =>
({
...prev,
provider: res.provider,
id: res.id,
type: res.type,
config: res.config,
logo: llmList?.find((x: AiProviderLlmsItems) => x.id === res.id)?.logo
}) as AiProviderDefaultConfig & { config: string }
@@ -1,132 +1,221 @@
import { Codebox } from "@common/components/postcat/api/Codebox"
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const"
import { useFetch } from "@common/hooks/http"
import { $t } from "@common/locales"
import { AiProviderDefaultConfig, AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList"
import { SimpleAiProviderItem } from "@core/pages/system/SystemConfig"
import { Form, message, Select, Tag } from "antd"
import { DefaultOptionType } from "antd/es/select"
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"
import { Codebox } from '@common/components/postcat/api/Codebox'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/AiSettingList'
import { LocalLlmType } from '@core/pages/loadBalancing/type'
import { SimpleAiProviderItem } from '@core/pages/system/SystemConfig'
import { Form, message, Select, Tag } from 'antd'
import { DefaultOptionType } from 'antd/es/select'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
export type AiServiceRouterModelConfigHandle = {
save:()=>Promise<{id:string, config:string}>
save: () => Promise<{ id: string; config: string, type: string, provider: string }>
}
export type AiServiceRouterModelConfigProps = {
entity:AiServiceRouterModelConfigField
llmList:AiProviderLlmsItems[]
entity: AiServiceRouterModelConfigField
llmList: AiProviderLlmsItems[]
}
type AiServiceRouterModelConfigField = {
provider:string
id:string
config:string
provider: string
id: string
config: string
type: string
}
const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle, AiServiceRouterModelConfigProps>((props, ref)=>{
const [form] = Form.useForm();
const {entity} = props
const [providerList, setProviderList]= useState<DefaultOptionType[]>([])
const [llmList, setLlmList]= useState<DefaultOptionType[]>([])
const {fetchData} = useFetch()
useImperativeHandle(ref, ()=>({
save:form.validateFields
})
)
const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle, AiServiceRouterModelConfigProps>(
(props, ref) => {
const [form] = Form.useForm()
const { entity } = props
const [providerList, setProviderList] = useState<DefaultOptionType[]>([])
const [llmList, setLlmList] = useState<DefaultOptionType[]>([])
const [modelType, setModelType] = useState<'online' | 'local'>('online')
const { fetchData } = useFetch()
useImperativeHandle(ref, () => ({
save: form.validateFields
}))
const [modelTypeList] = useState([
{
label: $t('线上模型'),
value: 'online'
},
{
label: $t('本地模型'),
value: 'local'
}
])
useEffect(()=>{
/**
*
* @param setDefaultValue
*/
const getLocalLlmList = (setDefaultValue?: boolean) => {
fetchData<LocalLlmType[]>('simple/ai/models/local/configured', {
method: 'GET',
eoTransformKeys: ['default_config']
}).then((response) => {
const models = response.data.models || []
setLlmList(
models.map((x: any) => ({
...x,
config: x.defaultConfig
}))
)
if (setDefaultValue && models.length) {
const id = models[0].id
form.setFieldsValue({
id,
config: models.find((x) => x.id === id)?.defaultConfig
})
}
})
}
/**
*
* @param e
*/
const modelTypeChange = (e: string) => {
setModelType(e as 'online' | 'local')
setLlmList([])
form.setFieldsValue({
provider: '',
id: '',
config: '',
type: e
})
if (e === 'online') {
getProviderList(true)
} else {
getLocalLlmList(true)
}
}
useEffect(() => {
setModelType(entity.type as 'online' | 'local')
if (entity.type === 'online') {
getProviderList()
form.setFieldsValue(entity)
},[])
getLlmList(entity.provider, false)
} else {
getLocalLlmList()
}
form.setFieldsValue(entity)
}, [])
const getProviderList = ()=>{
setProviderList([])
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers',{method:'GET',eoTransformKeys:[]}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setProviderList(data.providers?.filter(x=>x.configured)?.map((x:SimpleAiProviderItem)=>{return {...x,
label: x.name, value:x.id
}}))
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
const getProviderList = (setDefaultValue?: boolean) => {
setProviderList([])
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers/configured', {
method: 'GET',
eoTransformKeys: []
}).then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setProviderList(
data.providers
?.map((x: SimpleAiProviderItem) => {
return { ...x, label: x.name, value: x.id }
})
)
if (setDefaultValue && data.providers.length) {
const id = data.providers[0].id
form.setFieldValue('provider', id)
getLlmList(id)
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
}
const getLlmList = (provider: string, setDefaultValue = true) => {
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[]; provider: AiProviderDefaultConfig }>>('ai/provider/llms', {
method: 'GET',
eoParams: { provider },
eoTransformKeys: ['default_llm']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setLlmList(data.llms)
if (setDefaultValue && data.llms.length) {
form.setFieldsValue({
id: data.provider.defaultLlm,
config: data.llms.find((x) => x.id === data.provider.defaultLlm)?.config
})
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.catch((errorInfo) => console.error(errorInfo))
}
const getLlmList = (provider:string)=>{
fetchData<BasicResponse<{llms:AiProviderLlmsItems[],provider:AiProviderDefaultConfig}>>('ai/provider/llms',{method:'GET',eoParams:{provider}, eoTransformKeys:['default_llm']}).then(response=>{
const {code,data,msg} = response
if(code === STATUS_CODE.SUCCESS){
setLlmList(data.llms)
form.setFieldsValue({
id:data.provider.defaultLlm,
config:data.llms.find(x=>x.id===data.provider.defaultLlm)?.config})
}else{
message.error(msg || $t(RESPONSE_TIPS.error))
}
}).catch((errorInfo)=> console.error(errorInfo))
}
const handleChangeProvider = (provider:string)=>{
getLlmList(provider)
}
useEffect(()=>{
getLlmList(entity.provider)
},[])
return (
<Form
layout='vertical'
labelAlign='left'
scrollToFirstError
form={form}
className="mx-auto flex flex-col h-full"
name="aiServiceInsideRouterModalConfig"
autoComplete="off"
>
<Form.Item<AiServiceRouterModelConfigField>
label={$t("模型供应商")}
name="provider"
rules={[{ required: true }]}
>
<Select className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
options={providerList}
onChange={(e)=>{
handleChangeProvider(e)
}}>
</Select>
</Form.Item>
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="mx-auto flex flex-col h-full"
name="aiServiceInsideRouterModalConfig"
autoComplete="off"
>
<Form.Item<AiServiceRouterModelConfigField> label={$t('模型类型')} name="type" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
options={modelTypeList}
onChange={(e) => {
modelTypeChange(e)
}}
></Select>
</Form.Item>
{modelType === 'online' && (
<Form.Item<AiServiceRouterModelConfigField>
label={$t('模型供应商')}
name="provider"
rules={[{ required: true }]}
>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
options={providerList}
onChange={(e) => {
getLlmList(e)
}}
></Select>
</Form.Item>
)}
<Form.Item<AiServiceRouterModelConfigField>
label={$t("模型")}
name="id"
rules={[{ required: true }]}
>
<Select className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
options={llmList?.map(x=>({
value:x.id,
label:<div className="flex items-center gap-[10px]">
<span>{x.id}</span>
{x?.scopes?.map(s=><Tag >{s?.toLocaleUpperCase()}</Tag>)}
</div>}))}
onChange={(e)=>{
form.setFieldValue('config',llmList.find(x=>x.id===e)?.config)
}}>
</Select>
</Form.Item>
<Form.Item<AiServiceRouterModelConfigField> label={$t('模型')} name="id" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
options={
llmList?.map((x) => ({
value: x.id,
label: (
<div className="flex items-center gap-[10px]" key={x.id}>
<span>{x.id}</span>
{modelType === 'online' && x?.scopes?.map((s: any) => <Tag>{s?.toLocaleUpperCase()}</Tag>)}
</div>
)
}))
}
onChange={(e) => {
form.setFieldValue('config', llmList.find((x) => x.id === e)?.config)
}}
></Select>
</Form.Item>
<Form.Item<AiServiceRouterModelConfigField>
label={$t("参数")}
name="config"
>
<Codebox editorTheme="vs-dark"
width="100%" height="300px" language='json' enableToolbar={false} />
</Form.Item>
</Form>
<Form.Item<AiServiceRouterModelConfigField> label={$t('参数')} name="config">
<Codebox editorTheme="vs-dark" width="100%" height="300px" language="json" enableToolbar={false} />
</Form.Item>
</Form>
)
})
}
)
export default AiServiceRouterModelConfig
export default AiServiceRouterModelConfig
@@ -1,269 +0,0 @@
'use client'
import { BasicResponse } from '@common/const/const'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import {
CoordinateExtent,
Edge,
EdgeTypes,
Node,
NodeTypes,
PanOnScrollMode,
ReactFlow,
useEdgesState,
useNodesState
} from '@xyflow/react'
import '@xyflow/react/dist/style.css'
import { Button, Space, Spin } from 'antd'
import { useCallback, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import CustomEdge from './components/CustomEdge'
import { KeyStatusNode } from './components/KeyStatusNode'
import { ModelCardNode } from './components/ModelCardNode'
import { ServiceCardNode } from './components/NodeComponents'
import { LAYOUT } from './constants'
import './styles.css'
import { ModelListData } from './types'
export type ApiResponse = BasicResponse<{
backup: {
id: string
name: string
}
providers: ModelListData[]
}>
const calculateNodePositions = (models: ModelListData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => {
return models.reduce(
(acc, model, index) => {
const y = startY + index * gap
return {
...acc,
[model.id]: {
x: LAYOUT.MODEL_NODE_X,
y
},
[`${model.id}-keys`]: {
x: LAYOUT.KEY_NODE_X,
y: y + 16
}
}
},
{} as Record<string, { x: number; y: number }>
)
}
const nodeTypes: NodeTypes = {
modelCard: ModelCardNode,
keyCard: KeyStatusNode,
serviceCard: ServiceCardNode
} as const
const edgeTypes: EdgeTypes = {
custom: CustomEdge
}
const AIFlowChart = () => {
const [modelData, setModelData] = useState<ModelListData[]>([])
const [loading, setLoading] = useState(false)
const [nodes, setNodes, onNodesChange] = useNodesState<Node>([])
const [edges, setEdges, onEdgesChange] = useEdgesState<Edge>([])
const { fetchData } = useFetch()
const { aiConfigFlushed } = useGlobalContext()
const navigate = useNavigate()
useEffect(() => {
setLoading(true)
fetchData<ApiResponse>('ai/providers/configured', {
method: 'GET',
eoTransformKeys: ['default_llm']
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
})
.then((response) => {
const mockApiResponse: ApiResponse = response as ApiResponse
setModelData(mockApiResponse.data.providers)
})
.finally(() => {
setLoading(false)
})
}, [aiConfigFlushed])
useEffect(() => {
if (!modelData.length) return
const positions = calculateNodePositions(modelData)
const firstSuccessModel = modelData.find((model) => model.status === 'enabled')
console.log(firstSuccessModel)
// subtract 5 to make sure the service node is aligned with the top model node
const serviceY = positions[modelData[0].id].y - 5
const newNodes = [
{
id: 'apiService',
type: 'serviceCard',
position: { x: LAYOUT.SERVICE_NODE_X, y: serviceY },
draggable: false,
data: {
title: 'API Services',
count: modelData.length
}
},
...modelData.map((model) => ({
id: model.id,
type: 'modelCard',
position: positions[model.id],
data: {
name: model.name,
status: model.status,
defaultLlm: model.defaultLlm,
logo: model.logo,
id: model.id,
alternativeModel: firstSuccessModel
}
})),
...modelData.map((model) => ({
id: `${model.id}-keys`,
type: 'keyCard',
position: positions[`${model.id}-keys`],
data: {
title: '',
keys: (model.keys || []).map((key, index) => ({
id: key.id,
status: key.status,
priority: index + 1
}))
}
}))
]
const newEdges: any = [
...modelData.map((model) => ({
id: `service-${model.id}`,
source: 'apiService',
target: model.id,
label: `${model.api_count} apis`,
data: {
id: model.id,
status: model.status
},
animated: true,
style: { stroke: model.status === 'enabled' ? '#52c41a' : '#ff4d4f' }
})),
...modelData.map((model) => ({
id: `${model.id}-keys-edge`,
source: model.id,
target: `${model.id}-keys`,
label: `${model.key_count} keys`,
data: { id: model.id },
animated: true
}))
]
setNodes(newNodes)
setEdges(newEdges)
}, [modelData])
const calculateExtent = useCallback(() => {
const left = LAYOUT.SERVICE_NODE_X
const right = LAYOUT.KEY_NODE_X
const top = 0 // Allow slight negative scroll to reduce top padding
const bottom = LAYOUT.NODE_START_Y + modelData.length * LAYOUT.NODE_GAP
return [
[left, top],
[right, bottom < 100 ? 5000 : bottom]
] as CoordinateExtent
}, [modelData.length])
const updateProviderOrder = async (sortedProviderIds: string[]) => {
await fetchData('ai/provider/sort', {
method: 'PUT',
body: JSON.stringify({
providers: sortedProviderIds
})
})
}
const onNodeDragStop: any = useCallback((_: any, node: Node<any>) => {
if (node.type !== 'modelCard') return
setNodes((nds) => {
const modelNodes = nds.filter((n) => n.type === 'modelCard')
const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y)
const sortedProviderIds = sortedNodes.map((node) => node.id)
// Update provider order outside of setNodes callback
updateProviderOrder(sortedProviderIds)
// Update all node positions in a single pass
return nds.map((n) => {
if (n.type === 'modelCard') {
const index = sortedNodes.findIndex((sn) => sn.id === n.id)
return {
...n,
position: {
x: LAYOUT.MODEL_NODE_X,
y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP
}
}
}
if (n.type === 'keyCard') {
const modelId = n.id.replace('-keys', '')
const modelNode = sortedNodes.find((mn) => mn.id === modelId)
if (modelNode) {
const index = sortedNodes.findIndex((sn) => sn.id === modelId)
return {
...n,
position: {
x: LAYOUT.KEY_NODE_X,
y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + 16
}
}
}
}
return n
})
})
}, [])
return (
<div className="w-full h-full">
{loading ? (
<div className="flex justify-center items-center h-full">
<Spin size="large" />
</div>
) : modelData.length === 0 ? (
<Space className="flex flex-col justify-center items-center h-[200px]">
<div>{$t('未配置 AI 模型')}</div>
<Button type="primary" onClick={() => navigate('/aisetting?status=unconfigure')}>
{$t('前往设置')}
</Button>
</Space>
) : (
<ReactFlow
nodes={nodes}
edges={edges}
onNodesChange={onNodesChange}
onEdgesChange={onEdgesChange}
onNodeDragStop={onNodeDragStop}
proOptions={{ hideAttribution: true }}
draggable={false}
nodeTypes={nodeTypes}
elementsSelectable={false}
edgeTypes={edgeTypes}
zoomOnScroll={false}
panOnDrag={false}
zoomOnPinch={false}
zoomOnDoubleClick={false}
panOnScroll={true}
panOnScrollMode={PanOnScrollMode.Vertical}
defaultEdgeOptions={{
type: 'custom'
}}
translateExtent={calculateExtent()}
/>
)}
</div>
)
}
export default AIFlowChart
@@ -1,136 +0,0 @@
import Icon, { LoadingOutlined } from '@ant-design/icons'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Button, Card, Empty, Spin, Tag } from 'antd'
import { memo, useEffect, useState } from 'react'
import { useNavigate } from 'react-router-dom'
import { useAiSetting } from './contexts/AiSettingContext'
import { AiSettingListItem } from './types'
const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => {
const { openConfigModal } = useAiSetting()
const navigate = useNavigate()
const handleOpenModal = async (provider: AiSettingListItem) => {
await openConfigModal(provider)
navigate('/aisetting?status=configure')
}
return (
<Card
title={
<div className="flex w-full items-center justify-between gap-[4px]">
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
<span
className=" flex items-center h-[22px] ai-setting-svg-container"
dangerouslySetInnerHTML={{ __html: provider.logo }}
></span>
<span className="font-normal truncate">{provider.name}</span>
</div>
<Tag
bordered={false}
color={provider.configured ? 'green' : undefined}
className="h-[22px] px-[4px] text-center"
>
{provider.configured ? $t('已配置') : $t('未配置')}
</Tag>
</div>
}
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col "
classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }}
>
<div className="flex flex-col justify-between h-full gap-btnbase">
<div className="flex items-center w-full h-[32px] flex-1">
{provider.configured && (
<>
<label className="text-nowrap">{$t('默认')}</label>
<span className="overflow-hidden flex-1 truncate">{provider.defaultLlm}</span>
</>
)}
</div>
<WithPermission access="system.settings.ai_provider.view">
<Button
block
icon={<Icon icon="ic:outline-settings" width={18} height={18} />}
onClick={() => handleOpenModal(provider)}
classNames={{ icon: 'h-[18px]' }}
>
{$t('设置')}
</Button>
</WithPermission>
</div>
</Card>
)
})
const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => {
return (
<>
{modelList.length > 0 ? (
<div
className={className}
style={{
display: 'grid',
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 1fr))',
gap: '20px'
}}
>
{modelList.map((provider: AiSettingListItem) => (
<CardBox key={provider.id} provider={provider} />
))}
</div>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</>
)
}
const AIUnConfigure = () => {
const [modelData, setModelData] = useState<AiSettingListItem[]>([])
const { fetchData } = useFetch()
const [loading, setLoading] = useState<boolean>(false)
const { aiConfigFlushed } = useGlobalContext()
useEffect(() => {
setLoading(true)
fetchData<BasicResponse<{ providers: Omit<AiSettingListItem>[] }>>(`ai/providers/unconfigured`, {
method: 'GET',
eoTransformKeys: ['default_llm', 'default_llm_logo']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setModelData(data.providers)
} else {
const { message } = App.useApp()
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.finally(() => setLoading(false))
}, [aiConfigFlushed])
return (
<Spin
className="h-full"
wrapperClassName="h-full pr-PAGE_INSIDE_X"
indicator={<LoadingOutlined style={{ fontSize: 24 }} spin />}
spinning={loading}
>
{modelData && modelData.length > 0 ? (
<div>
{modelData.filter((item) => !item.configured).length > 0 && (
<>
<ModelCardArea modelList={modelData.filter((item) => !item.configured) || []} />
</>
)}
</div>
) : (
<Empty image={Empty.PRESENTED_IMAGE_SIMPLE} />
)}
</Spin>
)
}
export default AIUnConfigure
@@ -3,9 +3,9 @@ import { useI18n } from '@common/locales'
import { Tabs } from 'antd'
import { useEffect, useState } from 'react'
import { useSearchParams } from 'react-router-dom'
import AIFlowChart from './AIFlowChart'
import AIUnConfigure from './AIUnconfigure'
import { AiSettingProvider } from './contexts/AiSettingContext'
import OnlineModelList from './OnlineModelList'
import LocalModelList from './LocalModelList'
const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const
@@ -38,21 +38,19 @@ const AiSettingContent = () => {
items={[
{
key: 'flow',
label: $t('已设置'),
label: $t('在线模型'),
children: (
<div className="overflow-auto" style={CONTENT_STYLE}>
<AIFlowChart />
<OnlineModelList />
</div>
)
},
{
key: 'config',
label: $t('未设置'),
children: (
<div className="overflow-auto" style={CONTENT_STYLE}>
<AIUnConfigure />
</div>
)
label: $t('本地模型'),
children: <div className="overflow-auto" style={CONTENT_STYLE}>
<LocalModelList />
</div>
}
]}
/>
@@ -1,34 +1,53 @@
import { QuestionCircleOutlined } from '@ant-design/icons'
import { Codebox } from '@common/components/postcat/api/Codebox'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import { AiProviderLlmsItems, ModelDetailData } from './types'
import { forwardRef, useEffect, useImperativeHandle, useRef, useState } from 'react'
import { AiProviderLlmsItems, ModelDetailData, AiSettingListItem, AISettingEntityItem } from './types'
import { MemberItem, SimpleTeamItem } from '@common/const/type'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
export type AiSettingModalContentProps = {
entity: ModelDetailData & { defaultLlm: string }
entity?: AISettingEntityItem
readOnly: boolean
modelMode?: 'auto' | 'manual'
source?: string
/** 如果是手动选择 AI 模型,那么需要更新 footer 底部的内容,所以需要这个方法去更新外部的 footer */
updateEntityData: (entity: AISettingEntityItem) => void
}
export type AiSettingModalContentHandle = {
save: () => Promise<boolean | string>
deployAIServer: () => Promise<boolean | string>
}
const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingModalContentProps>((props, ref) => {
const [form] = Form.useForm()
const { message } = App.useApp()
const { entity, readOnly } = props
const { entity, readOnly, modelMode = 'auto', updateEntityData, source } = props
const { fetchData } = useFetch()
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>()
const [loading, setLoading] = useState<boolean>(false)
const [enableState, setEnableState] = useState<boolean>(entity.status === 'enabled')
const getLlmList = () => {
// AI 模型配置
const [localEntity, setLocalEntity] = useState(entity)
const [teamList, setTeamList] = useState<SimpleTeamItem[]>([])
// AI 模型提供商列表
const modelProviderListRef = useRef<AiSettingListItem[]>([])
// 模型模式加载
const [modelModeLoading, setModelModeLoading] = useState<boolean>(false)
const [enableState, setEnableState] = useState<boolean>(localEntity?.status === 'enabled')
const { checkPermission } = useGlobalContext()
/**
* llm
* @param id
*/
const getLlmList = (id?: string) => {
setLoading(true)
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[] }>>(`ai/provider/llms`, {
method: 'GET',
eoParams: { provider: entity.id }
eoParams: { provider: id || localEntity?.id }
})
.then((response) => {
const { code, data, msg } = response
@@ -43,41 +62,133 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
})
}
useEffect(() => {
getLlmList()
/**
*
* @returns
*/
const getTeamOptionList = async (): any[] => {
const response = await fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(
!checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams',
{ method: 'GET', eoTransformKeys: [] }
)
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const teamOptionList = data.teams?.map((x: MemberItem) => {
return { ...x, label: x.name, value: x.id }
})
setTeamList(teamOptionList)
if (form.getFieldValue('team') === undefined && data.teams?.length) {
form.setFieldValue('team', data.teams[0].id)
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return []
}
}
/**
*
*/
const getModelProviderList = () => {
setModelModeLoading(true)
fetchData<BasicResponse<{ providers: AiSettingListItem[] }>>(`ai/providers/unconfigured`, {
method: 'GET',
eoTransformKeys: ['default_llm', 'default_llm_logo']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const providers = data.providers || []
modelProviderListRef.current = providers
if (providers.length) {
const id = providers[0].id
form.setFieldValue('modelMode', id)
getModelConfig(id)
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.finally(() => {
setModelModeLoading(false)
})
}
/**
*
* @param id
*/
const getModelConfig = (id: string) => {
getLlmList(id)
fetchData<BasicResponse<{ providers: ModelDetailData[] }>>(`ai/provider/config`, {
method: 'GET',
eoParams: { provider: id },
eoTransformKeys: ['get_apikey_url', 'default_llm']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const modelEntity = {
...data.provider
}
setLocalEntity(modelEntity)
setFormFieldsValue(modelEntity)
updateEntityData?.(modelEntity)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.finally(() => {
setModelModeLoading(false)
})
}
/**
*
* @param fieldsValue
*/
const setFormFieldsValue = (fieldsValue: any) => {
try {
form.setFieldsValue({
defaultLlm: entity.defaultLlm,
config: entity!.config ? JSON.stringify(JSON.parse(entity!.config), null, 2) : '',
priority: entity.priority || 1,
enable: entity.status === 'enabled'
defaultLlm: fieldsValue.defaultLlm,
config: fieldsValue!.config ? JSON.stringify(JSON.parse(fieldsValue!.config), null, 2) : '',
enable: fieldsValue.status === 'enabled'
})
} catch (e) {
form.setFieldsValue({
defaultLlm: entity.defaultLlm,
defaultLlm: localEntity?.defaultLlm,
config: '',
priority: 1,
enable: true
})
}
}
useEffect(() => {
if (localEntity?.id) {
getModelConfig(localEntity.id)
setFormFieldsValue(localEntity)
} else {
getModelProviderList()
source && getTeamOptionList()
}
}, [])
const save: () => Promise<boolean | string> = () => {
/**
* AI
*/
const deployAIServer: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then((value) => {
const finalValue = {
...value,
priority: Math.max(1, value.priority)
config: value.config,
model: value.defaultLlm,
team: value.team,
provider: localEntity?.id
}
fetchData<BasicResponse<null>>('ai/provider/config', {
method: 'PUT',
eoParams: { provider: entity?.id },
eoBody: finalValue,
eoTransformKeys: ['defaultLlm']
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
fetchData<BasicResponse<null>>('quick/service/ai', {
method: 'POST',
eoBody: finalValue
})
.then((response) => {
const { code, msg } = response
@@ -95,6 +206,45 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
})
}
/**
*
* @returns
*/
const save: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
try {
form
.validateFields()
.then((value) => {
const finalValue = {
...value
}
fetchData<BasicResponse<null>>('ai/provider/config', {
method: 'PUT',
eoParams: { provider: localEntity?.id },
eoBody: finalValue,
eoTransformKeys: ['defaultLlm']
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
})
.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)
}
})
}
const getTooltipText = (isChecked: boolean) => {
if (!isChecked) {
return $t('保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。')
@@ -103,7 +253,8 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
}
useImperativeHandle(ref, () => ({
save
save,
deployAIServer
}))
return (
@@ -117,6 +268,26 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
autoComplete="off"
disabled={readOnly}
>
{modelMode === 'manual' && (
<Form.Item<ModelDetailData> label={$t('模型供应商')} name="modelMode" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
loading={modelModeLoading}
options={modelProviderListRef.current?.map((x) => ({
value: x.id,
label: (
<div className="flex items-center gap-[10px]">
<span>{x.name}</span>
</div>
)
}))}
onChange={(e) => {
getModelConfig(e)
}}
></Select>
</Form.Item>
)}
<Form.Item<ModelDetailData> label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
@@ -133,35 +304,13 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
}))}
></Select>
</Form.Item>
<Form.Item<ModelDetailData>
label={
<span className="flex items-center">
{$t('负载优先级')}
<Tooltip
title={$t('负载优先级决定在原供应商异常或停用后,优先使用哪一个供应商。优先级数字越小,优先级越高。')}
>
<QuestionCircleOutlined className="ml-1 text-gray-500" />
</Tooltip>
</span>
}
name="priority"
rules={[
{ required: true },
{
validator: async (_, value) => {
if (value <= 0) {
throw new Error($t('优先级必须大于 0'))
}
return Promise.resolve()
}
}
]}
initialValue={1}
>
<InputNumber className="w-INPUT_NORMAL" min={1} placeholder={$t('请输入优先级')} />
</Form.Item>
{source === 'guide' && (
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={teamList} onChange={(value) => {
form.setFieldValue('team', value)
}}></Select>
</Form.Item>
)}
<Form.Item<ModelDetailData> label={$t('API Key(默认 Key')} name="config">
<Codebox
editorTheme="vs-dark"
@@ -172,15 +321,14 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
enableToolbar={false}
/>
</Form.Item>
{entity.configured && (
{source !== 'guide' && (
<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>
{entity.status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
{entity.status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
{entity.status === 'abnormal' && <Tag color="error">{$t('异常')}</Tag>}
{localEntity?.status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
{localEntity?.status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
{localEntity?.status === 'abnormal' && <Tag color="error">{$t('异常')}</Tag>}
</div>
<Form.Item name="enable" valuePropName="checked" noStyle>
<Switch
@@ -193,7 +341,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
/>
</Form.Item>
</div>
{(entity.status === 'enabled' && !enableState) || (entity.status !== 'enabled' && enableState) ? (
{(localEntity?.status === 'enabled' && !enableState) || (localEntity?.status !== 'enabled' && enableState) ? (
<div className="mt-2 text-sm text-gray-500">* {getTooltipText(enableState)}</div>
) : null}
</Form.Item>
@@ -0,0 +1,357 @@
import { ActionType } from '@ant-design/pro-components'
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList'
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
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 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'
type EditLocalModelModalHandle = {
save: () => Promise<boolean | string>
}
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)
useEffect(() => {
form.setFieldsValue({ enable })
}, [])
/**
*
* @returns
*/
const save: () => Promise<boolean | string> = () => {
return new Promise((resolve, reject) => {
try {
form
.validateFields()
.then((value) => {
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))
})
.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 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)
const { message, modal } = App.useApp()
const { fetchData } = useFetch()
const [searchWord, setSearchWord] = useState<string>('')
const localAiDeployRef = useRef<LocalAiDeployHandle>()
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' }
})
const handleEdit = (record: ModelListData) => {
modal.confirm({
title: $t('模型设置'),
content: <EditLocalModelModal ref={EditLocalModelModalRef} modelID={record.id} enable={record.state !== 'disabled'}/>,
onOk: () => {
return EditLocalModelModalRef.current?.save().then((res) => {
if (res === true) {
pageListRef.current?.reload()
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const handleAdd = () => {
const modalInstance = modal.confirm({
title: $t('部署本地模型'),
content: (
<LocalAiDeploy
ref={localAiDeployRef}
onClose={() => {
modalInstance.destroy()
pageListRef.current?.reload()
}}
></LocalAiDeploy>
),
onOk: () => {
return localAiDeployRef.current?.deployLocalAIServer().then((res) => {
if (res === true) {
pageListRef.current?.reload()
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const handleDelete = async (id: string, apiCount: number) => {
modal.confirm({
title: $t('删除模型'),
content: `${$t('有')} ${apiCount} ${$t('个API使用当前模型,删除当前的模型配置后,该模型相关的API将会切换为使用负载均衡中优先级最高的可用模型。并且当前模型下的所有API KEY和相关数据将会被清空,是否确认删除当前模型?')}`,
onOk: () => {
return new Promise((resolve, reject) => {
try {
fetchData<BasicResponse<any>>('model/local', {
method: 'DELETE',
eoParams: {
model: id
}
})
.then((response) => {
if (response.code === STATUS_CODE.SUCCESS) {
message.success($t('删除成功'))
pageListRef.current?.reload()
} else {
message.error(response.msg || RESPONSE_TIPS.error)
}
resolve(true)
})
.catch((error) => {
message.error(RESPONSE_TIPS.error)
resolve(true)
})
} catch (error) {
message.error(RESPONSE_TIPS.error)
resolve(true)
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const requestList = async (params: any) => {
try {
const response = await fetchData<BasicResponse<{ data: ModelListData[] }>>('model/local/list', {
method: 'GET',
eoParams: {
page_size: params.pageSize,
keyword: searchWord,
page: params.current
},
eoTransformKeys: ['can_delete', 'api_count']
})
if (response.code === STATUS_CODE.SUCCESS) {
return {
data: response.data.models,
success: true,
total: response.data.total
}
} else {
message.error(response.msg || $t(RESPONSE_TIPS.error))
return {
data: [],
success: false,
total: response.data.total
}
}
} catch (error) {
return {
data: [],
success: false,
total: 0
}
}
}
const operation: PageProColumns<ModelListData>[] = [
{
title: '',
key: 'option',
btnNums: 4,
fixed: 'right',
valueType: 'option',
render: (_: React.ReactNode, entity: ModelListData) => [
<TableBtnWithPermission
access="system.devops.ai_provider.edit"
key="edit"
btnType="edit"
onClick={() => handleEdit(entity)}
btnTitle={$t('设置')}
/>,
<Divider type="vertical" className="mx-0" />,
<TableBtnWithPermission
disabled={!entity?.canDelete}
tooltip={$t('当前模型为最后一个模型,不支持删除')}
access="system.devops.ai_provider.edit"
key="delete"
btnType="delete"
onClick={() => handleDelete(entity.id as string, entity?.apiCount)}
btnTitle={$t('删除')}
/>
]
}
]
const openLogsModal = (record: any) => {
const closeModal = (reload = true) => {
reload && pageListRef.current?.reload()
modalInstance.destroy()
}
const modalInstance = modal.confirm({
title: $t('部署过程'),
content: <ServiceDeployment record={record} closeModal={closeModal} />,
footer: () => {
return <LogsFooter record={record} closeModal={closeModal} />
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const columns: PageProColumns<ModelListData>[] = [
{
title: $t('名称'),
dataIndex: 'name',
render: (dom: React.ReactNode, entity: ModelListData) => <Space>{entity.name}</Space>
},
{
title: $t('状态'),
width: 140,
dataIndex: 'state',
ellipsis: true,
render: (dom: React.ReactNode, entity: ModelListData) => (
<span
className={`text-[13px] ${stateColumnMap[entity?.state as string]?.className}`}
onClick={(e) => {
if (['deploying', 'deploying_error'].includes(entity?.state as string)) {
e?.stopPropagation()
openLogsModal(entity)
}
}}
>
{stateColumnMap[entity?.state as string]?.text || '-'}
</span>
)
},
{
title: $t('Apis'),
dataIndex: 'apiCount',
render: (dom: React.ReactNode, record: ModelListData) => (
<span className="[&>.key-link]:text-[#2196f3] cursor-pointer">
<a
href={`/aiApis?modelId=${record?.provider}`}
target="_blank"
className="key-link"
style={{
fontWeight: 500,
cursor: 'pointer',
pointerEvents: 'all',
textDecoration: 'none'
}}
>
{record.apiCount || '0'}
</a>
</span>
)
},
...operation
]
return (
<PageList
ref={pageListRef}
rowKey="id"
request={requestList}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
pageListRef.current?.reload()
}}
showPagination={true}
searchPlaceholder={$t('请输入名称搜索')}
columns={columns}
addNewBtnTitle={$t('部署模型')}
onAddNewBtnClick={handleAdd}
/>
)
}
export default LocalModelList
@@ -0,0 +1,225 @@
import { ActionType } from '@ant-design/pro-components'
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList'
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { App, Divider, Space, Typography } from 'antd'
import React, { useRef, useState } from 'react'
import { useAiSetting } from './contexts/AiSettingContext'
import { AiSettingListItem, ModelListData } from './types'
const OnlineModelList: React.FC = () => {
const pageListRef = useRef<ActionType>(null)
const { message, modal } = App.useApp()
const { fetchData } = useFetch()
const [searchWord, setSearchWord] = useState<string>('')
const [total, setTotal] = useState<number>(0)
const { openConfigModal } = useAiSetting()
const handleEdit = (record: ModelListData) => {
openConfigModal({ id: record.id, defaultLlm: record.defaultLlm } as AiSettingListItem, () => {
pageListRef.current?.reload()
})
}
const handleAdd = () => {
openConfigModal(undefined, () => {
pageListRef.current?.reload()
})
}
const handleDelete = async (id: string, apiCount: number) => {
modal.confirm({
title: $t('删除模型'),
content: `${$t('有')} ${apiCount} ${$t('个API使用当前模型,删除当前的模型配置后,该模型相关的API将会切换为使用负载均衡中优先级最高的可用模型。并且当前模型下的所有API KEY和相关数据将会被清空,是否确认删除当前模型?')}`,
onOk: () => {
return new Promise((resolve, reject) => {
try {
fetchData<BasicResponse<any>>('ai/provider', {
method: 'DELETE',
eoParams: {
provider: id
}
}).then((response) => {
if (response.code === STATUS_CODE.SUCCESS) {
message.success($t('删除成功'))
pageListRef.current?.reload()
} else {
message.error(response.msg || RESPONSE_TIPS.error)
}
resolve(true)
}).catch((error) => {
message.error(RESPONSE_TIPS.error)
resolve(true)
})
} catch (error) {
message.error(RESPONSE_TIPS.error)
resolve(true)
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const requestList = async (params: any) => {
try {
const response = await fetchData<BasicResponse<{ data: ModelListData[] }>>('ai/providers/configured', {
method: 'GET',
eoParams: {
page_size: params.pageSize,
keyword: searchWord,
page: params.current
},
eoTransformKeys: ['default_llm', 'api_count', 'key_count', 'can_delete']
})
if (response.code === STATUS_CODE.SUCCESS) {
setTotal(response.data.total)
return {
data: response.data.providers,
success: true,
total: response.data.total
}
} else {
message.error(response.msg || $t(RESPONSE_TIPS.error))
return {
data: [],
success: false,
total: response.data.total
}
}
} catch (error) {
return {
data: [],
success: false,
total: 0
}
}
}
const statusEnum = {
enabled: { text: <Typography.Text type="success">{$t('正常')}</Typography.Text> },
disabled: { text: <Typography.Text type="warning">{$t('停用')}</Typography.Text> },
abnormal: { text: <Typography.Text type="danger">{$t('异常')}</Typography.Text> }
}
const operation: PageProColumns<ModelListData>[] = [
{
title: '',
key: 'option',
btnNums: 4,
fixed: 'right',
valueType: 'option',
render: (_: React.ReactNode, entity: ModelListData) => [
<TableBtnWithPermission
access="system.devops.ai_provider.edit"
key="edit"
btnType="edit"
onClick={() => handleEdit(entity)}
btnTitle={$t('设置')}
/>,
<Divider type="vertical" className="mx-0" />,
<TableBtnWithPermission
access="system.devops.ai_provider.edit"
key="delete"
disabled={!entity?.canDelete}
tooltip={$t('当前模型为最后一个模型,不支持删除')}
btnType="delete"
onClick={() => handleDelete(entity.id as string, entity.apiCount)}
btnTitle={$t('删除')}
/>
]
}
]
const columns: PageProColumns<ModelListData>[] = [
{
title: $t('名称'),
dataIndex: 'name',
render: (dom: React.ReactNode, entity: ModelListData) => <Space>{entity.name}</Space>
},
{
title: $t('状态'),
dataIndex: 'status',
ellipsis: true,
valueType: 'select',
// filters: true,
// onFilter: true,
valueEnum: statusEnum,
render: (dom: React.ReactNode, entity: ModelListData) => statusEnum[entity.status]?.text || entity.status
},
{
title: $t('默认模型'),
ellipsis: true,
dataIndex: 'defaultLlm'
},
{
title: $t('Apis'),
dataIndex: 'apiCount',
render: (dom: React.ReactNode, record: ModelListData) => (
<span className="[&>.key-link]:text-[#2196f3] cursor-pointer">
<a
href={`/aiApis?modelId=${record?.id}`}
target="_blank"
className="key-link"
style={{
fontWeight: 500,
cursor: 'pointer',
pointerEvents: 'all',
textDecoration: 'none'
}}
>
{record.apiCount || '0'}
</a>
</span>
)
},
{
title: $t('Keys'),
dataIndex: 'keyCount',
render: (dom: React.ReactNode, record: ModelListData) => (
<span className="[&>.key-link]:text-[#2196f3] cursor-pointer">
<a
href={`/keysetting?modelId=${record?.id}`}
target="_blank"
className="key-link"
style={{
fontWeight: 500,
cursor: 'pointer',
pointerEvents: 'all',
textDecoration: 'none'
}}
>
{record.keyCount || '0'}
</a>
</span>
)
},
...operation
]
return (
<PageList
ref={pageListRef}
rowKey="id"
request={requestList}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
pageListRef.current?.reload()
}}
showPagination={true}
searchPlaceholder={$t('请输入名称搜索')}
columns={columns}
addNewBtnTitle={$t('添加模型')}
onAddNewBtnClick={handleAdd}
/>
)
}
export default OnlineModelList
@@ -1,72 +0,0 @@
import { BaseEdge, EdgeLabelRenderer, EdgeProps, getSmoothStepPath, useStore } from '@xyflow/react'
export default function CustomEdge({
id,
sourceX,
sourceY,
targetX,
targetY,
sourcePosition,
targetPosition,
style = {},
markerEnd,
label,
data,
source,
target
}: EdgeProps) {
// Get all edges to check for duplicates
const edges = useStore((state) => state.edges)
// Find duplicate edges between the same source and target
const duplicateEdges = edges.filter((edge) => edge.source === source && edge.target === target)
const edgeIndex = duplicateEdges.findIndex((edge) => edge.id === id)
// Adjust the path if this is a duplicate edge
const offset = edgeIndex * 20 // 20px offset for each duplicate edge
const [edgePath] = getSmoothStepPath({
sourceX,
sourceY: sourceY,
sourcePosition,
targetX,
targetY: targetY + offset,
targetPosition,
borderRadius: 16
})
const modelId = data?.id
return (
<>
<BaseEdge
path={edgePath}
markerEnd={markerEnd}
style={{
...style,
cursor: 'pointer'
}}
/>
{label && (
<EdgeLabelRenderer>
<a
href={`${label?.toString().includes('apis') ? '/aiApis' : '/keysetting'}?modelId=${modelId}`}
target="_blank"
style={{
position: 'absolute',
transform: `translate(${targetX - 80}px,${targetY - 20 + offset}px)`,
borderRadius: '4px',
fontSize: 12,
fontWeight: 500,
cursor: 'pointer',
pointerEvents: 'all',
textDecoration: 'none'
}}
>
{label}
</a>
</EdgeLabelRenderer>
)}
</>
)
}
@@ -1,53 +0,0 @@
import { Handle, Position } from '@xyflow/react'
import React from 'react'
import { KeyData } from '../types'
interface KeyStatusNodeData {
id: string
title: string
keys: KeyData[]
}
const KEY_SIZE = '1.25rem' // 20px
const KEY_GAP = '0.25rem' // 4px
const MAX_KEYS = 10
export const KeyStatusNode: React.FC<{ data: KeyStatusNodeData }> = ({ data }) => {
const { title, keys = [] } = data
const totalKeys = keys.length
const keyWidth = totalKeys > 5 ? `calc((100% - ${(totalKeys - 1) * 0.25}rem) / ${totalKeys})` : KEY_SIZE
return (
<div
className="relative p-4 bg-white rounded-lg shadow-sm node-card nodrag"
style={{ border: '1px solid var(--border-color)' }}
>
<Handle type="target" position={Position.Left} />
<div className="flex flex-col">
<div className="text-sm text-gray-900">{title}</div>
<div
className="flex gap-1 w-full"
style={{
minWidth: keys.length > 5 ? '118px' : 'auto',
maxWidth: `calc(${MAX_KEYS} * ${KEY_SIZE} + (${MAX_KEYS} - 1) * ${KEY_GAP})`,
minHeight: KEY_SIZE
}}
>
{keys.map((key) => (
<div
key={key.id}
style={{
width: keyWidth,
height: KEY_SIZE
}}
className={`
flex-shrink-0
${key.status === 'normal' ? 'bg-green-500' : 'bg-red-500'}
transition-all duration-200 hover:opacity-80
`}
/>
))}
</div>
</div>
</div>
)
}
@@ -1,76 +0,0 @@
import { $t } from '@common/locales'
import { Icon } from '@iconify/react'
import { Handle, Position } from '@xyflow/react'
import React from 'react'
import { useAiSetting } from '../contexts/AiSettingContext'
import { AiSettingListItem, ModelDetailData, ModelStatus } from '../types'
type ModelCardNodeData = ModelDetailData & {
id: string
position: { x: number; y: number }
alternativeModel?: ModelDetailData
}
export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) => {
const { name, status, defaultLlm, logo, alternativeModel } = data
const { openConfigModal } = useAiSetting()
const getStatusIcon = (status: ModelStatus) => {
switch (status) {
case 'enabled':
return { icon: 'mdi:check-circle', color: 'text-green-500' }
case 'disabled':
return { icon: 'mdi:pause-circle', color: 'text-gray-400' }
case 'abnormal':
return { icon: 'mdi:alert-circle', color: 'text-red-500' }
}
}
const statusConfig = getStatusIcon(status)
return (
<>
<div
className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[280px] group"
style={{ border: '1px solid var(--border-color)' }}
>
<Handle type="target" position={Position.Left} />
<Handle type="source" position={Position.Right} />
<div>
<div className="flex justify-between items-center">
<div className="flex gap-2 items-center">
<div className="flex flex-1 overflow-hidden items-center gap-[4px]">
<span
className="flex items-center h-[22px] ai-setting-svg-container"
dangerouslySetInnerHTML={{ __html: logo }}
></span>
</div>
<span className="text-base text-gray-900 max-w-[180px] truncate">{name}</span>
<Icon icon={statusConfig?.icon} className={`text-xl ${statusConfig?.color}`} />
</div>
{/* Action buttons */}
<div className="flex gap-2 transition-opacity duration-200">
<Icon
icon="mdi:cog"
className="text-xl text-gray-400 cursor-pointer hover:text-[--primary-color]"
onClick={() => {
openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem)
}}
/>
</div>
</div>
<div className="mt-2 text-sm text-gray-500">
{$t('默认:')}
{defaultLlm}
</div>
</div>
</div>
{status !== 'enabled' && alternativeModel && (
<div className="ml-4 mt-1 text-sm text-gray-500">
{$t('关联 API 已转用')} {alternativeModel.name}/{alternativeModel.defaultLlm}
</div>
)}
</>
)
}
@@ -1,3 +0,0 @@
export { KeyStatusNode } from './KeyStatusNode'
export { ModelCardNode } from './ModelCardNode'
export { ServiceCardNode } from './ServiceCardNode'
@@ -1,18 +0,0 @@
import { Icon } from '@iconify/react'
import { Handle, NodeProps, Position } from '@xyflow/react'
import React from 'react'
export const ServiceCardNode: React.FC<NodeProps> = () => {
return (
<div
className="node-card bg-white rounded-lg shadow-sm p-4 min-w-[150px] nodrag"
style={{ border: '1px solid var(--border-color)' }}
>
<Handle type="source" position={Position.Right} />
<div className="flex flex-col gap-2 items-center">
<Icon icon="mdi:robot" className="text-3xl text-[--primary-color]" />
<span className="text-base text-gray-900">AI Services</span>
</div>
</div>
)
}
@@ -1,45 +1,39 @@
import Icon from '@ant-design/icons'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales'
import { checkAccess } from '@common/utils/permission'
import { App } from 'antd'
import { createContext, useContext, useRef } from 'react'
import AiSettingModalContent, { AiSettingModalContentHandle } from '../AiSettingModal'
import { AiSettingListItem, ModelDetailData } from '../types'
import { AiSettingListItem } from '../types'
interface AiSettingContextType {
openConfigModal: (entity: AiSettingListItem) => Promise<void>
openConfigModal: (entity?: AiSettingListItem, callback?: () => void) => Promise<void>
}
const AiSettingContext = createContext<AiSettingContextType | undefined>(undefined)
export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
const { modal, message } = App.useApp()
const { fetchData } = useFetch()
const { modal } = App.useApp()
const { aiConfigFlushed, setAiConfigFlushed, accessData } = useGlobalContext()
const modalRef = useRef<AiSettingModalContentHandle>()
const entityData = useRef<any>(null)
const openConfigModal = async (entity: AiSettingListItem) => {
message.loading($t(RESPONSE_TIPS.loading))
const { code, data, msg } = await fetchData<BasicResponse<{ provider: ModelDetailData }>>('ai/provider/config', {
method: 'GET',
eoParams: { provider: entity!.id },
eoTransformKeys: ['get_apikey_url']
})
message.destroy()
if (code !== STATUS_CODE.SUCCESS) {
message.error(msg || $t(RESPONSE_TIPS.error))
return
const openConfigModal = async (entity?: AiSettingListItem, callback?: () => void) => {
// 更新弹窗
const updateEntityData = (data: any) => {
entityData.current = data
// 更新弹窗
modalInstance.update({})
}
modal.confirm({
const modalInstance = modal.confirm({
title: $t('模型配置'),
content: (
<AiSettingModalContent
ref={modalRef}
entity={{ ...data.provider, defaultLlm: entity.defaultLlm }}
entity={{ id: entity?.id, defaultLlm: entity?.defaultLlm }}
modelMode={entity ? 'auto' : 'manual'}
updateEntityData={updateEntityData}
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
/>
),
@@ -47,6 +41,7 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
return modalRef.current?.save().then((res) => {
if (res === true) {
setAiConfigFlushed(!aiConfigFlushed)
callback?.()
}
})
},
@@ -58,10 +53,10 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
<a
target="_blank"
rel="noopener noreferrer"
href={data.provider.getApikeyUrl}
href={entityData.current?.getApikeyUrl}
className="flex items-center gap-[8px]"
>
<span>{$t('从 (0) 获取 API KEY', [data.provider.name])}</span>
<span>{$t('从 (0) 获取 API KEY', [entityData.current?.name])}</span>
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
</a>
<div>
@@ -1,32 +1,42 @@
export type ModelStatus = 'enabled' | 'abnormal'|'disabled'
export type KeyStatus ='normal' | 'abnormal'|'disabled'
export type ModelStatus = 'enabled' | 'abnormal' | 'disabled'
export type KeyStatus = 'normal' | 'abnormal' | 'disabled'
export type ModelDeployStatus = 'normal' | 'disabled' | 'deploying' | 'error' | 'deploying_error' | undefined
export interface KeyData {
id: string
name: string
status: KeyStatus,
status: KeyStatus
}
export interface ModelListData {
id: string
id: string | undefined
name: string
logo: string
defaultLlm: string
defaultLlm: string | undefined
provider?: string
modelMode?: string
status: ModelStatus
api_count: number
key_count: number
state?: ModelDeployStatus
apiCount: number
keyCount: number
isDisabled?: boolean
keys: KeyData[]
canDelete: boolean
}
export interface ModelDetailData extends ModelListData{
enable:boolean
config: string,
priority?: number
export interface AISettingEntityItem {
id: string | undefined
status?: ModelStatus | undefined
defaultLlm: string | undefined
}
export interface ModelDetailData extends ModelListData {
enable: boolean
config: string
getApikeyUrl: string
status: ModelStatus
configured: boolean
}
export type AiSettingListItem = {
name: string
id: string
@@ -53,5 +63,3 @@ export type AiProviderDefaultConfig = {
defaultLlm: string
scopes: string[]
}
@@ -0,0 +1,186 @@
import restAPIPic from '@common/assets/restAPI.svg'
import onlineAIPic from '@common/assets/onlineAI.svg'
import localAIPic from '@common/assets/localAI.svg'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
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 { 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'
export const AIModelGuide = () => {
const { modal } = App.useApp()
const entityData = useRef<any>(null)
const navigateTo = useNavigate()
const { accessData } = useGlobalContext()
const modalRef = useRef<AiSettingModalContentHandle>()
const localAiDeployRef = useRef<LocalAiDeployHandle>()
const restAiDeployRef = useRef<RestAIDeployHandle>()
const { deployLocalModel } = useDeployLocalModel()
const dumpServerPage = () => {
navigateTo('/service/list')
}
/**
* rest
*/
const restCardClick = async () => {
modal.confirm({
title: $t('添加 Rest 服务'),
content: <RestAIDeploy ref={restAiDeployRef}></RestAIDeploy>,
onOk: () => {
return restAiDeployRef.current?.deployRestAIServer().then((res) => {
if (res === true) {
dumpServerPage()
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
/**
* AI
*/
const aiCardClick = () => {
// 更新弹窗
const updateEntityData = (data: any) => {
entityData.current = data
// 更新弹窗
modalInstance.update({})
}
const modalInstance = modal.confirm({
title: $t('模型配置'),
content: (
<AiSettingModalContent
ref={modalRef}
modelMode="manual"
updateEntityData={updateEntityData}
source="guide"
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
/>
),
onOk: () => {
return modalRef.current?.deployAIServer().then((res) => {
if (res === true) {
dumpServerPage()
}
})
},
width: 600,
okText: $t('确认'),
footer: (_, { OkBtn, CancelBtn }) => {
return (
<div className="flex justify-between items-center">
<a
target="_blank"
rel="noopener noreferrer"
href={entityData.current?.getApikeyUrl}
className="flex items-center gap-[8px]"
>
<span>{$t('从 (0) 获取 API KEY', [entityData.current?.name])}</span>
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
</a>
<div>
<CancelBtn />
{checkAccess('system.devops.ai_provider.edit', accessData) ? <OkBtn /> : null}
</div>
</div>
)
},
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
/**
* AI API
*/
const localModelCardClick = async () => {
const modalInstance = modal.confirm({
title: $t('部署本地模型'),
content: <LocalAiDeploy ref={localAiDeployRef} onClose={() => {
modalInstance.destroy()
dumpServerPage()
}}></LocalAiDeploy>,
onOk: () => {
return localAiDeployRef.current?.deployLocalAIServer().then((res) => {
if (res === true) {
dumpServerPage()
}
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const deployDeepSeek = async (e: any) => {
e.stopPropagation()
await deployLocalModel({
modelID: 'deepseek-r1'
})
dumpServerPage()
}
const cardList = [
{
imgSrc: restAPIPic,
title: $t('添加 Rest 服务'),
description: $t('支持批量添加现有 API 文档以实现统一的外部访问。'),
click: restCardClick
},
{
imgSrc: onlineAIPic,
title: $t('添加在线 AI API'),
description: $t('快速调用 AI 模型的云服务 API,方便管理提示词和统一计费。'),
click: aiCardClick
},
{
imgSrc: localAIPic,
title: $t('本地部署 AI 并生成 API'),
description: $t('快速在本地部署开源模型并自动生成 API。'),
click: localModelCardClick,
bottomRender: (
<span className="text-[#2196f3] text-[13px] hover:text-[#1976d2]" onClick={deployDeepSeek}>
<Icon className="align-sub mr-[5px]" icon="lsicon:lightning-filled" width="15" height="15" />
{$t('部署')} Deepseek-R1
</span>
)
}
]
return (
<div className="mb-[30px] p-[15px] flex justify-center">
{cardList.map((item, itemIndex) => (
<Card
key={itemIndex}
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] mr-[30px] rounded-[10px] overflow-visible cursor-pointer w-[250px] transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
classNames={{
header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal',
body: 'p-[20px] pt-[50px] pb-[50px] text-[12px] text-[#666] text-center'
}}
onClick={item.click}
>
<img src={item.imgSrc} alt="" width={60} height={60} />
<p className="text-[13px] font-bold text-black mt-[10px] mb-[10px]">{item.title}</p>
<p className="break-words mb-[10px]">{item.description}</p>
{item.bottomRender ? item.bottomRender : null}
</Card>
))}
</div>
)
}
+306 -215
View File
@@ -1,232 +1,323 @@
import InsidePage from "@common/components/aoplatform/InsidePage"
import { useGlobalContext } from "@common/contexts/GlobalStateContext"
import { $t } from "@common/locales"
import { Icon } from "@iconify/react/dist/iconify.js"
import { Button, Card, Collapse } from "antd"
import { Dispatch, SetStateAction, useEffect, useState } from "react"
import { useLocation, useNavigate } from "react-router-dom"
import InsidePage from '@common/components/aoplatform/InsidePage'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
import { $t } from '@common/locales'
import { Icon } from '@iconify/react/dist/iconify.js'
import { Button, Card, Collapse } from 'antd'
import { Dispatch, SetStateAction, useEffect, useState } from 'react'
import { useLocation, useNavigate } from 'react-router-dom'
import { AIModelGuide } from './AIModelGuide'
export default function Guide(){
const [showGuide, setShowGuide] = useState(localStorage.getItem('showGuide') !== 'false' )
const [showAdvancedGuide, setShowAdvancedGuide] = useState(localStorage.getItem('showAdvancedGuide') !== 'false' )
const [, forceUpdate] = useState<unknown>(null);
const {state} = useGlobalContext()
const location = useLocation()
const currentUrl = location.pathname
const navigator = useNavigate()
const guideSections = [
export default function Guide() {
const [showGuide, setShowGuide] = useState(localStorage.getItem('showGuide') !== 'false')
const [showAdvancedGuide, setShowAdvancedGuide] = useState(localStorage.getItem('showAdvancedGuide') !== 'false')
const [, forceUpdate] = useState<unknown>(null)
const { state } = useGlobalContext()
const location = useLocation()
const currentUrl = location.pathname
const navigator = useNavigate()
const guideSections = [
{
title: $t('快速接入 AI'),
items: [
{
title: $t('快速接入 AI'),
items: [
{
title: $t("配置你的 AI 模型"),
description: $t('通过 APIPark 快速接入各种 AI 模型,使用统一的格式来调用API,并且可以随意切换模型。'),
link: 'https://docs.apipark.com/docs/system_setting/ai_model_providers'
},
{
title: $t("创建 AI 服务和 API"),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/ai_services'
},
{
title: $t("创建调用 Token"),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t("调用"),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
]
title: $t('配置你的 AI 模型'),
description: $t('通过 APIPark 快速接入各种 AI 模型,使用统一的格式来调用API,并且可以随意切换模型。'),
link: 'https://docs.apipark.com/docs/system_setting/ai_model_providers'
},
{
title: $t('快速接入 REST API'),
items: [
{
title: $t("创建 REST 服务和 API"),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/rest_services'
},
{
title: $t("创建调用 Token"),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t("调用"),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
]
title: $t('创建 AI 服务和 API'),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/ai_services'
},
{
title: $t('仪表盘'),
items: [
{
title: $t("统计 API 调用情况"),
description: $t('仪表盘中提供了多种统计图表,帮助我们了解 API 的运行情况。'),
link: 'https://docs.apipark.com/docs/analysis'
}
]
title: $t('创建调用 Token'),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t('调用'),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
];
const advanceGuideSections = [
]
},
{
title: $t('快速接入 REST API'),
items: [
{
title: $t('核心功能'),
items: [
{
title: $t("账号与角色"),
description: $t('邀请你的团队成员加入 APIPark,共同管理和调用 API。'),
link: 'https://docs.apipark.com/docs/system_setting/account_role'
},
{
title: $t("团队"),
description: $t('团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。'),
link: 'https://docs.apipark.com/docs/teams'
},
{
title: $t("服务"),
description: $t('服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。'),
link: 'https://docs.apipark.com/docs/category/-%E6%9C%8D%E5%8A%A1'
}
]
title: $t('创建 REST 服务和 API'),
description: $t('创建 AI 类型的服务,并且你可以将 Prompt 提示词设置为一个 API,简化使用 AI 的流程。'),
link: 'https://docs.apipark.com/docs/services/rest_services'
},
{
title: $t('权限管理'),
items: [
{
title: $t("订阅服务"),
description: $t('如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。'),
link: 'https://docs.apipark.com/docs/developer_portal'
},
{
title: $t("审核订阅申请"),
description: $t('提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。'),
link: 'https://docs.apipark.com/docs/services/review_consumers'
}
]
title: $t('创建调用 Token'),
description: $t('为了安全地调用 API,你需要创建一个消费者以及Token。'),
link: 'https://docs.apipark.com/docs/consumers'
},
{
title: $t('集成'),
items: [
{
title: $t("日志"),
description: $t('APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。'),
link: 'https://docs.apipark.com/docs/system_setting/log/'
}
]
title: $t('调用'),
description: $t('现在你可以通过 Token 来调用这些 API。'),
link: 'https://docs.apipark.com/docs/call_api'
}
];
useEffect(()=>{
localStorage.setItem('showGuide', showGuide.toString())
},[showGuide])
useEffect(()=>{
localStorage.setItem('showAdvancedGuide', showAdvancedGuide.toString())
},[showAdvancedGuide])
useEffect(()=>{
if(currentUrl === '/guide'){
setTimeout(()=>{
navigator('/guide/page')
},0)
]
},
{
title: $t('仪表盘'),
items: [
{
title: $t('统计 API 调用情况'),
description: $t('仪表盘中提供了多种统计图表,帮助我们了解 API 的运行情况。'),
link: 'https://docs.apipark.com/docs/analysis'
}
},[])
useEffect(()=>{forceUpdate({})},[state.language])
return (
<InsidePage
pageTitle={<div className="flex items-center gap-[8px]">
<span>👋</span>
<span>{$t('Hello!欢迎使用 APIPark')}</span>
<a className="" href="https://github.com/APIParkLab/APIPark" target="_blank"><img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social"alt="" /></a>
</div>}
description={<div className="flex flex-col gap-[8px]">
<p>{$t("你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。")}</p>
<p>{$t("如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。")}</p>
</div>}
showBorder={false}
scrollPage={false}
contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B"
>
<div className="flex flex-col gap-[15px]">
{showGuide &&
<Collapse
size="large"
expandIconPosition='end'
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent "
items={[{ key: '1',
label:
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🚀</span><span>{`${$t('快速入门')}`}</span> </p>
<p className="text-[12px]" >{$t("我们提供了一些任务来帮你快速了解 APIPark")}</p></div>,
children:<QuickGuideContent changeGuideShow={setShowGuide} guideSections={guideSections} /> }]}
/>}
{showAdvancedGuide &&
<Collapse
size="large"
expandIconPosition='end'
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent "
items={[{ key: '1',
label:
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🏍</span><span>{`${$t('进阶教程')}`}</span> </p>
<p className="text-[12px]" >{$t("了解 APIPark 如何更好地管理 API 和 AI")}</p></div>,
children:<QuickGuideContent changeGuideShow={setShowAdvancedGuide} guideSections={advanceGuideSections} /> }]}
/>}
</div>
</InsidePage>)
]
}
]
const advanceGuideSections = [
{
title: $t('核心功能'),
items: [
{
title: $t('账号与角色'),
description: $t('邀请你的团队成员加入 APIPark,共同管理和调用 API。'),
link: 'https://docs.apipark.com/docs/system_setting/account_role'
},
{
title: $t('团队'),
description: $t(
'团队中包含了人员、消费者和服务,不同团队之间的消费者和服务数据是隔离的,可用于管理企业内部不同的部门/项目组/团队。'
),
link: 'https://docs.apipark.com/docs/teams'
},
{
title: $t('服务'),
description: $t('服务内包含一组 API,并且可以发布到 API 市场被其他团队使用。'),
link: 'https://docs.apipark.com/docs/category/-%E6%9C%8D%E5%8A%A1'
}
]
},
{
title: $t('权限管理'),
items: [
{
title: $t('订阅服务'),
description: $t(
'如果需要调用某个服务的 API,需要先订阅该服务,并且等待提供服务的团队审核后才可发起 API 请求。'
),
link: 'https://docs.apipark.com/docs/developer_portal'
},
{
title: $t('审核订阅申请'),
description: $t('提供服务的团队可以审核来自其他团队的订阅申请,审核通过后的消费者才可发起 API 请求。'),
link: 'https://docs.apipark.com/docs/services/review_consumers'
}
]
},
{
title: $t('集成'),
items: [
{
title: $t('日志'),
description: $t('APIPark 提供详尽的 API 调用日志,帮助企业监控、分析和审计 API 的运行状况。'),
link: 'https://docs.apipark.com/docs/system_setting/log/'
}
]
}
]
useEffect(() => {
localStorage.setItem('showGuide', showGuide.toString())
}, [showGuide])
useEffect(() => {
localStorage.setItem('showAdvancedGuide', showAdvancedGuide.toString())
}, [showAdvancedGuide])
useEffect(() => {
if (currentUrl === '/guide') {
setTimeout(() => {
navigator('/guide/page')
}, 0)
}
}, [])
useEffect(() => {
forceUpdate({})
}, [state.language])
return (
<InsidePage
pageTitle={
<div className="flex items-center gap-[8px]">
<span>👋</span>
<span>{$t('Hello!欢迎使用 APIPark')}</span>
</div>
}
description={
<div className="flex flex-col gap-[8px]">
<p>
{$t(
'你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。'
)}
</p>
<p>
{$t('如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。')}
<span className="font-bold">
{$t('点击这里')}
<span className="align-middle leading-[16px]">
&nbsp;
<Icon icon="pajamas:arrow-right" width="16" height="16" />
&nbsp;
</span>
<a className="align-text-top" href="https://github.com/APIParkLab/APIPark" target="_blank">
<img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social" alt="" />
</a>
<span className="align-middle leading-[16px]">
&nbsp;
<Icon icon="pajamas:arrow-right" width="16" height="16" />
&nbsp;
</span>
{$t('点击')}
&nbsp;
<span className="align-middle leading-[16px]">
<Icon icon="emojione:star" width="16" height="16" />
</span>
Star
</span>
</p>
</div>
}
showBorder={false}
scrollPage={false}
contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B"
>
<AIModelGuide></AIModelGuide>
<div className="flex flex-col gap-[15px]">
{showGuide && (
<Collapse
size="large"
expandIconPosition="end"
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent "
items={[
{
key: '1',
label: (
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🚀</span>
<span>{`${$t('快速入门')}`}</span>{' '}
</p>
<p className="text-[12px]">{$t('我们提供了一些任务来帮你快速了解 APIPark')}</p>
</div>
),
children: <QuickGuideContent changeGuideShow={setShowGuide} guideSections={guideSections} />
}
]}
/>
)}
{showAdvancedGuide && (
<Collapse
size="large"
expandIconPosition="end"
defaultActiveKey={['1']}
className="bg-[linear-gradient(153.41deg,rgba(244,245,255,1)_0.23%,rgba(255,255,255,1)_83.32%)] rounded-[10px] [&>.ant-collapse-item>.ant-collapse-content]:bg-transparent "
items={[
{
key: '1',
label: (
<div className="">
<p className="text-[14px] mb-[10px] flex gap-[8px] items-center font-bold">
<span>🏍</span>
<span>{`${$t('进阶教程')}`}</span>{' '}
</p>
<p className="text-[12px]">{$t('了解 APIPark 如何更好地管理 API 和 AI')}</p>
</div>
),
children: (
<QuickGuideContent changeGuideShow={setShowAdvancedGuide} guideSections={advanceGuideSections} />
)
}
]}
/>
)}
</div>
</InsidePage>
)
}
const QuickGuideContent = ({changeGuideShow,guideSections}:{changeGuideShow:Dispatch<SetStateAction<boolean>>,guideSections: {
title: string;
const QuickGuideContent = ({
changeGuideShow,
guideSections
}: {
changeGuideShow: Dispatch<SetStateAction<boolean>>
guideSections: {
title: string
items: {
title: string;
description: string;
link: string;
}[];
}[]})=>{
return (<>
<div className="">
{guideSections.map((section, index) => (
<div key={index}>
<p className="flex gap-[8px] items-center text-[14px] font-bold">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
{section.title}
</p>
<div className="ml-[9px] border-[0px] border-l-[1px] my-[10px] border-dashed border-BORDER">
<div className="grid gap-[20px] px-[20px] py-[10px] justify-start content-start" style={{
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 0fr))',
gridAutoRows: '1fr'
}}>
{section.items.map((item, itemIndex) => (
<Card
key={itemIndex}
title={item.title}
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible cursor-pointer w-[300px] transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
classNames={{ header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal', body: "p-[20px] pt-0 text-[12px] text-[#666]" }}
onClick={() => { window.open(item.link, '_blank') }}
>
<span>{item.description}</span>
</Card>
))}
</div>
</div>
</div>
))}
<p className="flex gap-[8px] items-center">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme"/>
<div className="flex items-center w-full gap-4">
<Button type="link" icon={<Icon icon="ic:baseline-open-in-new" width="18" height="18" />} iconPosition="end" classNames={{icon:'h-[22px] flex items-center'}} href="https://docs.apipark.com" target="_blank" className="text-[14px] font-bold px-0">{$t('了解更多功能')}</Button>
<Button type="text" icon={<Icon icon="ic:baseline-visibility-off" width="18" height="18" />} onClick={()=>changeGuideShow((prev)=>!prev)} classNames={{icon:'h-[22px] flex items-center'}} className="text-[14px] font-bold">{$t('隐藏该教程')}</Button>
</div>
</p>
</div>
</>)
}
title: string
description: string
link: string
}[]
}[]
}) => {
return (
<>
<div className="">
{guideSections.map((section, index) => (
<div key={index}>
<p className="flex gap-[8px] items-center text-[14px] font-bold">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
{section.title}
</p>
<div className="ml-[9px] border-[0px] border-l-[1px] my-[10px] border-dashed border-BORDER">
<div
className="grid gap-[20px] px-[20px] py-[10px] justify-start content-start"
style={{
gridTemplateColumns: 'repeat(auto-fill, minmax(300px, 0fr))',
gridAutoRows: '1fr'
}}
>
{section.items.map((item, itemIndex) => (
<Card
key={itemIndex}
title={item.title}
className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible cursor-pointer w-[300px] transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
classNames={{
header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal',
body: 'p-[20px] pt-0 text-[12px] text-[#666]'
}}
onClick={() => {
window.open(item.link, '_blank')
}}
>
<span>{item.description}</span>
</Card>
))}
</div>
</div>
</div>
))}
<div className="flex gap-[8px] items-center">
<Icon icon="ic:baseline-info" width="18" height="18" className="text-theme" />
<div className="flex items-center w-full gap-4">
<Button
type="link"
icon={<Icon icon="ic:baseline-open-in-new" width="18" height="18" />}
iconPosition="end"
classNames={{ icon: 'h-[22px] flex items-center' }}
href="https://docs.apipark.com"
target="_blank"
className="text-[14px] font-bold px-0"
>
{$t('了解更多功能')}
</Button>
<Button
type="text"
icon={<Icon icon="ic:baseline-visibility-off" width="18" height="18" />}
onClick={() => changeGuideShow((prev) => !prev)}
classNames={{ icon: 'h-[22px] flex items-center' }}
className="text-[14px] font-bold"
>
{$t('隐藏该教程')}
</Button>
</div>
</div>
</div>
</>
)
}
@@ -0,0 +1,197 @@
import { Icon } from '@iconify/react/dist/iconify.js'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { Form, message, Select } from 'antd'
import { $t } from '@common/locales'
import { LocalModelItem, SimpleTeamItem } from '@common/const/type'
import { useFetch } from '@common/hooks/http'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import useDeployLocalModel from './deployModelUtil'
export type LocalAiDeployHandle = {
deployLocalAIServer: () => Promise<boolean | string>
}
const LocalAiDeploy = forwardRef<LocalAiDeployHandle, any>((props: any, ref: any) => {
const { onClose } = props
const [form] = Form.useForm()
const { fetchData } = useFetch()
const [modelList, setModelList] = useState<any[]>([])
const [tagList, setTagList] = useState<any[]>([])
const [teamList, setTeamList] = useState<SimpleTeamItem[]>([])
const { deployLocalModel, getTeamOptionList } = useDeployLocalModel()
/**
*
* @returns
*/
const getLocalModelList = async (keyword?: string) => {
const response = await fetchData<BasicResponse<{ models: LocalModelItem[] }>>('model/local/can_deploy', {
method: 'GET',
eoTransformKeys: ['is_popular'],
...(keyword ? { eoParams: { keyword } } : {})
})
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
if (!keyword) {
const modelList = data.models?.map((x: LocalModelItem) => {
return { ...x, label: x.name, value: x.id }
})
setModelList(modelList)
} else {
const tagList = data.models?.map((x: LocalModelItem) => {
return { ...x, label: x.name, value: x.id }
})
setTagList(tagList)
if (tagList.length) {
form.setFieldValue('model', tagList[0].id)
}
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return []
}
}
/**
*
* @param id ID
* @returns
*/
const deployPopularModel = async (id: string) => {
const response = await deployLocalModel({
modelID: id
})
if (response.code !== STATUS_CODE.SUCCESS) {
return
}
onClose?.()
}
const getTeamList = async () => {
const teamOptionList = await getTeamOptionList()
setTeamList(teamOptionList)
if (form.getFieldValue('team') === undefined && teamOptionList.length) {
form.setFieldValue('team', teamOptionList[0].value)
}
}
useEffect(() => {
getLocalModelList()
getTeamList()
}, [])
/**
* AI
* @returns
*/
const deployLocalAIServer = () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then(async (value) => {
const response = await deployLocalModel({
modelID: value.model,
team: value.team
})
if (response.code !== STATUS_CODE.SUCCESS) {
return
}
resolve(true)
})
.catch((errorInfo) => reject(errorInfo))
})
}
useImperativeHandle(ref, () => ({
deployLocalAIServer
}))
return (
<WithPermission access="">
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="mx-auto "
name="partitionInsideCert"
autoComplete="off"
>
<Form.Item label={$t('模型供应商')} name="provider" rules={[{ required: true }]}>
<Select
showSearch
className="w-INPUT_NORMAL"
filterOption={(input, option) => (option?.searchText ?? '').includes(input.toLowerCase())}
placeholder={$t(PLACEHOLDER.input)}
options={modelList.map((provider) => ({
label: (
<div className="relative">
<span>{provider.name}</span>
<span className="absolute right-[10px] text-[#999]">{provider.size}</span>
</div>
),
value: provider.id,
searchText: provider.name.toLowerCase()
}))}
onChange={(value) => {
form.setFieldValue('provider', value)
getLocalModelList(value)
}}
></Select>
<div className="mt-[10px] mb-[5px]">
<Icon className="align-text-top" icon="noto-v1:fire" width="17" height="17" />
{$t('热点模型')}
</div>
<div className="pl-[5px] flex flex-wrap">
{modelList.length
? modelList
.filter((item) => item.isPopular)
.map((item) => (
<span
key={item.id}
className="text-[#2196f3] text-[15px] hover:text-[#1976d2] mr-[20px] cursor-pointer
"
onClick={() => {
deployPopularModel(item.id)
}}
>
{item.name}({item.size})
</span>
))
: null}
</div>
</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>
</Form.Item>
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
options={teamList}
onChange={(value) => {
form.setFieldValue('team', value)
}}
></Select>
</Form.Item>
</Form>
</WithPermission>
)
})
export default LocalAiDeploy
@@ -0,0 +1,122 @@
import { Icon } from '@iconify/react/dist/iconify.js'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { Upload, UploadProps, Form, message, Select } from 'antd'
import { $t } from '@common/locales'
import { SimpleTeamItem } from '@common/const/type'
import { useFetch } from '@common/hooks/http'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import useDeployLocalModel from './deployModelUtil'
const { Dragger } = Upload
export type RestAIDeployHandle = {
deployRestAIServer: () => Promise<boolean | string>
}
const RestAIDeploy = forwardRef<RestAIDeployHandle, any>((props: any, ref: any) => {
const [form] = Form.useForm()
const { fetchData } = useFetch()
const [teamList, setTeamList] = useState<SimpleTeamItem[]>([])
const { getTeamOptionList } = useDeployLocalModel()
const uploadProps: UploadProps = {
accept: '.json,.yaml',
name: 'file',
multiple: false,
maxCount: 1,
beforeUpload: (file) => {
form.setFieldsValue({ key: file })
return false
}
}
const getTeamList = async () => {
const teamOptionList = await getTeamOptionList()
setTeamList(teamOptionList)
if (form.getFieldValue('team') === undefined && teamOptionList.length) {
form.setFieldValue('team', teamOptionList[0].value)
}
}
useEffect(() => {
getTeamList()
}, [])
/**
* rest
* @param file
* @returns
*/
const deployRestServer = async (file: File) => {
return new Promise((resolve, reject) => {
const formData = new FormData()
formData.append('file', file)
formData.append('type', 'swagger')
formData.append('team', form.getFieldValue('team'))
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>('quick/service/rest', {
method: 'POST',
body: formData
}).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(false)
}
})
})
}
/**
* AI
* @returns
*/
const deployRestAIServer = () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then(async (value) => {
await deployRestServer(value.key.file)
resolve(true)
})
.catch((errorInfo) => reject(errorInfo))
})
}
useImperativeHandle(ref, () => ({
deployRestAIServer
}))
return (
<WithPermission access="">
<Form
layout="vertical"
labelAlign="left"
scrollToFirstError
form={form}
className="mx-auto "
name="partitionInsideCert"
autoComplete="off"
>
<Form.Item name="key" className="mb-0 bg-transparent p-0 border-none rounded-none" rules={[{ required: true }]}>
<Dragger {...uploadProps}>
<p className="ant-upload-drag-icon">
<Icon className="text-[#ccc]" icon="tdesign:upload" width="50" height="50" />
</p>
<p className="ant-upload-text">{$t('选择 OpenAPI 文件 (.json / .yaml)')}</p>
</Dragger>
</Form.Item>
<Form.Item label={$t('所属团队')} name="team" className="mt-[16px]" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
options={teamList}
onChange={(value) => {
form.setFieldValue('team', value)
}}
></Select>
</Form.Item>
</Form>
</WithPermission>
)
})
export default RestAIDeploy
@@ -0,0 +1,54 @@
// deployModelUtil.ts
import { useFetch } from '@common/hooks/http'
import { message } from 'antd'
import { STATUS_CODE, RESPONSE_TIPS, BasicResponse } from '@common/const/const'
import { $t } from '@common/locales'
import { MemberItem, SimpleTeamItem } from '@common/const/type'
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
const useDeployLocalModel = () => {
const { fetchData } = useFetch()
const { checkPermission } = useGlobalContext()
const deployLocalModel = async (value: { modelID: string; team?: number }) => {
const response = await fetchData<BasicResponse<null>>(
'model/local/deploy/start',
{
method: 'POST',
eoBody: {
model: value.modelID,
team: value?.team
}
}
)
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
message.success(msg || $t(RESPONSE_TIPS.success))
return response
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
}
/**
* team
* @returns
*/
const getTeamOptionList = async (): any[] => {
const response = await fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(
!checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams',
{ method: 'GET', eoTransformKeys: [] }
)
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
const teamOptionList = data.teams?.map((x: MemberItem) => {
return { ...x, label: x.name, value: x.id }
})
return teamOptionList
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return []
}
}
return { deployLocalModel, getTeamOptionList }
}
export default useDeployLocalModel
@@ -0,0 +1,227 @@
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales/index.ts'
import { App, Form, Select, Tag } from 'antd'
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
import { LoadBalancingHandle, LoadModelDetailData, LocalLlmType } from './type'
import { ApiResponse } from '../aiSetting/AIFlowChart'
import { AiProviderLlmsItems, ModelListData } from '../aiSetting/types'
import { DefaultOptionType } from 'antd/es/select'
const AddLoadBalancingModel = forwardRef<LoadBalancingHandle>((props, ref: any) => {
const [form] = Form.useForm()
const [modelProviderLoading, setModelProviderLoading] = useState(false)
const [modelProviderData, setModelProviderData] = useState<ModelListData[]>([])
const [llmList, setLlmList] = useState<DefaultOptionType[]>()
const [modelType, setModelType] = useState<'online' | 'local'>('online')
const { message } = App.useApp()
const [llmListLoading, setLlmListLoading] = useState<boolean>(false)
const { fetchData } = useFetch()
const [modelTypeList] = useState([
{
label: $t('线上模型'),
value: 'online'
},
{
label: $t('本地模型'),
value: 'local'
}
])
/**
* llm
* @param id
*/
const getLlmList = (id?: string) => {
setLlmListLoading(true)
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[] }>>(`ai/provider/llms`, {
method: 'GET',
eoParams: { provider: id },
eoTransformKeys: ['default_llm']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setLlmList(data.llms)
form.setFieldValue('model', data.provider?.defaultLlm)
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
}
})
.finally(() => {
setLlmListLoading(false)
})
}
/**
*
* @param e
*/
const resetFormData = (e = 'online') => {
form.setFieldValue('type', e)
form.setFieldValue('model', '')
form.setFieldValue('provider', '')
setModelProviderData([])
setLlmList([])
setModelType(e as 'online' | 'local')
}
/**
*
* @param e
*/
const modelTypeChange = (e: string) => {
resetFormData(e)
if (e === 'online') {
setModelProviderLoading(true)
fetchData<ApiResponse>('simple/ai/providers/configured', {
method: 'GET',
eoTransformKeys: ['default_llm']
})
.then((response) => {
const mockApiResponse: ApiResponse = response as ApiResponse
const providers = mockApiResponse.data.providers || []
setModelProviderData(providers)
if (providers.length) {
const id = providers[0].id
form.setFieldValue('provider', id)
getLlmList(id)
}
})
.finally(() => {
setModelProviderLoading(false)
})
} else {
setLlmListLoading(true)
fetchData<LocalLlmType[]>('simple/ai/models/local/configured', {
method: 'GET'
})
.then((response) => {
const models = response.data.models || []
setLlmList(models)
if (models.length) {
const id = models[0].id
form.setFieldValue('model', id)
}
})
.finally(() => {
setLlmListLoading(false)
})
}
}
/**
*
* @param e
*/
const modelProviderChange = (e: string) => {
form.setFieldValue('modelProvider', e)
getLlmList(e)
}
useEffect(() => {
modelTypeChange('online')
}, [])
/**
*
*/
const save = () => {
return new Promise((resolve, reject) => {
form
.validateFields()
.then((values) => {
fetchData<ApiResponse>('ai/balance', {
method: 'POST',
eoBody: values
})
.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((error) => {
reject(error)
})
})
.catch((errorInfo) => {
reject(errorInfo)
})
})
}
useImperativeHandle(ref, () => ({
save
}))
return (
<Form
form={form}
layout="vertical"
labelAlign="left"
scrollToFirstError
className="flex flex-col mx-auto h-full"
name="aiServiceInsideRouterModalConfig"
autoComplete="off"
>
<Form.Item<LoadModelDetailData> label={$t('模型类型')} name="type" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
options={modelTypeList}
onChange={(e) => {
modelTypeChange(e)
}}
></Select>
</Form.Item>
{modelType === 'online' && (
<Form.Item<LoadModelDetailData> label={$t('模型供应商')} name="provider" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.select)}
loading={modelProviderLoading}
options={modelProviderData?.map((x) => ({
value: x.id,
label: (
<div className="flex items-center gap-[10px]">
<span>{x.name}</span>
</div>
)
}))}
onChange={(e) => {
modelProviderChange(e)
}}
></Select>
</Form.Item>
)}
<Form.Item label={$t('模型')} name="model" className="mt-[16px]" rules={[{ required: true }]}>
<Select
className="w-INPUT_NORMAL"
placeholder={$t(PLACEHOLDER.input)}
loading={llmListLoading}
options={
llmList?.map((x) => ({
value: x.id,
label: (
<div className="flex items-center gap-[10px]">
<span>{x.id}</span>
{ modelType === 'online' &&x?.scopes?.map((s: any) => <Tag key={s}>{s?.toLocaleUpperCase()}</Tag>)}
</div>
)
}))
}
onChange={(value) => {
form.setFieldValue('model', value)
}}
></Select>
</Form.Item>
</Form>
)
})
export default AddLoadBalancingModel
@@ -0,0 +1,303 @@
import { ActionType } from '@ant-design/pro-components'
import InsidePage from '@common/components/aoplatform/InsidePage'
import PageList, { PageProColumns } from '@common/components/aoplatform/PageList'
import WithPermission from '@common/components/aoplatform/WithPermission'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
import { useFetch } from '@common/hooks/http'
import { $t } from '@common/locales/index.ts'
import { App, Button, Typography } from 'antd'
import { useEffect, useRef, useState } from 'react'
import { LoadBalancingHandle, LoadBalancingItems } from './type'
import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission'
import AddLoadBalancingModel from './AddModel'
const LoadBalancingPage = () => {
const pageListRef = useRef<ActionType>(null)
const [searchWord, setSearchWord] = useState<string>('')
const { modal, message } = App.useApp()
const [apiKeys, setApiKeys] = useState<LoadBalancingItems[]>([])
const addModelRef = useRef<LoadBalancingHandle>()
const statusEnum: Record<string, { text: React.ReactNode }> = {
normal: { text: <Typography.Text type="success">{$t('正常')}</Typography.Text> },
abnormal: { text: <Typography.Text type="danger">{$t('异常')}</Typography.Text> }
}
/**
*
*/
const { fetchData } = useFetch()
const addModel = () => {
modal.confirm({
title: $t('添加负载均衡'),
content: <AddLoadBalancingModel ref={addModelRef} />,
width: 600,
closable: true,
onOk: () => {
return addModelRef.current?.save().then((res) => {
if (res === true) {
pageListRef.current?.reload()
}
})
},
wrapClassName: 'ant-modal-without-footer',
okText: $t('确认'),
cancelText: $t('取消'),
icon: <></>
})
}
/**
*
* @param dataType
* @returns
*/
const requestApis = () => {
return fetchData<BasicResponse<{ list: LoadBalancingItems[]; total: number }>>(`ai/balances`, {
method: 'GET',
eoParams: {
keyword: searchWord
},
eoTransformKeys: ['api_count', 'key_count']
})
.then((response) => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
setApiKeys(response.data.list)
// 保存数据
return {
data: data.list,
total: data.total,
success: true
}
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
return { data: [], success: false }
}
})
.catch(() => {
return { data: [], success: false }
})
}
/**
*
* @param beforeIndex
* @param afterIndex
* @param newDataSource
*/
const handleDragSortEnd = async (beforeIndex: number, afterIndex: number, newDataSource: LoadBalancingItems[]) => {
try {
let targetId
let sortDirection
// Check if there's an item before afterIndex
if (afterIndex > 0) {
targetId = newDataSource[afterIndex - 1].id
sortDirection = 'after'
} else if (afterIndex < newDataSource.length - 1) {
// If no item before, use the item after
targetId = newDataSource[afterIndex + 1].id
sortDirection = 'before'
}
const response = await fetchData<BasicResponse<any>>('ai/balance/sort', {
method: 'PUT',
eoBody: {
origin: apiKeys[beforeIndex].id,
target: targetId,
sort: sortDirection
}
})
if (response.code === STATUS_CODE.SUCCESS) {
message.success($t('排序成功'))
pageListRef.current?.reload()
} else {
message.error(response.msg || RESPONSE_TIPS.error)
// Revert the UI if API call fails
pageListRef.current?.reload()
}
} catch (error) {
message.error(RESPONSE_TIPS.error)
// Revert the UI if API call fails
pageListRef.current?.reload()
}
}
/**
*
* @param id
*/
const handleDelete = (id: string) => {
fetchData<BasicResponse<null>>('ai/balance', {
method: 'DELETE',
eoParams: {
id
}
})
.then((response) => {
const { code } = response
if (code === STATUS_CODE.SUCCESS) {
message.success($t('删除成功'))
pageListRef.current?.reload()
} else {
message.error(RESPONSE_TIPS.error)
}
})
.catch((error) => {
message.error(RESPONSE_TIPS.error)
})
}
/**
*
*/
const columns: PageProColumns<LoadBalancingItems>[] = [
{
title: '',
dataIndex: 'drag',
width: '40px'
},
{
title: $t('优先级'),
dataIndex: 'priority',
width: 80,
ellipsis: true,
key: 'priority'
},
{
title: $t('模型'),
dataIndex: ['provider', 'name'],
ellipsis: true,
key: 'provider',
render: (dom: React.ReactNode, record: LoadBalancingItems) => (
<span>
{record.provider?.name} / {record.model?.name}
</span>
)
},
{
title: $t('类型'),
dataIndex: 'type',
width: 120,
ellipsis: true,
key: 'type',
render: (dom: React.ReactNode, record: LoadBalancingItems) => (
<span>{record.type === 'online' ? $t('线上模型') : $t('本地模型')}</span>
)
},
{
title: $t('状态'),
dataIndex: 'state',
width: 80,
ellipsis: true,
key: 'state',
render: (dom: React.ReactNode, record: LoadBalancingItems) => (
<span>{statusEnum[record.state]?.text || '-'}</span>
)
},
{
title: $t('Apis'),
dataIndex: 'apiCount',
ellipsis: true,
width: 80,
key: 'apiCount',
render: (dom: React.ReactNode, record: LoadBalancingItems) => (
<span className="[&>.key-link]:text-[#2196f3] cursor-pointer">
<a
href={`/aiApis?modelId=${record.provider?.id}`}
target="_blank"
className="key-link"
style={{
fontWeight: 500,
cursor: 'pointer',
pointerEvents: 'all',
textDecoration: 'none'
}}
>
{record.apiCount || '0'}
</a>
</span>
)
},
{
title: $t('Keys'),
dataIndex: 'keyCount',
ellipsis: true,
width: 80,
key: 'keyCount',
render: (dom: React.ReactNode, record: LoadBalancingItems) => (
<span className="[&>.key-link]:text-[#2196f3] cursor-pointer">
<a
href={`/keysetting?modelId=${record.provider?.id}`}
target="_blank"
className="key-link"
style={{
fontWeight: 500,
cursor: 'pointer',
pointerEvents: 'all',
textDecoration: 'none'
}}
>
{record.keyCount || '0'}
</a>
</span>
)
},
{
title: '',
key: 'option',
btnNums: 1,
width: 50,
fixed: 'right',
valueType: 'option',
render: (_: React.ReactNode, entity: any) => [
<TableBtnWithPermission
access="system.settings.ai_balance.delete"
key="delete"
btnType="delete"
onClick={() => handleDelete(entity.id as string)}
btnTitle={$t('删除')}
/>
]
}
]
return (
<>
<InsidePage
pageTitle={$t('负载均衡')}
description={$t(
'系统自动识别异常AI模型后,自动替换成以下优先级最高的可用模型。这将确保您的AI应用保持高可用性和最佳性能,从而防止任何单个LLM异常成为您的性能瓶颈。'
)}
showBorder={false}
scrollPage={false}
>
<div className="h-[calc(100%-1rem-36px)] pr-PAGE_INSIDE_X">
<PageList
ref={pageListRef}
rowKey="id"
afterNewBtn={[
<WithPermission key="removeFromDepPermission" access="system.settings.ai_balance.add">
<Button className="mr-btnbase" type="primary" key="removeFromDep" onClick={() => addModel()}>
{$t('添加模型')}
</Button>
</WithPermission>
]}
request={() => requestApis()}
onSearchWordChange={(e) => {
setSearchWord(e.target.value)
}}
showPagination={true}
dragSortKey="drag"
onDragSortEnd={handleDragSortEnd}
searchPlaceholder={$t('请输入...')}
columns={columns}
/>
</div>
</InsidePage>
</>
)
}
export default LoadBalancingPage
@@ -0,0 +1,16 @@
import { useEffect } from 'react'
import { Outlet, useLocation, useNavigate } from 'react-router-dom'
export default function LoadBalancingLayout() {
const location = useLocation()
const pathName = location.pathname
const navigator = useNavigate()
useEffect(() => {
if (pathName === '/loadBalancing') {
const queryParams = new URLSearchParams(location.search).toString()
navigator(`/loadBalancing/list${queryParams ? `?${queryParams}` : ''}`)
}
}, [pathName])
return <Outlet></Outlet>
}
@@ -0,0 +1,31 @@
export interface LoadBalancingItems {
id: string
priority: string
provider: {
id: string
name: string
}
model: {
id: string
name: string
}
type: string
state: string
apiCount: string
keyCount: string
}
export interface LoadModelDetailData {
type: string
provider: string
model: string
}
export interface LocalLlmType {
id: string
name: string
defaultConfig: string
}
export type LoadBalancingHandle = {
save: () => Promise<boolean | string>
}
@@ -1,7 +1,5 @@
'use client'
import AIFlowChart from '../aiSetting/AIFlowChart'
export default function Playground() {
return <AIFlowChart />
return <iframe src="/playground" />
}
@@ -74,9 +74,10 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
const getProviderOptionList = () => {
setProviderOptionList([])
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers', {
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers/configured', {
method: 'GET',
eoTransformKeys: []
eoTransformKeys: [],
eoParams: { all: true}
}).then(response => {
const { code, data, msg } = response
if (code === STATUS_CODE.SUCCESS) {
@@ -428,7 +429,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
label={$t('API 调用前缀')}
name="prefix"
extra={$t(
'作为服务内所有API的前缀,比如host/{service_name}/{api_path}一旦保存无法修改'
'作为服务内所有API的前缀,比如host/{service_name}/{api_path}影响较大,谨慎修改'
)}
rules={[
{ required: true, whitespace: true },
@@ -440,7 +441,6 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
<Input
prefix={onEdit ? '' : '/'}
className="w-INPUT_NORMAL"
disabled={onEdit}
placeholder={$t(PLACEHOLDER.input)}
/>
</Form.Item>
@@ -14,6 +14,8 @@ import { useNavigate } from 'react-router-dom'
import { SERVICE_KIND_OPTIONS, SYSTEM_TABLE_COLUMNS } from '../../const/system/const.tsx'
import { SystemConfigHandle, SystemTableListItem } from '../../const/system/type.ts'
import SystemConfig from './SystemConfig.tsx'
import { ServiceDeployment } from './serviceDeployment/ServiceDeployment.tsx'
import { LogsFooter } from './serviceDeployment/ServiceDeployMentFooter.tsx'
const SystemList: FC = () => {
const navigate = useNavigate()
@@ -23,13 +25,19 @@ const SystemList: FC = () => {
const { fetchData } = useFetch()
const [tableListDataSource, setTableListDataSource] = useState<SystemTableListItem[]>([])
const [tableHttpReload, setTableHttpReload] = useState(true)
const { message } = App.useApp()
const { message, modal } = App.useApp()
const pageListRef = useRef<ActionType>(null)
const [memberValueEnum, setMemberValueEnum] = useState<{ [k: string]: { text: string } }>({})
const [open, setOpen] = useState(false)
const drawerFormRef = useRef<SystemConfigHandle>(null)
const { checkPermission, accessInit, getGlobalAccessData, state } = useGlobalContext()
const [stateColumnMap] = useState<{ [k: string]: { text: string; className?: string } }>({
normal: { text: '正常' },
deploying: { text: '部署中', className: 'text-[#2196f3]' },
error: { text: '异常', className: 'text-[#ff4d4f]' },
public: { text: '公共服务' },
private: { text: '私有服务' }
})
const getSystemList = () => {
if (!accessInit) {
getGlobalAccessData()?.then?.(() => {
@@ -128,7 +136,24 @@ const SystemList: FC = () => {
const onClose = () => {
setOpen(false)
}
const openLogsModal = (record: any) => {
const closeModal = (reload = true) => {
modalInstance.destroy()
reload && manualReloadTable()
}
const modalInstance = modal.confirm({
title: $t('部署过程'),
content: <ServiceDeployment record={record} closeModal={closeModal} />,
footer: () => {
return <LogsFooter record={record} closeModal={closeModal} />
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const columns = useMemo(() => {
const res = SYSTEM_TABLE_COLUMNS.map((x) => {
const dataIndex = x.dataIndex as string[]
@@ -145,6 +170,21 @@ const SystemList: FC = () => {
;(x.valueEnum as any)[option.value] = { text: $t(option.label) }
})
}
if ((x.dataIndex as string) === 'state') {
x.render = (dom: React.ReactNode, record: any) => (
<span
className={`text-[13px] ${stateColumnMap[record.state]?.className}`}
onClick={(e) => {
if (['deploying', 'error'].includes(record.state)) {
e?.stopPropagation()
openLogsModal(record)
}
}}
>
{$t(stateColumnMap[record.state]?.text || '-')}
</span>
)
}
return { ...x, title: typeof x.title === 'string' ? $t(x.title as string) : x.title }
})
@@ -0,0 +1,87 @@
import { App, Button } from 'antd'
import { $t } from '@common/locales/index.ts'
import { useFetch } from '@common/hooks/http.ts'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
export const LogsFooter = (props: any) => {
const { record, closeModal = () => {} } = props
const { message, modal } = App.useApp()
const { fetchData } = useFetch()
const stopDeploy = () => {
modal.confirm({
title: $t('停止部署'),
content: $t('确定停止部署吗?'),
onOk: () => {
return new Promise((resolve, reject) => {
fetchData<BasicResponse<any>>('model/local/cancel_deploy', {
method: 'POST',
eoBody: { model: record.id }
})
.then((response) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
resolve(true)
closeModal()
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(false)
}
})
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
const deleteService = () => {
modal.confirm({
title: $t('删除服务'),
content: $t('确定删除服务吗?'),
onOk: () => {
return new Promise((resolve, reject) => {
fetchData<BasicResponse<any>>('model/local', {
method: 'DELETE',
eoParams: { model: record.id }
})
.then((response: BasicResponse<any>) => {
const { code, msg } = response
if (code === STATUS_CODE.SUCCESS) {
resolve(true)
closeModal()
} else {
message.error(msg || $t(RESPONSE_TIPS.error))
reject(false)
}
})
})
},
width: 600,
okText: $t('确认'),
cancelText: $t('取消'),
closable: true,
icon: <></>
})
}
return (
<>
{['deploying_error', 'error'].includes(record.state) ? (
<div className="flex justify-end items-center">
<Button onClick={() => { closeModal(true) }}>{$t('取消')}</Button>
<Button onClick={deleteService} type="primary" danger>
{$t('删除服务')}
</Button>
</div>
) : (
<div className="flex justify-end items-center">
<Button onClick={stopDeploy} type="primary" danger>
{$t('停止')}
</Button>
<Button type="primary" onClick={() => { closeModal() }}>{$t('继续等待')}</Button>
</div>
)}
</>
)
}
@@ -0,0 +1,202 @@
import { SystemTableListItem } from '@core/const/system/type'
import { App, Steps } from 'antd'
import { CheckCircleOutlined, LoadingOutlined, ClockCircleOutlined, CloseCircleOutlined } from '@ant-design/icons'
import { Codebox } from '@common/components/postcat/api/Codebox'
import { Collapse } from 'antd'
import { useEffect, useRef, useState } from 'react'
import { $t } from '@common/locales/index.ts'
import { useFetch } from '@common/hooks/http'
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
export const ServiceDeployment = (props: { record: SystemTableListItem, closeModal?: () => void }) => {
const { record, closeModal } = props
const { message } = App.useApp()
const getIcon = (status: string) => {
switch (status) {
case 'completed':
return <CheckCircleOutlined style={{ color: 'green', fontSize: '40px' }} />
case 'inProgress':
return <LoadingOutlined style={{ color: '#2196f3', fontSize: '40px' }} />
case 'pending':
return <ClockCircleOutlined style={{ color: 'gray', fontSize: '40px' }} />
case 'error':
return <CloseCircleOutlined style={{ color: 'red', fontSize: '40px' }} />
default:
return null
}
}
const [stepItem, setStepItem] = useState<
{
id: string
title: string
description?: string
status?: string
}[]
>([
{
id: 'download',
title: $t('下载'),
status: 'pending'
},
{
id: 'deploy',
title: $t('部署'),
status: 'pending'
},
{
id: 'initializing',
title: $t('初始化'),
status: 'pending'
}
])
const [scriptStr, setScriptStr] = useState('')
const step = useRef(0)
const [collapseText] = useState('Progress log')
const { fetchData } = useFetch()
/**
*
* @param currentState
* @returns
*/
const getCurrentStep = (currentState?: string) => {
switch (currentState) {
case 'download':
case 'download_error':
return 0
case 'deploy':
case 'deploy_error':
return 1
case 'initializing':
case 'initializing_error':
return 2
default:
return 0
}
}
/**
*
* @param targetStep
* @param description
* @param currentState
*/
const updateStepItems = (targetStep: number, description = '', currentState?: string) => {
setStepItem((prevItems) =>
prevItems.map((item, index) => ({
...item,
description: item.id === 'download' ? description : item.description,
status: index < targetStep ? 'completed' : index === targetStep ? currentState && currentState.includes('error') ? 'error' : 'inProgress' : 'pending',
}))
);
step.current = targetStep;
};
/**
*
* @returns
*/
const getLocalModelState = () => {
fetchData<BasicResponse<any>>('model/local/state', {
method: 'GET',
eoParams: {
model: record.id
}
})
.then((response) => {
if (response.code === STATUS_CODE.SUCCESS) {
updateStepItems(getCurrentStep(response.data?.state), `${response.data?.info?.current} / ${response.data?.info?.total}`, response.data?.state)
setScriptStr(response?.data?.info?.last_message || '')
} else {
message.error(response.msg || RESPONSE_TIPS.error)
}
})
.catch((error) => {
message.error(RESPONSE_TIPS.error)
})
}
useEffect(() => {
if (['deploying_error', 'error'].includes(record.state)) {
getLocalModelState()
} else {
fetchData(
'model/local/deploy',
{
method: 'POST',
eoBody: { model: record.id, team: record.team?.id },
isStream: true,
handleStream: (chunk) => {
const parsedChunk = JSON.parse(chunk)
// 下载中
if (parsedChunk?.data?.state.includes('download')) {
updateStepItems(0, `${parsedChunk?.data?.info?.current} / ${parsedChunk?.data?.info?.total}`);
// 部署中
} else if (parsedChunk?.data?.state.includes('deploy')) {
updateStepItems(1);
// 初始化中
} else if (parsedChunk?.data?.state.includes('initializing')) {
updateStepItems(2);
// 完成
} else if (parsedChunk?.data?.state.includes('finish')) {
updateStepItems(4);
setTimeout(() => {
closeModal?.()
}, 200)
} else if (parsedChunk?.data?.state.includes('error')) {
setStepItem((prevItems) =>
prevItems.map((item, index) => {
return { ...item, status: index === step.current ? 'error' : item.status }
})
)
}
setScriptStr(parsedChunk?.data?.message || '')
}
}
)
}
}, [])
return (
<>
<div className="flex justify-center items-center mb-[20px] mt-[20px] custom-steps">
<Steps labelPlacement="vertical">
{stepItem.map((item, index) => (
<Steps.Step
key={index}
title={item.title}
icon={getIcon(item.status || '')}
description={item.description}
/>
))}
</Steps>
</div>
<Collapse
expandIconPosition="end"
defaultActiveKey={['1']}
className="[&_.ant-collapse-content-box]:p-[0px]"
items={[
{
label: collapseText,
key: '1',
children: (
<Codebox
editorTheme="vs-dark"
readOnly={true}
autoScrollToEnd={true}
options={{
wordWrap: 'off'
}}
width="100%"
value={scriptStr}
height="200px"
language="json"
enableToolbar={false}
/>
)
}
]}
></Collapse>
</>
)
}
+11 -18
View File
@@ -535,15 +535,15 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string,
Priority: input.Priority,
Status: &status,
}
_, err = i.aiKeyService.DefaultKey(ctx, id)
_, err = i.aiKeyService.DefaultKey(txCtx, id)
if err != nil {
if !errors.Is(err, gorm.ErrRecordNotFound) {
return err
}
err = i.aiKeyService.Create(ctx, &ai_key.Create{
err = i.aiKeyService.Create(txCtx, &ai_key.Create{
ID: id,
Name: info.Name,
Config: info.Config,
Config: input.Config,
Provider: id,
Status: 1,
ExpireTime: 0,
@@ -551,28 +551,21 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string,
Priority: 1,
})
} else {
err = i.aiKeyService.Save(ctx, id, &ai_key.Edit{
Config: &info.Config,
err = i.aiKeyService.Save(txCtx, id, &ai_key.Edit{
Config: &input.Config,
Status: &status,
})
}
if err != nil {
return err
}
//if input.Enable != nil {
// status = 0
// if *input.Enable {
// status = 1
// }
// pInfo.Status = &status
//}
err = i.providerService.Save(ctx, id, pInfo)
err = i.providerService.Save(txCtx, id, pInfo)
if err != nil {
return err
}
if *pInfo.Status == 0 {
return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
return i.syncGateway(txCtx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
{
BasicItem: &gateway.BasicItem{
ID: id,
@@ -581,8 +574,8 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string,
},
}, false)
}
// 获取当前供应商所有Key信息
defaultKey, err := i.aiKeyService.DefaultKey(ctx, id)
// 获取当前供应商默认Key信息
defaultKey, err := i.aiKeyService.DefaultKey(txCtx, id)
if err != nil {
return err
}
@@ -592,7 +585,7 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string,
cfg["model_config"] = model.DefaultConfig()
cfg["priority"] = info.Priority
cfg["base"] = fmt.Sprintf("%s://%s", p.URI().Scheme(), p.URI().Host())
return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
return i.syncGateway(txCtx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
{
BasicItem: &gateway.BasicItem{
ID: id,
+4 -2
View File
@@ -100,7 +100,8 @@ func (i *imlAuthorizationModule) getApplications(ctx context.Context, appIds []s
Config: authCfg,
HideCredential: a.HideCredential,
Label: map[string]string{
"authorization": a.UUID,
"authorization": a.UUID,
"authorization_name": a.Name,
},
}
}),
@@ -157,7 +158,8 @@ func (i *imlAuthorizationModule) online(ctx context.Context, s *service.Service)
Config: authCfg,
HideCredential: a.HideCredential,
Label: map[string]string{
"authorization": a.UUID,
"authorization": a.UUID,
"authorization_name": a.Name,
},
}
}),
+1 -1
View File
@@ -211,7 +211,7 @@ APIParkはApache 2.0ライセンスの下で提供されています。詳細に
エンタープライズ機能や専門的な技術サポートについては、プリセールスの専門家に連絡し、個別デモ、カスタムソリューション、価格情報を入手してください。
- ウェブサイト: https://apipark.com
- メール: dev@apipark.com
- メール: contact@apipark.com
<br>
+1 -1
View File
@@ -215,7 +215,7 @@ APIPark 使用 Apache 2.0 许可证。更多详情请查看 LICENSE 文件。
对于企业级功能和专业技术支持,请联系售前专家进行个性化演示、定制方案和获取报价。
- 网站: https://apipark.com
- 电子邮件: dev@apipark.com
- 电子邮件: contact@apipark.com
<br>
+1 -1
View File
@@ -212,7 +212,7 @@ APIPark 使用 Apache 2.0 授權條款。更多詳情請參閱 LICENSE 文件。
如需企業級功能與專業技術支援,請聯絡我們的售前專家,獲取個性化演示、定制方案和報價。
- 網站: https://apipark.com
- 電子郵件: dev@apipark.com
- 電子郵件: contact@apipark.com
<br>