tool oauth

This commit is contained in:
zxhlyh
2025-07-11 14:40:36 +08:00
parent 90f800408d
commit cb0082c0b8
13 changed files with 222 additions and 57 deletions

View File

@@ -18,7 +18,6 @@ import AppIcon from '@/app/components/base/app-icon'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import Switch from '@/app/components/base/switch'
import Toast from '@/app/components/base/toast'
import ConfigContext from '@/context/debug-configuration'
import type { AgentTool } from '@/types/app'
import { type Collection, CollectionType } from '@/app/components/tools/types'
@@ -26,8 +25,6 @@ import { MAX_TOOLS_NUM } from '@/config'
import { AlertTriangle } from '@/app/components/base/icons/src/vender/solid/alertsAndFeedback'
import Tooltip from '@/app/components/base/tooltip'
import { DefaultToolIcon } from '@/app/components/base/icons/src/public/other'
import ConfigCredential from '@/app/components/tools/setting/build-in/config-credentials'
import { updateBuiltInToolCredential } from '@/service/tools'
import cn from '@/utils/classnames'
import ToolPicker from '@/app/components/workflow/block-selector/tool-picker'
import type { ToolDefaultValue, ToolValue } from '@/app/components/workflow/block-selector/types'
@@ -57,13 +54,7 @@ const AgentTools: FC = () => {
const formattingChangedDispatcher = useFormattingChangedDispatcher()
const [currentTool, setCurrentTool] = useState<AgentToolWithMoreInfo>(null)
const currentCollection = useMemo(() => {
if (!currentTool) return null
const collection = collectionList.find(collection => canFindTool(collection.id, currentTool?.provider_id) && collection.type === currentTool?.provider_type)
return collection
}, [currentTool, collectionList])
const [isShowSettingTool, setIsShowSettingTool] = useState(false)
const [isShowSettingAuth, setShowSettingAuth] = useState(false)
const tools = (modelConfig?.agentConfig?.tools as AgentTool[] || []).map((item) => {
const collection = collectionList.find(
collection =>
@@ -100,17 +91,6 @@ const AgentTools: FC = () => {
formattingChangedDispatcher()
}
const handleToolAuthSetting = (value: AgentToolWithMoreInfo) => {
const newModelConfig = produce(modelConfig, (draft) => {
const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === value?.collection?.id && item.tool_name === value?.tool_name)
if (tool)
(tool as AgentTool).notAuthor = false
})
setModelConfig(newModelConfig)
setIsShowSettingTool(false)
formattingChangedDispatcher()
}
const [isDeleting, setIsDeleting] = useState<number>(-1)
const getToolValue = (tool: ToolDefaultValue) => {
return {
@@ -144,6 +124,20 @@ const AgentTools: FC = () => {
return item.provider_name
}
const handleAuthorizationItemClick = useCallback((credentialId: string) => {
const newModelConfig = produce(modelConfig, (draft) => {
const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === currentTool?.provider_id)
if (tool)
(tool as AgentTool).credential_id = credentialId
})
setCurrentTool({
...currentTool,
credential_id: credentialId,
} as any)
setModelConfig(newModelConfig)
formattingChangedDispatcher()
}, [currentTool, modelConfig, setModelConfig, formattingChangedDispatcher])
return (
<>
<Panel
@@ -302,7 +296,7 @@ const AgentTools: FC = () => {
{item.notAuthor && (
<Button variant='secondary' size='small' onClick={() => {
setCurrentTool(item)
setShowSettingAuth(true)
setIsShowSettingTool(true)
}}>
{t('tools.notAuthorized')}
<Indicator className='ml-2' color='orange' />
@@ -322,21 +316,8 @@ const AgentTools: FC = () => {
isModel={currentTool?.collection?.type === CollectionType.model}
onSave={handleToolSettingChange}
onHide={() => setIsShowSettingTool(false)}
/>
)}
{isShowSettingAuth && (
<ConfigCredential
collection={currentCollection as any}
onCancel={() => setShowSettingAuth(false)}
onSaved={async (value) => {
await updateBuiltInToolCredential((currentCollection as any).name, value)
Toast.notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
handleToolAuthSetting(currentTool)
setShowSettingAuth(false)
}}
credentialId={currentTool?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
)}
</>

View File

@@ -14,7 +14,6 @@ import Icon from '@/app/components/plugins/card/base/card-icon'
import OrgInfo from '@/app/components/plugins/card/base/org-info'
import Description from '@/app/components/plugins/card/base/description'
import TabSlider from '@/app/components/base/tab-slider-plain'
import Button from '@/app/components/base/button'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { addDefaultValue, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
@@ -25,6 +24,10 @@ import I18n from '@/context/i18n'
import { getLanguage } from '@/i18n/language'
import cn from '@/utils/classnames'
import type { ToolWithProvider } from '@/app/components/workflow/types'
import {
AuthCategory,
PluginAuthInAgent,
} from '@/app/components/plugins/plugin-auth'
type Props = {
showBackButton?: boolean
@@ -36,6 +39,8 @@ type Props = {
readonly?: boolean
onHide: () => void
onSave?: (value: Record<string, any>) => void
credentialId?: string
onAuthorizationItemClick?: (id: string) => void
}
const SettingBuiltInTool: FC<Props> = ({
@@ -48,6 +53,8 @@ const SettingBuiltInTool: FC<Props> = ({
readonly,
onHide,
onSave,
credentialId,
onAuthorizationItemClick,
}) => {
const { locale } = useContext(I18n)
const language = getLanguage(locale)
@@ -197,8 +204,20 @@ const SettingBuiltInTool: FC<Props> = ({
</div>
<div className='system-md-semibold mt-1 text-text-primary'>{currTool?.label[language]}</div>
{!!currTool?.description[language] && (
<Description className='mt-3' text={currTool.description[language]} descriptionLineRows={2}></Description>
<Description className='mb-2 mt-3 h-auto' text={currTool.description[language]} descriptionLineRows={2}></Description>
)}
{
collection.allow_delete && collection.type === CollectionType.builtIn && (
<PluginAuthInAgent
pluginPayload={{
provider: collection.name,
category: AuthCategory.tool,
}}
credentialId={credentialId}
onAuthorizationItemClick={onAuthorizationItemClick}
/>
)
}
</div>
{/* form */}
<div className='h-full'>

View File

@@ -1,5 +1,6 @@
import {
memo,
useCallback,
useState,
} from 'react'
import { RiEqualizer2Line } from '@remixicon/react'
@@ -8,6 +9,10 @@ import type { ButtonProps } from '@/app/components/base/button'
import OAuthClientSettings from './oauth-client-settings'
import cn from '@/utils/classnames'
import type { PluginPayload } from '../types'
import { openOAuthPopup } from '@/hooks/use-oauth'
import {
useGetPluginOAuthUrlHook,
} from '../hooks/use-credential'
export type AddOAuthButtonProps = {
pluginPayload: PluginPayload
@@ -20,6 +25,7 @@ export type AddOAuthButtonProps = {
disabled?: boolean
}
const AddOAuthButton = ({
pluginPayload,
buttonVariant = 'primary',
buttonText = 'use oauth',
className,
@@ -29,6 +35,20 @@ const AddOAuthButton = ({
disabled,
}: AddOAuthButtonProps) => {
const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false)
const { mutateAsync: getPluginOAuthUrl } = useGetPluginOAuthUrlHook(pluginPayload)
const handleOAuth = useCallback(async () => {
const { authorization_url } = await getPluginOAuthUrl()
if (authorization_url) {
openOAuthPopup(
authorization_url,
() => {
console.log('success')
},
)
}
}, [getPluginOAuthUrl])
return (
<>
@@ -39,6 +59,7 @@ const AddOAuthButton = ({
className,
)}
disabled={disabled}
onClick={handleOAuth}
>
<div className={cn(
'flex h-full grow items-center justify-center rounded-l-lg hover:bg-components-button-primary-bg-hover',
@@ -55,7 +76,10 @@ const AddOAuthButton = ({
'flex h-full w-8 shrink-0 items-center justify-center rounded-r-lg hover:bg-components-button-primary-bg-hover',
buttonRightClassName,
)}
onClick={() => setIsOAuthSettingsOpen(true)}
onClick={(e) => {
e.stopPropagation()
setIsOAuthSettingsOpen(true)
}}
>
<RiEqualizer2Line className='h-4 w-4' />
</div>
@@ -63,7 +87,9 @@ const AddOAuthButton = ({
{
isOAuthSettingsOpen && (
<OAuthClientSettings
pluginPayload={pluginPayload}
onClose={() => setIsOAuthSettingsOpen(false)}
disabled={disabled}
/>
)
}

View File

@@ -1,12 +1,80 @@
import { memo } from 'react'
import {
memo,
useCallback,
useMemo,
useRef,
} from 'react'
import { useTranslation } from 'react-i18next'
import Modal from '@/app/components/base/modal/modal'
import {
useGetPluginOAuthClientSchemaHook,
useInvalidPluginCredentialInfoHook,
useSetPluginOAuthCustomClientHook,
} from '../hooks/use-credential'
import type { PluginPayload } from '../types'
import Loading from '@/app/components/base/loading'
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 { transformFormSchemasSecretInput } from '../utils'
import { useToastContext } from '@/app/components/base/toast'
type OAuthClientSettingsProps = {
pluginPayload: PluginPayload
onClose?: () => void
editValues?: Record<string, any>
disabled?: boolean
}
const OAuthClientSettings = ({
pluginPayload,
onClose,
editValues,
disabled,
}: OAuthClientSettingsProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
const {
data,
isLoading,
} = useGetPluginOAuthClientSchemaHook(pluginPayload)
const formSchemas = useMemo(() => {
return data?.schema || []
}, [data])
const defaultValues = formSchemas.reduce((acc, schema) => {
if (schema.default)
acc[schema.name] = schema.default
return acc
}, {} as Record<string, any>)
const { mutateAsync: setPluginOAuthCustomClient } = useSetPluginOAuthCustomClientHook(pluginPayload)
const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload)
const formRef = useRef<FromRefObject>(null)
const handleConfirm = useCallback(async () => {
const form = formRef.current?.getForm()
const store = form?.store
const values = store?.state.values
const isPristineSecretInputNames: string[] = []
formSchemas.forEach((schema) => {
if (schema.type === FormTypeEnum.secretInput) {
const fieldMeta = form?.getFieldMeta(schema.name)
if (fieldMeta?.isPristine)
isPristineSecretInputNames.push(schema.name)
}
})
const transformedValues = transformFormSchemasSecretInput(isPristineSecretInputNames, values)
await setPluginOAuthCustomClient({
client_params: transformedValues,
enable_oauth_custom_client: true,
})
notify({
type: 'success',
message: t('common.api.actionSuccess'),
})
onClose?.()
invalidatePluginCredentialInfo()
}, [onClose, invalidatePluginCredentialInfo, setPluginOAuthCustomClient, notify, t, formSchemas])
return (
<Modal
title='Oauth client settings'
@@ -17,8 +85,25 @@ const OAuthClientSettings = ({
extraButtonVariant='secondary'
onExtraButtonClick={onClose}
onClose={onClose}
onConfirm={handleConfirm}
>
<div>oauth</div>
{
isLoading && (
<div className='flex h-40 items-center justify-center'>
<Loading />
</div>
)
}
{
!isLoading && !!data?.schema.length && (
<AuthForm
ref={formRef}
formSchemas={formSchemas}
defaultValues={editValues || defaultValues}
disabled={disabled}
/>
)
}
</Modal>
)
}

View File

@@ -103,6 +103,7 @@ const AuthorizedInNode = ({
onItemClick={handleAuthorizationItemClick}
extraAuthorizationItems={extraAuthorizationItems}
showItemSelectedIcon
selectedCredentialId={credentialId || '__workspace_default__'}
/>
)
}

View File

@@ -51,6 +51,7 @@ type AuthorizedProps = {
onItemClick?: (id: string) => void
extraAuthorizationItems?: Credential[]
showItemSelectedIcon?: boolean
selectedCredentialId?: string
}
const Authorized = ({
pluginPayload,
@@ -69,6 +70,7 @@ const Authorized = ({
onItemClick,
extraAuthorizationItems,
showItemSelectedIcon,
selectedCredentialId,
}: AuthorizedProps) => {
const { t } = useTranslation()
const { notify } = useToastContext()
@@ -195,6 +197,7 @@ const Authorized = ({
disableDelete
disableSetDefault
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
/>
))
}
@@ -223,6 +226,7 @@ const Authorized = ({
disableSetDefault={disableSetDefault}
onItemClick={onItemClick}
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
/>
))
}
@@ -252,6 +256,7 @@ const Authorized = ({
onItemClick={onItemClick}
onRename={handleRename}
showSelectedIcon={showItemSelectedIcon}
selectedCredentialId={selectedCredentialId}
/>
))
}

View File

@@ -36,6 +36,7 @@ type ItemProps = {
disableSetDefault?: boolean
onItemClick?: (id: string) => void
showSelectedIcon?: boolean
selectedCredentialId?: string
}
const Item = ({
credential,
@@ -50,6 +51,7 @@ const Item = ({
disableSetDefault,
onItemClick,
showSelectedIcon,
selectedCredentialId,
}: ItemProps) => {
const { t } = useTranslation()
const [renaming, setRenaming] = useState(false)
@@ -107,7 +109,7 @@ const Item = ({
showSelectedIcon && (
<div className='h-4 w-4'>
{
credential.is_default && (
selectedCredentialId === credential.id && (
<RiCheckLine className='h-4 w-4 text-text-accent' />
)
}

View File

@@ -3,8 +3,12 @@ import {
useDeletePluginCredential,
useGetPluginCredentialInfo,
useGetPluginCredentialSchema,
useGetPluginOAuthClientSchema,
useGetPluginOAuthCustomClientSchema,
useGetPluginOAuthUrl,
useInvalidPluginCredentialInfo,
useSetPluginDefaultCredential,
useSetPluginOAuthCustomClient,
useUpdatePluginCredential,
} from '@/service/use-plugins-auth'
import { useGetApi } from './use-get-api'
@@ -51,3 +55,27 @@ export const useUpdatePluginCredentialHook = (pluginPayload: PluginPayload) => {
return useUpdatePluginCredential(apiMap.updateCredential)
}
export const useGetPluginOAuthUrlHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useGetPluginOAuthUrl(apiMap.getOauthUrl)
}
export const useGetPluginOAuthClientSchemaHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useGetPluginOAuthClientSchema(apiMap.getOauthClientSchema)
}
export const useSetPluginOAuthCustomClientHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useSetPluginOAuthCustomClient(apiMap.setCustomOauthClient)
}
export const useGetPluginOAuthCustomClientSchemaHook = (pluginPayload: PluginPayload) => {
const apiMap = useGetApi(pluginPayload)
return useGetPluginOAuthCustomClientSchema(apiMap.getCustomOAuthClient)
}

View File

@@ -109,6 +109,7 @@ const PluginAuthInAgent = ({
renderTrigger={renderTrigger}
isOpen={isOpen}
onOpenChange={setIsOpen}
selectedCredentialId={credentialId || '__workspace_default__'}
/>
)
}

View File

@@ -272,7 +272,7 @@ const DetailHeader = ({
</ActionButton>
</div>
</div>
<Description className='mt-3' text={description[locale]} descriptionLineRows={2}></Description>
<Description className='mb-2 mt-3 h-auto' text={description[locale]} descriptionLineRows={2}></Description>
{
category === PluginType.tool && (
<PluginAuth

View File

@@ -311,13 +311,13 @@ const ToolSelector: FC<Props> = ({
<Divider className='my-1 w-full' />
<div className='px-4 py-2'>
<PluginAuthInAgent
pluginPayload={{
provider: currentProvider.name,
category: AuthCategory.tool,
}}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
pluginPayload={{
provider: currentProvider.name,
category: AuthCategory.tool,
}}
credentialId={value?.credential_id}
onAuthorizationItemClick={handleAuthorizationItemClick}
/>
</div>
</>
)}

View File

@@ -102,9 +102,16 @@ export const useGetPluginCredentialSchema = (
export const useGetPluginOAuthUrl = (
url: string,
) => {
return useQuery({
queryKey: [NAME_SPACE, 'oauth-url', url],
queryFn: () => get(url),
return useMutation({
mutationKey: [NAME_SPACE, 'oauth-url', url],
mutationFn: () => {
return get<
{
authorization_url: string
state: string
context_id: string
}>(url)
},
})
}
@@ -113,7 +120,10 @@ export const useGetPluginOAuthClientSchema = (
) => {
return useQuery({
queryKey: [NAME_SPACE, 'oauth-client-schema', url],
queryFn: () => get(url),
queryFn: () => get<{
schema: FormSchema[]
is_oauth_custom_client_enabled: boolean
}>(url),
})
}
@@ -121,8 +131,11 @@ export const useSetPluginOAuthCustomClient = (
url: string,
) => {
return useMutation({
mutationFn: (params) => {
return post(url, { body: params })
mutationFn: (params: {
client_params: Record<string, any>
enable_oauth_custom_client: boolean
}) => {
return post<{ result: string }>(url, { body: params })
},
})
}
@@ -132,6 +145,9 @@ export const useGetPluginOAuthCustomClientSchema = (
) => {
return useQuery({
queryKey: [NAME_SPACE, 'oauth-custom-client-schema', url],
queryFn: () => get(url),
queryFn: () => get<{
client_id: string
client_secret: string
}>(url),
})
}

View File

@@ -130,6 +130,7 @@ export type AgentTool = {
enabled: boolean
isDeleted?: boolean
notAuthor?: boolean
credential_id?: string
}
export type ToolItem = {