tool oauth

This commit is contained in:
zxhlyh
2025-07-10 17:12:48 +08:00
parent bdf5af7a6f
commit 18699f8671
16 changed files with 599 additions and 282 deletions

View File

@ -14,6 +14,7 @@ import AuthForm from '@/app/components/base/form/form-scenarios/auth'
import type { FromRefObject } from '@/app/components/base/form/types'
import { FormTypeEnum } from '@/app/components/base/form/types'
import { useToastContext } from '@/app/components/base/toast'
import Loading from '@/app/components/base/loading'
import type { PluginPayload } from '../types'
import {
useAddPluginCredentialHook,
@ -21,6 +22,7 @@ import {
useInvalidPluginCredentialInfoHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential'
import { useRenderI18nObject } from '@/hooks/use-i18n'
export type ApiKeyModalProps = {
pluginPayload: PluginPayload
@ -38,18 +40,25 @@ const ApiKeyModal = ({
}: ApiKeyModalProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const { data = [] } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY)
const { data = [], isLoading } = useGetPluginCredentialSchemaHook(pluginPayload, CredentialTypeEnum.API_KEY)
const formSchemas = useMemo(() => {
return [
{
type: FormTypeEnum.textInput,
name: '__name__',
label: 'Authorization name',
label: t('plugin.auth.authorizationName'),
required: false,
},
...data,
]
}, [data])
}, [data, t])
const defaultValues = formSchemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
return acc
}, {} as Record<string, any>)
const secretInput = formSchemas.find(schema => schema.type === FormTypeEnum.secretInput)
const renderI18nObject = useRenderI18nObject()
const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload)
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload)
@ -77,7 +86,6 @@ const ApiKeyModal = ({
await updatePluginCredential({
credentials: transformedValues,
credential_id: __credential_id__,
type: CredentialTypeEnum.API_KEY,
name: __name__ || '',
})
}
@ -100,19 +108,21 @@ const ApiKeyModal = ({
return (
<Modal
size='md'
title='API Key Authorization Configuration'
subTitle='After configuring credentials, all members within the workspace can use this tool when orchestrating applications.'
title={t('plugin.auth.useApiAuth')}
subTitle={t('plugin.auth.useApiAuthDesc')}
onClose={onClose}
onCancel={onClose}
footerSlot={
<a
className='system-xs-regular flex h-8 grow items-center text-text-accent'
href=''
target='_blank'
>
Get your API Key from OpenAI
<RiExternalLinkLine className='ml-1 h-3 w-3' />
</a>
secretInput && (
<a
className='system-xs-regular flex h-8 grow items-center text-text-accent'
href={secretInput?.url}
target='_blank'
>
{renderI18nObject(secretInput?.help as any)}
<RiExternalLinkLine className='ml-1 h-3 w-3' />
</a>
)
}
bottomSlot={
<div className='flex items-center justify-center bg-background-section-burn py-3 text-xs text-text-tertiary'>
@ -133,12 +143,23 @@ const ApiKeyModal = ({
onExtraButtonClick={onRemove}
disabled={disabled}
>
<AuthForm
ref={formRef}
formSchemas={formSchemas}
defaultValues={editValues}
disabled={disabled}
/>
{
isLoading && (
<div className='flex h-40 items-center justify-center'>
<Loading />
</div>
)
}
{
!isLoading && !!data.length && (
<AuthForm
ref={formRef}
formSchemas={formSchemas}
defaultValues={editValues || defaultValues}
disabled={disabled}
/>
)
}
</Modal>
)
}

View File

@ -2,6 +2,7 @@ import {
memo,
useMemo,
} from 'react'
import { useTranslation } from 'react-i18next'
import AddOAuthButton from './add-oauth-button'
import type { AddOAuthButtonProps } from './add-oauth-button'
import AddApiKeyButton from './add-api-key-button'
@ -24,10 +25,11 @@ const Authorize = ({
canApiKey,
disabled,
}: AuthorizeProps) => {
const { t } = useTranslation()
const oAuthButtonProps: AddOAuthButtonProps = useMemo(() => {
if (theme === 'secondary') {
return {
buttonText: !canApiKey ? 'Add OAuth Authorization' : 'Add OAuth',
buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
buttonVariant: 'secondary',
className: 'hover:bg-components-button-secondary-bg',
buttonLeftClassName: 'hover:bg-components-button-secondary-bg-hover',
@ -38,25 +40,25 @@ const Authorize = ({
}
return {
buttonText: !canApiKey ? 'Use OAuth Authorization' : 'Use OAuth',
buttonText: !canApiKey ? t('plugin.auth.useOAuthAuth') : t('plugin.auth.addOAuth'),
pluginPayload,
}
}, [canApiKey, theme, pluginPayload])
}, [canApiKey, theme, pluginPayload, t])
const apiKeyButtonProps: AddApiKeyButtonProps = useMemo(() => {
if (theme === 'secondary') {
return {
pluginPayload,
buttonVariant: 'secondary',
buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Add API Key',
buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
}
}
return {
pluginPayload,
buttonText: !canOAuth ? 'API Key Authorization Configuration' : 'Use API Key',
buttonText: !canOAuth ? t('plugin.auth.useApiAuth') : t('plugin.auth.addApi'),
buttonVariant: !canOAuth ? 'primary' : 'secondary-accent',
}
}, [canOAuth, theme, pluginPayload])
}, [canOAuth, theme, pluginPayload, t])
return (
<>

View File

@ -3,6 +3,7 @@ import {
useCallback,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
@ -26,6 +27,7 @@ const AuthorizedInNode = ({
onAuthorizationItemClick,
credentialId,
}: AuthorizedInNodeProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const {
canApiKey,
@ -37,11 +39,11 @@ const AuthorizedInNode = ({
let label = ''
let removed = false
if (!credentialId) {
label = 'Workspace default'
label = t('plugin.auth.workspaceDefault')
}
else {
const credential = credentials.find(c => c.id === credentialId)
label = credential ? credential.name : 'Auth removed'
label = credential ? credential.name : t('plugin.auth.authRemoved')
removed = !credential
}
return (
@ -65,13 +67,13 @@ const AuthorizedInNode = ({
/>
</Button>
)
}, [credentialId, credentials])
}, [credentialId, credentials, t])
const extraAuthorizationItems: Credential[] = [
{
id: '__workspace_default__',
name: 'Workspace default',
name: t('plugin.auth.workspaceDefault'),
provider: '',
is_default: false,
is_default: !credentialId,
isWorkspaceDefault: true,
},
]
@ -100,6 +102,7 @@ const AuthorizedInNode = ({
disableSetDefault
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
/>
)
}

View File

@ -31,6 +31,7 @@ import {
useDeletePluginCredentialHook,
useInvalidPluginCredentialInfoHook,
useSetPluginDefaultCredentialHook,
useUpdatePluginCredentialHook,
} from '../hooks/use-credential'
type AuthorizedProps = {
@ -49,6 +50,7 @@ type AuthorizedProps = {
disableSetDefault?: boolean
onItemClick?: (id: string) => void
extraAuthorizationItems?: Credential[]
showItemSelectedIcon?: boolean
}
const Authorized = ({
pluginPayload,
@ -66,6 +68,7 @@ const Authorized = ({
disableSetDefault,
onItemClick,
extraAuthorizationItems,
showItemSelectedIcon,
}: AuthorizedProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
@ -125,6 +128,17 @@ const Authorized = ({
})
invalidatePluginCredentialInfo()
}, [setPluginDefaultCredential, invalidatePluginCredentialInfo, notify, t])
const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload)
const handleRename = useCallback(async (payload: {
credential_id: string
name: string
}) => {
await updatePluginCredential(payload)
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
}, [updatePluginCredential, notify, t])
return (
<>
@ -149,7 +163,12 @@ const Authorized = ({
isOpen && 'bg-components-button-secondary-bg-hover',
)}>
<Indicator className='mr-2' />
{credentials.length} Authorizations
{credentials.length}&nbsp;
{
credentials.length > 1
? t('plugin.auth.authorizations')
: t('plugin.auth.authorization')
}
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
)
@ -160,31 +179,35 @@ const Authorized = ({
'max-h-[360px] overflow-y-auto rounded-xl border-[0.5px] border-components-panel-border bg-components-panel-bg-blur shadow-lg',
popupClassName,
)}>
{
!!extraAuthorizationItems?.length && (
<div className='p-1'>
{
extraAuthorizationItems.map(credential => (
<Item
key={credential.id}
credential={credential}
disabled={disabled}
onItemClick={onItemClick}
disableRename
disableEdit
disableDelete
disableSetDefault
/>
))
}
</div>
)
}
<div className='py-1'>
{
!!extraAuthorizationItems?.length && (
<div className='p-1'>
{
extraAuthorizationItems.map(credential => (
<Item
key={credential.id}
credential={credential}
disabled={disabled}
onItemClick={onItemClick}
disableRename
disableEdit
disableDelete
disableSetDefault
showSelectedIcon={showItemSelectedIcon}
/>
))
}
</div>
)
}
{
!!oAuthCredentials.length && (
<div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
<div className={cn(
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
showItemSelectedIcon && 'pl-7',
)}>
OAuth
</div>
{
@ -192,6 +215,14 @@ const Authorized = ({
<Item
key={credential.id}
credential={credential}
disabled={disabled}
disableEdit
onDelete={openConfirm}
onSetDefault={handleSetDefault}
onRename={handleRename}
disableSetDefault={disableSetDefault}
onItemClick={onItemClick}
showSelectedIcon={showItemSelectedIcon}
/>
))
}
@ -201,7 +232,10 @@ const Authorized = ({
{
!!apiKeyCredentials.length && (
<div className='p-1'>
<div className='system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary'>
<div className={cn(
'system-xs-medium px-3 pb-0.5 pt-1 text-text-tertiary',
showItemSelectedIcon && 'pl-7',
)}>
API Keys
</div>
{
@ -214,7 +248,10 @@ const Authorized = ({
onEdit={handleEdit}
onSetDefault={handleSetDefault}
disableSetDefault={disableSetDefault}
disableRename
onItemClick={onItemClick}
onRename={handleRename}
showSelectedIcon={showItemSelectedIcon}
/>
))
}

View File

@ -1,8 +1,11 @@
import {
memo,
useMemo,
useState,
} from 'react'
import { useTranslation } from 'react-i18next'
import {
RiCheckLine,
RiDeleteBinLine,
RiEditLine,
RiEqualizer2Line,
@ -12,6 +15,8 @@ import Badge from '@/app/components/base/badge'
import ActionButton from '@/app/components/base/action-button'
import Tooltip from '@/app/components/base/tooltip'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
import cn from '@/utils/classnames'
import type { Credential } from '../types'
import { CredentialTypeEnum } from '../types'
@ -21,11 +26,16 @@ type ItemProps = {
onDelete?: (id: string) => void
onEdit?: (id: string, values: Record<string, any>) => void
onSetDefault?: (id: string) => void
onRename?: (payload: {
credential_id: string
name: string
}) => void
disableRename?: boolean
disableEdit?: boolean
disableDelete?: boolean
disableSetDefault?: boolean
onItemClick?: (id: string) => void
showSelectedIcon?: boolean
}
const Item = ({
credential,
@ -33,12 +43,17 @@ const Item = ({
onDelete,
onEdit,
onSetDefault,
onRename,
disableRename,
disableEdit,
disableDelete,
disableSetDefault,
onItemClick,
showSelectedIcon,
}: ItemProps) => {
const { t } = useTranslation()
const [renaming, setRenaming] = useState(false)
const [renameValue, setRenameValue] = useState(credential.name)
const isOAuth = credential.credential_type === CredentialTypeEnum.OAUTH2
const showAction = useMemo(() => {
return !(disableRename && disableEdit && disableDelete && disableSetDefault)
@ -47,27 +62,77 @@ const Item = ({
return (
<div
key={credential.id}
className='group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover'
className={cn(
'group flex h-8 items-center rounded-lg p-1 hover:bg-state-base-hover',
renaming && 'bg-state-base-hover',
)}
onClick={() => onItemClick?.(credential.id)}
>
<div className='flex w-0 grow items-center space-x-1.5 pl-2'>
<Indicator className='mr-1.5 shrink-0' />
<div
className='system-md-regular truncate text-text-secondary'
title={credential.name}
>
{credential.name}
</div>
{
credential.is_default && (
<Badge>
Default
</Badge>
)
}
</div>
{
showAction && (
renaming && (
<div className='flex w-full items-center space-x-1'>
<Input
wrapperClassName='grow rounded-[6px]'
className='h-6'
value={renameValue}
onChange={e => setRenameValue(e.target.value)}
placeholder={t('common.placeholder.input')}
/>
<Button
size='small'
variant='primary'
onClick={() => {
onRename?.({
credential_id: credential.id,
name: renameValue,
})
setRenaming(false)
}}
>
{t('common.operation.save')}
</Button>
<Button
size='small'
onClick={() => setRenaming(false)}
>
{t('common.operation.cancel')}
</Button>
</div>
)
}
{
!renaming && (
<div className='flex w-0 grow items-center space-x-1.5'>
{
showSelectedIcon && (
<div className='h-4 w-4'>
{
credential.is_default && (
<RiCheckLine className='h-4 w-4 text-text-accent' />
)
}
</div>
)
}
<Indicator className='ml-2 mr-1.5 shrink-0' />
<div
className='system-md-regular truncate text-text-secondary'
title={credential.name}
>
{credential.name}
</div>
{
credential.is_default && (
<Badge className='shrink-0'>
{t('plugin.auth.default')}
</Badge>
)
}
</div>
)
}
{
showAction && !renaming && (
<div className='ml-2 hidden shrink-0 items-center group-hover:flex'>
{
!credential.is_default && !disableSetDefault && (
@ -79,14 +144,21 @@ const Item = ({
onSetDefault?.(credential.id)
}}
>
Set as default
{t('plugin.auth.setDefault')}
</Button>
)
}
{
isOAuth && !disableRename && (
<Tooltip popupContent='rename'>
<ActionButton>
!disableRename && (
<Tooltip popupContent={t('common.operation.rename')}>
<ActionButton
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
setRenaming(true)
setRenameValue(credential.name)
}}
>
<RiEditLine className='h-4 w-4 text-text-tertiary' />
</ActionButton>
</Tooltip>
@ -94,7 +166,7 @@ const Item = ({
}
{
!isOAuth && !disableEdit && (
<Tooltip popupContent='edit'>
<Tooltip popupContent={t('common.operation.edit')}>
<ActionButton
disabled={disabled}
onClick={(e) => {
@ -116,15 +188,16 @@ const Item = ({
}
{
!disableDelete && (
<Tooltip popupContent='delete'>
<Tooltip popupContent={t('common.operation.delete')}>
<ActionButton
className='hover:bg-transparent'
disabled={disabled}
onClick={(e) => {
e.stopPropagation()
onDelete?.(credential.id)
}}
>
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary' />
<RiDeleteBinLine className='h-4 w-4 text-text-tertiary hover:text-text-destructive' />
</ActionButton>
</Tooltip>
)

View File

@ -1,5 +1,6 @@
export { default as PluginAuth } from './plugin-auth'
export { default as Authorized } from './authorized'
export { default as AuthorizedInNode } from './authorized-in-node'
export { default as PluginAuthInAgent } from './plugin-auth-in-agent'
export { usePluginAuth } from './hooks/use-plugin-auth'
export * from './types'

View File

@ -0,0 +1,119 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { RiArrowDownSLine } from '@remixicon/react'
import { useTranslation } from 'react-i18next'
import Authorize from './authorize'
import Authorized from './authorized'
import type {
Credential,
PluginPayload,
} from './types'
import { usePluginAuth } from './hooks/use-plugin-auth'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
type PluginAuthInAgentProps = {
pluginPayload: PluginPayload
credentialId?: string
onAuthorizationItemClick?: (id: string) => void
}
const PluginAuthInAgent = ({
pluginPayload,
credentialId,
onAuthorizationItemClick,
}: PluginAuthInAgentProps) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const {
isAuthorized,
canOAuth,
canApiKey,
credentials,
disabled,
} = usePluginAuth(pluginPayload, true)
const extraAuthorizationItems: Credential[] = [
{
id: '__workspace_default__',
name: t('plugin.auth.workspaceDefault'),
provider: '',
is_default: !credentialId,
isWorkspaceDefault: true,
},
]
const handleAuthorizationItemClick = useCallback((id: string) => {
onAuthorizationItemClick?.(id)
setIsOpen(false)
}, [
onAuthorizationItemClick,
setIsOpen,
])
const renderTrigger = useCallback((isOpen?: boolean) => {
let label = ''
let removed = false
if (!credentialId) {
label = t('plugin.auth.workspaceDefault')
}
else {
const credential = credentials.find(c => c.id === credentialId)
label = credential ? credential.name : t('plugin.auth.authRemoved')
removed = !credential
}
return (
<Button
className={cn(
'w-full',
isOpen && 'bg-components-button-secondary-bg-hover',
removed && 'text-text-destructive',
)}>
<Indicator
className='mr-2'
color={removed ? 'red' : 'green'}
/>
{label}
<RiArrowDownSLine className='ml-0.5 h-4 w-4' />
</Button>
)
}, [credentialId, credentials, t])
return (
<>
{
!isAuthorized && (
<Authorize
pluginPayload={pluginPayload}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
/>
)
}
{
isAuthorized && (
<Authorized
pluginPayload={pluginPayload}
credentials={credentials}
canOAuth={canOAuth}
canApiKey={canApiKey}
disabled={disabled}
disableSetDefault
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
renderTrigger={renderTrigger}
isOpen={isOpen}
onOpenChange={setIsOpen}
/>
)
}
</>
)
}
export default memo(PluginAuthInAgent)