Files
WeHub Mirror 6bf8bebf51
CI / Test and Build (push) Failing after 1s
CI / Migrate Dev DB (push) Has been skipped
CI / Migrate DB (push) Has been skipped
CodeQL / Analyze actions (push) Has been cancelled
CodeQL / Analyze javascript-typescript (push) Has been cancelled
CI / Detect Version (push) Has been cancelled
CI / Detect Desktop Changes (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/cron.Dockerfile, ubuntu-latest, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build AMD64 (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build AMD64 (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build AMD64 (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/cron.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/db.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/pii.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-4vcpu-ubuntu-2404-arm, ./docker/realtime.Dockerfile, ubuntu-24.04-arm, ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build ARM64 (GHCR Only) (blacksmith-8vcpu-ubuntu-2404-arm, ./docker/app.Dockerfile, linux-arm64-8-core, ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Check Docs Changes (push) Has been cancelled
Publish CLI Package / publish-npm (push) Has been cancelled
Publish Python SDK / publish-pypi (push) Has been cancelled
CI / Deploy Trigger.dev (Dev) (push) Has been cancelled
Helm Chart / Lint, test, and validate chart (push) Has been cancelled
Helm Chart / Chart version bumped (push) Has been cancelled
Publish TypeScript SDK / publish-npm (push) Has been cancelled
CI / Build Dev ECR (blacksmith-8vcpu-ubuntu-2404, ./docker/app.Dockerfile, ECR_APP, linux-x64-8-core) (push) Has been cancelled
CI / Promote Images (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/cron) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/migrations) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/pii) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/realtime) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-2vcpu-ubuntu-2404, ./docker/db.Dockerfile, ECR_MIGRATIONS, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/pii.Dockerfile, ECR_PII, ubuntu-latest) (push) Has been cancelled
CI / Build Dev ECR (blacksmith-4vcpu-ubuntu-2404, ./docker/realtime.Dockerfile, ECR_REALTIME, ubuntu-latest) (push) Has been cancelled
CI / Create GHCR Manifests (ghcr.io/simstudioai/simstudio) (push) Has been cancelled
CI / Process Docs (push) Has been cancelled
CI / Create GitHub Release (push) Has been cancelled
CI / Check Desktop Signing Secrets (push) Has been cancelled
CI / Desktop Release (push) Has been cancelled
CI / Create Desktop Prerelease (push) Has been cancelled
CI / Desktop Prerelease Build (push) Has been cancelled
CI / Publish Desktop Prerelease (push) Has been cancelled
CI / Prune Desktop Prereleases (push) Has been cancelled
Helm Chart / Install on kind and run helm test (push) Has been cancelled
WeHub snapshot of cb28d14c6f2c081de7a0d8729a8c816c9adef67a
2026-08-10 11:17:50 +08:00

321 lines
9.9 KiB
TypeScript

import { createLogger } from '@sim/logger'
import { toError } from '@sim/utils/errors'
import { isInputDefinitionTrigger } from '@/lib/workflows/triggers/input-definition-triggers'
import { generateMockPayloadFromOutputsDefinition } from '@/lib/workflows/triggers/mock-payload'
import { type StartBlockCandidate, StartBlockPath } from '@/lib/workflows/triggers/triggers'
import { getAllBlocks, getBlock } from '@/blocks'
import type { BlockConfig } from '@/blocks/types'
import type { BlockState, WorkflowState } from '@/stores/workflows/workflow/types'
import { getTrigger } from '@/triggers'
const logger = createLogger('TriggerUtils')
/**
* Check if a workflow state has a valid start block
*/
export function hasValidStartBlockInState(state: WorkflowState | null | undefined): boolean {
if (!state?.blocks) {
return false
}
const startBlock = Object.values(state.blocks).find((block: BlockState) => {
const blockType = block?.type
return isInputDefinitionTrigger(blockType)
})
return !!startBlock
}
interface TriggerInfo {
id: string
name: string
description: string
icon: React.ComponentType<{ className?: string }>
color: string
category: 'core' | 'integration'
enableTriggerMode?: boolean
}
/**
* Get all blocks that can act as triggers
* This includes both dedicated trigger blocks and tools with trigger capabilities
*/
export function getAllTriggerBlocks(): TriggerInfo[] {
const allBlocks = getAllBlocks()
const triggers: TriggerInfo[] = []
for (const block of allBlocks) {
// Skip hidden blocks
if (block.hideFromToolbar) continue
// Check if it's a core trigger block (category: 'triggers')
if (block.category === 'triggers') {
triggers.push({
id: block.type,
name: block.name,
description: block.description,
icon: block.icon,
color: block.bgColor,
category: 'core',
enableTriggerMode: hasTriggerCapability(block),
})
}
// Check if it's a tool with trigger capability (has trigger-config subblock)
else if (hasTriggerCapability(block)) {
triggers.push({
id: block.type,
name: block.name,
description: block.description.replace(' or trigger workflows from ', ', trigger from '),
icon: block.icon,
color: block.bgColor,
category: 'integration',
enableTriggerMode: true,
})
}
}
// Sort: core triggers first, then integration triggers, alphabetically within each category
return triggers.sort((a, b) => {
if (a.category !== b.category) {
return a.category === 'core' ? -1 : 1
}
return a.name.localeCompare(b.name)
})
}
/**
* Check if a block has trigger capability (contains trigger mode subblocks)
*/
export function hasTriggerCapability(block: BlockConfig): boolean {
const hasTriggerModeSubBlocks = block.subBlocks.some((subBlock) => subBlock.mode === 'trigger')
if (block.category === 'triggers') {
return hasTriggerModeSubBlocks
}
return (
(block.triggers?.enabled === true && block.triggers.available.length > 0) ||
hasTriggerModeSubBlocks
)
}
/**
* Get blocks that should appear in the triggers tab
* This includes all trigger blocks and tools with trigger mode
*/
export function getTriggersForSidebar(): BlockConfig[] {
const allBlocks = getAllBlocks()
return allBlocks.filter((block) => {
if (block.hideFromToolbar) return false
// Include blocks with triggers category or trigger-config subblock
return block.category === 'triggers' || hasTriggerCapability(block)
})
}
/**
* Get the proper display name for a trigger block in the UI
*/
export function getTriggerDisplayName(blockType: string): string {
const block = getBlock(blockType)
if (!block) return blockType
return block.name
}
/**
* Groups triggers by their immediate downstream blocks to identify disjoint paths
*/
export function groupTriggersByPath<
T extends { type: string; subBlocks?: Record<string, unknown> },
>(
candidates: StartBlockCandidate<T>[],
edges: Array<{ source: string; target: string }>
): Array<StartBlockCandidate<T>[]> {
if (candidates.length <= 1) {
return [candidates]
}
const groups: Array<StartBlockCandidate<T>[]> = []
const processed = new Set<string>()
// Build adjacency map (edges should already be filtered to exclude trigger-to-trigger)
const adjacency = new Map<string, string[]>()
for (const edge of edges) {
if (!adjacency.has(edge.source)) {
adjacency.set(edge.source, [])
}
adjacency.get(edge.source)!.push(edge.target)
}
// Group triggers that feed into the same immediate blocks
for (const trigger of candidates) {
if (processed.has(trigger.blockId)) continue
const immediateTargets = adjacency.get(trigger.blockId) || []
const targetSet = new Set(immediateTargets)
// Find all triggers with the same immediate targets
const group = candidates.filter((t) => {
if (processed.has(t.blockId)) return false
if (t.blockId === trigger.blockId) return true
const tTargets = adjacency.get(t.blockId) || []
// Different number of targets = different paths
if (immediateTargets.length !== tTargets.length) return false
// Check if all targets match
return tTargets.every((target) => targetSet.has(target))
})
group.forEach((t) => processed.add(t.blockId))
groups.push(group)
}
logger.info('Grouped triggers by path', {
groupCount: groups.length,
groups: groups.map((g) => ({
count: g.length,
triggers: g.map((t) => ({ id: t.blockId, type: t.block.type })),
})),
})
return groups
}
/**
* Selects the best trigger from a list of candidates based on priority
* Priority: Start Block > Schedules > External Triggers > Legacy
* If multiple disjoint paths exist, returns one trigger per path
*/
export function selectBestTrigger<T extends { type: string; subBlocks?: Record<string, unknown> }>(
candidates: StartBlockCandidate<T>[],
edges?: Array<{ source: string; target: string }>
): StartBlockCandidate<T>[] {
if (candidates.length === 0) {
throw new Error('No trigger candidates provided')
}
// If edges provided, group by path and select best from each group
if (edges) {
const groups = groupTriggersByPath(candidates, edges)
return groups.map((group) => selectBestFromGroup(group))
}
// Otherwise just select the single best trigger
return [selectBestFromGroup(candidates)]
}
/**
* Selects the best trigger from a group based on priority
*/
function selectBestFromGroup<T extends { type: string; subBlocks?: Record<string, unknown> }>(
candidates: StartBlockCandidate<T>[]
): StartBlockCandidate<T> {
if (candidates.length === 1) {
return candidates[0]
}
// Sort by priority (lower number = higher priority)
const sorted = [...candidates].sort((a, b) => {
const getPriority = (trigger: StartBlockCandidate<T>): number => {
// Start block - highest priority
if (trigger.path === StartBlockPath.UNIFIED) return 0
if (trigger.path === StartBlockPath.LEGACY_STARTER) return 1
// For external triggers, differentiate schedules from webhooks
if (trigger.path === StartBlockPath.EXTERNAL_TRIGGER) {
if (trigger.block.type === 'schedule') return 2
return 3 // Webhooks and other external triggers
}
// Other trigger types
if (trigger.path === StartBlockPath.SPLIT_API) return 4
if (trigger.path === StartBlockPath.SPLIT_INPUT) return 5
if (trigger.path === StartBlockPath.SPLIT_MANUAL) return 6
if (trigger.path === StartBlockPath.SPLIT_CHAT) return 7
return 99 // Unknown
}
return getPriority(a) - getPriority(b)
})
const selected = sorted[0]
logger.info('Selected best trigger from group', {
selectedId: selected.blockId,
selectedType: selected.block.type,
selectedPath: selected.path,
groupSize: candidates.length,
})
return selected
}
/**
* Checks if a trigger needs mock payload (external triggers/webhooks, but not schedules)
*/
export function triggerNeedsMockPayload<T extends { type: string }>(
trigger: StartBlockCandidate<T>
): boolean {
// Only webhooks and external integrations need mock payloads
// Schedules run normally without mock data
return trigger.path === StartBlockPath.EXTERNAL_TRIGGER && trigger.block.type !== 'schedule'
}
/**
* Extracts or generates mock payload for external trigger execution
*/
export function extractTriggerMockPayload<
T extends { type: string; subBlocks?: Record<string, unknown> },
>(trigger: StartBlockCandidate<T>): unknown {
const subBlocks = trigger.block.subBlocks as Record<string, { value?: unknown }> | undefined
// Determine the trigger ID
let triggerId: string
// Check for selectedTriggerId (multi-trigger blocks like Linear, Jira)
if (typeof subBlocks?.selectedTriggerId?.value === 'string') {
triggerId = subBlocks.selectedTriggerId.value
} else {
// For single-trigger blocks, get from block config
const blockConfig = getBlock(trigger.block.type)
if (blockConfig?.triggers?.available?.length === 1) {
triggerId = blockConfig.triggers.available[0]
} else {
// Fallback to block type (for blocks that are themselves triggers like schedule)
triggerId = trigger.block.type
}
}
try {
const triggerConfig = getTrigger(triggerId)
if (!triggerConfig || !triggerConfig.outputs) {
logger.warn('No trigger config or outputs found', {
triggerId,
blockId: trigger.blockId,
})
return {}
}
const payload = generateMockPayloadFromOutputsDefinition(triggerConfig.outputs)
logger.info('Generated mock payload from trigger outputs', {
triggerId,
blockId: trigger.blockId,
topLevelKeys: Object.keys(payload ?? {}),
})
return payload
} catch (error) {
logger.error('Failed to generate mock payload from trigger outputs', {
triggerId,
blockId: trigger.blockId,
error: toError(error).message,
})
return {}
}
}