diff --git a/frontend/packages/core/src/App.tsx b/frontend/packages/core/src/App.tsx index de83bc0f..aa5b4c40 100644 --- a/frontend/packages/core/src/App.tsx +++ b/frontend/packages/core/src/App.tsx @@ -1,47 +1,45 @@ - +import { StyleProvider } from '@ant-design/cssinjs' +import { BreadcrumbProvider } from '@common/contexts/BreadcrumbContext.tsx' +import { GlobalProvider } from '@common/contexts/GlobalStateContext' +import { useLocaleContext } from '@common/contexts/LocaleContext' +import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext' +import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext' +import useInitializeMonaco from '@common/hooks/useInitializeMonaco' +import { $t } from '@common/locales' +import RenderRoutes from '@core/components/aoplatform/RenderRoutes' +import { App as AppAntd, ConfigProvider } from 'antd' +import { useMemo } from 'react' import './App.css' -import { ConfigProvider, App as AppAntd } from 'antd'; -import RenderRoutes from '@core/components/aoplatform/RenderRoutes'; -import {BreadcrumbProvider} from "@common/contexts/BreadcrumbContext.tsx"; -import useInitializeMonaco from "@common/hooks/useInitializeMonaco"; -import { useMemo } from 'react'; -import { GlobalProvider } from '@common/contexts/GlobalStateContext'; -import { $t } from '@common/locales'; -import { PluginEventHubProvider } from '@common/contexts/PluginEventHubContext'; -import { PluginSlotHubProvider } from '@common/contexts/PluginSlotHubContext'; -import { useLocaleContext } from '@common/contexts/LocaleContext'; -import { StyleProvider } from '@ant-design/cssinjs'; - const antdComponentThemeToken = { token: { // Seed Token,影响范围大 colorPrimary: '#3D46F2', - colorLink:'#3D46F2', - colorBorder:'#ededed', - colorText:'#333', + colorLink: '#3D46F2', + colorBorder: '#ededed', + colorText: '#333', borderRadius: 4, // 派生变量,影响范围小 colorBgContainer: '#fff', - colorPrimaryBg:'#EBEEF2', - colorTextQuaternary:'#BBB', - colorTextTertiary:'#999' + colorPrimaryBg: '#EBEEF2', + colorTextQuaternary: '#BBB', + colorTextTertiary: '#999' }, - components:{ + components: { // 派生变量,影响范围小 - Input:{ - activeShadow:'none' + Input: { + activeShadow: 'none' }, - Select:{ - activeShadow:'none' + Select: { + activeShadow: 'none' }, - Checkbox:{ - activeShadow:'none' + Checkbox: { + activeShadow: 'none' }, - Cascader:{ - activeShadow:'none', - optionSelectedBg:'#EBEEF2', - optionHoverBg:'#EBEEF2' + Cascader: { + activeShadow: 'none', + optionSelectedBg: '#EBEEF2', + optionHoverBg: '#EBEEF2' }, Layout: { bodyBg: '#fff', @@ -50,122 +48,122 @@ const antdComponentThemeToken = { headerHeight: 50, headerPadding: '10 20px', lightSiderBg: '#fff', - siderBg: '#fff', + siderBg: '#fff' }, - Breadcrumb:{ - itemColor:'#666', - linkColor:'#666', - lastItemColor:'#333', + Breadcrumb: { + itemColor: '#666', + linkColor: '#666', + lastItemColor: '#333' }, - Table:{ - headerBorderRadius:0, - headerSplitColor:'#ededed', - borderColor:'#ededed', - cellPaddingBlockMD:'15px', - cellPaddingInlineMD:'12px', - cellPaddingBlockSM:'8px', - cellPaddingInlineSM:'12px', - headerFilterHoverBg:'#EBEEF2', - headerSortActiveBg:'#F7F8FA', - headerSortHoverBg:'#F7F8FA', - fixedHeaderSortActiveBg:'#F7F8FA', - headerBg:'#FAFAFA', - rowHoverBg:'#EBEEF2' - + Table: { + headerBorderRadius: 0, + headerSplitColor: '#ededed', + borderColor: '#ededed', + cellPaddingBlockMD: '15px', + cellPaddingInlineMD: '12px', + cellPaddingBlockSM: '8px', + cellPaddingInlineSM: '12px', + headerFilterHoverBg: '#EBEEF2', + headerSortActiveBg: '#F7F8FA', + headerSortHoverBg: '#F7F8FA', + fixedHeaderSortActiveBg: '#F7F8FA', + headerBg: '#FAFAFA', + rowHoverBg: '#EBEEF2' }, - Segmented:{ - itemColor:'#333', - itemSelectedColor:'#333', - trackBg:'#f7f8fa', - trackPadding:0, - // itemHoverColor:'#EBEEF2', - itemActiveBg:'#EBEEF2', - itemHoverBg:'#EBEEF2', - itemSelectedBg:'#EBEEF2', + Segmented: { + itemColor: '#333', + itemSelectedColor: '#333', + trackBg: '#f7f8fa', + trackPadding: 0, + // itemHoverColor:'#EBEEF2', + itemActiveBg: '#EBEEF2', + itemHoverBg: '#EBEEF2', + itemSelectedBg: '#EBEEF2' }, - Tree:{ - // titleHeight:30, - // fontSize:12, - directoryNodeSelectedBg:'#EBEEF2', - directoryNodeSelectedColor:'#333', - nodeSelectedBg:'#EBEEF2', - nodeHoverBg:'#EBEEF2' - }, - Collapse:{ - headerBg:'#f7f8fa', - headerPadding:"12px", - contentPadding:"0 10px 12px 10px" - }, - Button:{ - // paddingInline:8, - dangerShadow:'none', - defaultShadow:'none', - primaryShadow:'none' - }, - Tabs:{ - cardBg:'#EBEEF2', - cardHeight:42, - horizontalItemGutter:8, - horizontalItemPaddingSM:'12px 8px 8px 8px', - horizontalItemPadding:'12px 8px 8px 8px', - }, - Menu:{ - // itemBg:'#F7F8FA', - // subMenuItemBg:'#F7F8FA', - // itemMarginBlock:0, - // activeBarBorderWidth:0, - // itemSelectedColor:'#333', - // itemSelectedBg:'#EBEEF2', - // itemHoverBg:'#EBEEF2' - }, - List:{ - itemPadding:'8px 0' - }, - Form:{ - itemMarginBottom:10, - - }, - Alert:{ - defaultPadding:'12px 16px' - }, - Tag:{ - defaultBg:"#f7f8fa" - }, + Tree: { + // titleHeight:30, + // fontSize:12, + directoryNodeSelectedBg: '#EBEEF2', + directoryNodeSelectedColor: '#333', + nodeSelectedBg: '#EBEEF2', + nodeHoverBg: '#EBEEF2' + }, + Collapse: { + headerBg: '#f7f8fa', + headerPadding: '12px', + contentPadding: '0 10px 12px 10px' + }, + Button: { + // paddingInline:8, + dangerShadow: 'none', + defaultShadow: 'none', + primaryShadow: 'none' + }, + Tabs: { + cardBg: '#EBEEF2', + cardHeight: 42, + horizontalItemGutter: 8, + horizontalItemPaddingSM: '12px 8px 8px 8px', + horizontalItemPadding: '12px 8px 8px 8px' + }, + Menu: { + // itemBg:'#F7F8FA', + // subMenuItemBg:'#F7F8FA', + // itemMarginBlock:0, + // activeBarBorderWidth:0, + // itemSelectedColor:'#333', + // itemSelectedBg:'#EBEEF2', + // itemHoverBg:'#EBEEF2' + }, + List: { + itemPadding: '8px 0' + }, + Form: { + itemMarginBottom: 10 + }, + Alert: { + defaultPadding: '8px 12px' + }, + Tag: { + defaultBg: '#f7f8fa' + } } } - function App() { - const { locale } = useLocaleContext(); + const { locale } = useLocaleContext() useInitializeMonaco() - - - const validateMessages = useMemo(()=>({ - required: $t('必填项'), - email:$t('不是有效邮箱地址')} - ),[locale]) - + + const validateMessages = useMemo( + () => ({ + required: $t('必填项'), + email: $t('不是有效邮箱地址') + }), + [locale] + ) + return ( - - + - - - - - - - - - + form={{ validateMessages }} + > + + + + + + + + + - ); + ) } export default App diff --git a/frontend/packages/core/src/components/AIProviderSelect/index.tsx b/frontend/packages/core/src/components/AIProviderSelect/index.tsx index 3437fb1b..273d9117 100644 --- a/frontend/packages/core/src/components/AIProviderSelect/index.tsx +++ b/frontend/packages/core/src/components/AIProviderSelect/index.tsx @@ -1,17 +1,14 @@ import { STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' +import { ModelDetailData } from '@core/pages/aiSetting/types' import { Select, Space, message } from 'antd' import React, { useEffect, useState } from 'react' import { useTranslation } from 'react-i18next' -export interface AIProvider { - id: string - name: string - logo: string - configured: boolean - getApikeyUrl: string - status: string +export interface AIProvider extends ModelDetailData { default_config: string + backupName: string + backupModel: string } interface AIProviderResponse { @@ -41,13 +38,18 @@ const AIProviderSelect: React.FC = ({ value, onChange, st const fetchProviders = async () => { if (isMounted) setLoading(true) try { - const response = await fetchData('simple/ai/providers/configured', { method: 'GET' }) + const endpoint = 'simple/ai/providers/configured' + const response = await fetchData(endpoint, { method: 'GET' }) const { code, data, msg } = response if (code === STATUS_CODE.SUCCESS) { - isMounted && setProviders(data.providers) + const providers = data.providers.map((val) => ({ + ...val, + backupName: data.backup?.name, + backupModel: data.backup?.model?.name + })) + isMounted && setProviders(providers) if (!data.providers?.length) return - - const selectedProvider: AIProvider = value ? providers.find((p) => p.id === value) : data.providers[0] + const selectedProvider: AIProvider = value ? providers.find((p) => p.id === value) : providers[0] onChange?.(selectedProvider.id, selectedProvider) } else { message.error(msg || t('Failed to fetch AI providers')) diff --git a/frontend/packages/core/src/const/ai-service/const.tsx b/frontend/packages/core/src/const/ai-service/const.tsx index bd101041..91112b48 100644 --- a/frontend/packages/core/src/const/ai-service/const.tsx +++ b/frontend/packages/core/src/const/ai-service/const.tsx @@ -1,127 +1,134 @@ -import { AiServiceRouterTableListItem, VariableItems } from "./type"; -import { TabsProps } from "antd"; -import { frontendTimeSorter } from "@common/utils/dataTransfer"; -import { COLUMNS_TITLE, PLACEHOLDER } from "@common/const/const"; - -import { PageProColumns } from "@common/components/aoplatform/PageList"; - +import { COLUMNS_TITLE, PLACEHOLDER } from '@common/const/const' +import { frontendTimeSorter } from '@common/utils/dataTransfer' +import { TabsProps } from 'antd' +import { AiServiceRouterTableListItem, VariableItems } from './type' +import { PageProColumns } from '@common/components/aoplatform/PageList' export const AI_SERVICE_ROUTER_TABLE_COLUMNS: PageProColumns[] = [ - { - title:('URL'), - dataIndex: 'requestPath', - ellipsis:true - }, - { - title:('名称'), - dataIndex: 'name', - ellipsis:true, - }, - { - title:('模型'), - dataIndex: ['model','logo'], - ellipsis:true, - render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) =>{entity.model.id} - }, - { - title:('描述'), - dataIndex: 'description', - ellipsis:true - }, - { - title:('创建者'), - dataIndex: ['creator','name'], - ellipsis: true, - filters: true, - onFilter: true, - valueType: 'select', - filterSearch: true, - }, - { - title:('更新时间'), - dataIndex: 'updateTime', - ellipsis:true, - hideInSearch: true, - width:182, - sorter: (a,b)=>frontendTimeSorter(a,b,'updateTime') - }, -]; + { + title: 'URL', + dataIndex: 'requestPath', + ellipsis: true + }, + { + title: '名称', + dataIndex: 'name', + ellipsis: true + }, + { + title: '模型', + dataIndex: ['model', 'logo'], + ellipsis: true, + render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => ( + + {entity.model.id} + + ) + }, + { + title: '是否放行', + dataIndex: 'disabled', + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select' + }, + { + title: '描述', + dataIndex: 'description', + ellipsis: true + }, + { + title: '创建者', + dataIndex: ['creator', 'name'], + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + filterSearch: true + }, + { + title: '更新时间', + dataIndex: 'updateTime', + ellipsis: true, + hideInSearch: true, + width: 182, + sorter: (a, b) => frontendTimeSorter(a, b, 'updateTime') + } +] - -export const AI_SERVICE_VARIABLES_TABLE_COLUMNS: PageProColumns[] = [ - { - title:('Key'), - dataIndex: 'key', - key:'key', - width: '30%', - formItemProps: { - className:'p-0 bg-transparent border-none', - rules: [ - { - required: true, - whitespace: true - }, - { - pattern:/^[a-zA-Z][a-zA-Z0-9-_]*$/, - message: PLACEHOLDER.onlyAlphabet - } - ], - }, - ellipsis:true, - fieldProps:{ - allowClear:false - } - }, - { - title:('描述'), - dataIndex: 'description', - key:'description', - formItemProps: { - className:'p-0 bg-transparent border-none' +export const AI_SERVICE_VARIABLES_TABLE_COLUMNS: PageProColumns[] = [ + { + title: 'Key', + dataIndex: 'key', + key: 'key', + width: '30%', + formItemProps: { + className: 'p-0 bg-transparent border-none', + rules: [ + { + required: true, + whitespace: true }, - fieldProps:{ - allowClear:false + { + pattern: /^[a-zA-Z][a-zA-Z0-9-_]*$/, + message: PLACEHOLDER.onlyAlphabet } + ] }, - { - title:('必填'), - dataIndex: 'require', - key:'require', - valueType:'switch', - width:64, - formItemProps: { - className:'p-0 bg-transparent border-none'} - }, - { - title: COLUMNS_TITLE.operate, - valueType: 'option', - width:34, - render: ()=>null - }, - ]; - - -export const AiService_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [ - { - key: '0', - label:('待审核'), - }, - { - key: '1', - label: ('已审核'), + ellipsis: true, + fieldProps: { + allowClear: false } -]; - + }, + { + title: '描述', + dataIndex: 'description', + key: 'description', + formItemProps: { + className: 'p-0 bg-transparent border-none' + }, + fieldProps: { + allowClear: false + } + }, + { + title: '必填', + dataIndex: 'require', + key: 'require', + valueType: 'switch', + width: 64, + formItemProps: { + className: 'p-0 bg-transparent border-none' + } + }, + { + title: COLUMNS_TITLE.operate, + valueType: 'option', + width: 34, + render: () => null + } +] +export const AiService_INSIDE_APPROVAL_TAB_ITEMS: TabsProps['items'] = [ + { + key: '0', + label: '待审核' + }, + { + key: '1', + label: '已审核' + } +] export const AiService_PUBLISH_TAB_ITEMS: TabsProps['items'] = [ - { - key: '0', - label: ('发布版本'), - }, - { - key: '1', - label: ('发布申请记录'), - } -]; + { + key: '0', + label: '发布版本' + }, + { + key: '1', + label: '发布申请记录' + } +] diff --git a/frontend/packages/core/src/const/system/const.tsx b/frontend/packages/core/src/const/system/const.tsx index 2fdd5234..986f6590 100644 --- a/frontend/packages/core/src/const/system/const.tsx +++ b/frontend/packages/core/src/const/system/const.tsx @@ -258,7 +258,7 @@ export const SYSTEM_API_TABLE_COLUMNS: PageProColumns[] }, { title: '是否放行', - dataIndex: 'disable', + dataIndex: 'disabled', ellipsis: true, filters: true, onFilter: true, diff --git a/frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx b/frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx new file mode 100644 index 00000000..0ce3c4a4 --- /dev/null +++ b/frontend/packages/core/src/pages/aiApis/components/ApiKeyContent.tsx @@ -0,0 +1,111 @@ +import { Codebox } from '@common/components/postcat/api/Codebox' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { AIProvider } from '@core/components/AIProviderSelect' +import { App, DatePicker, Form, Input, Switch } from 'antd' +import dayjs from 'dayjs' +import React, { forwardRef, useEffect, useImperativeHandle, useState } from 'react' +import { EditAPIKey } from '../types' + +interface ApiKeyContentProps { + provider?: AIProvider + entity: EditAPIKey +} + +const ApiKeyContent: React.FC = forwardRef(({ provider, entity }, ref) => { + const [form] = Form.useForm() + const [neverExpire, setNeverExpire] = useState(true) + const { fetchData } = useFetch() + const { message } = App.useApp() + + useEffect(() => { + try { + const isNeverExpire = entity.expire_time === 0 + setNeverExpire(isNeverExpire) + form.setFieldsValue({ + name: entity.name, + expire_time: isNeverExpire ? undefined : dayjs(entity.expire_time), + config: entity.config + }) + } catch (e) { + form.setFieldsValue({ + name: entity.name, + expire_time: undefined, + config: '' + }) + } + }, []) + + const handleOk = async () => { + try { + const values = await form.validateFields() + const { expire_time, ...restValues } = values + const expireTime = neverExpire ? 0 : expire_time.valueOf() + + const response = await fetchData>('ai/resource/key', { + method: entity.id ? 'PUT' : 'POST', + eoParams: { provider: provider?.id, id: entity.id }, + eoBody: { ...restValues, expire_time: expireTime }, + eoTransformKeys: ['config'] + }) + + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || $t(RESPONSE_TIPS.success)) + return true + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return false + } + } catch (error) { + console.error('Validation failed:', error) + } + } + useImperativeHandle(ref, () => ({ + handleOk + })) + const handleNeverExpireChange = (checked: boolean) => { + setNeverExpire(checked) + if (!checked) { + form.setFieldsValue({ + expire_time: dayjs().add(7, 'days') + }) + } + } + + return ( + + + + + + + + + + + {neverExpire ? $t('永不过期') : $t('设置过期时间')} + + + {!neverExpire && ( + + + + )} + + ) +}) + +export default ApiKeyContent diff --git a/frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx b/frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx new file mode 100644 index 00000000..64cc18e9 --- /dev/null +++ b/frontend/packages/core/src/pages/aiApis/components/StatusFilter.tsx @@ -0,0 +1,40 @@ +import { $t } from '@common/locales' +import { Select, Space, theme } from 'antd' +import React from 'react' + +interface StatusFilterProps { + value: string[] + onChange: (value: string[]) => void +} + +const StatusFilter: React.FC = ({ value, onChange }) => { + const { token } = theme.useToken() + + const options = [ + { label: $t('Normal'), value: 'normal', color: token.colorSuccess }, + { label: $t('Exceeded'), value: 'exceeded', color: token.colorError }, + { label: $t('Expired'), value: 'expired', color: token.colorWarning }, + { label: $t('Disabled'), value: 'disabled', color: token.colorTextDisabled }, + { label: $t('Error'), value: 'error', color: token.colorError } + ] + + return ( + + {$t('Status')}: + ({ + ...option, + label: {option.label} + }))} + /> + + ) +} + +export default StatusFilter diff --git a/frontend/packages/core/src/pages/aiApis/index.tsx b/frontend/packages/core/src/pages/aiApis/index.tsx index 0d82a664..0a12ec3c 100644 --- a/frontend/packages/core/src/pages/aiApis/index.tsx +++ b/frontend/packages/core/src/pages/aiApis/index.tsx @@ -1,17 +1,258 @@ +import { ActionType } from '@ant-design/pro-components' import InsidePage from '@common/components/aoplatform/InsidePage' +import PageList, { PageProColumns } from '@common/components/aoplatform/PageList' +import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission' +import TimeRangeSelector, { TimeRangeButton } from '@common/components/aoplatform/TimeRangeSelector' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' -import React from 'react' +import AIProviderSelect, { AIProvider } from '@core/components/AIProviderSelect' +import { getTime } from '@dashboard/utils/dashboard' +import { Alert, App, Button, Typography } from 'antd' +import dayjs from 'dayjs' +import React, { useEffect, useRef, useState } from 'react' +import { useNavigate, useSearchParams } from 'react-router-dom' +import { APIKey } from './types' + +const ApiSettings: React.FC = () => { + const pageListRef = useRef(null) + const { modal, message } = App.useApp() + const [searchParams] = useSearchParams() + const [selectedProvider, setSelectedProvider] = useState(searchParams.get('modelId') || '') + const [provider, setProvider] = useState() + const { fetchData } = useFetch() + const [searchWord, setSearchWord] = useState('') + const [total, setTotal] = useState(0) + const [timeButton, setTimeButton] = useState('day') + const navigate = useNavigate() + const [timeRange, setTimeRange] = useState<{ start: number | null; end: number | null }>({ + start: null, + end: null + }) + const [queryBtnLoading, setQueryBtnLoading] = useState(false) + + useEffect(() => { + pageListRef.current?.reload() + }, [selectedProvider]) + + const requestApis = async (params: any) => { + if (!selectedProvider) return + setQueryBtnLoading(true) + try { + const eoParams = { + provider: selectedProvider, + page_size: params.pageSize, + keyword: searchWord, + page: params.current, + start: timeRange.start, + end: timeRange.end + } + if (!timeRange || !timeRange.start) { + const { startTime, endTime } = getTime(timeButton, []) + eoParams.start = startTime + eoParams.end = endTime + } + const response = await fetchData>('ai/apis', { + method: 'GET', + eoParams + }) + setQueryBtnLoading(false) + if (response.code === STATUS_CODE.SUCCESS) { + setTotal(response.data.total) + return { + data: response.data.apis, + 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[] = [ + { + title: '', + key: 'option', + btnNums: 4, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: APIKey) => [ + handleEdit(entity)} + btnTitle={$t('编辑')} + /> + ] + } + ] + + const columns: PageProColumns[] = [ + { + title: 'AI 服务', + dataIndex: 'name', + key: 'name', + width: 180 + }, + { + title: 'API URL', + dataIndex: 'request_path', + key: 'request_path', + width: 200, + ellipsis: true + }, + { + title: '模型', + dataIndex: ['model', 'name'], + key: 'model', + width: 150, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum: {} + }, + { + title: '已用 Token', + dataIndex: 'use_token', + key: 'use_token', + width: 120, + sorter: true + }, + { + title: '是否放行', + dataIndex: 'disabled', + ellipsis: true, + filters: true, + onFilter: true, + valueType: 'select', + valueEnum: { + true: { text: {$t('拦截')} }, + false: { text: {$t('放行')} } + } + }, + { + title: '编辑时间', + dataIndex: 'update_time', + key: 'update_time', + width: 200, + render: (time: string) => {dayjs(time).format('YYYY-MM-DD HH:mm:ss')} + }, + ...operation + ] + + const resetQuery = () => { + setTimeButton('day') + setTimeRange({ start: null, end: null }) + setSearchWord('') + } + + const getData = () => { + pageListRef.current?.reload() + } + + const renderProviderBanner = () => { + if (!provider) return null + + console.log(provider) + if (provider.status === 'disabled' || provider.status === 'abnormal') { + const message = + provider.status === 'disabled' + ? $t(`当前供应商异常,以下API均临时调用 ${provider.backupName} 下的 ${provider.backupModel} 模型能力。`) + : $t(`当前供应商异常,以下API均临时调用 ${provider.backupName} 下的 ${provider.backupModel} 模型能力。`) + + return ( + navigate('/aisetting')}> + {$t('查看详情')} + + } + /> + ) + } + return null + } -const AIApis: React.FC = () => { return ( + + { + setSelectedProvider(value) + setProvider(option) + }} + /> + + {renderProviderBanner()} + > + } showBorder={false} scrollPage={false} - > + > + + + { + setTimeRange($event) + }} + /> + + {$t('重置')} + { + setQueryBtnLoading(true) + getData() + }} + > + {$t('查询')} + + + + } + request={requestApis} + onSearchWordChange={(e) => { + setSearchWord(e.target.value) + }} + showPagination={true} + searchPlaceholder={$t('请输入 APIURL 搜索')} + columns={columns} + /> + + ) } -export default AIApis +export default ApiSettings diff --git a/frontend/packages/core/src/pages/aiApis/types.ts b/frontend/packages/core/src/pages/aiApis/types.ts new file mode 100644 index 00000000..a03bb2b1 --- /dev/null +++ b/frontend/packages/core/src/pages/aiApis/types.ts @@ -0,0 +1,16 @@ + + +export interface APIKey extends EditAPIKey { + status: 'normal' | 'exceeded' | 'expired' | 'disabled' | 'error' + use_token: number + update_time: string + can_delete: boolean + priority: number +} + +export interface EditAPIKey { + id?: string + name: string + config: string + expire_time: number +} diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx index e983f90d..3ed0819c 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterCreate.tsx @@ -1,354 +1,422 @@ -import {App, Button, Form, Input, InputNumber, Row, Select, Space, Spin, Tag} from "antd"; -import { MutableRefObject, useEffect, useMemo, useRef, useState} from "react"; -import {BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; -import {useFetch} from "@common/hooks/http.ts"; -import { $t } from "@common/locales/index.ts"; -import { LoadingOutlined } from "@ant-design/icons"; -import InsidePage from "@common/components/aoplatform/InsidePage.tsx"; -import { Icon } from "@iconify/react/dist/iconify.js"; -import { RouterParams } from "@core/components/aoplatform/RenderRoutes.tsx"; -import { useNavigate, useParams } from "react-router-dom"; -import { useAiServiceContext } from "@core/contexts/AiServiceContext.tsx"; -import EditableTableNotAutoGen from "@common/components/aoplatform/EditableTableNotAutoGen.tsx"; -import { AI_SERVICE_VARIABLES_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx"; -import { VariableItems } from "@core/const/ai-service/type.ts"; -import PromptEditorResizable from '@common/components/aoplatform/prompt-editor/PromptEditorResizable.tsx'; -import { DrawerWithFooter } from "@common/components/aoplatform/DrawerWithFooter"; -import AiServiceRouterModelConfig, { AiServiceRouterModelConfigHandle } from "./AiServiceInsideRouterModelConfig"; -import { AiProviderDefaultConfig, AiProviderLlmsItems } from "@core/pages/aiSetting/AiSettingList"; -import { EditableFormInstance } from "@ant-design/pro-components"; -import { validateUrlSlash } from "@common/utils/validate"; -import { API_PATH_MATCH_RULES } from "@core/const/system/const"; -import { useGlobalContext } from "@common/contexts/GlobalStateContext"; +import { LoadingOutlined } from '@ant-design/icons' +import { EditableFormInstance } from '@ant-design/pro-components' +import { DrawerWithFooter } from '@common/components/aoplatform/DrawerWithFooter' +import EditableTableNotAutoGen from '@common/components/aoplatform/EditableTableNotAutoGen.tsx' +import InsidePage from '@common/components/aoplatform/InsidePage.tsx' +import PromptEditorResizable from '@common/components/aoplatform/prompt-editor/PromptEditorResizable.tsx' +import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' +import { useFetch } from '@common/hooks/http.ts' +import { $t } from '@common/locales/index.ts' +import { validateUrlSlash } from '@common/utils/validate' +import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx' +import { AI_SERVICE_VARIABLES_TABLE_COLUMNS } from '@core/const/ai-service/const.tsx' +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, Select, 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' type AiServiceRouterField = { - name:string - path:string - prompt:string - variables:Array<{key:string, description:string, require:true}> - description:string - timeout:number - retry:number + name: string + path: string + prompt: string + variables: Array<{ key: string; description: string; require: true }> + description: string + timeout: number + retry: number + disabled: boolean } type AiServiceRouterConfig = { - name:string - path:string - aiPrompt:{ - prompt:string - variables:Array<{key:string, description:string, require:true}> - } - aiModel:{ - id:string - config:string - } - description:string - timeout:number - retry:number + name: string + path: string + aiPrompt: { + prompt: string + variables: Array<{ key: string; description: string; require: true }> + } + aiModel: { + id: string + config: string + } + description: string + timeout: number + retry: number } const AiServiceInsideRouterCreate = () => { - const navigator = useNavigate() - const { message } = App.useApp() - const {serviceId, teamId,routeId} = useParams() - const [form] = Form.useForm(); - const {fetchData} = useFetch() - const [loading, setLoading] = useState(false) - const {apiPrefix,prefixForce ,aiServiceInfo} = useAiServiceContext() - const [variablesTable,setVariablesTable] = useState([]) - const [drawerType,setDrawerType]= useState<'edit'|undefined>() - const [open, setOpen] = useState(false); - const drawerAddFormRef = useRef(null) - const [defaultLlm, setDefaultLlm] = useState() - const [llmList, setLlmList] = useState([]) - const [variablesTableRef, setVariablesTableRef] = useState | undefined>>() - const {state} = useGlobalContext() - - const onFinish = ()=>{ - return variablesTableRef?.current?.validateFields().then(()=>{ - return form.validateFields().then((formValue)=>{ - const {name, path, description, variables, prompt, timeout, retry,pathMatch} = formValue - const body = { - name, - path: `${prefixForce ? apiPrefix + '/' : ''}${path.trim()}${pathMatch === 'prefix' ? '/*' : ''}`, - description,timeout, retry,aiPrompt:{variables:variables, prompt:prompt},aiModel:{id:defaultLlm?.id, provider:defaultLlm?.provider, config:defaultLlm?.config}} - return fetchData>('service/ai-router',{method: routeId ? 'PUT' : 'POST',eoBody:(body), eoParams: {service:serviceId,team:teamId, ...(routeId ? {router:routeId}: {})},eoTransformKeys:['aiPrompt','aiModel']}).then(response=>{ - const {code,msg} = response - if(code === STATUS_CODE.SUCCESS){ - message.success(msg || $t(RESPONSE_TIPS.success)) - navigator(`/service/${teamId}/aiInside/${serviceId}/route`) - return Promise.resolve(true) - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - return Promise.reject(msg || $t(RESPONSE_TIPS.error)) - } - }).catch(errInfo=>Promise.reject(errInfo)) - }) - }) - .catch(errInfo=>Promise.reject(errInfo)) - } + const navigator = useNavigate() + const { message } = App.useApp() + const { serviceId, teamId, routeId } = useParams() + const [form] = Form.useForm() + const { fetchData } = useFetch() + const [loading, setLoading] = useState(false) + const { apiPrefix, prefixForce, aiServiceInfo } = useAiServiceContext() + const [variablesTable, setVariablesTable] = useState([]) + const [drawerType, setDrawerType] = useState<'edit' | undefined>() + const [open, setOpen] = useState(false) + const drawerAddFormRef = useRef(null) + const [defaultLlm, setDefaultLlm] = useState() + const [llmList, setLlmList] = useState([]) + const [variablesTableRef, setVariablesTableRef] = useState | undefined>>() + const { state } = useGlobalContext() - const openDrawer = (type:'edit')=>{ - setDrawerType(type) - } - - useEffect(()=>{drawerType !== undefined ? setOpen(true):setOpen(false)},[drawerType]) - - const getRouterConfig = ()=>{ - setLoading(true) - fetchData>('service/ai-router',{method:'GET',eoParams:{service:serviceId,team:teamId, router:routeId}, eoTransformKeys:['ai_model', 'ai_prompt']}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - const {path, aiPrompt,aiModel} = data.api - let newPath = path - let pathMatch = 'full' - if(prefixForce && path?.startsWith(apiPrefix + '/')){ - newPath = path.slice((apiPrefix?.length || 0) + 1) - } - if(newPath.endsWith('/*')){ - newPath = newPath.slice(0,-2) - pathMatch = 'prefix' - } - form.setFieldsValue({ - ...data.api, - ...aiPrompt, - path:newPath, - pathMatch}) - setVariablesTable(aiPrompt.variables as VariableItems[]) - setDefaultLlm(prev => ({...prev, provider: aiModel?.provider, id:aiModel?.id, config:aiModel.config}) as (AiProviderDefaultConfig & { config: string; })) - getDefaultModelConfig(aiModel?.provider) - }else{ + const onFinish = () => { + return variablesTableRef?.current + ?.validateFields() + .then(() => { + return form.validateFields().then((formValue) => { + const { name, path, description, variables, prompt, timeout, retry, pathMatch, disabled } = formValue + const body = { + name, + path: `${prefixForce ? apiPrefix + '/' : ''}${path.trim()}${pathMatch === 'prefix' ? '/*' : ''}`, + description, + timeout, + retry, + aiPrompt: { variables: variables, prompt: prompt }, + aiModel: { id: defaultLlm?.id, provider: defaultLlm?.provider, config: defaultLlm?.config }, + disabled + } + return fetchData>('service/ai-router', { + method: routeId ? 'PUT' : 'POST', + eoBody: body, + eoParams: { service: serviceId, team: teamId, ...(routeId ? { router: routeId } : {}) }, + eoTransformKeys: ['aiPrompt', 'aiModel'] + }) + .then((response) => { + const { code, msg } = response + if (code === STATUS_CODE.SUCCESS) { + message.success(msg || $t(RESPONSE_TIPS.success)) + navigator(`/service/${teamId}/aiInside/${serviceId}/route`) + return Promise.resolve(true) + } else { message.error(msg || $t(RESPONSE_TIPS.error)) - } - }).catch((errorInfo)=> console.error(errorInfo)) - .finally(()=>setLoading(false)) - } - - const getDefaultModelConfig = (provider?:string)=>{ - fetchData>('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)) - } - }).catch((errorInfo)=> console.error(errorInfo)) - } - - - useEffect(()=>{ - !routeId && aiServiceInfo?.provider && getDefaultModelConfig() - },[ - aiServiceInfo - ]) - - - useEffect(() => { - if(routeId){ - getRouterConfig() - }else{ - form.setFieldsValue({ - prefix:apiPrefix, - variables:[{key:'Query',value:'',require:true}], - prompt:'{{Query}}', - retry:0, - timeout:300000, - pathMatch:'prefix' + return Promise.reject(msg || $t(RESPONSE_TIPS.error)) + } }) - } - return (form.setFieldsValue({})) - }, []); - - const addVariable = ()=>{ - form.setFieldsValue({ - variables:[...form.getFieldValue('variables'),{key:'',value:'',require:true}] + .catch((errInfo) => Promise.reject(errInfo)) }) - } + }) + .catch((errInfo) => Promise.reject(errInfo)) + } - const handleVariablesChange = (newKeys:string[])=>{ - const variables = form.getFieldValue('variables') || [] - const variablesKeys = variables?.map(({key}:{key:string})=>(key)) - for(const key of newKeys){ - if(!variablesKeys ||variablesKeys.indexOf(key) === -1){ - variables.push({key, value:'',require:true}) - } + const openDrawer = (type: 'edit') => { + setDrawerType(type) + } + + useEffect(() => { + drawerType !== undefined ? setOpen(true) : setOpen(false) + }, [drawerType]) + + const getRouterConfig = () => { + setLoading(true) + fetchData>('service/ai-router', { + method: 'GET', + eoParams: { service: serviceId, team: teamId, router: routeId }, + eoTransformKeys: ['ai_model', 'ai_prompt'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + const { path, aiPrompt, aiModel } = data.api + let newPath = path + let pathMatch = 'full' + if (prefixForce && path?.startsWith(apiPrefix + '/')) { + newPath = path.slice((apiPrefix?.length || 0) + 1) + } + if (newPath.endsWith('/*')) { + newPath = newPath.slice(0, -2) + pathMatch = 'prefix' + } + form.setFieldsValue({ + ...data.api, + ...aiPrompt, + path: newPath, + pathMatch + }) + setVariablesTable(aiPrompt.variables as VariableItems[]) + setDefaultLlm( + (prev) => + ({ + ...prev, + provider: aiModel?.provider, + id: aiModel?.id, + config: aiModel.config + }) as AiProviderDefaultConfig & { config: string } + ) + getDefaultModelConfig(aiModel?.provider) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) } - form.setFieldsValue({ - variables:[...variables] - }) - setVariablesTable(variables as VariableItems[]) - } + }) + .catch((errorInfo) => console.error(errorInfo)) + .finally(() => setLoading(false)) + } - - const handleValuesChange = (changedValues:Record) => { - if(changedValues.variables){ - setVariablesTable(changedValues.variables as VariableItems[]) + const getDefaultModelConfig = (provider?: string) => { + fetchData>('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)) } - }; + }) + .catch((errorInfo) => console.error(errorInfo)) + } - - const handlerSubmit:() => Promise|undefined= ()=>{ - return drawerAddFormRef.current?.save()?.then((res:{id:string, config:string})=>{ - setDefaultLlm(prev => ({...prev, provider:res.provider, id:res.id, config:res.config, logo:llmList?.find((x:AiProviderLlmsItems)=>x.id === res.id)?.logo}) as (AiProviderDefaultConfig & { config: string; })) - return true}) + useEffect(() => { + !routeId && aiServiceInfo?.provider && getDefaultModelConfig() + }, [aiServiceInfo]) + + useEffect(() => { + if (routeId) { + getRouterConfig() + } else { + form.setFieldsValue({ + prefix: apiPrefix, + variables: [{ key: 'Query', value: '', require: true }], + prompt: '{{Query}}', + retry: 0, + timeout: 300000, + pathMatch: 'prefix' + }) } + return form.setFieldsValue({}) + }, []) - const onClose = () => { - setDrawerType(undefined); - }; + const addVariable = () => { + form.setFieldsValue({ + variables: [...form.getFieldValue('variables'), { key: '', value: '', require: true }] + }) + } - const apiPathMatchRulesOptions = useMemo(()=>API_PATH_MATCH_RULES.map( - x=>({label:$t(x.label), value:x.value})),[state.language]) + const handleVariablesChange = (newKeys: string[]) => { + const variables = form.getFieldValue('variables') || [] + const variablesKeys = variables?.map(({ key }: { key: string }) => key) + for (const key of newKeys) { + if (!variablesKeys || variablesKeys.indexOf(key) === -1) { + variables.push({ key, value: '', require: true }) + } + } + form.setFieldsValue({ + variables: [...variables] + }) + setVariablesTable(variables as VariableItems[]) + } - return ( - - - } iconPosition='end' onClick={()=>openDrawer('edit')}> - - - {defaultLlm?.id || defaultLlm?.defaultLlm} - {defaultLlm?.scopes?.map(x=>{x?.toLocaleUpperCase()})} - - - - - {$t('保存')} - + const handleValuesChange = (changedValues: Record) => { + if (changedValues.variables) { + setVariablesTable(changedValues.variables as VariableItems[]) + } + } + + const handlerSubmit: () => Promise | undefined = () => { + return drawerAddFormRef.current?.save()?.then((res: { id: string; config: string }) => { + setDefaultLlm( + (prev) => + ({ + ...prev, + provider: res.provider, + id: res.id, + config: res.config, + logo: llmList?.find((x: AiProviderLlmsItems) => x.id === res.id)?.logo + }) as AiProviderDefaultConfig & { config: string } + ) + return true + }) + } + + const onClose = () => { + setDrawerType(undefined) + } + + const apiPathMatchRulesOptions = useMemo( + () => API_PATH_MATCH_RULES.map((x) => ({ label: $t(x.label), value: x.value })), + [state.language] + ) + + return ( + + } + iconPosition="end" + onClick={() => openDrawer('edit')} + > + + + {defaultLlm?.id || defaultLlm?.defaultLlm} + {defaultLlm?.scopes?.map((x) => {x?.toLocaleUpperCase()})} + + + + + {$t('保存')} + + + } + > + } + spinning={loading} + wrapperClassName=" pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X" + > + + + + + className="flex-1" + label={$t('路由名称')} + name="name" + rules={[{ required: true, whitespace: true }]} + > + + + + + + + + + + name="path" + rules={[ + { required: true, whitespace: true }, + { + validator: validateUrlSlash + } + ]} + noStyle + > + { + if ((e.target.value as string).endsWith('/*')) { + form.setFieldValue('path', e.target.value.slice(0, -2)) + form.setFieldValue('pathMatch', 'prefix') + } + }} + /> + + + + + + label={$t('提示词')} name="prompt"> + + + + + label={ + + {$t('变量')} + + + New + - }> - } spinning={loading} wrapperClassName=' pb-PAGE_INSIDE_B pr-PAGE_INSIDE_X'> - - - - - className="flex-1" - label={$t("路由名称")} - name="name" - rules={[{ required: true,whitespace:true }]} - > - - - - - - - - - - - name="path" - rules={[{ required: true,whitespace:true }, - { - validator: validateUrlSlash, - }]} - noStyle - > - { - if((e.target.value as string).endsWith('/*')){ - form.setFieldValue('path',e.target.value.slice(0,-2)) - form.setFieldValue('pathMatch','prefix') - } - }}/> - - - + } + name="variables" + className="[&>.ant-row>.ant-col>label]:w-full" + > + + getFromRef={setVariablesTableRef} + configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS} + /> + - + label={$t('描述')} name="description"> + + - - label={$t("提示词")} - name="prompt" - > - - - - - label={{$t("变量")}New} - name="variables" - className="[&>.ant-row>.ant-col>label]:w-full" - > - - getFromRef={setVariablesTableRef} - configFields={AI_SERVICE_VARIABLES_TABLE_COLUMNS} - /> - - - - label={$t("描述")} - name="description" - > - - - - - - className="flex-1" - label={$t("请求超时时间")} - name={'timeout'} - rules={[{required: true}]} - > - - - - className="flex-1" - label={$t("重试次数")} - name={'retry'} - rules={[{required: true}]} - > - - - - - - - - - handlerSubmit()} - > - - - - ) + + + className="flex-1" + label={$t('请求超时时间')} + name={'timeout'} + rules={[{ required: true }]} + > + + + + className="flex-1" + label={$t('重试次数')} + name={'retry'} + rules={[{ required: true }]} + > + + + + + label={$t('拦截接口')} + name="disabled" + extra={$t('开启拦截后,网关会拦截所有该路径的请求。')} + > + + + + + + handlerSubmit()}> + + + + ) } export default AiServiceInsideRouterCreate - - - \ No newline at end of file diff --git a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx index 88a09f72..007f64f6 100644 --- a/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx +++ b/frontend/packages/core/src/pages/aiService/api/AiServiceInsideRouterList.tsx @@ -1,182 +1,224 @@ -import PageList, { PageProColumns } from "@common/components/aoplatform/PageList.tsx" -import {ActionType} from "@ant-design/pro-components"; -import {FC, useEffect, useMemo, useRef, useState} from "react"; -import {Link, useNavigate, useParams} from "react-router-dom"; -import {useBreadcrumb} from "@common/contexts/BreadcrumbContext.tsx"; -import {App, Divider} from "antd"; -import {BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE} from "@common/const/const.tsx"; -import { SimpleMemberItem} from '@common/const/type.ts' -import {useFetch} from "@common/hooks/http.ts"; -import {RouterParams} from "@core/components/aoplatform/RenderRoutes.tsx"; -import TableBtnWithPermission from "@common/components/aoplatform/TableBtnWithPermission.tsx"; -import { useGlobalContext } from "@common/contexts/GlobalStateContext.tsx"; -import { checkAccess } from "@common/utils/permission.ts"; -import { $t } from "@common/locales/index.ts"; -import { AiServiceRouterTableListItem } from "@core/const/ai-service/type.ts"; -import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from "@core/const/ai-service/const.tsx"; +import { ActionType } from '@ant-design/pro-components' +import PageList, { PageProColumns } from '@common/components/aoplatform/PageList.tsx' +import TableBtnWithPermission from '@common/components/aoplatform/TableBtnWithPermission.tsx' +import { BasicResponse, COLUMNS_TITLE, DELETE_TIPS, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const.tsx' +import { SimpleMemberItem } from '@common/const/type.ts' +import { useBreadcrumb } from '@common/contexts/BreadcrumbContext.tsx' +import { useGlobalContext } from '@common/contexts/GlobalStateContext.tsx' +import { useFetch } from '@common/hooks/http.ts' +import { $t } from '@common/locales/index.ts' +import { checkAccess } from '@common/utils/permission.ts' +import { RouterParams } from '@core/components/aoplatform/RenderRoutes.tsx' +import { AI_SERVICE_ROUTER_TABLE_COLUMNS } from '@core/const/ai-service/const.tsx' +import { AiServiceRouterTableListItem } from '@core/const/ai-service/type.ts' +import { App, Divider, Typography } from 'antd' +import { FC, useEffect, useMemo, useRef, useState } from 'react' +import { Link, useNavigate, useParams } from 'react-router-dom' -const AiServiceInsideRouterList:FC = ()=>{ - const [searchWord, setSearchWord] = useState('') - const { setBreadcrumb } = useBreadcrumb() - const { modal,message } = App.useApp() - const [tableListDataSource, setTableListDataSource] = useState([]); - const [tableHttpReload, setTableHttpReload] = useState(true); - const {fetchData} = useFetch() - const pageListRef = useRef(null); - const [memberValueEnum, setMemberValueEnum] = useState([]) - const {accessData,state} = useGlobalContext() - const {serviceId, teamId} = useParams() - const navigator = useNavigate() +const AiServiceInsideRouterList: FC = () => { + const [searchWord, setSearchWord] = useState('') + const { setBreadcrumb } = useBreadcrumb() + const { modal, message } = App.useApp() + const [tableListDataSource, setTableListDataSource] = useState([]) + const [tableHttpReload, setTableHttpReload] = useState(true) + const { fetchData } = useFetch() + const pageListRef = useRef(null) + const [memberValueEnum, setMemberValueEnum] = useState([]) + const { accessData, state } = useGlobalContext() + const { serviceId, teamId } = useParams() + const navigator = useNavigate() - const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[], success: boolean }>=> { - if(!tableHttpReload){ - setTableHttpReload(true) - return Promise.resolve({ - data: tableListDataSource, - success: true, - }); - } - - return fetchData>('service/ai-routers',{method:'GET',eoParams:{service:serviceId,team:teamId, keyword:searchWord},eoTransformKeys:['request_path','create_time','update_time','disable']}).then(response=>{ - const {code,data,msg} = response - if(code === STATUS_CODE.SUCCESS){ - setTableListDataSource(data.apis) - setTableHttpReload(false) - return {data:data.apis, success: true} - }else{ - message.error(msg || $t(RESPONSE_TIPS.error)) - return {data:[], success:false} - } - }).catch(() => { - return {data:[], success:false} - }) + const getRoutesList = (): Promise<{ data: AiServiceRouterTableListItem[]; success: boolean }> => { + if (!tableHttpReload) { + setTableHttpReload(true) + return Promise.resolve({ + data: tableListDataSource, + success: true + }) } - const deleteRoute = (entity:AiServiceRouterTableListItem)=>{ - return new Promise((resolve, reject)=>{ - fetchData>('service/ai-router',{method:'DELETE',eoParams:{service:serviceId,team:teamId, router:entity!.id}}).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)) - }) - } - - const openModal = async (type: 'delete',entity:AiServiceRouterTableListItem) =>{ - let title:string = '' - let content:string|React.ReactNode = '' - switch (type){ - case 'delete': - title=$t('删除') - content=$t(DELETE_TIPS.default) - break; + return fetchData>('service/ai-routers', { + method: 'GET', + eoParams: { service: serviceId, team: teamId, keyword: searchWord }, + eoTransformKeys: ['request_path', 'create_time', 'update_time', 'disable'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setTableListDataSource(data.apis) + setTableHttpReload(false) + return { data: data.apis, success: true } + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + return { data: [], success: false } } + }) + .catch(() => { + return { data: [], success: false } + }) + } - modal.confirm({ - title, - content, - onOk:()=> { - switch (type){ - case 'delete': - return deleteRoute(entity).then((res)=>{if(res === true) manualReloadTable()}) - } - }, - width:600, - okText:$t('确认'), - okButtonProps:{ - disabled : !checkAccess( `team.service.router.${type}`, accessData ) - }, - cancelText:$t('取消'), - closable:true, - icon:<>>, - }) - } - - const operation:PageProColumns[] =[ - { - title: COLUMNS_TITLE.operate, - key: 'option', - btnNums:2, - fixed:'right', - valueType: 'option', - render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [ - {navigator(`/service/${teamId}/aiInside/${serviceId}/route/${entity.id}`)}} btnTitle="编辑"/>, - , - {openModal('delete',entity)}} btnTitle="删除"/>, - ], - } - ] - - const manualReloadTable = () => { - setTableHttpReload(true); // 表格数据需要从后端接口获取 - pageListRef.current?.reload() - }; - - const getMemberList = async ()=>{ - setMemberValueEnum([]) - const {code,data,msg} = await fetchData>('simple/member',{method:'GET'}) - if(code === STATUS_CODE.SUCCESS){ - setMemberValueEnum(data.members) - }else{ + const deleteRoute = (entity: AiServiceRouterTableListItem) => { + return new Promise((resolve, reject) => { + fetchData>('service/ai-router', { + method: 'DELETE', + eoParams: { service: serviceId, team: teamId, router: entity!.id } + }) + .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)) + }) + } + + const openModal = async (type: 'delete', entity: AiServiceRouterTableListItem) => { + let title: string = '' + let content: string | React.ReactNode = '' + switch (type) { + case 'delete': + title = $t('删除') + content = $t(DELETE_TIPS.default) + break } - useEffect(() => { - setBreadcrumb([ - { - title:{$t('服务')} - }, - { - title:$t('路由') - } - ]) - getMemberList() - manualReloadTable() - }, [serviceId]); - - const columns = useMemo(()=>{ - return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map(x=>{ - if(x.filters &&((x.dataIndex as string[])?.indexOf('creator') !== -1) ){ - const tmpValueEnum:{[k:string]:{text:string}} = {} - memberValueEnum?.forEach((x:SimpleMemberItem)=>{ - tmpValueEnum[x.name] = {text:x.name} - }) - x.valueEnum = tmpValueEnum - } - - return {...x,title:typeof x.title === 'string' ? $t(x.title as string) : x.title}}) - },[memberValueEnum,state.language]) + modal.confirm({ + title, + content, + onOk: () => { + switch (type) { + case 'delete': + return deleteRoute(entity).then((res) => { + if (res === true) manualReloadTable() + }) + } + }, + width: 600, + okText: $t('确认'), + okButtonProps: { + disabled: !checkAccess(`team.service.router.${type}`, accessData) + }, + cancelText: $t('取消'), + closable: true, + icon: <>> + }) + } + const operation: PageProColumns[] = [ + { + title: COLUMNS_TITLE.operate, + key: 'option', + btnNums: 2, + fixed: 'right', + valueType: 'option', + render: (_: React.ReactNode, entity: AiServiceRouterTableListItem) => [ + { + navigator(`/service/${teamId}/aiInside/${serviceId}/route/${entity.id}`) + }} + btnTitle="编辑" + />, + , + { + openModal('delete', entity) + }} + btnTitle="删除" + /> + ] + } + ] - return ( - <> - getRoutesList()} - dataSource={tableListDataSource} - addNewBtnTitle={$t('添加路由')} - searchPlaceholder={$t('输入 URL 查找路由')} - onAddNewBtnClick={()=>{navigator(`/service/${teamId}/aiInside/${serviceId}/route/create`)}} - addNewBtnAccess="team.service.router.add" - tableClickAccess="team.service.router.view" - manualReloadTable={manualReloadTable} - onSearchWordChange={(e)=>{setSearchWord(e.target.value)}} - onChange={() => { - setTableHttpReload(false) - }} - onRowClick={(row:AiServiceRouterTableListItem)=>navigator(`/service/${teamId}/aiInside/${serviceId}/route/${row.id}`)} - tableClass="mr-PAGE_INSIDE_X " - /> - > - ) + const manualReloadTable = () => { + setTableHttpReload(true) // 表格数据需要从后端接口获取 + pageListRef.current?.reload() + } + const getMemberList = async () => { + setMemberValueEnum([]) + const { code, data, msg } = await fetchData>('simple/member', { + method: 'GET' + }) + if (code === STATUS_CODE.SUCCESS) { + setMemberValueEnum(data.members) + } else { + message.error(msg || $t(RESPONSE_TIPS.error)) + } + } + + useEffect(() => { + setBreadcrumb([ + { + title: {$t('服务')} + }, + { + title: $t('路由') + } + ]) + getMemberList() + manualReloadTable() + }, [serviceId]) + + const columns = useMemo(() => { + return [...AI_SERVICE_ROUTER_TABLE_COLUMNS].map((x) => { + if (x.filters && (x.dataIndex as string[])?.indexOf('creator') !== -1) { + const tmpValueEnum: { [k: string]: { text: string } } = {} + memberValueEnum?.forEach((x: SimpleMemberItem) => { + tmpValueEnum[x.name] = { text: x.name } + }) + x.valueEnum = tmpValueEnum + } + if (x.filters && (x.dataIndex as string[])?.indexOf('disabled') !== -1) { + x.valueEnum = { + true: { text: {$t('拦截')} }, + false: { text: {$t('放行')} } + } + } + + return { ...x, title: typeof x.title === 'string' ? $t(x.title as string) : x.title } + }) + }, [memberValueEnum, state.language]) + + return ( + <> + getRoutesList()} + dataSource={tableListDataSource} + addNewBtnTitle={$t('添加路由')} + searchPlaceholder={$t('输入 URL 查找路由')} + onAddNewBtnClick={() => { + navigator(`/service/${teamId}/aiInside/${serviceId}/route/create`) + }} + addNewBtnAccess="team.service.router.add" + tableClickAccess="team.service.router.view" + manualReloadTable={manualReloadTable} + onSearchWordChange={(e) => { + setSearchWord(e.target.value) + }} + onChange={() => { + setTableHttpReload(false) + }} + onRowClick={(row: AiServiceRouterTableListItem) => + navigator(`/service/${teamId}/aiInside/${serviceId}/route/${row.id}`) + } + tableClass="mr-PAGE_INSIDE_X " + /> + > + ) } -export default AiServiceInsideRouterList \ No newline at end of file +export default AiServiceInsideRouterList diff --git a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx index 06bf6549..79611d80 100644 --- a/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AIFlowChart.tsx @@ -1,6 +1,9 @@ 'use client' +import { BasicResponse } from '@common/const/const' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' import { CoordinateExtent, Edge, @@ -13,28 +16,26 @@ import { useNodesState } from '@xyflow/react' import '@xyflow/react/dist/style.css' +import { Button, Space, Spin } from 'antd' import { useCallback, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' import CustomEdge from './components/CustomEdge' import { KeyStatusNode } from './components/KeyStatusNode' import { ModelCardNode } from './components/ModelCardNode' import { ServiceCardNode } from './components/NodeComponents' import { LAYOUT } from './constants' import './styles.css' -import { ModelData } from './types' +import { ModelListData } from './types' -interface ApiResponse { - data: { - backup: { - id: string - name: string - } - providers: ModelData[] +export type ApiResponse = BasicResponse<{ + backup: { + id: string + name: string } - code: number - success: string -} + providers: ModelListData[] +}> -const calculateNodePositions = (models: ModelData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => { +const calculateNodePositions = (models: ModelListData[], startY = LAYOUT.NODE_START_Y, gap = LAYOUT.NODE_GAP) => { return models.reduce( (acc, model, index) => { const y = startY + index * gap @@ -46,7 +47,7 @@ const calculateNodePositions = (models: ModelData[], startY = LAYOUT.NODE_START_ }, [`${model.id}-keys`]: { x: LAYOUT.KEY_NODE_X, - y + y: y + 16 } } }, @@ -65,21 +66,29 @@ const edgeTypes: EdgeTypes = { } const AIFlowChart = () => { - const [modelData, setModelData] = useState([]) + const [modelData, setModelData] = useState([]) + const [loading, setLoading] = useState(false) const [nodes, setNodes, onNodesChange] = useNodesState([]) const [edges, setEdges, onEdgesChange] = useEdgesState([]) const { fetchData } = useFetch() + const { aiConfigFlushed } = useGlobalContext() + const navigate = useNavigate() useEffect(() => { - // Mock API call - replace with actual API call + setLoading(true) fetchData('ai/providers/configured', { - method: 'GET' + method: 'GET', + eoTransformKeys: ['default_llm'] // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - }).then((response) => { - const mockApiResponse: ApiResponse = response as ApiResponse - setModelData(mockApiResponse.data.providers) }) - }, []) + .then((response) => { + const mockApiResponse: ApiResponse = response as ApiResponse + setModelData(mockApiResponse.data.providers) + }) + .finally(() => { + setLoading(false) + }) + }, [aiConfigFlushed]) useEffect(() => { if (!modelData.length) return @@ -87,14 +96,13 @@ const AIFlowChart = () => { const positions = calculateNodePositions(modelData) // subtract 5 to make sure the service node is aligned with the top model node const serviceY = positions[modelData[0].id].y - 5 - const newNodes = [ { id: 'apiService', type: 'serviceCard', position: { x: LAYOUT.SERVICE_NODE_X, y: serviceY }, data: { - title: 'API Service', + title: 'API Services', count: modelData.length } }, @@ -103,10 +111,11 @@ const AIFlowChart = () => { type: 'modelCard', position: positions[model.id], data: { - title: model.name, + name: model.name, status: model.status, - defaultModel: model.default_llm, - logo: model.logo + defaultLlm: model.defaultLlm, + logo: model.logo, + id: model.id } })), ...modelData.map((model) => ({ @@ -114,7 +123,7 @@ const AIFlowChart = () => { type: 'keyCard', position: positions[`${model.id}-keys`], data: { - title: 'API Keys', + title: '', keys: (model.keys || []).map((key, index) => ({ id: key.id, status: key.status, @@ -138,17 +147,17 @@ const AIFlowChart = () => { source: model.id, target: `${model.id}-keys`, label: `${model.key_count} keys`, + data: { id: model.id }, animated: true })) ] - setNodes(newNodes) setEdges(newEdges) }, [modelData]) const calculateExtent = useCallback(() => { - const left = LAYOUT.SERVICE_NODE_X - 100 - const right = LAYOUT.KEY_NODE_X + 100 + const left = LAYOUT.SERVICE_NODE_X + const right = LAYOUT.KEY_NODE_X const top = 0 // Allow slight negative scroll to reduce top padding const bottom = LAYOUT.NODE_START_Y + modelData.length * LAYOUT.NODE_GAP return [ @@ -157,91 +166,91 @@ const AIFlowChart = () => { ] as CoordinateExtent }, [modelData.length]) - const onNodeDrag: any = useCallback( - (_: MouseEvent, node: Node) => { - if (node.type !== 'modelCard') return + const updateProviderOrder = async (sortedProviderIds: string[]) => { + await fetchData('ai/provider/sort', { + method: 'PUT', + body: JSON.stringify({ + providers: sortedProviderIds + }) + }) + } - setNodes((nds) => { - return nds.map((n) => { - if (n.type === 'keyCard' && n.id === `${node.id}-keys`) { + const onNodeDragStop: any = useCallback((_: any, node: Node) => { + if (node.type !== 'modelCard') return + + setNodes((nds) => { + const modelNodes = nds.filter((n) => n.type === 'modelCard') + const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y) + const sortedProviderIds = sortedNodes.map((node) => node.id) + + // Update provider order outside of setNodes callback + updateProviderOrder(sortedProviderIds) + // Update all node positions in a single pass + return nds.map((n) => { + if (n.type === 'modelCard') { + const index = sortedNodes.findIndex((sn) => sn.id === n.id) + return { + ...n, + position: { + x: LAYOUT.MODEL_NODE_X, + y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + } + } + } + if (n.type === 'keyCard') { + const modelId = n.id.replace('-keys', '') + const modelNode = sortedNodes.find((mn) => mn.id === modelId) + if (modelNode) { + const index = sortedNodes.findIndex((sn) => sn.id === modelId) return { ...n, position: { x: LAYOUT.KEY_NODE_X, - y: node.position.y + y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP + 16 } } } - return n - }) + } + return n }) - }, - [setNodes] - ) - - const onNodeDragStop: any = useCallback( - (_: any, node: Node) => { - if (node.type !== 'modelCard') return - - setNodes((nds) => { - const modelNodes = nds.filter((n) => n.type === 'modelCard') - const sortedNodes = [...modelNodes].sort((a, b) => a.position.y - b.position.y) - - return nds.map((n) => { - if (n.type === 'modelCard') { - const index = sortedNodes.findIndex((sn) => sn.id === n.id) - return { - ...n, - position: { - x: LAYOUT.MODEL_NODE_X, - y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP - } - } - } - if (n.type === 'keyCard') { - const modelId = n.id.replace('-keys', '') - const modelNode = sortedNodes.find((mn) => mn.id === modelId) - if (modelNode) { - const index = sortedNodes.findIndex((sn) => sn.id === modelId) - return { - ...n, - position: { - x: LAYOUT.KEY_NODE_X, - y: LAYOUT.NODE_START_Y + index * LAYOUT.NODE_GAP - } - } - } - } - return n - }) - }) - }, - [setNodes] - ) + }) + }, []) return ( - - + + {loading ? ( + + + + ) : modelData.length === 0 ? ( + + {$t('未配置 AI 模型')} + navigate('/aisetting?status=unconfigure')}> + {$t('前往设置')} + + + ) : ( + + )} ) } diff --git a/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx new file mode 100644 index 00000000..ba1ae334 --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/AIUnconfigure.tsx @@ -0,0 +1,136 @@ +import Icon, { LoadingOutlined } from '@ant-design/icons' +import WithPermission from '@common/components/aoplatform/WithPermission' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { App, Button, Card, Empty, Spin, Tag } from 'antd' +import { memo, useEffect, useState } from 'react' +import { useNavigate } from 'react-router-dom' +import { useAiSetting } from './contexts/AiSettingContext' +import { AiSettingListItem } from './types' + +const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => { + const { openConfigModal } = useAiSetting() + const navigate = useNavigate() + + const handleOpenModal = async (provider: AiSettingListItem) => { + await openConfigModal(provider) + navigate('/aisetting?status=configure') + } + + return ( + + + + {provider.name} + + + {provider.configured ? $t('已配置') : $t('未配置')} + + + } + className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " + classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }} + > + + + {provider.configured && ( + <> + {$t('默认')}: + {provider.defaultLlm} + > + )} + + + } + onClick={() => handleOpenModal(provider)} + classNames={{ icon: 'h-[18px]' }} + > + {$t('设置')} + + + + + ) +}) +const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => { + return ( + <> + {modelList.length > 0 ? ( + + {modelList.map((provider: AiSettingListItem) => ( + + ))} + + ) : ( + + )} + > + ) +} + +const AIUnConfigure = () => { + const [modelData, setModelData] = useState([]) + const { fetchData } = useFetch() + const [loading, setLoading] = useState(false) + const { aiConfigFlushed } = useGlobalContext() + + useEffect(() => { + setLoading(true) + fetchData[] }>>(`ai/providers/unconfigured`, { + method: 'GET', + eoTransformKeys: ['default_llm', 'default_llm_logo'] + }) + .then((response) => { + const { code, data, msg } = response + if (code === STATUS_CODE.SUCCESS) { + setModelData(data.providers) + } else { + const { message } = App.useApp() + message.error(msg || $t(RESPONSE_TIPS.error)) + } + }) + .finally(() => setLoading(false)) + }, [aiConfigFlushed]) + + return ( + } + spinning={loading} + > + {modelData && modelData.length > 0 ? ( + + {modelData.filter((item) => !item.configured).length > 0 && ( + <> + !item.configured) || []} /> + > + )} + + ) : ( + + )} + + ) +} +export default AIUnConfigure diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx index 5d2af89a..30694d5a 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingList.tsx @@ -1,243 +1,71 @@ -import { LoadingOutlined } from '@ant-design/icons' import InsidePage from '@common/components/aoplatform/InsidePage' -import WithPermission from '@common/components/aoplatform/WithPermission' -import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' -import { useGlobalContext } from '@common/contexts/GlobalStateContext' -import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' -import { checkAccess } from '@common/utils/permission' -import { Icon } from '@iconify/react/dist/iconify.js' -import { App, Button, Card, Divider, Empty, Spin, Tag } from 'antd' -import { memo, useEffect, useRef, useState } from 'react' +import { Tabs } from 'antd' +import { useEffect, useState } from 'react' +import { useSearchParams } from 'react-router-dom' import AIFlowChart from './AIFlowChart' -import AiSettingModalContent, { AiSettingModalContentHandle } from './AiSettingModal' +import AIUnConfigure from './AIUnconfigure' +import { AiSettingProvider } from './contexts/AiSettingContext' -export type AiSettingListItem = { - name: string - id: string - logo: string - defaultLlm: string - defaultLlmLogo: string - enable: boolean - configured: boolean -} +const CONTENT_STYLE = { height: 'calc(-300px + 100vh)' } as const -export type AiProviderLlmsItems = { - id: string - logo: string - scopes: ('chat' | 'completions')[] - config: string -} - -export type AiProviderDefaultConfig = { - id: string - provider: string - name: string - logo: string - defaultLlm: string - scopes: string[] -} - -export type AiProviderConfig = { - id: string - name: string - config: string - getApikeyUrl: string -} -const AiSettingList = () => { - const { modal, message } = App.useApp() - const { fetchData } = useFetch() - const [aiSettingList, setAiSettingList] = useState([]) - const [loading, setLoading] = useState(false) - const modalRef = useRef() - const { setAiConfigFlushed, accessData } = useGlobalContext() - - const getAiSettingList = () => { - setLoading(true) - return fetchData[] }>>( - `ai/providers/unconfigured`, - { method: 'GET', eoTransformKeys: ['default_llm', 'default_llm_logo'] } - // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' - ) - .then((response) => { - const { code, data, msg } = response - if (code === STATUS_CODE.SUCCESS) { - setAiSettingList( - data.providers?.map((x: AiSettingListItem) => ({ - ...x, - name: $t(x.name), - llmListStatus: 'unload', - availableLlms: [] - })) - ) - } else { - message.error(msg || $t(RESPONSE_TIPS.error)) - } - }) - .finally(() => setLoading(false)) - } - - const openModal = async (entity: AiSettingListItem) => { - message.loading($t(RESPONSE_TIPS.loading)) - const { code, data, msg } = await fetchData>('ai/provider/config', { - method: 'GET', - eoParams: { provider: entity!.id }, - eoTransformKeys: ['get_apikey_url'] - }) - message.destroy() - if (code !== STATUS_CODE.SUCCESS) { - message.error(msg || $t(RESPONSE_TIPS.error)) - return - } - modal.confirm({ - title: $t('模型配置'), - content: ( - - ), - onOk: () => { - return modalRef.current?.save().then((res) => { - if (res === true) setAiConfigFlushed(true) - getAiSettingList() - }) - }, - width: 600, - okText: $t('确认'), - footer: (_, { OkBtn, CancelBtn }) => { - return ( - - - {$t('从 (0) 获取 API KEY', [data.provider.name])} - - - - - {checkAccess('system.devops.ai_provider.edit', accessData) ? : null} - - - ) - }, - cancelText: $t('取消'), - closable: true, - icon: <>> - }) - } +const AiSettingContent = () => { + const [searchParams, setSearchParams] = useSearchParams() + const [activeKey, setActiveKey] = useState(searchParams.get('status') === 'unconfigure' ? 'config' : 'flow') useEffect(() => { - getAiSettingList() - }, []) - - const CardBox = memo(({ provider }: { provider: AiSettingListItem }) => { - return ( - - - - {provider.name} - - - {provider.configured ? $t('已配置') : $t('未配置')} - - - } - className="shadow-[0_5px_10px_0_rgba(0,0,0,0.05)] rounded-[10px] overflow-visible h-[156px] m-0 flex flex-col " - classNames={{ header: 'border-b-[0px] p-[20px] px-[24px]', body: 'pt-0 flex-1' }} - > - - - {provider.configured && ( - <> - {$t('默认')}: - {provider.defaultLlm} - > - )} - - - } - onClick={() => openModal(provider)} - classNames={{ icon: 'h-[18px]' }} - > - {$t('设置')} - - - - - ) - }) - - const ModelCardArea = ({ modelList, className }: { modelList: AiSettingListItem[]; className?: string }) => { - return ( - <> - {modelList.length > 0 ? ( - - {modelList.map((provider: AiSettingListItem) => ( - - ))} - - ) : ( - - )} - > - ) - } + const newActiveKey = searchParams.get('status') === 'unconfigure' ? 'config' : 'flow' + setActiveKey(newActiveKey) + }, [searchParams]) return ( - <> - - - } - spinning={loading} - > - {aiSettingList && aiSettingList.length > 0 ? ( - - {aiSettingList.filter((item) => !item.configured).length > 0 && ( - <> - - {$t('未配置')} - !item.configured) || []} /> - > - )} - - ) : ( - - )} - - - > + + + { + setActiveKey(key) + setSearchParams({ status: key === 'config' ? 'unconfigure' : 'configure' }) + }} + className="sticky top-0 flex-shrink-0" + items={[ + { + key: 'flow', + label: $t('已设置'), + children: ( + + + + ) + }, + { + key: 'config', + label: $t('未设置'), + children: ( + + + + ) + } + ]} + /> + + ) } + +const AiSettingList = () => { + return ( + + + + ) +} + export default AiSettingList diff --git a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx index 862a7d71..9527d009 100644 --- a/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx +++ b/frontend/packages/core/src/pages/aiSetting/AiSettingModal.tsx @@ -1,13 +1,14 @@ +import { QuestionCircleOutlined } from '@ant-design/icons' import { Codebox } from '@common/components/postcat/api/Codebox' import { BasicResponse, PLACEHOLDER, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' -import { App, Form, Select, Tag } from 'antd' +import { App, Form, InputNumber, Select, Switch, Tag, Tooltip } from 'antd' import { forwardRef, useEffect, useImperativeHandle, useState } from 'react' -import { AiProviderConfig, AiProviderLlmsItems } from './AiSettingList' +import { AiProviderLlmsItems, ModelDetailData } from './types' export type AiSettingModalContentProps = { - entity: AiProviderConfig & { defaultLlm: string } + entity: ModelDetailData & { defaultLlm: string } readOnly: boolean } @@ -15,11 +16,6 @@ export type AiSettingModalContentHandle = { save: () => Promise } -type AiSettingModalContentField = { - config: string - defaultLlm: string -} - const AiSettingModalContent = forwardRef((props, ref) => { const [form] = Form.useForm() const { message } = App.useApp() @@ -27,7 +23,7 @@ const AiSettingModalContent = forwardRef() const [loading, setLoading] = useState(false) - + const [enableState, setEnableState] = useState(entity.status === 'enabled') const getLlmList = () => { setLoading(true) fetchData>(`ai/provider/llms`, { @@ -52,12 +48,16 @@ const AiSettingModalContent = forwardRef { + const finalValue = { + ...value, + priority: Math.max(1, value.priority) + } + fetchData>('ai/provider/config', { method: 'PUT', eoParams: { provider: entity?.id }, - eoBody: value, + eoBody: finalValue, eoTransformKeys: ['defaultLlm'] + // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' }) .then((response) => { const { code, msg } = response @@ -89,21 +95,29 @@ const AiSettingModalContent = forwardRef { + if (!isChecked) { + return '保存后供应商状态变为【停用】,使用本供应商的 API 将临时使用负载优先级最高的正常供应商。' + } + return '保存后供应商状态变为【正常】,恢复调用本供应商的 AI 能力。' + } + useImperativeHandle(ref, () => ({ save })) return ( - label={$t('模型')} name="defaultLlm" rules={[{ required: true }]}> + label={$t('默认模型')} name="defaultLlm" rules={[{ required: true }]}> {x.id} - {x?.scopes?.map((s) => {s?.toLocaleUpperCase()})} + {x?.scopes?.map((s) => {s?.toLocaleUpperCase()})} ) }))} > - label={$t('参数')} name="config"> + + label={ + + {$t('负载优先级')} + + + + + } + name="priority" + rules={[ + { required: true }, + { + validator: async (_, value) => { + if (value <= 0) { + throw new Error($t('优先级必须大于 0')) + } + return Promise.resolve() + } + } + ]} + initialValue={1} + > + + + + label={$t('API Key(默认 Key)')} name="config"> + + {entity.configured && ( + + + + 当前调用状态: + {entity.status === 'enabled' && {$t('正常')}} + {entity.status === 'disabled' && {$t('停用')}} + {entity.status === 'abnormal' && {$t('异常')}} + + + { + form.setFieldsValue({ enable: checked }) + setEnableState(checked) + }} + /> + + + {(entity.status === 'enabled' && !enableState) || (entity.status !== 'enabled' && enableState) ? ( + * {getTooltipText(enableState)} + ) : null} + + )} ) }) diff --git a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx b/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx index f3cbff89..c4afd894 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/CustomEdge.tsx @@ -31,7 +31,7 @@ export default function CustomEdge({ {label && ( = ({ data }) = style={{ border: '1px solid var(--border-color)' }} > - + {title} = ({ data }) => { - const { title, status, defaultModel, logo } = data + const { name, status, defaultLlm, logo } = data + const { openConfigModal } = useAiSetting() + + const getStatusIcon = (status: ModelStatus) => { + switch (status) { + case 'enabled': + return { icon: 'mdi:check-circle', color: 'text-green-500' } + case 'disabled': + return { icon: 'mdi:pause-circle', color: 'text-gray-400' } + case 'abnormal': + return { icon: 'mdi:alert-circle', color: 'text-red-500' } + } + } + + const statusConfig = getStatusIcon(status) + return ( @@ -28,17 +37,14 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) = - + - {title} - + {name} + {/* Action buttons */} @@ -46,13 +52,15 @@ export const ModelCardNode: React.FC<{ data: ModelCardNodeData }> = ({ data }) = console.log('Default:', data.id)} + onClick={() => { + openConfigModal({ id: data.id, defaultLlm: defaultLlm } as AiSettingListItem) + }} /> {t('默认:')} - {defaultModel} + {defaultLlm} diff --git a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx b/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx index b2237fd3..176d80da 100644 --- a/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx +++ b/frontend/packages/core/src/pages/aiSetting/components/ServiceCardNode.tsx @@ -11,7 +11,7 @@ export const ServiceCardNode: React.FC = () => { - AI Service + AI Services ) diff --git a/frontend/packages/core/src/pages/aiSetting/constants.ts b/frontend/packages/core/src/pages/aiSetting/constants.ts index 9da8d084..a6635ffe 100644 --- a/frontend/packages/core/src/pages/aiSetting/constants.ts +++ b/frontend/packages/core/src/pages/aiSetting/constants.ts @@ -1,5 +1,5 @@ export const LAYOUT = { - SERVICE_NODE_X: 50, + SERVICE_NODE_X: 0, NODE_START_Y: 20, NODE_GAP: 120, MODEL_NODE_X: 500, diff --git a/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx new file mode 100644 index 00000000..f8e078bd --- /dev/null +++ b/frontend/packages/core/src/pages/aiSetting/contexts/AiSettingContext.tsx @@ -0,0 +1,89 @@ +import Icon from '@ant-design/icons' +import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' +import { useGlobalContext } from '@common/contexts/GlobalStateContext' +import { useFetch } from '@common/hooks/http' +import { $t } from '@common/locales' +import { checkAccess } from '@common/utils/permission' +import { App } from 'antd' +import { createContext, useContext, useRef } from 'react' +import AiSettingModalContent, { AiSettingModalContentHandle } from '../AiSettingModal' +import { AiSettingListItem, ModelDetailData } from '../types' + +interface AiSettingContextType { + openConfigModal: (entity: AiSettingListItem) => Promise +} + +const AiSettingContext = createContext(undefined) + +export const AiSettingProvider: React.FC<{ children: React.ReactNode }> = ({ children }) => { + const { modal, message } = App.useApp() + const { fetchData } = useFetch() + const { aiConfigFlushed, setAiConfigFlushed, accessData } = useGlobalContext() + const modalRef = useRef() + + const openConfigModal = async (entity: AiSettingListItem) => { + message.loading($t(RESPONSE_TIPS.loading)) + const { code, data, msg } = await fetchData>('ai/provider/config', { + method: 'GET', + eoParams: { provider: entity!.id }, + eoTransformKeys: ['get_apikey_url'] + }) + message.destroy() + if (code !== STATUS_CODE.SUCCESS) { + message.error(msg || $t(RESPONSE_TIPS.error)) + return + } + + modal.confirm({ + title: $t('模型配置'), + content: ( + + ), + onOk: () => { + return modalRef.current?.save().then((res) => { + if (res === true) { + setAiConfigFlushed(!aiConfigFlushed) + } + }) + }, + width: 600, + okText: $t('确认'), + footer: (_, { OkBtn, CancelBtn }) => { + return ( + + + {$t('从 (0) 获取 API KEY', [data.provider.name])} + + + + + {checkAccess('system.devops.ai_provider.edit', accessData) ? : null} + + + ) + }, + cancelText: $t('取消'), + closable: true, + icon: <>> + }) + } + + return {children} +} + +export const useAiSetting = () => { + const context = useContext(AiSettingContext) + if (!context) { + throw new Error('useAiSetting must be used within an AiSettingProvider') + } + return context +} diff --git a/frontend/packages/core/src/pages/aiSetting/styles.css b/frontend/packages/core/src/pages/aiSetting/styles.css index e12ed580..13d5b62b 100644 --- a/frontend/packages/core/src/pages/aiSetting/styles.css +++ b/frontend/packages/core/src/pages/aiSetting/styles.css @@ -1,4 +1,20 @@ /* Flow Chart Styles */ +.react-flow { + width: 100%; + height: 100%; + min-height: 500px; +} + +.react-flow__container { + width: 100%; + height: 100%; +} + +.react-flow__renderer { + width: 100%; + height: 100%; +} + .react-flow__node { padding: 0; border-radius: 8px; diff --git a/frontend/packages/core/src/pages/aiSetting/types.ts b/frontend/packages/core/src/pages/aiSetting/types.ts index 4935473e..6ba2ea7a 100644 --- a/frontend/packages/core/src/pages/aiSetting/types.ts +++ b/frontend/packages/core/src/pages/aiSetting/types.ts @@ -1,4 +1,4 @@ -export type ModelStatus = 'enable' | 'abnormal'|'disabled' +export type ModelStatus = 'enabled' | 'abnormal'|'disabled' export type KeyStatus ='normal' | 'abnormal'|'disabled' export interface KeyData { @@ -7,14 +7,51 @@ export interface KeyData { status: KeyStatus, } -export interface ModelData { +export interface ModelListData { id: string name: string logo: string - default_llm: string + defaultLlm: string status: ModelStatus api_count: number key_count: number keys: KeyData[] +} +export interface ModelDetailData extends ModelListData{ + enable:boolean + config: string, + priority?: number + getApikeyUrl: string + status: ModelStatus + configured: boolean +} + + +export type AiSettingListItem = { + name: string + id: string + logo: string + defaultLlm: string + defaultLlmLogo: string + enable: boolean + configured: boolean priority?: number } + +export type AiProviderLlmsItems = { + id: string + logo: string + scopes: ('chat' | 'completions')[] + config: string +} + +export type AiProviderDefaultConfig = { + id: string + provider: string + name: string + logo: string + defaultLlm: string + scopes: string[] +} + + diff --git a/frontend/packages/core/src/pages/keySettings/index.tsx b/frontend/packages/core/src/pages/keySettings/index.tsx index 58e8dc93..6ea97505 100644 --- a/frontend/packages/core/src/pages/keySettings/index.tsx +++ b/frontend/packages/core/src/pages/keySettings/index.tsx @@ -12,15 +12,16 @@ import AIProviderSelect, { AIProvider } from '@core/components/AIProviderSelect' import { App, Divider, Space, Typography } from 'antd' import dayjs from 'dayjs' import React, { useEffect, useRef, useState } from 'react' +import { useSearchParams } from 'react-router-dom' import ApiKeyContent from './components/ApiKeyContent' import { APIKey, EditAPIKey } from './types' const KeySettings: React.FC = () => { const pageListRef = useRef(null) const { modal, message } = App.useApp() - const [selectedProvider, setSelectedProvider] = useState() + const [searchParams] = useSearchParams() + const [selectedProvider, setSelectedProvider] = useState(searchParams.get('modelId') || '') const [provider, setProvider] = useState() - const [apiKeys, setApiKeys] = useState([]) const { fetchData } = useFetch() const [searchWord, setSearchWord] = useState('') const [total, setTotal] = useState(0) @@ -186,6 +187,8 @@ const KeySettings: React.FC = () => { page_size: params.pageSize, keyword: searchWord, page: params.current + //TODO API 筛选 + // statuses: params.statuses || [] } // eoApiPrefix: 'http://uat.apikit.com:11204/mockApi/aoplatform/api/v1/' }) @@ -306,7 +309,7 @@ const KeySettings: React.FC = () => { render: (dom: React.ReactNode, entity: APIKey) => { return entity.expire_time === 0 ? $t('永不过期') - : dayjs(Number(entity.expire_time)).format('YYYY-MM-DD HH:mm:ss') + : dayjs(Number(entity.expire_time) * 1000).format('YYYY-MM-DD HH:mm:ss') } }, ...operation diff --git a/frontend/packages/dashboard/src/component/MonitorApiPage.tsx b/frontend/packages/dashboard/src/component/MonitorApiPage.tsx index 3a824368..5bb5d2d4 100644 --- a/frontend/packages/dashboard/src/component/MonitorApiPage.tsx +++ b/frontend/packages/dashboard/src/component/MonitorApiPage.tsx @@ -1,23 +1,23 @@ import { CloseOutlined, ExpandOutlined, SearchOutlined } from '@ant-design/icons' -import { Select, Input, Button, App, Drawer } from 'antd' -import { debounce } from 'lodash-es' -import { useState, useEffect, useRef } from 'react' -import { MonitorApiData, SearchBody } from '@dashboard/const/type' -import { getTime } from '../utils/dashboard' import ScrollableSection from '@common/components/aoplatform/ScrollableSection' import TimeRangeSelector, { RangeValue, TimeRange, TimeRangeButton } from '@common/components/aoplatform/TimeRangeSelector' -import MonitorTable, { MonitorTableHandler } from './MonitorTable' import { BasicResponse, RESPONSE_TIPS, STATUS_CODE } from '@common/const/const' -import { DefaultOptionType } from 'antd/es/select' -import { useExcelExport } from '@common/hooks/excel' -import { API_TABLE_GLOBAL_COLUMNS_CONFIG } from '@dashboard/const/const' -import { useFetch } from '@common/hooks/http' import { EntityItem } from '@common/const/type' +import { useExcelExport } from '@common/hooks/excel' +import { useFetch } from '@common/hooks/http' import { $t } from '@common/locales' +import { API_TABLE_GLOBAL_COLUMNS_CONFIG } from '@dashboard/const/const' +import { MonitorApiData, SearchBody } from '@dashboard/const/type' +import { App, Button, Drawer, Input, Select } from 'antd' +import { DefaultOptionType } from 'antd/es/select' +import { debounce } from 'lodash-es' +import { useEffect, useRef, useState } from 'react' +import { getTime } from '../utils/dashboard' +import MonitorTable, { MonitorTableHandler } from './MonitorTable' export type MonitorApiPageProps = { fetchTableData: (body: SearchBody) => Promise> detailDrawerContent: React.ReactNode @@ -100,6 +100,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) { const getMonitorData = () => { let query = queryData if (!queryData || queryData.start === undefined) { + console.log(timeButton, datePickerValue) const { startTime, endTime } = getTime(timeButton, datePickerValue || []) query = { ...query, start: startTime, end: endTime } } @@ -186,7 +187,7 @@ export default function MonitorApiPage(props: MonitorApiPageProps) { } return ( - + - - {$t('服务')}: + + {$t('服务')}: - + API : ({ ...(prevData || {}), apis: value })) }} /> - {$t('路径')}: + {$t('路径')}: {/* setQueryData({ ...queryData, path: '' })} /> */}
{$t('未配置')}