mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
Compare commits
57 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| b02f47b8a4 | |||
| 9cb09905f9 | |||
| eeb2fbcad6 | |||
| 400faf92c0 | |||
| 901bef1463 | |||
| 8d44d796b4 | |||
| 5a10ad478e | |||
| fd6680d615 | |||
| e03cdfc42b | |||
| 945d53fcfd | |||
| ac7045b724 | |||
| c907bdc4a5 | |||
| 733ed9ac2f | |||
| 1d8e579a10 | |||
| 567cac9c95 | |||
| 095c09c8c0 | |||
| e9c949822d | |||
| 3482d5416c | |||
| d8cb4a0c94 | |||
| 59acfa7a47 | |||
| 2eb2e690d1 | |||
| f7801261c3 | |||
| 7e7be7f040 | |||
| 0187fd16b2 | |||
| ba0bdb5e99 | |||
| 9d3e4f07bf | |||
| bd81d7584d | |||
| 9577339e14 | |||
| 5c292ef1cb | |||
| 4f3de85068 | |||
| 07a25c9643 | |||
| 8f60426b4c | |||
| 37f87615bd | |||
| 3f96de660b | |||
| e86999770f | |||
| a8bb0c24ec | |||
| 6ba2a08b62 | |||
| d232269416 | |||
| 9d2208e14d | |||
| 8d69d45d1d | |||
| b0c37918b5 | |||
| d5af1c8da3 | |||
| a6105cfc3c | |||
| 7c827804f4 | |||
| b0dacbda0d | |||
| d5abde2593 | |||
| bc3290de3b | |||
| 7f438bf776 | |||
| 13cfe24b2f | |||
| f5cfd77550 | |||
| 4a8f5152b3 | |||
| 83ac747cb1 | |||
| d5eedd1dd2 | |||
| 86758383c4 | |||
| 6ce3e0bfac | |||
| e4eadf863e | |||
| ca328e784c |
@@ -25,7 +25,7 @@ jobs:
|
|||||||
echo "Build frontend..."
|
echo "Build frontend..."
|
||||||
cd ./frontend && pnpm run build
|
cd ./frontend && pnpm run build
|
||||||
- name: upload frontend release
|
- name: upload frontend release
|
||||||
uses: actions/upload-artifact@v3
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: frontend-package
|
name: frontend-package
|
||||||
path: frontend/dist
|
path: frontend/dist
|
||||||
@@ -41,7 +41,7 @@ jobs:
|
|||||||
- name: Checkout #Checkout代码
|
- name: Checkout #Checkout代码
|
||||||
uses: actions/checkout@v3
|
uses: actions/checkout@v3
|
||||||
- name: download frontend release
|
- name: download frontend release
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: frontend-package
|
name: frontend-package
|
||||||
path: frontend/dist
|
path: frontend/dist
|
||||||
@@ -71,7 +71,7 @@ jobs:
|
|||||||
- uses: actions/checkout@v3
|
- uses: actions/checkout@v3
|
||||||
|
|
||||||
- name: download frontend release
|
- name: download frontend release
|
||||||
uses: actions/download-artifact@v3
|
uses: actions/download-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: frontend-package
|
name: frontend-package
|
||||||
path: frontend/dist
|
path: frontend/dist
|
||||||
|
|||||||
@@ -210,7 +210,7 @@ APIPark uses the Apache 2.0 License. For more details, please refer to the LICEN
|
|||||||
For enterprise-level features and professional technical support, contact our pre-sales experts for personalized demos, customized solutions, and pricing.
|
For enterprise-level features and professional technical support, contact our pre-sales experts for personalized demos, customized solutions, and pricing.
|
||||||
|
|
||||||
- Website: https://apipark.com
|
- Website: https://apipark.com
|
||||||
- Email: dev@apipark.com
|
- Email: contact@apipark.com
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
+17
-11
@@ -3,6 +3,7 @@ package main
|
|||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
"log"
|
"log"
|
||||||
"strings"
|
"strings"
|
||||||
"time"
|
"time"
|
||||||
@@ -27,8 +28,8 @@ func init() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type NSQConfig struct {
|
type NSQConfig struct {
|
||||||
Addr string `json:"addr"`
|
Addr string `json:"addr" yaml:"addr"`
|
||||||
TopicPrefix string `json:"topic_prefix"`
|
TopicPrefix string `json:"topic_prefix" yaml:"topic_prefix"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// 定义 NSQ 消息结构
|
// 定义 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 读取的消息
|
// HandleMessage 处理从 NSQ 读取的消息
|
||||||
func (h *NSQHandler) HandleMessage(message *nsq.Message) error {
|
func (h *NSQHandler) HandleMessage(message *nsq.Message) error {
|
||||||
log.Printf("Received message: %s", string(message.Body))
|
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)
|
err := json.Unmarshal(message.Body, &data)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to unmarshal message: %v", err)
|
log.Printf("Failed to unmarshal message: %v", err)
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// 将时间字符串转换为 time.Time
|
// 将时间字符串转换为 time.Time
|
||||||
timestamp, err := time.Parse(time.RFC3339, data.TimeISO8601)
|
timestamp, err := time.Parse(time.RFC3339, data.TimeISO8601)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to parse timestamp: %v", err)
|
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())
|
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{}
|
finalStatus := &AIProviderStatus{}
|
||||||
for _, s := range data.AI.ProviderStats {
|
for _, s := range data.AI.ProviderStats {
|
||||||
status := ToKeyStatus(s.Status).Int()
|
status := ToKeyStatus(s.Status).Int()
|
||||||
keys := strings.Split(s.Key, "@")
|
key := genAIKey(s.Key, s.Provider)
|
||||||
key := keys[0]
|
|
||||||
err = h.aiKeyService.Save(ctx, key, &ai_key.Edit{
|
err = h.aiKeyService.Save(ctx, key, &ai_key.Edit{
|
||||||
Status: &status,
|
Status: &status,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to save AI key: %v", err)
|
log.Printf("Failed to save AI key: %v", err)
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
if s.Provider != data.AI.Provider {
|
if s.Provider != data.AI.Provider {
|
||||||
|
|
||||||
@@ -128,11 +133,12 @@ func (h *NSQHandler) HandleMessage(message *nsq.Message) error {
|
|||||||
finalStatus = &s
|
finalStatus = &s
|
||||||
}
|
}
|
||||||
if finalStatus != nil {
|
if finalStatus != nil {
|
||||||
keys := strings.Split(finalStatus.Key, "@")
|
//keys := strings.Split(finalStatus.Key, "@")
|
||||||
err = h.aiKeyService.IncrUseToken(ctx, keys[0], convertInt(data.AI.TotalToken))
|
key := genAIKey(finalStatus.Key, finalStatus.Provider)
|
||||||
|
err = h.aiKeyService.IncrUseToken(ctx, key, convertInt(data.AI.TotalToken))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Printf("Failed to increment AI key token: %v", err)
|
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 {
|
if err != nil {
|
||||||
log.Printf("Failed to call AI API: %v", err)
|
log.Printf("Failed to call AI API: %v", err)
|
||||||
return err
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
log.Printf("Message processed and saved to MySQL: %+v", data)
|
log.Printf("Message processed and saved to MySQL: %+v", data)
|
||||||
|
|||||||
@@ -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,8 @@ import { useI18n } from '@common/locales'
|
|||||||
import { Tabs } from 'antd'
|
import { Tabs } from 'antd'
|
||||||
import { useEffect, useState } from 'react'
|
import { useEffect, useState } from 'react'
|
||||||
import { useSearchParams } from 'react-router-dom'
|
import { useSearchParams } from 'react-router-dom'
|
||||||
import AIFlowChart from './AIFlowChart'
|
|
||||||
import AIUnConfigure from './AIUnconfigure'
|
|
||||||
import { AiSettingProvider } from './contexts/AiSettingContext'
|
import { AiSettingProvider } from './contexts/AiSettingContext'
|
||||||
|
import OnlineModelList from './OnlineModelList'
|
||||||
|
|
||||||
const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const
|
const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const
|
||||||
|
|
||||||
@@ -38,21 +37,17 @@ const AiSettingContent = () => {
|
|||||||
items={[
|
items={[
|
||||||
{
|
{
|
||||||
key: 'flow',
|
key: 'flow',
|
||||||
label: $t('已设置'),
|
label: $t('在线模型'),
|
||||||
children: (
|
children: (
|
||||||
<div className="overflow-auto" style={CONTENT_STYLE}>
|
<div className="overflow-auto" style={CONTENT_STYLE}>
|
||||||
<AIFlowChart />
|
<OnlineModelList />
|
||||||
</div>
|
</div>
|
||||||
)
|
)
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
key: 'config',
|
key: 'config',
|
||||||
label: $t('未设置'),
|
label: $t('本地模型'),
|
||||||
children: (
|
children: <div className="overflow-auto" style={CONTENT_STYLE}></div>
|
||||||
<div className="overflow-auto" style={CONTENT_STYLE}>
|
|
||||||
<AIUnConfigure />
|
|
||||||
</div>
|
|
||||||
)
|
|
||||||
}
|
}
|
||||||
]}
|
]}
|
||||||
/>
|
/>
|
||||||
|
|||||||
@@ -1,14 +1,13 @@
|
|||||||
import { QuestionCircleOutlined } from '@ant-design/icons'
|
|
||||||
import { Codebox } from '@common/components/postcat/api/Codebox'
|
import { Codebox } from '@common/components/postcat/api/Codebox'
|
||||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||||
import { useFetch } from '@common/hooks/http'
|
import { useFetch } from '@common/hooks/http'
|
||||||
import { $t } from '@common/locales'
|
import { $t } from '@common/locales'
|
||||||
import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd'
|
import { App, Form, Select, Switch, Tag } from 'antd'
|
||||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||||
import { AiProviderLlmsItems, ModelDetailData } from './types'
|
import { AiProviderLlmsItems, ModelDetailData } from './types'
|
||||||
|
|
||||||
export type AiSettingModalContentProps = {
|
export type AiSettingModalContentProps = {
|
||||||
entity: ModelDetailData & { defaultLlm: string }
|
entity?: { id: string | undefined; defaultLlm: string | undefined }
|
||||||
readOnly: boolean
|
readOnly: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -23,12 +22,41 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
|||||||
const { fetchData } = useFetch()
|
const { fetchData } = useFetch()
|
||||||
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>()
|
const [llmList, setLlmList] = useState<AiProviderLlmsItems[]>()
|
||||||
const [loading, setLoading] = useState<boolean>(false)
|
const [loading, setLoading] = useState<boolean>(false)
|
||||||
const [enableState, setEnableState] = useState<boolean>(entity.status === 'enabled')
|
const [enableState, setEnableState] = useState<boolean>(true)
|
||||||
|
const [status, setStatus] = useState<string>('enabled')
|
||||||
|
const [providers, setProviders] = useState<Array<{ id: string; name: string }>>([])
|
||||||
|
const [selectedProvider, setSelectedProvider] = useState<string>(entity?.id || '')
|
||||||
|
|
||||||
|
const getUnconfiguredProviders = () => {
|
||||||
|
if (entity?.id) return
|
||||||
|
fetchData<BasicResponse<{ providers: Array<{ id: string; name: string }> }>>('ai/providers/unconfigured', {
|
||||||
|
method: 'GET',
|
||||||
|
eoTransformKeys: ['default_llm']
|
||||||
|
}).then((response) => {
|
||||||
|
const { code, data, msg } = response
|
||||||
|
if (code === STATUS_CODE.SUCCESS) {
|
||||||
|
setProviders(data.providers)
|
||||||
|
if (data.providers.length > 0) {
|
||||||
|
const provider = data.providers[0]
|
||||||
|
setSelectedProvider(provider.id)
|
||||||
|
form.setFieldsValue({
|
||||||
|
provider: provider.id,
|
||||||
|
defaultLlm: provider.defaultLlm
|
||||||
|
})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
const getLlmList = () => {
|
const getLlmList = () => {
|
||||||
|
if (!selectedProvider) return
|
||||||
|
|
||||||
setLoading(true)
|
setLoading(true)
|
||||||
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[] }>>(`ai/provider/llms`, {
|
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[] }>>(`ai/provider/llms`, {
|
||||||
method: 'GET',
|
method: 'GET',
|
||||||
eoParams: { provider: entity.id }
|
eoParams: { provider: selectedProvider }
|
||||||
})
|
})
|
||||||
.then((response) => {
|
.then((response) => {
|
||||||
const { code, data, msg } = response
|
const { code, data, msg } = response
|
||||||
@@ -42,56 +70,80 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
|||||||
setLoading(false)
|
setLoading(false)
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
const initData = async () => {
|
||||||
|
if (entity?.id) {
|
||||||
|
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', 'default_llm']
|
||||||
|
})
|
||||||
|
message.destroy()
|
||||||
|
if (code !== STATUS_CODE.SUCCESS) {
|
||||||
|
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||||
|
return
|
||||||
|
}
|
||||||
|
const provider = data.provider
|
||||||
|
form.setFieldsValue({
|
||||||
|
defaultLlm: provider.defaultLlm,
|
||||||
|
config: provider.config ? JSON.stringify(JSON.parse(provider.config), null, 2) : '',
|
||||||
|
enable: provider.status === 'enabled'
|
||||||
|
})
|
||||||
|
setStatus(provider.status)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
form.setFieldsValue({
|
||||||
|
defaultLlm: entity?.defaultLlm,
|
||||||
|
config: '',
|
||||||
|
enable: true
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
useEffect(() => {
|
||||||
|
getUnconfiguredProviders()
|
||||||
|
}, [])
|
||||||
|
|
||||||
useEffect(() => {
|
useEffect(() => {
|
||||||
getLlmList()
|
getLlmList()
|
||||||
try {
|
}, [selectedProvider])
|
||||||
form.setFieldsValue({
|
|
||||||
defaultLlm: entity.defaultLlm,
|
useEffect(() => {
|
||||||
config: entity!.config ? JSON.stringify(JSON.parse(entity!.config), null, 2) : '',
|
initData()
|
||||||
priority: entity.priority || 1,
|
}, [entity])
|
||||||
enable: entity.status === 'enabled'
|
|
||||||
})
|
|
||||||
} catch (e) {
|
|
||||||
form.setFieldsValue({
|
|
||||||
defaultLlm: entity.defaultLlm,
|
|
||||||
config: '',
|
|
||||||
priority: 1,
|
|
||||||
enable: true
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}, [])
|
|
||||||
|
|
||||||
const save: () => Promise<boolean | string> = () => {
|
const save: () => Promise<boolean | string> = () => {
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise(async (resolve, reject) => {
|
||||||
form
|
try {
|
||||||
.validateFields()
|
form
|
||||||
.then((value) => {
|
.validateFields()
|
||||||
const finalValue = {
|
.then((value) => {
|
||||||
...value,
|
const finalValue = {
|
||||||
priority: Math.max(1, value.priority)
|
...value,
|
||||||
}
|
priority: Math.max(1, value.priority)
|
||||||
|
}
|
||||||
|
|
||||||
fetchData<BasicResponse<null>>('ai/provider/config', {
|
fetchData<BasicResponse<null>>('ai/provider/config', {
|
||||||
method: 'PUT',
|
method: entity ? 'PUT' : 'POST',
|
||||||
eoParams: { provider: entity?.id },
|
eoParams: { provider: selectedProvider },
|
||||||
eoBody: finalValue,
|
eoBody: finalValue,
|
||||||
eoTransformKeys: ['defaultLlm']
|
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))
|
.then((response) => {
|
||||||
})
|
const { code, msg } = response
|
||||||
.catch((errorInfo) => reject(errorInfo))
|
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)
|
||||||
|
}
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -117,6 +169,15 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
|||||||
autoComplete="off"
|
autoComplete="off"
|
||||||
disabled={readOnly}
|
disabled={readOnly}
|
||||||
>
|
>
|
||||||
|
{!entity?.id && (
|
||||||
|
<Form.Item label={$t('供应商')} name="provider" rules={[{ required: true, message: $t('请选择供应商') }]}>
|
||||||
|
<Select
|
||||||
|
placeholder={$t('请选择供应商')}
|
||||||
|
onChange={(value) => setSelectedProvider(value)}
|
||||||
|
options={providers.map((p) => ({ label: p.name, value: p.id }))}
|
||||||
|
/>
|
||||||
|
</Form.Item>
|
||||||
|
)}
|
||||||
<Form.Item<ModelDetailData> label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}>
|
<Form.Item<ModelDetailData> label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}>
|
||||||
<Select
|
<Select
|
||||||
className="w-INPUT_NORMAL"
|
className="w-INPUT_NORMAL"
|
||||||
@@ -134,34 +195,6 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
|||||||
></Select>
|
></Select>
|
||||||
</Form.Item>
|
</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>
|
|
||||||
|
|
||||||
<Form.Item<ModelDetailData> label={$t('API Key(默认 Key)')} name="config">
|
<Form.Item<ModelDetailData> label={$t('API Key(默认 Key)')} name="config">
|
||||||
<Codebox
|
<Codebox
|
||||||
editorTheme="vs-dark"
|
editorTheme="vs-dark"
|
||||||
@@ -172,15 +205,14 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
|||||||
enableToolbar={false}
|
enableToolbar={false}
|
||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
{entity?.id && (
|
||||||
{entity.configured && (
|
|
||||||
<Form.Item className="p-4 bg-white rounded-lg" label={$t('LLM 状态管理')}>
|
<Form.Item className="p-4 bg-white rounded-lg" label={$t('LLM 状态管理')}>
|
||||||
<div className="flex justify-between items-center">
|
<div className="flex justify-between items-center">
|
||||||
<div>
|
<div>
|
||||||
<span className="text-gray-600">{$t('当前调用状态:')}</span>
|
<span className="text-gray-600">{$t('当前调用状态:')}</span>
|
||||||
{entity.status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
|
{status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
|
||||||
{entity.status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
|
{status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
|
||||||
{entity.status === 'abnormal' && <Tag color="error">{$t('异常')}</Tag>}
|
{status === 'abnormal' && <Tag color="error">{$t('异常')}</Tag>}
|
||||||
</div>
|
</div>
|
||||||
<Form.Item name="enable" valuePropName="checked" noStyle>
|
<Form.Item name="enable" valuePropName="checked" noStyle>
|
||||||
<Switch
|
<Switch
|
||||||
@@ -193,7 +225,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
|||||||
/>
|
/>
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
</div>
|
</div>
|
||||||
{(entity.status === 'enabled' && !enableState) || (entity.status !== 'enabled' && enableState) ? (
|
{(status === 'enabled' && !enableState) || (status !== 'enabled' && enableState) ? (
|
||||||
<div className="mt-2 text-sm text-gray-500">* {getTooltipText(enableState)}</div>
|
<div className="mt-2 text-sm text-gray-500">* {getTooltipText(enableState)}</div>
|
||||||
) : null}
|
) : null}
|
||||||
</Form.Item>
|
</Form.Item>
|
||||||
|
|||||||
@@ -0,0 +1,167 @@
|
|||||||
|
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 } = 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)
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleAdd = () => {
|
||||||
|
openConfigModal()
|
||||||
|
}
|
||||||
|
|
||||||
|
const handleDelete = async (id: string) => {
|
||||||
|
try {
|
||||||
|
const response = await fetchData<BasicResponse<any>>('ai/provider', {
|
||||||
|
method: 'DELETE',
|
||||||
|
eoParams: {
|
||||||
|
provider: id
|
||||||
|
}
|
||||||
|
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (response.code === STATUS_CODE.SUCCESS) {
|
||||||
|
message.success($t('删除成功'))
|
||||||
|
pageListRef.current?.reload()
|
||||||
|
} else {
|
||||||
|
message.error(response.msg || RESPONSE_TIPS.error)
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
message.error(RESPONSE_TIPS.error)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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']
|
||||||
|
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||||
|
})
|
||||||
|
|
||||||
|
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"
|
||||||
|
btnType="delete"
|
||||||
|
onClick={() => handleDelete(entity.id as string)}
|
||||||
|
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('默认模型'),
|
||||||
|
dataIndex: 'defaultLlm'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('Apis'),
|
||||||
|
dataIndex: 'api_count'
|
||||||
|
},
|
||||||
|
{
|
||||||
|
title: $t('Keys'),
|
||||||
|
dataIndex: 'key_count'
|
||||||
|
},
|
||||||
|
...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,30 @@
|
|||||||
import Icon from '@ant-design/icons'
|
import Icon from '@ant-design/icons'
|
||||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
|
||||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
import { useGlobalContext } from '@common/contexts/GlobalStateContext'
|
||||||
import { useFetch } from '@common/hooks/http'
|
|
||||||
import { $t } from '@common/locales'
|
import { $t } from '@common/locales'
|
||||||
import { checkAccess } from '@common/utils/permission'
|
import { checkAccess } from '@common/utils/permission'
|
||||||
import { App } from 'antd'
|
import { App } from 'antd'
|
||||||
import { createContext, useContext, useRef } from 'react'
|
import { createContext, useContext, useRef } from 'react'
|
||||||
import AiSettingModalContent, { AiSettingModalContentHandle } from '../AiSettingModal'
|
import AiSettingModalContent, { AiSettingModalContentHandle } from '../AiSettingModal'
|
||||||
import { AiSettingListItem, ModelDetailData } from '../types'
|
import { AiSettingListItem } from '../types'
|
||||||
|
|
||||||
interface AiSettingContextType {
|
interface AiSettingContextType {
|
||||||
openConfigModal: (entity: AiSettingListItem) => Promise<void>
|
openConfigModal: (entity?: AiSettingListItem) => Promise<void>
|
||||||
}
|
}
|
||||||
|
|
||||||
const AiSettingContext = createContext<AiSettingContextType | undefined>(undefined)
|
const AiSettingContext = createContext<AiSettingContextType | undefined>(undefined)
|
||||||
|
|
||||||
export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => {
|
||||||
const { modal, message } = App.useApp()
|
const { modal } = App.useApp()
|
||||||
const { fetchData } = useFetch()
|
|
||||||
const { aiConfigFlushed, setAiConfigFlushed, accessData } = useGlobalContext()
|
const { aiConfigFlushed, setAiConfigFlushed, accessData } = useGlobalContext()
|
||||||
const modalRef = useRef<AiSettingModalContentHandle>()
|
const modalRef = useRef<AiSettingModalContentHandle>()
|
||||||
|
|
||||||
const openConfigModal = async (entity: AiSettingListItem) => {
|
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
|
|
||||||
}
|
|
||||||
|
|
||||||
modal.confirm({
|
modal.confirm({
|
||||||
title: $t('模型配置'),
|
title: $t('模型配置'),
|
||||||
content: (
|
content: (
|
||||||
<AiSettingModalContent
|
<AiSettingModalContent
|
||||||
ref={modalRef}
|
ref={modalRef}
|
||||||
entity={{ ...data.provider, defaultLlm: entity.defaultLlm }}
|
entity={{ id: entity?.id, defaultLlm: entity?.defaultLlm }}
|
||||||
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
|
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
|
||||||
/>
|
/>
|
||||||
),
|
),
|
||||||
@@ -58,10 +43,10 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
|||||||
<a
|
<a
|
||||||
target="_blank"
|
target="_blank"
|
||||||
rel="noopener noreferrer"
|
rel="noopener noreferrer"
|
||||||
href={data.provider.getApikeyUrl}
|
// href={data.provider.getApikeyUrl}
|
||||||
className="flex items-center gap-[8px]"
|
className="flex items-center gap-[8px]"
|
||||||
>
|
>
|
||||||
<span>{$t('从 (0) 获取 API KEY', [data.provider.name])}</span>
|
{/* <span>{$t('从 (0) 获取 API KEY', [data.provider.name])}</span> */}
|
||||||
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
|
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
|
||||||
</a>
|
</a>
|
||||||
<div>
|
<div>
|
||||||
|
|||||||
@@ -18,11 +18,15 @@
|
|||||||
.react-flow__node {
|
.react-flow__node {
|
||||||
padding: 0;
|
padding: 0;
|
||||||
border-radius: 8px;
|
border-radius: 8px;
|
||||||
min-width: 150px;
|
|
||||||
width: auto;
|
width: auto;
|
||||||
max-width: 100%;
|
max-width: 100%;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
.react-flow__node-modelCard,
|
||||||
|
.react-flow__node-serviceCard {
|
||||||
|
min-width: 150px;
|
||||||
|
}
|
||||||
|
|
||||||
/* Custom Node Styles */
|
/* Custom Node Styles */
|
||||||
.custom-node {
|
.custom-node {
|
||||||
background: white;
|
background: white;
|
||||||
|
|||||||
@@ -1,10 +1,10 @@
|
|||||||
export type ModelStatus = 'enabled' | 'abnormal'|'disabled'
|
export type ModelStatus = 'enabled' | 'abnormal' | 'disabled'
|
||||||
export type KeyStatus ='normal' | 'abnormal'|'disabled'
|
export type KeyStatus = 'normal' | 'abnormal' | 'disabled'
|
||||||
|
|
||||||
export interface KeyData {
|
export interface KeyData {
|
||||||
id: string
|
id: string
|
||||||
name: string
|
name: string
|
||||||
status: KeyStatus,
|
status: KeyStatus
|
||||||
}
|
}
|
||||||
|
|
||||||
export interface ModelListData {
|
export interface ModelListData {
|
||||||
@@ -17,16 +17,14 @@ export interface ModelListData {
|
|||||||
key_count: number
|
key_count: number
|
||||||
keys: KeyData[]
|
keys: KeyData[]
|
||||||
}
|
}
|
||||||
export interface ModelDetailData extends ModelListData{
|
export interface ModelDetailData extends ModelListData {
|
||||||
enable:boolean
|
enable: boolean
|
||||||
config: string,
|
config: string
|
||||||
priority?: number
|
|
||||||
getApikeyUrl: string
|
getApikeyUrl: string
|
||||||
status: ModelStatus
|
status: ModelStatus
|
||||||
configured: boolean
|
configured: boolean
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
export type AiSettingListItem = {
|
export type AiSettingListItem = {
|
||||||
name: string
|
name: string
|
||||||
id: string
|
id: string
|
||||||
@@ -53,5 +51,3 @@ export type AiProviderDefaultConfig = {
|
|||||||
defaultLlm: string
|
defaultLlm: string
|
||||||
scopes: string[]
|
scopes: string[]
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
'use client'
|
'use client'
|
||||||
|
|
||||||
import AIFlowChart from '../aiSetting/AIFlowChart'
|
|
||||||
|
|
||||||
export default function Playground() {
|
export default function Playground() {
|
||||||
return <AIFlowChart />
|
return <iframe src="/playground" />
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -28,7 +28,7 @@
|
|||||||
b: "subscription_service:#{application}"
|
b: "subscription_service:#{application}"
|
||||||
response:
|
response:
|
||||||
status_code: 403
|
status_code: 403
|
||||||
content_typ: "text/plan"
|
content_type: "text/plan"
|
||||||
charset: "utf-8"
|
charset: "utf-8"
|
||||||
body: "Forbidden"
|
body: "Forbidden"
|
||||||
|
|
||||||
|
|||||||
@@ -5,6 +5,7 @@ import (
|
|||||||
_ "github.com/APIParkLab/APIPark/frontend"
|
_ "github.com/APIParkLab/APIPark/frontend"
|
||||||
_ "github.com/APIParkLab/APIPark/gateway/apinto"
|
_ "github.com/APIParkLab/APIPark/gateway/apinto"
|
||||||
_ "github.com/APIParkLab/APIPark/plugins/core"
|
_ "github.com/APIParkLab/APIPark/plugins/core"
|
||||||
|
_ "github.com/APIParkLab/APIPark/plugins/openapi"
|
||||||
_ "github.com/APIParkLab/APIPark/plugins/permit"
|
_ "github.com/APIParkLab/APIPark/plugins/permit"
|
||||||
_ "github.com/APIParkLab/APIPark/plugins/publish_flow"
|
_ "github.com/APIParkLab/APIPark/plugins/publish_flow"
|
||||||
_ "github.com/APIParkLab/APIPark/resources/locale"
|
_ "github.com/APIParkLab/APIPark/resources/locale"
|
||||||
|
|||||||
+13
-18
@@ -533,16 +533,17 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string,
|
|||||||
DefaultLLM: &input.DefaultLLM,
|
DefaultLLM: &input.DefaultLLM,
|
||||||
Config: &input.Config,
|
Config: &input.Config,
|
||||||
Priority: input.Priority,
|
Priority: input.Priority,
|
||||||
|
Status: &status,
|
||||||
}
|
}
|
||||||
_, err = i.aiKeyService.DefaultKey(ctx, id)
|
_, err = i.aiKeyService.DefaultKey(txCtx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
if !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
err = i.aiKeyService.Create(ctx, &ai_key.Create{
|
err = i.aiKeyService.Create(txCtx, &ai_key.Create{
|
||||||
ID: id,
|
ID: id,
|
||||||
Name: info.Name,
|
Name: info.Name,
|
||||||
Config: info.Config,
|
Config: input.Config,
|
||||||
Provider: id,
|
Provider: id,
|
||||||
Status: 1,
|
Status: 1,
|
||||||
ExpireTime: 0,
|
ExpireTime: 0,
|
||||||
@@ -550,27 +551,21 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string,
|
|||||||
Priority: 1,
|
Priority: 1,
|
||||||
})
|
})
|
||||||
} else {
|
} else {
|
||||||
err = i.aiKeyService.Save(ctx, id, &ai_key.Edit{
|
err = i.aiKeyService.Save(txCtx, id, &ai_key.Edit{
|
||||||
Config: &info.Config,
|
Config: &input.Config,
|
||||||
|
Status: &status,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
err = i.providerService.Save(txCtx, id, pInfo)
|
||||||
if input.Enable != nil {
|
|
||||||
status = 0
|
|
||||||
if *input.Enable {
|
|
||||||
status = 1
|
|
||||||
}
|
|
||||||
pInfo.Status = &status
|
|
||||||
}
|
|
||||||
err = i.providerService.Save(ctx, id, pInfo)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
if *pInfo.Status == 0 {
|
if *pInfo.Status == 0 {
|
||||||
return i.syncGateway(ctx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
|
return i.syncGateway(txCtx, cluster.DefaultClusterID, []*gateway.DynamicRelease{
|
||||||
{
|
{
|
||||||
BasicItem: &gateway.BasicItem{
|
BasicItem: &gateway.BasicItem{
|
||||||
ID: id,
|
ID: id,
|
||||||
@@ -579,8 +574,8 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string,
|
|||||||
},
|
},
|
||||||
}, false)
|
}, false)
|
||||||
}
|
}
|
||||||
// 获取当前供应商所有Key信息
|
// 获取当前供应商默认Key信息
|
||||||
defaultKey, err := i.aiKeyService.DefaultKey(ctx, id)
|
defaultKey, err := i.aiKeyService.DefaultKey(txCtx, id)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
@@ -590,7 +585,7 @@ func (i *imlProviderModule) UpdateProviderConfig(ctx context.Context, id string,
|
|||||||
cfg["model_config"] = model.DefaultConfig()
|
cfg["model_config"] = model.DefaultConfig()
|
||||||
cfg["priority"] = info.Priority
|
cfg["priority"] = info.Priority
|
||||||
cfg["base"] = fmt.Sprintf("%s://%s", p.URI().Scheme(), p.URI().Host())
|
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{
|
BasicItem: &gateway.BasicItem{
|
||||||
ID: id,
|
ID: id,
|
||||||
|
|||||||
@@ -100,7 +100,8 @@ func (i *imlAuthorizationModule) getApplications(ctx context.Context, appIds []s
|
|||||||
Config: authCfg,
|
Config: authCfg,
|
||||||
HideCredential: a.HideCredential,
|
HideCredential: a.HideCredential,
|
||||||
Label: map[string]string{
|
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,
|
Config: authCfg,
|
||||||
HideCredential: a.HideCredential,
|
HideCredential: a.HideCredential,
|
||||||
Label: map[string]string{
|
Label: map[string]string{
|
||||||
"authorization": a.UUID,
|
"authorization": a.UUID,
|
||||||
|
"authorization_name": a.Name,
|
||||||
},
|
},
|
||||||
}
|
}
|
||||||
}),
|
}),
|
||||||
|
|||||||
@@ -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),
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -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
|
||||||
|
}
|
||||||
@@ -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
@@ -211,7 +211,7 @@ APIParkはApache 2.0ライセンスの下で提供されています。詳細に
|
|||||||
エンタープライズ機能や専門的な技術サポートについては、プリセールスの専門家に連絡し、個別デモ、カスタムソリューション、価格情報を入手してください。
|
エンタープライズ機能や専門的な技術サポートについては、プリセールスの専門家に連絡し、個別デモ、カスタムソリューション、価格情報を入手してください。
|
||||||
|
|
||||||
- ウェブサイト: https://apipark.com
|
- ウェブサイト: https://apipark.com
|
||||||
- メール: dev@apipark.com
|
- メール: contact@apipark.com
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ APIPark 使用 Apache 2.0 许可证。更多详情请查看 LICENSE 文件。
|
|||||||
对于企业级功能和专业技术支持,请联系售前专家进行个性化演示、定制方案和获取报价。
|
对于企业级功能和专业技术支持,请联系售前专家进行个性化演示、定制方案和获取报价。
|
||||||
|
|
||||||
- 网站: https://apipark.com
|
- 网站: https://apipark.com
|
||||||
- 电子邮件: dev@apipark.com
|
- 电子邮件: contact@apipark.com
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
@@ -212,7 +212,7 @@ APIPark 使用 Apache 2.0 授權條款。更多詳情請參閱 LICENSE 文件。
|
|||||||
如需企業級功能與專業技術支援,請聯絡我們的售前專家,獲取個性化演示、定制方案和報價。
|
如需企業級功能與專業技術支援,請聯絡我們的售前專家,獲取個性化演示、定制方案和報價。
|
||||||
|
|
||||||
- 網站: https://apipark.com
|
- 網站: https://apipark.com
|
||||||
- 電子郵件: dev@apipark.com
|
- 電子郵件: contact@apipark.com
|
||||||
|
|
||||||
<br>
|
<br>
|
||||||
|
|
||||||
|
|||||||
@@ -1,4 +1,4 @@
|
|||||||
version: v7
|
version: v8
|
||||||
sort:
|
sort:
|
||||||
- "access_log"
|
- "access_log"
|
||||||
- "monitor"
|
- "monitor"
|
||||||
@@ -41,7 +41,7 @@ plugin:
|
|||||||
b: "subscription_service:#{application}"
|
b: "subscription_service:#{application}"
|
||||||
response:
|
response:
|
||||||
status_code: 403
|
status_code: 403
|
||||||
content_typ: "text/plan"
|
content_type: "text/plan"
|
||||||
charset: "utf-8"
|
charset: "utf-8"
|
||||||
body: "Forbidden"
|
body: "Forbidden"
|
||||||
|
|
||||||
|
|||||||
+2
-2
@@ -8,8 +8,8 @@ RUN ln -snf /usr/share/zoneinfo/$TZ /etc/localtime && echo $TZ > /etc/timezone
|
|||||||
|
|
||||||
ARG APP
|
ARG APP
|
||||||
|
|
||||||
ENV NSQ_ADDR=nsq:4150
|
ENV NSQ_ADDR=${APP}-nsq:4150
|
||||||
ENV NSQ_TOPIC_PREFIX=apipark
|
ENV NSQ_TOPIC_PREFIX=${APP}
|
||||||
|
|
||||||
RUN mkdir -p /${APP}
|
RUN mkdir -p /${APP}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ source ./scripts/common.sh
|
|||||||
APP="apipark"
|
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)
|
VERSION=$(gen_version)
|
||||||
|
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ echo -e " - $s" >> config.yml
|
|||||||
done
|
done
|
||||||
echo -e "nsq:" >> config.yml
|
echo -e "nsq:" >> config.yml
|
||||||
echo -e " addr: ${NSQ_ADDR}" >> 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 "port: 8288" >> config.yml
|
||||||
echo -e "error_log:" >> config.yml
|
echo -e "error_log:" >> config.yml
|
||||||
echo -e " dir: ${ERROR_DIR}" >> config.yml
|
echo -e " dir: ${ERROR_DIR}" >> config.yml
|
||||||
|
|||||||
+1
-1
@@ -8,7 +8,7 @@ type Provider struct {
|
|||||||
Name string `gorm:"type:varchar(100);not null;column:name;comment:name"`
|
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"`
|
DefaultLLM string `gorm:"type:varchar(255);not null;column:default_llm;comment:默认模型ID"`
|
||||||
Config string `gorm:"type:text;not null;column:config;comment:配置信息"`
|
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:优先级,值越小优先级越高"`
|
Priority int `gorm:"type:int;not null;column:priority;comment:优先级,值越小优先级越高"`
|
||||||
Creator string `gorm:"size:36;not null;column:creator;comment:创建人;index:creator" aovalue:"creator"` // 创建人
|
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"` // 更新人
|
Updater string `gorm:"size:36;not null;column:updater;comment:更新人;index:updater" aovalue:"updater"` // 更新人
|
||||||
|
|||||||
+1
-1
@@ -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;"`
|
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;"`
|
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"`
|
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"`
|
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:更新时间"`
|
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:接口数量"`
|
APICount int64 `gorm:"type:int(11);not null;column:api_count;comment:接口数量"`
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ type Commit[H any] struct {
|
|||||||
UUID string `gorm:"size:36;not null;column:uuid;comment:uuid;uniqueIndex:uuid;"`
|
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;"`
|
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;"`
|
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:创建时间"`
|
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;"`
|
Operator string `gorm:"size:36;not null;column:operator;comment:操作人;index:operator;"`
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user