Compare commits

..

86 Commits

Author SHA1 Message Date
ningyv 4b03c56315 Merge pull request #190 from APIParkLab/feature/1.5-cx
feat:Feature/1.5
2025-02-17 13:38:22 +08:00
ningyv eaecc5c80a feat: feature/1.5-Data Integration 2025-02-17 11:30:52 +08:00
ningyv ed8c2f286c feat: feature/1.5-Data Integration 2025-02-17 11:06:23 +08:00
ningyv 7b2356f8f3 feat: feature/1.5-Data Integration 2025-02-15 18:50:38 +08:00
ningyv 6ddd2f2389 feat: feature/1.5-Data Integration 2025-02-15 18:30:21 +08:00
ningyv 4e98b09fa4 feat: feature/1.5-Data Integration 2025-02-15 18:11:36 +08:00
ningyv e786393523 feat: feature/1.5-Data Integration 2025-02-15 17:44:23 +08:00
ningyv 4a2995b533 feat: feature/1.5-Data Integration 2025-02-15 17:26:58 +08:00
ningyv 1495451901 feat: feature/1.5-Data Integration 2025-02-15 17:15:36 +08:00
ningyv d4ef5a7516 feat: feature/1.5-Data Integration 2025-02-15 10:13:10 +08:00
ningyv 044e31dd8a feat: feature/1.5-Data Integration 2025-02-14 22:23:21 +08:00
ningyv bd33dff2f3 feat: feature/1.5-Data Integration 2025-02-14 21:51:46 +08:00
ningyv edf30ac61f feat: feature/1.5-Data Integration 2025-02-14 20:56:35 +08:00
ningyv c67964045d feat: feature/1.5-Data Integration 2025-02-14 20:44:40 +08:00
ningyv 92a6f777ed feat: feature/1.5-Data Integration 2025-02-14 18:50:30 +08:00
ningyv 12ed7aafee feat: feature/1.5-Data Integration 2025-02-14 18:04:38 +08:00
ningyv 3becd8a0a7 feat: feature/1.5-Data Integration 2025-02-14 17:43:41 +08:00
ningyv 818436c946 feat: Local deployment popup optimization 2025-02-14 16:45:39 +08:00
ningyv d0813e8595 feat: Local deployment popup optimization 2025-02-14 15:39:01 +08:00
ningyv 3c0140f3b8 feat: feature/1.5-Model Deployment Process Popup Optimization 2025-02-13 18:22:58 +08:00
ningyv 8d415fa273 feat: Style and Prompt Optimization 2025-02-13 17:02:40 +08:00
ningyv f8fad4caf4 feat: Multilingual Translation 2025-02-13 09:51:43 +08:00
ningyv 3b54c03027 feat: progress log 2025-02-12 18:20:35 +08:00
ningyv a5f46a930f feat: local model list page 2025-02-12 16:45:57 +08:00
ningyv 6157a9d1fa feat: local model list page 2025-02-12 15:28:17 +08:00
ningyv f910fc84e5 feat: merge api model page 2025-02-12 11:27:47 +08:00
scarqin 9cb09905f9 feat: delete model 2025-02-12 00:33:16 +08:00
scarqin eeb2fbcad6 feat: api list 2025-02-11 19:15:11 +08:00
scarqin 400faf92c0 feat: online model settings 2025-02-11 19:01:15 +08:00
ningyv fb023a039b feat: load banancing list 2025-02-11 18:27:14 +08:00
ningyv 95b5d848f7 feat: feature/1.5 Extract home page navigation component 2025-02-11 10:36:47 +08:00
ningyv ded5e064e6 feat: Deployment Progress Popup Development 2025-02-10 17:03:28 +08:00
ningyv 7ea50ec380 feat: Home Page AI Service Deployment 2025-02-08 18:47:08 +08:00
Dot.L 901bef1463 Merge pull request #189 from APIParkLab/feature/openapi
update workflows actions/download-artifact to v4
2025-02-08 16:15:32 +08:00
Liujian 8d44d796b4 update workflows actions/download-artifact to v4 2025-02-08 16:14:01 +08:00
Dot.L 5a10ad478e Merge pull request #188 from APIParkLab/feature/openapi
update workflows actions/upload-artifact to v4
2025-02-08 15:59:53 +08:00
Liujian fd6680d615 update workflows actions/upload-artifact to v4 2025-02-08 15:57:36 +08:00
Dot.L e03cdfc42b Merge pull request #187 from APIParkLab/feature/openapi
Feature/openapi
2025-02-08 15:02:06 +08:00
Liujian 945d53fcfd Merge remote-tracking branch 'github-pro/main' into feature/openapi 2025-02-08 15:01:27 +08:00
Liujian ac7045b724 Fix: AI provider's default key synchronization to Apinto failed issue 2025-02-08 14:58:36 +08:00
Liujian c907bdc4a5 Merge remote-tracking branch 'origin/main' into feature/openapi 2025-01-23 16:54:58 +08:00
ningyv 733ed9ac2f Merge pull request #183 from APIParkLab/feature/1.4
chroe: optimize AI model node graphics
2025-01-23 14:01:10 +08:00
ningyv 1d8e579a10 Merge remote-tracking branch 'origin/main' into feature/1.4 2025-01-23 13:57:01 +08:00
lichunxian 567cac9c95 Merge branch 'feature/1.4' into 'main'
chroe: optimize AI model node graphics

See merge request apipark/APIPark!161
2025-01-21 11:52:02 +08:00
ningyv 095c09c8c0 chroe: optimize AI model node graphics 2025-01-21 11:50:58 +08:00
刘健 e9c949822d Merge branch 'feature/openapi' into 'main'
Feature/openapi

See merge request apipark/APIPark!160
2025-01-20 17:36:53 +08:00
Dot.L 3482d5416c Merge pull request #181 from APIParkLab/feature/openapi
fix:ai init bug
2025-01-20 14:08:37 +08:00
Liujian d8cb4a0c94 fix:ai init bug 2025-01-20 14:03:03 +08:00
Dot.L 59acfa7a47 Merge pull request #180 from APIParkLab/feature/openapi
Feature/openapi
2025-01-20 13:55:59 +08:00
Liujian 2eb2e690d1 update ai bug 2025-01-20 13:54:58 +08:00
刘健 f7801261c3 Merge branch 'feature/openapi' into 'main'
fix: Nsq returns no error directly after parsing JSON exceptionNsq returns no...

See merge request apipark/APIPark!159
2025-01-17 16:03:51 +08:00
Liujian 7e7be7f040 add openapi 2025-01-17 16:03:09 +08:00
Dot.L 0187fd16b2 Merge pull request #174 from jeak01/patch-2
Update readme-zh-cn.md
2025-01-17 15:55:09 +08:00
Dot.L ba0bdb5e99 Merge pull request #175 from jeak01/patch-3
Update readme-zh-tw.md
2025-01-17 15:54:50 +08:00
Dot.L 9d3e4f07bf Merge pull request #176 from jeak01/patch-4
Update readme-jp.md
2025-01-17 15:54:37 +08:00
Dot.L bd81d7584d Merge pull request #177 from jeak01/patch-1
Update README.md
2025-01-17 15:54:20 +08:00
jeak 9577339e14 Update readme-jp.md 2025-01-17 14:59:10 +08:00
jeak 5c292ef1cb Update readme-zh-tw.md 2025-01-17 14:58:46 +08:00
jeak 4f3de85068 Update readme-zh-cn.md 2025-01-17 14:58:19 +08:00
jeak 07a25c9643 Update README.md 2025-01-17 14:57:31 +08:00
Dot.L 8f60426b4c Merge pull request #173 from APIParkLab/feature/ai-balance
fix: Nsq returns no error directly after parsing JSON exceptionNsq re…
2025-01-17 11:35:43 +08:00
Liujian 37f87615bd fix: Nsq returns no error directly after parsing JSON exceptionNsq returns no error directly after parsing JSON exception 2025-01-17 11:34:34 +08:00
Dot.L 3f96de660b Merge pull request #172 from APIParkLab/feature/ai-balance
fix: ai event handler read event error
2025-01-17 10:42:14 +08:00
Liujian e86999770f fix: ai event handler read event error 2025-01-17 10:38:35 +08:00
Dot.L a8bb0c24ec Merge pull request #170 from APIParkLab/feature/ai-balance
update init plugin config
2025-01-16 18:58:36 +08:00
Liujian 6ba2a08b62 update init plugin config 2025-01-16 18:53:58 +08:00
Dot.L d232269416 Merge pull request #167 from APIParkLab/feature/ai-balance
Feature/ai balance
2025-01-16 16:37:41 +08:00
Liujian 9d2208e14d update provider status default value 2025-01-16 16:36:25 +08:00
Liujian 8d69d45d1d update build script 2025-01-16 16:36:06 +08:00
刘健 b0c37918b5 Merge branch 'feature/merge' into 'main'
fix: login page redirect multiple times (#166)

See merge request apipark/APIPark!158
2025-01-16 14:57:39 +08:00
刘健 d5af1c8da3 Merge branch 'feature/ai-balance' into 'main'
Feature/1.4 (#154)

See merge request apipark/APIPark!157
2025-01-15 16:04:24 +08:00
ScarChin a6105cfc3c fix: 1.3-beta版本,超级管理员(admin)账户无法修改分类和添加子分类,页面显示无权限操作 (#164) 2025-01-14 17:52:07 +08:00
秦圆圆 7c827804f4 Merge branch 'feature/1.4' into 'main'
fix: In the supplier load chart, the mouse should not show a hand shape except...

See merge request apipark/APIPark!156
2025-01-07 18:41:58 +08:00
scarqin b0dacbda0d fix: When the current supplier is abnormal, there should be a line on the model pointing to the next model, which means that the APIs on this link are associated with the next valid supplier. 2025-01-07 18:40:43 +08:00
scarqin d5abde2593 fix: The language option is wrong. The current language is Chinese, but the option is displayed as English. 2025-01-07 18:14:25 +08:00
scarqin bc3290de3b fix: jump link error 2025-01-07 17:56:16 +08:00
scarqin 7f438bf776 fix: When the current supplier is abnormal, there should be a line on the model pointing to the next model, which means that the APIs on this link are associated with the next valid supplier. 2025-01-07 17:54:53 +08:00
scarqin 13cfe24b2f fix: error line 2025-01-07 17:21:21 +08:00
刘健 f5cfd77550 Merge branch 'feature/ai-balance' into 'main'
fix: ai provider status error

See merge request apipark/APIPark!155
2025-01-07 16:50:27 +08:00
刘健 4a8f5152b3 Merge branch 'feature/ai-balance' into 'main'
AI API token quantity docking completed

See merge request apipark/APIPark!154
2025-01-07 12:49:51 +08:00
秦圆圆 83ac747cb1 Merge branch 'feature/1.4' into 'main'
fix: After the release log configuration is successful, there is no success...

See merge request apipark/APIPark!153
2025-01-07 11:33:54 +08:00
刘健 d5eedd1dd2 Merge branch 'feature/ai-balance' into 'main'
fix: ai key config error

See merge request apipark/APIPark!152
2025-01-07 09:23:34 +08:00
秦圆圆 86758383c4 Merge branch 'feature/1.4' into 'main'
Feature/1.4

See merge request apipark/APIPark!151
2025-01-06 19:45:37 +08:00
刘健 6ce3e0bfac Merge branch 'feature/ai-balance' into 'main'
update ai key status to gateway

See merge request apipark/APIPark!150
2025-01-06 10:57:54 +08:00
刘健 e4eadf863e Merge branch 'feature/ai-balance' into 'main'
fix: ai key sort

See merge request apipark/APIPark!149
2025-01-06 10:26:39 +08:00
刘健 ca328e784c Merge branch 'feature/ai-balance' into 'main'
Feature/ai balance

See merge request apipark/APIPark!148
2025-01-06 10:02:19 +08:00
73 changed files with 3425 additions and 1166 deletions
+3 -3
View File
@@ -25,7 +25,7 @@ jobs:
echo "Build frontend..."
cd ./frontend && pnpm run build
- name: upload frontend release
uses: actions/upload-artifact@v3
uses: actions/upload-artifact@v4
with:
name: frontend-package
path: frontend/dist
@@ -41,7 +41,7 @@ jobs:
- name: Checkout #Checkout代码
uses: actions/checkout@v3
- name: download frontend release
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: frontend-package
path: frontend/dist
@@ -71,7 +71,7 @@ jobs:
- uses: actions/checkout@v3
- name: download frontend release
uses: actions/download-artifact@v3
uses: actions/download-artifact@v4
with:
name: frontend-package
path: frontend/dist
+1 -1
View File
@@ -210,7 +210,7 @@ APIPark uses the Apache 2.0 License. For more details, please refer to the LICEN
For enterprise-level features and professional technical support, contact our pre-sales experts for personalized demos, customized solutions, and pricing.
- Website: https://apipark.com
- Email: dev@apipark.com
- Email: contact@apipark.com
<br>
+17 -11
View File
@@ -3,6 +3,7 @@ package main
import (
"context"
"encoding/json"
"fmt"
"log"
"strings"
"time"
@@ -27,8 +28,8 @@ func init() {
}
type NSQConfig struct {
Addr string `json:"addr"`
TopicPrefix string `json:"topic_prefix"`
Addr string `json:"addr" yaml:"addr"`
TopicPrefix string `json:"topic_prefix" yaml:"topic_prefix"`
}
// 定义 NSQ 消息结构
@@ -78,6 +79,11 @@ func convertInt(value interface{}) int {
}
}
func genAIKey(key string, provider string) string {
keys := strings.Split(key, "@")
return strings.TrimSuffix(keys[0], fmt.Sprintf("-%s", provider))
}
// HandleMessage 处理从 NSQ 读取的消息
func (h *NSQHandler) HandleMessage(message *nsq.Message) error {
log.Printf("Received message: %s", string(message.Body))
@@ -87,14 +93,14 @@ func (h *NSQHandler) HandleMessage(message *nsq.Message) error {
err := json.Unmarshal(message.Body, &data)
if err != nil {
log.Printf("Failed to unmarshal message: %v", err)
return err
return nil
}
// 将时间字符串转换为 time.Time
timestamp, err := time.Parse(time.RFC3339, data.TimeISO8601)
if err != nil {
log.Printf("Failed to parse timestamp: %v", err)
return err
return nil
}
day := time.Date(timestamp.Year(), timestamp.Month(), timestamp.Day(), 0, 0, 0, 0, timestamp.Location())
@@ -104,14 +110,13 @@ func (h *NSQHandler) HandleMessage(message *nsq.Message) error {
finalStatus := &AIProviderStatus{}
for _, s := range data.AI.ProviderStats {
status := ToKeyStatus(s.Status).Int()
keys := strings.Split(s.Key, "@")
key := keys[0]
key := genAIKey(s.Key, s.Provider)
err = h.aiKeyService.Save(ctx, key, &ai_key.Edit{
Status: &status,
})
if err != nil {
log.Printf("Failed to save AI key: %v", err)
return err
return nil
}
if s.Provider != data.AI.Provider {
@@ -128,11 +133,12 @@ func (h *NSQHandler) HandleMessage(message *nsq.Message) error {
finalStatus = &s
}
if finalStatus != nil {
keys := strings.Split(finalStatus.Key, "@")
err = h.aiKeyService.IncrUseToken(ctx, keys[0], convertInt(data.AI.TotalToken))
//keys := strings.Split(finalStatus.Key, "@")
key := genAIKey(finalStatus.Key, finalStatus.Provider)
err = h.aiKeyService.IncrUseToken(ctx, key, convertInt(data.AI.TotalToken))
if err != nil {
log.Printf("Failed to increment AI key token: %v", err)
return err
return nil
}
}
@@ -151,7 +157,7 @@ func (h *NSQHandler) HandleMessage(message *nsq.Message) error {
})
if err != nil {
log.Printf("Failed to call AI API: %v", err)
return err
return nil
}
log.Printf("Message processed and saved to MySQL: %+v", data)
@@ -0,0 +1,3 @@
<svg xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" width="100" height="100" viewBox="0 0 100 100">
<image id="icons8-server-100" y="11" width="99" height="78" xlink:href="data:img/png;base64,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"/>
</svg>

After

Width:  |  Height:  |  Size: 3.7 KiB

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

After

Width:  |  Height:  |  Size: 4.5 KiB

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

After

Width:  |  Height:  |  Size: 4.1 KiB

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