diff --git a/frontend/packages/common/src/contexts/GlobalStateContext.tsx b/frontend/packages/common/src/contexts/GlobalStateContext.tsx index 31fbb1d4..1a3eb1ff 100644 --- a/frontend/packages/common/src/contexts/GlobalStateContext.tsx +++ b/frontend/packages/common/src/contexts/GlobalStateContext.tsx @@ -160,6 +160,13 @@ const mockData = [ path: '/aiApis', icon: 'ic:baseline-api', access: 'system.settings.ai_api.view' + }, + { + name: '负载均衡', + key: 'loadBalancing', + path: '/loadBalancing', + icon: 'ph:network-x', + access: 'system.settings.data_source.view' } ] }, diff --git a/frontend/packages/common/src/hooks/pluginLoader.ts b/frontend/packages/common/src/hooks/pluginLoader.ts index a33eb8e4..84f06bc4 100644 --- a/frontend/packages/common/src/hooks/pluginLoader.ts +++ b/frontend/packages/common/src/hooks/pluginLoader.ts @@ -219,6 +219,16 @@ const mockData = { type: 'normal' } ] + }, + { + driver: 'apipark.builtIn.component', + name: 'loadBalancing', + router: [ + { + path: 'loadBalancing', + type: 'normal' + } + ] } // { // "driver": "apipark.remote.normal", diff --git a/frontend/packages/core/src/const/const.tsx b/frontend/packages/core/src/const/const.tsx index 79b6452d..1ee3f57b 100644 --- a/frontend/packages/core/src/const/const.tsx +++ b/frontend/packages/core/src/const/const.tsx @@ -799,5 +799,20 @@ export const routerMap: Map = new Map([ } ] } + ], + [ + 'loadBalancing', + { + type: 'module', + lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/loadBalancingLayout.tsx')), + key: 'loadBalancing', + children: [ + { + path: 'list', + lazy: lazy(() => import(/* webpackChunkName: "[request]" */ '@core/pages/loadBalancing/index.tsx')), + key: 'loadBalancingList' + } + ] + } ] ]) diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx index 489ed981..7fbf127a 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx @@ -79,7 +79,7 @@ const AiServiceInsideRouterCreate = () => { timeout, retry, aiPrompt: { variables: variables, prompt: prompt }, - aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config }, + aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config, type: defaultLlm?.type }, disabled } return fetchData>('service/ai-router', { @@ -237,13 +237,14 @@ const AiServiceInsideRouterCreate = () => { } const handlerSubmit: () => Promise | undefined = () => { - return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string }) => { + return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string, type: string, provider: string }) => { setDefaultLlm( (prev) => ({ ...prev, provider: res.provider, id: res.id, + type: res.type, config: res.config, logo: llmList?.find((x: AiProviderLlmsItems) => x.id === res.id)?.logo }) as AiProviderDefaultConfig & { config: string } diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx index 415a99f6..628b44df 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterModelConfig.tsx @@ -1,132 +1,217 @@ -import { Codebox } from "@common/components/postcat/api/Codebox" -import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from "@common/const/const" -import { useFetch } from "@common/hooks/http" -import { $t } from "@common/locales" -import { AiProviderDefaultConfig, AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList" -import { SimpleAiProviderItem } from "@core/pages/system/SystemConfig" -import { Form, message, Select, Tag } from "antd" -import { DefaultOptionType } from "antd/es/select" -import { forwardRef, useEffect, useImperativeHandle, useState } from "react" +import { Codebox } from '@common/components/postcat/api/Codebox' +import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { AiProviderDefaultConfig, AiProviderLlmsItems } from '@core/pages/aiSetting/AiSettingList' +import { LocalLlmType } from '@core/pages/loadBalancing/type' +import { SimpleAiProviderItem } from '@core/pages/system/SystemConfig' +import { Form, message, Select, Tag } from 'antd' +import { DefaultOptionType } from 'antd/es/select' +import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' export type AiServiceRouterModelConfigHandle = { - save:()=>Promise<{id:string, config:string}> + save: () => Promise<{ id: string; config: string, type: string, provider: string }> } export type AiServiceRouterModelConfigProps = { - entity:AiServiceRouterModelConfigField - llmList:AiProviderLlmsItems[] + entity: AiServiceRouterModelConfigField + llmList: AiProviderLlmsItems[] } type AiServiceRouterModelConfigField = { - provider:string - id:string - config:string + provider: string + id: string + config: string + type: string } -const AiServiceRouterModelConfig = forwardRef((props, ref)=>{ - const [form] = Form.useForm(); - const {entity} = props - const [providerList, setProviderList]= useState([]) - const [llmList, setLlmList]= useState([]) - const {fetchData} = useFetch() - useImperativeHandle(ref, ()=>({ - save:form.validateFields - }) - ) +const AiServiceRouterModelConfig = forwardRef( + (props, ref) => { + const [form] = Form.useForm() + const { entity } = props + const [providerList, setProviderList] = useState([]) + const [llmList, setLlmList] = useState([]) + const [modelType, setModelType] = useState<'online' | 'local'>('online') + const { fetchData } = useFetch() + useImperativeHandle(ref, () => ({ + save: form.validateFields + })) + const [modelTypeList] = useState([ + { + label: $t('线上模型'), + value: 'online' + }, + { + label: $t('本地模型'), + value: 'local' + } + ]) - useEffect(()=>{ + /** + * 获取本地模型列表 + * @param setDefaultValue + */ + const getLocalLlmList = (setDefaultValue?: boolean) => { + fetchData('simple/ai/models/local/configured', { + method: 'GET', + eoTransformKeys: ['default_config'] + }).then((response) => { + const models = response.data.models || [] + setLlmList(models) + 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(() => { + if (entity.type === 'online') { getProviderList() - form.setFieldsValue(entity) - },[]) - - const getProviderList = ()=>{ - setProviderList([]) - fetchData>('simple/ai/providers',{method:'GET',eoTransformKeys:[]}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - setProviderList(data.providers?.filter(x=>x.configured)?.map((x:SimpleAiProviderItem)=>{return {...x, - label: x.name, value:x.id - }})) - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - } - }) - } - - const getLlmList = (provider:string)=>{ - fetchData>('ai/provider/llms',{method:'GET',eoParams:{provider}, eoTransformKeys:['default_llm']}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - setLlmList(data.llms) - form.setFieldsValue({ - id:data.provider.defaultLlm, - config:data.llms.find(x=>x.id===data.provider.defaultLlm)?.config}) - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - } - }).catch((errorInfo)=> console.error(errorInfo)) - } - - const handleChangeProvider = (provider:string)=>{ - getLlmList(provider) - } - - useEffect(()=>{ getLlmList(entity.provider) - },[]) + } else { + getLocalLlmList() + } + form.setFieldsValue(entity) + }, []) + + const getProviderList = (setDefaultValue?: boolean) => { + setProviderList([]) + fetchData>('simple/ai/providers/configured', { + method: 'GET', + eoTransformKeys: [] + }).then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setProviderList( + data.providers + ?.map((x: SimpleAiProviderItem) => { + return { ...x, label: x.name, value: x.id } + }) + ) + if (setDefaultValue && data.providers.length) { + const id = data.providers[0].id + form.setFieldValue('provider', id) + getLlmList(id) + } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + } + + const getLlmList = (provider: string) => { + fetchData>('ai/provider/llms', { + method: 'GET', + eoParams: { provider }, + eoTransformKeys: ['default_llm'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setLlmList(data.llms) + form.setFieldsValue({ + id: data.provider.defaultLlm, + config: data.llms.find((x) => x.id === data.provider.defaultLlm)?.config + }) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + .catch((errorInfo) => console.error(errorInfo)) + } + + const handleChangeProvider = (provider: string) => { + getLlmList(provider) + } return ( -
- - label={$t("模型供应商")} - name="provider" - rules={[{ required: true }]} - > - - + + label={$t('模型类型')} name="type" rules={[{ required: true }]}> + + + {modelType === 'online' && ( + + label={$t('模型供应商')} + name="provider" + rules={[{ required: true }]} + > + + + )} - - label={$t("模型")} - name="id" - rules={[{ required: true }]} - > - - + label={$t('模型')} name="id" rules={[{ required: true }]}> + + - - label={$t("参数")} - name="config" - > - - - + label={$t('参数')} name="config"> + + + ) -}) + } +) -export default AiServiceRouterModelConfig \ No newline at end of file +export default AiServiceRouterModelConfig diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index f0df517d..83e197e1 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -272,7 +272,7 @@ const AiSettingModalContent = forwardRef {modelMode === 'manual' && ( - label={$t('模型来源')} name="modelMode" rules={[{ required: true }]}> + label={$t('模型供应商')} name="modelMode" rules={[{ required: true }]}> { + modelTypeChange(e) + }} + > + + {modelType === 'online' && ( + label={$t('模型供应商')} name="provider" rules={[{ required: true }]}> + + + )} + + + + + ) +}) + +export default AddLoadBalancingModel diff --git a/frontend/packages/core/src/pages/loadBalancing/index.tsx b/frontend/packages/core/src/pages/loadBalancing/index.tsx new file mode 100644 index 00000000..3441f5fd --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/index.tsx @@ -0,0 +1,349 @@ +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 { useSearchParams } from 'react-router-dom' +import { LoadBalancingHandle, LoadBalancingItems } from './type' +import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission' +import AddLoadBalancingModel from './AddModel' + + + +const LoadBalancingPage = () => { + const pageListRef = useRef(null) + const [searchParams] = useSearchParams() + const serviceId = searchParams.get('serviceId') + const [searchWord, setSearchWord] = useState('') + const [columns, setColumns] = useState[]>([]) + const { modal, message } = App.useApp() + const [apiKeys, setApiKeys] = useState([]) + const addModelRef = useRef() + const statusEnum: Record = { + normal: { text: {$t('正常')} }, + abnormal: { text: {$t('异常')} } + } + + /** + * 请求数据 + */ + const { fetchData } = useFetch() + const addModel = () => { + modal.confirm({ + title: $t('添加负载均衡'), + content: , + width: 600, + closable: true, + onOk: () => { + return addModelRef.current?.save().then((res) => { + if (res === true) { + pageListRef.current?.reload() + } + }) + }, + wrapClassName: 'ant-modal-without-footer', + okText: $t('确认'), + cancelText: $t('取消'), + icon: <> + }) + } + + /** + * 获取列表数据 + * @param dataType + * @returns + */ + const requestApis = ( + params: LoadBalancingItems & { + pageSize: number + current: number + }, + sort: Record, + filter: Record + ) => { + let filters + if (filter) { + filters = [] + if (filter.isStop) { + if (filter.isStop.indexOf('true') !== -1) { + filters.push('enable') + } + if (filter.isStop.indexOf('false') !== -1) { + filters.push('disable') + } + if (filter.publishStatus?.length > 0) { + filters = [...filters, ...filter.publishStatus] + } + } + } + + return fetchData>( + `strategy/${serviceId === undefined ? 'global' : 'service'}/data-masking/list`, + { + method: 'GET', + eoParams: { + order: Object.keys(sort)?.[0], + sort: Object.keys(sort)?.length > 0 ? (Object.values(sort)?.[0] === 'descend' ? 'desc' : 'asc') : undefined, + filters: JSON.stringify(filters), + keyword: searchWord, + service: serviceId + }, + eoTransformKeys: ['is_stop', 'is_delete', 'update_time', 'publish_status', 'processed_total'] + } + ) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setApiKeys(response.data.list) + // 保存数据 + return { + data: data.list, + total: data.total, + success: true + } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return { data: [], success: false } + } + }) + .catch(() => { + return { data: [], success: false } + }) + } + + /** + * 排序 + * @param beforeIndex + * @param afterIndex + * @param newDataSource + */ + const handleDragSortEnd = async (beforeIndex: number, afterIndex: number, newDataSource: LoadBalancingItems[]) => { + try { + let targetId + let sortDirection + + // Check if there's an item before afterIndex + if (afterIndex > 0) { + targetId = newDataSource[afterIndex - 1].id + sortDirection = 'after' + } else if (afterIndex < newDataSource.length - 1) { + // If no item before, use the item after + targetId = newDataSource[afterIndex + 1].id + sortDirection = 'before' + } + + const response = await fetchData>('ai/balance/sort', { + method: 'PUT', + eoBody: { + origin: apiKeys[beforeIndex].id, + target: targetId, + sort: sortDirection + } + }) + + if (response.code === STATUS_CODE.SUCCESS) { + message.success($t('排序成功')) + pageListRef.current?.reload() + } else { + message.error(response.msg || RESPONSE_TIPS.error) + // Revert the UI if API call fails + pageListRef.current?.reload() + } + } catch (error) { + message.error(RESPONSE_TIPS.error) + // Revert the UI if API call fails + pageListRef.current?.reload() + } + } + + /** + * 删除 + * @param id + */ + const handleDelete = (id: string) => { + fetchData>('ai/balance', { + method: 'DELETE', + eoBody: { + 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 setTableColumns = () => { + setColumns([ + { + title: '', + dataIndex: 'drag', + width: '40px' + }, + { + title: $t('优先级'), + dataIndex: 'priority', + width: 80, + ellipsis: true, + key: 'priority' + }, + { + title: $t('模型'), + dataIndex: ['provider', 'name'], + ellipsis: true, + width: 100, + key: 'provider', + render: (text: string, record: LoadBalancingItems) => ( + + {record.provider?.name} / {record.model?.name} + + ) + }, + { + title: $t('类型'), + dataIndex: 'type', + width: 100, + ellipsis: true, + key: 'type', + render: (text: string, record: LoadBalancingItems) => ( + {record.type === 'online' ? $t('线上模型') : $t('本地模型')} + ) + }, + { + title: $t('状态'), + dataIndex: 'state', + width: 120, + ellipsis: true, + key: 'state', + render: (text: string, record: LoadBalancingItems) => {statusEnum[record.state]?.text || '-'} + }, + { + title: $t('API 数量'), + dataIndex: 'api_count', + ellipsis: true, + width: 80, + key: 'api_count', + render: (text: string, record: LoadBalancingItems) => ( + + + {record.api_count || '-'} + + + ) + }, + { + title: $t('KEY 数量'), + dataIndex: 'key_count', + ellipsis: true, + width: 80, + key: 'key_count', + render: (text: string, record: LoadBalancingItems) => ( + + + {record.key_count || '-'} + + + ) + }, + { + title: '', + key: 'option', + btnNums: 1, + width: 80, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: any) => [ + handleDelete(entity.id as string)} + btnTitle={$t('删除')} + /> + ] + } + ]) + } + useEffect(() => { + setTableColumns() + }, []) + + return ( + <> + +
+ + + + ]} + request={async ( + params: any & { + pageSize: number + current: number + }, + sort: Record, + filter: Record + ) => requestApis(params, sort, filter)} + onSearchWordChange={(e) => { + setSearchWord(e.target.value) + }} + showPagination={true} + dragSortKey="drag" + onDragSortEnd={handleDragSortEnd} + searchPlaceholder={$t('请输入...')} + columns={columns} + /> +
+
+ + ) +} +export default LoadBalancingPage diff --git a/frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx b/frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx new file mode 100644 index 00000000..196fc934 --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/loadBalancingLayout.tsx @@ -0,0 +1,16 @@ +import { useEffect } from 'react' +import { Outlet, useLocation, useNavigate } from 'react-router-dom' + +export default function LoadBalancingLayout() { + const location = useLocation() + const pathName = location.pathname + const navigator = useNavigate() + + useEffect(() => { + if (pathName === '/loadBalancing') { + const queryParams = new URLSearchParams(location.search).toString() + navigator(`/loadBalancing/list${queryParams ? `?${queryParams}` : ''}`) + } + }, [pathName]) + return +} diff --git a/frontend/packages/core/src/pages/loadBalancing/type.ts b/frontend/packages/core/src/pages/loadBalancing/type.ts new file mode 100644 index 00000000..d7ff34ed --- /dev/null +++ b/frontend/packages/core/src/pages/loadBalancing/type.ts @@ -0,0 +1,31 @@ +export interface LoadBalancingItems { + id: string + priority: string + provider: { + id: string + name: string + } + model: { + id: string + name: string + } + type: string + state: string + api_count: string + key_count: string +} + +export interface LoadModelDetailData { + type: string + provider: string + model: string +} +export interface LocalLlmType { + id: string + name: string + defaultConfig: string +} + +export type LoadBalancingHandle = { + save: () => Promise +}