diff --git a/.gitignore b/.gitignore index 30432c4302..5f3614c628 100644 --- a/.gitignore +++ b/.gitignore @@ -218,3 +218,6 @@ mise.toml .roo/ api/.env.backup /clickzetta + +# mcp +.serena \ No newline at end of file diff --git a/api/controllers/console/workspace/trigger_providers.py b/api/controllers/console/workspace/trigger_providers.py index aa64461d08..a26f5231ae 100644 --- a/api/controllers/console/workspace/trigger_providers.py +++ b/api/controllers/console/workspace/trigger_providers.py @@ -293,13 +293,14 @@ class TriggerOAuthAuthorizeApi(Resource): credential_type=CredentialType.OAUTH2, credential_expires_at=0, expires_at=0, + name=f"{provider_name} OAuth Authentication", ) # Create response with cookie response = make_response( jsonable_encoder( { - "authorization_url": authorization_url_response, + "authorization_url": authorization_url_response.authorization_url, "subscription_builder": subscription_builder, } ) @@ -377,6 +378,7 @@ class TriggerOAuthCallbackApi(Resource): credential_type=CredentialType.OAUTH2, credential_expires_at=expires_at, expires_at=expires_at, + name=f"{provider_name} OAuth Authentication", ) # Redirect to OAuth callback page return redirect(f"{dify_config.CONSOLE_WEB_URL}/oauth-callback?subscription_id={subscription_builder.id}") diff --git a/web/app/components/workflow/block-selector/types.ts b/web/app/components/workflow/block-selector/types.ts index 17cf1ae04f..97db41dfdf 100644 --- a/web/app/components/workflow/block-selector/types.ts +++ b/web/app/components/workflow/block-selector/types.ts @@ -58,15 +58,56 @@ export type TriggerParameter = { name: string label: TypeWithI18N description?: TypeWithI18N - type: string + type: 'string' | 'number' | 'boolean' | 'select' | 'file' | 'files' + | 'model-selector' | 'app-selector' | 'object' | 'array' | 'dynamic-select' + auto_generate?: { + type: string + value?: any + } | null + template?: { + type: string + value?: any + } | null + scope?: string | null required?: boolean default?: any + min?: number | null + max?: number | null + precision?: number | null + options?: Array<{ + value: string + label: TypeWithI18N + icon?: string | null + }> | null +} + +export type TriggerCredentialField = { + type: 'secret-input' | 'text-input' | 'select' | 'boolean' + | 'app-selector' | 'model-selector' | 'tools-selector' + name: string + scope?: string | null + required: boolean + default?: string | number | boolean | Array | null + options?: Array<{ + value: string + label: TypeWithI18N + }> | null + label: TypeWithI18N + help?: TypeWithI18N + url?: string | null + placeholder?: TypeWithI18N +} + +export type TriggerSubscriptionSchema = { + parameters_schema: TriggerParameter[] + properties_schema: TriggerCredentialField[] } export type TriggerIdentity = { author: string name: string - version: string + label: TypeWithI18N + provider: string } export type TriggerDescription = { @@ -92,6 +133,9 @@ export type TriggerProviderApiEntity = { tags: string[] plugin_id?: string plugin_unique_identifier?: string + credentials_schema: TriggerCredentialField[] + oauth_client_schema: TriggerCredentialField[] + subscription_schema: TriggerSubscriptionSchema triggers: TriggerApiEntity[] } @@ -99,4 +143,56 @@ export type TriggerProviderApiEntity = { export type TriggerWithProvider = Collection & { tools: Tool[] // Use existing Tool type for compatibility meta: PluginMeta + credentials_schema?: TriggerCredentialField[] + oauth_client_schema?: TriggerCredentialField[] + subscription_schema?: TriggerSubscriptionSchema +} + +// ===== API Service Types ===== + +// Trigger subscription instance types +export type TriggerSubscription = { + id: string + name: string + provider: string + credential_type: 'api_key' | 'oauth2' | 'unauthorized' + credentials: Record + endpoint: string + parameters: Record + properties: Record +} + +export type TriggerSubscriptionBuilder = { + id: string + name: string + provider: string + endpoint: string + parameters: Record + properties: Record + credentials: Record + credential_type: 'api_key' | 'oauth2' | 'unauthorized' +} + +// OAuth configuration types +export type TriggerOAuthConfig = { + configured: boolean + custom_configured: boolean + custom_enabled: boolean + params: { + client_id: string + client_secret: string + } +} + +export type TriggerOAuthClientParams = { + client_id: string + client_secret: string + authorization_url?: string + token_url?: string + scope?: string +} + +export type TriggerOAuthResponse = { + authorization_url: string + subscription_builder: TriggerSubscriptionBuilder } diff --git a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx index 3d1b2b7df2..c21e919196 100644 --- a/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx +++ b/web/app/components/workflow/nodes/_base/components/workflow-panel/index.tsx @@ -59,11 +59,11 @@ import PanelWrap from '../before-run-form/panel-wrap' import SpecialResultPanel from '@/app/components/workflow/run/special-result-panel' import { Stop } from '@/app/components/base/icons/src/vender/line/mediaAndDevices' import { - AuthorizedInNode, PluginAuth, } from '@/app/components/plugins/plugin-auth' import { AuthCategory } from '@/app/components/plugins/plugin-auth' import { canFindTool } from '@/utils' +import NodeAuth from './node-auth-factory' type BasePanelProps = { children: ReactNode @@ -235,9 +235,13 @@ const BasePanel: FC = ({ const currCollection = useMemo(() => { return buildInTools.find(item => canFindTool(item.id, data.provider_id)) }, [buildInTools, data.provider_id]) - const showPluginAuth = useMemo(() => { - return data.type === BlockEnum.Tool && currCollection?.allow_delete - }, [currCollection, data.type]) + + // Unified check for any node that needs authentication UI + const needsAuth = useMemo(() => { + return (data.type === BlockEnum.Tool && currCollection?.allow_delete) + || (data.type === BlockEnum.TriggerPlugin) + }, [data.type, currCollection?.allow_delete]) + const handleAuthorizationItemClick = useCallback((credential_id: string) => { handleNodeDataUpdateWithSyncDraft({ id, @@ -379,7 +383,7 @@ const BasePanel: FC = ({ /> { - showPluginAuth && ( + needsAuth && data.type === BlockEnum.Tool && currCollection?.allow_delete && ( = ({ value={tabType} onChange={setTabType} /> - ) } { - !showPluginAuth && ( + needsAuth && data.type !== BlockEnum.Tool && ( +
+ + +
+ ) + } + { + !needsAuth && (
void +} + +const NodeAuth: FC = ({ data, onAuthorizationChange }) => { + const buildInTools = useStore(s => s.buildInTools) + const { notify } = useToastContext() + + // Construct the correct provider path for trigger plugins + // Format should be: plugin_id/provider_name (e.g., "langgenius/github_trigger/github_trigger") + const provider = useMemo(() => { + if (data.type === BlockEnum.TriggerPlugin) { + // If we have both plugin_id and provider_name, construct the full path + if (data.provider_id && data.provider_name) + return `${data.provider_id}/${data.provider_name}` + + // Otherwise use provider_id as fallback (might be already complete) + return data.provider_id || '' + } + return data.provider_id || '' + }, [data.type, data.provider_id, data.provider_name]) + + // Always call hooks at the top level + const { data: subscriptions = [] } = useTriggerSubscriptions( + provider, + data.type === BlockEnum.TriggerPlugin && !!provider, + ) + const deleteSubscription = useDeleteTriggerSubscription() + const initiateTriggerOAuth = useInitiateTriggerOAuth() + const invalidateSubscriptions = useInvalidateTriggerSubscriptions() + + const currCollection = useMemo(() => { + return buildInTools.find(item => canFindTool(item.id, data.provider_id)) + }, [buildInTools, data.provider_id]) + + // Convert TriggerSubscription to AuthSubscription format + const authSubscription: AuthSubscription = useMemo(() => { + if (data.type !== BlockEnum.TriggerPlugin) { + return { + id: '', + name: '', + status: 'not_configured', + credentials: {}, + } + } + + const subscription = subscriptions[0] // Use first subscription if available + + if (!subscription) { + return { + id: '', + name: '', + status: 'not_configured', + credentials: {}, + } + } + + const status = subscription.credential_type === 'unauthorized' + ? 'not_configured' + : 'authorized' + + return { + id: subscription.id, + name: subscription.name, + status, + credentials: subscription.credentials, + } + }, [data.type, subscriptions]) + + const handleConfigure = useCallback(async () => { + if (!provider) return + + try { + // Directly initiate OAuth flow, backend will automatically create subscription builder + const response = await initiateTriggerOAuth.mutateAsync(provider) + if (response.authorization_url) { + // Open OAuth authorization window + const authWindow = window.open(response.authorization_url, 'oauth_authorization', 'width=600,height=600') + + // Monitor window closure and refresh subscription list + const checkClosed = setInterval(() => { + if (authWindow?.closed) { + clearInterval(checkClosed) + invalidateSubscriptions(provider) + } + }, 1000) + } + } + catch (error: any) { + notify({ + type: 'error', + message: `Failed to configure authentication: ${error.message}`, + }) + } + }, [provider, initiateTriggerOAuth, invalidateSubscriptions, notify]) + + const handleRemove = useCallback(() => { + if (authSubscription.id) + deleteSubscription.mutate(authSubscription.id) + }, [authSubscription.id, deleteSubscription]) + + // Tool authentication + if (data.type === BlockEnum.Tool && currCollection?.allow_delete) { + return ( + + ) + } + + // Trigger Plugin authentication + if (data.type === BlockEnum.TriggerPlugin) { + return ( + + ) + } + + // No authentication needed + return null +} + +export default memo(NodeAuth) diff --git a/web/app/components/workflow/nodes/trigger-plugin/components/authentication-menu.tsx b/web/app/components/workflow/nodes/trigger-plugin/components/authentication-menu.tsx new file mode 100644 index 0000000000..78de87010f --- /dev/null +++ b/web/app/components/workflow/nodes/trigger-plugin/components/authentication-menu.tsx @@ -0,0 +1,144 @@ +'use client' + +import type { FC } from 'react' +import { memo, useCallback, useState } from 'react' +import { useTranslation } from 'react-i18next' +import { RiArrowDownSLine } from '@remixicon/react' +import Button from '@/app/components/base/button' +import Indicator from '@/app/components/header/indicator' +import cn from '@/utils/classnames' + +export type AuthenticationStatus = 'authorized' | 'not_configured' | 'error' + +export type AuthSubscription = { + id: string + name: string + status: AuthenticationStatus + credentials?: Record +} + +type AuthenticationMenuProps = { + subscription?: AuthSubscription + onConfigure: () => void + onRemove: () => void + className?: string +} + +const AuthenticationMenu: FC = ({ + subscription, + onConfigure, + onRemove, + className, +}) => { + const { t } = useTranslation() + const [isOpen, setIsOpen] = useState(false) + + const getStatusConfig = useCallback(() => { + if (!subscription) { + return { + label: t('workflow.nodes.triggerPlugin.notConfigured'), + color: 'red' as const, + } + } + + switch (subscription.status) { + case 'authorized': + return { + label: t('workflow.nodes.triggerPlugin.authorized'), + color: 'green' as const, + } + case 'error': + return { + label: t('workflow.nodes.triggerPlugin.error'), + color: 'red' as const, + } + default: + return { + label: t('workflow.nodes.triggerPlugin.notConfigured'), + color: 'red' as const, + } + } + }, [subscription, t]) + + const statusConfig = getStatusConfig() + + const handleConfigure = useCallback(() => { + onConfigure() + setIsOpen(false) + }, [onConfigure]) + + const handleRemove = useCallback(() => { + onRemove() + setIsOpen(false) + }, [onRemove]) + + return ( +
+ + + {isOpen && ( + <> + {/* Backdrop */} +
setIsOpen(false)} + /> + + {/* Dropdown Menu */} +
+
+ + {subscription && subscription.status === 'authorized' && ( + + )} +
+
+ + )} +
+ ) +} + +export default memo(AuthenticationMenu) diff --git a/web/app/components/workflow/nodes/trigger-plugin/default.ts b/web/app/components/workflow/nodes/trigger-plugin/default.ts index e63563df3d..19c971d2b8 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/default.ts +++ b/web/app/components/workflow/nodes/trigger-plugin/default.ts @@ -6,11 +6,11 @@ import { ALL_COMPLETION_AVAILABLE_BLOCKS } from '@/app/components/workflow/block const nodeDefault: NodeDefault = { defaultValue: { plugin_id: '', - plugin_name: '', + tool_name: '', event_type: '', config: {}, }, - getAvailablePrevNodes(isChatMode: boolean) { + getAvailablePrevNodes(_isChatMode: boolean) { return [] }, getAvailableNextNodes(isChatMode: boolean) { @@ -19,7 +19,7 @@ const nodeDefault: NodeDefault = { : ALL_COMPLETION_AVAILABLE_BLOCKS return nodes.filter(type => type !== BlockEnum.Start) }, - checkValid(payload: PluginTriggerNodeType, t: any) { + checkValid(_payload: PluginTriggerNodeType, _t: any) { return { isValid: true, errorMessage: '', diff --git a/web/app/components/workflow/nodes/trigger-plugin/node.tsx b/web/app/components/workflow/nodes/trigger-plugin/node.tsx index f40baec58e..982b3e62d8 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/node.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/node.tsx @@ -2,54 +2,45 @@ import type { FC } from 'react' import React from 'react' import type { PluginTriggerNodeType } from './types' import type { NodeProps } from '@/app/components/workflow/types' +import useConfig from './use-config' const Node: FC> = ({ + id, data, }) => { + const { isAuthenticated } = useConfig(id, data) const { config = {} } = data const configKeys = Object.keys(config) - if (!data.plugin_name && configKeys.length === 0) + // Only show config when authenticated and has config values + if (!isAuthenticated || configKeys.length === 0) return null return (
- {data.plugin_name && ( -
- {data.plugin_name} - {data.event_type && ( -
- {data.event_type} -
- )} -
- )} - - {configKeys.length > 0 && ( -
- {configKeys.map((key, index) => ( +
+ {configKeys.map((key, index) => ( +
-
- {key} -
-
- {typeof config[key] === 'string' && config[key].includes('secret') - ? '********' - : String(config[key] || '')} -
+ {key}
- ))} -
- )} +
+ {typeof config[key] === 'string' && config[key].includes('secret') + ? '********' + : String(config[key] || '')} +
+
+ ))} +
) } diff --git a/web/app/components/workflow/nodes/trigger-plugin/panel.tsx b/web/app/components/workflow/nodes/trigger-plugin/panel.tsx index 1538a66706..3187741734 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/panel.tsx +++ b/web/app/components/workflow/nodes/trigger-plugin/panel.tsx @@ -1,37 +1,85 @@ import type { FC } from 'react' import React from 'react' import type { PluginTriggerNodeType } from './types' -import Field from '@/app/components/workflow/nodes/_base/components/field' +import Split from '@/app/components/workflow/nodes/_base/components/split' +import OutputVars, { VarItem } from '@/app/components/workflow/nodes/_base/components/output-vars' import type { NodePanelProps } from '@/app/components/workflow/types' +import useConfig from './use-config' +import ToolForm from '@/app/components/workflow/nodes/tool/components/tool-form' +import StructureOutputItem from '@/app/components/workflow/nodes/_base/components/variable/object-child-tree-panel/show' +import { Type } from '../llm/types' const Panel: FC> = ({ + id, data, }) => { + const { + readOnly, + triggerParameterSchema, + triggerParameterValue, + setTriggerParameterValue, + outputSchema, + hasObjectOutput, + isAuthenticated, + } = useConfig(id, data) + + // Convert output schema to VarItem format + const outputVars = Object.entries(outputSchema.properties || {}).map(([name, schema]: [string, any]) => ({ + name, + type: schema.type || 'string', + description: schema.description || '', + })) + return (
-
- - {data.plugin_name ? ( -
-
- {data.plugin_name} - {data.event_type && ( - - {data.event_type} - - )} -
-
- Plugin trigger configured -
+ {/* Dynamic Parameters Form - Only show when authenticated */} + {isAuthenticated && triggerParameterSchema.length > 0 && ( + <> +
+ +
+ + + )} + + {/* Output Variables - Always show */} + + <> + {outputVars.map(varItem => ( + + ))} + {Object.entries(outputSchema.properties || {}).map(([name, schema]: [string, any]) => ( +
+ {schema.type === 'object' ? ( + + ) : null}
- ) : ( -
- No plugin selected. Configure this trigger in the workflow canvas. -
- )} - -
+ ))} + +
) } diff --git a/web/app/components/workflow/nodes/trigger-plugin/types.ts b/web/app/components/workflow/nodes/trigger-plugin/types.ts index 00d85e0753..46cf5fb80a 100644 --- a/web/app/components/workflow/nodes/trigger-plugin/types.ts +++ b/web/app/components/workflow/nodes/trigger-plugin/types.ts @@ -3,7 +3,7 @@ import type { CollectionType } from '@/app/components/tools/types' export type PluginTriggerNodeType = CommonNodeType & { plugin_id?: string - plugin_name?: string + tool_name?: string event_type?: string config?: Record provider_id?: string diff --git a/web/app/components/workflow/nodes/trigger-plugin/use-config.ts b/web/app/components/workflow/nodes/trigger-plugin/use-config.ts new file mode 100644 index 0000000000..f212d3c0f0 --- /dev/null +++ b/web/app/components/workflow/nodes/trigger-plugin/use-config.ts @@ -0,0 +1,122 @@ +import { useCallback, useMemo } from 'react' +import produce from 'immer' +import type { PluginTriggerNodeType } from './types' +import useNodeCrud from '@/app/components/workflow/nodes/_base/hooks/use-node-crud' +import { useNodesReadOnly } from '@/app/components/workflow/hooks' +import { useAllTriggerPlugins, useTriggerSubscriptions } from '@/service/use-triggers' +import { + addDefaultValue, + toolParametersToFormSchemas, +} from '@/app/components/tools/utils/to-form-schema' +import type { InputVar } from '@/app/components/workflow/types' +import type { TriggerWithProvider } from '@/app/components/workflow/block-selector/types' +import type { Tool } from '@/app/components/tools/types' + +const useConfig = (id: string, payload: PluginTriggerNodeType) => { + const { nodesReadOnly: readOnly } = useNodesReadOnly() + const { data: triggerPlugins = [] } = useAllTriggerPlugins() + + const { inputs, setInputs: doSetInputs } = useNodeCrud(id, payload) + + const { provider_id, provider_name, tool_name, config } = inputs + + // Construct provider for authentication check + const authProvider = useMemo(() => { + if (provider_id && provider_name) + return `${provider_id}/${provider_name}` + return provider_id || '' + }, [provider_id, provider_name]) + + const { data: subscriptions = [] } = useTriggerSubscriptions( + authProvider, + !!authProvider, + ) + + const currentProvider = useMemo(() => { + 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]) + + // Dynamic subscription parameters (from subscription_schema.parameters_schema) + const subscriptionParameterSchema = useMemo(() => { + if (!currentProvider?.subscription_schema?.parameters_schema) return [] + return toolParametersToFormSchemas(currentProvider.subscription_schema.parameters_schema as any) + }, [currentProvider]) + + // Dynamic trigger parameters (from specific trigger.parameters) + const triggerSpecificParameterSchema = useMemo(() => { + if (!currentTrigger) return [] + return toolParametersToFormSchemas(currentTrigger.parameters) + }, [currentTrigger]) + + // Combined parameter schema (subscription + trigger specific) + const triggerParameterSchema = useMemo(() => { + return [...subscriptionParameterSchema, ...triggerSpecificParameterSchema] + }, [subscriptionParameterSchema, triggerSpecificParameterSchema]) + + const triggerParameterValue = useMemo(() => { + if (!triggerParameterSchema.length) return {} + return addDefaultValue(config || {}, triggerParameterSchema) + }, [triggerParameterSchema, config]) + + const setTriggerParameterValue = useCallback((value: Record) => { + const newInputs = produce(inputs, (draft) => { + draft.config = value + }) + doSetInputs(newInputs) + }, [inputs, doSetInputs]) + + const setInputVar = useCallback((variable: InputVar, varDetail: InputVar) => { + const newInputs = produce(inputs, (draft) => { + draft.config = { + ...draft.config, + [variable.variable]: varDetail.variable, + } + }) + doSetInputs(newInputs) + }, [inputs, doSetInputs]) + + // Get output schema + const outputSchema = useMemo(() => { + return currentTrigger?.output_schema || {} + }, [currentTrigger]) + + // Check if trigger has complex output structure + const hasObjectOutput = useMemo(() => { + const properties = outputSchema.properties || {} + return Object.values(properties).some((prop: any) => prop.type === 'object') + }, [outputSchema]) + + // Authentication status check + const isAuthenticated = useMemo(() => { + if (!subscriptions.length) return false + const subscription = subscriptions[0] + return subscription.credential_type !== 'unauthorized' + }, [subscriptions]) + + const showAuthRequired = !isAuthenticated && !!currentProvider + + return { + readOnly, + inputs, + currentProvider, + currentTrigger, + triggerParameterSchema, + triggerParameterValue, + setTriggerParameterValue, + setInputVar, + outputSchema, + hasObjectOutput, + isAuthenticated, + showAuthRequired, + } +} + +export default useConfig diff --git a/web/i18n/en-US/workflow.ts b/web/i18n/en-US/workflow.ts index 7bb7b40dc1..768ce73a53 100644 --- a/web/i18n/en-US/workflow.ts +++ b/web/i18n/en-US/workflow.ts @@ -724,6 +724,13 @@ const translation = { json: 'tool generated json', }, }, + triggerPlugin: { + authorized: 'Authorized', + notConfigured: 'Not Configured', + error: 'Error', + configuration: 'Configuration', + remove: 'Remove', + }, questionClassifiers: { model: 'model', inputVars: 'Input Variables', diff --git a/web/i18n/ja-JP/workflow.ts b/web/i18n/ja-JP/workflow.ts index 01af166341..028d52bd2f 100644 --- a/web/i18n/ja-JP/workflow.ts +++ b/web/i18n/ja-JP/workflow.ts @@ -1034,6 +1034,13 @@ const translation = { invalidParameterType: 'パラメータ"{{name}}"の無効なパラメータタイプ"{{type}}"です', }, }, + triggerPlugin: { + authorized: '認可された', + notConfigured: '設定されていません', + error: 'エラー', + configuration: '構成', + remove: '削除する', + }, }, tracing: { stopBy: '{{user}}によって停止', diff --git a/web/i18n/zh-Hans/workflow.ts b/web/i18n/zh-Hans/workflow.ts index 0b79673c92..50af933c04 100644 --- a/web/i18n/zh-Hans/workflow.ts +++ b/web/i18n/zh-Hans/workflow.ts @@ -1034,6 +1034,13 @@ const translation = { invalidParameterType: '参数"{{name}}"的参数类型"{{type}}"无效', }, }, + triggerPlugin: { + authorized: '已授权', + notConfigured: '未配置', + error: '错误', + configuration: '配置', + remove: '移除', + }, }, tracing: { stopBy: '由{{user}}终止', diff --git a/web/service/use-triggers.ts b/web/service/use-triggers.ts index 1d1535ac18..4e9f5e3e57 100644 --- a/web/service/use-triggers.ts +++ b/web/service/use-triggers.ts @@ -1,10 +1,20 @@ -import { useQuery } from '@tanstack/react-query' -import { get } from './base' -import type { TriggerProviderApiEntity, TriggerWithProvider } from '@/app/components/workflow/block-selector/types' +import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query' +import { del, get, post } from './base' +import type { + TriggerOAuthClientParams, + TriggerOAuthConfig, + TriggerProviderApiEntity, + TriggerSubscription, + TriggerSubscriptionBuilder, + TriggerWithProvider, +} from '@/app/components/workflow/block-selector/types' import { CollectionType } from '@/app/components/tools/types' +import { useInvalid } from './use-base' const NAME_SPACE = 'triggers' +// Trigger Provider Service - Provider ID Format: plugin_id/provider_name + // Convert backend API response to frontend ToolWithProvider format const convertToTriggerWithProvider = (provider: TriggerProviderApiEntity): TriggerWithProvider => { return { @@ -37,19 +47,26 @@ const convertToTriggerWithProvider = (provider: TriggerProviderApiEntity): Trigg llm_description: JSON.stringify(param.description || {}), required: param.required || false, default: param.default || '', - options: [], + options: param.options?.map(option => ({ + label: option.label, + value: option.value, + })) || [], })), labels: provider.tags || [], output_schema: trigger.output_schema || {}, })), + // Trigger-specific schema fields + credentials_schema: provider.credentials_schema, + oauth_client_schema: provider.oauth_client_schema, + subscription_schema: provider.subscription_schema, + meta: { version: '1.0', }, } } -// Main hook - follows exact same pattern as tools export const useAllTriggerPlugins = (enabled = true) => { return useQuery({ queryKey: [NAME_SPACE, 'all'], @@ -61,7 +78,6 @@ export const useAllTriggerPlugins = (enabled = true) => { }) } -// Additional hook for consistency with tools pattern export const useTriggerPluginsByType = (triggerType: string, enabled = true) => { return useQuery({ queryKey: [NAME_SPACE, 'byType', triggerType], @@ -72,3 +88,200 @@ export const useTriggerPluginsByType = (triggerType: string, enabled = true) => enabled: enabled && !!triggerType, }) } + +export const useInvalidateAllTriggerPlugins = () => { + return useInvalid([NAME_SPACE, 'all']) +} + +// ===== Trigger Subscriptions Management ===== +export const useTriggerSubscriptions = (provider: string, enabled = true) => { + return useQuery({ + queryKey: [NAME_SPACE, 'subscriptions', provider], + queryFn: () => get(`/workspaces/current/trigger-provider/${provider}/subscriptions/list`), + enabled: enabled && !!provider, + }) +} + +export const useInvalidateTriggerSubscriptions = () => { + const queryClient = useQueryClient() + return (provider: string) => { + queryClient.invalidateQueries({ + queryKey: [NAME_SPACE, 'subscriptions', provider], + }) + } +} + +export const useCreateTriggerSubscriptionBuilder = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'create-subscription-builder'], + mutationFn: (payload: { + provider: string + name?: string + credentials?: Record + }) => { + const { provider, ...body } = payload + return post<{ subscription_builder: TriggerSubscriptionBuilder }>( + `/workspaces/current/trigger-provider/${provider}/subscriptions/builder/create`, + { body }, + ) + }, + }) +} + +export const useUpdateTriggerSubscriptionBuilder = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'update-subscription-builder'], + mutationFn: (payload: { + provider: string + subscriptionBuilderId: string + name?: string + parameters?: Record + properties?: Record + credentials?: Record + }) => { + const { provider, subscriptionBuilderId, ...body } = payload + return post( + `/workspaces/current/trigger-provider/${provider}/subscriptions/builder/update/${subscriptionBuilderId}`, + { body }, + ) + }, + }) +} + +export const useVerifyTriggerSubscriptionBuilder = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'verify-subscription-builder'], + mutationFn: (payload: { + provider: string + subscriptionBuilderId: string + }) => { + const { provider, subscriptionBuilderId } = payload + return post( + `/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`, + ) + }, + }) +} + +export const useBuildTriggerSubscription = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'build-subscription'], + mutationFn: (payload: { + provider: string + subscriptionBuilderId: string + }) => { + const { provider, subscriptionBuilderId } = payload + return post( + `/workspaces/current/trigger-provider/${provider}/subscriptions/builder/build/${subscriptionBuilderId}`, + ) + }, + }) +} + +export const useDeleteTriggerSubscription = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'delete-subscription'], + mutationFn: (subscriptionId: string) => { + return post<{ result: string }>( + `/workspaces/current/trigger-provider/${subscriptionId}/subscriptions/delete`, + ) + }, + }) +} + +export const useTriggerSubscriptionBuilderLogs = ( + provider: string, + subscriptionBuilderId: string, + options: { + enabled?: boolean + refetchInterval?: number | false + } = {}, +) => { + const { enabled = true, refetchInterval = false } = options + + return useQuery[]>({ + queryKey: [NAME_SPACE, 'subscription-builder-logs', provider, subscriptionBuilderId], + queryFn: () => get( + `/workspaces/current/trigger-provider/${provider}/subscriptions/builder/logs/${subscriptionBuilderId}`, + ), + enabled: enabled && !!provider && !!subscriptionBuilderId, + refetchInterval, + }) +} + +// ===== OAuth Management ===== +export const useTriggerOAuthConfig = (provider: string, enabled = true) => { + return useQuery({ + queryKey: [NAME_SPACE, 'oauth-config', provider], + queryFn: () => get(`/workspaces/current/trigger-provider/${provider}/oauth/client`), + enabled: enabled && !!provider, + }) +} + +export const useConfigureTriggerOAuth = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'configure-oauth'], + mutationFn: (payload: { + provider: string + client_params: TriggerOAuthClientParams + enabled: boolean + }) => { + const { provider, ...body } = payload + return post<{ result: string }>( + `/workspaces/current/trigger-provider/${provider}/oauth/client`, + { body }, + ) + }, + }) +} + +export const useDeleteTriggerOAuth = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'delete-oauth'], + mutationFn: (provider: string) => { + return del<{ result: string }>( + `/workspaces/current/trigger-provider/${provider}/oauth/client`, + ) + }, + }) +} + +export const useInitiateTriggerOAuth = () => { + return useMutation({ + mutationKey: [NAME_SPACE, 'initiate-oauth'], + mutationFn: (provider: string) => { + return get<{ authorization_url: string; subscription_builder: any }>( + `/workspaces/current/trigger-provider/${provider}/subscriptions/oauth/authorize`, + ) + }, + }) +} + +// ===== Dynamic Options Support ===== +export const useTriggerPluginDynamicOptions = (payload: { + plugin_id: string + provider: string + action: string + parameter: string + extra?: Record +}, enabled = true) => { + return useQuery<{ options: Array<{ value: string; label: any }> }>({ + queryKey: [NAME_SPACE, 'dynamic-options', payload.plugin_id, payload.provider, payload.action, payload.parameter, payload.extra], + queryFn: () => get<{ options: Array<{ value: string; label: any }> }>( + '/workspaces/current/plugin/parameters/dynamic-options', + { params: payload }, + ), + enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter, + }) +} + +// ===== Cache Invalidation Helpers ===== + +export const useInvalidateTriggerOAuthConfig = () => { + const queryClient = useQueryClient() + return (provider: string) => { + queryClient.invalidateQueries({ + queryKey: [NAME_SPACE, 'oauth-config', provider], + }) + } +}