feat(workflow): Plugin Trigger Node with Unified Entry Node System (#24205)

This commit is contained in:
lyzno1 2025-08-20 23:49:10 +08:00 committed by GitHub
parent 6eaea64b3f
commit 833c902b2b
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
19 changed files with 518 additions and 57 deletions

View File

@ -108,7 +108,7 @@ const BlockIcon: FC<BlockIconProps> = ({
`}
>
{
type !== BlockEnum.Tool && (
type !== BlockEnum.Tool && type !== BlockEnum.TriggerPlugin && (
getIcon(type,
(type === BlockEnum.TriggerSchedule || type === BlockEnum.TriggerWebhook)
? (size === 'xs' ? 'w-4 h-4' : 'w-4.5 h-4.5')
@ -117,7 +117,7 @@ const BlockIcon: FC<BlockIconProps> = ({
)
}
{
type === BlockEnum.Tool && toolIcon && (
(type === BlockEnum.Tool || type === BlockEnum.TriggerPlugin) && toolIcon && (
<>
{
typeof toolIcon === 'string'

View File

@ -0,0 +1,61 @@
'use client'
import { useRef } from 'react'
import { useTranslation } from 'react-i18next'
import type { BlockEnum } from '../types'
import type { ToolDefaultValue } from './types'
import StartBlocks from './start-blocks'
import TriggerPluginSelector from './trigger-plugin-selector'
import { ENTRY_NODE_TYPES } from './constants'
import cn from '@/utils/classnames'
import Link from 'next/link'
import { RiArrowRightUpLine } from '@remixicon/react'
import { getMarketplaceUrl } from '@/utils/var'
type AllStartBlocksProps = {
className?: string
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
}
const AllStartBlocks = ({
className,
searchText,
onSelect,
availableBlocksTypes,
}: AllStartBlocksProps) => {
const { t } = useTranslation()
const wrapElemRef = useRef<HTMLDivElement>(null)
return (
<div className={cn('min-w-[400px] max-w-[500px]', className)}>
<div
ref={wrapElemRef}
className='max-h-[464px] overflow-y-auto'
>
<StartBlocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={ENTRY_NODE_TYPES as BlockEnum[]}
/>
<TriggerPluginSelector
onSelect={onSelect}
searchText={searchText}
/>
</div>
{/* Footer - Same as Tools tab marketplace footer */}
<Link
className='system-sm-medium sticky bottom-0 z-10 flex h-8 cursor-pointer items-center rounded-b-lg border-[0.5px] border-t border-components-panel-border bg-components-panel-bg-blur px-4 py-1 text-text-accent-light-mode-only shadow-lg'
href={getMarketplaceUrl('')}
target='_blank'
>
<span>{t('plugin.findMoreInMarketplace')}</span>
<RiArrowRightUpLine className='ml-0.5 h-3 w-3' />
</Link>
</div>
)
}
export default AllStartBlocks

View File

@ -21,14 +21,16 @@ export const START_BLOCKS: Block[] = [
title: 'Webhook Trigger',
description: 'HTTP callback trigger',
},
{
classification: BlockClassificationEnum.Default,
type: BlockEnum.TriggerPlugin,
title: 'Plugin Trigger',
description: 'Third-party integration trigger',
},
]
// Entry node types that can start a workflow
export const ENTRY_NODE_TYPES = [
BlockEnum.Start,
BlockEnum.TriggerSchedule,
BlockEnum.TriggerWebhook,
BlockEnum.TriggerPlugin,
] as const
export const BLOCKS: Block[] = [
{
classification: BlockClassificationEnum.Default,

View File

@ -93,7 +93,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
}, [])
const searchPlaceholder = useMemo(() => {
if (activeTab === TabsEnum.Start)
return t('workflow.tabs.searchBlock')
return t('workflow.tabs.searchTrigger')
if (activeTab === TabsEnum.Blocks)
return t('workflow.tabs.searchBlock')
if (activeTab === TabsEnum.Tools)
@ -137,7 +137,7 @@ const NodeSelector: FC<NodeSelectorProps> = ({
onActiveTabChange={handleActiveTabChange}
filterElem={
<div className='relative m-2' onClick={e => e.stopPropagation()}>
{activeTab === TabsEnum.Blocks && (
{(activeTab === TabsEnum.Start || activeTab === TabsEnum.Blocks) && (
<Input
showLeftIcon
showClearIcon

View File

@ -6,6 +6,7 @@ import {
import { useTranslation } from 'react-i18next'
import BlockIcon from '../block-icon'
import type { BlockEnum } from '../types'
import { BlockEnum as BlockEnumValues } from '../types'
import { useNodesExtraData } from '../hooks'
import { START_BLOCKS } from './constants'
import type { ToolDefaultValue } from './types'
@ -27,8 +28,12 @@ const StartBlocks = ({
const filteredBlocks = useMemo(() => {
return START_BLOCKS.filter((block) => {
return block.title.toLowerCase().includes(searchText.toLowerCase())
&& availableBlocksTypes.includes(block.type)
// Filter by search text
if (!block.title.toLowerCase().includes(searchText.toLowerCase()))
return false
// availableBlocksTypes now contains properly filtered entry node types from parent
return availableBlocksTypes.includes(block.type)
})
}, [searchText, availableBlocksTypes])
@ -60,13 +65,18 @@ const StartBlocks = ({
className='mr-2 shrink-0'
type={block.type}
/>
<div className='grow text-sm text-text-secondary'>{block.title}</div>
<div className='flex w-0 grow items-center justify-between text-sm text-text-secondary'>
<span className='truncate'>{block.title}</span>
{block.type === BlockEnumValues.Start && (
<span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{t('workflow.blocks.originalStartNode')}</span>
)}
</div>
</div>
</Tooltip>
), [nodesExtraData, onSelect])
), [nodesExtraData, onSelect, t])
return (
<div className='p-1'>
<div className='min-w-[400px] max-w-[500px] p-1'>
{isEmpty && (
<div className='flex h-[22px] items-center px-3 text-xs font-medium text-text-tertiary'>
{t('workflow.tabs.noResult')}
@ -74,7 +84,16 @@ const StartBlocks = ({
)}
{!isEmpty && (
<div className='mb-1'>
{filteredBlocks.map(renderBlock)}
{filteredBlocks.map((block, index) => (
<div key={block.type}>
{renderBlock(block)}
{block.type === BlockEnumValues.Start && index < filteredBlocks.length - 1 && (
<div className='my-1 px-3'>
<div className='border-t border-divider-subtle' />
</div>
)}
</div>
))}
</div>
)}
</div>

View File

@ -6,7 +6,7 @@ import { useTabs } from './hooks'
import type { ToolDefaultValue } from './types'
import { TabsEnum } from './types'
import Blocks from './blocks'
import StartBlocks from './start-blocks'
import AllStartBlocks from './all-start-blocks'
import AllTools from './all-tools'
import cn from '@/utils/classnames'
@ -66,7 +66,7 @@ const Tabs: FC<TabsProps> = ({
{
activeTab === TabsEnum.Start && !noBlocks && (
<div className='border-t border-divider-subtle'>
<StartBlocks
<AllStartBlocks
searchText={searchText}
onSelect={onSelect}
availableBlocksTypes={availableBlocksTypes}

View File

@ -0,0 +1,24 @@
'use client'
import { memo } from 'react'
import TriggerPluginList from './trigger-plugin/list'
import type { BlockEnum } from '../types'
import type { ToolDefaultValue } from './types'
type TriggerPluginSelectorProps = {
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
searchText: string
}
const TriggerPluginSelector = ({
onSelect,
searchText,
}: TriggerPluginSelectorProps) => {
return (
<TriggerPluginList
onSelect={onSelect}
searchText={searchText}
/>
)
}
export default memo(TriggerPluginSelector)

View File

@ -0,0 +1,88 @@
'use client'
import type { FC } from 'react'
import React from 'react'
import type { ToolWithProvider } from '../../types'
import { BlockEnum } from '../../types'
import type { ToolDefaultValue } from '../types'
import Tooltip from '@/app/components/base/tooltip'
import type { Tool } from '@/app/components/tools/types'
import { useGetLanguage } from '@/context/i18n'
import BlockIcon from '../../block-icon'
import cn from '@/utils/classnames'
import { useTranslation } from 'react-i18next'
type Props = {
provider: ToolWithProvider
payload: Tool
disabled?: boolean
isAdded?: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const TriggerPluginActionItem: FC<Props> = ({
provider,
payload,
onSelect,
disabled,
isAdded,
}) => {
const { t } = useTranslation()
const language = useGetLanguage()
return (
<Tooltip
key={payload.name}
position='right'
needsDelay={false}
popupClassName='!p-0 !px-3 !py-2.5 !w-[200px] !leading-[18px] !text-xs !text-gray-700 !border-[0.5px] !border-black/5 !rounded-xl !shadow-lg'
popupContent={(
<div>
<BlockIcon
size='md'
className='mb-2'
type={BlockEnum.TriggerPlugin}
toolIcon={provider.icon}
/>
<div className='mb-1 text-sm leading-5 text-text-primary'>{payload.label[language]}</div>
<div className='text-xs leading-[18px] text-text-secondary'>{payload.description[language]}</div>
</div>
)}
>
<div
key={payload.name}
className='flex cursor-pointer items-center justify-between rounded-lg pl-[21px] pr-1 hover:bg-state-base-hover'
onClick={() => {
if (disabled) return
const params: Record<string, string> = {}
if (payload.parameters) {
payload.parameters.forEach((item) => {
params[item.name] = ''
})
}
onSelect(BlockEnum.TriggerPlugin, {
provider_id: provider.id,
provider_type: provider.type,
provider_name: provider.name,
tool_name: payload.name,
tool_label: payload.label[language],
tool_description: payload.description[language],
title: payload.label[language],
is_team_authorization: provider.is_team_authorization,
output_schema: payload.output_schema,
paramSchemas: payload.parameters,
params,
meta: provider.meta,
})
}}
>
<div className={cn('system-sm-medium h-8 truncate border-l-2 border-divider-subtle pl-4 leading-8 text-text-secondary')}>
<span className={cn(disabled && 'opacity-30')}>{payload.label[language]}</span>
</div>
{isAdded && (
<div className='system-xs-regular mr-4 text-text-tertiary'>{t('tools.addToolModal.added')}</div>
)}
</div>
</Tooltip >
)
}
export default React.memo(TriggerPluginActionItem)

View File

@ -0,0 +1,132 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useMemo, useRef } from 'react'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useGetLanguage } from '@/context/i18n'
import { CollectionType } from '../../../tools/types'
import type { ToolWithProvider } from '../../types'
import { BlockEnum } from '../../types'
import type { ToolDefaultValue } from '../types'
import TriggerPluginActionItem from './action-item'
import BlockIcon from '../../block-icon'
import { useTranslation } from 'react-i18next'
type Props = {
className?: string
payload: ToolWithProvider
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
}
const TriggerPluginItem: FC<Props> = ({
className,
payload,
hasSearchText,
onSelect,
}) => {
const { t } = useTranslation()
const language = useGetLanguage()
const notShowProvider = payload.type === CollectionType.workflow
const actions = payload.tools
const hasAction = !notShowProvider
const [isFold, setFold] = React.useState<boolean>(true)
const ref = useRef(null)
useEffect(() => {
if (hasSearchText && isFold) {
setFold(false)
return
}
if (!hasSearchText && !isFold)
setFold(true)
}, [hasSearchText])
const FoldIcon = isFold ? RiArrowRightSLine : RiArrowDownSLine
const groupName = useMemo(() => {
if (payload.type === CollectionType.builtIn)
return payload.author
if (payload.type === CollectionType.custom)
return t('workflow.tabs.customTool')
if (payload.type === CollectionType.workflow)
return t('workflow.tabs.workflowTool')
return ''
}, [payload.author, payload.type, t])
return (
<div
key={payload.id}
className={cn('mb-1 last-of-type:mb-0')}
ref={ref}
>
<div className={cn(className)}>
<div
className='group/item flex w-full cursor-pointer select-none items-center justify-between rounded-lg pl-3 pr-1 hover:bg-state-base-hover'
onClick={() => {
if (hasAction) {
setFold(!isFold)
return
}
const tool = actions[0]
const params: Record<string, string> = {}
if (tool.parameters) {
tool.parameters.forEach((item) => {
params[item.name] = ''
})
}
onSelect(BlockEnum.TriggerPlugin, {
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
tool_name: tool.name,
tool_label: tool.label[language],
tool_description: tool.description[language],
title: tool.label[language],
is_team_authorization: payload.is_team_authorization,
output_schema: tool.output_schema,
paramSchemas: tool.parameters,
params,
})
}}
>
<div className='flex h-8 grow items-center'>
<BlockIcon
className='shrink-0'
type={BlockEnum.TriggerPlugin}
toolIcon={payload.icon}
/>
<div className='ml-2 flex w-0 grow items-center text-sm text-text-primary'>
<span className='max-w-[250px] truncate'>{notShowProvider ? actions[0]?.label[language] : payload.label[language]}</span>
<span className='system-xs-regular ml-2 shrink-0 text-text-quaternary'>{groupName}</span>
</div>
</div>
<div className='ml-2 flex items-center'>
{hasAction && (
<FoldIcon className={cn('h-4 w-4 shrink-0 text-text-tertiary group-hover/item:text-text-tertiary', isFold && 'text-text-quaternary')} />
)}
</div>
</div>
{!notShowProvider && hasAction && !isFold && (
actions.map(action => (
<TriggerPluginActionItem
key={action.name}
provider={payload}
payload={action}
onSelect={onSelect}
disabled={false}
isAdded={false}
/>
))
)}
</div>
</div>
)
}
export default React.memo(TriggerPluginItem)

View File

@ -0,0 +1,51 @@
'use client'
import { memo, useMemo } from 'react'
import { useAllBuiltInTools } from '@/service/use-tools'
import TriggerPluginItem from './item'
import type { BlockEnum } from '../../types'
import type { ToolDefaultValue } from '../types'
import { useGetLanguage } from '@/context/i18n'
type TriggerPluginListProps = {
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
searchText: string
}
const TriggerPluginList = ({
onSelect,
searchText,
}: TriggerPluginListProps) => {
const { data: buildInTools = [] } = useAllBuiltInTools()
const language = useGetLanguage()
const triggerPlugins = useMemo(() => {
return buildInTools.filter((toolWithProvider) => {
if (toolWithProvider.tools.length === 0) return false
if (!searchText) return true
return toolWithProvider.name.toLowerCase().includes(searchText.toLowerCase())
|| toolWithProvider.tools.some(tool =>
tool.label[language].toLowerCase().includes(searchText.toLowerCase()),
)
})
}, [buildInTools, searchText, language])
if (!triggerPlugins.length)
return null
return (
<div className="p-1">
{triggerPlugins.map(plugin => (
<TriggerPluginItem
key={plugin.id}
payload={plugin}
onSelect={onSelect}
hasSearchText={!!searchText}
/>
))}
</div>
)
}
export default memo(TriggerPluginList)

View File

@ -18,6 +18,7 @@ import {
} from 'reactflow'
import { unionBy } from 'lodash-es'
import type { ToolDefaultValue } from '../block-selector/types'
import { ENTRY_NODE_TYPES } from '../block-selector/constants'
import type {
Edge,
Node,
@ -63,6 +64,24 @@ import { WorkflowHistoryEvent, useWorkflowHistory } from './use-workflow-history
import useInspectVarsCrud from './use-inspect-vars-crud'
import { getNodeUsedVars } from '../nodes/_base/components/variable/utils'
// Helper function to check if a node is an entry node
const isEntryNode = (nodeType: BlockEnum): boolean => {
return ENTRY_NODE_TYPES.includes(nodeType as any)
}
// Helper function to check if entry node can be deleted
const canDeleteEntryNode = (nodes: Node[], nodeId: string): boolean => {
const targetNode = nodes.find(node => node.id === nodeId)
if (!targetNode || !isEntryNode(targetNode.data.type))
return true // Non-entry nodes can always be deleted
// Count all entry nodes
const entryNodes = nodes.filter(node => isEntryNode(node.data.type))
// Can delete if there's more than one entry node
return entryNodes.length > 1
}
export const useNodesInteractions = () => {
const { t } = useTranslation()
const store = useStoreApi()
@ -548,15 +567,17 @@ export const useNodesInteractions = () => {
} = store.getState()
const nodes = getNodes()
// Check if entry node can be deleted (must keep at least one entry node)
if (!canDeleteEntryNode(nodes, nodeId))
return // Cannot delete the last entry node
const currentNodeIndex = nodes.findIndex(node => node.id === nodeId)
const currentNode = nodes[currentNodeIndex]
if (!currentNode)
return
if (currentNode.data.type === BlockEnum.Start)
return
deleteNodeInspectorVars(nodeId)
if (currentNode.data.type === BlockEnum.Iteration) {
const iterationChildren = nodes.filter(node => node.parentId === currentNode.id)
@ -1388,7 +1409,9 @@ export const useNodesInteractions = () => {
} = store.getState()
const nodes = getNodes()
const bundledNodes = nodes.filter(node => node.data._isBundled && node.data.type !== BlockEnum.Start)
const bundledNodes = nodes.filter(node =>
node.data._isBundled && canDeleteEntryNode(nodes, node.id),
)
if (bundledNodes.length) {
bundledNodes.forEach(node => handleNodeDelete(node.id))
@ -1400,7 +1423,9 @@ export const useNodesInteractions = () => {
if (edgeSelected)
return
const selectedNode = nodes.find(node => node.data.selected && node.data.type !== BlockEnum.Start)
const selectedNode = nodes.find(node =>
node.data.selected && canDeleteEntryNode(nodes, node.id),
)
if (selectedNode)
handleNodeDelete(selectedNode.id)

View File

@ -506,7 +506,7 @@ export const useToolIcon = (data: Node['data']) => {
const toolIcon = useMemo(() => {
if (!data)
return ''
if (data.type === BlockEnum.Tool) {
if (data.type === BlockEnum.Tool || data.type === BlockEnum.TriggerPlugin) {
let targetTools = workflowTools
if (data.provider_type === CollectionType.builtIn)
targetTools = buildInTools

View File

@ -1,24 +1,53 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { PluginTriggerNodeType } from './types'
import type { NodeProps } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.triggerPlugin'
const Node: FC<NodeProps<PluginTriggerNodeType>> = ({
data,
}) => {
const { t } = useTranslation()
const { config = {} } = data
const configKeys = Object.keys(config)
if (!data.plugin_name && configKeys.length === 0)
return null
return (
<div className="mb-1 px-3 py-1">
<div className="text-xs text-gray-700">
{t(`${i18nPrefix}.nodeTitle`)}
</div>
{data.plugin_name && (
<div className="text-xs text-gray-500">
<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
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
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>
</div>
))}
</div>
)}
</div>

View File

@ -1,25 +1,35 @@
import type { FC } from 'react'
import React from 'react'
import { useTranslation } from 'react-i18next'
import type { PluginTriggerNodeType } from './types'
import Field from '@/app/components/workflow/nodes/_base/components/field'
import type { NodePanelProps } from '@/app/components/workflow/types'
const i18nPrefix = 'workflow.nodes.triggerPlugin'
const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
id,
data,
}) => {
const { t } = useTranslation()
return (
<div className='mt-2'>
<div className='space-y-4 px-4 pb-2'>
<Field title={t(`${i18nPrefix}.title`)}>
<div className="text-sm text-gray-500">
{t(`${i18nPrefix}.configPlaceholder`)}
</div>
<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>
</div>
) : (
<div className="text-sm text-gray-500">
No plugin selected. Configure this trigger in the workflow canvas.
</div>
)}
</Field>
</div>
</div>

View File

@ -1,8 +1,12 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { CollectionType } from '@/app/components/tools/types'
export type PluginTriggerNodeType = CommonNodeType & {
plugin_id?: string
plugin_name?: string
event_type?: string
config?: Record<string, any>
provider_id?: string
provider_type?: CollectionType
provider_name?: string
}

View File

@ -224,6 +224,7 @@ const translation = {
'start': 'Start',
'blocks': 'Nodes',
'searchTool': 'Search tool',
'searchTrigger': 'Search triggers...',
'tools': 'Tools',
'allTool': 'All',
'plugin': 'Plugin',
@ -240,6 +241,7 @@ const translation = {
},
blocks: {
'start': 'User Input',
'originalStartNode': 'original start node',
'end': 'End',
'answer': 'Answer',
'llm': 'LLM',
@ -979,11 +981,6 @@ const translation = {
nodeTitle: '🔗 Webhook Trigger',
configPlaceholder: 'Webhook trigger configuration will be implemented here',
},
triggerPlugin: {
title: 'Plugin Trigger',
nodeTitle: '🔌 Plugin Trigger',
configPlaceholder: 'Plugin trigger configuration will be implemented here',
},
},
triggerStatus: {
enabled: 'TRIGGER',

View File

@ -223,6 +223,7 @@ const translation = {
'searchBlock': 'ブロック検索',
'blocks': 'ブロック',
'searchTool': 'ツール検索',
'searchTrigger': 'トリガー検索...',
'tools': 'ツール',
'allTool': 'すべて',
'customTool': 'カスタム',
@ -240,6 +241,7 @@ const translation = {
},
blocks: {
'start': '開始',
'originalStartNode': '元の開始ノード',
'end': '終了',
'answer': '回答',
'llm': 'LLM',
@ -979,11 +981,6 @@ const translation = {
nodeTitle: '🔗 Webhook トリガー',
configPlaceholder: 'Webhook トリガーの設定がここに実装されます',
},
triggerPlugin: {
title: 'プラグイントリガー',
nodeTitle: '🔌 プラグイントリガー',
configPlaceholder: 'プラグイントリガーの設定がここに実装されます',
},
},
tracing: {
stopBy: '{{user}}によって停止',

View File

@ -223,6 +223,7 @@ const translation = {
'searchBlock': '搜索节点',
'blocks': '节点',
'searchTool': '搜索工具',
'searchTrigger': '搜索触发器...',
'tools': '工具',
'allTool': '全部',
'plugin': '插件',
@ -240,6 +241,7 @@ const translation = {
},
blocks: {
'start': '开始',
'originalStartNode': '原始开始节点',
'end': '结束',
'answer': '直接回复',
'llm': 'LLM',
@ -979,11 +981,6 @@ const translation = {
title: 'Webhook 触发器',
nodeTitle: '🔗 Webhook 触发器',
},
triggerPlugin: {
title: '插件触发器',
nodeTitle: '🔌 插件触发器',
configPlaceholder: '插件触发器配置将在此处实现',
},
},
tracing: {
stopBy: '由{{user}}终止',

View File

@ -0,0 +1,25 @@
import { useQuery } from '@tanstack/react-query'
import { get } from './base'
import type { ToolWithProvider } from '@/app/components/workflow/types'
const NAME_SPACE = 'triggers'
// Get all plugins that support trigger functionality
// TODO: Backend API not implemented yet - replace with actual triggers endpoint
export const useAllTriggerPlugins = (enabled = true) => {
return useQuery<ToolWithProvider[]>({
queryKey: [NAME_SPACE, 'all'],
queryFn: () => get<ToolWithProvider[]>('/workspaces/current/triggers/plugins'),
enabled,
})
}
// Get trigger-capable plugins by type (schedule, webhook, etc.)
// TODO: Backend API not implemented yet - replace with actual triggers endpoint
export const useTriggerPluginsByType = (triggerType: string, enabled = true) => {
return useQuery<ToolWithProvider[]>({
queryKey: [NAME_SPACE, 'byType', triggerType],
queryFn: () => get<ToolWithProvider[]>(`/workspaces/current/triggers/plugins?type=${triggerType}`),
enabled: enabled && !!triggerType,
})
}