feat/trigger plugin apikey (#25388)

This commit is contained in:
lyzno1 2025-09-09 15:01:06 +08:00 committed by GitHub
parent 1c8850fc95
commit b433322e8d
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
12 changed files with 628 additions and 162 deletions

View File

@ -7,6 +7,7 @@ import { useLanguage } from '@/app/components/header/account-setting/model-provi
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { VarType } from '@/app/components/workflow/types'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import { useTriggerPluginDynamicOptions } from '@/service/use-triggers'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import FormInputTypeSwitch from './form-input-type-switch'
@ -50,8 +51,8 @@ const FormInputItem: FC<Props> = ({
providerType,
}) => {
const language = useLanguage()
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
const [isLoadingOptions, setIsLoadingOptions] = useState(false)
const [toolsOptions, setToolsOptions] = useState<FormOption[] | null>(null)
const [isLoadingToolsOptions, setIsLoadingToolsOptions] = useState(false)
const {
placeholder,
@ -136,7 +137,7 @@ const FormInputItem: FC<Props> = ({
return VarKindType.mixed
}
// Fetch dynamic options hook
// Fetch dynamic options hook for tools
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
currentProvider?.plugin_id || '',
currentProvider?.name || '',
@ -146,27 +147,48 @@ const FormInputItem: FC<Props> = ({
extraParams,
)
// Fetch dynamic options when component mounts or dependencies change
// Fetch dynamic options hook for triggers
const { data: triggerDynamicOptions, isLoading: isTriggerOptionsLoading } = useTriggerPluginDynamicOptions({
plugin_id: currentProvider?.plugin_id || '',
provider: currentProvider?.name || '',
action: currentResource?.name || '',
parameter: variable || '',
extra: extraParams,
}, isDynamicSelect && providerType === 'trigger' && !!currentResource && !!currentProvider)
// Computed values for dynamic options (unified for triggers and tools)
const dynamicOptions = providerType === 'trigger' ? triggerDynamicOptions?.options || [] : toolsOptions
const isLoadingOptions = providerType === 'trigger' ? isTriggerOptionsLoading : isLoadingToolsOptions
// Fetch dynamic options for tools only (triggers use hook directly)
useEffect(() => {
const fetchOptions = async () => {
if (isDynamicSelect && currentResource && currentProvider) {
setIsLoadingOptions(true)
const fetchToolOptions = async () => {
if (isDynamicSelect && currentResource && currentProvider && providerType === 'tool') {
setIsLoadingToolsOptions(true)
try {
const data = await fetchDynamicOptions()
setDynamicOptions(data?.options || [])
setToolsOptions(data?.options || [])
}
catch (error) {
console.error('Failed to fetch dynamic options:', error)
setDynamicOptions([])
setToolsOptions([])
}
finally {
setIsLoadingOptions(false)
setIsLoadingToolsOptions(false)
}
}
}
fetchOptions()
}, [isDynamicSelect, currentResource?.name, currentProvider?.name, variable, extraParams])
fetchToolOptions()
}, [
isDynamicSelect,
currentResource?.name,
currentProvider?.name,
variable,
extraParams,
providerType,
fetchDynamicOptions,
])
const handleTypeChange = (newType: string) => {
if (newType === VarKindType.variable) {

View File

@ -49,7 +49,7 @@ import { BlockEnum, type Node, NodeRunningStatus } from '@/app/components/workfl
import { useStore as useAppStore } from '@/app/components/app/store'
import { useStore } from '@/app/components/workflow/store'
import Tab, { TabType } from './tab'
import { useAllTriggerPlugins, useTriggerSubscriptions } from '@/service/use-triggers'
import { useAllTriggerPlugins } from '@/service/use-triggers'
import AuthMethodSelector from '@/app/components/workflow/nodes/trigger-plugin/components/auth-method-selector'
import LastRun from './last-run'
import useLastRun from './last-run/use-last-run'
@ -238,23 +238,8 @@ const BasePanel: FC<BasePanelProps> = ({
return buildInTools.find(item => canFindTool(item.id, data.provider_id))
}, [buildInTools, data.provider_id])
// For trigger plugins, check if they have existing subscriptions (authenticated)
const triggerProvider = useMemo(() => {
if (data.type === BlockEnum.TriggerPlugin) {
if (data.provider_name)
return data.provider_name
return data.provider_id || ''
}
return ''
}, [data.type, data.provider_id, data.provider_name])
const { data: triggerSubscriptions = [] } = useTriggerSubscriptions(
triggerProvider,
data.type === BlockEnum.TriggerPlugin && !!triggerProvider,
)
// For trigger plugins, get basic provider info
const { data: triggerProviders = [] } = useAllTriggerPlugins()
const currentTriggerProvider = useMemo(() => {
if (data.type !== BlockEnum.TriggerPlugin || !data.provider_id || !data.provider_name)
return undefined
@ -271,26 +256,20 @@ const BasePanel: FC<BasePanelProps> = ({
return methods
}, [currentTriggerProvider])
const isTriggerAuthenticated = useMemo(() => {
if (data.type !== BlockEnum.TriggerPlugin) return true
if (!triggerSubscriptions.length) return false
// Simplified: Always show auth selector for trigger plugins
const shouldShowTriggerAuthSelector = useMemo(() => {
return data.type === BlockEnum.TriggerPlugin && currentTriggerProvider && supportedAuthMethods.length > 0
}, [data.type, currentTriggerProvider, supportedAuthMethods.length])
const subscription = triggerSubscriptions[0]
return subscription.credential_type !== 'unauthorized'
}, [data.type, triggerSubscriptions])
// Simplified: Always show tab for trigger plugins
const shouldShowTriggerTab = useMemo(() => {
return data.type === BlockEnum.TriggerPlugin && currentTriggerProvider
}, [data.type, currentTriggerProvider])
const shouldShowAuthSelector = useMemo(() => {
return data.type === BlockEnum.TriggerPlugin
&& !isTriggerAuthenticated
&& supportedAuthMethods.length > 0
&& !!currentTriggerProvider
}, [data.type, isTriggerAuthenticated, supportedAuthMethods.length, currentTriggerProvider])
// Unified check for any node that needs authentication UI
const needsAuth = useMemo(() => {
// Unified check for tool authentication UI
const needsToolAuth = useMemo(() => {
return (data.type === BlockEnum.Tool && currCollection?.allow_delete)
|| (data.type === BlockEnum.TriggerPlugin && isTriggerAuthenticated)
}, [data.type, currCollection?.allow_delete, isTriggerAuthenticated])
}, [data.type, currCollection?.allow_delete])
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
handleNodeDataUpdateWithSyncDraft({
@ -433,7 +412,7 @@ const BasePanel: FC<BasePanelProps> = ({
/>
</div>
{
needsAuth && data.type === BlockEnum.Tool && currCollection?.allow_delete && (
needsToolAuth && (
<PluginAuth
className='px-4 pb-2'
pluginPayload={{
@ -455,7 +434,15 @@ const BasePanel: FC<BasePanelProps> = ({
)
}
{
needsAuth && data.type !== BlockEnum.Tool && (
shouldShowTriggerAuthSelector && (
<AuthMethodSelector
provider={currentTriggerProvider!}
supportedMethods={supportedAuthMethods}
/>
)
}
{
shouldShowTriggerTab && (
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}
@ -469,15 +456,7 @@ const BasePanel: FC<BasePanelProps> = ({
)
}
{
shouldShowAuthSelector && (
<AuthMethodSelector
provider={currentTriggerProvider!}
supportedMethods={supportedAuthMethods}
/>
)
}
{
!needsAuth && data.type !== BlockEnum.TriggerPlugin && (
!needsToolAuth && data.type !== BlockEnum.TriggerPlugin && (
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}

View File

@ -11,15 +11,11 @@ import Loading from '@/app/components/base/loading'
import Form from '@/app/components/header/account-setting/model-provider-page/model-modal/Form'
import { LinkExternal02 } from '@/app/components/base/icons/src/vender/line/general'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import {
useBuildTriggerSubscription,
useCreateTriggerSubscriptionBuilder,
useInvalidateTriggerSubscriptions,
useUpdateTriggerSubscriptionBuilder,
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import { useInvalidateTriggerSubscriptions } from '@/service/use-triggers'
import { useToastContext } from '@/app/components/base/toast'
import { findMissingRequiredField, sanitizeFormValues } from '../utils/form-helpers'
import { useTriggerAuthFlow } from '../hooks/use-trigger-auth-flow'
import ParametersForm from './parameters-form'
type ApiKeyConfigModalProps = {
provider: TriggerWithProvider
@ -35,29 +31,47 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
const { t } = useTranslation()
const { notify } = useToastContext()
const language = useLanguage()
const [credentialSchema, setCredentialSchema] = useState<any[]>([])
const [tempCredential, setTempCredential] = useState<Record<string, any>>({})
const [isLoading, setIsLoading] = useState(false)
const [subscriptionBuilderId, setSubscriptionBuilderId] = useState<string>('')
const createBuilder = useCreateTriggerSubscriptionBuilder()
const updateBuilder = useUpdateTriggerSubscriptionBuilder()
const verifyBuilder = useVerifyTriggerSubscriptionBuilder()
const buildSubscription = useBuildTriggerSubscription()
const invalidateSubscriptions = useInvalidateTriggerSubscriptions()
const [credentialSchema, setCredentialSchema] = useState<any[]>([])
const [credentials, setCredentials] = useState<Record<string, any>>({})
const [parameters, setParameters] = useState<Record<string, any>>({})
const [properties, setProperties] = useState<Record<string, any>>({})
const [subscriptionName, setSubscriptionName] = useState<string>('')
const {
step,
builderId,
isLoading,
startAuth,
verifyAuth,
completeConfig,
reset,
} = useTriggerAuthFlow(provider)
useEffect(() => {
if (provider.credentials_schema) {
const schemas = toolCredentialToFormSchemas(provider.credentials_schema as any)
setCredentialSchema(schemas)
const defaultCredentials = addDefaultValue({}, schemas)
// Use utility function for consistent data sanitization
setTempCredential(sanitizeFormValues(defaultCredentials))
setCredentials(sanitizeFormValues(defaultCredentials))
}
}, [provider.credentials_schema])
const handleSave = async () => {
// Validate required fields using utility function
useEffect(() => {
startAuth().catch((err) => {
notify({
type: 'error',
message: t('workflow.nodes.triggerPlugin.failedToStart', { error: err.message }),
})
})
return () => {
reset()
}
}, []) // Remove dependencies to run only once on mount
const handleCredentialsSubmit = async () => {
const requiredFields = credentialSchema
.filter(field => field.required)
.map(field => ({
@ -65,7 +79,7 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
label: field.label[language] || field.label.en_US,
}))
const missingField = findMissingRequiredField(tempCredential, requiredFields)
const missingField = findMissingRequiredField(credentials, requiredFields)
if (missingField) {
Toast.notify({
type: 'error',
@ -76,56 +90,156 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
return
}
setIsLoading(true)
try {
await verifyAuth(credentials)
notify({
type: 'success',
message: t('workflow.nodes.triggerPlugin.credentialsVerified'),
})
}
catch (err: any) {
notify({
type: 'error',
message: t('workflow.nodes.triggerPlugin.credentialVerificationFailed', {
error: err.message,
}),
})
}
}
const handleFinalSubmit = async () => {
if (!subscriptionName.trim()) {
notify({
type: 'error',
message: t('workflow.nodes.triggerPlugin.subscriptionNameRequired'),
})
return
}
try {
// Step 1: Create subscription builder
let builderId = subscriptionBuilderId
if (!builderId) {
const createResponse = await createBuilder.mutateAsync({
provider: provider.name,
credentials: tempCredential,
})
builderId = createResponse.subscription_builder.id
setSubscriptionBuilderId(builderId)
}
else {
// Update existing builder
await updateBuilder.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
credentials: tempCredential,
})
}
await completeConfig(parameters, properties, subscriptionName)
// Step 2: Verify credentials
await verifyBuilder.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
})
// Step 3: Build final subscription
await buildSubscription.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
})
// Step 4: Invalidate and notify success
invalidateSubscriptions(provider.name)
notify({
type: 'success',
message: t('workflow.nodes.triggerPlugin.apiKeyConfigured'),
message: t('workflow.nodes.triggerPlugin.configurationComplete'),
})
onSuccess()
}
catch (error: any) {
catch (err: any) {
notify({
type: 'error',
message: t('workflow.nodes.triggerPlugin.configurationFailed', { error: error.message }),
message: t('workflow.nodes.triggerPlugin.configurationFailed', { error: err.message }),
})
}
finally {
setIsLoading(false)
}
const getTitle = () => {
switch (step) {
case 'auth':
return t('workflow.nodes.triggerPlugin.configureApiKey')
case 'params':
return t('workflow.nodes.triggerPlugin.configureParameters')
case 'complete':
return t('workflow.nodes.triggerPlugin.configurationComplete')
default:
return t('workflow.nodes.triggerPlugin.configureApiKey')
}
}
const getDescription = () => {
switch (step) {
case 'auth':
return t('workflow.nodes.triggerPlugin.apiKeyDescription')
case 'params':
return t('workflow.nodes.triggerPlugin.parametersDescription')
case 'complete':
return t('workflow.nodes.triggerPlugin.configurationCompleteDescription')
default:
return ''
}
}
const renderContent = () => {
if (credentialSchema.length === 0 && step === 'auth')
return <Loading type='app' />
switch (step) {
case 'auth':
return (
<>
<Form
value={credentials}
onChange={setCredentials}
formSchemas={credentialSchema}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='!bg-components-input-bg-normal'
fieldMoreInfo={item => item.url ? (
<a
href={item.url}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<LinkExternal02 className='ml-1 h-3 w-3' />
</a>
) : null}
/>
<div className='mt-4 flex justify-end space-x-2'>
<Button onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
<Button
loading={isLoading}
disabled={isLoading}
variant='primary'
onClick={handleCredentialsSubmit}
>
{t('workflow.nodes.triggerPlugin.verifyAndContinue')}
</Button>
</div>
</>
)
case 'params':
return (
<ParametersForm
provider={provider}
builderId={builderId}
parametersValue={parameters}
propertiesValue={properties}
subscriptionName={subscriptionName}
onParametersChange={setParameters}
onPropertiesChange={setProperties}
onSubscriptionNameChange={setSubscriptionName}
onSubmit={handleFinalSubmit}
onCancel={onCancel}
isLoading={isLoading}
/>
)
case 'complete':
return (
<div className="flex flex-col items-center justify-center py-8">
<div className="bg-background-success-emphasis mb-4 flex h-12 w-12 items-center justify-center rounded-full">
<svg className="h-6 w-6 text-text-success" fill="currentColor" viewBox="0 0 20 20">
<path fillRule="evenodd" d="M16.707 5.293a1 1 0 010 1.414l-8 8a1 1 0 01-1.414 0l-4-4a1 1 0 011.414-1.414L8 12.586l7.293-7.293a1 1 0 011.414 0z" clipRule="evenodd" />
</svg>
</div>
<p className="mb-4 text-center text-text-primary">
{t('workflow.nodes.triggerPlugin.configurationCompleteMessage')}
</p>
<Button variant="primary" onClick={onSuccess}>
{t('common.operation.done')}
</Button>
</div>
)
default:
return null
}
}
@ -133,8 +247,8 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
<Drawer
isShow
onHide={onCancel}
title={t('workflow.nodes.triggerPlugin.configureApiKey')}
titleDescription={t('workflow.nodes.triggerPlugin.apiKeyDescription')}
title={getTitle()}
titleDescription={getDescription()}
panelClassName='mt-[64px] mb-2 !w-[420px] border-components-panel-border'
maxWidthClassName='!max-w-[420px]'
height='calc(100vh - 64px)'
@ -142,45 +256,7 @@ const ApiKeyConfigModal: FC<ApiKeyConfigModalProps> = ({
headerClassName='!border-b-divider-subtle'
body={
<div className='h-full px-6 py-3'>
{credentialSchema.length === 0 ? (
<Loading type='app' />
) : (
<>
<Form
value={tempCredential}
onChange={setTempCredential}
formSchemas={credentialSchema}
isEditMode={true}
showOnVariableMap={{}}
validating={false}
inputClassName='!bg-components-input-bg-normal'
fieldMoreInfo={item => item.url ? (
<a
href={item.url}
target='_blank'
rel='noopener noreferrer'
className='inline-flex items-center text-xs text-text-accent'
>
{t('tools.howToGet')}
<LinkExternal02 className='ml-1 h-3 w-3' />
</a>
) : null}
/>
<div className='mt-4 flex justify-end space-x-2'>
<Button onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
<Button
loading={isLoading}
disabled={isLoading}
variant='primary'
onClick={handleSave}
>
{t('common.operation.save')}
</Button>
</div>
</>
)}
{renderContent()}
</div>
}
isShowMask={true}

View File

@ -0,0 +1,171 @@
'use client'
import type { FC } from 'react'
import { useMemo } from 'react'
import { useTranslation } from 'react-i18next'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
import type { Trigger } from '@/app/components/tools/types'
import { toolCredentialToFormSchemas, toolParametersToFormSchemas } from '@/app/components/tools/utils/to-form-schema'
import TriggerForm from './trigger-form'
import Button from '@/app/components/base/button'
import Input from '@/app/components/base/input'
type ParametersFormProps = {
provider: TriggerWithProvider
trigger?: Trigger
builderId: string
parametersValue: Record<string, any>
propertiesValue: Record<string, any>
subscriptionName: string
onParametersChange: (value: Record<string, any>) => void
onPropertiesChange: (value: Record<string, any>) => void
onSubscriptionNameChange: (value: string) => void
onSubmit: () => void
onCancel: () => void
isLoading?: boolean
readOnly?: boolean
}
const ParametersForm: FC<ParametersFormProps> = ({
provider,
trigger,
builderId,
parametersValue,
propertiesValue,
subscriptionName,
onParametersChange,
onPropertiesChange,
onSubscriptionNameChange,
onSubmit,
onCancel,
isLoading = false,
readOnly = false,
}) => {
const { t } = useTranslation()
// Use the first trigger if no specific trigger is provided
// This is needed for dynamic options API which requires a trigger action
const currentTrigger = trigger || provider.triggers?.[0]
const parametersSchema = useMemo(() => {
if (!provider.subscription_schema?.parameters_schema) return []
return toolParametersToFormSchemas(provider.subscription_schema.parameters_schema as any)
}, [provider.subscription_schema?.parameters_schema])
const propertiesSchema = useMemo(() => {
if (!provider.subscription_schema?.properties_schema) return []
return toolCredentialToFormSchemas(provider.subscription_schema.properties_schema as any)
}, [provider.subscription_schema?.properties_schema])
const hasParameters = parametersSchema.length > 0
const hasProperties = propertiesSchema.length > 0
if (!hasParameters && !hasProperties) {
return (
<div className="flex flex-col items-center justify-center py-8">
<p className="mb-4 text-text-tertiary">
{t('workflow.nodes.triggerPlugin.noConfigurationRequired')}
</p>
<div className="flex space-x-2">
<Button onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
<Button
variant="primary"
onClick={onSubmit}
loading={isLoading}
disabled={isLoading}
>
{t('common.operation.save')}
</Button>
</div>
</div>
)
}
return (
<div className="space-y-6">
{/* Subscription Name Section */}
<div className="space-y-3">
<div>
<h3 className="text-sm font-medium text-text-primary">
{t('workflow.nodes.triggerPlugin.subscriptionName')}
</h3>
<p className="text-xs text-text-tertiary">
{t('workflow.nodes.triggerPlugin.subscriptionNameDescription')}
</p>
</div>
<Input
value={subscriptionName}
onChange={e => onSubscriptionNameChange(e.target.value)}
placeholder={t('workflow.nodes.triggerPlugin.subscriptionNamePlaceholder')}
readOnly={readOnly}
/>
</div>
{/* Parameters Section */}
{hasParameters && (
<div className="space-y-3">
<div>
<h3 className="text-sm font-medium text-text-primary">
{t('workflow.nodes.triggerPlugin.parameters')}
</h3>
<p className="text-xs text-text-tertiary">
{t('workflow.nodes.triggerPlugin.parametersDescription')}
</p>
</div>
<TriggerForm
readOnly={readOnly}
nodeId=""
schema={parametersSchema as any}
value={parametersValue}
onChange={onParametersChange}
currentTrigger={currentTrigger}
currentProvider={provider}
extraParams={{ subscription_builder_id: builderId }}
/>
</div>
)}
{/* Properties Section */}
{hasProperties && (
<div className="space-y-3">
<div>
<h3 className="text-sm font-medium text-text-primary">
{t('workflow.nodes.triggerPlugin.properties')}
</h3>
<p className="text-xs text-text-tertiary">
{t('workflow.nodes.triggerPlugin.propertiesDescription')}
</p>
</div>
<TriggerForm
readOnly={readOnly}
nodeId=""
schema={propertiesSchema as any}
value={propertiesValue}
onChange={onPropertiesChange}
currentTrigger={currentTrigger}
currentProvider={provider}
extraParams={{ subscription_builder_id: builderId }}
/>
</div>
)}
{/* Action Buttons */}
<div className="flex justify-end space-x-2 border-t border-divider-subtle pt-4">
<Button onClick={onCancel}>
{t('common.operation.cancel')}
</Button>
<Button
variant="primary"
onClick={onSubmit}
loading={isLoading}
disabled={isLoading}
>
{t('common.operation.save')}
</Button>
</div>
</div>
)
}
export default ParametersForm

View File

@ -2,9 +2,9 @@
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Trigger } from '@/app/components/tools/types'
import type { FC } from 'react'
import type { PluginTriggerVarInputs } from '../types'
import type { PluginTriggerVarInputs } from '../../types'
import TriggerFormItem from './item'
import type { TriggerWithProvider } from '../../../block-selector/types'
import type { TriggerWithProvider } from '../../../../block-selector/types'
type Props = {
readOnly: boolean

View File

@ -3,7 +3,7 @@ import type { FC } from 'react'
import {
RiBracesLine,
} from '@remixicon/react'
import type { PluginTriggerVarInputs } from '../types'
import type { PluginTriggerVarInputs } from '../../types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
@ -13,7 +13,7 @@ import FormInputItem from '@/app/components/workflow/nodes/_base/components/form
import { useBoolean } from 'ahooks'
import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal'
import type { Trigger } from '@/app/components/tools/types'
import type { TriggerWithProvider } from '../../../block-selector/types'
import type { TriggerWithProvider } from '../../../../block-selector/types'
type Props = {
readOnly: boolean

View File

@ -0,0 +1,162 @@
import { useCallback, useState } from 'react'
import {
useBuildTriggerSubscription,
useCreateTriggerSubscriptionBuilder,
useUpdateTriggerSubscriptionBuilder,
useVerifyTriggerSubscriptionBuilder,
} from '@/service/use-triggers'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
// Helper function to serialize complex values to strings for backend encryption
const serializeFormValues = (values: Record<string, any>): Record<string, string> => {
const result: Record<string, string> = {}
for (const [key, value] of Object.entries(values)) {
if (value === null || value === undefined)
result[key] = ''
else if (typeof value === 'object')
result[key] = JSON.stringify(value)
else
result[key] = String(value)
}
return result
}
export type AuthFlowStep = 'auth' | 'params' | 'complete'
export type AuthFlowState = {
step: AuthFlowStep
builderId: string
isLoading: boolean
error: string | null
}
export type AuthFlowActions = {
startAuth: () => Promise<void>
verifyAuth: (credentials: Record<string, any>) => Promise<void>
completeConfig: (parameters: Record<string, any>, properties?: Record<string, any>, name?: string) => Promise<void>
reset: () => void
}
export const useTriggerAuthFlow = (provider: TriggerWithProvider): AuthFlowState & AuthFlowActions => {
const [step, setStep] = useState<AuthFlowStep>('auth')
const [builderId, setBuilderId] = useState<string>('')
const [isLoading, setIsLoading] = useState(false)
const [error, setError] = useState<string | null>(null)
const createBuilder = useCreateTriggerSubscriptionBuilder()
const updateBuilder = useUpdateTriggerSubscriptionBuilder()
const verifyBuilder = useVerifyTriggerSubscriptionBuilder()
const buildSubscription = useBuildTriggerSubscription()
const startAuth = useCallback(async () => {
if (builderId) return // Prevent multiple calls if already started
setIsLoading(true)
setError(null)
try {
const response = await createBuilder.mutateAsync({
provider: provider.name,
})
setBuilderId(response.subscription_builder.id)
setStep('auth')
}
catch (err: any) {
setError(err.message || 'Failed to start authentication flow')
throw err
}
finally {
setIsLoading(false)
}
}, [provider.name, createBuilder, builderId])
const verifyAuth = useCallback(async (credentials: Record<string, any>) => {
if (!builderId) {
setError('No builder ID available')
return
}
setIsLoading(true)
setError(null)
try {
await updateBuilder.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
credentials: serializeFormValues(credentials),
})
await verifyBuilder.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
})
setStep('params')
}
catch (err: any) {
setError(err.message || 'Authentication verification failed')
throw err
}
finally {
setIsLoading(false)
}
}, [provider.name, builderId, updateBuilder, verifyBuilder])
const completeConfig = useCallback(async (
parameters: Record<string, any>,
properties: Record<string, any> = {},
name?: string,
) => {
if (!builderId) {
setError('No builder ID available')
return
}
setIsLoading(true)
setError(null)
try {
await updateBuilder.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
parameters: serializeFormValues(parameters),
properties: serializeFormValues(properties),
name,
})
await buildSubscription.mutateAsync({
provider: provider.name,
subscriptionBuilderId: builderId,
})
setStep('complete')
}
catch (err: any) {
setError(err.message || 'Configuration failed')
throw err
}
finally {
setIsLoading(false)
}
}, [provider.name, builderId, updateBuilder, buildSubscription])
const reset = useCallback(() => {
setStep('auth')
setBuilderId('')
setIsLoading(false)
setError(null)
}, [])
return {
step,
builderId,
isLoading,
error,
startAuth,
verifyAuth,
completeConfig,
reset,
}
}

View File

@ -5,7 +5,7 @@ import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import type { NodePanelProps } from '@/app/components/workflow/types'
import useConfig from './use-config'
import TriggerForm from './trigger-form'
import TriggerForm from './components/trigger-form'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types'

View File

@ -743,6 +743,23 @@ const translation = {
apiKeyDescription: 'Configure API key credentials for authentication',
apiKeyConfigured: 'API key configured successfully',
configurationFailed: 'Configuration failed',
failedToStart: 'Failed to start authentication flow',
credentialsVerified: 'Credentials verified successfully',
credentialVerificationFailed: 'Credential verification failed',
verifyAndContinue: 'Verify & Continue',
configureParameters: 'Configure Parameters',
parametersDescription: 'Configure trigger parameters and properties',
configurationComplete: 'Configuration Complete',
configurationCompleteDescription: 'Your trigger has been configured successfully',
configurationCompleteMessage: 'Your trigger configuration is now complete and ready to use.',
parameters: 'Parameters',
properties: 'Properties',
propertiesDescription: 'Additional configuration properties for this trigger',
noConfigurationRequired: 'No additional configuration required for this trigger.',
subscriptionName: 'Subscription Name',
subscriptionNameDescription: 'Enter a unique name for this trigger subscription',
subscriptionNamePlaceholder: 'Enter subscription name...',
subscriptionNameRequired: 'Subscription name is required',
},
questionClassifiers: {
model: 'model',

View File

@ -1053,6 +1053,23 @@ const translation = {
apiKeyDescription: '認証のための API キー認証情報を設定してください',
apiKeyConfigured: 'API キーが正常に設定されました',
configurationFailed: '設定に失敗しました',
failedToStart: '認証フローの開始に失敗しました',
credentialsVerified: '認証情報が正常に検証されました',
credentialVerificationFailed: '認証情報の検証に失敗しました',
verifyAndContinue: '検証して続行',
configureParameters: 'パラメーターを設定',
parametersDescription: 'トリガーのパラメーターとプロパティを設定してください',
configurationComplete: '設定完了',
configurationCompleteDescription: 'トリガーが正常に設定されました',
configurationCompleteMessage: 'トリガーの設定が完了し、使用する準備ができました。',
parameters: 'パラメーター',
properties: 'プロパティ',
propertiesDescription: 'このトリガーの追加設定プロパティ',
noConfigurationRequired: 'このトリガーには追加の設定は必要ありません。',
subscriptionName: 'サブスクリプション名',
subscriptionNameDescription: 'このトリガーサブスクリプションの一意な名前を入力してください',
subscriptionNamePlaceholder: 'サブスクリプション名を入力...',
subscriptionNameRequired: 'サブスクリプション名は必須です',
},
},
tracing: {

View File

@ -1053,6 +1053,23 @@ const translation = {
apiKeyDescription: '配置 API key 凭据进行身份验证',
apiKeyConfigured: 'API key 配置成功',
configurationFailed: '配置失败',
failedToStart: '启动身份验证流程失败',
credentialsVerified: '凭据验证成功',
credentialVerificationFailed: '凭据验证失败',
verifyAndContinue: '验证并继续',
configureParameters: '配置参数',
parametersDescription: '配置触发器参数和属性',
configurationComplete: '配置完成',
configurationCompleteDescription: '您的触发器已成功配置',
configurationCompleteMessage: '您的触发器配置已完成,现在可以使用了。',
parameters: '参数',
properties: '属性',
propertiesDescription: '此触发器的额外配置属性',
noConfigurationRequired: '此触发器不需要额外配置。',
subscriptionName: '订阅名称',
subscriptionNameDescription: '为此触发器订阅输入一个唯一名称',
subscriptionNamePlaceholder: '输入订阅名称...',
subscriptionNameRequired: '订阅名称是必需的',
},
},
tracing: {

View File

@ -268,7 +268,12 @@ export const useTriggerPluginDynamicOptions = (payload: {
queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.extra],
queryFn: () => get<{ options: Array<{ value: string; label: any }> }>(
'/workspaces/current/plugin/parameters/dynamic-options',
{ params: payload },
{
params: {
...payload,
provider_type: 'trigger', // Add required provider_type parameter
},
},
),
enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter,
})