feat(trigger): enhance plugin and trigger integration with updated naming conventions

- Refactored `PluginFetchDynamicSelectOptionsApi` to replace the `extra` argument with `credential_id`, improving clarity in dynamic option fetching.
- Updated `ProviderConfigEncrypter` to rename `mask_tool_credentials` to `mask_credentials` for consistency, and added a new method to maintain backward compatibility.
- Enhanced `PluginParameterService` to utilize `credential_id` for fetching subscriptions, improving the handling of trigger credentials.
- Adjusted various components and types in the frontend to replace `tool_name` with `trigger_name`, ensuring consistency across the application.
- Introduced `multiple` property in `TriggerParameter` to support multi-select functionality.

These changes improve the integration of triggers and plugins, enhance code clarity, and align naming conventions across the codebase.
This commit is contained in:
Harry 2025-09-08 23:14:50 +08:00
parent 01b2f9cff6
commit 2a3ce6baa9
40 changed files with 734 additions and 163 deletions

View File

@ -516,20 +516,20 @@ class PluginFetchDynamicSelectOptionsApi(Resource):
parser.add_argument("provider", type=str, required=True, location="args")
parser.add_argument("action", type=str, required=True, location="args")
parser.add_argument("parameter", type=str, required=True, location="args")
parser.add_argument("extra", type=dict, required=False, location="args")
parser.add_argument("credential_id", type=str, required=False, location="args")
parser.add_argument("provider_type", type=str, required=True, location="args")
args = parser.parse_args()
try:
options = PluginParameterService.get_dynamic_select_options(
tenant_id,
user_id,
args["plugin_id"],
args["provider"],
args["action"],
args["parameter"],
args["extra"],
args["provider_type"],
tenant_id=tenant_id,
user_id=user_id,
plugin_id=args["plugin_id"],
provider=args["provider"],
action=args["action"],
parameter=args["parameter"],
credential_id=args["credential_id"],
provider_type=args["provider_type"],
)
except PluginDaemonClientSideError as e:
raise ValueError(e)

View File

@ -66,9 +66,9 @@ class ProviderConfigEncrypter:
return data
def mask_tool_credentials(self, data: dict[str, Any]) -> dict[str, Any]:
def mask_credentials(self, data: dict[str, Any]) -> dict[str, Any]:
"""
mask tool credentials
mask credentials
return a deep copy of credentials with masked values
"""
@ -91,6 +91,10 @@ class ProviderConfigEncrypter:
return data
def mask_tool_credentials(self, data: dict[str, Any]) -> dict[str, Any]:
return self.mask_credentials(data)
def decrypt(self, data: dict[str, str]) -> dict[str, Any]:
"""
decrypt tool credentials with tenant id

View File

@ -27,9 +27,9 @@ class PluginTriggerManager(BasePluginClient):
def transformer(json_response: dict[str, Any]) -> dict:
for provider in json_response.get("data", []):
declaration = provider.get("declaration", {}) or {}
provider_name = declaration.get("identity", {}).get("name")
provider_id = provider.get("plugin_id") + "/" + provider.get("provider")
for trigger in declaration.get("triggers", []):
trigger["identity"]["provider"] = provider_name
trigger["identity"]["provider"] = provider_id
return json_response
@ -42,10 +42,11 @@ class PluginTriggerManager(BasePluginClient):
)
for provider in response:
provider.declaration.identity.name = str(provider.provider)
provider.declaration.identity.name = f"{provider.plugin_id}/{provider.declaration.identity.name}"
# override the provider name for each trigger to plugin_id/provider_name
for trigger in provider.declaration.triggers:
trigger.identity.provider = str(provider.provider)
trigger.identity.provider = provider.declaration.identity.name
return response

View File

@ -40,6 +40,10 @@ class TriggerParameter(BaseModel):
template: Optional[PluginParameterTemplate] = Field(default=None, description="The template of the parameter")
scope: Optional[str] = None
required: Optional[bool] = False
multiple: bool | None = Field(
default=False,
description="Whether the parameter is multiple select, only valid for select or dynamic-select type",
)
default: Union[int, float, str, list, None] = None
min: Union[float, int, None] = None
max: Union[float, int, None] = None

View File

@ -9,9 +9,13 @@ from core.plugin.entities.plugin_daemon import CredentialType
from core.plugin.impl.dynamic_select import DynamicSelectClient
from core.tools.tool_manager import ToolManager
from core.tools.utils.encryption import create_tool_provider_encrypter
from core.trigger.entities.api_entities import TriggerProviderSubscriptionApiEntity
from core.trigger.entities.entities import SubscriptionBuilder
from core.trigger.trigger_manager import TriggerManager
from extensions.ext_database import db
from models.tools import BuiltinToolProvider
from services.trigger.trigger_provider_service import TriggerProviderService
from services.trigger.trigger_subscription_builder_service import TriggerSubscriptionBuilderService
class PluginParameterService:
@ -23,7 +27,7 @@ class PluginParameterService:
provider: str,
action: str,
parameter: str,
extra: dict | None,
credential_id: str | None,
provider_type: Literal["tool", "trigger"],
) -> Sequence[PluginParameterOption]:
"""
@ -37,7 +41,7 @@ class PluginParameterService:
parameter: The parameter name.
"""
credentials: Mapping[str, Any] = {}
credential_type: str = CredentialType.API_KEY.value
credential_type: str = CredentialType.UNAUTHORIZED.value
match provider_type:
case "tool":
provider_controller = ToolManager.get_builtin_provider(provider, tenant_id)
@ -53,8 +57,7 @@ class PluginParameterService:
else:
# fetch credentials from db
with Session(db.engine) as session:
if extra and "credential_id" in extra:
credential_id = extra["credential_id"]
if credential_id:
db_record = (
session.query(BuiltinToolProvider)
.where(
@ -82,7 +85,21 @@ class PluginParameterService:
credential_type = db_record.credential_type
case "trigger":
provider_controller = TriggerManager.get_trigger_provider(tenant_id, TriggerProviderID(provider))
if credential_id:
subscription: TriggerProviderSubscriptionApiEntity | SubscriptionBuilder | None = (
TriggerSubscriptionBuilderService.get_subscription_builder(credential_id)
or TriggerProviderService.get_subscription_by_id(tenant_id, credential_id)
)
else:
subscription: TriggerProviderSubscriptionApiEntity | SubscriptionBuilder | None = (
TriggerProviderService.get_subscription_by_id(tenant_id)
)
if subscription is None:
raise ValueError(f"Subscription {credential_id} not found")
credentials = subscription.credentials
credential_type = subscription.credential_type or CredentialType.UNAUTHORIZED
case _:
raise ValueError(f"Invalid provider type: {provider_type}")

View File

@ -69,7 +69,7 @@ class TriggerProviderService:
controller=provider_controller,
subscription=subscription,
)
subscription.credentials = encrypter.decrypt(subscription.credentials)
subscription.credentials = encrypter.mask_credentials(subscription.credentials)
return subscriptions
@classmethod
@ -165,6 +165,34 @@ class TriggerProviderService:
logger.exception("Failed to add trigger provider")
raise ValueError(str(e))
@classmethod
def get_subscription_by_id(
cls, tenant_id: str, subscription_id: str | None = None
) -> TriggerProviderSubscriptionApiEntity | None:
"""
Get a trigger subscription by the ID.
"""
with Session(db.engine, expire_on_commit=False) as session:
subscription: TriggerSubscription | None = None
if subscription_id:
subscription = (
session.query(TriggerSubscription).filter_by(tenant_id=tenant_id, id=subscription_id).first()
)
else:
subscription = session.query(TriggerSubscription).filter_by(tenant_id=tenant_id).first()
if subscription:
provider_controller = TriggerManager.get_trigger_provider(
tenant_id, TriggerProviderID(subscription.provider_id)
)
encrypter, _ = create_trigger_provider_encrypter_for_subscription(
tenant_id=tenant_id,
controller=provider_controller,
subscription=subscription,
)
subscription.credentials = encrypter.decrypt(subscription.credentials)
return subscription.to_api_entity()
return None
@classmethod
def delete_trigger_provider(cls, session: Session, tenant_id: str, subscription_id: str):
"""

View File

@ -97,7 +97,7 @@ const AgentTools: FC = () => {
provider_id: tool.provider_id,
provider_type: tool.provider_type as CollectionType,
provider_name: tool.provider_name,
tool_name: tool.tool_name,
tool_name: tool.trigger_name,
tool_label: tool.tool_label,
tool_parameters: tool.params,
notAuthor: !tool.is_team_authorization,

View File

@ -118,7 +118,7 @@ const ToolSelector: FC<Props> = ({
provider_name: tool.provider_id,
provider_show_name: tool.provider_name,
type: tool.provider_type,
tool_name: tool.tool_name,
tool_name: tool.trigger_name,
tool_label: tool.tool_label,
tool_description: tool.tool_description,
settings: settingValues,

View File

@ -69,6 +69,7 @@ export type ToolParameter = {
form: string
llm_description: string
required: boolean
multiple: boolean
default: string
options?: {
label: TypeWithI18N
@ -78,7 +79,33 @@ export type ToolParameter = {
max?: number
}
export type TriggerParameter = {
name: string
label: TypeWithI18N
human_description: TypeWithI18N
type: string
form: string
llm_description: string
required: boolean
multiple: boolean
default: string
options?: {
label: TypeWithI18N
value: string
}[]
}
// Action
export type Trigger = {
name: string
author: string
label: TypeWithI18N
description: any
parameters: TriggerParameter[]
labels: string[]
output_schema: Record<string, any>
}
export type Tool = {
name: string
author: string

View File

@ -1,8 +1,8 @@
'use client'
import { useCallback, useRef, useState } from 'react'
import { useTranslation } from 'react-i18next'
import type { BlockEnum } from '../types'
import type { ToolDefaultValue } from './types'
import type { BlockEnum, OnSelectBlock } from '../types'
import type { TriggerDefaultValue } from './types'
import StartBlocks from './start-blocks'
import TriggerPluginSelector from './trigger-plugin-selector'
import { ENTRY_NODE_TYPES } from './constants'
@ -16,7 +16,7 @@ import { SearchMenu } from '@/app/components/base/icons/src/vender/line/general'
type AllStartBlocksProps = {
className?: string
searchText: string
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
tags?: string[]
}
@ -75,7 +75,7 @@ const AllStartBlocks = ({
<>
<StartBlocks
searchText={searchText}
onSelect={onSelect}
onSelect={onSelect as OnSelectBlock}
availableBlocksTypes={ENTRY_NODE_TYPES as unknown as BlockEnum[]}
onContentStateChange={handleStartBlocksContentChange}
/>

View File

@ -86,9 +86,9 @@ const NodeSelector: FC<NodeSelectorProps> = ({
e.stopPropagation()
handleOpenChange(!open)
}, [handleOpenChange, open, disabled])
const handleSelect = useCallback<OnSelectBlock>((type, toolDefaultValue) => {
const handleSelect = useCallback<OnSelectBlock>((type, pluginDefaultValue) => {
handleOpenChange(false)
onSelect(type, toolDefaultValue)
onSelect(type, pluginDefaultValue)
}, [handleOpenChange, onSelect])
const [activeTab, setActiveTab] = useState(

View File

@ -3,7 +3,7 @@ import { memo } from 'react'
import { useAllBuiltInTools, useAllCustomTools, useAllMCPTools, useAllWorkflowTools } from '@/service/use-tools'
import type { BlockEnum } from '../types'
import { useTabs } from './hooks'
import type { ToolDefaultValue } from './types'
import type { PluginDefaultValue } from './types'
import { TabsEnum } from './types'
import Blocks from './blocks'
import AllStartBlocks from './all-start-blocks'
@ -15,7 +15,7 @@ export type TabsProps = {
onActiveTabChange: (activeTab: TabsEnum) => void
searchText: string
tags: string[]
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
onSelect: (type: BlockEnum, plugin?: PluginDefaultValue) => void
availableBlocksTypes?: BlockEnum[]
filterElem: React.ReactNode
noBlocks?: boolean

View File

@ -64,7 +64,7 @@ const ToolItem: FC<Props> = ({
provider_id: provider.id,
provider_type: provider.type,
provider_name: provider.name,
tool_name: payload.name,
trigger_name: payload.name,
tool_label: payload.label[language],
tool_description: payload.description[language],
title: payload.label[language],

View File

@ -165,7 +165,7 @@ const Tool: FC<Props> = ({
provider_id: payload.id,
provider_type: payload.type,
provider_name: payload.name,
tool_name: tool.name,
trigger_name: tool.name,
tool_label: tool.label[language],
tool_description: tool.description[language],
title: tool.label[language],

View File

@ -2,10 +2,10 @@
import { memo } from 'react'
import TriggerPluginList from './trigger-plugin/list'
import type { BlockEnum } from '../types'
import type { ToolDefaultValue } from './types'
import type { TriggerDefaultValue } from './types'
type TriggerPluginSelectorProps = {
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
searchText: string
onContentStateChange?: (hasContent: boolean) => void
tags?: string[]

View File

@ -2,9 +2,9 @@
import type { FC } from 'react'
import React from 'react'
import type { TriggerWithProvider } from '../types'
import type { Tool } from '@/app/components/tools/types'
import type { Trigger } from '@/app/components/tools/types'
import { BlockEnum } from '../../types'
import type { ToolDefaultValue } from '../types'
import type { TriggerDefaultValue } from '../types'
import Tooltip from '@/app/components/base/tooltip'
import { useGetLanguage } from '@/context/i18n'
import BlockIcon from '../../block-icon'
@ -13,10 +13,10 @@ import { useTranslation } from 'react-i18next'
type Props = {
provider: TriggerWithProvider
payload: Tool
payload: Trigger
disabled?: boolean
isAdded?: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
}
const TriggerPluginActionItem: FC<Props> = ({
@ -63,9 +63,9 @@ const TriggerPluginActionItem: FC<Props> = ({
provider_id: provider.id,
provider_type: provider.type as string,
provider_name: provider.name,
tool_name: payload.name,
tool_label: payload.label[language],
tool_description: payload.description[language],
trigger_name: payload.name,
trigger_label: payload.label[language],
trigger_description: payload.description[language],
title: payload.label[language],
is_team_authorization: provider.is_team_authorization,
output_schema: payload.output_schema || {},

View File

@ -1,22 +1,21 @@
'use client'
import type { FC } from 'react'
import React, { useEffect, useMemo, useRef } from 'react'
import { useGetLanguage } from '@/context/i18n'
import cn from '@/utils/classnames'
import { RiArrowDownSLine, RiArrowRightSLine } from '@remixicon/react'
import { useGetLanguage } from '@/context/i18n'
import { CollectionType } from '../../../tools/types'
import type { TriggerWithProvider } from '../types'
import { BlockEnum } from '../../types'
import type { ToolDefaultValue } from '../types'
import TriggerPluginActionItem from './action-item'
import BlockIcon from '../../block-icon'
import type { FC } from 'react'
import React, { useEffect, useMemo, useRef } from 'react'
import { useTranslation } from 'react-i18next'
import { CollectionType } from '../../../tools/types'
import BlockIcon from '../../block-icon'
import { BlockEnum } from '../../types'
import type { TriggerDefaultValue, TriggerWithProvider } from '../types'
import TriggerPluginActionItem from './action-item'
type Props = {
className?: string
payload: TriggerWithProvider
hasSearchText: boolean
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
}
const TriggerPluginItem: FC<Props> = ({
@ -28,7 +27,7 @@ const TriggerPluginItem: FC<Props> = ({
const { t } = useTranslation()
const language = useGetLanguage()
const notShowProvider = payload.type === CollectionType.workflow
const actions = payload.tools
const actions = payload.triggers
const hasAction = !notShowProvider
const [isFold, setFold] = React.useState<boolean>(true)
const ref = useRef(null)
@ -72,10 +71,10 @@ const TriggerPluginItem: FC<Props> = ({
return
}
const tool = actions[0]
const trigger = actions[0]
const params: Record<string, string> = {}
if (tool.parameters) {
tool.parameters.forEach((item) => {
if (trigger.parameters) {
trigger.parameters.forEach((item) => {
params[item.name] = ''
})
}
@ -83,13 +82,13 @@ const TriggerPluginItem: FC<Props> = ({
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],
trigger_name: trigger.name,
trigger_label: trigger.label[language],
trigger_description: trigger.description[language],
title: trigger.label[language],
is_team_authorization: payload.is_team_authorization,
output_schema: tool.output_schema || {},
paramSchemas: tool.parameters,
output_schema: trigger.output_schema || {},
paramSchemas: trigger.parameters,
params,
})
}}

View File

@ -3,11 +3,11 @@ import { memo, useEffect, useMemo } from 'react'
import { useAllTriggerPlugins } from '@/service/use-triggers'
import TriggerPluginItem from './item'
import type { BlockEnum } from '../../types'
import type { ToolDefaultValue } from '../types'
import type { TriggerDefaultValue } from '../types'
import { useGetLanguage } from '@/context/i18n'
type TriggerPluginListProps = {
onSelect: (type: BlockEnum, tool?: ToolDefaultValue) => void
onSelect: (type: BlockEnum, trigger?: TriggerDefaultValue) => void
searchText: string
onContentStateChange?: (hasContent: boolean) => void
tags?: string[]
@ -24,13 +24,13 @@ const TriggerPluginList = ({
const triggerPlugins = useMemo(() => {
// Follow exact same pattern as tools
return (triggerPluginsData || []).filter((toolWithProvider) => {
if (toolWithProvider.tools.length === 0) return false
return (triggerPluginsData || []).filter((triggerWithProvider) => {
if (triggerWithProvider.triggers.length === 0) return false
// Filter by search text
if (searchText) {
const matchesSearch = toolWithProvider.name.toLowerCase().includes(searchText.toLowerCase())
|| toolWithProvider.tools.some(tool =>
const matchesSearch = triggerWithProvider.name.toLowerCase().includes(searchText.toLowerCase())
|| triggerWithProvider.triggers.some(tool =>
tool.label[language].toLowerCase().includes(searchText.toLowerCase()),
)
if (!matchesSearch) return false

View File

@ -1,5 +1,5 @@
import type { PluginMeta } from '../../plugins/types'
import type { Collection, Tool } from '../../tools/types'
import type { Collection, Trigger } from '../../tools/types'
import type { TypeWithI18N } from '../../base/form/types'
export enum TabsEnum {
@ -24,11 +24,32 @@ export enum BlockClassificationEnum {
Utilities = 'utilities',
}
export type ToolDefaultValue = {
export type PluginDefaultValue = {
provider_id: string
provider_type: string
provider_name: string
tool_name: string
plugin_name: string
plugin_label: string
}
export type TriggerDefaultValue = PluginDefaultValue & {
trigger_name: string
trigger_label: string
trigger_description: string
title: string
is_team_authorization: boolean
params: Record<string, any>
paramSchemas: Record<string, any>[]
output_schema: Record<string, any>
credential_id?: string
meta?: PluginMeta
}
export type ToolDefaultValue = PluginDefaultValue & {
provider_id: string
provider_type: string
provider_name: string
trigger_name: string
tool_label: string
tool_description: string
title: string
@ -55,6 +76,7 @@ export type ToolValue = {
// Backend API types - exact match with Python definitions
export type TriggerParameter = {
multiple: boolean
name: string
label: TypeWithI18N
description?: TypeWithI18N
@ -141,7 +163,7 @@ export type TriggerProviderApiEntity = {
// Frontend types - compatible with ToolWithProvider
export type TriggerWithProvider = Collection & {
tools: Tool[] // Use existing Tool type for compatibility
triggers: Trigger[]
meta: PluginMeta
credentials_schema?: TriggerCredentialField[]
oauth_client_schema?: TriggerCredentialField[]

View File

@ -209,7 +209,7 @@ export const AgentStrategySelector = memo((props: AgentStrategySelectorProps) =>
viewType={viewType}
onSelect={(_, tool) => {
onChange({
agent_strategy_name: tool!.tool_name,
agent_strategy_name: tool!.trigger_name,
agent_strategy_provider_name: tool!.provider_name,
agent_strategy_label: tool!.tool_label,
agent_output_schema: tool!.output_schema,

View File

@ -1,20 +1,19 @@
'use client'
import type { FC } from 'react'
import { useEffect, useState } from 'react'
import type { ToolVarInputs } from '@/app/components/workflow/nodes/tool/types'
import { type BaseResource, type BaseResourceProvider, type ResourceVarInputs, VarKindType } from '../types'
import type { CredentialFormSchema, FormOption } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import { VarType } from '@/app/components/workflow/types'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import type { ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { ValueSelector, Var } from '@/app/components/workflow/types'
import FormInputTypeSwitch from './form-input-type-switch'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import Input from '@/app/components/base/input'
import { SimpleSelect } from '@/app/components/base/select'
import MixedVariableTextInput from '@/app/components/workflow/nodes/tool/components/mixed-variable-text-input'
import MixedVariableTextInput from './mixed-variable-text-input'
import FormInputBoolean from './form-input-boolean'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-selector'
@ -22,19 +21,20 @@ import VarReferencePicker from '@/app/components/workflow/nodes/_base/components
import CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import cn from '@/utils/classnames'
import type { Tool } from '@/app/components/tools/types'
import { Listbox, ListboxButton, ListboxOption, ListboxOptions } from '@headlessui/react'
import { ChevronDownIcon } from '@heroicons/react/20/solid'
import { RiCheckLine } from '@remixicon/react'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema
value: ToolVarInputs
value: ResourceVarInputs
onChange: (value: any) => void
inPanel?: boolean
currentTool?: Tool
currentProvider?: ToolWithProvider
currentResource?: BaseResource
currentProvider?: BaseResourceProvider
extraParams?: Record<string, any>
providerType?: 'tool' | 'trigger'
providerType?: string
}
const FormInputItem: FC<Props> = ({
@ -44,10 +44,10 @@ const FormInputItem: FC<Props> = ({
value,
onChange,
inPanel,
currentTool,
currentResource,
currentProvider,
extraParams,
providerType = 'tool',
providerType,
}) => {
const language = useLanguage()
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
@ -59,6 +59,7 @@ const FormInputItem: FC<Props> = ({
type,
default: defaultValue,
options,
multiple,
scope,
} = schema as any
const varInput = value[variable]
@ -76,6 +77,7 @@ const FormInputItem: FC<Props> = ({
const showTypeSwitch = isNumber || isBoolean || isObject || isArray
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable
const isMultipleSelect = multiple && (isSelect || isDynamicSelect)
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false,
@ -138,7 +140,7 @@ const FormInputItem: FC<Props> = ({
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
currentProvider?.plugin_id || '',
currentProvider?.name || '',
currentTool?.name || '',
currentResource?.name || '',
variable || '',
providerType,
extraParams,
@ -147,7 +149,7 @@ const FormInputItem: FC<Props> = ({
// Fetch dynamic options when component mounts or dependencies change
useEffect(() => {
const fetchOptions = async () => {
if (isDynamicSelect && currentTool && currentProvider) {
if (isDynamicSelect && currentResource && currentProvider) {
setIsLoadingOptions(true)
try {
const data = await fetchDynamicOptions()
@ -164,7 +166,7 @@ const FormInputItem: FC<Props> = ({
}
fetchOptions()
}, [isDynamicSelect, currentTool?.name, currentProvider?.name, variable, extraParams])
}, [isDynamicSelect, currentResource?.name, currentProvider?.name, variable, extraParams])
const handleTypeChange = (newType: string) => {
if (newType === VarKindType.variable) {
@ -200,6 +202,24 @@ const FormInputItem: FC<Props> = ({
})
}
const getSelectedLabels = (selectedValues: any[]) => {
if (!selectedValues || selectedValues.length === 0)
return ''
const optionsList = isDynamicSelect ? (dynamicOptions || options || []) : (options || [])
const selectedOptions = optionsList.filter((opt: any) =>
selectedValues.includes(opt.value),
)
if (selectedOptions.length <= 2) {
return selectedOptions
.map((opt: any) => opt.label?.[language] || opt.label?.en_US || opt.value)
.join(', ')
}
return `${selectedOptions.length} selected`
}
const handleAppOrModelSelect = (newValue: any) => {
onChange({
...value,
@ -250,7 +270,7 @@ const FormInputItem: FC<Props> = ({
onChange={handleValueChange}
/>
)}
{isSelect && (
{isSelect && !isMultipleSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
disabled={readOnly}
@ -277,7 +297,64 @@ const FormInputItem: FC<Props> = ({
) : undefined}
/>
)}
{isDynamicSelect && (
{isSelect && isMultipleSelect && (
<Listbox
multiple
value={varInput?.value || []}
onChange={handleValueChange}
disabled={readOnly}
>
<div className="relative">
<ListboxButton className="relative h-8 w-full cursor-pointer rounded-lg bg-components-input-bg-normal px-3 py-1.5 text-left text-sm focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300">
<span className="block truncate text-components-input-text-filled">
{getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-4 w-4 text-text-tertiary"
aria-hidden="true"
/>
</span>
</ListboxButton>
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{options.filter((option: { show_on: any[] }) => {
if (option.show_on?.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
<ListboxOption
key={option.value}
value={option.value}
className={({ focus }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
focus ? 'bg-state-base-hover text-text-secondary' : 'text-text-primary'
}`
}
>
{({ selected }) => (
<>
<div className="flex items-center">
{option.icon && (
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
)}
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{option.label[language] || option.label.en_US}
</span>
</div>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600">
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
)}
{isDynamicSelect && !isMultipleSelect && (
<SimpleSelect
wrapperClassName='h-8 grow'
disabled={readOnly || isLoadingOptions}
@ -304,6 +381,65 @@ const FormInputItem: FC<Props> = ({
)}
/>
)}
{isDynamicSelect && isMultipleSelect && (
<Listbox
multiple
value={varInput?.value || []}
onChange={handleValueChange}
disabled={readOnly || isLoadingOptions}
>
<div className="relative">
<ListboxButton className="relative h-8 w-full cursor-pointer rounded-lg bg-components-input-bg-normal px-3 py-1.5 text-left text-sm focus:outline-none focus-visible:border-indigo-500 focus-visible:ring-2 focus-visible:ring-white/75 focus-visible:ring-offset-2 focus-visible:ring-offset-orange-300">
<span className="block truncate text-components-input-text-filled">
{isLoadingOptions
? 'Loading...'
: getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'}
</span>
<span className="pointer-events-none absolute inset-y-0 right-0 flex items-center pr-2">
<ChevronDownIcon
className="h-4 w-4 text-text-tertiary"
aria-hidden="true"
/>
</span>
</ListboxButton>
<ListboxOptions className="absolute z-10 mt-1 max-h-60 w-full overflow-auto rounded-md bg-components-panel-bg-blur py-1 text-base shadow-lg ring-1 ring-black/5 focus:outline-none sm:text-sm">
{(dynamicOptions || options || []).filter((option: { show_on?: any[] }) => {
if (option.show_on?.length)
return option.show_on.every(showOnItem => value[showOnItem.variable] === showOnItem.value)
return true
}).map((option: { value: any; label: { [x: string]: any; en_US: any }; icon?: string }) => (
<ListboxOption
key={option.value}
value={option.value}
className={({ focus }) =>
`relative cursor-pointer select-none py-2 pl-10 pr-4 ${
focus ? 'bg-state-base-hover text-text-secondary' : 'text-text-primary'
}`
}
>
{({ selected }) => (
<>
<div className="flex items-center">
{option.icon && (
<img src={option.icon} alt="" className="mr-2 h-4 w-4" />
)}
<span className={`block truncate ${selected ? 'font-medium' : 'font-normal'}`}>
{option.label[language] || option.label.en_US}
</span>
</div>
{selected && (
<span className="absolute inset-y-0 left-0 flex items-center pl-3 text-primary-600">
<RiCheckLine className="h-4 w-4" aria-hidden="true" />
</span>
)}
</>
)}
</ListboxOption>
))}
</ListboxOptions>
</div>
</Listbox>
)}
{isShowJSONEditor && isConstant && (
<div className='mt-1 w-full'>
<CodeEditor
@ -349,7 +485,7 @@ const FormInputItem: FC<Props> = ({
filterVar={getFilterVar()}
schema={schema}
valueTypePlaceHolder={targetVarType()}
currentTool={currentTool}
currentResource={currentResource}
currentProvider={currentProvider}
/>
)}

View File

@ -0,0 +1,62 @@
import {
memo,
} from 'react'
import { useTranslation } from 'react-i18next'
import PromptEditor from '@/app/components/base/prompt-editor'
import Placeholder from './placeholder'
import type {
Node,
NodeOutPutVar,
} from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import cn from '@/utils/classnames'
type MixedVariableTextInputProps = {
readOnly?: boolean
nodesOutputVars?: NodeOutPutVar[]
availableNodes?: Node[]
value?: string
onChange?: (text: string) => void
}
const MixedVariableTextInput = ({
readOnly = false,
nodesOutputVars,
availableNodes = [],
value = '',
onChange,
}: MixedVariableTextInputProps) => {
const { t } = useTranslation()
return (
<PromptEditor
wrapperClassName={cn(
'w-full rounded-lg border border-transparent bg-components-input-bg-normal px-2 py-1',
'hover:border-components-input-border-hover hover:bg-components-input-bg-hover',
'focus-within:border-components-input-border-active focus-within:bg-components-input-bg-active focus-within:shadow-xs',
)}
className='caret:text-text-accent'
editable={!readOnly}
value={value}
workflowVariableBlock={{
show: true,
variables: nodesOutputVars || [],
workflowNodesMap: availableNodes.reduce((acc, node) => {
acc[node.id] = {
title: node.data.title,
type: node.data.type,
}
if (node.data.type === BlockEnum.Start) {
acc.sys = {
title: t('workflow.blocks.start'),
type: BlockEnum.Start,
}
}
return acc
}, {} as any),
}}
placeholder={<Placeholder />}
onChange={onChange}
/>
)
}
export default memo(MixedVariableTextInput)

View File

@ -0,0 +1,52 @@
import { useCallback } from 'react'
import { useTranslation } from 'react-i18next'
import { useLexicalComposerContext } from '@lexical/react/LexicalComposerContext'
import { FOCUS_COMMAND } from 'lexical'
import { $insertNodes } from 'lexical'
import { CustomTextNode } from '@/app/components/base/prompt-editor/plugins/custom-text/node'
import Badge from '@/app/components/base/badge'
const Placeholder = () => {
const { t } = useTranslation()
const [editor] = useLexicalComposerContext()
const handleInsert = useCallback((text: string) => {
editor.update(() => {
const textNode = new CustomTextNode(text)
$insertNodes([textNode])
})
editor.dispatchCommand(FOCUS_COMMAND, undefined as any)
}, [editor])
return (
<div
className='pointer-events-auto flex h-full w-full cursor-text items-center px-2'
onClick={(e) => {
e.stopPropagation()
handleInsert('')
}}
>
<div className='flex grow items-center'>
{t('workflow.nodes.tool.insertPlaceholder1')}
<div className='system-kbd mx-0.5 flex h-4 w-4 items-center justify-center rounded bg-components-kbd-bg-gray text-text-placeholder'>/</div>
<div
className='system-sm-regular cursor-pointer text-components-input-text-placeholder underline decoration-dotted decoration-auto underline-offset-auto hover:text-text-tertiary'
onMouseDown={((e) => {
e.preventDefault()
e.stopPropagation()
handleInsert('/')
})}
>
{t('workflow.nodes.tool.insertPlaceholder2')}
</div>
</div>
<Badge
className='shrink-0'
text='String'
uppercase={false}
/>
</div>
)
}
export default Placeholder

View File

@ -17,7 +17,7 @@ import VarReferencePopup from './var-reference-popup'
import { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils'
import ConstantField from './constant-field'
import cn from '@/utils/classnames'
import type { Node, NodeOutPutVar, ToolWithProvider, ValueSelector, Var } from '@/app/components/workflow/types'
import type { Node, NodeOutPutVar, ValueSelector, Var } from '@/app/components/workflow/types'
import type { CredentialFormSchemaSelect } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { BlockEnum } from '@/app/components/workflow/types'
@ -34,6 +34,7 @@ import {
useWorkflowVariables,
} from '@/app/components/workflow/hooks'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import type { BaseResource, BaseResourceProvider } from '@/app/components/workflow/nodes/_base/types'
import TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import AddButton from '@/app/components/base/button/add-button'
import Badge from '@/app/components/base/badge'
@ -42,7 +43,6 @@ import { isExceptionVariable } from '@/app/components/workflow/utils'
import VarFullPathPanel from './var-full-path-panel'
import { noop } from 'lodash-es'
import { useFetchDynamicOptions } from '@/service/use-plugins'
import type { Tool } from '@/app/components/tools/types'
import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
const TRIGGER_DEFAULT_WIDTH = 227
@ -72,8 +72,8 @@ type Props = {
minWidth?: number
popupFor?: 'assigned' | 'toAssigned'
zIndex?: number
currentTool?: Tool
currentProvider?: ToolWithProvider
currentResource?: BaseResource
currentProvider?: BaseResourceProvider
}
const DEFAULT_VALUE_SELECTOR: Props['value'] = []
@ -103,7 +103,7 @@ const VarReferencePicker: FC<Props> = ({
minWidth,
popupFor,
zIndex,
currentTool,
currentResource,
currentProvider,
}) => {
const { t } = useTranslation()
@ -328,11 +328,11 @@ const VarReferencePicker: FC<Props> = ({
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
const [isLoading, setIsLoading] = useState(false)
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
currentProvider?.plugin_id || '', currentProvider?.name || '', currentTool?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '',
currentProvider?.plugin_id || '', currentProvider?.name || '', currentResource?.name || '', (schema as CredentialFormSchemaSelect)?.variable || '',
'tool',
)
const handleFetchDynamicOptions = async () => {
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentTool || !currentProvider)
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentResource || !currentProvider)
return
setIsLoading(true)
try {
@ -345,7 +345,7 @@ const VarReferencePicker: FC<Props> = ({
}
useEffect(() => {
handleFetchDynamicOptions()
}, [currentTool, currentProvider, schema])
}, [currentResource, currentProvider, schema])
const schemaWithDynamicSelect = useMemo(() => {
if (schema?.type !== FormTypeEnum.dynamicSelect)

View File

@ -0,0 +1,27 @@
import type { ValueSelector } from '@/app/components/workflow/types'
// Generic variable types for all resource forms
export enum VarKindType {
variable = 'variable',
constant = 'constant',
mixed = 'mixed',
}
// Generic resource variable inputs
export type ResourceVarInputs = Record<string, {
type: VarKindType
value?: string | ValueSelector | any
}>
// Base resource interface
export type BaseResource = {
name: string
[key: string]: any
}
// Base resource provider interface
export type BaseResourceProvider = {
plugin_id?: string
name: string
[key: string]: any
}

View File

@ -44,7 +44,7 @@ const ImportFromTool: FC<Props> = ({
const workflowTools = useStore(s => s.workflowTools)
const handleSelectTool = useCallback((_type: BlockEnum, toolInfo?: ToolDefaultValue) => {
const { provider_id, provider_type, tool_name } = toolInfo!
const { provider_id, provider_type, trigger_name: tool_name } = toolInfo!
const currentTools = (() => {
switch (provider_type) {
case CollectionType.builtIn:

View File

@ -17,7 +17,6 @@ type Props = {
currentTool?: Tool
currentProvider?: ToolWithProvider
extraParams?: Record<string, any>
providerType?: 'tool' | 'trigger'
}
const ToolForm: FC<Props> = ({
@ -30,7 +29,6 @@ const ToolForm: FC<Props> = ({
currentTool,
currentProvider,
extraParams,
providerType = 'tool',
}) => {
return (
<div className='space-y-1'>
@ -47,7 +45,7 @@ const ToolForm: FC<Props> = ({
currentTool={currentTool}
currentProvider={currentProvider}
extraParams={extraParams}
providerType={providerType}
providerType='tool'
/>
))
}

View File

@ -91,7 +91,7 @@ const ToolFormItem: FC<Props> = ({
value={value}
onChange={onChange}
inPanel={inPanel}
currentTool={currentTool}
currentResource={currentTool}
currentProvider={currentProvider}
extraParams={extraParams}
providerType={providerType}

View File

@ -1,16 +1,10 @@
import type { CollectionType } from '@/app/components/tools/types'
import type { CommonNodeType, ValueSelector } from '@/app/components/workflow/types'
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { ResourceVarInputs } from '../_base/types'
export enum VarType {
variable = 'variable',
constant = 'constant',
mixed = 'mixed',
}
export type ToolVarInputs = Record<string, {
type: VarType
value?: string | ValueSelector | any
}>
// Use base types directly
export { VarKindType as VarType } from '../_base/types'
export type ToolVarInputs = ResourceVarInputs
export type ToolNodeType = CommonNodeType & {
provider_id: string

View File

@ -32,7 +32,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
* tool_parameters: tool dynamic setting(form type = llm)
* output_schema: tool dynamic output
*/
const { provider_id, provider_type, tool_name, tool_configurations, output_schema, tool_parameters } = inputs
const { provider_id, provider_type, trigger_name: tool_name, tool_configurations, output_schema, tool_parameters } = inputs
const isBuiltIn = provider_type === CollectionType.builtIn
const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools)

View File

@ -6,7 +6,7 @@ import { ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/block
const nodeDefault: NodeDefault<PluginTriggerNodeType> = {
defaultValue: {
plugin_id: '',
tool_name: '',
trigger_name: '',
event_type: '',
config: {},
},

View File

@ -5,7 +5,7 @@ import Split from '@/app/components/workflow/nodes/_base/components/split'
import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import type { NodePanelProps } from '@/app/components/workflow/types'
import useConfig from './use-config'
import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form'
import TriggerForm from './trigger-form'
import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types'
@ -21,6 +21,8 @@ const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
outputSchema,
hasObjectOutput,
isAuthenticated,
currentProvider,
currentTrigger,
} = useConfig(id, data)
// Convert output schema to VarItem format
@ -36,13 +38,14 @@ const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
{isAuthenticated && triggerParameterSchema.length > 0 && (
<>
<div className='px-4 pb-4'>
<ToolForm
<TriggerForm
readOnly={readOnly}
nodeId={id}
schema={triggerParameterSchema as any}
value={triggerParameterValue}
onChange={setTriggerParameterValue}
providerType="trigger"
currentProvider={currentProvider}
currentTrigger={currentTrigger}
/>
</div>
<Split />

View File

@ -0,0 +1,54 @@
'use client'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import type { Trigger } from '@/app/components/tools/types'
import type { FC } from 'react'
import type { PluginTriggerVarInputs } from '../types'
import TriggerFormItem from './item'
import type { TriggerWithProvider } from '../../../block-selector/types'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema[]
value: PluginTriggerVarInputs
onChange: (value: PluginTriggerVarInputs) => void
onOpen?: (index: number) => void
inPanel?: boolean
currentTrigger?: Trigger
currentProvider?: TriggerWithProvider
extraParams?: Record<string, any>
}
const TriggerForm: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentTrigger,
currentProvider,
extraParams,
}) => {
return (
<div className='space-y-1'>
{
schema.map((schema, index) => (
<TriggerFormItem
key={index}
readOnly={readOnly}
nodeId={nodeId}
schema={schema}
value={value}
onChange={onChange}
inPanel={inPanel}
currentTrigger={currentTrigger}
currentProvider={currentProvider}
extraParams={extraParams}
/>
))
}
</div>
)
}
export default TriggerForm

View File

@ -0,0 +1,109 @@
'use client'
import type { FC } from 'react'
import {
RiBracesLine,
} from '@remixicon/react'
import type { PluginTriggerVarInputs } from '../types'
import type { CredentialFormSchema } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import Button from '@/app/components/base/button'
import Tooltip from '@/app/components/base/tooltip'
import FormInputItem from '@/app/components/workflow/nodes/_base/components/form-input-item'
import { useBoolean } from 'ahooks'
import SchemaModal from '@/app/components/plugins/plugin-detail-panel/tool-selector/schema-modal'
import type { Trigger } from '@/app/components/tools/types'
import type { TriggerWithProvider } from '../../../block-selector/types'
type Props = {
readOnly: boolean
nodeId: string
schema: CredentialFormSchema
value: PluginTriggerVarInputs
onChange: (value: PluginTriggerVarInputs) => void
inPanel?: boolean
currentTrigger?: Trigger
currentProvider?: TriggerWithProvider
extraParams?: Record<string, any>
}
const TriggerFormItem: FC<Props> = ({
readOnly,
nodeId,
schema,
value,
onChange,
inPanel,
currentTrigger,
currentProvider,
extraParams,
}) => {
const language = useLanguage()
const { name, label, type, required, tooltip, input_schema } = schema
const showSchemaButton = type === FormTypeEnum.object || type === FormTypeEnum.array
const showDescription = type === FormTypeEnum.textInput || type === FormTypeEnum.secretInput
const [isShowSchema, {
setTrue: showSchema,
setFalse: hideSchema,
}] = useBoolean(false)
return (
<div className='space-y-0.5 py-1'>
<div>
<div className='flex h-6 items-center'>
<div className='system-sm-medium text-text-secondary'>{label[language] || label.en_US}</div>
{required && (
<div className='system-xs-regular ml-1 text-text-destructive-secondary'>*</div>
)}
{!showDescription && tooltip && (
<Tooltip
popupContent={<div className='w-[200px]'>
{tooltip[language] || tooltip.en_US}
</div>}
triggerClassName='ml-1 w-4 h-4'
asChild={false}
/>
)}
{showSchemaButton && (
<>
<div className='system-xs-regular ml-1 mr-0.5 text-text-quaternary'>·</div>
<Button
variant='ghost'
size='small'
onClick={showSchema}
className='system-xs-regular px-1 text-text-tertiary'
>
<RiBracesLine className='mr-1 size-3.5' />
<span>JSON Schema</span>
</Button>
</>
)}
</div>
{showDescription && tooltip && (
<div className='body-xs-regular pb-0.5 text-text-tertiary'>{tooltip[language] || tooltip.en_US}</div>
)}
</div>
<FormInputItem
readOnly={readOnly}
nodeId={nodeId}
schema={schema}
value={value}
onChange={onChange}
inPanel={inPanel}
currentResource={currentTrigger}
currentProvider={currentProvider}
providerType='trigger'
extraParams={extraParams}
/>
{isShowSchema && (
<SchemaModal
isShow
onClose={hideSchema}
rootName={name}
schema={input_schema!}
/>
)}
</div>
)
}
export default TriggerFormItem

View File

@ -1,12 +1,23 @@
import type { CommonNodeType } from '@/app/components/workflow/types'
import type { CollectionType } from '@/app/components/tools/types'
import type { ResourceVarInputs } from '../_base/types'
export type PluginTriggerNodeType = CommonNodeType & {
provider_id: string
provider_type: CollectionType
provider_name: string
trigger_name: string
trigger_label: string
trigger_parameters: PluginTriggerVarInputs
trigger_configurations: Record<string, any>
output_schema: Record<string, any>
parameters_schema?: Record<string, any>[]
version?: string
trigger_node_version?: string
plugin_id?: string
tool_name?: string
event_type?: string
config?: Record<string, any>
provider_id?: string
provider_type?: CollectionType
provider_name?: string
}
// Use base types directly
export { VarKindType as PluginTriggerVarType } from '../_base/types'
export type PluginTriggerVarInputs = ResourceVarInputs

View File

@ -3,27 +3,32 @@ 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 {
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'
import type { Trigger } 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 { inputs, setInputs: doSetInputs } = useNodeCrud<PluginTriggerNodeType>(
id,
payload,
)
const { provider_id, provider_name, tool_name, config } = inputs
const { provider_id, provider_name, trigger_name, config } = inputs
// Construct provider for authentication check
const authProvider = useMemo(() => {
if (provider_id && provider_name)
return `${provider_id}/${provider_name}`
if (provider_id && provider_name) return `${provider_id}/${provider_name}`
return provider_id || ''
}, [provider_id, provider_name])
@ -33,21 +38,26 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
)
const currentProvider = useMemo<TriggerWithProvider | undefined>(() => {
return triggerPlugins.find(provider =>
provider.name === provider_name
|| provider.id === provider_id
|| (provider_id && provider.plugin_id === provider_id),
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])
const currentTrigger = useMemo<Trigger | undefined>(() => {
return currentProvider?.triggers.find(
trigger => trigger.name === trigger_name,
)
}, [currentProvider, trigger_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)
return toolParametersToFormSchemas(
currentProvider.subscription_schema.parameters_schema as any,
)
}, [currentProvider])
// Dynamic trigger parameters (from specific trigger.parameters)
@ -66,22 +76,28 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
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 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])
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(() => {
@ -91,7 +107,9 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
// Check if trigger has complex output structure
const hasObjectOutput = useMemo(() => {
const properties = outputSchema.properties || {}
return Object.values(properties).some((prop: any) => prop.type === 'object')
return Object.values(properties).some(
(prop: any) => prop.type === 'object',
)
}, [outputSchema])
// Authentication status check
@ -109,10 +127,16 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
const methods = []
if (currentProvider.oauth_client_schema && currentProvider.oauth_client_schema.length > 0)
if (
currentProvider.oauth_client_schema
&& currentProvider.oauth_client_schema.length > 0
)
methods.push('oauth')
if (currentProvider.credentials_schema && currentProvider.credentials_schema.length > 0)
if (
currentProvider.credentials_schema
&& currentProvider.credentials_schema.length > 0
)
methods.push('api_key')
return methods

View File

@ -5,7 +5,7 @@ import type {
XYPosition,
} from 'reactflow'
import type { Resolution, TransferMethod } from '@/types/app'
import type { ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { PluginDefaultValue, ToolDefaultValue } from '@/app/components/workflow/block-selector/types'
import type { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import type { FileResponse, NodeTracing, PanelProps } from '@/types/workflow'
import type { Collection, Tool } from '@/app/components/tools/types'
@ -321,7 +321,7 @@ export type NodeDefault<T> = {
checkValid: (payload: T, t: any, moreDataForCheckValid?: any) => { isValid: boolean; errorMessage?: string }
}
export type OnSelectBlock = (type: BlockEnum, toolDefaultValue?: ToolDefaultValue) => void
export type OnSelectBlock = (type: BlockEnum, pluginDefaultValue?: PluginDefaultValue) => void
export enum WorkflowRunningStatus {
Waiting = 'waiting',

View File

@ -14,7 +14,7 @@ export const getToolCheckParams = (
workflowTools: ToolWithProvider[],
language: string,
) => {
const { provider_id, provider_type, tool_name } = toolData
const { provider_id, provider_type, trigger_name: tool_name } = toolData
const isBuiltIn = provider_type === CollectionType.builtIn
const currentTools = provider_type === CollectionType.builtIn ? buildInTools : provider_type === CollectionType.custom ? customTools : workflowTools
const currCollection = currentTools.find(item => canFindTool(item.id, provider_id))

View File

@ -613,7 +613,7 @@ export const usePluginInfo = (providerName?: string) => {
})
}
export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type: 'tool' | 'trigger', extra?: Record<string, any>) => {
export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type?: string, extra?: Record<string, any>) => {
return useMutation({
mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', {
params: {

View File

@ -31,9 +31,7 @@ const convertToTriggerWithProvider = (provider: TriggerProviderApiEntity): Trigg
allow_delete: false,
labels: provider.tags || [],
plugin_id: provider.plugin_id,
// ToolWithProvider fields - convert "triggers" to "tools"
tools: provider.triggers.map(trigger => ({
triggers: provider.triggers.map(trigger => ({
name: trigger.name,
author: provider.author,
label: trigger.description.human, // Already TypeWithI18N format
@ -51,6 +49,7 @@ const convertToTriggerWithProvider = (provider: TriggerProviderApiEntity): Trigg
label: option.label,
value: option.value,
})) || [],
multiple: param.multiple || false,
})),
labels: provider.tags || [],
output_schema: trigger.output_schema || {},