diff --git a/.github/workflows/release.yaml b/.github/workflows/release.yaml index 49b80caa..aa645b89 100644 --- a/.github/workflows/release.yaml +++ b/.github/workflows/release.yaml @@ -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 diff --git a/README.md b/README.md index 6c52c63a..4ab6e887 100644 --- a/README.md +++ b/README.md @@ -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
diff --git a/frontend/packages/common/src/assets/localAI.svg b/frontend/packages/common/src/assets/localAI.svg new file mode 100644 index 00000000..ea2ac931 --- /dev/null +++ b/frontend/packages/common/src/assets/localAI.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/packages/common/src/assets/onlineAI.svg b/frontend/packages/common/src/assets/onlineAI.svg new file mode 100644 index 00000000..008a606f --- /dev/null +++ b/frontend/packages/common/src/assets/onlineAI.svg @@ -0,0 +1,3 @@ + + + diff --git a/frontend/packages/common/src/assets/restAPI.svg b/frontend/packages/common/src/assets/restAPI.svg new file mode 100644 index 00000000..680b98f3 --- /dev/null +++ b/frontend/packages/common/src/assets/restAPI.svg @@ -0,0 +1,14 @@ + + + + + + REST + diff --git a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx index 931bf7f0..19767e95 100644 --- a/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx +++ b/frontend/packages/common/src/components/aoplatform/BasicLayout.tsx @@ -116,6 +116,10 @@ function BasicLayout({ project = 'core' }: { project: string }) { getGlobalAccessData() }, []) + useEffect(() => { + setPathname(location.pathname) + }, [location.pathname]) + const logOut = () => { fetchData>('account/logout', { method: 'GET' }).then((response) => { const { code, msg } = response @@ -182,7 +186,7 @@ function BasicLayout({ project = 'core' }: { project: string }) { , ...((pluginSlotHub.getSlot('basicLayoutAfterBtns') as unknown[]) || []) ] - }, [pluginSlotHub.getSlot('basicLayoutAfterBtns')]) + }, [state.language, pluginSlotHub.getSlot('basicLayoutAfterBtns')]) return (
- 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(``) @@ -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 => { diff --git a/frontend/packages/common/src/const/permissions.ts b/frontend/packages/common/src/const/permissions.ts index dbe2786e..132f9641 100644 --- a/frontend/packages/common/src/const/permissions.ts +++ b/frontend/packages/common/src/const/permissions.ts @@ -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'] }] diff --git a/frontend/packages/common/src/const/type.ts b/frontend/packages/common/src/const/type.ts index 96f29cb5..47044c70 100644 --- a/frontend/packages/common/src/const/type.ts +++ b/frontend/packages/common/src/const/type.ts @@ -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 diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx index 31fbb1d4..a1cfcd29 100644 --- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -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' } ] }, diff --git a/frontend/packages/common/src/hooks/http.ts b/frontend/packages/common/src/hooks/http.ts index 6688fdad..92eb99a0 100644 --- a/frontend/packages/common/src/hooks/http.ts +++ b/frontend/packages/common/src/hooks/http.ts @@ -134,6 +134,8 @@ type EoRequest = RequestInit & { eoTransformKeys?: string[] eoApiPrefix?: string eoBody?: { [k: string]: unknown } | Array | 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) => { // 全局错误处理 diff --git a/frontend/packages/common/src/hooks/pluginLoader.ts b/frontend/packages/common/src/hooks/pluginLoader.ts index a33eb8e4..84f06bc4 100644 --- a/frontend/packages/common/src/hooks/pluginLoader.ts +++ b/frontend/packages/common/src/hooks/pluginLoader.ts @@ -219,6 +219,16 @@ const mockData = { type: 'normal' } ] + }, + { + driver: 'apipark.builtIn.component', + name: 'loadBalancing', + router: [ + { + path: 'loadBalancing', + type: 'normal' + } + ] } // { // "driver": "apipark.remote.normal", diff --git a/frontend/packages/common/src/locales/scan/en-US.json b/frontend/packages/common/src/locales/scan/en-US.json index c70caa9c..5834a6fc 100644 --- a/frontend/packages/common/src/locales/scan/en-US.json +++ b/frontend/packages/common/src/locales/scan/en-US.json @@ -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" } diff --git a/frontend/packages/common/src/locales/scan/ja-JP.json b/frontend/packages/common/src/locales/scan/ja-JP.json index efe1d341..b7f43805 100644 --- a/frontend/packages/common/src/locales/scan/ja-JP.json +++ b/frontend/packages/common/src/locales/scan/ja-JP.json @@ -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} のように、大きな影響を与えるため、慎重に変更してください。" } diff --git a/frontend/packages/common/src/locales/scan/zh-CN.json b/frontend/packages/common/src/locales/scan/zh-CN.json index 5b9563b7..7a6129dc 100644 --- a/frontend/packages/common/src/locales/scan/zh-CN.json +++ b/frontend/packages/common/src/locales/scan/zh-CN.json @@ -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},影响较大,谨慎修改" } diff --git a/frontend/packages/common/src/locales/scan/zh-TW.json b/frontend/packages/common/src/locales/scan/zh-TW.json index ca25b4e6..fb6af1cd 100644 --- a/frontend/packages/common/src/locales/scan/zh-TW.json +++ b/frontend/packages/common/src/locales/scan/zh-TW.json @@ -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},這會產生較大的影響,請謹慎修改" } diff --git a/frontend/packages/core/src/components/AIProviderSelect/index.tsx b/frontend/packages/core/src/components/AIProviderSelect/index.tsx index 84e5ce60..5b9e28f4 100644 --- a/frontend/packages/core/src/components/AIProviderSelect/index.tsx +++ b/frontend/packages/core/src/components/AIProviderSelect/index.tsx @@ -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 = ({ value, onChange, style = { width: 200 } }) => { +const AIProviderSelect: React.FC = ({ value, onChange, source = 'ai', style = { width: 200 } }) => { const { t } = useTranslation() const [providers, setProviders] = useState([]) const [loading, setLoading] = useState(false) @@ -40,7 +41,7 @@ const AIProviderSelect: React.FC = ({ value, onChange, st if (isMounted) setLoading(true) try { const endpoint = 'simple/ai/providers/configured' - const response = await fetchData(endpoint, { method: 'GET' }) + const response = await fetchData(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) => ({ diff --git a/frontend/packages/core/src/const/ai-service/type.ts b/frontend/packages/core/src/const/ai-service/type.ts index fc788ae5..27062c72 100644 --- a/frontend/packages/core/src/const/ai-service/type.ts +++ b/frontend/packages/core/src/const/ai-service/type.ts @@ -19,6 +19,7 @@ export type AiServiceConfigFieldType = { serviceType?:'public'|'inner'; catalogue?:string | string[]; approvalType?:string; + providerType?:string }; export type AiServiceSubServiceTableListItem = { diff --git a/frontend/packages/core/src/const/const.tsx b/frontend/packages/core/src/const/const.tsx index 79b6452d..1ee3f57b 100644 --- a/frontend/packages/core/src/const/const.tsx +++ b/frontend/packages/core/src/const/const.tsx @@ -799,5 +799,20 @@ export const routerMap: Map = 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' + } + ] + } ] ]) diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index 986f6590..bfd95cf1 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -108,6 +108,12 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns[] = [ dataIndex: ['team', 'name'], ellipsis: true }, + { + title: '状态', + width: 140, + dataIndex: 'state', + ellipsis: true + }, { title: 'API 数量', dataIndex: 'apiNum', diff --git a/frontend/packages/core/src/const/system/type.ts b/frontend/packages/core/src/const/system/type.ts index c4e7742c..abb0ab97 100644 --- a/frontend/packages/core/src/const/system/type.ts +++ b/frontend/packages/core/src/const/system/type.ts @@ -11,6 +11,7 @@ export type SystemTableListItem = { serviceNum: number, description:string; master:EntityItem; + state: string service_kind:'ai'|'rest', createTime:string; }; diff --git a/frontend/packages/core/src/index.css b/frontend/packages/core/src/index.css index 38b3f917..c912b574 100644 --- a/frontend/packages/core/src/index.css +++ b/frontend/packages/core/src/index.css @@ -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{ diff --git a/frontend/packages/core/src/pages/aiApis/index.tsx b/frontend/packages/core/src/pages/aiApis/index.tsx index c5cadf06..04db39bc 100644 --- a/frontend/packages/core/src/pages/aiApis/index.tsx +++ b/frontend/packages/core/src/pages/aiApis/index.tsx @@ -255,6 +255,7 @@ const ApiSettings: React.FC = () => {
{ setSelectedProvider(value) setProvider(option) diff --git a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx index 16178602..c438539f 100644 --- a/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx +++ b/frontend/packages/core/src/pages/aiService/AiServiceInsidePage.tsx @@ -31,7 +31,8 @@ const AiServiceInsidePage: FC = () => { const getAiServiceInfo = () => { fetchData>('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) { diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx index 489ed981..7a063f9b 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx @@ -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>('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>('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>('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>('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 | 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 } diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx index 415a99f6..006732de 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx @@ -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((props, ref)=>{ - const [form] = Form.useForm(); - const {entity} = props - const [providerList, setProviderList]= useState([]) - const [llmList, setLlmList]= useState([]) - const {fetchData} = useFetch() - useImperativeHandle(ref, ()=>({ - save:form.validateFields - }) - ) +const AiServiceRouterModelConfig = forwardRef( + (props, ref) => { + const [form] = Form.useForm() + const { entity } = props + const [providerList, setProviderList] = useState([]) + const [llmList, setLlmList] = useState([]) + 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('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>('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>('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>('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>('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 ( -
- - label={$t("模型供应商")} - name="provider" - rules={[{ required: true }]} - > - - + + label={$t('模型类型')} name="type" rules={[{ required: true }]}> + + + {modelType === 'online' && ( + + label={$t('模型供应商')} + name="provider" + rules={[{ required: true }]} + > + + + )} - - label={$t("模型")} - name="id" - rules={[{ required: true }]} - > - - + label={$t('模型')} name="id" rules={[{ required: true }]}> + + - - label={$t("参数")} - name="config" - > - - - + label={$t('参数')} name="config"> + + + ) -}) + } +) -export default AiServiceRouterModelConfig \ No newline at end of file +export default AiServiceRouterModelConfig diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx deleted file mode 100644 index dfd90d5c..00000000 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ /dev/null @@ -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 - ) -} - -const nodeTypes: NodeTypes = { - modelCard: ModelCardNode, - keyCard: KeyStatusNode, - serviceCard: ServiceCardNode -} as const - -const edgeTypes: EdgeTypes = { - custom: CustomEdge -} - -const AIFlowChart = () => { - const [modelData, setModelData] = useState([]) - const [loading, setLoading] = useState(false) - const [nodes, setNodes, onNodesChange] = useNodesState([]) - const [edges, setEdges, onEdgesChange] = useEdgesState([]) - const { fetchData } = useFetch() - const { aiConfigFlushed } = useGlobalContext() - const navigate = useNavigate() - - useEffect(() => { - setLoading(true) - fetchData('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) => { - 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 ( -
- {loading ? ( -
- -
- ) : modelData.length === 0 ? ( - -
{$t('未配置 AI 模型')}
- -
- ) : ( - - )} -
- ) -} - -export default AIFlowChart diff --git a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx deleted file mode 100644 index ba1ae334..00000000 --- a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx +++ /dev/null @@ -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 ( - -
- - {provider.name} -
- - {provider.configured ? $t('已配置') : $t('未配置')} - -
- } - 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' }} - > -
-
- {provider.configured && ( - <> - - {provider.defaultLlm} - - )} -
- - - -
- - ) -}) -const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => { - return ( - <> - {modelList.length > 0 ? ( -
- {modelList.map((provider: AiSettingListItem) => ( - - ))} -
- ) : ( - - )} - - ) -} - -const AIUnConfigure = () => { - const [modelData, setModelData] = useState([]) - const { fetchData } = useFetch() - const [loading, setLoading] = useState(false) - const { aiConfigFlushed } = useGlobalContext() - - useEffect(() => { - setLoading(true) - fetchData[] }>>(`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 ( - } - spinning={loading} - > - {modelData && modelData.length > 0 ? ( -
- {modelData.filter((item) => !item.configured).length > 0 && ( - <> - !item.configured) || []} /> - - )} -
- ) : ( - - )} -
- ) -} -export default AIUnConfigure diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx index aa7034d2..0fbccad4 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -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: (
- +
) }, { key: 'config', - label: $t('未设置'), - children: ( -
- -
- ) + label: $t('本地模型'), + children:
+ +
} ]} /> diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index a81a4c7d..9aa3e430 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -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 + deployAIServer: () => Promise } const AiSettingModalContent = forwardRef((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() const [loading, setLoading] = useState(false) - const [enableState, setEnableState] = useState(entity.status === 'enabled') - const getLlmList = () => { + // AI 模型配置 + const [localEntity, setLocalEntity] = useState(entity) + const [teamList, setTeamList] = useState([]) + // AI 模型提供商列表 + const modelProviderListRef = useRef([]) + // 模型模式加载 + const [modelModeLoading, setModelModeLoading] = useState(false) + const [enableState, setEnableState] = useState(localEntity?.status === 'enabled') + const { checkPermission } = useGlobalContext() + + /** + * 获取 llm 列表 + * @param id + */ + const getLlmList = (id?: string) => { setLoading(true) fetchData>(`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 { - getLlmList() + /** + * 获取团队选项列表 + * @returns + */ + const getTeamOptionList = async (): any[] => { + const response = await fetchData>( + !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>(`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>(`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 = () => { + /** + * 部署 AI 服务 + */ + const deployAIServer: () => Promise = () => { 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>('ai/provider/config', { - method: 'PUT', - eoParams: { provider: entity?.id }, - eoBody: finalValue, - eoTransformKeys: ['defaultLlm'] - // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' + fetchData>('quick/service/ai', { + method: 'POST', + eoBody: finalValue }) .then((response) => { const { code, msg } = response @@ -95,6 +206,45 @@ const AiSettingModalContent = forwardRef Promise = () => { + return new Promise((resolve, reject) => { + try { + form + .validateFields() + .then((value) => { + const finalValue = { + ...value + } + + fetchData>('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 ({ - save + save, + deployAIServer })) return ( @@ -117,6 +268,26 @@ const AiSettingModalContent = forwardRef + {modelMode === 'manual' && ( + label={$t('模型供应商')} name="modelMode" rules={[{ required: true }]}> + + + )} label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}> - - - label={ - - {$t('负载优先级')} - - - - - } - name="priority" - rules={[ - { required: true }, - { - validator: async (_, value) => { - if (value <= 0) { - throw new Error($t('优先级必须大于 0')) - } - return Promise.resolve() - } - } - ]} - initialValue={1} - > - - - + {source === 'guide' && ( + + + + )} label={$t('API Key(默认 Key)')} name="config"> - - {entity.configured && ( + {source !== 'guide' && (
{$t('当前调用状态:')} - {entity.status === 'enabled' && {$t('正常')}} - {entity.status === 'disabled' && {$t('停用')}} - {entity.status === 'abnormal' && {$t('异常')}} + {localEntity?.status === 'enabled' && {$t('正常')}} + {localEntity?.status === 'disabled' && {$t('停用')}} + {localEntity?.status === 'abnormal' && {$t('异常')}}
- {(entity.status === 'enabled' && !enableState) || (entity.status !== 'enabled' && enableState) ? ( + {(localEntity?.status === 'enabled' && !enableState) || (localEntity?.status !== 'enabled' && enableState) ? (
* {getTooltipText(enableState)}
) : null}
diff --git a/frontend/packages/core/src/pages/aiSetting/LocalModelList.tsx b/frontend/packages/core/src/pages/aiSetting/LocalModelList.tsx new file mode 100644 index 00000000..1ad6c662 --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/LocalModelList.tsx @@ -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 +} +type EditLocalModelModalProps = { + enable: boolean + modelID?: string +} +const EditLocalModelModal = forwardRef((props: EditLocalModelModalProps, ref) => { + const { enable, modelID } = props + const { fetchData } = useFetch() + const { message } = App.useApp() + const [form] = Form.useForm() + const [currentStatus, setCurrentStatus] = useState(enable) + + useEffect(() => { + form.setFieldsValue({ enable }) + }, []) + /** + * 保存 + * @returns + */ + const save: () => Promise = () => { + return new Promise((resolve, reject) => { + try { + form + .validateFields() + .then((value) => { + const finalValue = { + disable: !value.enable + } + + fetchData>('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 ( + +
+ +
+
+ {$t('当前调用状态:')} + {currentStatus && {$t('正常')}} + {!currentStatus && {$t('停用')}} +
+ + { + form.setFieldsValue({ enable: checked }) + setCurrentStatus(checked) + }} + /> + +
+
+
+
+ ) +}) + +const LocalModelList: React.FC = () => { + const pageListRef = useRef(null) + const { message, modal } = App.useApp() + const { fetchData } = useFetch() + const [searchWord, setSearchWord] = useState('') + const localAiDeployRef = useRef() + const EditLocalModelModalRef = useRef() + 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: , + 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: ( + { + modalInstance.destroy() + pageListRef.current?.reload() + }} + > + ), + 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>('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>('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[] = [ + { + title: '', + key: 'option', + btnNums: 4, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: ModelListData) => [ + handleEdit(entity)} + btnTitle={$t('设置')} + />, + , + 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: , + footer: () => { + return + }, + width: 600, + okText: $t('确认'), + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + const columns: PageProColumns[] = [ + { + title: $t('名称'), + dataIndex: 'name', + render: (dom: React.ReactNode, entity: ModelListData) => {entity.name} + }, + { + title: $t('状态'), + width: 140, + dataIndex: 'state', + ellipsis: true, + render: (dom: React.ReactNode, entity: ModelListData) => ( + { + if (['deploying', 'deploying_error'].includes(entity?.state as string)) { + e?.stopPropagation() + openLogsModal(entity) + } + }} + > + {stateColumnMap[entity?.state as string]?.text || '-'} + + ) + }, + { + title: $t('Apis'), + dataIndex: 'apiCount', + render: (dom: React.ReactNode, record: ModelListData) => ( + + + {record.apiCount || '0'} + + + ) + }, + ...operation + ] + + return ( + { + setSearchWord(e.target.value) + pageListRef.current?.reload() + }} + showPagination={true} + searchPlaceholder={$t('请输入名称搜索')} + columns={columns} + addNewBtnTitle={$t('部署模型')} + onAddNewBtnClick={handleAdd} + /> + ) +} + +export default LocalModelList diff --git a/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx b/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx new file mode 100644 index 00000000..7b31cea6 --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/OnlineModelList.tsx @@ -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(null) + const { message, modal } = App.useApp() + const { fetchData } = useFetch() + const [searchWord, setSearchWord] = useState('') + const [total, setTotal] = useState(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>('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>('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: {$t('正常')} }, + disabled: { text: {$t('停用')} }, + abnormal: { text: {$t('异常')} } + } + + const operation: PageProColumns[] = [ + { + title: '', + key: 'option', + btnNums: 4, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: ModelListData) => [ + handleEdit(entity)} + btnTitle={$t('设置')} + />, + , + handleDelete(entity.id as string, entity.apiCount)} + btnTitle={$t('删除')} + /> + ] + } + ] + + const columns: PageProColumns[] = [ + { + title: $t('名称'), + dataIndex: 'name', + render: (dom: React.ReactNode, entity: ModelListData) => {entity.name} + }, + { + 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) => ( + + + {record.apiCount || '0'} + + + ) + }, + { + title: $t('Keys'), + dataIndex: 'keyCount', + render: (dom: React.ReactNode, record: ModelListData) => ( + + + {record.keyCount || '0'} + + + ) + }, + ...operation + ] + + return ( + { + setSearchWord(e.target.value) + pageListRef.current?.reload() + }} + showPagination={true} + searchPlaceholder={$t('请输入名称搜索')} + columns={columns} + addNewBtnTitle={$t('添加模型')} + onAddNewBtnClick={handleAdd} + /> + ) +} + +export default OnlineModelList diff --git a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx b/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx deleted file mode 100644 index 35d585f5..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx +++ /dev/null @@ -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 ( - <> - - {label && ( - - - {label} - - - )} - - ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx deleted file mode 100644 index ef24aee6..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/KeyStatusNode.tsx +++ /dev/null @@ -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 ( -
- -
-
{title}
-
5 ? '118px' : 'auto', - maxWidth: `calc(${MAX_KEYS} * ${KEY_SIZE} + (${MAX_KEYS} - 1) * ${KEY_GAP})`, - minHeight: KEY_SIZE - }} - > - {keys.map((key) => ( -
- ))} -
-
-
- ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx deleted file mode 100644 index ede8936b..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/ModelCardNode.tsx +++ /dev/null @@ -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 ( - <> -
- - -
-
-
-
- -
- {name} - -
- - {/* Action buttons */} -
- { - openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem) - }} - /> -
-
-
- {$t('默认:')} - {defaultLlm} -
-
-
- {status !== 'enabled' && alternativeModel && ( -
- {$t('关联 API 已转用')} {alternativeModel.name}/{alternativeModel.defaultLlm} -
- )} - - ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx b/frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx deleted file mode 100644 index e2626cc2..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/NodeComponents.tsx +++ /dev/null @@ -1,3 +0,0 @@ -export { KeyStatusNode } from './KeyStatusNode' -export { ModelCardNode } from './ModelCardNode' -export { ServiceCardNode } from './ServiceCardNode' diff --git a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx deleted file mode 100644 index 176d80da..00000000 --- a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx +++ /dev/null @@ -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 = () => { - return ( -
- -
- - AI Services -
-
- ) -} diff --git a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx index f8e078bd..ce71f5a7 100644 --- a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx +++ b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx @@ -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 + openConfigModal: (entity?: AiSettingListItem, callback?: () => void) => Promise } const AiSettingContext = createContext(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() + const entityData = useRef(null) - const openConfigModal = async (entity: AiSettingListItem) => { - message.loading($t(RESPONSE_TIPS.loading)) - const { code, data, msg } = await fetchData>('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: ( ), @@ -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 - {$t('从 (0) 获取 API KEY', [data.provider.name])} + {$t('从 (0) 获取 API KEY', [entityData.current?.name])}
diff --git a/frontend/packages/core/src/pages/aiSetting/types.ts b/frontend/packages/core/src/pages/aiSetting/types.ts index 6ba2ea7a..920de61d 100644 --- a/frontend/packages/core/src/pages/aiSetting/types.ts +++ b/frontend/packages/core/src/pages/aiSetting/types.ts @@ -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[] } - - diff --git a/frontend/packages/core/src/pages/guide/AIModelGuide.tsx b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx new file mode 100644 index 00000000..75c6e679 --- /dev/null +++ b/frontend/packages/core/src/pages/guide/AIModelGuide.tsx @@ -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(null) + const navigateTo = useNavigate() + const { accessData } = useGlobalContext() + const modalRef = useRef() + const localAiDeployRef = useRef() + const restAiDeployRef = useRef() + const { deployLocalModel } = useDeployLocalModel() + + const dumpServerPage = () => { + navigateTo('/service/list') + } + + /** + * rest 服务卡片点击事件 + */ + const restCardClick = async () => { + modal.confirm({ + title: $t('添加 Rest 服务'), + content: , + 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: ( + + ), + onOk: () => { + return modalRef.current?.deployAIServer().then((res) => { + if (res === true) { + dumpServerPage() + } + }) + }, + width: 600, + okText: $t('确认'), + footer: (_, { OkBtn, CancelBtn }) => { + return ( +
+ + {$t('从 (0) 获取 API KEY', [entityData.current?.name])} + + +
+ + {checkAccess('system.devops.ai_provider.edit', accessData) ? : null} +
+
+ ) + }, + cancelText: $t('取消'), + closable: true, + icon: <> + }) + } + + /** + * 本地部署 AI 并生成 API + */ + const localModelCardClick = async () => { + const modalInstance = modal.confirm({ + title: $t('部署本地模型'), + content: { + modalInstance.destroy() + dumpServerPage() + }}>, + 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: ( + + + {$t('部署')} Deepseek-R1 + + ) + } + ] + return ( +
+ {cardList.map((item, itemIndex) => ( + + +

{item.title}

+

{item.description}

+ {item.bottomRender ? item.bottomRender : null} +
+ ))} +
+ ) +} diff --git a/frontend/packages/core/src/pages/guide/Guide.tsx b/frontend/packages/core/src/pages/guide/Guide.tsx index 9e79f2e8..deeff7fe 100644 --- a/frontend/packages/core/src/pages/guide/Guide.tsx +++ b/frontend/packages/core/src/pages/guide/Guide.tsx @@ -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(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(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 ( - - 👋 - {$t('Hello!欢迎使用 APIPark')} - -
} - description={
-

{$t("你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。")}

-

{$t("如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。")}

-
} - showBorder={false} - scrollPage={false} - contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B" - > -
- {showGuide && - -

- 🚀{`${$t('快速入门')}`}

-

{$t("我们提供了一些任务来帮你快速了解 APIPark")}

, - children: }]} - />} - {showAdvancedGuide && - -

- 🏍️{`${$t('进阶教程')}`}

-

{$t("了解 APIPark 如何更好地管理 API 和 AI")}

, - children: }]} - />} -
- - ) + ] + } + ] + 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 ( + + 👋 + {$t('Hello!欢迎使用 APIPark')} + + } + description={ +
+

+ {$t( + '你能通过 APIPark 快速在企业内部构建 API 开放门户/市场,享受极致的转发性能、API 可观测、服务治理、多租户管理、订阅审核流程等诸多好处。' + )} +

+

+ {$t('如果你喜欢我们的产品,欢迎给我们 Star 或提供产品反馈意见。')} + + {$t('点击这里')} + +   + +   + + + + + +   + +   + + {$t('点击')} +   + + + + Star + +

+
+ } + showBorder={false} + scrollPage={false} + contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B" + > + +
+ {showGuide && ( + +

+ 🚀 + {`${$t('快速入门')}`}{' '} +

+

{$t('我们提供了一些任务来帮你快速了解 APIPark')}

+
+ ), + children: + } + ]} + /> + )} + {showAdvancedGuide && ( + +

+ 🏍️ + {`${$t('进阶教程')}`}{' '} +

+

{$t('了解 APIPark 如何更好地管理 API 和 AI')}

+ + ), + children: ( + + ) + } + ]} + /> + )} + +
+ ) } -const QuickGuideContent = ({changeGuideShow,guideSections}:{changeGuideShow:Dispatch>,guideSections: { - title: string; +const QuickGuideContent = ({ + changeGuideShow, + guideSections +}: { + changeGuideShow: Dispatch> + guideSections: { + title: string items: { - title: string; - description: string; - link: string; - }[]; -}[]})=>{ - - - return (<> -
- {guideSections.map((section, index) => ( -
-

- - {section.title} -

-
-
- {section.items.map((item, itemIndex) => ( - { window.open(item.link, '_blank') }} - > - {item.description} - - ))} -
-
-
- ))} -

- -

- - -
-

-
- ) -} \ No newline at end of file + title: string + description: string + link: string + }[] + }[] +}) => { + return ( + <> +
+ {guideSections.map((section, index) => ( +
+

+ + {section.title} +

+
+
+ {section.items.map((item, itemIndex) => ( + { + window.open(item.link, '_blank') + }} + > + {item.description} + + ))} +
+
+
+ ))} +
+ +
+ + +
+
+
+ + ) +} diff --git a/frontend/packages/core/src/pages/guide/LocalAiDeploy.tsx b/frontend/packages/core/src/pages/guide/LocalAiDeploy.tsx new file mode 100644 index 00000000..64d3fc3c --- /dev/null +++ b/frontend/packages/core/src/pages/guide/LocalAiDeploy.tsx @@ -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 +} +const LocalAiDeploy = forwardRef((props: any, ref: any) => { + const { onClose } = props + const [form] = Form.useForm() + const { fetchData } = useFetch() + const [modelList, setModelList] = useState([]) + const [tagList, setTagList] = useState([]) + const [teamList, setTeamList] = useState([]) + const { deployLocalModel, getTeamOptionList } = useDeployLocalModel() + + /** + * 获取本地模型列表 + * @returns 本地模型列表 + */ + const getLocalModelList = async (keyword?: string) => { + const response = await fetchData>('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 ( + +
+ + +
+ + {$t('热点模型')} +
+
+ {modelList.length + ? modelList + .filter((item) => item.isPopular) + .map((item) => ( + { + deployPopularModel(item.id) + }} + > + {item.name}({item.size}) + + )) + : null} +
+
+ + + + + + +
+
+ ) +}) + +export default LocalAiDeploy diff --git a/frontend/packages/core/src/pages/guide/RestAIDeploy.tsx b/frontend/packages/core/src/pages/guide/RestAIDeploy.tsx new file mode 100644 index 00000000..8dfad314 --- /dev/null +++ b/frontend/packages/core/src/pages/guide/RestAIDeploy.tsx @@ -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 +} +const RestAIDeploy = forwardRef((props: any, ref: any) => { + const [form] = Form.useForm() + const { fetchData } = useFetch() + const [teamList, setTeamList] = useState([]) + 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>('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 ( + +
+ + +

+ +

+

{$t('选择 OpenAPI 文件 (.json / .yaml)')}

+
+
+ + + +
+
+ ) +}) + +export default RestAIDeploy diff --git a/frontend/packages/core/src/pages/guide/deployModelUtil.ts b/frontend/packages/core/src/pages/guide/deployModelUtil.ts new file mode 100644 index 00000000..a0477234 --- /dev/null +++ b/frontend/packages/core/src/pages/guide/deployModelUtil.ts @@ -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>( + '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>( + !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 diff --git a/frontend/packages/core/src/pages/loadBalancing/AddModel.tsx b/frontend/packages/core/src/pages/loadBalancing/AddModel.tsx new file mode 100644 index 00000000..0e4b790a --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/AddModel.tsx @@ -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((props, ref: any) => { + const [form] = Form.useForm() + const [modelProviderLoading, setModelProviderLoading] = useState(false) + const [modelProviderData, setModelProviderData] = useState([]) + const [llmList, setLlmList] = useState() + const [modelType, setModelType] = useState<'online' | 'local'>('online') + const { message } = App.useApp() + const [llmListLoading, setLlmListLoading] = useState(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>(`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('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('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('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 ( +
+ label={$t('模型类型')} name="type" rules={[{ required: true }]}> + + + {modelType === 'online' && ( + label={$t('模型供应商')} name="provider" rules={[{ required: true }]}> + + + )} + + + + + ) +}) + +export default AddLoadBalancingModel diff --git a/frontend/packages/core/src/pages/loadBalancing/index.tsx b/frontend/packages/core/src/pages/loadBalancing/index.tsx new file mode 100644 index 00000000..0ce69cf4 --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/index.tsx @@ -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(null) + const [searchWord, setSearchWord] = useState('') + const { modal, message } = App.useApp() + const [apiKeys, setApiKeys] = useState([]) + const addModelRef = useRef() + const statusEnum: Record = { + normal: { text: {$t('正常')} }, + abnormal: { text: {$t('异常')} } + } + + /** + * 请求数据 + */ + const { fetchData } = useFetch() + const addModel = () => { + modal.confirm({ + title: $t('添加负载均衡'), + content: , + 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>(`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>('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>('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[] = [ + { + 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) => ( + + {record.provider?.name} / {record.model?.name} + + ) + }, + { + title: $t('类型'), + dataIndex: 'type', + width: 120, + ellipsis: true, + key: 'type', + render: (dom: React.ReactNode, record: LoadBalancingItems) => ( + {record.type === 'online' ? $t('线上模型') : $t('本地模型')} + ) + }, + { + title: $t('状态'), + dataIndex: 'state', + width: 80, + ellipsis: true, + key: 'state', + render: (dom: React.ReactNode, record: LoadBalancingItems) => ( + {statusEnum[record.state]?.text || '-'} + ) + }, + { + title: $t('Apis'), + dataIndex: 'apiCount', + ellipsis: true, + width: 80, + key: 'apiCount', + render: (dom: React.ReactNode, record: LoadBalancingItems) => ( + + + {record.apiCount || '0'} + + + ) + }, + { + title: $t('Keys'), + dataIndex: 'keyCount', + ellipsis: true, + width: 80, + key: 'keyCount', + render: (dom: React.ReactNode, record: LoadBalancingItems) => ( + + + {record.keyCount || '0'} + + + ) + }, + { + title: '', + key: 'option', + btnNums: 1, + width: 50, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: any) => [ + handleDelete(entity.id as string)} + btnTitle={$t('删除')} + /> + ] + } + ] + + + return ( + <> + +
+ + + + ]} + request={() => requestApis()} + onSearchWordChange={(e) => { + setSearchWord(e.target.value) + }} + showPagination={true} + dragSortKey="drag" + onDragSortEnd={handleDragSortEnd} + searchPlaceholder={$t('请输入...')} + columns={columns} + /> +
+
+ + ) +} +export default LoadBalancingPage diff --git a/frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx b/frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx new file mode 100644 index 00000000..196fc934 --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx @@ -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 +} diff --git a/frontend/packages/core/src/pages/loadBalancing/type.ts b/frontend/packages/core/src/pages/loadBalancing/type.ts new file mode 100644 index 00000000..48cb7a3c --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/type.ts @@ -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 +} diff --git a/frontend/packages/core/src/pages/playground/index.tsx b/frontend/packages/core/src/pages/playground/index.tsx index 12f5ec43..12a1b689 100644 --- a/frontend/packages/core/src/pages/playground/index.tsx +++ b/frontend/packages/core/src/pages/playground/index.tsx @@ -1,7 +1,5 @@ 'use client' -import AIFlowChart from '../aiSetting/AIFlowChart' - export default function Playground() { - return + return