feat: implement trigger plugin frontend integration (#25283)

This commit is contained in:
lyzno1 2025-09-06 16:18:46 +08:00 committed by GitHub
parent 814787677a
commit a91e59d544
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
15 changed files with 886 additions and 83 deletions

3
.gitignore vendored
View File

@ -218,3 +218,6 @@ mise.toml
.roo/
api/.env.backup
/clickzetta
# mcp
.serena

View File

@ -293,13 +293,14 @@ class TriggerOAuthAuthorizeApi(Resource):
credential_type=CredentialType.OAUTH2,
credential_expires_at=0,
expires_at=0,
name=f"{provider_name} OAuth Authentication",
)
# Create response with cookie
response = make_response(
jsonable_encoder(
{
"authorization_url": authorization_url_response,
"authorization_url": authorization_url_response.authorization_url,
"subscription_builder": subscription_builder,
}
)
@ -377,6 +378,7 @@ class TriggerOAuthCallbackApi(Resource):
credential_type=CredentialType.OAUTH2,
credential_expires_at=expires_at,
expires_at=expires_at,
name=f"{provider_name} OAuth Authentication",
)
# Redirect to OAuth callback page
return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback?subscription_id={subscription_builder.id}")

View File

@ -58,15 +58,56 @@ export type TriggerParameter = {
name: string
label: TypeWithI18N
description?: TypeWithI18N
type: string
type: 'string' | 'number' | 'boolean' | 'select' | 'file' | 'files'
| 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select'
auto_generate?: {
type: string
value?: any
} | null
template?: {
type: string
value?: any
} | null
scope?: string | null
required?: boolean
default?: any
min?: number | null
max?: number | null
precision?: number | null
options?: Array<{
value: string
label: TypeWithI18N
icon?: string | null
}> | null
}
export type TriggerCredentialField = {
type: 'secret-input' | 'text-input' | 'select' | 'boolean'
| 'app-selector' | 'model-selector' | 'tools-selector'
name: string
scope?: string | null
required: boolean
default?: string | number | boolean | Array<any> | null
options?: Array<{
value: string
label: TypeWithI18N
}> | null
label: TypeWithI18N
help?: TypeWithI18N
url?: string | null
placeholder?: TypeWithI18N
}
export type TriggerSubscriptionSchema = {
parameters_schema: TriggerParameter[]
properties_schema: TriggerCredentialField[]
}
export type TriggerIdentity = {
author: string
name: string
version: string
label: TypeWithI18N
provider: string
}
export type TriggerDescription = {
@ -92,6 +133,9 @@ export type TriggerProviderApiEntity = {
tags: string[]
plugin_id?: string
plugin_unique_identifier?: string
credentials_schema: TriggerCredentialField[]
oauth_client_schema: TriggerCredentialField[]
subscription_schema: TriggerSubscriptionSchema
triggers: TriggerApiEntity[]
}
@ -99,4 +143,56 @@ export type TriggerProviderApiEntity = {
export type TriggerWithProvider = Collection & {
tools: Tool[] // Use existing Tool type for compatibility
meta: PluginMeta
credentials_schema?: TriggerCredentialField[]
oauth_client_schema?: TriggerCredentialField[]
subscription_schema?: TriggerSubscriptionSchema
}
// ===== API Service Types =====
// Trigger subscription instance types
export type TriggerSubscription = {
id: string
name: string
provider: string
credential_type: 'api_key' | 'oauth2' | 'unauthorized'
credentials: Record<string, any>
endpoint: string
parameters: Record<string, any>
properties: Record<string, any>
}
export type TriggerSubscriptionBuilder = {
id: string
name: string
provider: string
endpoint: string
parameters: Record<string, any>
properties: Record<string, any>
credentials: Record<string, any>
credential_type: 'api_key' | 'oauth2' | 'unauthorized'
}
// OAuth configuration types
export type TriggerOAuthConfig = {
configured: boolean
custom_configured: boolean
custom_enabled: boolean
params: {
client_id: string
client_secret: string
}
}
export type TriggerOAuthClientParams = {
client_id: string
client_secret: string
authorization_url?: string
token_url?: string
scope?: string
}
export type TriggerOAuthResponse = {
authorization_url: string
subscription_builder: TriggerSubscriptionBuilder
}

View File

@ -59,11 +59,11 @@ import PanelWrap from '../before-run-form/panel-wrap'
import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel'
import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices'
import {
AuthorizedInNode,
PluginAuth,
} from '@/app/components/plugins/plugin-auth'
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
import { canFindTool } from '@/utils'
import NodeAuth from './node-auth-factory'
type BasePanelProps = {
children: ReactNode
@ -235,9 +235,13 @@ const BasePanel: FC<BasePanelProps> = ({
const currCollection = useMemo(() => {
return buildInTools.find(item => canFindTool(item.id, data.provider_id))
}, [buildInTools, data.provider_id])
const showPluginAuth = useMemo(() => {
return data.type === BlockEnum.Tool && currCollection?.allow_delete
}, [currCollection, data.type])
// Unified check for any node that needs authentication UI
const needsAuth = useMemo(() => {
return (data.type === BlockEnum.Tool && currCollection?.allow_delete)
|| (data.type === BlockEnum.TriggerPlugin)
}, [data.type, currCollection?.allow_delete])
const handleAuthorizationItemClick = useCallback((credential_id: string) => {
handleNodeDataUpdateWithSyncDraft({
id,
@ -379,7 +383,7 @@ const BasePanel: FC<BasePanelProps> = ({
/>
</div>
{
showPluginAuth && (
needsAuth && data.type === BlockEnum.Tool && currCollection?.allow_delete && (
<PluginAuth
className='px-4 pb-2'
pluginPayload={{
@ -392,20 +396,30 @@ const BasePanel: FC<BasePanelProps> = ({
value={tabType}
onChange={setTabType}
/>
<AuthorizedInNode
pluginPayload={{
provider: currCollection?.name || '',
category: AuthCategory.tool,
}}
onAuthorizationItemClick={handleAuthorizationItemClick}
credentialId={data.credential_id}
<NodeAuth
data={data}
onAuthorizationChange={handleAuthorizationItemClick}
/>
</div>
</PluginAuth>
)
}
{
!showPluginAuth && (
needsAuth && data.type !== BlockEnum.Tool && (
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}
onChange={setTabType}
/>
<NodeAuth
data={data}
onAuthorizationChange={handleAuthorizationItemClick}
/>
</div>
)
}
{
!needsAuth && (
<div className='flex items-center justify-between pl-4 pr-3'>
<Tab
value={tabType}

View File

@ -0,0 +1,149 @@
import type { FC } from 'react'
import { memo, useCallback, useMemo } from 'react'
import { AuthorizedInNode } from '@/app/components/plugins/plugin-auth'
import { AuthCategory } from '@/app/components/plugins/plugin-auth'
import { BlockEnum, type Node } from '@/app/components/workflow/types'
import { canFindTool } from '@/utils'
import { useStore } from '@/app/components/workflow/store'
import AuthenticationMenu from '@/app/components/workflow/nodes/trigger-plugin/components/authentication-menu'
import type { AuthSubscription } from '@/app/components/workflow/nodes/trigger-plugin/components/authentication-menu'
import {
useDeleteTriggerSubscription,
useInitiateTriggerOAuth,
useInvalidateTriggerSubscriptions,
useTriggerSubscriptions,
} from '@/service/use-triggers'
import { useToastContext } from '@/app/components/base/toast'
type NodeAuthProps = {
data: Node['data']
onAuthorizationChange: (credential_id: string) => void
}
const NodeAuth: FC<NodeAuthProps> = ({ data, onAuthorizationChange }) => {
const buildInTools = useStore(s => s.buildInTools)
const { notify } = useToastContext()
// Construct the correct provider path for trigger plugins
// Format should be: plugin_id/provider_name (e.g., "langgenius/github_trigger/github_trigger")
const provider = useMemo(() => {
if (data.type === BlockEnum.TriggerPlugin) {
// If we have both plugin_id and provider_name, construct the full path
if (data.provider_id && data.provider_name)
return `${data.provider_id}/${data.provider_name}`
// Otherwise use provider_id as fallback (might be already complete)
return data.provider_id || ''
}
return data.provider_id || ''
}, [data.type, data.provider_id, data.provider_name])
// Always call hooks at the top level
const { data: subscriptions = [] } = useTriggerSubscriptions(
provider,
data.type === BlockEnum.TriggerPlugin && !!provider,
)
const deleteSubscription = useDeleteTriggerSubscription()
const initiateTriggerOAuth = useInitiateTriggerOAuth()
const invalidateSubscriptions = useInvalidateTriggerSubscriptions()
const currCollection = useMemo(() => {
return buildInTools.find(item => canFindTool(item.id, data.provider_id))
}, [buildInTools, data.provider_id])
// Convert TriggerSubscription to AuthSubscription format
const authSubscription: AuthSubscription = useMemo(() => {
if (data.type !== BlockEnum.TriggerPlugin) {
return {
id: '',
name: '',
status: 'not_configured',
credentials: {},
}
}
const subscription = subscriptions[0] // Use first subscription if available
if (!subscription) {
return {
id: '',
name: '',
status: 'not_configured',
credentials: {},
}
}
const status = subscription.credential_type === 'unauthorized'
? 'not_configured'
: 'authorized'
return {
id: subscription.id,
name: subscription.name,
status,
credentials: subscription.credentials,
}
}, [data.type, subscriptions])
const handleConfigure = useCallback(async () => {
if (!provider) return
try {
// Directly initiate OAuth flow, backend will automatically create subscription builder
const response = await initiateTriggerOAuth.mutateAsync(provider)
if (response.authorization_url) {
// Open OAuth authorization window
const authWindow = window.open(response.authorization_url, 'oauth_authorization', 'width=600,height=600')
// Monitor window closure and refresh subscription list
const checkClosed = setInterval(() => {
if (authWindow?.closed) {
clearInterval(checkClosed)
invalidateSubscriptions(provider)
}
}, 1000)
}
}
catch (error: any) {
notify({
type: 'error',
message: `Failed to configure authentication: ${error.message}`,
})
}
}, [provider, initiateTriggerOAuth, invalidateSubscriptions, notify])
const handleRemove = useCallback(() => {
if (authSubscription.id)
deleteSubscription.mutate(authSubscription.id)
}, [authSubscription.id, deleteSubscription])
// Tool authentication
if (data.type === BlockEnum.Tool && currCollection?.allow_delete) {
return (
<AuthorizedInNode
pluginPayload={{
provider: currCollection?.name || '',
category: AuthCategory.tool,
}}
onAuthorizationItemClick={onAuthorizationChange}
credentialId={data.credential_id}
/>
)
}
// Trigger Plugin authentication
if (data.type === BlockEnum.TriggerPlugin) {
return (
<AuthenticationMenu
subscription={authSubscription}
onConfigure={handleConfigure}
onRemove={handleRemove}
/>
)
}
// No authentication needed
return null
}
export default memo(NodeAuth)

View File

@ -0,0 +1,144 @@
'use client'
import type { FC } from 'react'
import { memo, useCallback, useState } from 'react'
import { useTranslation } from 'react-i18next'
import { RiArrowDownSLine } from '@remixicon/react'
import Button from '@/app/components/base/button'
import Indicator from '@/app/components/header/indicator'
import cn from '@/utils/classnames'
export type AuthenticationStatus = 'authorized' | 'not_configured' | 'error'
export type AuthSubscription = {
id: string
name: string
status: AuthenticationStatus
credentials?: Record<string, any>
}
type AuthenticationMenuProps = {
subscription?: AuthSubscription
onConfigure: () => void
onRemove: () => void
className?: string
}
const AuthenticationMenu: FC<AuthenticationMenuProps> = ({
subscription,
onConfigure,
onRemove,
className,
}) => {
const { t } = useTranslation()
const [isOpen, setIsOpen] = useState(false)
const getStatusConfig = useCallback(() => {
if (!subscription) {
return {
label: t('workflow.nodes.triggerPlugin.notConfigured'),
color: 'red' as const,
}
}
switch (subscription.status) {
case 'authorized':
return {
label: t('workflow.nodes.triggerPlugin.authorized'),
color: 'green' as const,
}
case 'error':
return {
label: t('workflow.nodes.triggerPlugin.error'),
color: 'red' as const,
}
default:
return {
label: t('workflow.nodes.triggerPlugin.notConfigured'),
color: 'red' as const,
}
}
}, [subscription, t])
const statusConfig = getStatusConfig()
const handleConfigure = useCallback(() => {
onConfigure()
setIsOpen(false)
}, [onConfigure])
const handleRemove = useCallback(() => {
onRemove()
setIsOpen(false)
}, [onRemove])
return (
<div className={cn('relative', className)}>
<Button
size='small'
variant='ghost'
className={cn(
'h-6 px-1.5 py-1',
'hover:bg-components-button-ghost-bg-hover',
isOpen && 'bg-components-button-ghost-bg-hover',
)}
onClick={() => setIsOpen(!isOpen)}
>
<Indicator
className='mr-1.5'
color={statusConfig.color}
/>
<span className="text-xs font-medium text-components-button-ghost-text">
{statusConfig.label}
</span>
<RiArrowDownSLine
className='ml-1 h-3.5 w-3.5 text-components-button-ghost-text'
/>
</Button>
{isOpen && (
<>
{/* Backdrop */}
<div
className="fixed inset-0 z-20"
onClick={() => setIsOpen(false)}
/>
{/* Dropdown Menu */}
<div className={cn(
'absolute right-0 z-30 mt-1',
'w-[136px] rounded-xl border-[0.5px] border-components-panel-border',
'bg-components-panel-bg-blur shadow-lg backdrop-blur-sm',
)}>
<div className="py-1">
<button
className={cn(
'block w-full px-4 py-2 text-left text-sm',
'text-text-secondary hover:bg-state-base-hover',
'mx-1 rounded-lg',
)}
onClick={handleConfigure}
>
{t('workflow.nodes.triggerPlugin.configuration')}
</button>
{subscription && subscription.status === 'authorized' && (
<button
className={cn(
'block w-full px-4 py-2 text-left text-sm',
'text-text-destructive hover:bg-state-destructive-hover',
'mx-1 rounded-lg',
)}
onClick={handleRemove}
>
{t('workflow.nodes.triggerPlugin.remove')}
</button>
)}
</div>
</div>
</>
)}
</div>
)
}
export default memo(AuthenticationMenu)

View File

@ -6,11 +6,11 @@ import { ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/block
const nodeDefault: NodeDefault<PluginTriggerNodeType> = {
defaultValue: {
plugin_id: '',
plugin_name: '',
tool_name: '',
event_type: '',
config: {},
},
getAvailablePrevNodes(isChatMode: boolean) {
getAvailablePrevNodes(_isChatMode: boolean) {
return []
},
getAvailableNextNodes(isChatMode: boolean) {
@ -19,7 +19,7 @@ const nodeDefault: NodeDefault<PluginTriggerNodeType> = {
: ALL_COMPLETION_AVAILABLE_BLOCKS
return nodes.filter(type => type !== BlockEnum.Start)
},
checkValid(payload: PluginTriggerNodeType, t: any) {
checkValid(_payload: PluginTriggerNodeType, _t: any) {
return {
isValid: true,
errorMessage: '',

View File

@ -2,54 +2,45 @@ import type { FC } from 'react'
import React from 'react'
import type { PluginTriggerNodeType } from './types'
import type { NodeProps } from '@/app/components/workflow/types'
import useConfig from './use-config'
const Node: FC<NodeProps<PluginTriggerNodeType>> = ({
id,
data,
}) => {
const { isAuthenticated } = useConfig(id, data)
const { config = {} } = data
const configKeys = Object.keys(config)
if (!data.plugin_name && configKeys.length === 0)
// Only show config when authenticated and has config values
if (!isAuthenticated || configKeys.length === 0)
return null
return (
<div className="mb-1 px-3 py-1">
{data.plugin_name && (
<div className="mb-1 text-xs font-medium text-gray-700">
{data.plugin_name}
{data.event_type && (
<div className="text-xs text-gray-500">
{data.event_type}
</div>
)}
</div>
)}
{configKeys.length > 0 && (
<div className="space-y-0.5">
{configKeys.map((key, index) => (
<div className="space-y-0.5">
{configKeys.map((key, index) => (
<div
key={index}
className="flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary"
>
<div
key={index}
className="flex h-6 items-center justify-between space-x-1 rounded-md bg-workflow-block-parma-bg px-1 text-xs font-normal text-text-secondary"
title={key}
className="max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary"
>
<div
title={key}
className="max-w-[100px] shrink-0 truncate text-xs font-medium uppercase text-text-tertiary"
>
{key}
</div>
<div
title={String(config[key] || '')}
className="w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary"
>
{typeof config[key] === 'string' && config[key].includes('secret')
? '********'
: String(config[key] || '')}
</div>
{key}
</div>
))}
</div>
)}
<div
title={String(config[key] || '')}
className="w-0 shrink-0 grow truncate text-right text-xs font-normal text-text-secondary"
>
{typeof config[key] === 'string' && config[key].includes('secret')
? '********'
: String(config[key] || '')}
</div>
</div>
))}
</div>
</div>
)
}

View File

@ -1,37 +1,85 @@
import type { FC } from 'react'
import React from 'react'
import type { PluginTriggerNodeType } from './types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
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 ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types'
const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
id,
data,
}) => {
const {
readOnly,
triggerParameterSchema,
triggerParameterValue,
setTriggerParameterValue,
outputSchema,
hasObjectOutput,
isAuthenticated,
} = useConfig(id, data)
// Convert output schema to VarItem format
const outputVars = Object.entries(outputSchema.properties || {}).map(([name, schema]: [string, any]) => ({
name,
type: schema.type || 'string',
description: schema.description || '',
}))
return (
<div className='mt-2'>
<div className='space-y-4 px-4 pb-2'>
<Field title="Plugin Trigger">
{data.plugin_name ? (
<div className="space-y-2">
<div className="flex items-center space-x-2">
<span className="text-sm font-medium">{data.plugin_name}</span>
{data.event_type && (
<span className="rounded bg-blue-100 px-2 py-1 text-xs text-blue-800">
{data.event_type}
</span>
)}
</div>
<div className="text-xs text-gray-500">
Plugin trigger configured
</div>
{/* Dynamic Parameters Form - Only show when authenticated */}
{isAuthenticated && triggerParameterSchema.length > 0 && (
<>
<div className='px-4 pb-4'>
<ToolForm
readOnly={readOnly}
nodeId={id}
schema={triggerParameterSchema as any}
value={triggerParameterValue}
onChange={setTriggerParameterValue}
/>
</div>
<Split />
</>
)}
{/* Output Variables - Always show */}
<OutputVars>
<>
{outputVars.map(varItem => (
<VarItem
key={varItem.name}
name={varItem.name}
type={varItem.type}
description={varItem.description}
isIndent={hasObjectOutput}
/>
))}
{Object.entries(outputSchema.properties || {}).map(([name, schema]: [string, any]) => (
<div key={name}>
{schema.type === 'object' ? (
<StructureOutputItem
rootClassName='code-sm-semibold text-text-secondary'
payload={{
schema: {
type: Type.object,
properties: {
[name]: schema,
},
additionalProperties: false,
},
}}
/>
) : null}
</div>
) : (
<div className="text-sm text-gray-500">
No plugin selected. Configure this trigger in the workflow canvas.
</div>
)}
</Field>
</div>
))}
</>
</OutputVars>
</div>
)
}

View File

@ -3,7 +3,7 @@ import type { CollectionType } from '@/app/components/tools/types'
export type PluginTriggerNodeType = CommonNodeType & {
plugin_id?: string
plugin_name?: string
tool_name?: string
event_type?: string
config?: Record<string, any>
provider_id?: string

View File

@ -0,0 +1,122 @@
import { useCallback, useMemo } from 'react'
import produce from 'immer'
import type { PluginTriggerNodeType } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import { useAllTriggerPlugins, useTriggerSubscriptions } from '@/service/use-triggers'
import {
addDefaultValue,
toolParametersToFormSchemas,
} from '@/app/components/tools/utils/to-form-schema'
import type { InputVar } from '@/app/components/workflow/types'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
import type { Tool } from '@/app/components/tools/types'
const useConfig = (id: string, payload: PluginTriggerNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { data: triggerPlugins = [] } = useAllTriggerPlugins()
const { inputs, setInputs: doSetInputs } = useNodeCrud<PluginTriggerNodeType>(id, payload)
const { provider_id, provider_name, tool_name, config } = inputs
// Construct provider for authentication check
const authProvider = useMemo(() => {
if (provider_id && provider_name)
return `${provider_id}/${provider_name}`
return provider_id || ''
}, [provider_id, provider_name])
const { data: subscriptions = [] } = useTriggerSubscriptions(
authProvider,
!!authProvider,
)
const currentProvider = useMemo<TriggerWithProvider | undefined>(() => {
return triggerPlugins.find(provider =>
provider.name === provider_name
|| provider.id === provider_id
|| (provider_id && provider.plugin_id === provider_id),
)
}, [triggerPlugins, provider_name, provider_id])
const currentTrigger = useMemo<Tool | undefined>(() => {
return currentProvider?.tools.find(tool => tool.name === tool_name)
}, [currentProvider, tool_name])
// Dynamic subscription parameters (from subscription_schema.parameters_schema)
const subscriptionParameterSchema = useMemo(() => {
if (!currentProvider?.subscription_schema?.parameters_schema) return []
return toolParametersToFormSchemas(currentProvider.subscription_schema.parameters_schema as any)
}, [currentProvider])
// Dynamic trigger parameters (from specific trigger.parameters)
const triggerSpecificParameterSchema = useMemo(() => {
if (!currentTrigger) return []
return toolParametersToFormSchemas(currentTrigger.parameters)
}, [currentTrigger])
// Combined parameter schema (subscription + trigger specific)
const triggerParameterSchema = useMemo(() => {
return [...subscriptionParameterSchema, ...triggerSpecificParameterSchema]
}, [subscriptionParameterSchema, triggerSpecificParameterSchema])
const triggerParameterValue = useMemo(() => {
if (!triggerParameterSchema.length) return {}
return addDefaultValue(config || {}, triggerParameterSchema)
}, [triggerParameterSchema, config])
const setTriggerParameterValue = useCallback((value: Record<string, any>) => {
const newInputs = produce(inputs, (draft) => {
draft.config = value
})
doSetInputs(newInputs)
}, [inputs, doSetInputs])
const setInputVar = useCallback((variable: InputVar, varDetail: InputVar) => {
const newInputs = produce(inputs, (draft) => {
draft.config = {
...draft.config,
[variable.variable]: varDetail.variable,
}
})
doSetInputs(newInputs)
}, [inputs, doSetInputs])
// Get output schema
const outputSchema = useMemo(() => {
return currentTrigger?.output_schema || {}
}, [currentTrigger])
// Check if trigger has complex output structure
const hasObjectOutput = useMemo(() => {
const properties = outputSchema.properties || {}
return Object.values(properties).some((prop: any) => prop.type === 'object')
}, [outputSchema])
// Authentication status check
const isAuthenticated = useMemo(() => {
if (!subscriptions.length) return false
const subscription = subscriptions[0]
return subscription.credential_type !== 'unauthorized'
}, [subscriptions])
const showAuthRequired = !isAuthenticated && !!currentProvider
return {
readOnly,
inputs,
currentProvider,
currentTrigger,
triggerParameterSchema,
triggerParameterValue,
setTriggerParameterValue,
setInputVar,
outputSchema,
hasObjectOutput,
isAuthenticated,
showAuthRequired,
}
}
export default useConfig

View File

@ -724,6 +724,13 @@ const translation = {
json: 'tool generated json',
},
},
triggerPlugin: {
authorized: 'Authorized',
notConfigured: 'Not Configured',
error: 'Error',
configuration: 'Configuration',
remove: 'Remove',
},
questionClassifiers: {
model: 'model',
inputVars: 'Input Variables',

View File

@ -1034,6 +1034,13 @@ const translation = {
invalidParameterType: 'パラメータ"{{name}}"の無効なパラメータタイプ"{{type}}"です',
},
},
triggerPlugin: {
authorized: '認可された',
notConfigured: '設定されていません',
error: 'エラー',
configuration: '構成',
remove: '削除する',
},
},
tracing: {
stopBy: '{{user}}によって停止',

View File

@ -1034,6 +1034,13 @@ const translation = {
invalidParameterType: '参数"{{name}}"的参数类型"{{type}}"无效',
},
},
triggerPlugin: {
authorized: '已授权',
notConfigured: '未配置',
error: '错误',
configuration: '配置',
remove: '移除',
},
},
tracing: {
stopBy: '由{{user}}终止',

View File

@ -1,10 +1,20 @@
import { useQuery } from '@tanstack/react-query'
import { get } from './base'
import type { TriggerProviderApiEntity, TriggerWithProvider } from '@/app/components/workflow/block-selector/types'
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { del, get, post } from './base'
import type {
TriggerOAuthClientParams,
TriggerOAuthConfig,
TriggerProviderApiEntity,
TriggerSubscription,
TriggerSubscriptionBuilder,
TriggerWithProvider,
} from '@/app/components/workflow/block-selector/types'
import { CollectionType } from '@/app/components/tools/types'
import { useInvalid } from './use-base'
const NAME_SPACE = 'triggers'
// Trigger Provider Service - Provider ID Format: plugin_id/provider_name
// Convert backend API response to frontend ToolWithProvider format
const convertToTriggerWithProvider = (provider: TriggerProviderApiEntity): TriggerWithProvider => {
return {
@ -37,19 +47,26 @@ const convertToTriggerWithProvider = (provider: TriggerProviderApiEntity): Trigg
llm_description: JSON.stringify(param.description || {}),
required: param.required || false,
default: param.default || '',
options: [],
options: param.options?.map(option => ({
label: option.label,
value: option.value,
})) || [],
})),
labels: provider.tags || [],
output_schema: trigger.output_schema || {},
})),
// Trigger-specific schema fields
credentials_schema: provider.credentials_schema,
oauth_client_schema: provider.oauth_client_schema,
subscription_schema: provider.subscription_schema,
meta: {
version: '1.0',
},
}
}
// Main hook - follows exact same pattern as tools
export const useAllTriggerPlugins = (enabled = true) => {
return useQuery<TriggerWithProvider[]>({
queryKey: [NAME_SPACE, 'all'],
@ -61,7 +78,6 @@ export const useAllTriggerPlugins = (enabled = true) => {
})
}
// Additional hook for consistency with tools pattern
export const useTriggerPluginsByType = (triggerType: string, enabled = true) => {
return useQuery<TriggerWithProvider[]>({
queryKey: [NAME_SPACE, 'byType', triggerType],
@ -72,3 +88,200 @@ export const useTriggerPluginsByType = (triggerType: string, enabled = true) =>
enabled: enabled && !!triggerType,
})
}
export const useInvalidateAllTriggerPlugins = () => {
return useInvalid([NAME_SPACE, 'all'])
}
// ===== Trigger Subscriptions Management =====
export const useTriggerSubscriptions = (provider: string, enabled = true) => {
return useQuery<TriggerSubscription[]>({
queryKey: [NAME_SPACE, 'subscriptions', provider],
queryFn: () => get<TriggerSubscription[]>(`/workspaces/current/trigger-provider/${provider}/subscriptions/list`),
enabled: enabled && !!provider,
})
}
export const useInvalidateTriggerSubscriptions = () => {
const queryClient = useQueryClient()
return (provider: string) => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'subscriptions', provider],
})
}
}
export const useCreateTriggerSubscriptionBuilder = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'create-subscription-builder'],
mutationFn: (payload: {
provider: string
name?: string
credentials?: Record<string, any>
}) => {
const { provider, ...body } = payload
return post<{ subscription_builder: TriggerSubscriptionBuilder }>(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/create`,
{ body },
)
},
})
}
export const useUpdateTriggerSubscriptionBuilder = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-subscription-builder'],
mutationFn: (payload: {
provider: string
subscriptionBuilderId: string
name?: string
parameters?: Record<string, any>
properties?: Record<string, any>
credentials?: Record<string, any>
}) => {
const { provider, subscriptionBuilderId, ...body } = payload
return post<TriggerSubscriptionBuilder>(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/update/${subscriptionBuilderId}`,
{ body },
)
},
})
}
export const useVerifyTriggerSubscriptionBuilder = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'verify-subscription-builder'],
mutationFn: (payload: {
provider: string
subscriptionBuilderId: string
}) => {
const { provider, subscriptionBuilderId } = payload
return post(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`,
)
},
})
}
export const useBuildTriggerSubscription = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'build-subscription'],
mutationFn: (payload: {
provider: string
subscriptionBuilderId: string
}) => {
const { provider, subscriptionBuilderId } = payload
return post(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/build/${subscriptionBuilderId}`,
)
},
})
}
export const useDeleteTriggerSubscription = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete-subscription'],
mutationFn: (subscriptionId: string) => {
return post<{ result: string }>(
`/workspaces/current/trigger-provider/${subscriptionId}/subscriptions/delete`,
)
},
})
}
export const useTriggerSubscriptionBuilderLogs = (
provider: string,
subscriptionBuilderId: string,
options: {
enabled?: boolean
refetchInterval?: number | false
} = {},
) => {
const { enabled = true, refetchInterval = false } = options
return useQuery<Record<string, any>[]>({
queryKey: [NAME_SPACE, 'subscription-builder-logs', provider, subscriptionBuilderId],
queryFn: () => get(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/logs/${subscriptionBuilderId}`,
),
enabled: enabled && !!provider && !!subscriptionBuilderId,
refetchInterval,
})
}
// ===== OAuth Management =====
export const useTriggerOAuthConfig = (provider: string, enabled = true) => {
return useQuery<TriggerOAuthConfig>({
queryKey: [NAME_SPACE, 'oauth-config', provider],
queryFn: () => get<TriggerOAuthConfig>(`/workspaces/current/trigger-provider/${provider}/oauth/client`),
enabled: enabled && !!provider,
})
}
export const useConfigureTriggerOAuth = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'configure-oauth'],
mutationFn: (payload: {
provider: string
client_params: TriggerOAuthClientParams
enabled: boolean
}) => {
const { provider, ...body } = payload
return post<{ result: string }>(
`/workspaces/current/trigger-provider/${provider}/oauth/client`,
{ body },
)
},
})
}
export const useDeleteTriggerOAuth = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'delete-oauth'],
mutationFn: (provider: string) => {
return del<{ result: string }>(
`/workspaces/current/trigger-provider/${provider}/oauth/client`,
)
},
})
}
export const useInitiateTriggerOAuth = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'initiate-oauth'],
mutationFn: (provider: string) => {
return get<{ authorization_url: string; subscription_builder: any }>(
`/workspaces/current/trigger-provider/${provider}/subscriptions/oauth/authorize`,
)
},
})
}
// ===== Dynamic Options Support =====
export const useTriggerPluginDynamicOptions = (payload: {
plugin_id: string
provider: string
action: string
parameter: string
extra?: Record<string, any>
}, enabled = true) => {
return useQuery<{ options: Array<{ value: string; label: any }> }>({
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 },
),
enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter,
})
}
// ===== Cache Invalidation Helpers =====
export const useInvalidateTriggerOAuthConfig = () => {
const queryClient = useQueryClient()
return (provider: string) => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'oauth-config', provider],
})
}
}