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("provider", type=str, required=True, location="args")
parser.add_argument("action", 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("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") parser.add_argument("provider_type", type=str, required=True, location="args")
args = parser.parse_args() args = parser.parse_args()
try: try:
options = PluginParameterService.get_dynamic_select_options( options = PluginParameterService.get_dynamic_select_options(
tenant_id, tenant_id=tenant_id,
user_id, user_id=user_id,
args["plugin_id"], plugin_id=args["plugin_id"],
args["provider"], provider=args["provider"],
args["action"], action=args["action"],
args["parameter"], parameter=args["parameter"],
args["extra"], credential_id=args["credential_id"],
args["provider_type"], provider_type=args["provider_type"],
) )
except PluginDaemonClientSideError as e: except PluginDaemonClientSideError as e:
raise ValueError(e) raise ValueError(e)

View File

@ -66,9 +66,9 @@ class ProviderConfigEncrypter:
return data 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 return a deep copy of credentials with masked values
""" """
@ -91,6 +91,10 @@ class ProviderConfigEncrypter:
return data 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]: def decrypt(self, data: dict[str, str]) -> dict[str, Any]:
""" """
decrypt tool credentials with tenant id decrypt tool credentials with tenant id

View File

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

View File

@ -40,6 +40,10 @@ class TriggerParameter(BaseModel):
template: Optional[PluginParameterTemplate] = Field(default=None, description="The template of the parameter") template: Optional[PluginParameterTemplate] = Field(default=None, description="The template of the parameter")
scope: Optional[str] = None scope: Optional[str] = None
required: Optional[bool] = False 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 default: Union[int, float, str, list, None] = None
min: Union[float, int, None] = None min: Union[float, int, None] = None
max: 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.plugin.impl.dynamic_select import DynamicSelectClient
from core.tools.tool_manager import ToolManager from core.tools.tool_manager import ToolManager
from core.tools.utils.encryption import create_tool_provider_encrypter 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 core.trigger.trigger_manager import TriggerManager
from extensions.ext_database import db from extensions.ext_database import db
from models.tools import BuiltinToolProvider from models.tools import BuiltinToolProvider
from services.trigger.trigger_provider_service import TriggerProviderService
from services.trigger.trigger_subscription_builder_service import TriggerSubscriptionBuilderService
class PluginParameterService: class PluginParameterService:
@ -23,7 +27,7 @@ class PluginParameterService:
provider: str, provider: str,
action: str, action: str,
parameter: str, parameter: str,
extra: dict | None, credential_id: str | None,
provider_type: Literal["tool", "trigger"], provider_type: Literal["tool", "trigger"],
) -> Sequence[PluginParameterOption]: ) -> Sequence[PluginParameterOption]:
""" """
@ -37,7 +41,7 @@ class PluginParameterService:
parameter: The parameter name. parameter: The parameter name.
""" """
credentials: Mapping[str, Any] = {} credentials: Mapping[str, Any] = {}
credential_type: str = CredentialType.API_KEY.value credential_type: str = CredentialType.UNAUTHORIZED.value
match provider_type: match provider_type:
case "tool": case "tool":
provider_controller = ToolManager.get_builtin_provider(provider, tenant_id) provider_controller = ToolManager.get_builtin_provider(provider, tenant_id)
@ -53,8 +57,7 @@ class PluginParameterService:
else: else:
# fetch credentials from db # fetch credentials from db
with Session(db.engine) as session: with Session(db.engine) as session:
if extra and "credential_id" in extra: if credential_id:
credential_id = extra["credential_id"]
db_record = ( db_record = (
session.query(BuiltinToolProvider) session.query(BuiltinToolProvider)
.where( .where(
@ -82,7 +85,21 @@ class PluginParameterService:
credential_type = db_record.credential_type credential_type = db_record.credential_type
case "trigger": case "trigger":
provider_controller = TriggerManager.get_trigger_provider(tenant_id, TriggerProviderID(provider)) 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 _: case _:
raise ValueError(f"Invalid provider type: {provider_type}") raise ValueError(f"Invalid provider type: {provider_type}")

View File

@ -69,7 +69,7 @@ class TriggerProviderService:
controller=provider_controller, controller=provider_controller,
subscription=subscription, subscription=subscription,
) )
subscription.credentials = encrypter.decrypt(subscription.credentials) subscription.credentials = encrypter.mask_credentials(subscription.credentials)
return subscriptions return subscriptions
@classmethod @classmethod
@ -165,6 +165,34 @@ class TriggerProviderService:
logger.exception("Failed to add trigger provider") logger.exception("Failed to add trigger provider")
raise ValueError(str(e)) 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 @classmethod
def delete_trigger_provider(cls, session: Session, tenant_id: str, subscription_id: str): 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_id: tool.provider_id,
provider_type: tool.provider_type as CollectionType, provider_type: tool.provider_type as CollectionType,
provider_name: tool.provider_name, provider_name: tool.provider_name,
tool_name: tool.tool_name, tool_name: tool.trigger_name,
tool_label: tool.tool_label, tool_label: tool.tool_label,
tool_parameters: tool.params, tool_parameters: tool.params,
notAuthor: !tool.is_team_authorization, notAuthor: !tool.is_team_authorization,

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -1,20 +1,19 @@
'use client' 'use client'
import type { FC } from 'react' import type { FC } from 'react'
import { useEffect, useState } 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 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 { useLanguage } from '@/app/components/header/account-setting/model-provider-page/hooks'
import { FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations' 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 { VarType } from '@/app/components/workflow/types'
import { useFetchDynamicOptions } from '@/service/use-plugins' 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 FormInputTypeSwitch from './form-input-type-switch'
import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list' import useAvailableVarList from '@/app/components/workflow/nodes/_base/hooks/use-available-var-list'
import Input from '@/app/components/base/input' import Input from '@/app/components/base/input'
import { SimpleSelect } from '@/app/components/base/select' 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 FormInputBoolean from './form-input-boolean'
import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector' import AppSelector from '@/app/components/plugins/plugin-detail-panel/app-selector'
import ModelParameterModal from '@/app/components/plugins/plugin-detail-panel/model-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 CodeEditor from '@/app/components/workflow/nodes/_base/components/editor/code-editor'
import { CodeLanguage } from '@/app/components/workflow/nodes/code/types' import { CodeLanguage } from '@/app/components/workflow/nodes/code/types'
import cn from '@/utils/classnames' 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 = { type Props = {
readOnly: boolean readOnly: boolean
nodeId: string nodeId: string
schema: CredentialFormSchema schema: CredentialFormSchema
value: ToolVarInputs value: ResourceVarInputs
onChange: (value: any) => void onChange: (value: any) => void
inPanel?: boolean inPanel?: boolean
currentTool?: Tool currentResource?: BaseResource
currentProvider?: ToolWithProvider currentProvider?: BaseResourceProvider
extraParams?: Record<string, any> extraParams?: Record<string, any>
providerType?: 'tool' | 'trigger' providerType?: string
} }
const FormInputItem: FC<Props> = ({ const FormInputItem: FC<Props> = ({
@ -44,10 +44,10 @@ const FormInputItem: FC<Props> = ({
value, value,
onChange, onChange,
inPanel, inPanel,
currentTool, currentResource,
currentProvider, currentProvider,
extraParams, extraParams,
providerType = 'tool', providerType,
}) => { }) => {
const language = useLanguage() const language = useLanguage()
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null) const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
@ -59,6 +59,7 @@ const FormInputItem: FC<Props> = ({
type, type,
default: defaultValue, default: defaultValue,
options, options,
multiple,
scope, scope,
} = schema as any } = schema as any
const varInput = value[variable] const varInput = value[variable]
@ -76,6 +77,7 @@ const FormInputItem: FC<Props> = ({
const showTypeSwitch = isNumber || isBoolean || isObject || isArray const showTypeSwitch = isNumber || isBoolean || isObject || isArray
const isConstant = varInput?.type === VarKindType.constant || !varInput?.type const isConstant = varInput?.type === VarKindType.constant || !varInput?.type
const showVariableSelector = isFile || varInput?.type === VarKindType.variable const showVariableSelector = isFile || varInput?.type === VarKindType.variable
const isMultipleSelect = multiple && (isSelect || isDynamicSelect)
const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, { const { availableVars, availableNodesWithParent } = useAvailableVarList(nodeId, {
onlyLeafNodeVar: false, onlyLeafNodeVar: false,
@ -138,7 +140,7 @@ const FormInputItem: FC<Props> = ({
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions( const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions(
currentProvider?.plugin_id || '', currentProvider?.plugin_id || '',
currentProvider?.name || '', currentProvider?.name || '',
currentTool?.name || '', currentResource?.name || '',
variable || '', variable || '',
providerType, providerType,
extraParams, extraParams,
@ -147,7 +149,7 @@ const FormInputItem: FC<Props> = ({
// Fetch dynamic options when component mounts or dependencies change // Fetch dynamic options when component mounts or dependencies change
useEffect(() => { useEffect(() => {
const fetchOptions = async () => { const fetchOptions = async () => {
if (isDynamicSelect && currentTool && currentProvider) { if (isDynamicSelect && currentResource && currentProvider) {
setIsLoadingOptions(true) setIsLoadingOptions(true)
try { try {
const data = await fetchDynamicOptions() const data = await fetchDynamicOptions()
@ -164,7 +166,7 @@ const FormInputItem: FC<Props> = ({
} }
fetchOptions() fetchOptions()
}, [isDynamicSelect, currentTool?.name, currentProvider?.name, variable, extraParams]) }, [isDynamicSelect, currentResource?.name, currentProvider?.name, variable, extraParams])
const handleTypeChange = (newType: string) => { const handleTypeChange = (newType: string) => {
if (newType === VarKindType.variable) { 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) => { const handleAppOrModelSelect = (newValue: any) => {
onChange({ onChange({
...value, ...value,
@ -250,7 +270,7 @@ const FormInputItem: FC<Props> = ({
onChange={handleValueChange} onChange={handleValueChange}
/> />
)} )}
{isSelect && ( {isSelect && !isMultipleSelect && (
<SimpleSelect <SimpleSelect
wrapperClassName='h-8 grow' wrapperClassName='h-8 grow'
disabled={readOnly} disabled={readOnly}
@ -277,7 +297,64 @@ const FormInputItem: FC<Props> = ({
) : undefined} ) : 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 <SimpleSelect
wrapperClassName='h-8 grow' wrapperClassName='h-8 grow'
disabled={readOnly || isLoadingOptions} 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 && ( {isShowJSONEditor && isConstant && (
<div className='mt-1 w-full'> <div className='mt-1 w-full'>
<CodeEditor <CodeEditor
@ -349,7 +485,7 @@ const FormInputItem: FC<Props> = ({
filterVar={getFilterVar()} filterVar={getFilterVar()}
schema={schema} schema={schema}
valueTypePlaceHolder={targetVarType()} valueTypePlaceHolder={targetVarType()}
currentTool={currentTool} currentResource={currentResource}
currentProvider={currentProvider} 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 { getNodeInfoById, isConversationVar, isENV, isSystemVar, varTypeToStructType } from './utils'
import ConstantField from './constant-field' import ConstantField from './constant-field'
import cn from '@/utils/classnames' 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 { 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 { type CredentialFormSchema, type FormOption, FormTypeEnum } from '@/app/components/header/account-setting/model-provider-page/declarations'
import { BlockEnum } from '@/app/components/workflow/types' import { BlockEnum } from '@/app/components/workflow/types'
@ -34,6 +34,7 @@ import {
useWorkflowVariables, useWorkflowVariables,
} from '@/app/components/workflow/hooks' } from '@/app/components/workflow/hooks'
import { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types' 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 TypeSelector from '@/app/components/workflow/nodes/_base/components/selector'
import AddButton from '@/app/components/base/button/add-button' import AddButton from '@/app/components/base/button/add-button'
import Badge from '@/app/components/base/badge' 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 VarFullPathPanel from './var-full-path-panel'
import { noop } from 'lodash-es' import { noop } from 'lodash-es'
import { useFetchDynamicOptions } from '@/service/use-plugins' 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' import { VariableIconWithColor } from '@/app/components/workflow/nodes/_base/components/variable/variable-label'
const TRIGGER_DEFAULT_WIDTH = 227 const TRIGGER_DEFAULT_WIDTH = 227
@ -72,8 +72,8 @@ type Props = {
minWidth?: number minWidth?: number
popupFor?: 'assigned' | 'toAssigned' popupFor?: 'assigned' | 'toAssigned'
zIndex?: number zIndex?: number
currentTool?: Tool currentResource?: BaseResource
currentProvider?: ToolWithProvider currentProvider?: BaseResourceProvider
} }
const DEFAULT_VALUE_SELECTOR: Props['value'] = [] const DEFAULT_VALUE_SELECTOR: Props['value'] = []
@ -103,7 +103,7 @@ const VarReferencePicker: FC<Props> = ({
minWidth, minWidth,
popupFor, popupFor,
zIndex, zIndex,
currentTool, currentResource,
currentProvider, currentProvider,
}) => { }) => {
const { t } = useTranslation() const { t } = useTranslation()
@ -328,11 +328,11 @@ const VarReferencePicker: FC<Props> = ({
const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null) const [dynamicOptions, setDynamicOptions] = useState<FormOption[] | null>(null)
const [isLoading, setIsLoading] = useState(false) const [isLoading, setIsLoading] = useState(false)
const { mutateAsync: fetchDynamicOptions } = useFetchDynamicOptions( 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', 'tool',
) )
const handleFetchDynamicOptions = async () => { const handleFetchDynamicOptions = async () => {
if (schema?.type !== FormTypeEnum.dynamicSelect || !currentTool || !currentProvider) if (schema?.type !== FormTypeEnum.dynamicSelect || !currentResource || !currentProvider)
return return
setIsLoading(true) setIsLoading(true)
try { try {
@ -345,7 +345,7 @@ const VarReferencePicker: FC<Props> = ({
} }
useEffect(() => { useEffect(() => {
handleFetchDynamicOptions() handleFetchDynamicOptions()
}, [currentTool, currentProvider, schema]) }, [currentResource, currentProvider, schema])
const schemaWithDynamicSelect = useMemo(() => { const schemaWithDynamicSelect = useMemo(() => {
if (schema?.type !== FormTypeEnum.dynamicSelect) 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 workflowTools = useStore(s => s.workflowTools)
const handleSelectTool = useCallback((_type: BlockEnum, toolInfo?: ToolDefaultValue) => { 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 = (() => { const currentTools = (() => {
switch (provider_type) { switch (provider_type) {
case CollectionType.builtIn: case CollectionType.builtIn:

View File

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

View File

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

View File

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

View File

@ -32,7 +32,7 @@ const useConfig = (id: string, payload: ToolNodeType) => {
* tool_parameters: tool dynamic setting(form type = llm) * tool_parameters: tool dynamic setting(form type = llm)
* output_schema: tool dynamic output * 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 isBuiltIn = provider_type === CollectionType.builtIn
const buildInTools = useStore(s => s.buildInTools) const buildInTools = useStore(s => s.buildInTools)
const customTools = useStore(s => s.customTools) 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> = { const nodeDefault: NodeDefault<PluginTriggerNodeType> = {
defaultValue: { defaultValue: {
plugin_id: '', plugin_id: '',
tool_name: '', trigger_name: '',
event_type: '', event_type: '',
config: {}, 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 OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars'
import type { NodePanelProps } from '@/app/components/workflow/types' import type { NodePanelProps } from '@/app/components/workflow/types'
import useConfig from './use-config' 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 StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show'
import { Type } from '../llm/types' import { Type } from '../llm/types'
@ -21,6 +21,8 @@ const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
outputSchema, outputSchema,
hasObjectOutput, hasObjectOutput,
isAuthenticated, isAuthenticated,
currentProvider,
currentTrigger,
} = useConfig(id, data) } = useConfig(id, data)
// Convert output schema to VarItem format // Convert output schema to VarItem format
@ -36,13 +38,14 @@ const Panel: FC<NodePanelProps<PluginTriggerNodeType>> = ({
{isAuthenticated && triggerParameterSchema.length > 0 && ( {isAuthenticated && triggerParameterSchema.length > 0 && (
<> <>
<div className='px-4 pb-4'> <div className='px-4 pb-4'>
<ToolForm <TriggerForm
readOnly={readOnly} readOnly={readOnly}
nodeId={id} nodeId={id}
schema={triggerParameterSchema as any} schema={triggerParameterSchema as any}
value={triggerParameterValue} value={triggerParameterValue}
onChange={setTriggerParameterValue} onChange={setTriggerParameterValue}
providerType="trigger" currentProvider={currentProvider}
currentTrigger={currentTrigger}
/> />
</div> </div>
<Split /> <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 { CommonNodeType } from '@/app/components/workflow/types'
import type { CollectionType } from '@/app/components/tools/types' import type { CollectionType } from '@/app/components/tools/types'
import type { ResourceVarInputs } from '../_base/types'
export type PluginTriggerNodeType = CommonNodeType & { 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 plugin_id?: string
tool_name?: string
event_type?: string
config?: Record<string, any> 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 type { PluginTriggerNodeType } from './types'
import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud'
import { useNodesReadOnly } from '@/app/components/workflow/hooks' import { useNodesReadOnly } from '@/app/components/workflow/hooks'
import { useAllTriggerPlugins, useTriggerSubscriptions } from '@/service/use-triggers' import {
useAllTriggerPlugins,
useTriggerSubscriptions,
} from '@/service/use-triggers'
import { import {
addDefaultValue, addDefaultValue,
toolParametersToFormSchemas, toolParametersToFormSchemas,
} from '@/app/components/tools/utils/to-form-schema' } from '@/app/components/tools/utils/to-form-schema'
import type { InputVar } from '@/app/components/workflow/types' import type { InputVar } from '@/app/components/workflow/types'
import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/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 useConfig = (id: string, payload: PluginTriggerNodeType) => {
const { nodesReadOnly: readOnly } = useNodesReadOnly() const { nodesReadOnly: readOnly } = useNodesReadOnly()
const { data: triggerPlugins = [] } = useAllTriggerPlugins() 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 // Construct provider for authentication check
const authProvider = useMemo(() => { const authProvider = useMemo(() => {
if (provider_id && provider_name) if (provider_id && provider_name) return `${provider_id}/${provider_name}`
return `${provider_id}/${provider_name}`
return provider_id || '' return provider_id || ''
}, [provider_id, provider_name]) }, [provider_id, provider_name])
@ -33,21 +38,26 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
) )
const currentProvider = useMemo<TriggerWithProvider | undefined>(() => { const currentProvider = useMemo<TriggerWithProvider | undefined>(() => {
return triggerPlugins.find(provider => return triggerPlugins.find(
provider.name === provider_name provider =>
|| provider.id === provider_id provider.name === provider_name
|| (provider_id && provider.plugin_id === provider_id), || provider.id === provider_id
|| (provider_id && provider.plugin_id === provider_id),
) )
}, [triggerPlugins, provider_name, provider_id]) }, [triggerPlugins, provider_name, provider_id])
const currentTrigger = useMemo<Tool | undefined>(() => { const currentTrigger = useMemo<Trigger | undefined>(() => {
return currentProvider?.tools.find(tool => tool.name === tool_name) return currentProvider?.triggers.find(
}, [currentProvider, tool_name]) trigger => trigger.name === trigger_name,
)
}, [currentProvider, trigger_name])
// Dynamic subscription parameters (from subscription_schema.parameters_schema) // Dynamic subscription parameters (from subscription_schema.parameters_schema)
const subscriptionParameterSchema = useMemo(() => { const subscriptionParameterSchema = useMemo(() => {
if (!currentProvider?.subscription_schema?.parameters_schema) return [] 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]) }, [currentProvider])
// Dynamic trigger parameters (from specific trigger.parameters) // Dynamic trigger parameters (from specific trigger.parameters)
@ -66,22 +76,28 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
return addDefaultValue(config || {}, triggerParameterSchema) return addDefaultValue(config || {}, triggerParameterSchema)
}, [triggerParameterSchema, config]) }, [triggerParameterSchema, config])
const setTriggerParameterValue = useCallback((value: Record<string, any>) => { const setTriggerParameterValue = useCallback(
const newInputs = produce(inputs, (draft) => { (value: Record<string, any>) => {
draft.config = value const newInputs = produce(inputs, (draft) => {
}) draft.config = value
doSetInputs(newInputs) })
}, [inputs, doSetInputs]) doSetInputs(newInputs)
},
[inputs, doSetInputs],
)
const setInputVar = useCallback((variable: InputVar, varDetail: InputVar) => { const setInputVar = useCallback(
const newInputs = produce(inputs, (draft) => { (variable: InputVar, varDetail: InputVar) => {
draft.config = { const newInputs = produce(inputs, (draft) => {
...draft.config, draft.config = {
[variable.variable]: varDetail.variable, ...draft.config,
} [variable.variable]: varDetail.variable,
}) }
doSetInputs(newInputs) })
}, [inputs, doSetInputs]) doSetInputs(newInputs)
},
[inputs, doSetInputs],
)
// Get output schema // Get output schema
const outputSchema = useMemo(() => { const outputSchema = useMemo(() => {
@ -91,7 +107,9 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
// Check if trigger has complex output structure // Check if trigger has complex output structure
const hasObjectOutput = useMemo(() => { const hasObjectOutput = useMemo(() => {
const properties = outputSchema.properties || {} 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]) }, [outputSchema])
// Authentication status check // Authentication status check
@ -109,10 +127,16 @@ const useConfig = (id: string, payload: PluginTriggerNodeType) => {
const methods = [] 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') 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') methods.push('api_key')
return methods return methods

View File

@ -5,7 +5,7 @@ import type {
XYPosition, XYPosition,
} from 'reactflow' } from 'reactflow'
import type { Resolution, TransferMethod } from '@/types/app' 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 { VarType as VarKindType } from '@/app/components/workflow/nodes/tool/types'
import type { FileResponse, NodeTracing, PanelProps } from '@/types/workflow' import type { FileResponse, NodeTracing, PanelProps } from '@/types/workflow'
import type { Collection, Tool } from '@/app/components/tools/types' 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 } 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 { export enum WorkflowRunningStatus {
Waiting = 'waiting', Waiting = 'waiting',

View File

@ -14,7 +14,7 @@ export const getToolCheckParams = (
workflowTools: ToolWithProvider[], workflowTools: ToolWithProvider[],
language: string, 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 isBuiltIn = provider_type === CollectionType.builtIn
const currentTools = provider_type === CollectionType.builtIn ? buildInTools : provider_type === CollectionType.custom ? customTools : workflowTools const currentTools = provider_type === CollectionType.builtIn ? buildInTools : provider_type === CollectionType.custom ? customTools : workflowTools
const currCollection = currentTools.find(item => canFindTool(item.id, provider_id)) 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({ return useMutation({
mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', { mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', {
params: { params: {

View File

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