mirror of
https://github.com/APIParkLab/APIPark.git
synced 2026-06-14 20:41:15 +08:00
Compare commits
48 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 9c54fec65e | |||
| e09d35ec53 | |||
| d424b68b2b | |||
| 94503a4ab6 | |||
| 587eec4f15 | |||
| 5759195b23 | |||
| bd4ad2bac8 | |||
| 6b9df8c5e9 | |||
| bb1371090c | |||
| 4c35a51f64 | |||
| 0b6aa6b343 | |||
| 1218ac43f8 | |||
| 04a5da862f | |||
| 66d0653dc6 | |||
| 4390e9767c | |||
| 0481d42929 | |||
| 3140589299 | |||
| 27ba933099 | |||
| e74b022986 | |||
| eaecc5c80a | |||
| ed8c2f286c | |||
| 7b2356f8f3 | |||
| 6ddd2f2389 | |||
| 4e98b09fa4 | |||
| e786393523 | |||
| 4a2995b533 | |||
| 1495451901 | |||
| d4ef5a7516 | |||
| 044e31dd8a | |||
| bd33dff2f3 | |||
| edf30ac61f | |||
| c67964045d | |||
| 92a6f777ed | |||
| 12ed7aafee | |||
| 3becd8a0a7 | |||
| 818436c946 | |||
| d0813e8595 | |||
| 3c0140f3b8 | |||
| 8d415fa273 | |||
| f8fad4caf4 | |||
| 3b54c03027 | |||
| a5f46a930f | |||
| 6157a9d1fa | |||
| f910fc84e5 | |||
| fb023a039b | |||
| 95b5d848f7 | |||
| ded5e064e6 | |||
| 7ea50ec380 |
@@ -43,7 +43,7 @@
|
||||
"react-dom": "^18.2.0",
|
||||
"react-i18next": "^15.0.1",
|
||||
"react-joyride": "^2.8.2",
|
||||
"react-router-dom": "^6.20.0",
|
||||
"react-router-dom": "6.20.0",
|
||||
"swagger-ui-react": "^5.17.14",
|
||||
"tailwindcss": "^3.3.5",
|
||||
"uuid": "^9.0.1",
|
||||
|
||||
@@ -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
|
||||
@@ -237,6 +241,9 @@ function BasicLayout({ project = 'core' }: { project: string }) {
|
||||
headerTitleRender={() => (
|
||||
<div className="w-[192px] flex items-center">
|
||||
<img className="h-[20px] cursor-pointer " src={Logo} onClick={() => navigator(mainPage)} />
|
||||
<a className="align-text-top" href="https://github.com/APIParkLab/APIPark" target="_blank" className="ml-[5px] h-[25px] relative">
|
||||
<img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social" className='absolute top-[6px]' width={75} alt="" />
|
||||
</a>
|
||||
</div>
|
||||
)}
|
||||
logo={Logo}
|
||||
@@ -276,9 +283,9 @@ function BasicLayout({ project = 'core' }: { project: string }) {
|
||||
collapsedButtonRender={false}
|
||||
>
|
||||
<div
|
||||
className={`w-full h-calc-100vh-minus-navbar pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T ${
|
||||
className={`w-full h-calc-100vh-minus-navbar ${
|
||||
currentUrl.startsWith('/role/list') ? 'overflow-auto' : 'overflow-hidden'
|
||||
}`}
|
||||
} ${currentUrl.startsWith('/guide/page') ? '' : 'pl-PAGE_INSIDE_X pt-PAGE_INSIDE_T'}`}
|
||||
>
|
||||
<Outlet />
|
||||
</div>
|
||||
|
||||
@@ -22,6 +22,8 @@ class InsidePageProps {
|
||||
headerClassName?: string = ''
|
||||
/** 整个页面滚动 */
|
||||
scrollPage?: boolean = true
|
||||
scrollInsidePage?: boolean = false
|
||||
customPadding?: boolean
|
||||
customBtn?: ReactNode
|
||||
}
|
||||
|
||||
@@ -41,6 +43,8 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
contentClassName = '',
|
||||
headerClassName = '',
|
||||
scrollPage = true,
|
||||
scrollInsidePage = false,
|
||||
customPadding = false,
|
||||
customBtn
|
||||
}) => {
|
||||
const navigate = useNavigate()
|
||||
@@ -49,7 +53,9 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
navigate(backUrl || '/')
|
||||
}
|
||||
return (
|
||||
<div className={`flex overflow-hidden flex-col flex-1 h-full ${className}`}>
|
||||
<div
|
||||
className={`flex flex-col flex-1 h-full ${scrollInsidePage ? 'overflow-auto' : 'overflow-hidden'} ${className}`}
|
||||
>
|
||||
{showBanner && (
|
||||
<div
|
||||
className={`border-[0px] mr-PAGE_INSIDE_X ${showBorder ? 'border-solid border-b-[1px] border-BORDER' : ''} ${headerClassName}`}
|
||||
@@ -57,7 +63,7 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
{!pageTitle && !description && !backUrl && !customBtn ? (
|
||||
<></>
|
||||
) : (
|
||||
<div className="mb-[30px]">
|
||||
<div className={customPadding ? '' : 'mb-[30px]'}>
|
||||
{backUrl && (
|
||||
<div className="text-[18px] leading-[25px] mb-[12px]">
|
||||
<Button type="text" onClick={goBack}>
|
||||
@@ -98,7 +104,9 @@ const InsidePage: FC<InsidePageProps> = ({
|
||||
)}
|
||||
</div>
|
||||
)}
|
||||
<div className={`h-full ${scrollPage ? 'overflow-hidden' : 'overflow-auto'} ${contentClassName || ''}`}>
|
||||
<div
|
||||
className={`h-full ${scrollInsidePage ? 'overflow-visible' : scrollPage ? 'overflow-hidden' : 'overflow-auto'} ${contentClassName || ''}`}
|
||||
>
|
||||
{children}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -36,11 +36,13 @@ interface PageListProps<T> extends ProTableProps<T, unknown>, RefAttributes<Acti
|
||||
primaryKey?: string
|
||||
addNewBtnTitle?: string
|
||||
addNewBtnAccess?: string
|
||||
addNewBtnDisabled?: boolean
|
||||
tableClickAccess?: string
|
||||
onAddNewBtnClick?: () => void
|
||||
beforeSearchNode?: React.ReactNode[]
|
||||
onSearchWordChange?: (e: ChangeEvent<HTMLInputElement>) => void
|
||||
afterNewBtn?: React.ReactNode[]
|
||||
beforeNewBtn?: React.ReactNode[]
|
||||
dragSortKey?: string
|
||||
onDragSortEnd?: (beforeIndex: number, afterIndex: number, newDataSource: T[]) => void | Promise<void>
|
||||
tableTitle?: string
|
||||
@@ -56,7 +58,8 @@ interface PageListProps<T> extends ProTableProps<T, unknown>, RefAttributes<Acti
|
||||
delayLoading?: boolean
|
||||
noScroll?: boolean
|
||||
/* 前端分页的表格,需要传入该字段以支持后端搜索 */
|
||||
manualReloadTable?: () => void
|
||||
manualReloadTable?: () => void,
|
||||
customEmptyRender?: () => React.ReactNode
|
||||
}
|
||||
|
||||
const PageList = <T extends Record<string, unknown>>(
|
||||
@@ -73,6 +76,7 @@ const PageList = <T extends Record<string, unknown>>(
|
||||
primaryKey = 'id',
|
||||
addNewBtnTitle,
|
||||
addNewBtnAccess,
|
||||
addNewBtnDisabled = false,
|
||||
tableClickAccess,
|
||||
tableClass,
|
||||
onAddNewBtnClick,
|
||||
@@ -80,6 +84,7 @@ const PageList = <T extends Record<string, unknown>>(
|
||||
onSearchWordChange,
|
||||
manualReloadTable,
|
||||
afterNewBtn,
|
||||
beforeNewBtn,
|
||||
dragSortKey,
|
||||
onDragSortEnd,
|
||||
tableTitle,
|
||||
@@ -94,7 +99,8 @@ const PageList = <T extends Record<string, unknown>>(
|
||||
tableTitleClass,
|
||||
delayLoading = true,
|
||||
besidesTableHeight,
|
||||
noScroll
|
||||
noScroll,
|
||||
customEmptyRender
|
||||
} = props
|
||||
const parentRef = useRef<HTMLDivElement>(null)
|
||||
const [tableHeight, setTableHeight] = useState(minVirtualHeight || window.innerHeight)
|
||||
@@ -190,6 +196,7 @@ const PageList = <T extends Record<string, unknown>>(
|
||||
const headerTitle = () => {
|
||||
return (
|
||||
<>
|
||||
{beforeNewBtn ? (beforeNewBtn as React.ReactNode[]) : undefined}
|
||||
{tableTitle ? (
|
||||
<span className={`text-[30px] leading-[42px] my-mbase pl-[20px] ${tableTitleClass}`}>{tableTitle}</span>
|
||||
) : addNewBtnTitle ? (
|
||||
@@ -197,6 +204,7 @@ const PageList = <T extends Record<string, unknown>>(
|
||||
<Button
|
||||
type="primary"
|
||||
className={`mr-btnrbase my-btnbase ${addNewBtnWrapperClass}`}
|
||||
disabled={addNewBtnDisabled}
|
||||
onClick={onAddNewBtnClick}
|
||||
>
|
||||
{addNewBtnTitle}
|
||||
@@ -345,6 +353,11 @@ const PageList = <T extends Record<string, unknown>>(
|
||||
)
|
||||
onChange?.(pagination, filters, sorter, extra)
|
||||
}}
|
||||
locale={{
|
||||
emptyText: customEmptyRender ? customEmptyRender?.() : undefined
|
||||
}}
|
||||
style={customEmptyRender ? { height: '100%' } : undefined}
|
||||
bodyStyle={customEmptyRender ? { height: '100%' } : undefined}
|
||||
rowKey={primaryKey}
|
||||
dataSource={dataSource}
|
||||
search={false}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -148,7 +148,7 @@ const mockData = [
|
||||
access: 'system.settings.ai_provider.view'
|
||||
},
|
||||
{
|
||||
name: 'APIKey 资源池',
|
||||
name: 'API Key 负载',
|
||||
key: 'aiKeys',
|
||||
path: '/keysetting',
|
||||
icon: 'ic:baseline-key',
|
||||
@@ -160,6 +160,13 @@ const mockData = [
|
||||
path: '/aiApis',
|
||||
icon: 'ic:baseline-api',
|
||||
access: 'system.settings.ai_api.view'
|
||||
},
|
||||
{
|
||||
name: '模型灾备',
|
||||
key: 'loadBalancing',
|
||||
path: '/loadBalancing',
|
||||
icon: 'ph:network-x',
|
||||
access: 'system.settings.ai_balance.view'
|
||||
}
|
||||
]
|
||||
},
|
||||
|
||||
@@ -134,6 +134,9 @@ type EoRequest = RequestInit & {
|
||||
eoTransformKeys?: string[]
|
||||
eoApiPrefix?: string
|
||||
eoBody?: { [k: string]: unknown } | Array<unknown> | string
|
||||
isStream?: boolean
|
||||
handleStream?: (line: any) => void
|
||||
callback?: (cancel: () => void) => void
|
||||
}
|
||||
|
||||
type EoHeaders = Headers | { [k: string]: string }
|
||||
@@ -143,6 +146,14 @@ export function useFetch() {
|
||||
const pluginEventHub = usePluginEventHub()
|
||||
|
||||
function fetchData<T>(url: string, options: EoRequest) {
|
||||
const controller = new AbortController()
|
||||
const signal = controller.signal
|
||||
|
||||
// 如果提供了callback,则传递取消请求的函数
|
||||
if (options.callback) {
|
||||
options.callback(() => controller.abort())
|
||||
}
|
||||
|
||||
// 合并传入的headers与默认headers
|
||||
const headers = { ...(options.body ? {} : DEFAULT_HEADERS), ...options.headers }
|
||||
|
||||
@@ -161,7 +172,8 @@ export function useFetch() {
|
||||
headers: {
|
||||
...headers
|
||||
// Authorization: 'Bearer your-token', // 示例:添加统一的Token认证
|
||||
}
|
||||
},
|
||||
signal // 将signal传递给fetch请求
|
||||
}
|
||||
|
||||
return fetch(`${options?.eoApiPrefix === undefined ? '/api/v1/' : options.eoApiPrefix}${url}`, finalOptions)
|
||||
@@ -186,14 +198,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,68 @@
|
||||
"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",
|
||||
"Kb8185132": "OR",
|
||||
"K83829a3b": "Model Exception",
|
||||
"Kb92fb02b": "Deployment Failed",
|
||||
"Kcf8e3b18": "Tags",
|
||||
"K9ae48909": "API Key Balancing",
|
||||
"K437724fc": "Supports creating multiple API Keys for intelligent load balancing under a single API model provider",
|
||||
"K7d17707e": "Model Fallback",
|
||||
"Kc007db4a": "Only .png, .jpg, .jpeg, .svg image files are supported",
|
||||
"Kacf10c44": "Ollama Endpoint",
|
||||
"K8d4f5b44": "Input example: https://www.apipark.com",
|
||||
"K481442d3": "Configure Ollama Service",
|
||||
"Kf9b341e3": "How to deploy Ollama?",
|
||||
"K8632bef2": "Model deployment service not configured",
|
||||
"Kbbd8ce81": "Configure Service",
|
||||
"K39a8d392": "Import OpenAPI documents to publish existing system APIs to APIPark.",
|
||||
"Ka742e079": "Add API Key for public cloud AI models to call public cloud AI models via APIPark.",
|
||||
"K8097d6be": "is an open-source AI Gateway and API Portal that unifies access to OpenAI, DeepSeek, and other AI models. With enterprise-grade security features and real-time monitoring, it helps teams safely manage and share their AI APIs through a unified gateway.",
|
||||
"Kf1ce5b3": "✨ We'd love your support on Github! Leave us a star or share your feedback. ",
|
||||
"K3af90490": "⚡ You can quickly open the API for everyone to use via the following methods:",
|
||||
"K6b99dce8": "address",
|
||||
"Kce2fcdbf": "No Permission"
|
||||
}
|
||||
|
||||
@@ -830,5 +830,68 @@
|
||||
"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} のように、大きな影響を与えるため、慎重に変更してください。",
|
||||
"Kb8185132": "または",
|
||||
"K83829a3b": "モデル異常",
|
||||
"Kb92fb02b": "デプロイ失敗",
|
||||
"Kcf8e3b18": "タグ",
|
||||
"K9ae48909": "APIキーのペイロード",
|
||||
"K437724fc": "1つのAPIモデルプロバイダーで複数のAPIキーを作成し、インテリジェント負荷分散をサポート",
|
||||
"K7d17707e": "モデルフォールバック",
|
||||
"Kc007db4a": "画像ファイルは .png, .jpg, .jpeg, .svg 形式のみサポートされています",
|
||||
"Kacf10c44": "Ollama エンドポイント",
|
||||
"K8d4f5b44": "入力例:https://www.apipark.com",
|
||||
"K481442d3": "Ollama サービスの設定",
|
||||
"Kf9b341e3": "Ollamaのデプロイ方法は?",
|
||||
"K8632bef2": "モデルデプロイサービスが設定されていません",
|
||||
"Kbbd8ce81": "サービスの設定",
|
||||
"K39a8d392": "OpenAPIドキュメントをインポートし、既存のシステムのAPIをAPIParkに公開します。",
|
||||
"Ka742e079": "パブリッククラウドAIモデルのAPIキーを追加し、APIParkを介してパブリッククラウドのAIモデルを統一的に呼び出します。",
|
||||
"K8097d6be": "OpenAIやDeepSeekなどのさまざまなAIモデルに迅速にアクセスできるオープンソースのワンストップAIゲートウェイおよびAPIポータルです。統一されたリクエスト形式を使用して、モデルの切り替えによるビジネスへの影響を回避し、企業レベルのAPIセキュリティ(認証/レート制限/センシティブワードフィルタリング)とリアルタイムの使用量監視を提供します。チーム内でのAPI共有やコラボレーションをサポートし、インターフェースのサブスクリプション認証を管理してAPIのセキュリティを確保します。",
|
||||
"Kf1ce5b3": "✨ Githubでスターを付けていただくか、製品フィードバックをお寄せください。",
|
||||
"K3af90490": "⚡ 以下の方法で、APIをすぐに公開して皆さんに利用してもらえます:",
|
||||
"K6b99dce8": "Ollama アドレス",
|
||||
"Kce2fcdbf": "権限がありません"
|
||||
}
|
||||
|
||||
@@ -761,5 +761,68 @@
|
||||
"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},影响较大,谨慎修改",
|
||||
"Kb8185132": "或",
|
||||
"K83829a3b": "模型异常",
|
||||
"Kb92fb02b": "部署失败",
|
||||
"Kcf8e3b18": "Tags",
|
||||
"K9ae48909": "API Key 负载",
|
||||
"K437724fc": "支持单个 API 模型供应商下创建多个 API Key 进行智能负载均衡",
|
||||
"K7d17707e": "模型灾备",
|
||||
"Kc007db4a": "仅支持 .png .jpg .jpeg .svg 格式的图片文件",
|
||||
"Kacf10c44": "Ollama 端点",
|
||||
"K8d4f5b44": "输入例如:https://www.apipark.com",
|
||||
"K481442d3": "配置 Ollama 服务",
|
||||
"Kf9b341e3": "如何部署 Ollama?",
|
||||
"K8632bef2": "模型部署服务未配置",
|
||||
"Kbbd8ce81": "配置服务",
|
||||
"K39a8d392": "导入OpenAPI文档,将现有系统的API发布到APIPark。",
|
||||
"Ka742e079": "添加公有云AI模型的 API Key,通过APIPark 统一调用公有云的AI模型。",
|
||||
"K8097d6be": "是开源的一站式 AI 网关与 API 门户,可快速接入 OpenAI/DeepSeek 等各类 AI 模型,通过统一请求格式避免模型切换对业务造成影响,提供企业级 API 安全防护(鉴权/限流/敏感词过滤)与实时用量监控,支持团队内 API 共享协作,管理接口订阅授权并保证您的API安全。",
|
||||
"Kf1ce5b3": "✨ 欢迎在 Github 为我们 Star 或提供产品反馈意见。",
|
||||
"K3af90490": "⚡您可快速通过以下方式开放API供大家使用:",
|
||||
"K6b99dce8": "Ollama 地址",
|
||||
"Kce2fcdbf": "暂无权限"
|
||||
}
|
||||
|
||||
@@ -830,5 +830,68 @@
|
||||
"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},這會產生較大的影響,請謹慎修改",
|
||||
"Kb8185132": "或",
|
||||
"K83829a3b": "模型異常",
|
||||
"Kb92fb02b": "部署失敗",
|
||||
"Kcf8e3b18": "標籤",
|
||||
"K9ae48909": "API Key 負載",
|
||||
"K437724fc": "支持在單個 API 模型供應商下創建多個 API Key 進行智能負載均衡",
|
||||
"K7d17707e": "模型災備",
|
||||
"Kc007db4a": "僅支持 .png, .jpg, .jpeg, .svg 格式的圖片文件",
|
||||
"Kacf10c44": "Ollama 端點",
|
||||
"K8d4f5b44": "輸入範例:https://www.apipark.com",
|
||||
"K481442d3": "配置 Ollama 服務",
|
||||
"Kf9b341e3": "如何部署 Ollama?",
|
||||
"K8632bef2": "模型部署服務未配置",
|
||||
"Kbbd8ce81": "配置服務",
|
||||
"K39a8d392": "導入 OpenAPI 文件,將現有系統的 API 發佈到 APIPark。",
|
||||
"Ka742e079": "添加公有雲 AI 模型的 API Key,通過 APIPark 統一調用公有雲的 AI 模型。",
|
||||
"K8097d6be": "是一個開源的一站式 AI 閘道和 API 入口網站,可快速接入 OpenAI/DeepSeek 等各類 AI 模型,通過統一的請求格式避免模型切換對業務造成影響,提供企業級 API 安全防護(鑑權/限流/敏感詞過濾)與實時用量監控,支持團隊內 API 共享協作,管理介面訂閱授權並保證您的 API 安全。",
|
||||
"Kf1ce5b3": "✨ 歡迎在 Github 為我們 Star 或提供產品反饋意見。",
|
||||
"K3af90490": "⚡ 您可以快速通過以下方式開放 API 供大家使用:",
|
||||
"K6b99dce8": "Ollama 地址",
|
||||
"Kce2fcdbf": "暫無權限"
|
||||
}
|
||||
|
||||
@@ -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) => ({
|
||||
|
||||
@@ -9,6 +9,7 @@ export type AiServiceConfigFieldType = {
|
||||
name?: string;
|
||||
id?: string;
|
||||
provider?:string
|
||||
model?:string
|
||||
prefix?:string;
|
||||
logo?:string;
|
||||
logoFile?:UploadFile;
|
||||
@@ -19,6 +20,7 @@ export type AiServiceConfigFieldType = {
|
||||
serviceType?:'public'|'inner';
|
||||
catalogue?:string | string[];
|
||||
approvalType?:string;
|
||||
providerType?:string
|
||||
};
|
||||
|
||||
export type AiServiceSubServiceTableListItem = {
|
||||
|
||||
@@ -799,5 +799,20 @@ export const routerMap: Map<string, RouterMapConfig> = new Map([
|
||||
}
|
||||
]
|
||||
}
|
||||
],
|
||||
[
|
||||
'loadBalancing',
|
||||
{
|
||||
type: 'module',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/loadBalancingLayout.tsx')),
|
||||
key: 'loadBalancing',
|
||||
children: [
|
||||
{
|
||||
path: 'list',
|
||||
lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/index.tsx')),
|
||||
key: 'loadBalancingList'
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
])
|
||||
|
||||
@@ -108,6 +108,12 @@ export const SYSTEM_TABLE_COLUMNS: PageProColumns<SystemTableListItem>[] = [
|
||||
dataIndex: ['team', 'name'],
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: '状态',
|
||||
width: 140,
|
||||
dataIndex: 'state',
|
||||
ellipsis: true
|
||||
},
|
||||
{
|
||||
title: 'API 数量',
|
||||
dataIndex: 'apiNum',
|
||||
|
||||
@@ -11,6 +11,7 @@ export type SystemTableListItem = {
|
||||
serviceNum: number,
|
||||
description:string;
|
||||
master:EntityItem;
|
||||
state: string
|
||||
service_kind:'ai'|'rest',
|
||||
createTime:string;
|
||||
};
|
||||
|
||||
@@ -746,6 +746,15 @@ p{
|
||||
padding:16px 20px !important
|
||||
}
|
||||
}
|
||||
.custom-steps .ant-steps-icon span {
|
||||
width: auto !important;
|
||||
}
|
||||
.custom-steps .ant-steps-item-content {
|
||||
margin-top: 0 !important;
|
||||
}
|
||||
.custom-steps .ant-steps-item-content .ant-steps-item-description {
|
||||
width: 138px !important;
|
||||
}
|
||||
|
||||
|
||||
.ant-modal-body .pr-PAGE_INSIDE_X{
|
||||
@@ -987,6 +996,13 @@ p{
|
||||
min-width:unset !important;
|
||||
}
|
||||
|
||||
.local-model-list .ant-pro-table .ant-table-body {
|
||||
overflow: hidden !important;
|
||||
}
|
||||
.local-model-list .ant-pro-table td {
|
||||
border-bottom: none !important;
|
||||
}
|
||||
|
||||
.table-border {
|
||||
.ant-table:not(.ant-table-bordered){
|
||||
border:1px solid var(--border-color) !important;
|
||||
|
||||
@@ -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?.model
|
||||
|
||||
if (replaceDefaultLlm) {
|
||||
setDefaultLlm((prev) => {
|
||||
const llmSetting = data.models?.find(
|
||||
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? localId)
|
||||
)
|
||||
return {
|
||||
...prev,
|
||||
defaultLlm: localId,
|
||||
provider: localId,
|
||||
name: aiServiceInfo?.name,
|
||||
config: llmSetting?.defaultConfig || '',
|
||||
type: 'local',
|
||||
...(llmSetting ?? {})
|
||||
} as AiProviderDefaultConfig & { config: string }
|
||||
})
|
||||
}
|
||||
if (setIcon) {
|
||||
setDefaultLlm((prev) => {
|
||||
const llmSetting = data.models?.find(
|
||||
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? localId)
|
||||
)
|
||||
return {
|
||||
...prev,
|
||||
logo: llmSetting?.logo,
|
||||
scopes: llmSetting?.scopes
|
||||
} as AiProviderDefaultConfig & { config: string }
|
||||
})
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => console.error(errorInfo))
|
||||
} else {
|
||||
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[]; provider: AiProviderDefaultConfig }>>('ai/provider/llms', {
|
||||
method: 'GET',
|
||||
eoParams: { provider: provider ?? aiServiceInfo?.provider?.id },
|
||||
eoTransformKeys: ['default_llm']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setLlmList(data.llms)
|
||||
if (replaceDefaultLlm) {
|
||||
setDefaultLlm((prev) => {
|
||||
const llmSetting = data.llms?.find(
|
||||
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? data.provider.defaultLlm)
|
||||
)
|
||||
return {
|
||||
...prev,
|
||||
defaultLlm: data.provider.defaultLlm,
|
||||
provider: data.provider.id,
|
||||
name: data.provider.name,
|
||||
config: llmSetting?.config || '',
|
||||
type: 'online',
|
||||
...(llmSetting ?? {})
|
||||
} as AiProviderDefaultConfig & { config: string }
|
||||
})
|
||||
}
|
||||
if (setIcon) {
|
||||
setDefaultLlm((prev) => {
|
||||
const llmSetting = data.llms?.find(
|
||||
(x: AiProviderLlmsItems) => x.id === (prev?.id ?? data.provider.defaultLlm)
|
||||
)
|
||||
return {
|
||||
...prev,
|
||||
logo: llmSetting?.logo,
|
||||
scopes: llmSetting?.scopes
|
||||
} as AiProviderDefaultConfig & { config: string }
|
||||
})
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => console.error(errorInfo))
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
@@ -237,13 +319,21 @@ const AiServiceInsideRouterCreate = () => {
|
||||
}
|
||||
|
||||
const handlerSubmit: () => Promise<boolean> | undefined = () => {
|
||||
return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string }) => {
|
||||
return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string, type: string, provider: string }) => {
|
||||
getDefaultModelConfig({
|
||||
provider: res.provider,
|
||||
id: res.id,
|
||||
type: res.type,
|
||||
replaceDefaultLlm: false,
|
||||
setIcon: true
|
||||
})
|
||||
setDefaultLlm(
|
||||
(prev) =>
|
||||
({
|
||||
...prev,
|
||||
provider: res.provider,
|
||||
id: res.id,
|
||||
type: res.type,
|
||||
config: res.config,
|
||||
logo: llmList?.find((x: AiProviderLlmsItems) => x.id === res.id)?.logo
|
||||
}) as AiProviderDefaultConfig & { config: string }
|
||||
|
||||
+199
-110
@@ -1,132 +1,221 @@
|
||||
import { Codebox } from "@common/components/postcat/api/Codebox"
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const"
|
||||
import { useFetch } from "@common/hooks/http"
|
||||
import { $t } from "@common/locales"
|
||||
import { AiProviderDefaultConfig, AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList"
|
||||
import { SimpleAiProviderItem } from "@core/pages/system/SystemConfig"
|
||||
import { Form, message, Select, Tag } from "antd"
|
||||
import { DefaultOptionType } from "antd/es/select"
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from "react"
|
||||
import { Codebox } from '@common/components/postcat/api/Codebox'
|
||||
import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
import { $t } from '@common/locales'
|
||||
import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/AiSettingList'
|
||||
import { LocalLlmType } from '@core/pages/loadBalancing/type'
|
||||
import { SimpleAiProviderItem } from '@core/pages/system/SystemConfig'
|
||||
import { Form, message, Select, Tag } from 'antd'
|
||||
import { DefaultOptionType } from 'antd/es/select'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
|
||||
export type AiServiceRouterModelConfigHandle = {
|
||||
save:()=>Promise<{id:string, config:string}>
|
||||
save: () => Promise<{ id: string; config: string, type: string, provider: string }>
|
||||
}
|
||||
|
||||
export type AiServiceRouterModelConfigProps = {
|
||||
entity:AiServiceRouterModelConfigField
|
||||
llmList:AiProviderLlmsItems[]
|
||||
entity: AiServiceRouterModelConfigField
|
||||
llmList: AiProviderLlmsItems[]
|
||||
}
|
||||
|
||||
type AiServiceRouterModelConfigField = {
|
||||
provider:string
|
||||
id:string
|
||||
config:string
|
||||
provider: string
|
||||
id: string
|
||||
config: string
|
||||
type: string
|
||||
}
|
||||
|
||||
const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle, AiServiceRouterModelConfigProps>((props, ref)=>{
|
||||
const [form] = Form.useForm();
|
||||
const {entity} = props
|
||||
const [providerList, setProviderList]= useState<DefaultOptionType[]>([])
|
||||
const [llmList, setLlmList]= useState<DefaultOptionType[]>([])
|
||||
const {fetchData} = useFetch()
|
||||
useImperativeHandle(ref, ()=>({
|
||||
save:form.validateFields
|
||||
})
|
||||
)
|
||||
const AiServiceRouterModelConfig = forwardRef<AiServiceRouterModelConfigHandle, AiServiceRouterModelConfigProps>(
|
||||
(props, ref) => {
|
||||
const [form] = Form.useForm()
|
||||
const { entity } = props
|
||||
const [providerList, setProviderList] = useState<DefaultOptionType[]>([])
|
||||
const [llmList, setLlmList] = useState<DefaultOptionType[]>([])
|
||||
const [modelType, setModelType] = useState<'online' | 'local'>('online')
|
||||
const { fetchData } = useFetch()
|
||||
useImperativeHandle(ref, () => ({
|
||||
save: form.validateFields
|
||||
}))
|
||||
const [modelTypeList] = useState([
|
||||
{
|
||||
label: $t('线上模型'),
|
||||
value: 'online'
|
||||
},
|
||||
{
|
||||
label: $t('本地模型'),
|
||||
value: 'local'
|
||||
}
|
||||
])
|
||||
|
||||
useEffect(()=>{
|
||||
/**
|
||||
* 获取本地模型列表
|
||||
* @param setDefaultValue
|
||||
*/
|
||||
const getLocalLlmList = (setDefaultValue?: boolean) => {
|
||||
fetchData<LocalLlmType[]>('simple/ai/models/local/configured', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: ['default_config']
|
||||
}).then((response) => {
|
||||
const models = response.data.models || []
|
||||
setLlmList(
|
||||
models.map((x: any) => ({
|
||||
...x,
|
||||
config: x.defaultConfig
|
||||
}))
|
||||
)
|
||||
if (setDefaultValue && models.length) {
|
||||
const id = models[0].id
|
||||
form.setFieldsValue({
|
||||
id,
|
||||
config: models.find((x) => x.id === id)?.defaultConfig
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* 切换模型类型
|
||||
* @param e
|
||||
*/
|
||||
const modelTypeChange = (e: string) => {
|
||||
setModelType(e as 'online' | 'local')
|
||||
setLlmList([])
|
||||
form.setFieldsValue({
|
||||
provider: '',
|
||||
id: '',
|
||||
config: '',
|
||||
type: e
|
||||
})
|
||||
if (e === 'online') {
|
||||
getProviderList(true)
|
||||
} else {
|
||||
getLocalLlmList(true)
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
setModelType(entity.type as 'online' | 'local')
|
||||
if (entity.type === 'online') {
|
||||
getProviderList()
|
||||
form.setFieldsValue(entity)
|
||||
},[])
|
||||
getLlmList(entity.provider, false)
|
||||
} else {
|
||||
getLocalLlmList()
|
||||
}
|
||||
form.setFieldsValue(entity)
|
||||
}, [])
|
||||
|
||||
const getProviderList = ()=>{
|
||||
setProviderList([])
|
||||
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers',{method:'GET',eoTransformKeys:[]}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setProviderList(data.providers?.filter(x=>x.configured)?.map((x:SimpleAiProviderItem)=>{return {...x,
|
||||
label: x.name, value:x.id
|
||||
}}))
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
const getProviderList = (setDefaultValue?: boolean) => {
|
||||
setProviderList([])
|
||||
fetchData<BasicResponse<{ providers: SimpleAiProviderItem[] }>>('simple/ai/providers/configured', {
|
||||
method: 'GET',
|
||||
eoTransformKeys: []
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setProviderList(
|
||||
data.providers
|
||||
?.map((x: SimpleAiProviderItem) => {
|
||||
return { ...x, label: x.name, value: x.id }
|
||||
})
|
||||
)
|
||||
if (setDefaultValue && data.providers.length) {
|
||||
const id = data.providers[0].id
|
||||
form.setFieldValue('provider', id)
|
||||
getLlmList(id)
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getLlmList = (provider: string, setDefaultValue = true) => {
|
||||
fetchData<BasicResponse<{ llms: AiProviderLlmsItems[]; provider: AiProviderDefaultConfig }>>('ai/provider/llms', {
|
||||
method: 'GET',
|
||||
eoParams: { provider },
|
||||
eoTransformKeys: ['default_llm']
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setLlmList(data.llms)
|
||||
if (setDefaultValue && data.llms.length) {
|
||||
form.setFieldsValue({
|
||||
id: data.provider.defaultLlm,
|
||||
config: data.llms.find((x) => x.id === data.provider.defaultLlm)?.config
|
||||
})
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch((errorInfo) => console.error(errorInfo))
|
||||
}
|
||||
|
||||
const getLlmList = (provider:string)=>{
|
||||
fetchData<BasicResponse<{llms:AiProviderLlmsItems[],provider:AiProviderDefaultConfig}>>('ai/provider/llms',{method:'GET',eoParams:{provider}, eoTransformKeys:['default_llm']}).then(response=>{
|
||||
const {code,data,msg} = response
|
||||
if(code === STATUS_CODE.SUCCESS){
|
||||
setLlmList(data.llms)
|
||||
form.setFieldsValue({
|
||||
id:data.provider.defaultLlm,
|
||||
config:data.llms.find(x=>x.id===data.provider.defaultLlm)?.config})
|
||||
}else{
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}).catch((errorInfo)=> console.error(errorInfo))
|
||||
}
|
||||
|
||||
const handleChangeProvider = (provider:string)=>{
|
||||
getLlmList(provider)
|
||||
}
|
||||
|
||||
useEffect(()=>{
|
||||
getLlmList(entity.provider)
|
||||
},[])
|
||||
|
||||
return (
|
||||
<Form
|
||||
layout='vertical'
|
||||
labelAlign='left'
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto flex flex-col h-full"
|
||||
name="aiServiceInsideRouterModalConfig"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t("模型供应商")}
|
||||
name="provider"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={providerList}
|
||||
onChange={(e)=>{
|
||||
handleChangeProvider(e)
|
||||
}}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form
|
||||
layout="vertical"
|
||||
labelAlign="left"
|
||||
scrollToFirstError
|
||||
form={form}
|
||||
className="mx-auto flex flex-col h-full"
|
||||
name="aiServiceInsideRouterModalConfig"
|
||||
autoComplete="off"
|
||||
>
|
||||
<Form.Item<AiServiceRouterModelConfigField> label={$t('模型类型')} name="type" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={modelTypeList}
|
||||
onChange={(e) => {
|
||||
modelTypeChange(e)
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
{modelType === 'online' && (
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t('模型供应商')}
|
||||
name="provider"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={providerList}
|
||||
onChange={(e) => {
|
||||
getLlmList(e)
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t("模型")}
|
||||
name="id"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Select className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={llmList?.map(x=>({
|
||||
value:x.id,
|
||||
label:<div className="flex items-center gap-[10px]">
|
||||
<span>{x.id}</span>
|
||||
{x?.scopes?.map(s=><Tag >{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>}))}
|
||||
onChange={(e)=>{
|
||||
form.setFieldValue('config',llmList.find(x=>x.id===e)?.config)
|
||||
}}>
|
||||
</Select>
|
||||
</Form.Item>
|
||||
<Form.Item<AiServiceRouterModelConfigField> label={$t('模型')} name="id" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.select)}
|
||||
options={
|
||||
llmList?.map((x) => ({
|
||||
value: x.id,
|
||||
label: (
|
||||
<div className="flex items-center gap-[10px]" key={x.id}>
|
||||
<span>{x.id}</span>
|
||||
{modelType === 'online' && x?.scopes?.map((s: any) => <Tag>{s?.toLocaleUpperCase()}</Tag>)}
|
||||
</div>
|
||||
)
|
||||
}))
|
||||
}
|
||||
onChange={(e) => {
|
||||
form.setFieldValue('config', llmList.find((x) => x.id === e)?.config)
|
||||
}}
|
||||
></Select>
|
||||
</Form.Item>
|
||||
|
||||
<Form.Item<AiServiceRouterModelConfigField>
|
||||
label={$t("参数")}
|
||||
name="config"
|
||||
>
|
||||
<Codebox editorTheme="vs-dark"
|
||||
width="100%" height="300px" language='json' enableToolbar={false} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Form.Item<AiServiceRouterModelConfigField> label={$t('参数')} name="config">
|
||||
<Codebox editorTheme="vs-dark" width="100%" height="300px" language="json" enableToolbar={false} />
|
||||
</Form.Item>
|
||||
</Form>
|
||||
)
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
export default AiServiceRouterModelConfig
|
||||
export default AiServiceRouterModelConfig
|
||||
|
||||
@@ -5,6 +5,7 @@ import { useEffect, useState } from 'react'
|
||||
import { useSearchParams } from 'react-router-dom'
|
||||
import { AiSettingProvider } from './contexts/AiSettingContext'
|
||||
import OnlineModelList from './OnlineModelList'
|
||||
import LocalModelList from './LocalModelList'
|
||||
|
||||
const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const
|
||||
|
||||
@@ -47,7 +48,9 @@ const AiSettingContent = () => {
|
||||
{
|
||||
key: 'config',
|
||||
label: $t('本地模型'),
|
||||
children: <div className="overflow-auto" style={CONTENT_STYLE}></div>
|
||||
children: <div className="overflow-auto" style={CONTENT_STYLE}>
|
||||
<LocalModelList />
|
||||
</div>
|
||||
}
|
||||
]}
|
||||
/>
|
||||
|
||||
@@ -2,61 +2,52 @@ 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, Select, Switch, Tag } from 'antd'
|
||||
import { forwardRef, useEffect, useImperativeHandle, useState } from 'react'
|
||||
import { AiProviderLlmsItems, ModelDetailData } from './types'
|
||||
import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd'
|
||||
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?: { id: string | undefined; defaultLlm: string | undefined }
|
||||
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>(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 = () => {
|
||||
if (!selectedProvider) return
|
||||
// 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: selectedProvider }
|
||||
eoParams: { provider: id || localEntity?.id }
|
||||
})
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
@@ -70,75 +61,182 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
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']
|
||||
|
||||
/**
|
||||
* 获取团队选项列表
|
||||
* @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 }
|
||||
})
|
||||
message.destroy()
|
||||
if (code !== STATUS_CODE.SUCCESS) {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return
|
||||
setTeamList(teamOptionList)
|
||||
if (form.getFieldValue('team') === undefined && data.teams?.length) {
|
||||
form.setFieldValue('team', data.teams[0].id)
|
||||
}
|
||||
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
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
return []
|
||||
}
|
||||
form.setFieldsValue({
|
||||
defaultLlm: entity?.defaultLlm,
|
||||
config: '',
|
||||
enable: true
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取未配置模型提供者列表
|
||||
*/
|
||||
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: fieldsValue.defaultLlm,
|
||||
config: fieldsValue!.config ? JSON.stringify(JSON.parse(fieldsValue!.config), null, 2) : '',
|
||||
enable: fieldsValue.status === 'enabled'
|
||||
})
|
||||
} catch (e) {
|
||||
form.setFieldsValue({
|
||||
defaultLlm: localEntity?.defaultLlm,
|
||||
config: '',
|
||||
enable: true
|
||||
})
|
||||
}
|
||||
}
|
||||
useEffect(() => {
|
||||
if (localEntity?.id) {
|
||||
getModelConfig(localEntity.id)
|
||||
setFormFieldsValue(localEntity)
|
||||
} else {
|
||||
getModelProviderList()
|
||||
source && getTeamOptionList()
|
||||
}
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* 部署 AI 服务
|
||||
*/
|
||||
const deployAIServer: () => Promise<boolean | string> = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
form
|
||||
.validateFields()
|
||||
.then((value) => {
|
||||
const finalValue = {
|
||||
config: value.config,
|
||||
model: value.defaultLlm,
|
||||
team: value.team,
|
||||
provider: localEntity?.id
|
||||
}
|
||||
fetchData<BasicResponse<null>>('quick/service/ai', {
|
||||
method: 'POST',
|
||||
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))
|
||||
})
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getUnconfiguredProviders()
|
||||
}, [])
|
||||
|
||||
useEffect(() => {
|
||||
getLlmList()
|
||||
}, [selectedProvider])
|
||||
|
||||
useEffect(() => {
|
||||
initData()
|
||||
}, [entity])
|
||||
|
||||
/**
|
||||
* 保存
|
||||
* @returns
|
||||
*/
|
||||
const save: () => Promise<boolean | string> = () => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
form
|
||||
.validateFields()
|
||||
.then((value) => {
|
||||
const finalValue = {
|
||||
...value,
|
||||
priority: Math.max(1, value.priority)
|
||||
...value
|
||||
}
|
||||
|
||||
fetchData<BasicResponse<null>>('ai/provider/config', {
|
||||
method: entity ? 'PUT' : 'POST',
|
||||
eoParams: { provider: selectedProvider },
|
||||
eoBody: finalValue,
|
||||
eoTransformKeys: ['defaultLlm']
|
||||
})
|
||||
.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))
|
||||
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) {
|
||||
@@ -155,7 +253,8 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
}
|
||||
|
||||
useImperativeHandle(ref, () => ({
|
||||
save
|
||||
save,
|
||||
deployAIServer
|
||||
}))
|
||||
|
||||
return (
|
||||
@@ -169,13 +268,24 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
autoComplete="off"
|
||||
disabled={readOnly}
|
||||
>
|
||||
{!entity?.id && (
|
||||
<Form.Item label={$t('供应商')} name="provider" rules={[{ required: true, message: $t('请选择供应商') }]}>
|
||||
{modelMode === 'manual' && (
|
||||
<Form.Item<ModelDetailData> label={$t('模型供应商')} name="modelMode" rules={[{ required: true }]}>
|
||||
<Select
|
||||
placeholder={$t('请选择供应商')}
|
||||
onChange={(value) => setSelectedProvider(value)}
|
||||
options={providers.map((p) => ({ label: p.name, value: p.id }))}
|
||||
/>
|
||||
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 }]}>
|
||||
@@ -194,7 +304,13 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
}))}
|
||||
></Select>
|
||||
</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"
|
||||
@@ -205,14 +321,14 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
enableToolbar={false}
|
||||
/>
|
||||
</Form.Item>
|
||||
{entity?.id && (
|
||||
{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>
|
||||
{status === 'enabled' && <Tag color="success">{$t('正常')}</Tag>}
|
||||
{status === 'disabled' && <Tag color="warning">{$t('停用')}</Tag>}
|
||||
{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
|
||||
@@ -225,7 +341,7 @@ const AiSettingModalContent = forwardRef<AiSettingModalContentHandle, AiSettingM
|
||||
/>
|
||||
</Form.Item>
|
||||
</div>
|
||||
{(status === 'enabled' && !enableState) || (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,85 @@
|
||||
import { forwardRef, useEffect, useImperativeHandle } from 'react'
|
||||
import { App, Divider, Form, Space, Switch, Tag, Input } from 'antd'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { $t } from '@common/locales'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
|
||||
export type ConfigureOllamaServiceHandle = {
|
||||
save: () => Promise<boolean | string>
|
||||
}
|
||||
|
||||
const ConfigureOllamaService = forwardRef<ConfigureOllamaServiceHandle, any>((props, ref) => {
|
||||
const { address = '' } = props
|
||||
const [form] = Form.useForm()
|
||||
const { fetchData } = useFetch()
|
||||
const { message } = App.useApp()
|
||||
|
||||
useEffect(() => {
|
||||
form.setFieldsValue({ address })
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* 保存
|
||||
* @returns
|
||||
*/
|
||||
const save: () => Promise<boolean | string> = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
try {
|
||||
form
|
||||
.validateFields()
|
||||
.then((value) => {
|
||||
fetchData<BasicResponse<null>>('model/local/source/ollama', {
|
||||
method: 'PUT',
|
||||
eoBody: { address: value.address }
|
||||
})
|
||||
.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
|
||||
name="address"
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
className="p-4 bg-white rounded-lg"
|
||||
label={$t('Ollama 地址')}
|
||||
>
|
||||
<Input
|
||||
placeholder={$t('输入例如:https://www.apipark.com')}
|
||||
value={address}
|
||||
onChange={(e) => form.setFieldValue('address', e.target.value)}
|
||||
/>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
</WithPermission>
|
||||
)
|
||||
})
|
||||
|
||||
export default ConfigureOllamaService
|
||||
@@ -0,0 +1,469 @@
|
||||
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, Button } 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'
|
||||
import { Icon } from '@iconify/react/dist/iconify.js'
|
||||
import ConfigureOllamaService, { ConfigureOllamaServiceHandle } from './ConfigureOllamaService'
|
||||
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 ConfigureOllamaServiceRef = useRef<ConfigureOllamaServiceHandle>()
|
||||
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 [ollamaAddress, setOllamaAddress] = useState<string>('')
|
||||
|
||||
useEffect(() => {
|
||||
getOllamaData()
|
||||
}, [])
|
||||
|
||||
const configureService = (address?: string) => {
|
||||
modal.confirm({
|
||||
title: $t('配置 Ollama 服务'),
|
||||
content: <ConfigureOllamaService ref={ConfigureOllamaServiceRef} address={address}></ConfigureOllamaService>,
|
||||
onOk: () => {
|
||||
return ConfigureOllamaServiceRef.current?.save().then((res) => {
|
||||
if (res === true) {
|
||||
getOllamaData()
|
||||
}
|
||||
})
|
||||
},
|
||||
footer: (_, { OkBtn, CancelBtn }) => {
|
||||
return (
|
||||
<div className="flex justify-between items-center">
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
href="https://ollama.com/download/linux"
|
||||
className="flex items-center gap-[8px]"
|
||||
>
|
||||
<span>{$t('如何部署 Ollama?')}</span>
|
||||
</a>
|
||||
<div>
|
||||
<CancelBtn />
|
||||
<OkBtn />
|
||||
</div>
|
||||
</div>
|
||||
)
|
||||
},
|
||||
width: 600,
|
||||
okText: $t('确认'),
|
||||
cancelText: $t('取消'),
|
||||
closable: true,
|
||||
icon: <></>
|
||||
})
|
||||
}
|
||||
|
||||
const customEmptyRender = () => {
|
||||
return (
|
||||
<>
|
||||
<div>
|
||||
<Icon className="align-sub mr-[5px]" icon="ph:hard-drives-light" width="50" height="50" />
|
||||
<div>{$t('模型部署服务未配置')}</div>
|
||||
<WithPermission access="system.devops.ai_provider.edit">
|
||||
<Button type="primary" className="mt-[10px]" onClick={() => configureService()}>
|
||||
{$t('配置服务')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
|
||||
const getOllamaData = async () => {
|
||||
const response = await fetchData<BasicResponse<{ data: any[] }>>('model/local/source/ollama', {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
setOllamaAddress(response.data?.config?.address || '')
|
||||
pageListRef.current?.reload()
|
||||
} else {
|
||||
message.error(response.msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}
|
||||
|
||||
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 {
|
||||
if (!ollamaAddress) {
|
||||
return {
|
||||
data: [],
|
||||
success: true,
|
||||
total: 0
|
||||
}
|
||||
}
|
||||
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: 2,
|
||||
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 updateFooter = () => {
|
||||
record.state = 'error'
|
||||
modalInstance.update({})
|
||||
}
|
||||
let cancelCb: () => void = () => {}
|
||||
const cancel = (cancel: () => void) => {
|
||||
cancelCb = cancel
|
||||
}
|
||||
const modalInstance = modal.confirm({
|
||||
title: $t('部署过程'),
|
||||
content: (
|
||||
<ServiceDeployment record={record} closeModal={closeModal} updateFooter={updateFooter} cancelCb={cancel} />
|
||||
),
|
||||
footer: () => {
|
||||
return <LogsFooter record={record} closeModal={closeModal} />
|
||||
},
|
||||
afterClose: () => {
|
||||
cancelCb()
|
||||
},
|
||||
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)
|
||||
}
|
||||
}}
|
||||
>
|
||||
{$t(stateColumnMap[entity?.state as string]?.text || '-')}
|
||||
</span>
|
||||
)
|
||||
},
|
||||
{
|
||||
title: $t('Apis'),
|
||||
dataIndex: 'apiCount',
|
||||
width: 100,
|
||||
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"
|
||||
tableClass="local-model-list"
|
||||
customEmptyRender={!ollamaAddress ? customEmptyRender : undefined}
|
||||
request={requestList}
|
||||
onSearchWordChange={(e) => {
|
||||
setSearchWord(e.target.value)
|
||||
pageListRef.current?.reload()
|
||||
}}
|
||||
beforeNewBtn={[
|
||||
<WithPermission access="system.devops.ai_provider.edit">
|
||||
<Button className="mr-btnbase" key="removeFromDep" onClick={() => configureService(ollamaAddress)}>
|
||||
{$t('配置服务')}
|
||||
</Button>
|
||||
</WithPermission>
|
||||
]}
|
||||
showPagination={true}
|
||||
searchPlaceholder={$t('请输入名称搜索')}
|
||||
columns={columns}
|
||||
addNewBtnTitle={$t('部署模型')}
|
||||
onAddNewBtnClick={handleAdd}
|
||||
addNewBtnAccess="system.devops.ai_provider.edit"
|
||||
addNewBtnDisabled={!ollamaAddress}
|
||||
/>
|
||||
)
|
||||
}
|
||||
|
||||
export default LocalModelList
|
||||
@@ -11,39 +11,61 @@ import { AiSettingListItem, ModelListData } from './types'
|
||||
|
||||
const OnlineModelList: React.FC = () => {
|
||||
const pageListRef = useRef<ActionType>(null)
|
||||
const { message } = App.useApp()
|
||||
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)
|
||||
openConfigModal({ id: record.id, defaultLlm: record.defaultLlm } as AiSettingListItem, () => {
|
||||
pageListRef.current?.reload()
|
||||
})
|
||||
}
|
||||
|
||||
const handleAdd = () => {
|
||||
openConfigModal()
|
||||
openConfigModal(undefined, () => {
|
||||
pageListRef.current?.reload()
|
||||
})
|
||||
}
|
||||
|
||||
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/'
|
||||
})
|
||||
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: <></>
|
||||
})
|
||||
|
||||
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) => {
|
||||
@@ -55,8 +77,7 @@ const OnlineModelList: React.FC = () => {
|
||||
keyword: searchWord,
|
||||
page: params.current
|
||||
},
|
||||
eoTransformKeys: ['default_llm']
|
||||
// eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/'
|
||||
eoTransformKeys: ['default_llm', 'api_count', 'key_count', 'can_delete']
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
@@ -107,8 +128,10 @@ const OnlineModelList: React.FC = () => {
|
||||
<TableBtnWithPermission
|
||||
access="system.devops.ai_provider.edit"
|
||||
key="delete"
|
||||
disabled={!entity?.canDelete}
|
||||
tooltip={$t('当前模型为最后一个模型,不支持删除')}
|
||||
btnType="delete"
|
||||
onClick={() => handleDelete(entity.id as string)}
|
||||
onClick={() => handleDelete(entity.id as string, entity.apiCount)}
|
||||
btnTitle={$t('删除')}
|
||||
/>
|
||||
]
|
||||
@@ -133,15 +156,50 @@ const OnlineModelList: React.FC = () => {
|
||||
},
|
||||
{
|
||||
title: $t('默认模型'),
|
||||
ellipsis: true,
|
||||
dataIndex: 'defaultLlm'
|
||||
},
|
||||
{
|
||||
title: $t('Apis'),
|
||||
dataIndex: 'api_count'
|
||||
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: 'key_count'
|
||||
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
|
||||
]
|
||||
@@ -158,6 +216,7 @@ const OnlineModelList: React.FC = () => {
|
||||
showPagination={true}
|
||||
searchPlaceholder={$t('请输入名称搜索')}
|
||||
columns={columns}
|
||||
addNewBtnAccess="system.devops.ai_provider.edit"
|
||||
addNewBtnTitle={$t('添加模型')}
|
||||
onAddNewBtnClick={handleAdd}
|
||||
/>
|
||||
|
||||
@@ -8,7 +8,7 @@ import AiSettingModalContent, { AiSettingModalContentHandle } from '../AiSetting
|
||||
import { AiSettingListItem } from '../types'
|
||||
|
||||
interface AiSettingContextType {
|
||||
openConfigModal: (entity?: AiSettingListItem) => Promise<void>
|
||||
openConfigModal: (entity?: AiSettingListItem, callback?: () => void) => Promise<void>
|
||||
}
|
||||
|
||||
const AiSettingContext = createContext<AiSettingContextType | undefined>(undefined)
|
||||
@@ -17,14 +17,23 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||
const { modal } = App.useApp()
|
||||
const { aiConfigFlushed, setAiConfigFlushed, accessData } = useGlobalContext()
|
||||
const modalRef = useRef<AiSettingModalContentHandle>()
|
||||
const entityData = useRef<any>(null)
|
||||
|
||||
const openConfigModal = async (entity?: AiSettingListItem) => {
|
||||
modal.confirm({
|
||||
const openConfigModal = async (entity?: AiSettingListItem, callback?: () => void) => {
|
||||
// 更新弹窗
|
||||
const updateEntityData = (data: any) => {
|
||||
entityData.current = data
|
||||
// 更新弹窗
|
||||
modalInstance.update({})
|
||||
}
|
||||
const modalInstance = modal.confirm({
|
||||
title: $t('模型配置'),
|
||||
content: (
|
||||
<AiSettingModalContent
|
||||
ref={modalRef}
|
||||
entity={{ id: entity?.id, defaultLlm: entity?.defaultLlm }}
|
||||
modelMode={entity ? 'auto' : 'manual'}
|
||||
updateEntityData={updateEntityData}
|
||||
readOnly={!checkAccess('system.devops.ai_provider.edit', accessData)}
|
||||
/>
|
||||
),
|
||||
@@ -32,6 +41,7 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||
return modalRef.current?.save().then((res) => {
|
||||
if (res === true) {
|
||||
setAiConfigFlushed(!aiConfigFlushed)
|
||||
callback?.()
|
||||
}
|
||||
})
|
||||
},
|
||||
@@ -43,10 +53,10 @@ export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ chi
|
||||
<a
|
||||
target="_blank"
|
||||
rel="noopener noreferrer"
|
||||
// href={data.provider.getApikeyUrl}
|
||||
href={entityData.current?.getApikeyUrl}
|
||||
className="flex items-center gap-[8px]"
|
||||
>
|
||||
{/* <span>{$t('从 (0) 获取 API KEY', [data.provider.name])}</span> */}
|
||||
<span>{$t('从 (0) 获取 API KEY', [entityData.current?.name])}</span>
|
||||
<Icon icon="ic:baseline-open-in-new" width={16} height={16} />
|
||||
</a>
|
||||
<div>
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
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
|
||||
@@ -8,14 +9,25 @@ export interface KeyData {
|
||||
}
|
||||
|
||||
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 AISettingEntityItem {
|
||||
id: string | undefined
|
||||
status?: ModelStatus | undefined
|
||||
defaultLlm: string | undefined
|
||||
}
|
||||
export interface ModelDetailData extends ModelListData {
|
||||
enable: boolean
|
||||
|
||||
@@ -0,0 +1,234 @@
|
||||
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 { useEffect, useRef, useState } 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'
|
||||
import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const'
|
||||
import { useFetch } from '@common/hooks/http'
|
||||
|
||||
export const AIModelGuide = () => {
|
||||
const { message, 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 { fetchData } = useFetch()
|
||||
const [ollamaAddress, setOllamaAddress] = useState<string>('')
|
||||
|
||||
const dumpServerPage = () => {
|
||||
navigateTo('/service/list')
|
||||
}
|
||||
|
||||
/**
|
||||
* rest 服务卡片点击事件
|
||||
*/
|
||||
const restCardClick = async () => {
|
||||
const permission = checkAccess('system.workspace.service.edit', accessData)
|
||||
if (!permission) {
|
||||
return message.warning($t('暂无权限'))
|
||||
}
|
||||
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 permission = checkAccess('system.devops.ai_provider.edit', accessData)
|
||||
if (!permission) {
|
||||
return message.warning($t('暂无权限'))
|
||||
}
|
||||
// 更新弹窗
|
||||
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: <></>
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
const getOllamaData = async () => {
|
||||
const response = await fetchData<BasicResponse<{ data: any[] }>>('model/local/source/ollama', {
|
||||
method: 'GET'
|
||||
})
|
||||
|
||||
if (response.code === STATUS_CODE.SUCCESS) {
|
||||
setOllamaAddress(response.data?.config?.address || '')
|
||||
} else {
|
||||
message.error(response.msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
}
|
||||
|
||||
useEffect(() => {
|
||||
getOllamaData()
|
||||
}, [])
|
||||
|
||||
/**
|
||||
* 本地部署 AI 并生成 API
|
||||
*/
|
||||
const localModelCardClick = async () => {
|
||||
const permission = checkAccess('system.devops.ai_provider.edit', accessData)
|
||||
if (!permission) {
|
||||
return message.warning($t('暂无权限'))
|
||||
}
|
||||
if (!ollamaAddress) {
|
||||
navigateTo('/aisetting?status=unconfigure')
|
||||
return
|
||||
}
|
||||
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()
|
||||
const permission = checkAccess('system.devops.ai_provider.edit', accessData)
|
||||
if (!permission) {
|
||||
return message.warning($t('暂无权限'))
|
||||
}
|
||||
if (!ollamaAddress) {
|
||||
navigateTo('/aisetting?status=unconfigure')
|
||||
return
|
||||
}
|
||||
await deployLocalModel({
|
||||
modelID: 'deepseek-r1'
|
||||
})
|
||||
dumpServerPage()
|
||||
}
|
||||
|
||||
const cardList = [
|
||||
{
|
||||
imgSrc: restAPIPic,
|
||||
title: $t('添加 Rest 服务'),
|
||||
description: $t('导入OpenAPI文档,将现有系统的API发布到APIPark。'),
|
||||
click: restCardClick
|
||||
},
|
||||
{
|
||||
imgSrc: onlineAIPic,
|
||||
title: $t('添加在线 AI API'),
|
||||
description: $t('添加公有云AI模型的 API Key,通过APIPark 统一调用公有云的AI模型。'),
|
||||
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 (
|
||||
<>
|
||||
<p>{$t('⚡您可快速通过以下方式开放API供大家使用:')}</p>
|
||||
<div className="mb-[30px] pt-[25px] flex justify-between space-x-4">
|
||||
{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%)] rounded-[10px] overflow-visible cursor-pointer flex-1 transition duration-500 hover:shadow-[0_5px_20px_0_rgba(0,0,0,0.15)] hover:scale-[1.05]"
|
||||
classNames={{
|
||||
header: 'border-b-[0px] p-[20px] pb-[10px] text-[14px] font-normal',
|
||||
body: 'p-[20px] pt-[50px] pb-[50px] text-[12px] text-[#666] text-center'
|
||||
}}
|
||||
onClick={item.click}
|
||||
>
|
||||
<img src={item.imgSrc} alt="" width={60} height={60} />
|
||||
<p className="text-[13px] font-bold text-black mt-[10px] mb-[10px]">{item.title}</p>
|
||||
<p className="break-words mb-[10px]">{item.description}</p>
|
||||
{item.bottomRender ? item.bottomRender : null}
|
||||
</Card>
|
||||
))}
|
||||
</div>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -1,232 +1,327 @@
|
||||
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>
|
||||
<span className="font-bold">🦄 APIPark </span>
|
||||
{$t(
|
||||
'是开源的一站式 AI 网关与 API 门户,可快速接入 OpenAI/DeepSeek 等各类 AI 模型,通过统一请求格式避免模型切换对业务造成影响,提供企业级 API 安全防护(鉴权/限流/敏感词过滤)与实时用量监控,支持团队内 API 共享协作,管理接口订阅授权并保证您的API安全。'
|
||||
)}
|
||||
</p>
|
||||
<p>
|
||||
{$t('✨ 欢迎在 Github 为我们 Star 或提供产品反馈意见。')}
|
||||
<span className="font-bold">
|
||||
{$t('点击这里')}
|
||||
<span className="align-middle leading-[16px]">
|
||||
|
||||
<Icon icon="pajamas:arrow-right" width="16" height="16" />
|
||||
|
||||
</span>
|
||||
<a className="align-text-top" href="https://github.com/APIParkLab/APIPark" target="_blank">
|
||||
<img src="https://img.shields.io/github/stars/APIParkLab/APIPark?style=social" alt="" />
|
||||
</a>
|
||||
<span className="align-middle leading-[16px]">
|
||||
|
||||
<Icon icon="pajamas:arrow-right" width="16" height="16" />
|
||||
|
||||
</span>
|
||||
{$t('点击')}
|
||||
|
||||
<span className="align-middle leading-[16px]">
|
||||
<Icon icon="emojione:star" width="16" height="16" />
|
||||
</span>
|
||||
Star
|
||||
</span>
|
||||
</p>
|
||||
</div>
|
||||
}
|
||||
showBorder={false}
|
||||
scrollPage={false}
|
||||
scrollInsidePage={true}
|
||||
customPadding={true}
|
||||
headerClassName="pt-[30px] pl-[40px]"
|
||||
contentClassName=" w-full pr-PAGE_INSIDE_X pb-PAGE_INSIDE_B pl-[40px]"
|
||||
>
|
||||
<AIModelGuide></AIModelGuide>
|
||||
<div className="flex flex-col gap-[15px] pb-PAGE_INSIDE_B">
|
||||
{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('Tags')} 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
|
||||
@@ -338,10 +338,10 @@ const KeySettings: React.FC = () => {
|
||||
return (
|
||||
<InsidePage
|
||||
className="overflow-y-auto gap-4 pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X"
|
||||
pageTitle={$t('APIKey 资源池')}
|
||||
pageTitle={$t('API Key 负载')}
|
||||
description={
|
||||
<>
|
||||
{$t('支持单个 API 模型供应商下创建多个 APIKey APIKey 进行智能负载均衡')}
|
||||
{$t('支持单个 API 模型供应商下创建多个 API Key 进行智能负载均衡')}
|
||||
<div className="mt-4">
|
||||
<AIProviderSelect
|
||||
value={selectedProvider}
|
||||
|
||||
@@ -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,12 +1,6 @@
|
||||
import { LoadingOutlined } from '@ant-design/icons'
|
||||
import WithPermission from '@common/components/aoplatform/WithPermission.tsx'
|
||||
import {
|
||||
BasicResponse,
|
||||
DELETE_TIPS,
|
||||
PLACEHOLDER,
|
||||
RESPONSE_TIPS,
|
||||
STATUS_CODE
|
||||
} from '@common/const/const.tsx'
|
||||
import { BasicResponse, DELETE_TIPS, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx'
|
||||
import { EntityItem, MemberItem, SimpleTeamItem } from '@common/const/type.ts'
|
||||
import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx'
|
||||
import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx'
|
||||
@@ -50,15 +44,10 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
const [tagOptionList, setTagOptionList] = useState<DefaultOptionType[]>([])
|
||||
const [serviceClassifyOptionList, setServiceClassifyOptionList] = useState<DefaultOptionType[]>()
|
||||
const [uploadLoading, setUploadLoading] = useState<boolean>(false)
|
||||
const {
|
||||
checkPermission,
|
||||
accessInit,
|
||||
getGlobalAccessData,
|
||||
state,
|
||||
aiConfigFlushed,
|
||||
setAiConfigFlushed
|
||||
} = useGlobalContext()
|
||||
const { checkPermission, accessInit, getGlobalAccessData, state, aiConfigFlushed, setAiConfigFlushed } =
|
||||
useGlobalContext()
|
||||
const [providerOptionList, setProviderOptionList] = useState<DefaultOptionType[]>()
|
||||
const [modelList, setModelList] = useState<DefaultOptionType[]>()
|
||||
const location = useLocation()
|
||||
const currentUrl = location.pathname
|
||||
|
||||
@@ -74,20 +63,83 @@ 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: []
|
||||
}).then(response => {
|
||||
eoTransformKeys: [],
|
||||
eoParams: { all: true }
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const configuredProvider = data.providers
|
||||
?.filter(x => x.configured)
|
||||
?.filter((x) => x.configured)
|
||||
?.map((x: SimpleAiProviderItem) => {
|
||||
return { ...x, label: x.name, value: x.id }
|
||||
})
|
||||
setProviderOptionList(configuredProvider)
|
||||
if (!serviceId && configuredProvider.length > 0) {
|
||||
form.setFieldsValue({ provider: configuredProvider[0]?.id })
|
||||
if (configuredProvider[0]?.type === 'local') {
|
||||
getLocalModelList()
|
||||
} else {
|
||||
getOnlineModelList(configuredProvider[0]?.id)
|
||||
}
|
||||
}
|
||||
if (serviceId && configuredProvider.length > 0) {
|
||||
const providerID = form.getFieldValue('provider')
|
||||
const provider = configuredProvider?.find((item: any) => item.id === providerID)
|
||||
if (provider?.type === 'local') {
|
||||
getLocalModelList(false)
|
||||
} else {
|
||||
getOnlineModelList(provider?.id, false)
|
||||
}
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const modelProviderChange = (id: string) => {
|
||||
const provider = providerOptionList?.find((item) => item.id === id)
|
||||
if (provider?.type === 'local') {
|
||||
getLocalModelList()
|
||||
} else {
|
||||
getOnlineModelList(provider?.id)
|
||||
}
|
||||
}
|
||||
|
||||
const getLocalModelList = (setDefaultLlm = true) => {
|
||||
fetchData<BasicResponse<{ providers: any[] }>>('simple/ai/models/local/configured', {
|
||||
method: 'GET'
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const localModelList = data.models?.map((x: any) => {
|
||||
return { ...x, label: x.name, value: x.id }
|
||||
})
|
||||
setModelList(localModelList)
|
||||
if (setDefaultLlm && localModelList.length > 0) {
|
||||
form.setFieldsValue({ model: localModelList[0]?.id })
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
}
|
||||
const getOnlineModelList = (id: string, setDefaultLlm = true) => {
|
||||
fetchData<BasicResponse<{ providers: any[] }>>('ai/provider/llms', {
|
||||
method: 'GET',
|
||||
eoParams: { provider: id },
|
||||
eoTransformKeys: ['default_llm']
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
const localModelList = data.llms?.map((x: any) => {
|
||||
return { ...x, label: x.id, value: x.id }
|
||||
})
|
||||
setModelList(localModelList)
|
||||
if (setDefaultLlm && localModelList.length > 0) {
|
||||
form.setFieldsValue({ model: localModelList[0]?.id })
|
||||
}
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -136,9 +188,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
}
|
||||
|
||||
const uploadButton = (
|
||||
<div>
|
||||
{uploadLoading ? <LoadingOutlined /> : <Icon icon="ic:baseline-add" width="24" height="24" />}
|
||||
</div>
|
||||
<div>{uploadLoading ? <LoadingOutlined /> : <Icon icon="ic:baseline-add" width="24" height="24" />}</div>
|
||||
)
|
||||
|
||||
const getTagAndServiceClassifyList = () => {
|
||||
@@ -146,7 +196,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
setServiceClassifyOptionList([])
|
||||
fetchData<BasicResponse<{ catalogues: CategorizesType[]; tags: EntityItem[] }>>('catalogues', {
|
||||
method: 'GET'
|
||||
}).then(response => {
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTagOptionList(
|
||||
@@ -174,7 +224,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
method: 'GET',
|
||||
eoParams: { team: teamId, service: serviceId },
|
||||
eoTransformKeys: ['team_id', 'service_type', 'approval_type', 'service_kind']
|
||||
}).then(response => {
|
||||
}).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTimeout(() => {
|
||||
@@ -195,6 +245,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
})
|
||||
setImageBase64(data.service.logo)
|
||||
setShowClassify(data.service.serviceType === 'public')
|
||||
getProviderOptionList()
|
||||
}, 0)
|
||||
} else {
|
||||
message.error(msg || $t(RESPONSE_TIPS.error))
|
||||
@@ -203,21 +254,19 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
}
|
||||
|
||||
const onFinish: () => Promise<boolean | string> = () => {
|
||||
return form.validateFields().then(value => {
|
||||
return form.validateFields().then((value) => {
|
||||
return fetchData<BasicResponse<{ service: { id: string } }>>(
|
||||
serviceId === undefined ? 'team/service' : 'service/info',
|
||||
{
|
||||
method: serviceId === undefined ? 'POST' : 'PUT',
|
||||
eoParams: {
|
||||
...(serviceId === undefined
|
||||
? { team: value.team }
|
||||
: { service: serviceId, team: teamId })
|
||||
...(serviceId === undefined ? { team: value.team } : { service: serviceId, team: teamId })
|
||||
},
|
||||
eoBody: { ...value, prefix: value.prefix?.trim() },
|
||||
eoTransformKeys: ['serviceType', 'approvalType', 'serviceKind']
|
||||
}
|
||||
)
|
||||
.then(response => {
|
||||
.then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
@@ -228,7 +277,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
return Promise.reject(msg || $t(RESPONSE_TIPS.error))
|
||||
}
|
||||
})
|
||||
.catch(errorInfo => {
|
||||
.catch((errorInfo) => {
|
||||
return Promise.reject(errorInfo)
|
||||
})
|
||||
})
|
||||
@@ -240,7 +289,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
fetchData<BasicResponse<{ teams: SimpleTeamItem[] }>>(
|
||||
!checkPermission('system.workspace.team.view_all') ? 'simple/teams/mine' : 'simple/teams',
|
||||
{ method: 'GET', eoTransformKeys: [] }
|
||||
).then(response => {
|
||||
).then((response) => {
|
||||
const { code, data, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
setTeamOptionList(
|
||||
@@ -261,7 +310,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
fetchData<BasicResponse<null>>('team/service', {
|
||||
method: 'DELETE',
|
||||
eoParams: { team: teamId, service: serviceId }
|
||||
}).then(response => {
|
||||
}).then((response) => {
|
||||
const { code, msg } = response
|
||||
if (code === STATUS_CODE.SUCCESS) {
|
||||
message.success(msg || $t(RESPONSE_TIPS.success))
|
||||
@@ -284,7 +333,6 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
getTeamOptionList()
|
||||
})
|
||||
}
|
||||
getProviderOptionList()
|
||||
getTagAndServiceClassifyList()
|
||||
if (serviceId !== undefined) {
|
||||
setOnEdit(true)
|
||||
@@ -298,6 +346,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
}
|
||||
])
|
||||
} else {
|
||||
getProviderOptionList()
|
||||
setOnEdit(false)
|
||||
const id = uuidv4()
|
||||
form.setFieldValue('id', id)
|
||||
@@ -332,7 +381,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
// const serviceTypeOptions = useMemo(()=>SERVICE_KIND_OPTIONS.map((x)=>({...x, label:$t(x.label)})),[state.language]);
|
||||
// const visualizationOptions = useMemo(()=>SERVICE_VISUALIZATION_OPTIONS.map((x)=>({...x, label:$t(x.label)})),[state.language])
|
||||
const approvalOptions = useMemo(
|
||||
() => SERVICE_APPROVAL_OPTIONS.map(x => ({ ...x, label: $t(x.label) })),
|
||||
() => SERVICE_APPROVAL_OPTIONS.map((x) => ({ ...x, label: $t(x.label) })),
|
||||
[state.language]
|
||||
)
|
||||
|
||||
@@ -363,21 +412,13 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
name="id"
|
||||
rules={[{ required: true, whitespace: true }]}
|
||||
>
|
||||
<Input
|
||||
className="w-INPUT_NORMAL"
|
||||
disabled={onEdit}
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
/>
|
||||
<Input className="w-INPUT_NORMAL" disabled={onEdit} placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
{!onEdit && (
|
||||
<Form.Item<SystemConfigFieldType>
|
||||
label={$t('服务类型')}
|
||||
name="serviceKind"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Form.Item<SystemConfigFieldType> label={$t('服务类型')} name="serviceKind" rules={[{ required: true }]}>
|
||||
<Radio.Group
|
||||
disabled={onEdit}
|
||||
onChange={e => {
|
||||
onChange={(e) => {
|
||||
setShowAI(e.target.value === 'ai')
|
||||
}}
|
||||
>
|
||||
@@ -397,39 +438,40 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
</Form.Item>
|
||||
)}
|
||||
{showAI && (
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t('默认 AI 供应商')}
|
||||
name="provider"
|
||||
rules={[{ required: true }]}
|
||||
extra={
|
||||
serviceId
|
||||
? $t('创建 API 时会默认选择该供应商,修改默认供应商不会影响现有 API')
|
||||
: ''
|
||||
}
|
||||
>
|
||||
{providerOptionList && providerOptionList.length > 0 ? (
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={providerOptionList}
|
||||
></Select>
|
||||
) : (
|
||||
<p>
|
||||
{$t('未配置任何 AI 模型供应商,')}
|
||||
<a href="/aisetting" target="_blank" onClick={() => setAiConfigFlushed(false)}>
|
||||
{$t('立即配置')}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</Form.Item>
|
||||
<>
|
||||
<Form.Item<AiServiceConfigFieldType>
|
||||
label={$t('默认 AI 供应商')}
|
||||
name="provider"
|
||||
rules={[{ required: true }]}
|
||||
extra={serviceId ? $t('创建 API 时会默认选择该供应商,修改默认供应商不会影响现有 API') : ''}
|
||||
>
|
||||
{providerOptionList && providerOptionList.length > 0 ? (
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
options={providerOptionList}
|
||||
onChange={(e) => {
|
||||
modelProviderChange(e)
|
||||
}}
|
||||
></Select>
|
||||
) : (
|
||||
<p>
|
||||
{$t('未配置任何 AI 模型供应商,')}
|
||||
<a href="/aisetting" target="_blank" onClick={() => setAiConfigFlushed(false)}>
|
||||
{$t('立即配置')}
|
||||
</a>
|
||||
</p>
|
||||
)}
|
||||
</Form.Item>
|
||||
<Form.Item<AiServiceConfigFieldType> label={$t('默认模型')} name="model" rules={[{ required: true }]}>
|
||||
<Select className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} options={modelList}></Select>
|
||||
</Form.Item>
|
||||
</>
|
||||
)}
|
||||
|
||||
<Form.Item<SystemConfigFieldType>
|
||||
label={$t('API 调用前缀')}
|
||||
name="prefix"
|
||||
extra={$t(
|
||||
'作为服务内所有API的前缀,比如host/{service_name}/{api_path},一旦保存无法修改'
|
||||
)}
|
||||
extra={$t('作为服务内所有API的前缀,比如host/{service_name}/{api_path},影响较大,谨慎修改')}
|
||||
rules={[
|
||||
{ required: true, whitespace: true },
|
||||
{
|
||||
@@ -437,19 +479,10 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
}
|
||||
]}
|
||||
>
|
||||
<Input
|
||||
prefix={onEdit ? '' : '/'}
|
||||
className="w-INPUT_NORMAL"
|
||||
disabled={onEdit}
|
||||
placeholder={$t(PLACEHOLDER.input)}
|
||||
/>
|
||||
<Input prefix={onEdit ? '' : '/'} className="w-INPUT_NORMAL" placeholder={$t(PLACEHOLDER.input)} />
|
||||
</Form.Item>
|
||||
{!onEdit && (
|
||||
<Form.Item<SystemConfigFieldType>
|
||||
label={$t('所属团队')}
|
||||
name="team"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Form.Item<SystemConfigFieldType> label={$t('所属团队')} name="team" rules={[{ required: true }]}>
|
||||
<Select
|
||||
className="w-INPUT_NORMAL"
|
||||
disabled={onEdit}
|
||||
@@ -459,11 +492,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
</Form.Item>
|
||||
)}
|
||||
|
||||
<Form.Item<SystemConfigFieldType>
|
||||
label={$t('订阅审核')}
|
||||
name="approvalType"
|
||||
rules={[{ required: true }]}
|
||||
>
|
||||
<Form.Item<SystemConfigFieldType> label={$t('订阅审核')} name="approvalType" rules={[{ required: true }]}>
|
||||
<Radio.Group className="flex flex-col" options={approvalOptions} />
|
||||
</Form.Item>
|
||||
|
||||
@@ -498,7 +527,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
<Form.Item<SystemConfigFieldType>
|
||||
label={$t('图标')}
|
||||
name="logoFile"
|
||||
extra={$t('仅支持 .png .jpg .jpeg .svg 格式的图片文件, 大于 1KB 的文件将被压缩')}
|
||||
extra={$t('仅支持 .png .jpg .jpeg .svg 格式的图片文件')}
|
||||
valuePropName="fileList"
|
||||
getValueFromEvent={normFile}
|
||||
>
|
||||
@@ -515,11 +544,7 @@ const SystemConfig = forwardRef<SystemConfigHandle>((_, ref) => {
|
||||
style={{ marginTop: 8 }}
|
||||
>
|
||||
{imageBase64 ? (
|
||||
<img
|
||||
src={imageBase64}
|
||||
alt="Logo"
|
||||
style={{ maxWidth: '200px', width: '68px', height: '68px' }}
|
||||
/>
|
||||
<img src={imageBase64} alt="Logo" style={{ maxWidth: '200px', width: '68px', height: '68px' }} />
|
||||
) : (
|
||||
uploadButton
|
||||
)}
|
||||
|
||||
@@ -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,35 @@ const SystemList: FC = () => {
|
||||
const onClose = () => {
|
||||
setOpen(false)
|
||||
}
|
||||
|
||||
const openLogsModal = (record: any) => {
|
||||
const closeModal = (reload = true) => {
|
||||
modalInstance.destroy()
|
||||
reload && manualReloadTable()
|
||||
}
|
||||
const updateFooter = () => {
|
||||
record.state = 'error'
|
||||
modalInstance.update({})
|
||||
}
|
||||
let cancelCb: () => void = () => {}
|
||||
const cancel = (cancel: () => void) => {
|
||||
cancelCb = cancel
|
||||
}
|
||||
const modalInstance = modal.confirm({
|
||||
title: $t('部署过程'),
|
||||
content: <ServiceDeployment record={record} closeModal={closeModal} updateFooter={updateFooter} cancelCb={cancel} />,
|
||||
footer: () => {
|
||||
return <LogsFooter record={record} closeModal={closeModal} />
|
||||
},
|
||||
afterClose: () => {
|
||||
cancelCb()
|
||||
},
|
||||
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 +181,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,91 @@
|
||||
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) {
|
||||
setTimeout(() => {
|
||||
resolve(true)
|
||||
closeModal()
|
||||
}, 500)
|
||||
} 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) {
|
||||
setTimeout(() => {
|
||||
resolve(true)
|
||||
closeModal()
|
||||
}, 500)
|
||||
} 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,206 @@
|
||||
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, updateFooter?: () => void, cancelCb?: (cancel: () => void) => void }) => {
|
||||
const { record, closeModal, updateFooter, cancelCb } = 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,
|
||||
callback: (cancel: () => void) => {
|
||||
cancelCb?.(cancel)
|
||||
},
|
||||
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?.()
|
||||
}, 500)
|
||||
} else if (parsedChunk?.data?.state.includes('error')) {
|
||||
updateFooter?.()
|
||||
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>
|
||||
</>
|
||||
)
|
||||
}
|
||||
@@ -68,12 +68,12 @@ const Integrate = ({ service }: { service: ServiceDetailType }) => {
|
||||
<div className="my-[10px]">{$t('可通过以下 URL 或 下载 Json 文件,导入 API 文档数据到 Agent 平台中。')}</div>
|
||||
<div className="flex w-full items-center gap-[30px]">
|
||||
<Space.Compact className="w-[700px]">
|
||||
<Input className="truncate" disabled title={url} value={url} />
|
||||
<Input className="truncate" readOnly title={url} value={url} />
|
||||
<Button type="primary" onClick={copyURL}>
|
||||
{$t('复制 URL')}
|
||||
</Button>
|
||||
</Space.Compact>
|
||||
<span className="text-[14px] font-bold">OR</span>
|
||||
<span className="text-[14px] font-bold">{$t('或')}</span>
|
||||
<Button href={`/api/v1/export/openapi/${serviceId}`} target="_blank">
|
||||
{$t('下载 Json 文件')}
|
||||
</Button>
|
||||
|
||||
Reference in New Issue
Block a user