diff --git a/web/app/components/base/form/types.ts b/web/app/components/base/form/types.ts index d18c166186..3351ad053c 100644 --- a/web/app/components/base/form/types.ts +++ b/web/app/components/base/form/types.ts @@ -6,6 +6,7 @@ import type { AnyFormApi, FieldValidators, } from '@tanstack/react-form' +import type { Locale } from '@/i18n-config' export type TypeWithI18N = { en_US: T @@ -36,7 +37,7 @@ export enum FormTypeEnum { } export type FormOption = { - label: TypeWithI18N | string + label: string | TypeWithI18N | Record value: string show_on?: FormShowOnObject[] icon?: string @@ -47,15 +48,15 @@ export type AnyValidators = FieldValidators required: boolean default?: any - tooltip?: string | TypeWithI18N + tooltip?: string | TypeWithI18N | Record show_on?: FormShowOnObject[] url?: string scope?: string - help?: string | TypeWithI18N - placeholder?: string | TypeWithI18N + help?: string | TypeWithI18N | Record + placeholder?: string | TypeWithI18N | Record options?: FormOption[] labelClassName?: string validators?: AnyValidators diff --git a/web/app/components/plugins/plugin-detail-panel/index.tsx b/web/app/components/plugins/plugin-detail-panel/index.tsx index 3ec867faae..956bb6bde8 100644 --- a/web/app/components/plugins/plugin-detail-panel/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/index.tsx @@ -6,8 +6,10 @@ import EndpointList from './endpoint-list' import ActionList from './action-list' import ModelList from './model-list' import AgentStrategyList from './agent-strategy-list' +import { SubscriptionList } from './subscription-list' +import { TriggerEventsList } from './trigger-events-list' import Drawer from '@/app/components/base/drawer' -import type { PluginDetail } from '@/app/components/plugins/types' +import { type PluginDetail, PluginType } from '@/app/components/plugins/types' import cn from '@/utils/classnames' type Props = { @@ -48,6 +50,12 @@ const PluginDetailPanel: FC = ({ onUpdate={handleUpdate} />
+ {detail.declaration.category === PluginType.trigger && ( + <> + + + + )} {!!detail.declaration.tool && } {!!detail.declaration.agent_strategy && } {!!detail.declaration.endpoint && } diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/add-type-dropdown.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/add-type-dropdown.tsx new file mode 100644 index 0000000000..a6bc7249d5 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/add-type-dropdown.tsx @@ -0,0 +1,148 @@ +'use client' +import React, { useEffect, useRef } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiQuestionLine, + RiSettings4Line, +} from '@remixicon/react' +import cn from '@/utils/classnames' + +type SubscriptionAddType = 'api-key' | 'oauth' | 'manual' + +type Props = { + onSelect: (type: SubscriptionAddType) => void + onClose: () => void + position?: 'bottom' | 'right' +} + +const AddTypeDropdown = ({ onSelect, onClose, position = 'bottom' }: Props) => { + const { t } = useTranslation() + const dropdownRef = useRef(null) + + useEffect(() => { + const handleClickOutside = (event: MouseEvent) => { + if (dropdownRef.current && !dropdownRef.current.contains(event.target as Node)) + onClose() + } + + document.addEventListener('mousedown', handleClickOutside) + return () => document.removeEventListener('mousedown', handleClickOutside) + }, [onClose]) + + const options = [ + { + key: 'oauth' as const, + title: t('pluginTrigger.subscription.addType.options.oauth.title'), + rightIcon: RiSettings4Line, + hasRightIcon: true, + }, + { + key: 'api-key' as const, + title: t('pluginTrigger.subscription.addType.options.apiKey.title'), + hasRightIcon: false, + }, + { + key: 'manual' as const, + title: t('pluginTrigger.subscription.addType.options.manual.description'), // 使用 description 作为标题 + rightIcon: RiQuestionLine, + hasRightIcon: true, + tooltip: t('pluginTrigger.subscription.addType.options.manual.tip'), + }, + ] + + const handleOptionClick = (type: SubscriptionAddType) => { + onSelect(type) + } + + return ( +
+ {/* Context Menu Content */} +
+ {/* First Group - OAuth & API Key */} +
+ {options.slice(0, 2).map((option, index) => { + const RightIconComponent = option.rightIcon + return ( + + ) + })} +
+ + {/* Separator */} +
+ + {/* Second Group - Manual */} +
+ {options.slice(2).map((option) => { + const RightIconComponent = option.rightIcon + return ( + + ) + })} +
+
+ + {/* Border overlay */} +
+
+ ) +} + +export default AddTypeDropdown diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/api-key-add-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/api-key-add-modal.tsx new file mode 100644 index 0000000000..d55c8399c8 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/api-key-add-modal.tsx @@ -0,0 +1,314 @@ +'use client' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiArrowLeftLine, + RiArrowRightLine, + RiCloseLine, +} from '@remixicon/react' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import Toast from '@/app/components/base/toast' +import Form from '@/app/components/base/form/form-scenarios/auth' +import type { FormRefObject } from '@/app/components/base/form/types' +import { + useBuildTriggerSubscription, + useCreateTriggerSubscriptionBuilder, + useVerifyTriggerSubscriptionBuilder, +} from '@/service/use-triggers' +import type { PluginDetail } from '@/app/components/plugins/types' +import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types' + +type Props = { + pluginDetail: PluginDetail + onClose: () => void + onSuccess: () => void +} + +enum ApiKeyStep { + Verify = 'verify', + Configuration = 'configuration', +} + +const ApiKeyAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => { + const { t } = useTranslation() + + // State + const [currentStep, setCurrentStep] = useState(ApiKeyStep.Verify) + const [subscriptionName, setSubscriptionName] = useState('') + const [subscriptionBuilder, setSubscriptionBuilder] = useState(null) + const [verificationError, setVerificationError] = useState('') + + // Form refs + const credentialsFormRef = React.useRef(null) + const parametersFormRef = React.useRef(null) + + // API mutations + const { mutate: createBuilder, isPending: isCreatingBuilder } = useCreateTriggerSubscriptionBuilder() + const { mutate: verifyBuilder, isPending: isVerifying } = useVerifyTriggerSubscriptionBuilder() + const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription() + + // Get provider name and schemas + const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}` + const credentialsSchema = pluginDetail.declaration.trigger?.credentials_schema || [] + const parametersSchema = pluginDetail.declaration.trigger?.subscription_schema?.parameters_schema || [] + + const handleVerify = () => { + const credentialsFormValues = credentialsFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false } + const credentials = credentialsFormValues.values + + if (!Object.keys(credentials).length) { + Toast.notify({ + type: 'error', + message: 'Please fill in all required credentials', + }) + return + } + + setVerificationError('') + + // First create builder + createBuilder( + { + provider: providerName, + credential_type: TriggerCredentialTypeEnum.ApiKey, + }, + { + onSuccess: (response) => { + const builder = response.subscription_builder + setSubscriptionBuilder(builder) + + // setCurrentStep('configuration') + + verifyBuilder( + { + provider: providerName, + subscriptionBuilderId: builder.id, + credentials, + }, + { + onSuccess: () => { + Toast.notify({ + type: 'success', + message: t('pluginTrigger.modal.apiKey.verify.success'), + }) + setCurrentStep(ApiKeyStep.Configuration) + }, + onError: (error: any) => { + setVerificationError(error?.message || t('pluginTrigger.modal.apiKey.verify.error')) + }, + }, + ) + }, + onError: (error: any) => { + Toast.notify({ + type: 'error', + message: error?.message || t('pluginTrigger.modal.errors.verifyFailed'), + }) + }, + }, + ) + } + + const handleCreate = () => { + if (!subscriptionName.trim()) { + Toast.notify({ + type: 'error', + message: t('pluginTrigger.modal.form.subscriptionName.required'), + }) + return + } + + if (!subscriptionBuilder) + return + + buildSubscription( + { + provider: providerName, + subscriptionBuilderId: subscriptionBuilder.id, + }, + { + onSuccess: () => { + Toast.notify({ + type: 'success', + message: 'Subscription created successfully', + }) + onSuccess() + onClose() + }, + onError: (error: any) => { + Toast.notify({ + type: 'error', + message: error?.message || t('modal.errors.createFailed'), + }) + }, + }, + ) + } + + const handleBack = () => { + setCurrentStep(ApiKeyStep.Verify) + } + + return ( + +
+
+ {currentStep === ApiKeyStep.Configuration && ( + + )} +

+ {t('pluginTrigger.modal.apiKey.title')} +

+
+ +
+ + {/* Step indicator */} +
+
+
+
+ 1 +
+ {t('pluginTrigger.modal.steps.verify')} +
+ +
+ +
+
+ 2 +
+ {t('pluginTrigger.modal.steps.configuration')} +
+
+
+ +
+ {currentStep === ApiKeyStep.Verify ? ( + // Step 1: Verify Credentials +
+ + {credentialsSchema.length > 0 && ( +
+
+
+ )} + + {verificationError && ( +
+
+ {verificationError} +
+
+ )} +
+ ) : ( + // Step 2: Configuration +
+ {/*
+

+ {t('pluginTrigger.modal.apiKey.configuration.title')} +

+

+ {t('pluginTrigger.modal.apiKey.configuration.description')} +

+
*/} + + {/* Subscription Name */} +
+ + setSubscriptionName(e.target.value)} + placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')} + /> +
+ + {/* Callback URL (read-only) */} + {subscriptionBuilder?.endpoint && ( +
+ + +
+ {t('pluginTrigger.modal.form.callbackUrl.description')} +
+
+ )} + + {/* Dynamic Parameters Form */} + {parametersSchema.length > 0 && ( +
+
+ Subscription Parameters +
+ +
+ )} +
+ )} +
+ + {/* Footer */} +
+ + + {currentStep === ApiKeyStep.Verify ? ( + + ) : ( + + )} +
+
+ ) +} + +export default ApiKeyAddModal diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx new file mode 100644 index 0000000000..ff95d6cd08 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/index.tsx @@ -0,0 +1,165 @@ +'use client' +import React from 'react' +import { useTranslation } from 'react-i18next' +import { useBoolean } from 'ahooks' +import { + RiAddLine, + RiBookOpenLine, + RiWebhookLine, +} from '@remixicon/react' +import { useDocLink } from '@/context/i18n' +import SubscriptionCard from './subscription-card' +import SubscriptionAddModal from './subscription-add-modal' +import AddTypeDropdown from './add-type-dropdown' +import ActionButton from '@/app/components/base/action-button' +import Button from '@/app/components/base/button' +import Tooltip from '@/app/components/base/tooltip' +import { useTriggerSubscriptions } from '@/service/use-triggers' +import type { PluginDetail } from '@/app/components/plugins/types' +import cn from '@/utils/classnames' + +type Props = { + detail: PluginDetail +} + +type SubscriptionAddType = 'api-key' | 'oauth' | 'manual' + +export const SubscriptionList = ({ detail }: Props) => { + const { t } = useTranslation() + const docLink = useDocLink() + const showTopBorder = detail.declaration.tool || detail.declaration.endpoint + + // Fetch subscriptions + const { data: subscriptions, isLoading } = useTriggerSubscriptions(`${detail.plugin_id}/${detail.declaration.name}`) + + // Modal states + const [isShowAddModal, { + setTrue: showAddModal, + setFalse: hideAddModal, + }] = useBoolean(false) + + const [selectedAddType, setSelectedAddType] = React.useState(null) + + // Dropdown state for add button + const [isShowAddDropdown, { + setTrue: showAddDropdown, + setFalse: hideAddDropdown, + }] = useBoolean(false) + + const handleAddTypeSelect = (type: SubscriptionAddType) => { + setSelectedAddType(type) + hideAddDropdown() + showAddModal() + } + + const handleModalClose = () => { + hideAddModal() + setSelectedAddType(null) + } + + const handleRefreshList = () => { + // This will be called after successful operations + // The query will auto-refresh due to React Query + } + + if (isLoading) { + return ( +
+
+
{t('common.dataLoading')}
+
+
+ ) + } + + const hasSubscriptions = subscriptions && subscriptions.length > 0 + + return ( +
+ {!hasSubscriptions ? ( +
+ + {isShowAddDropdown && ( + + )} +
+ ) : ( + // List state with header and secondary add button + <> +
+
+ {t('pluginTrigger.subscription.list.title')} + +
+ +
+
+ {t('pluginTrigger.subscription.list.tooltip')} +
+ +
+ + {t('pluginTrigger.subscription.list.tooltip.viewDocument')} +
+
+
+ } + /> +
+
+ + + + {isShowAddDropdown && ( + + )} +
+
+ +
+ {subscriptions?.map(subscription => ( + + ))} +
+ + )} + + {isShowAddModal && selectedAddType && ( + + )} +
+ ) +} diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/manual-add-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/manual-add-modal.tsx new file mode 100644 index 0000000000..e7fb9592d4 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/manual-add-modal.tsx @@ -0,0 +1,480 @@ +'use client' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiArrowDownSLine, + RiArrowRightSLine, + RiCheckboxCircleLine, + RiCloseLine, + RiErrorWarningLine, + RiFileCopyLine, +} from '@remixicon/react' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import Toast from '@/app/components/base/toast' +import { + useBuildTriggerSubscription, + useCreateTriggerSubscriptionBuilder, + // useTriggerSubscriptionBuilderLogs, +} from '@/service/use-triggers' +import type { PluginDetail } from '@/app/components/plugins/types' +import cn from '@/utils/classnames' +import type { TriggerSubscriptionBuilder } from '@/app/components/workflow/block-selector/types' +import { TriggerCredentialTypeEnum } from '@/app/components/workflow/block-selector/types' +// import { BaseForm } from '@/app/components/base/form/components/base' +// import type { FormRefObject } from '@/app/components/base/form/types' +import ActionButton from '@/app/components/base/action-button' +import { CopyFeedbackNew } from '@/app/components/base/copy-feedback' + +type Props = { + pluginDetail: PluginDetail + onClose: () => void + onSuccess: () => void +} + +// type LogEntry = { +// timestamp: string +// method: string +// path: string +// status: number +// headers: Record +// body: any +// response: any +// } + +const ManualAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => { + const { t } = useTranslation() + + // Form state + const [subscriptionName, setSubscriptionName] = useState('') + const [subscriptionBuilder, setSubscriptionBuilder] = useState() + const [expandedLogs, setExpandedLogs] = useState>(new Set()) + // const formRef = React.useRef(null) + + // API mutations + const { mutate: createBuilder /* isPending: isCreatingBuilder */ } = useCreateTriggerSubscriptionBuilder() + const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription() + + // Get provider name + const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}` + + // const { data: logs, isLoading: isLoadingLogs } = useTriggerSubscriptionBuilderLogs( + // providerName, + // subscriptionBuilder?.id || '', + // { + // enabled: !!subscriptionBuilder?.id, + // refetchInterval: 3000, // Poll every 3 seconds + // }, + // ) + + // Mock data for demonstration + const mockLogs = [ + { + id: '1', + timestamp: '2024-01-15T18:09:14Z', + method: 'POST', + path: '/webhook', + status: 500, + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Slack-Hooks/1.0', + 'X-Slack-Signature': 'v0=a2114d57b48eac39b9ad189dd8316235a7b4a8d21a10bd27519666489c69b503', + }, + body: { + verification_token: 'secret_tMrlL1qK5vuQAhCh', + event: { + type: 'message', + text: 'Hello world', + user: 'U1234567890', + }, + }, + response: { + error: 'Internal server error', + message: 'Failed to process webhook', + }, + }, + { + id: '2', + timestamp: '2024-01-15T18:09:14Z', + method: 'POST', + path: '/webhook', + status: 200, + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Slack-Hooks/1.0', + }, + body: { + verification_token: 'secret_tMrlL1qK5vuQAhCh', + }, + response: { + success: true, + }, + }, + { + id: '3', + timestamp: '2024-01-15T18:09:14Z', + method: 'POST', + path: '/webhook', + status: 200, + headers: { + 'Content-Type': 'application/json', + 'User-Agent': 'Slack-Hooks/1.0', + }, + body: { + verification_token: 'secret_tMrlL1qK5vuQAhCh', + }, + response: { + output: { + output: 'I am the GPT-3 model from OpenAI, an artificial intelligence assistant.', + }, + raw_output: 'I am the GPT-3 model from OpenAI, an artificial intelligence assistant.', + }, + }, + ] + + const logs = mockLogs + const isLoadingLogs = false + + // Create subscription builder on mount + useEffect(() => { + if (!subscriptionBuilder) { + createBuilder( + { + provider: providerName, + credential_type: TriggerCredentialTypeEnum.Unauthorized, + }, + { + onSuccess: (response) => { + const builder = response.subscription_builder + setSubscriptionBuilder(builder) + }, + onError: (error) => { + Toast.notify({ + type: 'error', + message: t('pluginTrigger.modal.errors.createFailed'), + }) + console.error('Failed to create subscription builder:', error) + }, + }, + ) + } + }, [createBuilder, providerName, subscriptionBuilder, t]) + + const handleCreate = () => { + if (!subscriptionName.trim()) { + Toast.notify({ + type: 'error', + message: t('pluginTrigger.modal.form.subscriptionName.required'), + }) + return + } + + if (!subscriptionBuilder) + return + + // Get form values using the ref (for future use if needed) + // const formValues = formRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false } + + buildSubscription( + { + provider: providerName, + subscriptionBuilderId: subscriptionBuilder.id, + }, + { + onSuccess: () => { + Toast.notify({ + type: 'success', + message: 'Subscription created successfully', + }) + onSuccess() + onClose() + }, + onError: (error: any) => { + Toast.notify({ + type: 'error', + message: error?.message || t('pluginTrigger.modal.errors.createFailed'), + }) + }, + }, + ) + } + + const toggleLogExpansion = (logId: string) => { + const newExpanded = new Set(expandedLogs) + if (newExpanded.has(logId)) + newExpanded.delete(logId) + else + newExpanded.add(logId) + + setExpandedLogs(newExpanded) + } + + return ( + +
+

+ {t('pluginTrigger.modal.manual.title')} +

+ + + +
+ +
+ {/* Subscription Name */} +
+ + setSubscriptionName(e.target.value)} + placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')} + /> +
+ + {/* Callback URL */} +
+ +
+ + +
+
+ {t('pluginTrigger.modal.form.callbackUrl.description')} +
+
+ + {/* Dynamic Parameters Form */} + {/* {parametersSchema.length > 0 && ( +
+
+ Subscription Parameters +
+ +
+ )} */} + + {/* Request Logs */} + {subscriptionBuilder && ( +
+ {/* Divider with Title */} +
+
+ REQUESTS HISTORY +
+
+
+ + {/* Request List */} +
+ {isLoadingLogs && ( +
+
+ + + + + + +
+
+ Awaiting request from Slack... +
+
+ )} + + {!isLoadingLogs && logs && logs.length > 0 && ( + <> + {logs.map((log, index) => { + const logId = log.id || index.toString() + const isExpanded = expandedLogs.has(logId) + const isSuccess = log.status >= 200 && log.status < 300 + const isError = log.status >= 400 + + return ( +
+ {/* Error background decoration */} + {isError && ( +
+
+
+ )} + + {/* Request Header */} + + + {/* Expanded Content */} + {isExpanded && ( +
+ {/* Request Block */} +
+
+
+ REQUEST +
+ +
+
+
+
+ {JSON.stringify(log.body, null, 2).split('\n').map((_, i) => ( +
{String(i + 1).padStart(2, '0')}
+ ))} +
+
+
+
+                                    {JSON.stringify(log.body, null, 2)}
+                                  
+
+
+
+ + {/* Response Block */} +
+
+
+ RESPONSE +
+ +
+
+
+
+ {JSON.stringify(log.response, null, 2).split('\n').map((_, i) => ( +
{String(i + 1).padStart(2, '0')}
+ ))} +
+
+
+
+                                    {JSON.stringify(log.response, null, 2)}
+                                  
+
+
+
+
+ )} +
+ ) + })} + + )} + + {!isLoadingLogs && (!logs || logs.length === 0) && ( +
+
+ + + + + + +
+
+ Awaiting request from Slack... +
+
+ )} +
+
+ )} +
+ + {/* Footer */} +
+ + +
+ + ) +} + +export default ManualAddModal diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/oauth-add-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/oauth-add-modal.tsx new file mode 100644 index 0000000000..2ee068dcd1 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/oauth-add-modal.tsx @@ -0,0 +1,373 @@ +'use client' +import React, { useEffect, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCloseLine, + RiExternalLinkLine, +} from '@remixicon/react' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import Toast from '@/app/components/base/toast' +import Form from '@/app/components/base/form/form-scenarios/auth' +import type { FormRefObject } from '@/app/components/base/form/types' +import { + useBuildTriggerSubscription, + useConfigureTriggerOAuth, + useInitiateTriggerOAuth, + useVerifyTriggerSubscriptionBuilder, +} from '@/service/use-triggers' +import type { PluginDetail } from '@/app/components/plugins/types' +import { CopyFeedbackNew } from '@/app/components/base/copy-feedback' + +type Props = { + pluginDetail: PluginDetail + onClose: () => void + onSuccess: () => void +} + +type OAuthStep = 'setup' | 'authorize' | 'configuration' + +const OAuthAddModal = ({ pluginDetail, onClose, onSuccess }: Props) => { + const { t } = useTranslation() + + // State + const [currentStep, setCurrentStep] = useState('setup') + const [subscriptionName, setSubscriptionName] = useState('') + const [authorizationUrl, setAuthorizationUrl] = useState('') + const [subscriptionBuilder, setSubscriptionBuilder] = useState(null) + const [redirectUrl, setRedirectUrl] = useState('') + const [authorizationStatus, setAuthorizationStatus] = useState<'pending' | 'success' | 'failed'>('pending') + + // Form refs + const clientFormRef = React.useRef(null) + const parametersFormRef = React.useRef(null) + + // API mutations + const { mutate: initiateOAuth, isPending: isInitiating } = useInitiateTriggerOAuth() + const { mutate: configureOAuth, isPending: isConfiguring } = useConfigureTriggerOAuth() + const { mutate: verifyBuilder } = useVerifyTriggerSubscriptionBuilder() + const { mutate: buildSubscription, isPending: isBuilding } = useBuildTriggerSubscription() + + // Get provider name and schemas + const providerName = `${pluginDetail.plugin_id}/${pluginDetail.declaration.name}` + const clientSchema = pluginDetail.declaration.trigger?.oauth_schema?.client_schema || [] + const parametersSchema = pluginDetail.declaration.trigger?.subscription_schema?.parameters_schema || [] + + // Poll for authorization status + useEffect(() => { + if (currentStep === 'authorize' && subscriptionBuilder && authorizationStatus === 'pending') { + const pollInterval = setInterval(() => { + verifyBuilder( + { + provider: providerName, + subscriptionBuilderId: subscriptionBuilder.id, + }, + { + onSuccess: () => { + setAuthorizationStatus('success') + setCurrentStep('configuration') + Toast.notify({ + type: 'success', + message: t('pluginTrigger.modal.oauth.authorization.authSuccess'), + }) + }, + onError: () => { + // Continue polling - auth might still be in progress + }, + }, + ) + }, 3000) + + return () => clearInterval(pollInterval) + } + }, [currentStep, subscriptionBuilder, authorizationStatus, verifyBuilder, providerName, t]) + + const handleSetupOAuth = () => { + const clientFormValues = clientFormRef.current?.getFormValues({}) || { values: {}, isCheckValidated: false } + const clientParams = clientFormValues.values + + if (!Object.keys(clientParams).length) { + Toast.notify({ + type: 'error', + message: t('pluginTrigger.modal.oauth.authorization.authFailed'), + }) + return + } + + // First configure OAuth client + configureOAuth( + { + provider: providerName, + client_params: clientParams as any, + enabled: true, + }, + { + onSuccess: () => { + // Then get redirect URL and initiate OAuth + const baseUrl = window.location.origin + const redirectPath = `/plugins/oauth/callback/${providerName}` + const fullRedirectUrl = `${baseUrl}${redirectPath}` + setRedirectUrl(fullRedirectUrl) + + // Initiate OAuth flow + initiateOAuth(providerName, { + onSuccess: (response) => { + setAuthorizationUrl(response.authorization_url) + setSubscriptionBuilder(response.subscription_builder) + setCurrentStep('authorize') + }, + onError: (error: any) => { + Toast.notify({ + type: 'error', + message: error?.message || t('pluginTrigger.modal.errors.authFailed'), + }) + }, + }) + }, + onError: (error: any) => { + Toast.notify({ + type: 'error', + message: error?.message || t('pluginTrigger.modal.errors.authFailed'), + }) + }, + }, + ) + } + + const handleAuthorize = () => { + if (authorizationUrl) { + // Open authorization URL in new window + window.open(authorizationUrl, '_blank', 'width=500,height=600') + } + } + + const handleCreate = () => { + if (!subscriptionName.trim()) { + Toast.notify({ + type: 'error', + message: t('pluginTrigger.modal.form.subscriptionName.required'), + }) + return + } + + if (!subscriptionBuilder) + return + + buildSubscription( + { + provider: providerName, + subscriptionBuilderId: subscriptionBuilder.id, + }, + { + onSuccess: () => { + Toast.notify({ + type: 'success', + message: t('pluginTrigger.modal.oauth.configuration.success'), + }) + onSuccess() + onClose() + }, + onError: (error: any) => { + Toast.notify({ + type: 'error', + message: error?.message || t('pluginTrigger.modal.errors.createFailed'), + }) + }, + }, + ) + } + + return ( + +
+

+ {t('pluginTrigger.modal.oauth.title')} +

+ +
+ +
+ {currentStep === 'setup' && ( +
+
+

+ {t('pluginTrigger.modal.oauth.authorization.title')} +

+

+ {t('pluginTrigger.modal.oauth.authorization.description')} +

+
+ + {clientSchema.length > 0 && ( +
+ +
+ )} +
+ )} + + {currentStep === 'authorize' && ( +
+
+

+ {t('pluginTrigger.modal.oauth.authorization.title')} +

+

+ {t('pluginTrigger.modal.oauth.authorization.description')} +

+
+ + {/* Redirect URL */} + {redirectUrl && ( +
+ +
+ + +
+
+ {t('pluginTrigger.modal.oauth.authorization.redirectUrlHelp')} +
+
+ )} + + {/* Authorization Status */} +
+ {authorizationStatus === 'pending' && ( +
+ {t('pluginTrigger.modal.oauth.authorization.waitingAuth')} +
+ )} + {authorizationStatus === 'success' && ( +
+ {t('pluginTrigger.modal.oauth.authorization.authSuccess')} +
+ )} + {authorizationStatus === 'failed' && ( +
+ {t('pluginTrigger.modal.oauth.authorization.authFailed')} +
+ )} +
+ + {/* Authorize Button */} + {authorizationStatus === 'pending' && ( + + )} +
+ )} + + {currentStep === 'configuration' && ( +
+
+

+ {t('pluginTrigger.modal.oauth.configuration.title')} +

+

+ {t('pluginTrigger.modal.oauth.configuration.description')} +

+
+ + {/* Subscription Name */} +
+ + setSubscriptionName(e.target.value)} + placeholder={t('pluginTrigger.modal.form.subscriptionName.placeholder')} + /> +
+ + {/* Callback URL (read-only) */} + {subscriptionBuilder?.endpoint && ( +
+ + +
+ {t('pluginTrigger.modal.form.callbackUrl.description')} +
+
+ )} + + {/* Dynamic Parameters Form */} + {parametersSchema.length > 0 && ( +
+ +
+ )} +
+ )} +
+ + {/* Footer */} +
+ + + {currentStep === 'setup' && ( + + )} + + {currentStep === 'configuration' && ( + + )} +
+
+ ) +} + +export default OAuthAddModal diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-add-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-add-modal.tsx new file mode 100644 index 0000000000..896642afa5 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-add-modal.tsx @@ -0,0 +1,56 @@ +'use client' +import React from 'react' +// import { useTranslation } from 'react-i18next' +// import Modal from '@/app/components/base/modal' +import ManualAddModal from './manual-add-modal' +import ApiKeyAddModal from './api-key-add-modal' +import OAuthAddModal from './oauth-add-modal' +import type { PluginDetail } from '@/app/components/plugins/types' + +type SubscriptionAddType = 'api-key' | 'oauth' | 'manual' + +type Props = { + type: SubscriptionAddType + pluginDetail: PluginDetail + onClose: () => void + onSuccess: () => void +} + +const SubscriptionAddModal = ({ type, pluginDetail, onClose, onSuccess }: Props) => { + // const { t } = useTranslation() + + const renderModalContent = () => { + switch (type) { + case 'manual': + return ( + + ) + case 'api-key': + return ( + + ) + case 'oauth': + return ( + + ) + default: + return null + } + } + + return renderModalContent() +} + +export default SubscriptionAddModal diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx new file mode 100644 index 0000000000..f5e670a8d7 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-card.tsx @@ -0,0 +1,163 @@ +'use client' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { useBoolean } from 'ahooks' +import { + RiDeleteBinLine, + RiGitBranchLine, + RiKeyLine, + RiUserLine, + RiWebhookLine, +} from '@remixicon/react' +import ActionButton from '@/app/components/base/action-button' +import Confirm from '@/app/components/base/confirm' +import Toast from '@/app/components/base/toast' +import { useDeleteTriggerSubscription } from '@/service/use-triggers' +import type { TriggerSubscription } from '@/app/components/workflow/block-selector/types' +import cn from '@/utils/classnames' + +type Props = { + data: TriggerSubscription + onRefresh: () => void +} + +const getProviderIcon = (provider: string) => { + switch (provider) { + case 'github': + return + case 'gitlab': + return + default: + return + } +} + +const getCredentialIcon = (credentialType: string) => { + switch (credentialType) { + case 'oauth2': + return + case 'api_key': + return + case 'unauthorized': + return + default: + return + } +} + +const SubscriptionCard = ({ data, onRefresh }: Props) => { + const { t } = useTranslation() + const [isHovered, setIsHovered] = useState(false) + const [isShowDeleteModal, { + setTrue: showDeleteModal, + setFalse: hideDeleteModal, + }] = useBoolean(false) + + // API mutations + const { mutate: deleteSubscription, isPending: isDeleting } = useDeleteTriggerSubscription() + + const handleDelete = () => { + deleteSubscription(data.id, { + onSuccess: () => { + Toast.notify({ + type: 'success', + message: t('pluginTrigger.subscription.list.item.actions.deleteConfirm.title'), + }) + onRefresh() + hideDeleteModal() + }, + onError: (error: any) => { + Toast.notify({ + type: 'error', + message: error?.message || 'Failed to delete subscription', + }) + hideDeleteModal() + }, + }) + } + + // Determine if subscription is active/enabled based on properties + const isActive = data.properties?.active !== false + + return ( + <> +
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
+
+ {getProviderIcon(data.provider)} +
+
+
+ + {data.name} + + {getCredentialIcon(data.credential_type)} +
+
+ {data.provider} + + + {isActive + ? t('pluginTrigger.subscription.list.item.status.active') + : t('pluginTrigger.subscription.list.item.status.inactive') + } + +
+
+
+ + {/* Delete button - only show on hover */} +
+ + + +
+
+ + {isShowDeleteModal && ( + + )} + + ) +} + +export default SubscriptionCard diff --git a/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-modal.tsx b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-modal.tsx new file mode 100644 index 0000000000..4069016c85 --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/subscription-list/subscription-modal.tsx @@ -0,0 +1,237 @@ +'use client' +import React, { useState } from 'react' +import { useTranslation } from 'react-i18next' +import { + RiCloseLine, + RiEditLine, + RiKeyLine, + RiUserLine, +} from '@remixicon/react' +import Modal from '@/app/components/base/modal' +import Button from '@/app/components/base/button' +import Input from '@/app/components/base/input' +import type { PluginDetail } from '@/app/components/plugins/types' + +type Props = { + pluginDetail: PluginDetail + onCancel: () => void + onSaved: (data: any) => void +} + +type CreateMode = 'api-key' | 'oauth' | 'manual' + +const SubscriptionModal = ({ pluginDetail, onCancel, onSaved }: Props) => { + const { t } = useTranslation() + const [selectedMode, setSelectedMode] = useState(null) + const [subscriptionName, setSubscriptionName] = useState('') + const [apiKey, setApiKey] = useState('') + const [webhookUrl, setWebhookUrl] = useState('') + const [isLoading, setIsLoading] = useState(false) + + const handleModeSelect = (mode: CreateMode) => { + setSelectedMode(mode) + } + + const handleBack = () => { + setSelectedMode(null) + } + + const handleCreate = async () => { + if (!selectedMode || !subscriptionName.trim()) return + + setIsLoading(true) + try { + const subscriptionData = { + name: subscriptionName, + mode: selectedMode, + plugin_id: pluginDetail.plugin_id, + ...(selectedMode === 'api-key' && { api_key: apiKey }), + ...(selectedMode === 'manual' && { webhook_url: webhookUrl }), + } + + onSaved(subscriptionData) + } + finally { + setIsLoading(false) + } + } + + const canCreate = subscriptionName.trim() && ( + selectedMode === 'oauth' + || (selectedMode === 'api-key' && apiKey.trim()) + || (selectedMode === 'manual' && webhookUrl.trim()) + ) + + if (!selectedMode) { + return ( + +
+

+ {t('plugin.detailPanel.createSubscription')} +

+ +
+ +
+

+ {t('plugin.detailPanel.createSubscriptionDesc')} +

+
+ +
+
+ + + + + +
+
+
+ ) + } + + return ( + +
+
+ +

+ {selectedMode === 'api-key' && t('plugin.detailPanel.createViaApiKey')} + {selectedMode === 'oauth' && t('plugin.detailPanel.createViaOAuth')} + {selectedMode === 'manual' && t('plugin.detailPanel.createManual')} +

+
+ +
+ +
+
+
+ + setSubscriptionName(e.target.value)} + placeholder={t('plugin.detailPanel.subscriptionNamePlaceholder')} + className='w-full' + /> +
+ + {selectedMode === 'api-key' && ( +
+ + setApiKey(e.target.value)} + placeholder={t('plugin.detailPanel.apiKeyPlaceholder')} + className='w-full' + /> +
+ )} + + {selectedMode === 'oauth' && ( +
+

+ {t('plugin.detailPanel.oauthCreateNote')} +

+
+ )} + + {selectedMode === 'manual' && ( +
+ + setWebhookUrl(e.target.value)} + placeholder={t('plugin.detailPanel.webhookUrlPlaceholder')} + className='w-full' + /> +
+ )} +
+ +
+ + +
+
+
+ ) +} + +export default SubscriptionModal diff --git a/web/app/components/plugins/plugin-detail-panel/trigger-events-list.tsx b/web/app/components/plugins/plugin-detail-panel/trigger-events-list.tsx new file mode 100644 index 0000000000..0ec8b8350a --- /dev/null +++ b/web/app/components/plugins/plugin-detail-panel/trigger-events-list.tsx @@ -0,0 +1,45 @@ +import React from 'react' +import { useTranslation } from 'react-i18next' +import ToolItem from '@/app/components/tools/provider/tool-item' +import type { PluginDetail } from '@/app/components/plugins/types' + +type Props = { + detail: PluginDetail +} + +export const TriggerEventsList = ({ + detail, +}: Props) => { + const { t } = useTranslation() + const triggers = detail.declaration.trigger?.triggers || [] + + if (!triggers.length) + return null + + // todo: add collection & update ToolItem + return ( +
+
+
+ {t('pluginTrigger.events.actionNum', { num: triggers.length, event: triggers.length > 1 ? 'events' : 'event' })} +
+
+
+ {triggers.map(triggerEvent => ( + + tool={{ + label: triggerEvent.identity.label as any, + description: triggerEvent.description.human, + }} + isBuiltIn={false} + isModel={false} + /> + ))} +
+
+ ) +} diff --git a/web/app/components/plugins/types.ts b/web/app/components/plugins/types.ts index c60d9e260c..5f4f603368 100644 --- a/web/app/components/plugins/types.ts +++ b/web/app/components/plugins/types.ts @@ -3,11 +3,14 @@ import type { ToolCredential } from '@/app/components/tools/types' import type { Locale } from '@/i18n-config' import type { AgentFeature } from '@/app/components/workflow/nodes/agent/types' import type { AutoUpdateConfig } from './reference-setting-modal/auto-update-setting/types' +import type { FormTypeEnum } from '../base/form/types' + export enum PluginType { tool = 'tool', model = 'model', extension = 'extension', agent = 'agent-strategy', + trigger = 'trigger', } export enum PluginSource { @@ -80,6 +83,103 @@ export type PluginDeclaration = { tags: string[] agent_strategy: any meta: PluginDeclarationMeta + trigger: PluginTriggerDefinition +} + +export type PluginTriggerDefinition = { + identity: Identity + credentials_schema: CredentialsSchema[] + oauth_schema: OauthSchema + subscription_schema: SubscriptionSchema + triggers: Trigger[] +} + +export type CredentialsSchema = { + name: string + label: Record + description: Record + type: FormTypeEnum + scope: any + required: boolean + default: any + options: any + help: Record + url: string + placeholder: Record +} + +export type OauthSchema = { + client_schema: CredentialsSchema[] + credentials_schema: CredentialsSchema[] +} + +export type SubscriptionSchema = { + parameters_schema: ParametersSchema[] + properties_schema: PropertiesSchema[] +} + +export type ParametersSchema = { + name: string + label: Record + type: FormTypeEnum + auto_generate: any + template: any + scope: any + required: boolean + multiple: boolean + default?: string[] + min: any + max: any + precision: any + options?: Array<{ + value: string + label: Record + icon?: string + }> + description: Record +} + +export type PropertiesSchema = { + type: FormTypeEnum + name: string + scope: any + required: boolean + default: any + options: Array<{ + value: string + label: Record + icon?: string + }> + label: Record + help: Record + url: any + placeholder: any +} + +export type Trigger = { + identity: Identity + description: Record + parameters: { + name: string + label: Record + type: string + auto_generate: any + template: any + scope: any + required: boolean + multiple: boolean + default: any + min: any + max: any + precision: any + options?: Array<{ + value: string + label: Record + icon?: string + }> + description?: Record + }[] + output_schema: Record } export type PluginManifestInMarket = { @@ -461,15 +561,18 @@ export type StrategyDetail = { features: AgentFeature[] } +export type Identity = { + author: string + name: string + label: Record + description: Record + icon: string + icon_dark?: string + tags: string[] +} + export type StrategyDeclaration = { - identity: { - author: string - name: string - description: Record - icon: string - label: Record - tags: string[] - }, + identity: Identity, plugin_id: string strategies: StrategyDetail[] } diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index 2cc83ccf09..4ee9b97e01 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -81,7 +81,7 @@ export type TriggerParameter = { label: TypeWithI18N description?: TypeWithI18N type: 'string' | 'number' | 'boolean' | 'select' | 'file' | 'files' - | 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select' + | 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select' auto_generate?: { type: string value?: any @@ -105,7 +105,7 @@ export type TriggerParameter = { export type TriggerCredentialField = { type: 'secret-input' | 'text-input' | 'select' | 'boolean' - | 'app-selector' | 'model-selector' | 'tools-selector' + | 'app-selector' | 'model-selector' | 'tools-selector' name: string scope?: string | null required: boolean @@ -173,28 +173,45 @@ export type TriggerWithProvider = Collection & { // ===== API Service Types ===== // Trigger subscription instance types -export type TriggerSubscription = { - id: string - name: string - provider: string - credential_type: 'api_key' | 'oauth2' | 'unauthorized' - credentials: Record - endpoint: string - parameters: Record - properties: Record + +export enum TriggerCredentialTypeEnum { + ApiKey = 'api-key', + Oauth2 = 'oauth2', + Unauthorized = 'unauthorized', } -export type TriggerSubscriptionBuilder = { +type TriggerSubscriptionStructure = { id: string name: string provider: string + credential_type: TriggerCredentialTypeEnum + credentials: TriggerSubCredentials endpoint: string - parameters: Record - properties: Record - credentials: Record - credential_type: 'api_key' | 'oauth2' | 'unauthorized' + parameters: TriggerSubParameters + properties: TriggerSubProperties } +export type TriggerSubscription = TriggerSubscriptionStructure + +export type TriggerSubCredentials = { + access_tokens: string +} + +export type TriggerSubParameters = { + repository: string + webhook_secret?: string +} + +export type TriggerSubProperties = { + active: boolean + events: string[] + external_id: string + repository: string + webhook_secret?: string +} + +export type TriggerSubscriptionBuilder = TriggerSubscriptionStructure + // OAuth configuration types export type TriggerOAuthConfig = { configured: boolean diff --git a/web/i18n-config/i18next-config.ts b/web/i18n-config/i18next-config.ts index da3a2f3425..9bfc9d1989 100644 --- a/web/i18n-config/i18next-config.ts +++ b/web/i18n-config/i18next-config.ts @@ -36,6 +36,7 @@ const NAMESPACES = [ 'login', 'oauth', 'plugin-tags', + 'plugin-trigger', 'plugin', 'register', 'run-log', diff --git a/web/i18n/en-US/plugin-trigger.ts b/web/i18n/en-US/plugin-trigger.ts new file mode 100644 index 0000000000..4c636d7f2f --- /dev/null +++ b/web/i18n/en-US/plugin-trigger.ts @@ -0,0 +1,169 @@ +const translation = { + subscription: { + title: 'Subscriptions', + empty: { + title: 'No subscriptions', + description: 'Create your first subscription to start receiving events', + button: 'New subscription', + }, + list: { + title: 'Subscriptions', + addButton: 'Add', + item: { + enabled: 'Enabled', + disabled: 'Disabled', + credentialType: { + api_key: 'API Key', + oauth2: 'OAuth', + unauthorized: 'Manual', + }, + actions: { + delete: 'Delete', + deleteConfirm: { + title: 'Delete subscription', + content: 'Are you sure you want to delete "{{name}}"?', + contentWithApps: 'This subscription is being used in {{count}} apps. Are you sure you want to delete "{{name}}"?', + confirm: 'Delete', + cancel: 'Cancel', + }, + }, + status: { + active: 'Active', + inactive: 'Inactive', + }, + }, + }, + addType: { + title: 'Add subscription', + description: 'Choose how you want to create your trigger subscription', + options: { + apiKey: { + title: 'Via API Key', + description: 'Automatically create subscription using API credentials', + }, + oauth: { + title: 'Via OAuth', + description: 'Authorize with third-party platform to create subscription', + }, + manual: { + title: 'Manual Setup', + description: 'Manually configure webhook URL and settings', + tip: 'Configure URL on third-party platform manually', + }, + }, + }, + }, + modal: { + steps: { + verify: 'Verify', + configuration: 'Configuration', + }, + common: { + cancel: 'Cancel', + back: 'Back', + next: 'Next', + create: 'Create', + verify: 'Verify', + authorize: 'Authorize', + creating: 'Creating...', + verifying: 'Verifying...', + authorizing: 'Authorizing...', + }, + apiKey: { + title: 'Create via API Key', + verify: { + title: 'Verify Credentials', + description: 'Please provide your API credentials to verify access', + error: 'Credential verification failed. Please check your API key.', + success: 'Credentials verified successfully', + }, + configuration: { + title: 'Configure Subscription', + description: 'Set up your subscription parameters', + }, + }, + oauth: { + title: 'Create via OAuth', + authorization: { + title: 'OAuth Authorization', + description: 'Authorize Dify to access your account', + redirectUrl: 'Redirect URL', + redirectUrlHelp: 'Use this URL in your OAuth app configuration', + authorizeButton: 'Authorize with {{provider}}', + waitingAuth: 'Waiting for authorization...', + authSuccess: 'Authorization successful', + authFailed: 'Authorization failed', + }, + configuration: { + title: 'Configure Subscription', + description: 'Set up your subscription parameters after authorization', + }, + }, + manual: { + title: 'Manual Setup', + description: 'Configure your webhook subscription manually', + instruction: { + title: 'Setup Instructions', + step1: '1. Copy the callback URL below', + step2: '2. Go to your third-party platform webhook settings', + step3: '3. Add the callback URL as a webhook endpoint', + step4: '4. Configure the events you want to receive', + step5: '5. Test the webhook by triggering an event', + step6: '6. Return here to verify the webhook is working and complete setup', + }, + logs: { + title: 'Request Logs', + description: 'Monitor incoming webhook requests', + empty: 'No requests received yet. Make sure to test your webhook configuration.', + status: { + success: 'Success', + error: 'Error', + }, + expandAll: 'Expand All', + collapseAll: 'Collapse All', + timestamp: 'Timestamp', + method: 'Method', + path: 'Path', + headers: 'Headers', + body: 'Body', + response: 'Response', + }, + }, + form: { + subscriptionName: { + label: 'Subscription Name', + placeholder: 'Enter subscription name', + required: 'Subscription name is required', + }, + callbackUrl: { + label: 'Callback URL', + description: 'This URL will receive webhook events', + copy: 'Copy', + copied: 'Copied!', + }, + }, + errors: { + createFailed: 'Failed to create subscription', + verifyFailed: 'Failed to verify credentials', + authFailed: 'Authorization failed', + networkError: 'Network error, please try again', + }, + }, + events: { + title: 'Available Events', + description: 'Events that this trigger plugin can subscribe to', + empty: 'No events available', + actionNum: '{{num}} {{event}} INCLUDED', + item: { + parameters: '{{count}} parameters', + }, + }, + provider: { + github: 'GitHub', + gitlab: 'GitLab', + notion: 'Notion', + webhook: 'Webhook', + }, +} + +export default translation diff --git a/web/i18n/zh-Hans/plugin-trigger.ts b/web/i18n/zh-Hans/plugin-trigger.ts new file mode 100644 index 0000000000..6f623736e8 --- /dev/null +++ b/web/i18n/zh-Hans/plugin-trigger.ts @@ -0,0 +1,169 @@ +const translation = { + subscription: { + title: '订阅', + empty: { + title: '暂无订阅', + description: '创建您的第一个订阅以开始接收事件', + button: '新建订阅', + }, + list: { + title: '订阅列表', + addButton: '添加', + item: { + enabled: '已启用', + disabled: '已禁用', + credentialType: { + api_key: 'API密钥', + oauth2: 'OAuth', + unauthorized: '手动', + }, + actions: { + delete: '删除', + deleteConfirm: { + title: '删除订阅', + content: '确定要删除"{{name}}"吗?', + contentWithApps: '该订阅正在被{{count}}个应用使用。确定要删除"{{name}}"吗?', + confirm: '删除', + cancel: '取消', + }, + }, + status: { + active: '活跃', + inactive: '非活跃', + }, + }, + }, + addType: { + title: '添加订阅', + description: '选择创建触发器订阅的方式', + options: { + apiKey: { + title: '通过API密钥', + description: '使用API凭据自动创建订阅', + }, + oauth: { + title: '通过OAuth', + description: '与第三方平台授权以创建订阅', + }, + manual: { + title: '手动设置', + description: '手动配置Webhook URL和设置', + tip: '手动配置 URL 到第三方平台', + }, + }, + }, + }, + modal: { + steps: { + verify: '验证', + configuration: '配置', + }, + common: { + cancel: '取消', + back: '返回', + next: '下一步', + create: '创建', + verify: '验证', + authorize: '授权', + creating: '创建中...', + verifying: '验证中...', + authorizing: '授权中...', + }, + apiKey: { + title: '通过API密钥创建', + verify: { + title: '验证凭据', + description: '请提供您的API凭据以验证访问权限', + error: '凭据验证失败,请检查您的API密钥。', + success: '凭据验证成功', + }, + configuration: { + title: '配置订阅', + description: '设置您的订阅参数', + }, + }, + oauth: { + title: '通过OAuth创建', + authorization: { + title: 'OAuth授权', + description: '授权Dify访问您的账户', + redirectUrl: '重定向URL', + redirectUrlHelp: '在您的OAuth应用配置中使用此URL', + authorizeButton: '使用{{provider}}授权', + waitingAuth: '等待授权中...', + authSuccess: '授权成功', + authFailed: '授权失败', + }, + configuration: { + title: '配置订阅', + description: '授权完成后设置您的订阅参数', + }, + }, + manual: { + title: '手动设置', + description: '手动配置您的Webhook订阅', + instruction: { + title: '设置说明', + step1: '1. 复制下方的回调URL', + step2: '2. 前往您的第三方平台Webhook设置', + step3: '3. 将回调URL添加为Webhook端点', + step4: '4. 配置您想要接收的事件', + step5: '5. 通过触发事件来测试Webhook', + step6: '6. 返回此处验证Webhook正常工作并完成设置', + }, + logs: { + title: '请求日志', + description: '监控传入的Webhook请求', + empty: '尚未收到任何请求。请确保测试您的Webhook配置。', + status: { + success: '成功', + error: '错误', + }, + expandAll: '展开全部', + collapseAll: '收起全部', + timestamp: '时间戳', + method: '方法', + path: '路径', + headers: '请求头', + body: '请求体', + response: '响应', + }, + }, + form: { + subscriptionName: { + label: '订阅名称', + placeholder: '输入订阅名称', + required: '订阅名称为必填项', + }, + callbackUrl: { + label: '回调URL', + description: '此URL将接收Webhook事件', + copy: '复制', + copied: '已复制!', + }, + }, + errors: { + createFailed: '创建订阅失败', + verifyFailed: '验证凭据失败', + authFailed: '授权失败', + networkError: '网络错误,请重试', + }, + }, + events: { + title: '可用事件', + description: '此触发器插件可以订阅的事件', + empty: '没有可用事件', + eventNum: '包含 {{num}} 个 {{event}}', + item: { + parameters: '{{count}}个参数', + }, + }, + provider: { + github: 'GitHub', + gitlab: 'GitLab', + notion: 'Notion', + webhook: 'Webhook', + }, +} + +export default translation diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts index 4f8a38ec3b..f3aa280247 100644 --- a/web/service/use-triggers.ts +++ b/web/service/use-triggers.ts @@ -95,7 +95,7 @@ export const useInvalidateAllTriggerPlugins = () => { // ===== Trigger Subscriptions Management ===== export const useTriggerSubscriptions = (provider: string, enabled = true) => { return useQuery({ - queryKey: [NAME_SPACE, 'subscriptions', provider], + queryKey: [NAME_SPACE, 'list-subscriptions', provider], queryFn: () => get(`/workspaces/current/trigger-provider/${provider}/subscriptions/list`), enabled: enabled && !!provider, }) @@ -115,8 +115,7 @@ export const useCreateTriggerSubscriptionBuilder = () => { mutationKey: [NAME_SPACE, 'create-subscription-builder'], mutationFn: (payload: { provider: string - name?: string - credentials?: Record + credential_type?: string }) => { const { provider, ...body } = payload return post<{ subscription_builder: TriggerSubscriptionBuilder }>( @@ -153,10 +152,12 @@ export const useVerifyTriggerSubscriptionBuilder = () => { mutationFn: (payload: { provider: string subscriptionBuilderId: string + credentials?: Record }) => { - const { provider, subscriptionBuilderId } = payload + const { provider, subscriptionBuilderId, ...body } = payload return post( `/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`, + { body }, ) }, })