mirror of https://github.com/langgenius/dify.git
feat: implement trigger plugin frontend integration (#25283)
This commit is contained in:
parent
814787677a
commit
a91e59d544
|
|
@ -218,3 +218,6 @@ mise.toml
|
|||
.roo/
|
||||
api/.env.backup
|
||||
/clickzetta
|
||||
|
||||
# mcp
|
||||
.serena
|
||||
|
|
@ -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}")
|
||||
|
|
|
|||
|
|
@ -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
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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)
|
||||
|
|
@ -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)
|
||||
|
|
@ -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: '',
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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>
|
||||
)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
@ -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',
|
||||
|
|
|
|||
|
|
@ -1034,6 +1034,13 @@ const translation = {
|
|||
invalidParameterType: 'パラメータ"{{name}}"の無効なパラメータタイプ"{{type}}"です',
|
||||
},
|
||||
},
|
||||
triggerPlugin: {
|
||||
authorized: '認可された',
|
||||
notConfigured: '設定されていません',
|
||||
error: 'エラー',
|
||||
configuration: '構成',
|
||||
remove: '削除する',
|
||||
},
|
||||
},
|
||||
tracing: {
|
||||
stopBy: '{{user}}によって停止',
|
||||
|
|
|
|||
|
|
@ -1034,6 +1034,13 @@ const translation = {
|
|||
invalidParameterType: '参数"{{name}}"的参数类型"{{type}}"无效',
|
||||
},
|
||||
},
|
||||
triggerPlugin: {
|
||||
authorized: '已授权',
|
||||
notConfigured: '未配置',
|
||||
error: '错误',
|
||||
configuration: '配置',
|
||||
remove: '移除',
|
||||
},
|
||||
},
|
||||
tracing: {
|
||||
stopBy: '由{{user}}终止',
|
||||
|
|
|
|||
|
|
@ -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],
|
||||
})
|
||||
}
|
||||
}
|
||||
|
|
|
|||
Loading…
Reference in New Issue