From 61be6f5d2c110857d26c22bcb44689baf559d01f Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Wed, 13 Aug 2025 17:54:39 +0800 Subject: [PATCH] model auth --- .../base/form/hooks/use-get-validators.ts | 28 ++- .../model-provider-page/declarations.ts | 8 + .../model-provider-page/hooks.ts | 62 ++++-- .../model-provider-page/index.tsx | 5 +- .../model-auth/auth-panel.tsx | 83 ------- .../model-auth/authorized/index.tsx | 203 ++++++++++++++++++ .../model-auth/authorized/item.tsx | 118 ++++++++++ .../model-provider-page/model-auth/hooks.ts | 56 +++++ .../model-provider-page/model-modal/index.tsx | 29 ++- .../provider-added-card/credential-panel.tsx | 78 +++++-- .../provider-added-card/index.tsx | 9 +- .../model-provider-page/utils.ts | 15 +- web/context/modal-context.tsx | 3 + web/service/use-models.ts | 30 ++- 14 files changed, 574 insertions(+), 153 deletions(-) delete mode 100644 web/app/components/header/account-setting/model-provider-page/model-auth/auth-panel.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/model-auth/authorized/item.tsx create mode 100644 web/app/components/header/account-setting/model-provider-page/model-auth/hooks.ts diff --git a/web/app/components/base/form/hooks/use-get-validators.ts b/web/app/components/base/form/hooks/use-get-validators.ts index 91754bc1ba..63b93d2c0a 100644 --- a/web/app/components/base/form/hooks/use-get-validators.ts +++ b/web/app/components/base/form/hooks/use-get-validators.ts @@ -1,34 +1,52 @@ -import { useCallback } from 'react' +import { + isValidElement, + useCallback, +} from 'react' +import type { ReactNode } from 'react' import { useTranslation } from 'react-i18next' import type { FormSchema } from '../types' +import { useRenderI18nObject } from '@/hooks/use-i18n' export const useGetValidators = () => { const { t } = useTranslation() + const renderI18nObject = useRenderI18nObject() + const getLabel = useCallback((label: string | Record | ReactNode) => { + if (isValidElement(label)) + return '' + + if (typeof label === 'string') + return label + + if (typeof label === 'object' && label !== null) + return renderI18nObject(label as Record) + }, []) const getValidators = useCallback((formSchema: FormSchema) => { const { name, validators, required, + label, } = formSchema let mergedValidators = validators + const memorizedLabel = getLabel(label) if (required && !validators) { mergedValidators = { onMount: ({ value }: any) => { if (!value) - return t('common.errorMsg.fieldRequired', { field: name }) + return t('common.errorMsg.fieldRequired', { field: memorizedLabel || name }) }, onChange: ({ value }: any) => { if (!value) - return t('common.errorMsg.fieldRequired', { field: name }) + return t('common.errorMsg.fieldRequired', { field: memorizedLabel || name }) }, onBlur: ({ value }: any) => { if (!value) - return t('common.errorMsg.fieldRequired', { field: name }) + return t('common.errorMsg.fieldRequired', { field: memorizedLabel }) }, } } return mergedValidators - }, [t]) + }, [t, getLabel]) return { getValidators, diff --git a/web/app/components/header/account-setting/model-provider-page/declarations.ts b/web/app/components/header/account-setting/model-provider-page/declarations.ts index 1f5ced612c..575c96e1ed 100644 --- a/web/app/components/header/account-setting/model-provider-page/declarations.ts +++ b/web/app/components/header/account-setting/model-provider-page/declarations.ts @@ -181,6 +181,11 @@ export type QuotaConfiguration = { is_valid: boolean } +export type Credential = { + credential_id: string + credential_name: string +} + export type ModelProvider = { provider: string label: TypeWithI18N @@ -207,6 +212,9 @@ export type ModelProvider = { preferred_provider_type: PreferredProviderTypeEnum custom_configuration: { status: CustomConfigurationStatusEnum + current_credential_id?: string + current_credential_name?: string + available_credentials?: Credential[] } system_configuration: { enabled: boolean diff --git a/web/app/components/header/account-setting/model-provider-page/hooks.ts b/web/app/components/header/account-setting/model-provider-page/hooks.ts index 48acaeb64a..66b3015a6b 100644 --- a/web/app/components/header/account-setting/model-provider-page/hooks.ts +++ b/web/app/components/header/account-setting/model-provider-page/hooks.ts @@ -7,6 +7,7 @@ import { import useSWR, { useSWRConfig } from 'swr' import { useContext } from 'use-context-selector' import type { + Credential, CustomConfigurationModelFixedFields, DefaultModel, DefaultModelResponse, @@ -77,16 +78,17 @@ export const useProviderCredentialsAndLoadBalancing = ( configurationMethod: ConfigurationMethodEnum, configured?: boolean, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, + credentialId?: string, ) => { const { data: predefinedFormSchemasValue, mutate: mutatePredefined } = useSWR( - (configurationMethod === ConfigurationMethodEnum.predefinedModel && configured) - ? `/workspaces/current/model-providers/${provider}/credentials` + (configurationMethod === ConfigurationMethodEnum.predefinedModel && configured && credentialId) + ? `/workspaces/current/model-providers/${provider}/credentials${credentialId ? `?credential_id=${credentialId}` : ''}` : null, fetchModelProviderCredentials, ) const { data: customFormSchemasValue, mutate: mutateCustomized } = useSWR( - (configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields) - ? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}` + (configurationMethod === ConfigurationMethodEnum.customizableModel && currentCustomConfigurationModelFixedFields && credentialId) + ? `/workspaces/current/model-providers/${provider}/models/credentials?model=${currentCustomConfigurationModelFixedFields?.__model_name}&model_type=${currentCustomConfigurationModelFixedFields?.__model_type}${credentialId ? `&credential_id=${credentialId}` : ''}` : null, fetchModelProviderCredentials, ) @@ -102,6 +104,7 @@ export const useProviderCredentialsAndLoadBalancing = ( : undefined }, [ configurationMethod, + credentialId, currentCustomConfigurationModelFixedFields, customFormSchemasValue?.credentials, predefinedFormSchemasValue?.credentials, @@ -313,40 +316,53 @@ export const useMarketplaceAllPlugins = (providers: ModelProvider[], searchText: } } -export const useModelModalHandler = () => { - const setShowModelModal = useModalContextSelector(state => state.setShowModelModal) +export const useRefreshModel = () => { + const { eventEmitter } = useEventEmitterContextContext() const updateModelProviders = useUpdateModelProviders() const updateModelList = useUpdateModelList() - const { eventEmitter } = useEventEmitterContextContext() + const handleRefreshModel = useCallback((provider: ModelProvider, configurationMethod: ConfigurationMethodEnum, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => { + updateModelProviders() + + provider.supported_model_types.forEach((type) => { + updateModelList(type) + }) + + if (configurationMethod === ConfigurationMethodEnum.customizableModel + && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) { + eventEmitter?.emit({ + type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST, + payload: provider.provider, + } as any) + + if (CustomConfigurationModelFixedFields?.__model_type) + updateModelList(CustomConfigurationModelFixedFields.__model_type) + } + }, [eventEmitter, updateModelList, updateModelProviders]) + + return { + handleRefreshModel, + } +} + +export const useModelModalHandler = () => { + const setShowModelModal = useModalContextSelector(state => state.setShowModelModal) + const { handleRefreshModel } = useRefreshModel() return ( provider: ModelProvider, configurationMethod: ConfigurationMethodEnum, CustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, + credential?: Credential, ) => { setShowModelModal({ payload: { currentProvider: provider, currentConfigurationMethod: configurationMethod, currentCustomConfigurationModelFixedFields: CustomConfigurationModelFixedFields, + credential, }, onSaveCallback: () => { - updateModelProviders() - - provider.supported_model_types.forEach((type) => { - updateModelList(type) - }) - - if (configurationMethod === ConfigurationMethodEnum.customizableModel - && provider.custom_configuration.status === CustomConfigurationStatusEnum.active) { - eventEmitter?.emit({ - type: UPDATE_MODEL_PROVIDER_CUSTOM_MODEL_LIST, - payload: provider.provider, - } as any) - - if (CustomConfigurationModelFixedFields?.__model_type) - updateModelList(CustomConfigurationModelFixedFields.__model_type) - } + handleRefreshModel(provider, configurationMethod, CustomConfigurationModelFixedFields) }, }) } diff --git a/web/app/components/header/account-setting/model-provider-page/index.tsx b/web/app/components/header/account-setting/model-provider-page/index.tsx index 4aa98daf66..569ca4c777 100644 --- a/web/app/components/header/account-setting/model-provider-page/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/index.tsx @@ -9,6 +9,7 @@ import SystemModelSelector from './system-model-selector' import ProviderAddedCard from './provider-added-card' import type { ConfigurationMethodEnum, + Credential, CustomConfigurationModelFixedFields, ModelProvider, } from './declarations' @@ -126,7 +127,7 @@ const ModelProviderPage = ({ searchText }: Props) => { handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)} + onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, credential?: Credential) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields, credential)} /> ))} @@ -140,7 +141,7 @@ const ModelProviderPage = ({ searchText }: Props) => { notConfigured key={provider.provider} provider={provider} - onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields)} + onOpenModal={(configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, credential?: Credential) => handleOpenModal(provider, configurationMethod, currentCustomConfigurationModelFixedFields, credential)} /> ))} diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/auth-panel.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/auth-panel.tsx deleted file mode 100644 index ac0cd5dcfd..0000000000 --- a/web/app/components/header/account-setting/model-provider-page/model-auth/auth-panel.tsx +++ /dev/null @@ -1,83 +0,0 @@ -import type { FC } from 'react' -import { useCallback } from 'react' -import { useTranslation } from 'react-i18next' -import { - RiEqualizer2Line, - RiMoreFill, -} from '@remixicon/react' -import type { ModelProvider } from '../declarations' -import { - CustomConfigurationStatusEnum, -} from '../declarations' -import Indicator from '@/app/components/header/indicator' -import Button from '@/app/components/base/button' -import { - AuthCategory, - Authorized, -} from '@/app/components/plugins/plugin-auth' - -type AuthPanelProps = { - provider: ModelProvider - onSetup: () => void -} -const AuthPanel: FC = ({ - provider, - onSetup, -}) => { - const authorized = false - const { t } = useTranslation() - const customConfig = provider.custom_configuration - const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active - - const renderTrigger = useCallback(() => { - return ( - - ) - }, []) - - return ( - <> - { - provider.provider_credential_schema && ( -
-
- API-KEY - -
-
- - { - authorized && ( - - ) - } -
-
- ) - } - - ) -} - -export default AuthPanel diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx new file mode 100644 index 0000000000..c56e03e8ff --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/index.tsx @@ -0,0 +1,203 @@ +import { + memo, + useCallback, + useRef, + useState, +} from 'react' +import { + RiEqualizer2Line, +} from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import type { + PortalToFollowElemOptions, +} from '@/app/components/base/portal-to-follow-elem' +import Button from '@/app/components/base/button' +import cn from '@/utils/classnames' +import Confirm from '@/app/components/base/confirm' +import Item from './item' +import { useToastContext } from '@/app/components/base/toast' +import type { Credential } from '../../declarations' +import { useDeleteModelCredential } from '@/service/use-models' + +type AuthorizedProps = { + provider: string + credentials: Credential[] + disabled?: boolean + renderTrigger?: (open?: boolean) => React.ReactNode + isOpen?: boolean + onOpenChange?: (open: boolean) => void + offset?: PortalToFollowElemOptions['offset'] + placement?: PortalToFollowElemOptions['placement'] + triggerPopupSameWidth?: boolean + popupClassName?: string + onItemClick?: (id: string) => void + showItemSelectedIcon?: boolean + selectedCredentialId?: string + onUpdate?: () => void + onSetup: (credential?: Credential) => void +} +const Authorized = ({ + provider, + credentials, + disabled, + renderTrigger, + isOpen, + onOpenChange, + offset = 8, + placement = 'bottom-end', + triggerPopupSameWidth = false, + popupClassName, + onItemClick, + showItemSelectedIcon, + selectedCredentialId, + onUpdate, + onSetup, +}: AuthorizedProps) => { + const { t } = useTranslation() + const { notify } = useToastContext() + const [isLocalOpen, setIsLocalOpen] = useState(false) + const mergedIsOpen = isOpen ?? isLocalOpen + const setMergedIsOpen = useCallback((open: boolean) => { + if (onOpenChange) + onOpenChange(open) + + setIsLocalOpen(open) + }, [onOpenChange]) + const pendingOperationCredentialId = useRef(null) + const [deleteCredentialId, setDeleteCredentialId] = useState(null) + const openConfirm = useCallback((credentialId?: string) => { + if (credentialId) + pendingOperationCredentialId.current = credentialId + + setDeleteCredentialId(pendingOperationCredentialId.current) + }, []) + const closeConfirm = useCallback(() => { + setDeleteCredentialId(null) + pendingOperationCredentialId.current = null + }, []) + const [doingAction, setDoingAction] = useState(false) + const doingActionRef = useRef(doingAction) + const handleSetDoingAction = useCallback((doing: boolean) => { + doingActionRef.current = doing + setDoingAction(doing) + }, []) + const { mutateAsync: deleteModelCredential } = useDeleteModelCredential(provider) + const handleConfirm = useCallback(async () => { + if (doingActionRef.current) + return + if (!pendingOperationCredentialId.current) { + setDeleteCredentialId(null) + return + } + try { + handleSetDoingAction(true) + await deleteModelCredential(pendingOperationCredentialId.current) + notify({ + type: 'success', + message: t('common.api.actionSuccess'), + }) + onUpdate?.() + setDeleteCredentialId(null) + pendingOperationCredentialId.current = null + } + finally { + handleSetDoingAction(false) + } + }, [onUpdate, notify, t, handleSetDoingAction]) + const handleEdit = useCallback((credential: Credential) => { + onSetup(credential) + }, [onSetup]) + + return ( + <> + + setMergedIsOpen(!mergedIsOpen)} + asChild + > + { + renderTrigger + ? renderTrigger(mergedIsOpen) + : ( + + ) + } + + +
+
+ { + !!credentials.length && ( +
+
+ API Keys +
+ { + credentials.map(credential => ( + + )) + } +
+ ) + } +
+
+
+ +
+
+
+
+ { + deleteCredentialId && ( + + ) + } + + ) +} + +export default memo(Authorized) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/item.tsx b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/item.tsx new file mode 100644 index 0000000000..9387839d9f --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/authorized/item.tsx @@ -0,0 +1,118 @@ +import { + memo, + useMemo, +} from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCheckLine, + RiDeleteBinLine, + RiEqualizer2Line, +} from '@remixicon/react' +import Indicator from '@/app/components/header/indicator' +import ActionButton from '@/app/components/base/action-button' +import Tooltip from '@/app/components/base/tooltip' +import cn from '@/utils/classnames' +import type { Credential } from '../../declarations' + +type ItemProps = { + credential: Credential + disabled?: boolean + onDelete?: (id: string) => void + onEdit?: (credential: Credential) => void + onSetDefault?: (id: string) => void + disableRename?: boolean + disableEdit?: boolean + disableDelete?: boolean + disableSetDefault?: boolean + onItemClick?: (id: string) => void + showSelectedIcon?: boolean + selectedCredentialId?: string +} +const Item = ({ + credential, + disabled, + onDelete, + onEdit, + disableRename, + disableEdit, + disableDelete, + disableSetDefault, + onItemClick, + showSelectedIcon, + selectedCredentialId, +}: ItemProps) => { + const { t } = useTranslation() + const showAction = useMemo(() => { + return !(disableRename && disableEdit && disableDelete && disableSetDefault) + }, [disableRename, disableEdit, disableDelete, disableSetDefault]) + + return ( +
onItemClick?.(credential.credential_id)} + > +
+ { + showSelectedIcon && ( +
+ { + selectedCredentialId === credential.credential_id && ( + + ) + } +
+ ) + } + +
+ {credential.credential_name} +
+
+ { + showAction && ( +
+ { + !disableEdit && ( + + { + e.stopPropagation() + onEdit?.(credential) + }} + > + + + + ) + } + { + !disableDelete && ( + + { + e.stopPropagation() + onDelete?.(credential.credential_id) + }} + > + + + + ) + } +
+ ) + } +
+ ) +} + +export default memo(Item) diff --git a/web/app/components/header/account-setting/model-provider-page/model-auth/hooks.ts b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks.ts new file mode 100644 index 0000000000..94db6cfc8d --- /dev/null +++ b/web/app/components/header/account-setting/model-provider-page/model-auth/hooks.ts @@ -0,0 +1,56 @@ +import { useMemo } from 'react' +import { useTranslation } from 'react-i18next' +import type { + ModelLoadBalancingConfig, + ModelProvider, +} from '../declarations' +import { + genModelNameFormSchema, + genModelTypeFormSchema, +} from '../utils' +import { FormTypeEnum } from '@/app/components/base/form/types' + +export const useModelFormSchemas = ( + provider: ModelProvider, + providerFormSchemaPredefined: boolean, + draftConfig?: ModelLoadBalancingConfig, +) => { + const { t } = useTranslation() + const { + provider_credential_schema, + supported_model_types, + model_credential_schema, + } = provider + const formSchemas = useMemo(() => { + return providerFormSchemaPredefined + ? provider_credential_schema.credential_form_schemas + : [ + genModelTypeFormSchema(supported_model_types), + genModelNameFormSchema(model_credential_schema?.model), + ...(draftConfig?.enabled ? [] : model_credential_schema.credential_form_schemas), + ] + }, [ + providerFormSchemaPredefined, + provider_credential_schema?.credential_form_schemas, + supported_model_types, + model_credential_schema?.credential_form_schemas, + model_credential_schema?.model, + draftConfig?.enabled, + ]) + + const formSchemasWithAuthorizationName = useMemo(() => { + return [ + { + type: FormTypeEnum.textInput, + variable: '__authorization_name__', + label: t('plugin.auth.authorizationName'), + required: true, + }, + ...formSchemas, + ] + }, [formSchemas, t]) + + return { + formSchemas: formSchemasWithAuthorizationName, + } +} diff --git a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx index d23fd6a31e..31f9cf0a6d 100644 --- a/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx +++ b/web/app/components/header/account-setting/model-provider-page/model-modal/index.tsx @@ -45,11 +45,14 @@ import type { FormRefObject, FormSchema, } from '@/app/components/base/form/types' +import { useModelFormSchemas } from '../model-auth/hooks' +import type { Credential } from '../declarations' type ModelModalProps = { provider: ModelProvider configurateMethod: ConfigurationMethodEnum currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields + credential?: Credential onCancel: () => void onSave: () => void } @@ -58,6 +61,7 @@ const ModelModal: FC = ({ provider, configurateMethod, currentCustomConfigurationModelFixedFields, + credential, onCancel, onSave, }) => { @@ -71,6 +75,7 @@ const ModelModal: FC = ({ configurateMethod, providerFormSchemaPredefined && provider.custom_configuration.status === CustomConfigurationStatusEnum.active, currentCustomConfigurationModelFixedFields, + credential?.credential_id, ) const { isCurrentWorkspaceManager } = useAppContext() const isEditMode = !!formSchemasValue && isCurrentWorkspaceManager @@ -95,22 +100,7 @@ const ModelModal: FC = ({ setDraftConfig(originalConfig) }, [draftConfig, originalConfig]) - const formSchemas = useMemo(() => { - return providerFormSchemaPredefined - ? provider.provider_credential_schema.credential_form_schemas - : [ - genModelTypeFormSchema(provider.supported_model_types), - genModelNameFormSchema(provider.model_credential_schema?.model), - ...(draftConfig?.enabled ? [] : provider.model_credential_schema.credential_form_schemas), - ] - }, [ - providerFormSchemaPredefined, - provider.provider_credential_schema?.credential_form_schemas, - provider.supported_model_types, - provider.model_credential_schema?.credential_form_schemas, - provider.model_credential_schema?.model, - draftConfig?.enabled, - ]) + const { formSchemas } = useModelFormSchemas(provider, providerFormSchemaPredefined, draftConfig) const formRef = useRef(null) const extendedSecretFormSchemas = useMemo( @@ -152,6 +142,7 @@ const ModelModal: FC = ({ }) || { isCheckValidated: false, values: {} } if (!isCheckValidated) return + const res = await saveCredentials( providerFormSchemaPredefined, provider.provider, @@ -190,6 +181,7 @@ const ModelModal: FC = ({ providerFormSchemaPredefined, provider.provider, values, + credential?.credential_id, ) if (res.result === 'success') { notify({ type: 'success', message: t('common.actionMsg.modifiedSuccessfully') }) @@ -227,7 +219,10 @@ const ModelModal: FC = ({ showRadioUI: formSchema.type === FormTypeEnum.radio, } }) as FormSchema[]} - defaultValues={formSchemasValue} + defaultValues={{ + ...formSchemasValue, + __authorization_name__: credential?.credential_name, + }} inputClassName='justify-start' ref={formRef} /> diff --git a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx index 822df5f726..059c2ec73b 100644 --- a/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx +++ b/web/app/components/header/account-setting/model-provider-page/provider-added-card/credential-panel.tsx @@ -1,7 +1,10 @@ -import type { FC } from 'react' +import { useMemo } from 'react' import { useTranslation } from 'react-i18next' import { RiEqualizer2Line } from '@remixicon/react' -import type { ModelProvider } from '../declarations' +import type { + Credential, + ModelProvider, +} from '../declarations' import { ConfigurationMethodEnum, CustomConfigurationStatusEnum, @@ -19,15 +22,18 @@ import Button from '@/app/components/base/button' import { changeModelProviderPriority } from '@/service/common' import { useToastContext } from '@/app/components/base/toast' import { useEventEmitterContextContext } from '@/context/event-emitter' +import Authorized from '../model-auth/authorized' type CredentialPanelProps = { provider: ModelProvider - onSetup: () => void + onSetup: (credential?: Credential) => void + onUpdate: () => void } -const CredentialPanel: FC = ({ +const CredentialPanel = ({ provider, onSetup, -}) => { + onUpdate, +}: CredentialPanelProps) => { const { t } = useTranslation() const { notify } = useToastContext() const { eventEmitter } = useEventEmitterContextContext() @@ -38,6 +44,13 @@ const CredentialPanel: FC = ({ const priorityUseType = provider.preferred_provider_type const isCustomConfigured = customConfig.status === CustomConfigurationStatusEnum.active const configurateMethods = provider.configurate_methods + const { + current_credential_id, + current_credential_name, + available_credentials, + } = provider.custom_configuration + const authorized = current_credential_id && current_credential_name && available_credentials?.every(item => !!item.credential_id) + const authRemoved = !!available_credentials?.length && available_credentials?.every(item => !item.credential_id) const handleChangePriority = async (key: PreferredProviderTypeEnum) => { const res = await changeModelProviderPriority({ @@ -61,25 +74,58 @@ const CredentialPanel: FC = ({ } as any) } } + const credentialLabel = useMemo(() => { + if (authorized) + return current_credential_name + if (authRemoved) + return 'Auth removed' + return 'Unauthorized' + }, [authorized, authRemoved, current_credential_name]) return ( <> { provider.provider_credential_schema && (
-
- API-KEY - +
+
+ {credentialLabel} +
+
- + { + (!authorized || authRemoved) && ( + + ) + } + { + authorized && ( + + ) + } { systemConfig.enabled && isCustomConfigured && ( void + onOpenModal: (configurationMethod: ConfigurationMethodEnum, currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields, credential?: Credential) => void } const ProviderAddedCard: FC = ({ notConfigured, @@ -80,6 +82,8 @@ const ProviderAddedCard: FC = ({ getModelList(v.payload) }) + const { handleRefreshModel } = useRefreshModel() + return (
= ({ { showCredential && ( onOpenModal(ConfigurationMethodEnum.predefinedModel)} + onSetup={(credential?: Credential) => onOpenModal(ConfigurationMethodEnum.predefinedModel, undefined, credential)} + onUpdate={() => handleRefreshModel(provider, ConfigurationMethodEnum.predefinedModel, undefined)} provider={provider} /> ) diff --git a/web/app/components/header/account-setting/model-provider-page/utils.ts b/web/app/components/header/account-setting/model-provider-page/utils.ts index 9056afe69b..91ed6a96ff 100644 --- a/web/app/components/header/account-setting/model-provider-page/utils.ts +++ b/web/app/components/header/account-setting/model-provider-page/utils.ts @@ -82,12 +82,14 @@ export const saveCredentials = async (predefined: boolean, provider: string, v: let body, url if (predefined) { + const { __authorization_name__, ...rest } = v body = { config_from: ConfigurationMethodEnum.predefinedModel, - credentials: v, + credentials: rest, load_balancing: loadBalancing, + name: __authorization_name__, } - url = `/workspaces/current/model-providers/${provider}` + url = `/workspaces/current/model-providers/${provider}/credentials` } else { const { __model_name, __model_type, ...credentials } = v @@ -117,12 +119,17 @@ export const savePredefinedLoadBalancingConfig = async (provider: string, v: For return setModelProvider({ url, body }) } -export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue) => { +export const removeCredentials = async (predefined: boolean, provider: string, v: FormValue, credentialId?: string) => { let url = '' let body if (predefined) { - url = `/workspaces/current/model-providers/${provider}` + url = `/workspaces/current/model-providers/${provider}/credentials` + if (credentialId) { + body = { + credential_id: credentialId, + } + } } else { if (v) { diff --git a/web/context/modal-context.tsx b/web/context/modal-context.tsx index f6425ec11f..f6d7e72302 100644 --- a/web/context/modal-context.tsx +++ b/web/context/modal-context.tsx @@ -6,6 +6,7 @@ import { createContext, useContext, useContextSelector } from 'use-context-selec import { useRouter, useSearchParams } from 'next/navigation' import type { ConfigurationMethodEnum, + Credential, CustomConfigurationModelFixedFields, ModelLoadBalancingConfigEntry, ModelProvider, @@ -79,6 +80,7 @@ export type ModelModalType = { currentProvider: ModelProvider currentConfigurationMethod: ConfigurationMethodEnum currentCustomConfigurationModelFixedFields?: CustomConfigurationModelFixedFields + credential?: Credential } export type LoadBalancingEntryModalType = ModelModalType & { entry?: ModelLoadBalancingConfigEntry @@ -337,6 +339,7 @@ export const ModalContextProvider = ({ provider={showModelModal.payload.currentProvider} configurateMethod={showModelModal.payload.currentConfigurationMethod} currentCustomConfigurationModelFixedFields={showModelModal.payload.currentCustomConfigurationModelFixedFields} + credential={showModelModal.payload.credential} onCancel={handleCancelModelModal} onSave={handleSaveModelModal} /> diff --git a/web/service/use-models.ts b/web/service/use-models.ts index 84122cdd1f..099aefec6f 100644 --- a/web/service/use-models.ts +++ b/web/service/use-models.ts @@ -1,8 +1,13 @@ -import { get } from './base' +import { + del, + get, + post, +} from './base' import type { ModelItem, } from '@/app/components/header/account-setting/model-provider-page/declarations' import { + useMutation, useQuery, // useQueryClient, } from '@tanstack/react-query' @@ -15,3 +20,26 @@ export const useModelProviderModelList = (provider: string) => { queryFn: () => get<{ data: ModelItem[] }>(`/workspaces/current/model-providers/${provider}/models`), }) } + +export const useAddModelCredential = (providerName: string) => { + return useMutation({ + mutationFn: (data: any) => post<{ result: string }>(`/workspaces/current/model-providers/${providerName}/credentials`, data), + }) +} + +export const useGetModelCredential = (providerName: string, credentialId: string) => { + return useQuery({ + queryKey: [NAME_SPACE, 'model-credential', providerName, credentialId], + queryFn: () => get<{ data: Credential[] }>(`/workspaces/current/model-providers/${providerName}/credentials?credential_id=${credentialId}`), + }) +} + +export const useDeleteModelCredential = (providerName: string) => { + return useMutation({ + mutationFn: (credentialId: string) => del<{ result: string }>(`/workspaces/current/model-providers/${providerName}/credentials`, { + body: { + credential_id: credentialId, + }, + }), + }) +}