feat: introduce trigger functionality (#27644)

Signed-off-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: Stream <Stream_2@qq.com>
Co-authored-by: lyzno1 <92089059+lyzno1@users.noreply.github.com>
Co-authored-by: zhsama <torvalds@linux.do>
Co-authored-by: Harry <xh001x@hotmail.com>
Co-authored-by: lyzno1 <yuanyouhuilyz@gmail.com>
Co-authored-by: yessenia <yessenia.contact@gmail.com>
Co-authored-by: hjlarry <hjlarry@163.com>
Co-authored-by: gemini-code-assist[bot] <176961590+gemini-code-assist[bot]@users.noreply.github.com>
Co-authored-by: Copilot Autofix powered by AI <62310815+github-advanced-security[bot]@users.noreply.github.com>
Co-authored-by: autofix-ci[bot] <114827586+autofix-ci[bot]@users.noreply.github.com>
Co-authored-by: WTW0313 <twwu@dify.ai>
Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Yeuoly
2025-11-12 17:59:37 +08:00
committed by GitHub
parent ca7794305b
commit b76e17b25d
785 changed files with 41186 additions and 3725 deletions

View File

@ -1,8 +1,8 @@
import type { Fetcher } from 'swr'
import { del, get, patch, post, put } from './base'
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WorkflowDailyConversationsResponse } from '@/models/app'
import type { ApiKeysListResponse, AppDailyConversationsResponse, AppDailyEndUsersResponse, AppDailyMessagesResponse, AppDetailResponse, AppListResponse, AppStatisticsResponse, AppTemplatesResponse, AppTokenCostsResponse, AppVoicesListResponse, CreateApiKeyResponse, DSLImportMode, DSLImportResponse, GenerationIntroductionResponse, TracingConfig, TracingStatus, UpdateAppModelConfigResponse, UpdateAppSiteCodeResponse, UpdateOpenAIKeyResponse, ValidateOpenAIKeyResponse, WebhookTriggerResponse, WorkflowDailyConversationsResponse } from '@/models/app'
import type { CommonResponse } from '@/models/common'
import type { AppIconType, AppMode, ModelConfig } from '@/types/app'
import type { AppIconType, AppModeEnum, ModelConfig } from '@/types/app'
import type { TracingProvider } from '@/app/(commonLayout)/app/(appDetailLayout)/[appId]/overview/tracing/type'
export const fetchAppList: Fetcher<AppListResponse, { url: string; params?: Record<string, any> }> = ({ url, params }) => {
@ -22,7 +22,7 @@ export const fetchAppTemplates: Fetcher<AppTemplatesResponse, { url: string }> =
return get<AppTemplatesResponse>(url)
}
export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: AppIconType; icon?: string; icon_background?: string; mode: AppMode; description?: string; config?: ModelConfig }> = ({ name, icon_type, icon, icon_background, mode, description, config }) => {
export const createApp: Fetcher<AppDetailResponse, { name: string; icon_type?: AppIconType; icon?: string; icon_background?: string; mode: AppModeEnum; description?: string; config?: ModelConfig }> = ({ name, icon_type, icon, icon_background, mode, description, config }) => {
return post<AppDetailResponse>('apps', { body: { name, icon_type, icon, icon_background, mode, description, model_config: config } })
}
@ -31,7 +31,7 @@ export const updateAppInfo: Fetcher<AppDetailResponse, { appID: string; name: st
return put<AppDetailResponse>(`apps/${appID}`, { body })
}
export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppMode; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => {
export const copyApp: Fetcher<AppDetailResponse, { appID: string; name: string; icon_type: AppIconType; icon: string; icon_background?: string | null; mode: AppModeEnum; description?: string }> = ({ appID, name, icon_type, icon, icon_background, mode, description }) => {
return post<AppDetailResponse>(`apps/${appID}/copy`, { body: { name, icon_type, icon, icon_background, mode, description } })
}
@ -162,6 +162,11 @@ export const updateTracingStatus: Fetcher<CommonResponse, { appId: string; body:
return post(`/apps/${appId}/trace`, { body })
}
// Webhook Trigger
export const fetchWebhookUrl: Fetcher<WebhookTriggerResponse, { appId: string; nodeId: string }> = ({ appId, nodeId }) => {
return get<WebhookTriggerResponse>(`apps/${appId}/workflows/triggers/webhook`, { params: { node_id: nodeId } })
}
export const fetchTracingConfig: Fetcher<TracingConfig & { has_not_configured: true }, { appId: string; provider: TracingProvider }> = ({ appId, provider }) => {
return get(`/apps/${appId}/trace-config`, {
params: {

View File

@ -155,7 +155,7 @@ export function format(text: string) {
return res.replaceAll('\n', '<br/>').replaceAll('```', '')
}
const handleStream = (
export const handleStream = (
response: Response,
onData: IOnData,
onCompleted?: IOnCompleted,

View File

@ -1,8 +1,9 @@
import { get, post, ssePost } from './base'
import type { IOnCompleted, IOnData, IOnError, IOnFile, IOnMessageEnd, IOnMessageReplace, IOnThought } from './base'
import type { ChatPromptConfig, CompletionPromptConfig } from '@/models/debug'
import type { ModelModeType } from '@/types/app'
import type { AppModeEnum, ModelModeType } from '@/types/app'
import type { ModelParameterRule } from '@/app/components/header/account-setting/model-provider-page/declarations'
export type BasicAppFirstRes = {
prompt: string
variables: string[]
@ -105,7 +106,7 @@ export const fetchPromptTemplate = ({
mode,
modelName,
hasSetDataSet,
}: { appMode: string; mode: ModelModeType; modelName: string; hasSetDataSet: boolean }) => {
}: { appMode: AppModeEnum; mode: ModelModeType; modelName: string; hasSetDataSet: boolean }) => {
return get<Promise<{ chat_prompt_config: ChatPromptConfig; completion_prompt_config: CompletionPromptConfig; stop: [] }>>('/app/prompt-templates', {
params: {
app_mode: appMode,

View File

@ -4,6 +4,8 @@ import React from 'react'
import useSWR, { useSWRConfig } from 'swr'
import { createApp, fetchAppDetail, fetchAppList, getAppDailyConversations, getAppDailyEndUsers, updateAppApiStatus, updateAppModelConfig, updateAppRateLimit, updateAppSiteAccessToken, updateAppSiteConfig, updateAppSiteStatus } from '../apps'
import Loading from '@/app/components/base/loading'
import { AppModeEnum } from '@/types/app'
const Service: FC = () => {
const { data: appList, error: appListError } = useSWR({ url: '/apps', params: { page: 1 } }, fetchAppList)
const { data: firstApp, error: appDetailError } = useSWR({ url: '/apps', id: '1' }, fetchAppDetail)
@ -21,7 +23,7 @@ const Service: FC = () => {
const handleCreateApp = async () => {
await createApp({
name: `new app${Math.round(Math.random() * 100)}`,
mode: 'chat',
mode: AppModeEnum.CHAT,
})
// reload app list
mutate({ url: '/apps', params: { page: 1 } })

View File

@ -2,7 +2,7 @@ import type { AfterResponseHook, BeforeErrorHook, BeforeRequestHook, Hooks } fro
import ky from 'ky'
import type { IOtherOptions } from './base'
import Toast from '@/app/components/base/toast'
import { API_PREFIX, APP_VERSION, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, MARKETPLACE_API_PREFIX, PASSPORT_HEADER_NAME, PUBLIC_API_PREFIX, WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config'
import { API_PREFIX, APP_VERSION, CSRF_COOKIE_NAME, CSRF_HEADER_NAME, IS_MARKETPLACE, MARKETPLACE_API_PREFIX, PASSPORT_HEADER_NAME, PUBLIC_API_PREFIX, WEB_APP_SHARE_CODE_HEADER_NAME } from '@/config'
import Cookies from 'js-cookie'
import { getWebAppAccessToken, getWebAppPassport } from './webapp-auth'
@ -160,7 +160,7 @@ async function base<T>(url: string, options: FetchOptionType = {}, otherOptions:
// ! For Marketplace API, help to filter tags added in new version
if (isMarketplaceAPI)
(headers as any).set('X-Dify-Version', APP_VERSION)
(headers as any).set('X-Dify-Version', !IS_MARKETPLACE ? APP_VERSION : '999.0.0')
const client = baseClient.extend({
hooks: {

View File

@ -295,7 +295,8 @@ export const fetchAccessToken = async ({ userId, appCode }: { userId?: string, a
if (accessToken)
headers.append('Authorization', `Bearer ${accessToken}`)
const params = new URLSearchParams()
userId && params.append('user_id', userId)
if (userId)
params.append('user_id', userId)
const url = `/passport?${params.toString()}`
return get<{ access_token: string }>(url, { headers }) as Promise<{ access_token: string }>
}

View File

@ -3,9 +3,11 @@ import {
useQueryClient,
} from '@tanstack/react-query'
export const useInvalid = (key: QueryKey) => {
export const useInvalid = (key?: QueryKey) => {
const queryClient = useQueryClient()
return () => {
if (!key)
return
queryClient.invalidateQueries(
{
queryKey: key,
@ -14,9 +16,11 @@ export const useInvalid = (key: QueryKey) => {
}
}
export const useReset = (key: QueryKey) => {
export const useReset = (key?: QueryKey) => {
const queryClient = useQueryClient()
return () => {
if (!key)
return
queryClient.resetQueries(
{
queryKey: key,

View File

@ -19,7 +19,6 @@ import type {
PluginDetail,
PluginInfoFromMarketPlace,
PluginTask,
PluginType,
PluginsFromMarketplaceByInfoResponse,
PluginsFromMarketplaceResponse,
ReferenceSetting,
@ -28,7 +27,7 @@ import type {
uploadGitHubResponse,
} from '@/app/components/plugins/types'
import { TaskStatus } from '@/app/components/plugins/types'
import { PluginType as PluginTypeEnum } from '@/app/components/plugins/types'
import { PluginCategoryEnum } from '@/app/components/plugins/types'
import type {
PluginsSearchParams,
} from '@/app/components/plugins/marketplace/types'
@ -45,6 +44,7 @@ import useReferenceSetting from '@/app/components/plugins/plugin-page/use-refere
import { uninstallPlugin } from '@/service/plugins'
import useRefreshPluginList from '@/app/components/plugins/install-plugin/hooks/use-refresh-plugin-list'
import { cloneDeep } from 'lodash-es'
import { getFormattedPlugin } from '@/app/components/plugins/marketplace/utils'
const NAME_SPACE = 'plugins'
@ -68,6 +68,66 @@ export const useCheckInstalled = ({
})
}
const useRecommendedMarketplacePluginsKey = [NAME_SPACE, 'recommendedMarketplacePlugins']
export const useRecommendedMarketplacePlugins = ({
collection = '__recommended-plugins-tools',
enabled = true,
limit = 15,
}: {
collection?: string
enabled?: boolean
limit?: number
} = {}) => {
return useQuery<Plugin[]>({
queryKey: [...useRecommendedMarketplacePluginsKey, collection, limit],
queryFn: async () => {
const response = await postMarketplace<{ data: { plugins: Plugin[] } }>(
`/collections/${collection}/plugins`,
{
body: {
limit,
},
},
)
return response.data.plugins.map(plugin => getFormattedPlugin(plugin))
},
enabled,
staleTime: 60 * 1000,
})
}
export const useFeaturedToolsRecommendations = (enabled: boolean, limit = 15) => {
const {
data: plugins = [],
isLoading,
} = useRecommendedMarketplacePlugins({
collection: '__recommended-plugins-tools',
enabled,
limit,
})
return {
plugins,
isLoading,
}
}
export const useFeaturedTriggersRecommendations = (enabled: boolean, limit = 15) => {
const {
data: plugins = [],
isLoading,
} = useRecommendedMarketplacePlugins({
collection: '__recommended-plugins-triggers',
enabled,
limit,
})
return {
plugins,
isLoading,
}
}
export const useInstalledPluginList = (disable?: boolean, pageSize = 100) => {
const fetchPlugins = async ({ pageParam = 1 }) => {
const response = await get<InstalledPluginListWithTotalResponse>(
@ -518,7 +578,7 @@ export const useFetchPluginsInMarketPlaceByInfo = (infos: Record<string, any>[])
}
const usePluginTaskListKey = [NAME_SPACE, 'pluginTaskList']
export const usePluginTaskList = (category?: PluginType) => {
export const usePluginTaskList = (category?: PluginCategoryEnum | string) => {
const [initialized, setInitialized] = useState(false)
const {
canManagement,
@ -544,20 +604,20 @@ export const usePluginTaskList = (category?: PluginType) => {
useEffect(() => {
// After first fetch, refresh plugin list each time all tasks are done
// Skip initialization period, because the query cache is not updated yet
if (initialized && !isRefetching) {
const lastData = cloneDeep(data)
const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
const taskAllFailed = lastData?.tasks.every(task => task.status === TaskStatus.failed)
if (taskDone) {
if (lastData?.tasks.length && !taskAllFailed)
refreshPluginList(category ? { category } as any : undefined, !category)
}
}
}, [isRefetching])
if (!initialized || isRefetching)
return
const lastData = cloneDeep(data)
const taskDone = lastData?.tasks.every(task => task.status === TaskStatus.success || task.status === TaskStatus.failed)
const taskAllFailed = lastData?.tasks.every(task => task.status === TaskStatus.failed)
if (taskDone && lastData?.tasks.length && !taskAllFailed)
refreshPluginList(category ? { category } as any : undefined, !category)
}, [initialized, isRefetching, data, category, refreshPluginList])
useEffect(() => {
setInitialized(true)
}, [])
if (isFetched && !initialized)
setInitialized(true)
}, [isFetched, initialized])
const handleRefetch = useCallback(() => {
refetch()
@ -641,7 +701,7 @@ export const usePluginInfo = (providerName?: string) => {
const name = parts[1]
try {
const response = await fetchPluginInfoFromMarketPlace({ org, name })
return response.data.plugin.category === PluginTypeEnum.model ? response.data.plugin : null
return response.data.plugin.category === PluginCategoryEnum.model ? response.data.plugin : null
}
catch {
return null
@ -651,7 +711,7 @@ export const usePluginInfo = (providerName?: string) => {
})
}
export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type: 'tool') => {
export const useFetchDynamicOptions = (plugin_id: string, provider: string, action: string, parameter: string, provider_type?: string, extra?: Record<string, any>) => {
return useMutation({
mutationFn: () => get<{ options: FormOption[] }>('/workspaces/current/plugin/parameters/dynamic-options', {
params: {
@ -660,7 +720,26 @@ export const useFetchDynamicOptions = (plugin_id: string, provider: string, acti
action,
parameter,
provider_type,
...extra,
},
}),
})
}
export const usePluginReadme = ({ plugin_unique_identifier, language }: { plugin_unique_identifier: string, language?: string }) => {
return useQuery({
queryKey: ['pluginReadme', plugin_unique_identifier, language],
queryFn: () => get<{ readme: string }>('/workspaces/current/plugin/readme', { params: { plugin_unique_identifier, language } }, { silent: true }),
enabled: !!plugin_unique_identifier,
retry: 0,
})
}
export const usePluginReadmeAsset = ({ file_name, plugin_unique_identifier }: { file_name?: string, plugin_unique_identifier?: string }) => {
const normalizedFileName = file_name?.replace(/(^\.\/_assets\/|^_assets\/)/, '')
return useQuery({
queryKey: ['pluginReadmeAsset', plugin_unique_identifier, file_name],
queryFn: () => get<Blob>('/workspaces/current/plugin/asset', { params: { plugin_unique_identifier, file_name: normalizedFileName } }, { silent: true }),
enabled: !!plugin_unique_identifier && !!file_name && /(^\.\/_assets|^_assets)/.test(file_name),
})
}

View File

@ -84,8 +84,9 @@ const useInvalidToolsKeyMap: Record<string, QueryKey> = {
[CollectionType.workflow]: useAllWorkflowToolsKey,
[CollectionType.mcp]: useAllMCPToolsKey,
}
export const useInvalidToolsByType = (type: CollectionType | string) => {
return useInvalid(useInvalidToolsKeyMap[type])
export const useInvalidToolsByType = (type?: CollectionType | string) => {
const queryKey = type ? useInvalidToolsKeyMap[type] : undefined
return useInvalid(queryKey)
}
export const useCreateMCP = () => {
@ -339,3 +340,53 @@ export const useRAGRecommendedPlugins = () => {
export const useInvalidateRAGRecommendedPlugins = () => {
return useInvalid(useRAGRecommendedPluginListKey)
}
// App Triggers API hooks
export type AppTrigger = {
id: string
trigger_type: 'trigger-webhook' | 'trigger-schedule' | 'trigger-plugin'
title: string
node_id: string
provider_name: string
icon: string
status: 'enabled' | 'disabled' | 'unauthorized'
created_at: string
updated_at: string
}
export const useAppTriggers = (appId: string | undefined, options?: any) => {
return useQuery<{ data: AppTrigger[] }>({
queryKey: [NAME_SPACE, 'app-triggers', appId],
queryFn: () => get<{ data: AppTrigger[] }>(`/apps/${appId}/triggers`),
enabled: !!appId,
...options, // Merge additional options while maintaining backward compatibility
})
}
export const useInvalidateAppTriggers = () => {
const queryClient = useQueryClient()
return (appId: string) => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'app-triggers', appId],
})
}
}
export const useUpdateTriggerStatus = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'update-trigger-status'],
mutationFn: (payload: {
appId: string
triggerId: string
enableTrigger: boolean
}) => {
const { appId, triggerId, enableTrigger } = payload
return post<AppTrigger>(`/apps/${appId}/trigger-enable`, {
body: {
trigger_id: triggerId,
enable_trigger: enableTrigger,
},
})
},
})
}

320
web/service/use-triggers.ts Normal file
View File

@ -0,0 +1,320 @@
import { useMutation, useQuery, useQueryClient } from '@tanstack/react-query'
import { del, get, post } from './base'
import type {
TriggerLogEntity,
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 {
// Collection fields
id: provider.plugin_id || provider.name,
name: provider.name,
author: provider.author,
description: provider.description,
icon: provider.icon || '',
label: provider.label,
type: CollectionType.trigger,
team_credentials: {},
is_team_authorization: false,
allow_delete: false,
labels: provider.tags || [],
plugin_id: provider.plugin_id,
plugin_unique_identifier: provider.plugin_unique_identifier || '',
events: provider.events.map(event => ({
name: event.name,
author: provider.author,
label: event.identity.label,
description: event.description,
parameters: event.parameters.map(param => ({
name: param.name,
label: param.label,
human_description: param.description || param.label,
type: param.type,
form: param.type,
llm_description: JSON.stringify(param.description || {}),
required: param.required || false,
default: param.default || '',
options: param.options?.map(option => ({
label: option.label,
value: option.value,
})) || [],
multiple: param.multiple || false,
})),
labels: provider.tags || [],
output_schema: event.output_schema || {},
})),
// Trigger-specific schema fields
subscription_constructor: provider.subscription_constructor,
subscription_schema: provider.subscription_schema,
supported_creation_methods: provider.supported_creation_methods,
meta: {
version: '1.0',
},
}
}
export const useAllTriggerPlugins = (enabled = true) => {
return useQuery<TriggerWithProvider[]>({
queryKey: [NAME_SPACE, 'all'],
queryFn: async () => {
const response = await get<TriggerProviderApiEntity[]>('/workspaces/current/triggers')
return response.map(convertToTriggerWithProvider)
},
enabled,
staleTime: 0,
gcTime: 0,
})
}
export const useTriggerPluginsByType = (triggerType: string, enabled = true) => {
return useQuery<TriggerWithProvider[]>({
queryKey: [NAME_SPACE, 'byType', triggerType],
queryFn: async () => {
const response = await get<TriggerProviderApiEntity[]>(`/workspaces/current/triggers?type=${triggerType}`)
return response.map(convertToTriggerWithProvider)
},
enabled: enabled && !!triggerType,
})
}
export const useInvalidateAllTriggerPlugins = () => {
return useInvalid([NAME_SPACE, 'all'])
}
// ===== Trigger Subscriptions Management =====
export const useTriggerProviderInfo = (provider: string, enabled = true) => {
return useQuery<TriggerProviderApiEntity>({
queryKey: [NAME_SPACE, 'provider-info', provider],
queryFn: () => get<TriggerProviderApiEntity>(`/workspaces/current/trigger-provider/${provider}/info`),
enabled: enabled && !!provider,
staleTime: 0,
gcTime: 0,
})
}
export const useTriggerSubscriptions = (provider: string, enabled = true) => {
return useQuery<TriggerSubscription[]>({
queryKey: [NAME_SPACE, 'list-subscriptions', provider],
queryFn: () => get<TriggerSubscription[]>(`/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
credential_type?: string
}) => {
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
properties?: Record<string, any>
parameters?: Record<string, any>
credentials?: Record<string, any>
}) => {
const { provider, subscriptionBuilderId, ...body } = payload
return post<TriggerSubscriptionBuilder>(
`/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
credentials?: Record<string, any>
}) => {
const { provider, subscriptionBuilderId, ...body } = payload
return post<{ verified: boolean }>(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/verify/${subscriptionBuilderId}`,
{ body },
{ silent: true },
)
},
})
}
export type BuildTriggerSubscriptionPayload = {
provider: string
subscriptionBuilderId: string
name?: string
parameters?: Record<string, any>
}
export const useBuildTriggerSubscription = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'build-subscription'],
mutationFn: (payload: BuildTriggerSubscriptionPayload) => {
const { provider, subscriptionBuilderId, ...body } = payload
return post(
`/workspaces/current/trigger-provider/${provider}/subscriptions/builder/build/${subscriptionBuilderId}`,
{ body },
)
},
})
}
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<{ logs: TriggerLogEntity[] }>({
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<TriggerOAuthConfig>({
queryKey: [NAME_SPACE, 'oauth-config', provider],
queryFn: () => get<TriggerOAuthConfig>(`/workspaces/current/trigger-provider/${provider}/oauth/client`),
enabled: enabled && !!provider,
})
}
export type ConfigureTriggerOAuthPayload = {
provider: string
client_params?: TriggerOAuthClientParams
enabled: boolean
}
export const useConfigureTriggerOAuth = () => {
return useMutation({
mutationKey: [NAME_SPACE, 'configure-oauth'],
mutationFn: (payload: ConfigureTriggerOAuthPayload) => {
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: TriggerSubscriptionBuilder }>(
`/workspaces/current/trigger-provider/${provider}/subscriptions/oauth/authorize`,
{},
{ silent: true },
)
},
})
}
// ===== Dynamic Options Support =====
export const useTriggerPluginDynamicOptions = (payload: {
plugin_id: string
provider: string
action: string
parameter: string
credential_id: string
extra?: Record<string, any>
}, 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.credential_id, payload.extra],
queryFn: () => get<{ options: Array<{ value: string; label: any }> }>(
'/workspaces/current/plugin/parameters/dynamic-options',
{
params: {
...payload,
provider_type: 'trigger', // Add required provider_type parameter
},
},
{ silent: true },
),
enabled: enabled && !!payload.plugin_id && !!payload.provider && !!payload.action && !!payload.parameter && !!payload.credential_id,
retry: 0,
})
}
// ===== Cache Invalidation Helpers =====
export const useInvalidateTriggerOAuthConfig = () => {
const queryClient = useQueryClient()
return (provider: string) => {
queryClient.invalidateQueries({
queryKey: [NAME_SPACE, 'oauth-config', provider],
})
}
}

View File

@ -0,0 +1,152 @@
import { produce } from 'immer'
import type { Edge, Node } from '@/app/components/workflow/types'
import { BlockEnum } from '@/app/components/workflow/types'
import type { PluginTriggerNodeType } from '@/app/components/workflow/nodes/trigger-plugin/types'
import type { FetchWorkflowDraftResponse } from '@/types/workflow'
export type TriggerPluginNodePayload = {
title: string
desc: string
plugin_id: string
provider_id: string
event_name: string
subscription_id: string
plugin_unique_identifier: string
event_parameters: Record<string, unknown>
}
export type WorkflowDraftSyncParams = Pick<
FetchWorkflowDraftResponse,
'graph' | 'features' | 'environment_variables' | 'conversation_variables'
>
const removeTempProperties = (data: Record<string, unknown>): void => {
Object.keys(data).forEach((key) => {
if (key.startsWith('_'))
delete data[key]
})
}
type TriggerParameterSchema = Record<string, unknown>
type TriggerPluginHydratePayload = (PluginTriggerNodeType & {
paramSchemas?: TriggerParameterSchema[]
parameters_schema?: TriggerParameterSchema[]
})
const sanitizeTriggerPluginNode = (node: Node<TriggerPluginNodePayload>): Node<TriggerPluginNodePayload> => {
const data = node.data
if (!data || data.type !== BlockEnum.TriggerPlugin)
return node
const sanitizedData: TriggerPluginNodePayload & { type: BlockEnum.TriggerPlugin } = {
type: BlockEnum.TriggerPlugin,
title: data.title ?? '',
desc: data.desc ?? '',
plugin_id: data.plugin_id ?? '',
provider_id: data.provider_id ?? '',
event_name: data.event_name ?? '',
subscription_id: data.subscription_id ?? '',
plugin_unique_identifier: data.plugin_unique_identifier ?? '',
event_parameters: (typeof data.event_parameters === 'object' && data.event_parameters !== null)
? data.event_parameters as Record<string, unknown>
: {},
}
return {
...node,
data: sanitizedData,
}
}
export const sanitizeWorkflowDraftPayload = (params: WorkflowDraftSyncParams): WorkflowDraftSyncParams => {
const { graph } = params
if (!graph?.nodes?.length)
return params
const sanitizedNodes = graph.nodes.map(node => sanitizeTriggerPluginNode(node as Node<TriggerPluginNodePayload>))
return {
...params,
graph: {
...graph,
nodes: sanitizedNodes,
},
}
}
const isTriggerPluginNode = (node: Node): node is Node<TriggerPluginHydratePayload> => {
const data = node.data as unknown
if (!data || typeof data !== 'object')
return false
const payload = data as Partial<TriggerPluginHydratePayload> & { type?: BlockEnum }
if (payload.type !== BlockEnum.TriggerPlugin)
return false
return 'event_parameters' in payload
}
const hydrateTriggerPluginNode = (node: Node): Node => {
if (!isTriggerPluginNode(node))
return node
const typedNode = node as Node<TriggerPluginHydratePayload>
const data = typedNode.data
const eventParameters = data.event_parameters ?? {}
const parametersSchema = data.parameters_schema ?? data.paramSchemas ?? []
const config = data.config ?? eventParameters ?? {}
const nextData: typeof data = {
...data,
config,
paramSchemas: data.paramSchemas ?? parametersSchema,
parameters_schema: parametersSchema,
}
return {
...typedNode,
data: nextData,
}
}
export const hydrateWorkflowDraftResponse = (draft: FetchWorkflowDraftResponse): FetchWorkflowDraftResponse => {
return produce(draft, (mutableDraft) => {
if (!mutableDraft?.graph)
return
if (mutableDraft.graph.nodes) {
mutableDraft.graph.nodes = mutableDraft.graph.nodes
.filter((node: Node) => !node.data?._isTempNode)
.map((node: Node) => {
if (node.data)
removeTempProperties(node.data as Record<string, unknown>)
return hydrateTriggerPluginNode(node)
})
}
if (mutableDraft.graph.edges) {
mutableDraft.graph.edges = mutableDraft.graph.edges
.filter((edge: Edge) => !edge.data?._isTemp)
.map((edge: Edge) => {
if (edge.data)
removeTempProperties(edge.data as Record<string, unknown>)
return edge
})
}
if (mutableDraft.environment_variables) {
mutableDraft.environment_variables = mutableDraft.environment_variables.map(env =>
env.value_type === 'secret'
? { ...env, value: '[__HIDDEN__]' }
: env,
)
}
})
}