diff --git a/api/controllers/console/workspace/plugin.py b/api/controllers/console/workspace/plugin.py index 499bfdbcac..b51af69af9 100644 --- a/api/controllers/console/workspace/plugin.py +++ b/api/controllers/console/workspace/plugin.py @@ -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) diff --git a/api/core/helper/provider_encryption.py b/api/core/helper/provider_encryption.py index 7f301833e9..bac44629c0 100644 --- a/api/core/helper/provider_encryption.py +++ b/api/core/helper/provider_encryption.py @@ -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 diff --git a/api/core/plugin/impl/trigger.py b/api/core/plugin/impl/trigger.py index d9bc62359f..c0316cd46d 100644 --- a/api/core/plugin/impl/trigger.py +++ b/api/core/plugin/impl/trigger.py @@ -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 diff --git a/api/core/trigger/entities/entities.py b/api/core/trigger/entities/entities.py index 82b1aa29e2..1902164b2e 100644 --- a/api/core/trigger/entities/entities.py +++ b/api/core/trigger/entities/entities.py @@ -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 diff --git a/api/services/plugin/plugin_parameter_service.py b/api/services/plugin/plugin_parameter_service.py index 6dd8ebf7ce..cc81ccc158 100644 --- a/api/services/plugin/plugin_parameter_service.py +++ b/api/services/plugin/plugin_parameter_service.py @@ -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}") diff --git a/api/services/trigger/trigger_provider_service.py b/api/services/trigger/trigger_provider_service.py index ef5b532039..ae61e3fb02 100644 --- a/api/services/trigger/trigger_provider_service.py +++ b/api/services/trigger/trigger_provider_service.py @@ -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): """ diff --git a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx index b4711ea39a..26a1fb2d34 100644 --- a/web/app/components/app/configuration/config/agent/agent-tools/index.tsx +++ b/web/app/components/app/configuration/config/agent/agent-tools/index.tsx @@ -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, diff --git a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx index d2797b99f4..12fc81745b 100644 --- a/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx +++ b/web/app/components/plugins/plugin-detail-panel/tool-selector/index.tsx @@ -118,7 +118,7 @@ const ToolSelector: FC = ({ 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, diff --git a/web/app/components/tools/types.ts b/web/app/components/tools/types.ts index 01f436dedc..e58a5f6111 100644 --- a/web/app/components/tools/types.ts +++ b/web/app/components/tools/types.ts @@ -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 +} + export type Tool = { name: string author: string diff --git a/web/app/components/workflow/block-selector/all-start-blocks.tsx b/web/app/components/workflow/block-selector/all-start-blocks.tsx index 37378bf334..ff23db52c2 100644 --- a/web/app/components/workflow/block-selector/all-start-blocks.tsx +++ b/web/app/components/workflow/block-selector/all-start-blocks.tsx @@ -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 = ({ <> diff --git a/web/app/components/workflow/block-selector/index.tsx b/web/app/components/workflow/block-selector/index.tsx index 2944c2f092..457a028770 100644 --- a/web/app/components/workflow/block-selector/index.tsx +++ b/web/app/components/workflow/block-selector/index.tsx @@ -86,9 +86,9 @@ const NodeSelector: FC = ({ e.stopPropagation() handleOpenChange(!open) }, [handleOpenChange, open, disabled]) - const handleSelect = useCallback((type, toolDefaultValue) => { + const handleSelect = useCallback((type, pluginDefaultValue) => { handleOpenChange(false) - onSelect(type, toolDefaultValue) + onSelect(type, pluginDefaultValue) }, [handleOpenChange, onSelect]) const [activeTab, setActiveTab] = useState( diff --git a/web/app/components/workflow/block-selector/tabs.tsx b/web/app/components/workflow/block-selector/tabs.tsx index 3842dfa6c2..e90b42b52b 100644 --- a/web/app/components/workflow/block-selector/tabs.tsx +++ b/web/app/components/workflow/block-selector/tabs.tsx @@ -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 diff --git a/web/app/components/workflow/block-selector/tool/action-item.tsx b/web/app/components/workflow/block-selector/tool/action-item.tsx index 4a2ed4a87b..fd532604f1 100644 --- a/web/app/components/workflow/block-selector/tool/action-item.tsx +++ b/web/app/components/workflow/block-selector/tool/action-item.tsx @@ -64,7 +64,7 @@ const ToolItem: FC = ({ 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], diff --git a/web/app/components/workflow/block-selector/tool/tool.tsx b/web/app/components/workflow/block-selector/tool/tool.tsx index e0da011d2a..726dc6e865 100644 --- a/web/app/components/workflow/block-selector/tool/tool.tsx +++ b/web/app/components/workflow/block-selector/tool/tool.tsx @@ -165,7 +165,7 @@ const Tool: FC = ({ 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], diff --git a/web/app/components/workflow/block-selector/trigger-plugin-selector.tsx b/web/app/components/workflow/block-selector/trigger-plugin-selector.tsx index 141bf318ec..b47b0cba67 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin-selector.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin-selector.tsx @@ -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[] diff --git a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx index f586172e95..81e63e271e 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/action-item.tsx @@ -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 = ({ @@ -63,9 +63,9 @@ const TriggerPluginActionItem: FC = ({ 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 || {}, diff --git a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx index ad2df35cc4..cf2b7444ba 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/item.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/item.tsx @@ -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 = ({ @@ -28,7 +27,7 @@ const TriggerPluginItem: FC = ({ 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(true) const ref = useRef(null) @@ -72,10 +71,10 @@ const TriggerPluginItem: FC = ({ return } - const tool = actions[0] + const trigger = actions[0] const params: Record = {} - if (tool.parameters) { - tool.parameters.forEach((item) => { + if (trigger.parameters) { + trigger.parameters.forEach((item) => { params[item.name] = '' }) } @@ -83,13 +82,13 @@ const TriggerPluginItem: FC = ({ 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, }) }} diff --git a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx index c99b0fbd3e..4d2cd88dd3 100644 --- a/web/app/components/workflow/block-selector/trigger-plugin/list.tsx +++ b/web/app/components/workflow/block-selector/trigger-plugin/list.tsx @@ -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 diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index 97db41dfdf..2cc83ccf09 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -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 + paramSchemas: Record[] + output_schema: Record + 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[] diff --git a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx index 7635e0faf0..b8ecf7a33f 100644 --- a/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx +++ b/web/app/components/workflow/nodes/_base/components/agent-strategy-selector.tsx @@ -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, diff --git a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx index 750adafd45..d861a9e1c9 100644 --- a/web/app/components/workflow/nodes/_base/components/form-input-item.tsx +++ b/web/app/components/workflow/nodes/_base/components/form-input-item.tsx @@ -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 - providerType?: 'tool' | 'trigger' + providerType?: string } const FormInputItem: FC = ({ @@ -44,10 +44,10 @@ const FormInputItem: FC = ({ value, onChange, inPanel, - currentTool, + currentResource, currentProvider, extraParams, - providerType = 'tool', + providerType, }) => { const language = useLanguage() const [dynamicOptions, setDynamicOptions] = useState(null) @@ -59,6 +59,7 @@ const FormInputItem: FC = ({ type, default: defaultValue, options, + multiple, scope, } = schema as any const varInput = value[variable] @@ -76,6 +77,7 @@ const FormInputItem: FC = ({ 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 = ({ const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions( currentProvider?.plugin_id || '', currentProvider?.name || '', - currentTool?.name || '', + currentResource?.name || '', variable || '', providerType, extraParams, @@ -147,7 +149,7 @@ const FormInputItem: FC = ({ // 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 = ({ } 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 = ({ }) } + 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 = ({ onChange={handleValueChange} /> )} - {isSelect && ( + {isSelect && !isMultipleSelect && ( = ({ ) : undefined} /> )} - {isDynamicSelect && ( + {isSelect && isMultipleSelect && ( + +
+ + + {getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'} + + + + + + {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 }) => ( + + `relative cursor-pointer select-none py-2 pl-10 pr-4 ${ + focus ? 'bg-state-base-hover text-text-secondary' : 'text-text-primary' + }` + } + > + {({ selected }) => ( + <> +
+ {option.icon && ( + + )} + + {option.label[language] || option.label.en_US} + +
+ {selected && ( + + + )} + + )} +
+ ))} +
+
+
+ )} + {isDynamicSelect && !isMultipleSelect && ( = ({ )} /> )} + {isDynamicSelect && isMultipleSelect && ( + +
+ + + {isLoadingOptions + ? 'Loading...' + : getSelectedLabels(varInput?.value) || placeholder?.[language] || placeholder?.en_US || 'Select options'} + + + + + + {(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 }) => ( + + `relative cursor-pointer select-none py-2 pl-10 pr-4 ${ + focus ? 'bg-state-base-hover text-text-secondary' : 'text-text-primary' + }` + } + > + {({ selected }) => ( + <> +
+ {option.icon && ( + + )} + + {option.label[language] || option.label.en_US} + +
+ {selected && ( + + + )} + + )} +
+ ))} +
+
+
+ )} {isShowJSONEditor && isConstant && (
= ({ filterVar={getFilterVar()} schema={schema} valueTypePlaceHolder={targetVarType()} - currentTool={currentTool} + currentResource={currentResource} currentProvider={currentProvider} /> )} diff --git a/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/index.tsx b/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/index.tsx new file mode 100644 index 0000000000..6680c8ebb6 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/index.tsx @@ -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 ( + { + 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={} + onChange={onChange} + /> + ) +} + +export default memo(MixedVariableTextInput) diff --git a/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/placeholder.tsx b/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/placeholder.tsx new file mode 100644 index 0000000000..75d4c91996 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/components/mixed-variable-text-input/placeholder.tsx @@ -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 ( +
{ + e.stopPropagation() + handleInsert('') + }} + > +
+ {t('workflow.nodes.tool.insertPlaceholder1')} +
/
+
{ + e.preventDefault() + e.stopPropagation() + handleInsert('/') + })} + > + {t('workflow.nodes.tool.insertPlaceholder2')} +
+
+ +
+ ) +} + +export default Placeholder diff --git a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx index 7af5c5f901..9944fca6dc 100644 --- a/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx +++ b/web/app/components/workflow/nodes/_base/components/variable/var-reference-picker.tsx @@ -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 = ({ minWidth, popupFor, zIndex, - currentTool, + currentResource, currentProvider, }) => { const { t } = useTranslation() @@ -328,11 +328,11 @@ const VarReferencePicker: FC = ({ const [dynamicOptions, setDynamicOptions] = useState(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 = ({ } useEffect(() => { handleFetchDynamicOptions() - }, [currentTool, currentProvider, schema]) + }, [currentResource, currentProvider, schema]) const schemaWithDynamicSelect = useMemo(() => { if (schema?.type !== FormTypeEnum.dynamicSelect) diff --git a/web/app/components/workflow/nodes/_base/types.ts b/web/app/components/workflow/nodes/_base/types.ts new file mode 100644 index 0000000000..18ad9c4e71 --- /dev/null +++ b/web/app/components/workflow/nodes/_base/types.ts @@ -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 + +// 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 +} diff --git a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx index bfb664aef1..1b6f45d78b 100644 --- a/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx +++ b/web/app/components/workflow/nodes/parameter-extractor/components/extract-parameter/import-from-tool.tsx @@ -44,7 +44,7 @@ const ImportFromTool: FC = ({ 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: diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx index 1e45b4d0b1..cdbe2c5752 100644 --- a/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx +++ b/web/app/components/workflow/nodes/tool/components/tool-form/index.tsx @@ -17,7 +17,6 @@ type Props = { currentTool?: Tool currentProvider?: ToolWithProvider extraParams?: Record - providerType?: 'tool' | 'trigger' } const ToolForm: FC = ({ @@ -30,7 +29,6 @@ const ToolForm: FC = ({ currentTool, currentProvider, extraParams, - providerType = 'tool', }) => { return (
@@ -47,7 +45,7 @@ const ToolForm: FC = ({ currentTool={currentTool} currentProvider={currentProvider} extraParams={extraParams} - providerType={providerType} + providerType='tool' /> )) } diff --git a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx index 18dd770db0..c5e5412981 100644 --- a/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx +++ b/web/app/components/workflow/nodes/tool/components/tool-form/item.tsx @@ -91,7 +91,7 @@ const ToolFormItem: FC = ({ value={value} onChange={onChange} inPanel={inPanel} - currentTool={currentTool} + currentResource={currentTool} currentProvider={currentProvider} extraParams={extraParams} providerType={providerType} diff --git a/web/app/components/workflow/nodes/tool/types.ts b/web/app/components/workflow/nodes/tool/types.ts index 6294b9b689..c4f931306f 100644 --- a/web/app/components/workflow/nodes/tool/types.ts +++ b/web/app/components/workflow/nodes/tool/types.ts @@ -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 +// Use base types directly +export { VarKindType as VarType } from '../_base/types' +export type ToolVarInputs = ResourceVarInputs export type ToolNodeType = CommonNodeType & { provider_id: string diff --git a/web/app/components/workflow/nodes/tool/use-config.ts b/web/app/components/workflow/nodes/tool/use-config.ts index c169ae71a4..f93d938f1f 100644 --- a/web/app/components/workflow/nodes/tool/use-config.ts +++ b/web/app/components/workflow/nodes/tool/use-config.ts @@ -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) diff --git a/web/app/components/workflow/nodes/trigger-plugin/default.ts b/web/app/components/workflow/nodes/trigger-plugin/default.ts index 19c971d2b8..d9b6986c09 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/default.ts +++ b/web/app/components/workflow/nodes/trigger-plugin/default.ts @@ -6,7 +6,7 @@ import { ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/block const nodeDefault: NodeDefault = { defaultValue: { plugin_id: '', - tool_name: '', + trigger_name: '', event_type: '', config: {}, }, diff --git a/web/app/components/workflow/nodes/trigger-plugin/panel.tsx b/web/app/components/workflow/nodes/trigger-plugin/panel.tsx index b656be177c..917bb8eaf7 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/panel.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/panel.tsx @@ -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> = ({ outputSchema, hasObjectOutput, isAuthenticated, + currentProvider, + currentTrigger, } = useConfig(id, data) // Convert output schema to VarItem format @@ -36,13 +38,14 @@ const Panel: FC> = ({ {isAuthenticated && triggerParameterSchema.length > 0 && ( <>
-
diff --git a/web/app/components/workflow/nodes/trigger-plugin/trigger-form/index.tsx b/web/app/components/workflow/nodes/trigger-plugin/trigger-form/index.tsx new file mode 100644 index 0000000000..f8f9d1c6c2 --- /dev/null +++ b/web/app/components/workflow/nodes/trigger-plugin/trigger-form/index.tsx @@ -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 +} + +const TriggerForm: FC = ({ + readOnly, + nodeId, + schema, + value, + onChange, + inPanel, + currentTrigger, + currentProvider, + extraParams, +}) => { + return ( +
+ { + schema.map((schema, index) => ( + + )) + } +
+ ) +} +export default TriggerForm diff --git a/web/app/components/workflow/nodes/trigger-plugin/trigger-form/item.tsx b/web/app/components/workflow/nodes/trigger-plugin/trigger-form/item.tsx new file mode 100644 index 0000000000..d096343e30 --- /dev/null +++ b/web/app/components/workflow/nodes/trigger-plugin/trigger-form/item.tsx @@ -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 +} + +const TriggerFormItem: FC = ({ + 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 ( +
+
+
+
{label[language] || label.en_US}
+ {required && ( +
*
+ )} + {!showDescription && tooltip && ( + + {tooltip[language] || tooltip.en_US} +
} + triggerClassName='ml-1 w-4 h-4' + asChild={false} + /> + )} + {showSchemaButton && ( + <> +
ยท
+ + + )} +
+ {showDescription && tooltip && ( +
{tooltip[language] || tooltip.en_US}
+ )} +
+ + + {isShowSchema && ( + + )} +
+ ) +} +export default TriggerFormItem diff --git a/web/app/components/workflow/nodes/trigger-plugin/types.ts b/web/app/components/workflow/nodes/trigger-plugin/types.ts index 46cf5fb80a..efa6f31816 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/types.ts +++ b/web/app/components/workflow/nodes/trigger-plugin/types.ts @@ -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 + output_schema: Record + parameters_schema?: Record[] + version?: string + trigger_node_version?: string plugin_id?: string - tool_name?: string - event_type?: string config?: Record - provider_id?: string - provider_type?: CollectionType - provider_name?: string } + +// Use base types directly +export { VarKindType as PluginTriggerVarType } from '../_base/types' +export type PluginTriggerVarInputs = ResourceVarInputs diff --git a/web/app/components/workflow/nodes/trigger-plugin/use-config.ts b/web/app/components/workflow/nodes/trigger-plugin/use-config.ts index c4baa3683b..d517219838 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/use-config.ts +++ b/web/app/components/workflow/nodes/trigger-plugin/use-config.ts @@ -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(id, payload) + const { inputs, setInputs: doSetInputs } = useNodeCrud( + 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(() => { - 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(() => { - return currentProvider?.tools.find(tool => tool.name === tool_name) - }, [currentProvider, tool_name]) + const currentTrigger = useMemo(() => { + 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) => { - const newInputs = produce(inputs, (draft) => { - draft.config = value - }) - doSetInputs(newInputs) - }, [inputs, doSetInputs]) + const setTriggerParameterValue = useCallback( + (value: Record) => { + 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 diff --git a/web/app/components/workflow/types.ts b/web/app/components/workflow/types.ts index beaacbe4c4..1f5ec739cf 100644 --- a/web/app/components/workflow/types.ts +++ b/web/app/components/workflow/types.ts @@ -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 = { 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', diff --git a/web/app/components/workflow/utils/tool.ts b/web/app/components/workflow/utils/tool.ts index d1dda12180..964118ad7a 100644 --- a/web/app/components/workflow/utils/tool.ts +++ b/web/app/components/workflow/utils/tool.ts @@ -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)) diff --git a/web/service/use-plugins.ts b/web/service/use-plugins.ts index 6f184d356b..b249180b33 100644 --- a/web/service/use-plugins.ts +++ b/web/service/use-plugins.ts @@ -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) => { +export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type?: string, extra?: Record) => { return useMutation({ mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', { params: { diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts index 4e9f5e3e57..2d341a5d41 100644 --- a/web/service/use-triggers.ts +++ b/web/service/use-triggers.ts @@ -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 || {},