From cb4670cd687ae18fdbabc88ab8915b6d8fd02c4b Mon Sep 17 00:00:00 2001 From: zhsama Date: Thu, 27 Nov 2025 17:44:04 +0800 Subject: [PATCH] feat: enhance agent tools with end user credential support --- .../config/agent/agent-tools/index.tsx | 38 ++- .../agent-tools/setting-built-in-tool.tsx | 12 + .../plugin-auth/hooks/use-plugin-auth.ts | 2 + .../plugin-auth/plugin-auth-in-agent.tsx | 246 +++++++++++++++++- .../plugins/plugin-auth/plugin-auth.tsx | 112 ++++---- .../tool-selector/index.tsx | 16 ++ web/types/app.ts | 2 + 7 files changed, 376 insertions(+), 52 deletions(-) diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index f2b9c105fc..133f41074f 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -33,7 +33,7 @@ import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTo import type { ToolWithProvider } from '@/app/components/workflow/types' import { useMittContextSelector } from '@/context/mitt-context' -type AgentToolWithMoreInfo = AgentTool & { icon: any; collection?: Collection } | null +type AgentToolWithMoreInfo = (AgentTool & { icon: any; collection?: Collection; use_end_user_credentials?: boolean; end_user_credential_type?: string }) | null const AgentTools: FC = () => { const { t } = useTranslation() const [isShowChooseTool, setIsShowChooseTool] = useState(false) @@ -102,7 +102,9 @@ const AgentTools: FC = () => { tool_parameters: tool.params, notAuthor: !tool.is_team_authorization, enabled: true, - } + use_end_user_credentials: false, + end_user_credential_type: '', + } as any } const handleSelectTool = (tool: ToolDefaultValue) => { const newModelConfig = produce(modelConfig, (draft) => { @@ -138,6 +140,34 @@ const AgentTools: FC = () => { formattingChangedDispatcher() }, [currentTool, modelConfig, setModelConfig, formattingChangedDispatcher]) + const handleEndUserCredentialChange = useCallback((enabled: boolean) => { + const newModelConfig = produce(modelConfig, (draft) => { + const tool = (draft.agentConfig.tools).find((item: any) => item.provider_id === currentTool?.provider_id) + if (tool) + (tool as AgentTool).use_end_user_credentials = enabled + }) + setCurrentTool({ + ...currentTool, + use_end_user_credentials: enabled, + } as any) + setModelConfig(newModelConfig) + formattingChangedDispatcher() + }, [currentTool, modelConfig, setModelConfig, formattingChangedDispatcher]) + + const handleEndUserCredentialTypeChange = useCallback((type: 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).end_user_credential_type = type + }) + setCurrentTool({ + ...currentTool, + end_user_credential_type: type, + } as any) + setModelConfig(newModelConfig) + formattingChangedDispatcher() + }, [currentTool, modelConfig, setModelConfig, formattingChangedDispatcher]) + return ( <> { onHide={() => setIsShowSettingTool(false)} credentialId={currentTool?.credential_id} onAuthorizationItemClick={handleAuthorizationItemClick} + useEndUserCredentialEnabled={currentTool?.use_end_user_credentials} + endUserCredentialType={currentTool?.end_user_credential_type} + onEndUserCredentialChange={handleEndUserCredentialChange} + onEndUserCredentialTypeChange={handleEndUserCredentialTypeChange} /> )} diff --git a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx index ef28dd222c..aaa4d5830e 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/setting-built-in-tool.tsx @@ -42,6 +42,10 @@ type Props = { onSave?: (value: Record) => void credentialId?: string onAuthorizationItemClick?: (id: string) => void + useEndUserCredentialEnabled?: boolean + endUserCredentialType?: string + onEndUserCredentialChange?: (enabled: boolean) => void + onEndUserCredentialTypeChange?: (type: string) => void } const SettingBuiltInTool: FC = ({ @@ -56,6 +60,10 @@ const SettingBuiltInTool: FC = ({ onSave, credentialId, onAuthorizationItemClick, + useEndUserCredentialEnabled, + endUserCredentialType, + onEndUserCredentialChange, + onEndUserCredentialTypeChange, }) => { const { locale } = useContext(I18n) const language = getLanguage(locale) @@ -220,6 +228,10 @@ const SettingBuiltInTool: FC = ({ }} credentialId={credentialId} onAuthorizationItemClick={onAuthorizationItemClick} + useEndUserCredentialEnabled={useEndUserCredentialEnabled} + endUserCredentialType={endUserCredentialType} + onEndUserCredentialChange={onEndUserCredentialChange} + onEndUserCredentialTypeChange={onEndUserCredentialTypeChange} /> ) } diff --git a/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts index 6d0c0496b6..4c8b162e0e 100644 --- a/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts +++ b/web/app/components/plugins/plugin-auth/hooks/use-plugin-auth.ts @@ -13,6 +13,7 @@ export const usePluginAuth = (pluginPayload: PluginPayload, enable?: boolean) => const canOAuth = data?.supported_credential_types.includes(CredentialTypeEnum.OAUTH2) const canApiKey = data?.supported_credential_types.includes(CredentialTypeEnum.API_KEY) const invalidPluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) + const hasOAuthClientConfigured = data?.is_oauth_custom_client_enabled return { isAuthorized, @@ -22,5 +23,6 @@ export const usePluginAuth = (pluginPayload: PluginPayload, enable?: boolean) => disabled: !isCurrentWorkspaceManager, notAllowCustomCredential: data?.allow_custom_token === false, invalidPluginCredentialInfo, + hasOAuthClientConfigured: !!hasOAuthClientConfigured, } } diff --git a/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx b/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx index 9a9fca78a0..d4b45edb26 100644 --- a/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx +++ b/web/app/components/plugins/plugin-auth/plugin-auth-in-agent.tsx @@ -1,12 +1,23 @@ import { memo, useCallback, + useEffect, + useMemo, useState, } from 'react' -import { RiArrowDownSLine } from '@remixicon/react' +import type { ReactNode } from 'react' +import { + RiAddLine, + RiArrowDownSLine, + RiEqualizer2Line, + RiKey2Line, + RiUserStarLine, +} from '@remixicon/react' import { useTranslation } from 'react-i18next' import Authorize from './authorize' import Authorized from './authorized' +import AddOAuthButton from './authorize/add-oauth-button' +import AddApiKeyButton from './authorize/add-api-key-button' import type { Credential, PluginPayload, @@ -15,19 +26,35 @@ 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' +import { + PortalToFollowElem, + PortalToFollowElemContent, + PortalToFollowElemTrigger, +} from '@/app/components/base/portal-to-follow-elem' +import Switch from '@/app/components/base/switch' type PluginAuthInAgentProps = { pluginPayload: PluginPayload credentialId?: string onAuthorizationItemClick?: (id: string) => void + useEndUserCredentialEnabled?: boolean + endUserCredentialType?: string + onEndUserCredentialChange?: (enabled: boolean) => void + onEndUserCredentialTypeChange?: (type: string) => void } const PluginAuthInAgent = ({ pluginPayload, credentialId, onAuthorizationItemClick, + useEndUserCredentialEnabled, + endUserCredentialType, + onEndUserCredentialChange, + onEndUserCredentialTypeChange, }: PluginAuthInAgentProps) => { const { t } = useTranslation() const [isOpen, setIsOpen] = useState(false) + const [showAddMenu, setShowAddMenu] = useState(false) + const [showEndUserTypeMenu, setShowEndUserTypeMenu] = useState(false) const { isAuthorized, canOAuth, @@ -36,6 +63,7 @@ const PluginAuthInAgent = ({ disabled, invalidPluginCredentialInfo, notAllowCustomCredential, + hasOAuthClientConfigured, } = usePluginAuth(pluginPayload, true) const extraAuthorizationItems: Credential[] = [ @@ -94,10 +122,219 @@ const PluginAuthInAgent = ({ ) }, [credentialId, credentials, t]) + const availableEndUserTypes = useMemo(() => { + const list: { value: string; label: string; icon: ReactNode }[] = [] + if (canOAuth) { + list.push({ + value: 'oauth2', + label: t('plugin.auth.endUserCredentials.optionOAuth'), + icon: , + }) + } + if (canApiKey) { + list.push({ + value: 'api-key', + label: t('plugin.auth.endUserCredentials.optionApiKey'), + icon: , + }) + } + return list + }, [canOAuth, canApiKey, t]) + + const endUserCredentialLabel = useMemo(() => { + const found = availableEndUserTypes.find(item => item.value === endUserCredentialType) + return found?.label || availableEndUserTypes[0]?.label || '-' + }, [availableEndUserTypes, endUserCredentialType]) + + useEffect(() => { + if (!useEndUserCredentialEnabled) + return + if (!availableEndUserTypes.length) + return + const isValid = availableEndUserTypes.some(item => item.value === endUserCredentialType) + if (!isValid) + onEndUserCredentialTypeChange?.(availableEndUserTypes[0].value) + }, [useEndUserCredentialEnabled, endUserCredentialType, availableEndUserTypes, onEndUserCredentialTypeChange]) + + const handleSelectEndUserType = useCallback((value: string) => { + onEndUserCredentialTypeChange?.(value) + setShowEndUserTypeMenu(false) + }, [onEndUserCredentialTypeChange]) + + const shouldShowAuthorizeCard = !credentials.length && (canOAuth || canApiKey || hasOAuthClientConfigured) + + const endUserSection = ( +
+ +
+
+
+
+ {t('plugin.auth.endUserCredentials.title')} +
+
+ {t('plugin.auth.endUserCredentials.desc')} +
+
+ +
+ { + useEndUserCredentialEnabled && availableEndUserTypes.length > 0 && ( +
+
+ {t('plugin.auth.endUserCredentials.typeLabel')} +
+ + + + + +
+
+ {canOAuth && ( + { + handleSelectEndUserType('oauth2') + invalidPluginCredentialInfo() + }} + /> + )} + {canApiKey && ( + { + handleSelectEndUserType('api-key') + invalidPluginCredentialInfo() + }} + /> + )} +
+
+
+
+
+ ) + } +
+
+ ) + return ( - <> +
+
+
+ +
+
+ {t('plugin.auth.configuredCredentials.title')} +
+
+ {t('plugin.auth.configuredCredentials.desc')} +
+
+
+ + + + + +
+
+ { + canOAuth && ( + { + setShowAddMenu(false) + invalidPluginCredentialInfo() + }} + /> + ) + } + { + canApiKey && ( + { + setShowAddMenu(false) + invalidPluginCredentialInfo() + }} + /> + ) + } +
+
+
+
+
{ - !isAuthorized && ( + !isAuthorized && shouldShowAuthorizeCard && ( +
+
+
+ +
+
+
+ ) + } + { + !isAuthorized && !shouldShowAuthorizeCard && ( ) } - + {endUserSection} +
) } diff --git a/web/app/components/plugins/plugin-auth/plugin-auth.tsx b/web/app/components/plugins/plugin-auth/plugin-auth.tsx index b9bd7339da..6ba0a6f0a2 100644 --- a/web/app/components/plugins/plugin-auth/plugin-auth.tsx +++ b/web/app/components/plugins/plugin-auth/plugin-auth.tsx @@ -9,7 +9,6 @@ import type { ReactNode } from 'react' import { RiAddLine, RiArrowDownSLine, - RiCheckLine, RiEqualizer2Line, RiKey2Line, RiUserStarLine, @@ -20,7 +19,6 @@ import { PortalToFollowElemContent, PortalToFollowElemTrigger, } from '@/app/components/base/portal-to-follow-elem' -import { SimpleSelect } from '@/app/components/base/select' import Authorize from './authorize' import Authorized from './authorized' import AddApiKeyButton from './authorize/add-api-key-button' @@ -60,11 +58,18 @@ const PluginAuth = ({ disabled, invalidPluginCredentialInfo, notAllowCustomCredential, + hasOAuthClientConfigured, } = usePluginAuth(pluginPayload, !!pluginPayload.provider) const shouldShowGuide = !!showConnectGuide const [showCredentialPanel, setShowCredentialPanel] = useState(false) const [showAddMenu, setShowAddMenu] = useState(false) + const [showEndUserTypeMenu, setShowEndUserTypeMenu] = useState(false) const configuredDisabled = !!endUserCredentialEnabled + const shouldShowAuthorizeCard = useMemo(() => { + const hasCredential = credentials.length > 0 + const canAdd = canOAuth || canApiKey || hasOAuthClientConfigured + return !hasCredential && canAdd + }, [credentials.length, canOAuth, canApiKey, hasOAuthClientConfigured]) const availableEndUserTypes = useMemo(() => { const list: { value: string; label: string; icon: ReactNode }[] = [] if (canOAuth) { @@ -100,6 +105,7 @@ const PluginAuth = ({ const handleSelectEndUserType = useCallback((value: string) => { onEndUserCredentialTypeChange?.(value) + setShowEndUserTypeMenu(false) }, [onEndUserCredentialTypeChange]) const containerClassName = useMemo(() => { if (showConnectGuide) @@ -165,43 +171,54 @@ const PluginAuth = ({
{t('plugin.auth.endUserCredentials.typeLabel')}
- ({ - value: item.value, - name: item.label, - icon: item.icon, - }))} - defaultValue={endUserCredentialType || availableEndUserTypes[0]?.value} - disabled={disabled} - onSelect={item => handleSelectEndUserType(item.value as string)} - renderTrigger={(value, open) => ( + + - )} - renderOption={({ item, selected }) => ( -
-
- {item.icon} - {item.name} + + +
+
+ {canOAuth && ( + { + handleSelectEndUserType('oauth2') + invalidPluginCredentialInfo() + }} + /> + )} + {canApiKey && ( + { + handleSelectEndUserType('api-key') + invalidPluginCredentialInfo() + }} + /> + )}
- {selected && }
- )} - optionWrapClassName='p-1' - optionClassName='px-3 py-2 rounded-lg' - hideChecked - /> +
+
) } @@ -282,8 +299,9 @@ const PluginAuth = ({ { setShowAddMenu(false) invalidPluginCredentialInfo() @@ -297,7 +315,7 @@ const PluginAuth = ({ pluginPayload={pluginPayload} buttonVariant='ghost' buttonText={t('plugin.auth.addApi')} - disabled={disabled || configuredDisabled} + disabled={disabled} onUpdate={() => { setShowAddMenu(false) invalidPluginCredentialInfo() @@ -314,22 +332,24 @@ const PluginAuth = ({ {credentialList}
{ - credentials.length === 0 && ( + shouldShowAuthorizeCard && (
- +
+ +
) diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx index ea7892be32..839ac7de51 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx @@ -210,6 +210,18 @@ const ToolSelector: FC = ({ credential_id: id, } as any) } + const handleEndUserCredentialChange = (enabled: boolean) => { + onSelect({ + ...value, + use_end_user_credentials: enabled, + } as any) + } + const handleEndUserCredentialTypeChange = (type: string) => { + onSelect({ + ...value, + end_user_credential_type: type, + } as any) + } return ( <> @@ -323,6 +335,10 @@ const ToolSelector: FC = ({ }} credentialId={value?.credential_id} onAuthorizationItemClick={handleAuthorizationItemClick} + useEndUserCredentialEnabled={value?.use_end_user_credentials} + endUserCredentialType={value?.end_user_credential_type} + onEndUserCredentialChange={handleEndUserCredentialChange} + onEndUserCredentialTypeChange={handleEndUserCredentialTypeChange} /> diff --git a/web/types/app.ts b/web/types/app.ts index 73e11d396a..12b97b4e88 100644 --- a/web/types/app.ts +++ b/web/types/app.ts @@ -130,6 +130,8 @@ export type AgentTool = { isDeleted?: boolean notAuthor?: boolean credential_id?: string + use_end_user_credentials?: boolean + end_user_credential_type?: string } export type ToolItem = {