feat: add multi model credentials (#24451)

Co-authored-by: zxhlyh <jasonapring2015@outlook.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
This commit is contained in:
非法操作
2025-08-25 16:12:29 +08:00
committed by GitHub
parent b08bfa203a
commit 6010d5f24c
65 changed files with 5202 additions and 1814 deletions

View File

@@ -8,6 +8,8 @@ import type { AddOAuthButtonProps } from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button'
import type { AddApiKeyButtonProps } from './add-api-key-button'
import type { PluginPayload } from '../types'
import cn from '@/utils/classnames'
import Tooltip from '@/app/components/base/tooltip'
type AuthorizeProps = {
pluginPayload: PluginPayload
@@ -17,6 +19,7 @@ type AuthorizeProps = {
canApiKey?: boolean
disabled?: boolean
onUpdate?: () => void
notAllowCustomCredential?: boolean
}
const Authorize = ({
pluginPayload,
@@ -26,6 +29,7 @@ const Authorize = ({
canApiKey,
disabled,
onUpdate,
notAllowCustomCredential,
}: AuthorizeProps) => {
const { t } = useTranslation()
const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => {
@@ -62,18 +66,54 @@ const Authorize = ({
}
}, [canOAuth, theme, pluginPayload, t])
const OAuthButton = useMemo(() => {
const Item = (
<div className={cn('min-w-0 flex-[1]', notAllowCustomCredential && 'opacity-50')}>
<AddOAuthButton
{...oAuthButtonProps}
disabled={disabled || notAllowCustomCredential}
onUpdate={onUpdate}
/>
</div>
)
if (notAllowCustomCredential) {
return (
<Tooltip popupContent={t('plugin.auth.credentialUnavailable')}>
{Item}
</Tooltip>
)
}
return Item
}, [notAllowCustomCredential, oAuthButtonProps, disabled, onUpdate, t])
const ApiKeyButton = useMemo(() => {
const Item = (
<div className={cn('min-w-0 flex-[1]', notAllowCustomCredential && 'opacity-50')}>
<AddApiKeyButton
{...apiKeyButtonProps}
disabled={disabled || notAllowCustomCredential}
onUpdate={onUpdate}
/>
</div>
)
if (notAllowCustomCredential) {
return (
<Tooltip popupContent={t('plugin.auth.credentialUnavailable')}>
{Item}
</Tooltip>
)
}
return Item
}, [notAllowCustomCredential, apiKeyButtonProps, disabled, onUpdate, t])
return (
<>
<div className='flex items-center space-x-1.5'>
{
canOAuth && (
<div className='min-w-0 flex-[1]'>
<AddOAuthButton
{...oAuthButtonProps}
disabled={disabled}
onUpdate={onUpdate}
/>
</div>
OAuthButton
)
}
{
@@ -87,13 +127,7 @@ const Authorize = ({
}
{
canApiKey && (
<div className='min-w-0 flex-[1]'>
<AddApiKeyButton
{...apiKeyButtonProps}
disabled={disabled}
onUpdate={onUpdate}
/>
</div>
ApiKeyButton
)
}
</div>

View File

@@ -35,10 +35,13 @@ const AuthorizedInNode = ({
credentials,
disabled,
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, isOpen || !!credentialId)
const renderTrigger = useCallback((open?: boolean) => {
let label = ''
let removed = false
let unavailable = false
let color = 'green'
if (!credentialId) {
label = t('plugin.auth.workspaceDefault')
}
@@ -46,6 +49,12 @@ const AuthorizedInNode = ({
const credential = credentials.find(c => c.id === credentialId)
label = credential ? credential.name : t('plugin.auth.authRemoved')
removed = !credential
unavailable = !!credential?.not_allowed_to_use && !credential?.from_enterprise
if (removed)
color = 'red'
else if (unavailable)
color = 'gray'
}
return (
<Button
@@ -57,9 +66,12 @@ const AuthorizedInNode = ({
>
<Indicator
className='mr-1.5'
color={removed ? 'red' : 'green'}
color={color as any}
/>
{label}
{
unavailable && t('plugin.auth.unavailable')
}
<RiArrowDownSLine
className={cn(
'h-3.5 w-3.5 text-components-button-ghost-text',
@@ -106,6 +118,7 @@ const AuthorizedInNode = ({
showItemSelectedIcon
selectedCredentialId={credentialId || '__workspace_default__'}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}

View File

@@ -52,6 +52,7 @@ type AuthorizedProps = {
showItemSelectedIcon?: boolean
selectedCredentialId?: string
onUpdate?: () => void
notAllowCustomCredential?: boolean
}
const Authorized = ({
pluginPayload,
@@ -72,6 +73,7 @@ const Authorized = ({
showItemSelectedIcon,
selectedCredentialId,
onUpdate,
notAllowCustomCredential,
}: AuthorizedProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
@@ -171,6 +173,7 @@ const Authorized = ({
handleSetDoingAction(false)
}
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
const unavailableCredentials = credentials.filter(credential => credential.not_allowed_to_use)
return (
<>
@@ -201,6 +204,11 @@ const Authorized = ({
? t('plugin.auth.authorizations')
: t('plugin.auth.authorization')
}
{
!!unavailableCredentials.length && (
` (${unavailableCredentials.length} ${t('plugin.auth.unavailable')})`
)
}
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
)
@@ -294,18 +302,24 @@ const Authorized = ({
)
}
</div>
<div className='h-px bg-divider-subtle'></div>
<div className='p-2'>
<Authorize
pluginPayload={pluginPayload}
theme='secondary'
showDivider={false}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
onUpdate={onUpdate}
/>
</div>
{
!notAllowCustomCredential && (
<>
<div className='h-[1px] bg-divider-subtle'></div>
<div className='p-2'>
<Authorize
pluginPayload={pluginPayload}
theme='secondary'
showDivider={false}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
onUpdate={onUpdate}
/>
</div>
</>
)
}
</div>
</PortalToFollowElemContent>
</PortalToFollowElem>

View File

@@ -61,14 +61,19 @@ const Item = ({
return !(disableRename && disableEdit && disableDelete && disableSetDefault)
}, [disableRename, disableEdit, disableDelete, disableSetDefault])
return (
const CredentialItem = (
<div
key={credential.id}
className={cn(
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
renaming && 'bg-state-base-hover',
(disabled || credential.not_allowed_to_use) && 'cursor-not-allowed opacity-50',
)}
onClick={() => onItemClick?.(credential.id === '__workspace_default__' ? '' : credential.id)}
onClick={() => {
if (credential.not_allowed_to_use || disabled)
return
onItemClick?.(credential.id === '__workspace_default__' ? '' : credential.id)
}}
>
{
renaming && (
@@ -121,7 +126,10 @@ const Item = ({
</div>
)
}
<Indicator className='ml-2 mr-1.5 shrink-0' />
<Indicator
className='ml-2 mr-1.5 shrink-0'
color={credential.not_allowed_to_use ? 'gray' : 'green'}
/>
<div
className='system-md-regular truncate text-text-secondary'
title={credential.name}
@@ -138,11 +146,18 @@ const Item = ({
</div>
)
}
{
credential.from_enterprise && (
<Badge className='shrink-0'>
Enterprise
</Badge>
)
}
{
showAction && !renaming && (
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
{
!credential.is_default && !disableSetDefault && (
!credential.is_default && !disableSetDefault && !credential.not_allowed_to_use && (
<Button
size='small'
disabled={disabled}
@@ -156,7 +171,7 @@ const Item = ({
)
}
{
!disableRename && (
!disableRename && !credential.from_enterprise && !credential.not_allowed_to_use && (
<Tooltip popupContent={t('common.operation.rename')}>
<ActionButton
disabled={disabled}
@@ -172,7 +187,7 @@ const Item = ({
)
}
{
!isOAuth && !disableEdit && (
!isOAuth && !disableEdit && !credential.from_enterprise && !credential.not_allowed_to_use && (
<Tooltip popupContent={t('common.operation.edit')}>
<ActionButton
disabled={disabled}
@@ -194,7 +209,7 @@ const Item = ({
)
}
{
!disableDelete && (
!disableDelete && !credential.from_enterprise && (
<Tooltip popupContent={t('common.operation.delete')}>
<ActionButton
className='hover:bg-transparent'
@@ -214,6 +229,18 @@ const Item = ({
}
</div>
)
if (credential.not_allowed_to_use) {
return (
<Tooltip popupContent={t('plugin.auth.customCredentialUnavailable')}>
{CredentialItem}
</Tooltip>
)
}
return (
CredentialItem
)
}
export default memo(Item)

View File

@@ -0,0 +1,125 @@
import {
useCallback,
useRef,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { useToastContext } from '@/app/components/base/toast'
import type { PluginPayload } from '@/app/components/plugins/plugin-auth/types'
import {
useDeletePluginCredentialHook,
useSetPluginDefaultCredentialHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential'
export const usePluginAuthAction = (
pluginPayload: PluginPayload,
onUpdate?: () => void,
) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const pendingOperationCredentialId = useRef<string | null>(null)
const [deleteCredentialId, setDeleteCredentialId] = useState<string | null>(null)
const { mutateAsync: deletePluginCredential } = useDeletePluginCredentialHook(pluginPayload)
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 [editValues, setEditValues] = useState<Record<string, any> | null>(null)
const handleConfirm = useCallback(async () => {
if (doingActionRef.current)
return
if (!pendingOperationCredentialId.current) {
setDeleteCredentialId(null)
return
}
try {
handleSetDoingAction(true)
await deletePluginCredential({ credential_id: pendingOperationCredentialId.current })
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
setDeleteCredentialId(null)
pendingOperationCredentialId.current = null
setEditValues(null)
}
finally {
handleSetDoingAction(false)
}
}, [deletePluginCredential, onUpdate, notify, t, handleSetDoingAction])
const handleEdit = useCallback((id: string, values: Record<string, any>) => {
pendingOperationCredentialId.current = id
setEditValues(values)
}, [])
const handleRemove = useCallback(() => {
setDeleteCredentialId(pendingOperationCredentialId.current)
}, [])
const { mutateAsync: setPluginDefaultCredential } = useSetPluginDefaultCredentialHook(pluginPayload)
const handleSetDefault = useCallback(async (id: string) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await setPluginDefaultCredential(id)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [setPluginDefaultCredential, onUpdate, notify, t, handleSetDoingAction])
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const handleRename = useCallback(async (payload: {
credential_id: string
name: string
}) => {
if (doingActionRef.current)
return
try {
handleSetDoingAction(true)
await updatePluginCredential(payload)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onUpdate?.()
}
finally {
handleSetDoingAction(false)
}
}, [updatePluginCredential, notify, t, handleSetDoingAction, onUpdate])
return {
doingAction,
handleSetDoingAction,
openConfirm,
closeConfirm,
deleteCredentialId,
setDeleteCredentialId,
handleConfirm,
editValues,
setEditValues,
handleEdit,
handleRemove,
handleSetDefault,
handleRename,
pendingOperationCredentialId,
}
}

View File

@@ -20,6 +20,7 @@ export const usePluginAuth = (pluginPayload: PluginPayload, enable?: boolean) =>
canApiKey,
credentials: data?.credentials || [],
disabled: !isCurrentWorkspaceManager,
notAllowCustomCredential: data?.allow_custom_token === false,
invalidPluginCredentialInfo,
}
}

View File

@@ -35,6 +35,7 @@ const PluginAuthInAgent = ({
credentials,
disabled,
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, true)
const extraAuthorizationItems: Credential[] = [
@@ -58,6 +59,8 @@ const PluginAuthInAgent = ({
const renderTrigger = useCallback((isOpen?: boolean) => {
let label = ''
let removed = false
let unavailable = false
let color = 'green'
if (!credentialId) {
label = t('plugin.auth.workspaceDefault')
}
@@ -65,6 +68,11 @@ const PluginAuthInAgent = ({
const credential = credentials.find(c => c.id === credentialId)
label = credential ? credential.name : t('plugin.auth.authRemoved')
removed = !credential
unavailable = !!credential?.not_allowed_to_use && !credential?.from_enterprise
if (removed)
color = 'red'
else if (unavailable)
color = 'gray'
}
return (
<Button
@@ -75,9 +83,12 @@ const PluginAuthInAgent = ({
)}>
<Indicator
className='mr-2'
color={removed ? 'red' : 'green'}
color={color as any}
/>
{label}
{
unavailable && t('plugin.auth.unavailable')
}
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
)
@@ -93,6 +104,7 @@ const PluginAuthInAgent = ({
canApiKey={canApiKey}
disabled={disabled}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}
@@ -113,6 +125,7 @@ const PluginAuthInAgent = ({
onOpenChange={setIsOpen}
selectedCredentialId={credentialId || '__workspace_default__'}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}

View File

@@ -22,6 +22,7 @@ const PluginAuth = ({
credentials,
disabled,
invalidPluginCredentialInfo,
notAllowCustomCredential,
} = usePluginAuth(pluginPayload, !!pluginPayload.provider)
return (
@@ -34,6 +35,7 @@ const PluginAuth = ({
canApiKey={canApiKey}
disabled={disabled}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}
@@ -46,6 +48,7 @@ const PluginAuth = ({
canApiKey={canApiKey}
disabled={disabled}
onUpdate={invalidPluginCredentialInfo}
notAllowCustomCredential={notAllowCustomCredential}
/>
)
}

View File

@@ -22,4 +22,6 @@ export type Credential = {
is_default: boolean
credentials?: Record<string, any>
isWorkspaceDefault?: boolean
from_enterprise?: boolean
not_allowed_to_use?: boolean
}