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