From 83ab69d2eb297bc1ac2211c44d3d8e6cfe07b7a2 Mon Sep 17 00:00:00 2001 From: zxhlyh Date: Fri, 11 Jul 2025 17:37:13 +0800 Subject: [PATCH] tool oauth --- .../base/form/components/base/base-field.tsx | 39 +++- web/app/components/base/form/types.ts | 7 +- .../authorize/add-oauth-button.tsx | 198 ++++++++++++++---- .../plugin-auth/authorize/api-key-modal.tsx | 4 +- .../authorize/oauth-client-settings.tsx | 72 +++---- .../plugin-auth/hooks/use-credential.ts | 7 - .../plugins/plugin-auth/hooks/use-get-api.ts | 4 +- web/i18n/en-US/plugin.ts | 3 + web/i18n/zh-Hans/plugin.ts | 3 + web/service/use-plugins-auth.ts | 15 +- 10 files changed, 250 insertions(+), 102 deletions(-) diff --git a/web/app/components/base/form/components/base/base-field.tsx b/web/app/components/base/form/components/base/base-field.tsx index 47a8cca419..70cd3a9f78 100644 --- a/web/app/components/base/form/components/base/base-field.tsx +++ b/web/app/components/base/form/components/base/base-field.tsx @@ -36,6 +36,8 @@ const BaseField = ({ required, placeholder, options, + labelClassName: formLabelClassName, + show_on = [], } = formSchema const memorizedLabel = useMemo(() => { @@ -64,10 +66,25 @@ const BaseField = ({ }) || [] }, [options, renderI18nObject]) const value = useStore(field.form.store, s => s.values[field.name]) + const values = useStore(field.form.store, (s) => { + return show_on.reduce((acc, condition) => { + acc[condition.variable] = s.values[condition.variable] + return acc + }, {} as Record) + }) + const show = useMemo(() => { + return show_on.every((condition) => { + const conditionValue = values[condition.variable] + return conditionValue === condition.value + }) + }, [values, show_on]) + + if (!show) + return null return (
-
+
{memorizedLabel} { required && ( @@ -132,6 +149,26 @@ const BaseField = ({ /> ) } + { + formSchema.type === FormTypeEnum.radio && ( +
+ { + memorizedOptions.map(option => ( +
field.handleChange(option.value)} + > + {option.label} +
+ )) + } +
+ ) + }
) diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index 3e1001b54e..9255181518 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -34,7 +34,7 @@ export enum FormTypeEnum { export type FormOption = { label: TypeWithI18N | string value: string - show_on: FormShowOnObject[] + show_on?: FormShowOnObject[] icon?: string } @@ -51,11 +51,12 @@ export type FormSchema = { help?: string | TypeWithI18N placeholder?: string | TypeWithI18N options?: FormOption[] + labelClassName?: string } export type FormValues = Record -export type FromRefObject = { +export type FormRefObject = { getForm: () => AnyFormApi } -export type FormRef = ForwardedRef +export type FormRef = ForwardedRef diff --git a/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx index d5666520b0..c2a252969e 100644 --- a/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/add-oauth-button.tsx @@ -1,18 +1,31 @@ import { memo, useCallback, + useMemo, useState, } from 'react' -import { RiEqualizer2Line } from '@remixicon/react' +import { useTranslation } from 'react-i18next' +import { + RiClipboardLine, + RiEqualizer2Line, + RiInformation2Fill, +} from '@remixicon/react' import Button from '@/app/components/base/button' 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 Badge from '@/app/components/base/badge' import { + useGetPluginOAuthClientSchemaHook, useGetPluginOAuthUrlHook, + useInvalidPluginCredentialInfoHook, } from '../hooks/use-credential' +import type { FormSchema } from '@/app/components/base/form/types' +import { FormTypeEnum } from '@/app/components/base/form/types' +import ActionButton from '@/app/components/base/action-button' +import { useRenderI18nObject } from '@/hooks/use-i18n' export type AddOAuthButtonProps = { pluginPayload: PluginPayload @@ -34,62 +47,173 @@ const AddOAuthButton = ({ dividerClassName, disabled, }: AddOAuthButtonProps) => { + const { t } = useTranslation() + const renderI18nObject = useRenderI18nObject() const [isOAuthSettingsOpen, setIsOAuthSettingsOpen] = useState(false) const { mutateAsync: getPluginOAuthUrl } = useGetPluginOAuthUrlHook(pluginPayload) + const { data } = useGetPluginOAuthClientSchemaHook(pluginPayload) + const { + schema = [], + is_oauth_custom_client_enabled, + is_system_oauth_params_exists, + client_params, + redirect_uri, + } = data || {} + const isConfigured = is_system_oauth_params_exists || !!client_params + const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) const handleOAuth = useCallback(async () => { const { authorization_url } = await getPluginOAuthUrl() if (authorization_url) { openOAuthPopup( authorization_url, - () => { - console.log('success') - }, + invalidatePluginCredentialInfo, ) } - }, [getPluginOAuthUrl]) + }, [getPluginOAuthUrl, invalidatePluginCredentialInfo]) + + const renderCustomLabel = useCallback((item: FormSchema) => { + return ( +
+
+
+ +
+
+
+ {t('plugin.auth.clientInfo')} +
+ { + redirect_uri && ( +
+ {redirect_uri} + { + navigator.clipboard.writeText(redirect_uri || '') + }} + > + + +
+ ) + } +
+
+
+ {renderI18nObject(item.label as Record)} +
+
+ ) + }, [t, redirect_uri, renderI18nObject]) + const memorizedSchemas = useMemo(() => { + const result: FormSchema[] = schema.map((item, index) => { + return { + ...item, + label: index === 0 ? renderCustomLabel(item) : item.label, + labelClassName: index === 0 ? 'h-auto' : undefined, + } + }) + if (is_system_oauth_params_exists) { + result.unshift({ + name: '__oauth_client__', + label: t('plugin.auth.oauthClient'), + type: FormTypeEnum.radio, + options: [ + { + label: t('plugin.auth.default'), + value: 'default', + }, + { + label: t('plugin.auth.custom'), + value: 'custom', + }, + ], + required: false, + default: is_oauth_custom_client_enabled ? 'custom' : 'default', + } as FormSchema) + result.forEach((item, index) => { + if (index > 0) { + item.show_on = [ + { + variable: '__oauth_client__', + value: 'custom', + }, + ] + if (client_params) + item.default = client_params[item.name] || item.default + } + }) + } + + return result + }, [schema, renderCustomLabel, t, is_system_oauth_params_exists, is_oauth_custom_client_enabled, client_params]) return ( <> - + { + isConfigured && ( + + ) + } + { + !isConfigured && ( + + ) + } { isOAuthSettingsOpen && ( setIsOAuthSettingsOpen(false)} disabled={disabled} + schemas={memorizedSchemas} + onAuth={handleOAuth} /> ) } diff --git a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx index 572dfe125e..c29e984ae4 100644 --- a/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/api-key-modal.tsx @@ -11,7 +11,7 @@ import Modal from '@/app/components/base/modal/modal' import { CredentialTypeEnum } from '../types' import { transformFormSchemasSecretInput } from '../utils' import AuthForm from '@/app/components/base/form/form-scenarios/auth' -import type { FromRefObject } from '@/app/components/base/form/types' +import type { FormRefObject } 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' @@ -62,7 +62,7 @@ const ApiKeyModal = ({ const { mutateAsync: addPluginCredential } = useAddPluginCredentialHook(pluginPayload) const { mutateAsync: updatePluginCredential } = useUpdatePluginCredentialHook(pluginPayload) const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) - const formRef = useRef(null) + const formRef = useRef(null) const handleConfirm = useCallback(async () => { const form = formRef.current?.getForm() const store = form?.store diff --git a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx index e7ea175817..8a844d0de8 100644 --- a/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx +++ b/web/app/components/plugins/plugin-auth/authorize/oauth-client-settings.tsx @@ -1,20 +1,20 @@ 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 type { + FormRefObject, + FormSchema, +} 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' @@ -24,36 +24,36 @@ type OAuthClientSettingsProps = { onClose?: () => void editValues?: Record disabled?: boolean + schemas: FormSchema[] + onAuth?: () => Promise } const OAuthClientSettings = ({ pluginPayload, onClose, editValues, disabled, + schemas, + onAuth, }: 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) => { + const defaultValues = schemas.reduce((acc, schema) => { if (schema.default) acc[schema.name] = schema.default return acc }, {} as Record) const { mutateAsync: setPluginOAuthCustomClient } = useSetPluginOAuthCustomClientHook(pluginPayload) const invalidatePluginCredentialInfo = useInvalidPluginCredentialInfoHook(pluginPayload) - const formRef = useRef(null) + const formRef = useRef(null) const handleConfirm = useCallback(async () => { const form = formRef.current?.getForm() const store = form?.store - const values = store?.state.values + const { + __oauth_client__, + ...values + } = store?.state.values const isPristineSecretInputNames: string[] = [] - formSchemas.forEach((schema) => { + schemas.forEach((schema) => { if (schema.type === FormTypeEnum.secretInput) { const fieldMeta = form?.getFieldMeta(schema.name) if (fieldMeta?.isPristine) @@ -74,36 +74,32 @@ const OAuthClientSettings = ({ onClose?.() invalidatePluginCredentialInfo() - }, [onClose, invalidatePluginCredentialInfo, setPluginOAuthCustomClient, notify, t, formSchemas]) + }, [onClose, invalidatePluginCredentialInfo, setPluginOAuthCustomClient, notify, t, schemas]) + + const handleConfirmAndAuthorize = useCallback(async () => { + await handleConfirm() + if (onAuth) + await onAuth() + }, [handleConfirm, onAuth]) return ( - { - isLoading && ( -
- -
- ) - } - { - !isLoading && !!data?.schema.length && ( - - ) - } +
) } diff --git a/web/app/components/plugins/plugin-auth/hooks/use-credential.ts b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts index 3b2080253c..de1ec8a80d 100644 --- a/web/app/components/plugins/plugin-auth/hooks/use-credential.ts +++ b/web/app/components/plugins/plugin-auth/hooks/use-credential.ts @@ -4,7 +4,6 @@ import { useGetPluginCredentialInfo, useGetPluginCredentialSchema, useGetPluginOAuthClientSchema, - useGetPluginOAuthCustomClientSchema, useGetPluginOAuthUrl, useInvalidPluginCredentialInfo, useSetPluginDefaultCredential, @@ -73,9 +72,3 @@ export const useSetPluginOAuthCustomClientHook = (pluginPayload: PluginPayload) return useSetPluginOAuthCustomClient(apiMap.setCustomOauthClient) } - -export const useGetPluginOAuthCustomClientSchemaHook = (pluginPayload: PluginPayload) => { - const apiMap = useGetApi(pluginPayload) - - return useGetPluginOAuthCustomClientSchema(apiMap.getCustomOAuthClient) -} diff --git a/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts b/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts index 8551332ddb..9b88cb0dad 100644 --- a/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts +++ b/web/app/components/plugins/plugin-auth/hooks/use-get-api.ts @@ -19,7 +19,7 @@ export const useGetApi = ({ category = AuthCategory.tool, provider }: PluginPayl getOauthUrl: `/oauth/plugin/${provider}/tool/authorization-url`, getOauthClientSchema: `/workspaces/current/tool-provider/builtin/${provider}/oauth/client-schema`, setCustomOauthClient: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, - getCustomOAuthClient: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, + getCustomOAuthClientValues: `/workspaces/current/tool-provider/builtin/${provider}/oauth/custom-client`, } } @@ -34,6 +34,6 @@ export const useGetApi = ({ category = AuthCategory.tool, provider }: PluginPayl getOauthUrl: '', getOauthClientSchema: '', setCustomOauthClient: '', - getCustomOAuthClient: '', + getCustomOAuthClientValues: '', } } diff --git a/web/i18n/en-US/plugin.ts b/web/i18n/en-US/plugin.ts index 6d59810e94..2984f5f77c 100644 --- a/web/i18n/en-US/plugin.ts +++ b/web/i18n/en-US/plugin.ts @@ -216,6 +216,7 @@ const translation = { difyVersionNotCompatible: 'The current Dify version is not compatible with this plugin, please upgrade to the minimum version required: {{minimalDifyVersion}}', auth: { default: 'Default', + custom: 'Custom', setDefault: 'Set as default', useOAuth: 'Use OAuth', useOAuthAuth: 'Use OAuth Authorization', @@ -233,6 +234,8 @@ const translation = { authorizationName: 'Authorization Name', workspaceDefault: 'Workspace Default', authRemoved: 'Auth removed', + clientInfo: 'As no system client secrets found for this tool provider, setup it manually is required, for redirect_uri, please use', + oauthClient: 'OAuth Client', }, } diff --git a/web/i18n/zh-Hans/plugin.ts b/web/i18n/zh-Hans/plugin.ts index 039b798fa4..5f8f641b72 100644 --- a/web/i18n/zh-Hans/plugin.ts +++ b/web/i18n/zh-Hans/plugin.ts @@ -216,6 +216,7 @@ const translation = { difyVersionNotCompatible: '当前 Dify 版本不兼容该插件,其最低版本要求为 {{minimalDifyVersion}}', auth: { default: '默认', + custom: '自定义', setDefault: '设为默认', useOAuth: '使用 OAuth', useOAuthAuth: '使用 OAuth 授权', @@ -233,6 +234,8 @@ const translation = { authorizationName: '凭据名称', workspaceDefault: '工作区默认', authRemoved: '凭据已移除', + clientInfo: '由于未找到此工具提供者的系统客户端密钥,因此需要手动设置,对于 redirect_uri,请使用', + oauthClient: 'OAuth 客户端', }, } diff --git a/web/service/use-plugins-auth.ts b/web/service/use-plugins-auth.ts index 7d65f8b884..05364ad688 100644 --- a/web/service/use-plugins-auth.ts +++ b/web/service/use-plugins-auth.ts @@ -123,6 +123,9 @@ export const useGetPluginOAuthClientSchema = ( queryFn: () => get<{ schema: FormSchema[] is_oauth_custom_client_enabled: boolean + is_system_oauth_params_exists?: boolean + client_params?: Record + redirect_uri?: string }>(url), }) } @@ -139,15 +142,3 @@ export const useSetPluginOAuthCustomClient = ( }, }) } - -export const useGetPluginOAuthCustomClientSchema = ( - url: string, -) => { - return useQuery({ - queryKey: [NAME_SPACE, 'oauth-custom-client-schema', url], - queryFn: () => get<{ - client_id: string - client_secret: string - }>(url), - }) -}