mirror of
https://github.com/simstudioai/sim.git
synced 2026-01-07 22:24:06 -05:00
improvement(code-quality): centralize regex checks, normalization (#2554)
* improvement(code-quality): centralize regex checks, normalization * simplify resolution * fix(copilot): don't allow duplicate name blocks * centralize uuid check
This commit is contained in:
committed by
GitHub
parent
b23299dae4
commit
bf8fbebe22
@@ -10,9 +10,9 @@ import {
|
||||
createRequestTracker,
|
||||
createUnauthorizedResponse,
|
||||
} from '@/lib/copilot/request-helpers'
|
||||
import { validateUUID } from '@/lib/core/security/input-validation'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { isUuidV4 } from '@/executor/constants'
|
||||
|
||||
const logger = createLogger('CheckpointRevertAPI')
|
||||
|
||||
@@ -87,9 +87,8 @@ export async function POST(request: NextRequest) {
|
||||
isDeployed: cleanedState.isDeployed,
|
||||
})
|
||||
|
||||
const workflowIdValidation = validateUUID(checkpoint.workflowId, 'workflowId')
|
||||
if (!workflowIdValidation.isValid) {
|
||||
logger.error(`[${tracker.requestId}] Invalid workflow ID: ${workflowIdValidation.error}`)
|
||||
if (!isUuidV4(checkpoint.workflowId)) {
|
||||
logger.error(`[${tracker.requestId}] Invalid workflow ID format`)
|
||||
return NextResponse.json({ error: 'Invalid workflow ID format' }, { status: 400 })
|
||||
}
|
||||
|
||||
|
||||
@@ -14,6 +14,8 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getEffectiveDecryptedEnv } from '@/lib/environment/utils'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { REFERENCE } from '@/executor/constants'
|
||||
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
|
||||
import { executeTool } from '@/tools'
|
||||
import { getTool } from '@/tools/utils'
|
||||
|
||||
@@ -33,14 +35,18 @@ const ExecuteToolSchema = z.object({
|
||||
function resolveEnvVarReferences(value: any, envVars: Record<string, string>): any {
|
||||
if (typeof value === 'string') {
|
||||
// Check for exact match: entire string is "{{VAR_NAME}}"
|
||||
const exactMatch = /^\{\{([^}]+)\}\}$/.exec(value)
|
||||
const exactMatchPattern = new RegExp(
|
||||
`^\\${REFERENCE.ENV_VAR_START}([^}]+)\\${REFERENCE.ENV_VAR_END}$`
|
||||
)
|
||||
const exactMatch = exactMatchPattern.exec(value)
|
||||
if (exactMatch) {
|
||||
const envVarName = exactMatch[1].trim()
|
||||
return envVars[envVarName] ?? value
|
||||
}
|
||||
|
||||
// Check for embedded references: "prefix {{VAR}} suffix"
|
||||
return value.replace(/\{\{([^}]+)\}\}/g, (match, varName) => {
|
||||
const envVarPattern = createEnvVarPattern()
|
||||
return value.replace(envVarPattern, (match, varName) => {
|
||||
const trimmedName = varName.trim()
|
||||
return envVars[trimmedName] ?? match
|
||||
})
|
||||
|
||||
@@ -14,6 +14,7 @@ import type { StorageConfig } from '@/lib/uploads/core/storage-client'
|
||||
import { getFileMetadataByKey } from '@/lib/uploads/server/metadata'
|
||||
import { inferContextFromKey } from '@/lib/uploads/utils/file-utils'
|
||||
import { getUserEntityPermissions } from '@/lib/workspaces/permissions/utils'
|
||||
import { isUuid } from '@/executor/constants'
|
||||
|
||||
const logger = createLogger('FileAuthorization')
|
||||
|
||||
@@ -85,9 +86,7 @@ function extractWorkspaceIdFromKey(key: string): string | null {
|
||||
const parts = key.split('/')
|
||||
const workspaceId = parts[0]
|
||||
|
||||
// Validate UUID format
|
||||
const UUID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i
|
||||
if (workspaceId && UUID_PATTERN.test(workspaceId)) {
|
||||
if (workspaceId && isUuid(workspaceId)) {
|
||||
return workspaceId
|
||||
}
|
||||
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { sanitizeFileName } from '@/executor/constants'
|
||||
import '@/lib/uploads/core/setup.server'
|
||||
import { getSession } from '@/lib/auth'
|
||||
import type { StorageContext } from '@/lib/uploads/config'
|
||||
@@ -154,7 +155,7 @@ export async function POST(request: NextRequest) {
|
||||
logger.info(`Uploading knowledge-base file: ${originalName}`)
|
||||
|
||||
const timestamp = Date.now()
|
||||
const safeFileName = originalName.replace(/\s+/g, '-')
|
||||
const safeFileName = sanitizeFileName(originalName)
|
||||
const storageKey = `kb/${timestamp}-${safeFileName}`
|
||||
|
||||
const metadata: Record<string, string> = {
|
||||
@@ -267,9 +268,8 @@ export async function POST(request: NextRequest) {
|
||||
|
||||
logger.info(`Uploading ${context} file: ${originalName}`)
|
||||
|
||||
// Generate storage key with context prefix and timestamp to ensure uniqueness
|
||||
const timestamp = Date.now()
|
||||
const safeFileName = originalName.replace(/\s+/g, '-')
|
||||
const safeFileName = sanitizeFileName(originalName)
|
||||
const storageKey = `${context}/${timestamp}-${safeFileName}`
|
||||
|
||||
const metadata: Record<string, string> = {
|
||||
|
||||
@@ -5,6 +5,7 @@ import { executeInE2B } from '@/lib/execution/e2b'
|
||||
import { executeInIsolatedVM } from '@/lib/execution/isolated-vm'
|
||||
import { CodeLanguage, DEFAULT_CODE_LANGUAGE, isValidCodeLanguage } from '@/lib/execution/languages'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { escapeRegExp, normalizeName, REFERENCE } from '@/executor/constants'
|
||||
import {
|
||||
createEnvVarPattern,
|
||||
createWorkflowVariablePattern,
|
||||
@@ -405,7 +406,7 @@ function resolveWorkflowVariables(
|
||||
|
||||
// Find the variable by name (workflowVariables is indexed by ID, values are variable objects)
|
||||
const foundVariable = Object.entries(workflowVariables).find(
|
||||
([_, variable]) => (variable.name || '').replace(/\s+/g, '') === variableName
|
||||
([_, variable]) => normalizeName(variable.name || '') === variableName
|
||||
)
|
||||
|
||||
let variableValue: unknown = ''
|
||||
@@ -513,31 +514,26 @@ function resolveTagVariables(
|
||||
): string {
|
||||
let resolvedCode = code
|
||||
|
||||
const tagMatches = resolvedCode.match(/<([a-zA-Z_][a-zA-Z0-9_.]*[a-zA-Z0-9_])>/g) || []
|
||||
const tagPattern = new RegExp(
|
||||
`${REFERENCE.START}([a-zA-Z_][a-zA-Z0-9_${REFERENCE.PATH_DELIMITER}]*[a-zA-Z0-9_])${REFERENCE.END}`,
|
||||
'g'
|
||||
)
|
||||
const tagMatches = resolvedCode.match(tagPattern) || []
|
||||
|
||||
for (const match of tagMatches) {
|
||||
const tagName = match.slice(1, -1).trim()
|
||||
const tagName = match.slice(REFERENCE.START.length, -REFERENCE.END.length).trim()
|
||||
|
||||
// Handle nested paths like "getrecord.response.data" or "function1.response.result"
|
||||
// First try params, then blockData directly, then try with block name mapping
|
||||
let tagValue = getNestedValue(params, tagName) || getNestedValue(blockData, tagName) || ''
|
||||
|
||||
// If not found and the path starts with a block name, try mapping the block name to ID
|
||||
if (!tagValue && tagName.includes('.')) {
|
||||
const pathParts = tagName.split('.')
|
||||
if (!tagValue && tagName.includes(REFERENCE.PATH_DELIMITER)) {
|
||||
const pathParts = tagName.split(REFERENCE.PATH_DELIMITER)
|
||||
const normalizedBlockName = pathParts[0] // This should already be normalized like "function1"
|
||||
|
||||
// Find the block ID by looking for a block name that normalizes to this value
|
||||
let blockId = null
|
||||
|
||||
for (const [blockName, id] of Object.entries(blockNameMapping)) {
|
||||
// Apply the same normalization logic as the UI: remove spaces and lowercase
|
||||
const normalizedName = blockName.replace(/\s+/g, '').toLowerCase()
|
||||
if (normalizedName === normalizedBlockName) {
|
||||
blockId = id
|
||||
break
|
||||
}
|
||||
}
|
||||
// Direct lookup using normalized block name
|
||||
const blockId = blockNameMapping[normalizedBlockName] ?? null
|
||||
|
||||
if (blockId) {
|
||||
const remainingPath = pathParts.slice(1).join('.')
|
||||
@@ -617,13 +613,6 @@ function getNestedValue(obj: any, path: string): any {
|
||||
}, obj)
|
||||
}
|
||||
|
||||
/**
|
||||
* Escape special regex characters in a string
|
||||
*/
|
||||
function escapeRegExp(string: string): string {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove one trailing newline from stdout
|
||||
* This handles the common case where print() or console.log() adds a trailing \n
|
||||
|
||||
@@ -6,6 +6,8 @@ import { getParsedBody, withMcpAuth } from '@/lib/mcp/middleware'
|
||||
import type { McpServerConfig, McpTransport } from '@/lib/mcp/types'
|
||||
import { validateMcpServerUrl } from '@/lib/mcp/url-validator'
|
||||
import { createMcpErrorResponse, createMcpSuccessResponse } from '@/lib/mcp/utils'
|
||||
import { REFERENCE } from '@/executor/constants'
|
||||
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
|
||||
|
||||
const logger = createLogger('McpServerTestAPI')
|
||||
|
||||
@@ -23,12 +25,13 @@ function isUrlBasedTransport(transport: McpTransport): boolean {
|
||||
* Resolve environment variables in strings
|
||||
*/
|
||||
function resolveEnvVars(value: string, envVars: Record<string, string>): string {
|
||||
const envMatches = value.match(/\{\{([^}]+)\}\}/g)
|
||||
const envVarPattern = createEnvVarPattern()
|
||||
const envMatches = value.match(envVarPattern)
|
||||
if (!envMatches) return value
|
||||
|
||||
let resolvedValue = value
|
||||
for (const match of envMatches) {
|
||||
const envKey = match.slice(2, -2).trim()
|
||||
const envKey = match.slice(REFERENCE.ENV_VAR_START.length, -REFERENCE.ENV_VAR_END.length).trim()
|
||||
const envValue = envVars[envKey]
|
||||
|
||||
if (envValue === undefined) {
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
import { type NextRequest, NextResponse } from 'next/server'
|
||||
import { authorizeCredentialUse } from '@/lib/auth/credential-access'
|
||||
import { validateUUID } from '@/lib/core/security/input-validation'
|
||||
import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { refreshAccessTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { isUuidV4 } from '@/executor/constants'
|
||||
export const dynamic = 'force-dynamic'
|
||||
|
||||
const logger = createLogger('GoogleCalendarAPI')
|
||||
@@ -35,18 +35,14 @@ export async function GET(request: NextRequest) {
|
||||
return NextResponse.json({ error: 'Credential ID is required' }, { status: 400 })
|
||||
}
|
||||
|
||||
const credentialValidation = validateUUID(credentialId, 'credentialId')
|
||||
if (!credentialValidation.isValid) {
|
||||
if (!isUuidV4(credentialId)) {
|
||||
logger.warn(`[${requestId}] Invalid credentialId format`, { credentialId })
|
||||
return NextResponse.json({ error: credentialValidation.error }, { status: 400 })
|
||||
return NextResponse.json({ error: 'Invalid credential ID format' }, { status: 400 })
|
||||
}
|
||||
|
||||
if (workflowId) {
|
||||
const workflowValidation = validateUUID(workflowId, 'workflowId')
|
||||
if (!workflowValidation.isValid) {
|
||||
logger.warn(`[${requestId}] Invalid workflowId format`, { workflowId })
|
||||
return NextResponse.json({ error: workflowValidation.error }, { status: 400 })
|
||||
}
|
||||
if (workflowId && !isUuidV4(workflowId)) {
|
||||
logger.warn(`[${requestId}] Invalid workflowId format`, { workflowId })
|
||||
return NextResponse.json({ error: 'Invalid workflow ID format' }, { status: 400 })
|
||||
}
|
||||
const authz = await authorizeCredentialUse(request, { credentialId, workflowId })
|
||||
if (!authz.ok || !authz.credentialOwnerUserId) {
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
import { createStreamingResponse } from '@/lib/workflows/streaming/streaming'
|
||||
import { createHttpResponseFromBlock, workflowHasResponseBlock } from '@/lib/workflows/utils'
|
||||
import type { WorkflowExecutionPayload } from '@/background/workflow-execution'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import type { StreamingExecution } from '@/executor/types'
|
||||
import { Serializer } from '@/serializer'
|
||||
@@ -86,10 +87,9 @@ function resolveOutputIds(
|
||||
const blockName = outputId.substring(0, dotIndex)
|
||||
const path = outputId.substring(dotIndex + 1)
|
||||
|
||||
const normalizedBlockName = blockName.toLowerCase().replace(/\s+/g, '')
|
||||
const normalizedBlockName = normalizeName(blockName)
|
||||
const block = Object.values(blocks).find((b: any) => {
|
||||
const normalized = (b.name || '').toLowerCase().replace(/\s+/g, '')
|
||||
return normalized === normalizedBlockName
|
||||
return normalizeName(b.name || '') === normalizedBlockName
|
||||
})
|
||||
|
||||
if (!block) {
|
||||
|
||||
@@ -18,6 +18,7 @@ import { getEnv } from '@/lib/core/config/env'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getInputFormatExample as getInputFormatExampleUtil } from '@/lib/workflows/operations/deployment-utils'
|
||||
import type { WorkflowDeploymentVersionResponse } from '@/lib/workflows/persistence/utils'
|
||||
import { startsWithUuid } from '@/executor/constants'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
@@ -289,10 +290,9 @@ export function DeployModal({
|
||||
if (!open || selectedStreamingOutputs.length === 0) return
|
||||
|
||||
const blocks = Object.values(useWorkflowStore.getState().blocks)
|
||||
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
|
||||
|
||||
const validOutputs = selectedStreamingOutputs.filter((outputId) => {
|
||||
if (UUID_REGEX.test(outputId)) {
|
||||
if (startsWithUuid(outputId)) {
|
||||
const underscoreIndex = outputId.indexOf('_')
|
||||
if (underscoreIndex === -1) return false
|
||||
|
||||
|
||||
@@ -7,9 +7,9 @@ import {
|
||||
} from '@/lib/workflows/sanitization/references'
|
||||
import { checkTagTrigger } from '@/app/workspace/[workspaceId]/w/[workflowId]/components/panel/components/editor/components/sub-block/components/tag-dropdown/tag-dropdown'
|
||||
import { useAccessibleReferencePrefixes } from '@/app/workspace/[workspaceId]/w/[workflowId]/hooks/use-accessible-reference-prefixes'
|
||||
import { normalizeName, REFERENCE } from '@/executor/constants'
|
||||
import { createEnvVarPattern, createReferencePattern } from '@/executor/utils/reference-validation'
|
||||
import { useCollaborativeWorkflow } from '@/hooks/use-collaborative-workflow'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
@@ -89,7 +89,7 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId
|
||||
*/
|
||||
const shouldHighlightReference = useCallback(
|
||||
(part: string): boolean => {
|
||||
if (!part.startsWith('<') || !part.endsWith('>')) {
|
||||
if (!part.startsWith(REFERENCE.START) || !part.endsWith(REFERENCE.END)) {
|
||||
return false
|
||||
}
|
||||
|
||||
@@ -108,8 +108,8 @@ export function useSubflowEditor(currentBlock: BlockState | null, currentBlockId
|
||||
return true
|
||||
}
|
||||
|
||||
const inner = reference.slice(1, -1)
|
||||
const [prefix] = inner.split('.')
|
||||
const inner = reference.slice(REFERENCE.START.length, -REFERENCE.END.length)
|
||||
const [prefix] = inner.split(REFERENCE.PATH_DELIMITER)
|
||||
const normalizedPrefix = normalizeName(prefix)
|
||||
|
||||
if (SYSTEM_REFERENCE_PREFIXES.has(normalizedPrefix)) {
|
||||
|
||||
@@ -2,7 +2,7 @@ import { useMemo } from 'react'
|
||||
import { useShallow } from 'zustand/react/shallow'
|
||||
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
|
||||
import { SYSTEM_REFERENCE_PREFIXES } from '@/lib/workflows/sanitization/references'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
import type { Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBlockOutputs } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||
import { REFERENCE } from '@/executor/constants'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
@@ -44,7 +45,7 @@ function parseResponseFormatSafely(responseFormatValue: any, blockId: string): a
|
||||
if (typeof responseFormatValue === 'string') {
|
||||
const trimmedValue = responseFormatValue.trim()
|
||||
|
||||
if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) {
|
||||
if (trimmedValue.startsWith(REFERENCE.START) && trimmedValue.includes(REFERENCE.END)) {
|
||||
return trimmedValue
|
||||
}
|
||||
|
||||
|
||||
@@ -16,6 +16,7 @@ import {
|
||||
import { Trash } from '@/components/emcn/icons/trash'
|
||||
import { Input, Skeleton } from '@/components/ui'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { isValidEnvVarName } from '@/executor/constants'
|
||||
import {
|
||||
usePersonalEnvironment,
|
||||
useRemoveWorkspaceEnvironment,
|
||||
@@ -28,7 +29,6 @@ import {
|
||||
const logger = createLogger('EnvironmentVariables')
|
||||
|
||||
const GRID_COLS = 'grid grid-cols-[minmax(0,1fr)_8px_minmax(0,1fr)_auto] items-center'
|
||||
const ENV_VAR_PATTERN = /^[A-Za-z_][A-Za-z0-9_]*$/
|
||||
const PRIMARY_BUTTON_STYLES =
|
||||
'!bg-[var(--brand-tertiary-2)] !text-[var(--text-inverse)] hover:!bg-[var(--brand-tertiary-2)]/90'
|
||||
|
||||
@@ -59,7 +59,7 @@ interface UIEnvironmentVariable {
|
||||
function validateEnvVarKey(key: string): string | undefined {
|
||||
if (!key) return undefined
|
||||
if (key.includes(' ')) return 'Spaces are not allowed'
|
||||
if (!ENV_VAR_PATTERN.test(key)) return 'Only letters, numbers, and underscores allowed'
|
||||
if (!isValidEnvVarName(key)) return 'Only letters, numbers, and underscores allowed'
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -377,7 +377,7 @@ export function EnvironmentVariables({ registerBeforeLeaveHandler }: Environment
|
||||
if (equalIndex === -1 || equalIndex === 0) return null
|
||||
|
||||
const potentialKey = withoutExport.substring(0, equalIndex).trim()
|
||||
if (!ENV_VAR_PATTERN.test(potentialKey)) return null
|
||||
if (!isValidEnvVarName(potentialKey)) return null
|
||||
|
||||
let value = withoutExport.substring(equalIndex + 1)
|
||||
|
||||
|
||||
@@ -22,8 +22,10 @@ import {
|
||||
getScheduleTimeValues,
|
||||
getSubBlockValue,
|
||||
} from '@/lib/workflows/schedules/utils'
|
||||
import { REFERENCE } from '@/executor/constants'
|
||||
import { type ExecutionMetadata, ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import type { ExecutionResult } from '@/executor/types'
|
||||
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
|
||||
import { mergeSubblockState } from '@/stores/workflows/server-utils'
|
||||
|
||||
const logger = createLogger('TriggerScheduleExecution')
|
||||
@@ -128,17 +130,25 @@ async function ensureBlockVariablesResolvable(
|
||||
await Promise.all(
|
||||
Object.values(subBlocks).map(async (subBlock) => {
|
||||
const value = subBlock.value
|
||||
if (typeof value !== 'string' || !value.includes('{{') || !value.includes('}}')) {
|
||||
if (
|
||||
typeof value !== 'string' ||
|
||||
!value.includes(REFERENCE.ENV_VAR_START) ||
|
||||
!value.includes(REFERENCE.ENV_VAR_END)
|
||||
) {
|
||||
return
|
||||
}
|
||||
|
||||
const matches = value.match(/{{([^}]+)}}/g)
|
||||
const envVarPattern = createEnvVarPattern()
|
||||
const matches = value.match(envVarPattern)
|
||||
if (!matches) {
|
||||
return
|
||||
}
|
||||
|
||||
for (const match of matches) {
|
||||
const varName = match.slice(2, -2)
|
||||
const varName = match.slice(
|
||||
REFERENCE.ENV_VAR_START.length,
|
||||
-REFERENCE.ENV_VAR_END.length
|
||||
)
|
||||
const encryptedValue = variables[varName]
|
||||
if (!encryptedValue) {
|
||||
throw new Error(`Environment variable "${varName}" was not found`)
|
||||
|
||||
@@ -165,6 +165,10 @@ export const AGENT = {
|
||||
CUSTOM_TOOL_PREFIX: 'custom_',
|
||||
} as const
|
||||
|
||||
export const MCP = {
|
||||
TOOL_PREFIX: 'mcp-',
|
||||
} as const
|
||||
|
||||
export const MEMORY = {
|
||||
DEFAULT_SLIDING_WINDOW_SIZE: 10,
|
||||
DEFAULT_SLIDING_WINDOW_TOKENS: 4000,
|
||||
@@ -338,3 +342,60 @@ export function parseReferencePath(reference: string): string[] {
|
||||
const content = extractReferenceContent(reference)
|
||||
return content.split(REFERENCE.PATH_DELIMITER)
|
||||
}
|
||||
|
||||
export const PATTERNS = {
|
||||
UUID: /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i,
|
||||
UUID_V4: /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i,
|
||||
UUID_PREFIX: /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i,
|
||||
ENV_VAR_NAME: /^[A-Za-z_][A-Za-z0-9_]*$/,
|
||||
} as const
|
||||
|
||||
export function isUuid(value: string): boolean {
|
||||
return PATTERNS.UUID.test(value)
|
||||
}
|
||||
|
||||
export function isUuidV4(value: string): boolean {
|
||||
return PATTERNS.UUID_V4.test(value)
|
||||
}
|
||||
|
||||
export function startsWithUuid(value: string): boolean {
|
||||
return PATTERNS.UUID_PREFIX.test(value)
|
||||
}
|
||||
|
||||
export function isValidEnvVarName(name: string): boolean {
|
||||
return PATTERNS.ENV_VAR_NAME.test(name)
|
||||
}
|
||||
|
||||
export function sanitizeFileName(fileName: string): string {
|
||||
return fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||
}
|
||||
|
||||
export function isCustomTool(toolId: string): boolean {
|
||||
return toolId.startsWith(AGENT.CUSTOM_TOOL_PREFIX)
|
||||
}
|
||||
|
||||
export function isMcpTool(toolId: string): boolean {
|
||||
return toolId.startsWith(MCP.TOOL_PREFIX)
|
||||
}
|
||||
|
||||
export function stripCustomToolPrefix(name: string): string {
|
||||
return name.startsWith(AGENT.CUSTOM_TOOL_PREFIX)
|
||||
? name.slice(AGENT.CUSTOM_TOOL_PREFIX.length)
|
||||
: name
|
||||
}
|
||||
|
||||
export function stripMcpToolPrefix(name: string): string {
|
||||
return name.startsWith(MCP.TOOL_PREFIX) ? name.slice(MCP.TOOL_PREFIX.length) : name
|
||||
}
|
||||
|
||||
export function escapeRegExp(value: string): string {
|
||||
return value.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a name for comparison by converting to lowercase and removing spaces.
|
||||
* Used for both block names and variable names to ensure consistent matching.
|
||||
*/
|
||||
export function normalizeName(name: string): string {
|
||||
return name.toLowerCase().replace(/\s+/g, '')
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { afterEach, beforeEach, describe, expect, it, type Mock, vi } from 'vitest'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import { BlockType } from '@/executor/constants'
|
||||
import { BlockType, isMcpTool } from '@/executor/constants'
|
||||
import { AgentBlockHandler } from '@/executor/handlers/agent/agent-handler'
|
||||
import type { ExecutionContext, StreamingExecution } from '@/executor/types'
|
||||
import { executeProviderRequest } from '@/providers'
|
||||
@@ -1384,7 +1384,7 @@ describe('AgentBlockHandler', () => {
|
||||
|
||||
it('should handle MCP tools in agent execution', async () => {
|
||||
mockExecuteTool.mockImplementation((toolId, params, skipProxy, skipPostProcess, context) => {
|
||||
if (toolId.startsWith('mcp-')) {
|
||||
if (isMcpTool(toolId)) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
output: {
|
||||
@@ -1660,7 +1660,7 @@ describe('AgentBlockHandler', () => {
|
||||
let capturedContext: any
|
||||
mockExecuteTool.mockImplementation((toolId, params, skipProxy, skipPostProcess, context) => {
|
||||
capturedContext = context
|
||||
if (toolId.startsWith('mcp-')) {
|
||||
if (isMcpTool(toolId)) {
|
||||
return Promise.resolve({
|
||||
success: true,
|
||||
output: { content: [{ type: 'text', text: 'Success' }] },
|
||||
|
||||
@@ -6,7 +6,14 @@ import { createMcpToolId } from '@/lib/mcp/utils'
|
||||
import { refreshTokenIfNeeded } from '@/app/api/auth/oauth/utils'
|
||||
import { getAllBlocks } from '@/blocks'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { AGENT, BlockType, DEFAULTS, HTTP } from '@/executor/constants'
|
||||
import {
|
||||
AGENT,
|
||||
BlockType,
|
||||
DEFAULTS,
|
||||
HTTP,
|
||||
REFERENCE,
|
||||
stripCustomToolPrefix,
|
||||
} from '@/executor/constants'
|
||||
import { memoryService } from '@/executor/handlers/agent/memory'
|
||||
import type {
|
||||
AgentInputs,
|
||||
@@ -105,7 +112,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
if (typeof responseFormat === 'string') {
|
||||
const trimmedValue = responseFormat.trim()
|
||||
|
||||
if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) {
|
||||
if (trimmedValue.startsWith(REFERENCE.START) && trimmedValue.includes(REFERENCE.END)) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
@@ -1337,7 +1344,7 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
private formatToolCall(tc: any) {
|
||||
const toolName = this.stripCustomToolPrefix(tc.name)
|
||||
const toolName = stripCustomToolPrefix(tc.name)
|
||||
|
||||
return {
|
||||
...tc,
|
||||
@@ -1349,10 +1356,4 @@ export class AgentBlockHandler implements BlockHandler {
|
||||
result: tc.result || tc.output,
|
||||
}
|
||||
}
|
||||
|
||||
private stripCustomToolPrefix(name: string): string {
|
||||
return name.startsWith(AGENT.CUSTOM_TOOL_PREFIX)
|
||||
? name.replace(AGENT.CUSTOM_TOOL_PREFIX, '')
|
||||
: name
|
||||
}
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ vi.mock('@/tools', () => ({
|
||||
vi.mock('@/executor/utils/block-data', () => ({
|
||||
collectBlockData: vi.fn(() => ({
|
||||
blockData: { 'source-block-1': { value: 10, text: 'hello' } },
|
||||
blockNameMapping: { 'Source Block': 'source-block-1' },
|
||||
blockNameMapping: { sourceblock: 'source-block-1' },
|
||||
})),
|
||||
}))
|
||||
|
||||
@@ -200,7 +200,7 @@ describe('ConditionBlockHandler', () => {
|
||||
envVars: mockContext.environmentVariables,
|
||||
workflowVariables: mockContext.workflowVariables,
|
||||
blockData: { 'source-block-1': { value: 10, text: 'hello' } },
|
||||
blockNameMapping: { 'Source Block': 'source-block-1' },
|
||||
blockNameMapping: { sourceblock: 'source-block-1' },
|
||||
_context: {
|
||||
workflowId: 'test-workflow-id',
|
||||
workspaceId: 'test-workspace-id',
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBlock } from '@/blocks/index'
|
||||
import { isMcpTool } from '@/executor/constants'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { executeTool } from '@/tools'
|
||||
@@ -17,10 +18,10 @@ export class GenericBlockHandler implements BlockHandler {
|
||||
block: SerializedBlock,
|
||||
inputs: Record<string, any>
|
||||
): Promise<any> {
|
||||
const isMcpTool = block.config.tool?.startsWith('mcp-')
|
||||
const isMcp = block.config.tool ? isMcpTool(block.config.tool) : false
|
||||
let tool = null
|
||||
|
||||
if (!isMcpTool) {
|
||||
if (!isMcp) {
|
||||
tool = getTool(block.config.tool)
|
||||
if (!tool) {
|
||||
throw new Error(`Tool not found: ${block.config.tool}`)
|
||||
|
||||
@@ -7,7 +7,9 @@ import {
|
||||
buildResumeUiUrl,
|
||||
type FieldType,
|
||||
HTTP,
|
||||
normalizeName,
|
||||
PAUSE_RESUME,
|
||||
REFERENCE,
|
||||
} from '@/executor/constants'
|
||||
import {
|
||||
generatePauseContextId,
|
||||
@@ -16,7 +18,6 @@ import {
|
||||
import type { BlockHandler, ExecutionContext, PauseMetadata } from '@/executor/types'
|
||||
import { collectBlockData } from '@/executor/utils/block-data'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
import { executeTool } from '@/tools'
|
||||
|
||||
const logger = createLogger('HumanInTheLoopBlockHandler')
|
||||
@@ -477,7 +478,11 @@ export class HumanInTheLoopBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
private isVariableReference(value: any): boolean {
|
||||
return typeof value === 'string' && value.trim().startsWith('<') && value.trim().includes('>')
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.trim().startsWith(REFERENCE.START) &&
|
||||
value.trim().includes(REFERENCE.END)
|
||||
)
|
||||
}
|
||||
|
||||
private parseObjectStrings(data: any): any {
|
||||
@@ -590,7 +595,6 @@ export class HumanInTheLoopBlockHandler implements BlockHandler {
|
||||
blockDataWithPause[pauseBlockId] = pauseOutput
|
||||
|
||||
if (pauseBlockName) {
|
||||
blockNameMappingWithPause[pauseBlockName] = pauseBlockId
|
||||
blockNameMappingWithPause[normalizeName(pauseBlockName)] = pauseBlockId
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { BlockOutput } from '@/blocks/types'
|
||||
import { BlockType, HTTP } from '@/executor/constants'
|
||||
import { BlockType, HTTP, REFERENCE } from '@/executor/constants'
|
||||
import type { BlockHandler, ExecutionContext } from '@/executor/types'
|
||||
import type { SerializedBlock } from '@/serializer/types'
|
||||
|
||||
@@ -220,7 +220,11 @@ export class ResponseBlockHandler implements BlockHandler {
|
||||
}
|
||||
|
||||
private isVariableReference(value: any): boolean {
|
||||
return typeof value === 'string' && value.trim().startsWith('<') && value.trim().includes('>')
|
||||
return (
|
||||
typeof value === 'string' &&
|
||||
value.trim().startsWith(REFERENCE.START) &&
|
||||
value.trim().includes(REFERENCE.END)
|
||||
)
|
||||
}
|
||||
|
||||
private parseObjectStrings(data: any): any {
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
|
||||
export interface BlockDataCollection {
|
||||
@@ -14,9 +15,7 @@ export function collectBlockData(ctx: ExecutionContext): BlockDataCollection {
|
||||
blockData[id] = state.output
|
||||
const workflowBlock = ctx.workflow?.blocks?.find((b) => b.id === id)
|
||||
if (workflowBlock?.metadata?.name) {
|
||||
blockNameMapping[workflowBlock.metadata.name] = id
|
||||
const normalized = workflowBlock.metadata.name.replace(/\s+/g, '').toLowerCase()
|
||||
blockNameMapping[normalized] = id
|
||||
blockNameMapping[normalizeName(workflowBlock.metadata.name)] = id
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { BlockType, REFERENCE } from '@/executor/constants'
|
||||
import { BlockType } from '@/executor/constants'
|
||||
import type { ExecutionState, LoopScope } from '@/executor/execution/state'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import { replaceValidReferences } from '@/executor/utils/reference-validation'
|
||||
import { createEnvVarPattern, replaceValidReferences } from '@/executor/utils/reference-validation'
|
||||
import { BlockResolver } from '@/executor/variables/resolvers/block'
|
||||
import { EnvResolver } from '@/executor/variables/resolvers/env'
|
||||
import { LoopResolver } from '@/executor/variables/resolvers/loop'
|
||||
@@ -185,8 +185,7 @@ export class VariableResolver {
|
||||
throw replacementError
|
||||
}
|
||||
|
||||
const envRegex = new RegExp(`${REFERENCE.ENV_VAR_START}([^}]+)${REFERENCE.ENV_VAR_END}`, 'g')
|
||||
result = result.replace(envRegex, (match) => {
|
||||
result = result.replace(createEnvVarPattern(), (match) => {
|
||||
const resolved = this.resolveReference(match, resolutionContext)
|
||||
return typeof resolved === 'string' ? resolved : match
|
||||
})
|
||||
@@ -236,8 +235,7 @@ export class VariableResolver {
|
||||
throw replacementError
|
||||
}
|
||||
|
||||
const envRegex = new RegExp(`${REFERENCE.ENV_VAR_START}([^}]+)${REFERENCE.ENV_VAR_END}`, 'g')
|
||||
result = result.replace(envRegex, (match) => {
|
||||
result = result.replace(createEnvVarPattern(), (match) => {
|
||||
const resolved = this.resolveReference(match, resolutionContext)
|
||||
return typeof resolved === 'string' ? resolved : match
|
||||
})
|
||||
|
||||
@@ -1,22 +1,24 @@
|
||||
import { isReference, parseReferencePath, SPECIAL_REFERENCE_PREFIXES } from '@/executor/constants'
|
||||
import {
|
||||
isReference,
|
||||
normalizeName,
|
||||
parseReferencePath,
|
||||
SPECIAL_REFERENCE_PREFIXES,
|
||||
} from '@/executor/constants'
|
||||
import {
|
||||
navigatePath,
|
||||
type ResolutionContext,
|
||||
type Resolver,
|
||||
} from '@/executor/variables/resolvers/reference'
|
||||
import type { SerializedWorkflow } from '@/serializer/types'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
|
||||
export class BlockResolver implements Resolver {
|
||||
private blockByNormalizedName: Map<string, string>
|
||||
private nameToBlockId: Map<string, string>
|
||||
|
||||
constructor(private workflow: SerializedWorkflow) {
|
||||
this.blockByNormalizedName = new Map()
|
||||
this.nameToBlockId = new Map()
|
||||
for (const block of workflow.blocks) {
|
||||
this.blockByNormalizedName.set(block.id, block.id)
|
||||
if (block.metadata?.name) {
|
||||
const normalized = normalizeName(block.metadata.name)
|
||||
this.blockByNormalizedName.set(normalized, block.id)
|
||||
this.nameToBlockId.set(normalizeName(block.metadata.name), block.id)
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -80,11 +82,7 @@ export class BlockResolver implements Resolver {
|
||||
}
|
||||
|
||||
private findBlockIdByName(name: string): string | undefined {
|
||||
if (this.blockByNormalizedName.has(name)) {
|
||||
return this.blockByNormalizedName.get(name)
|
||||
}
|
||||
const normalized = normalizeName(name)
|
||||
return this.blockByNormalizedName.get(normalized)
|
||||
return this.nameToBlockId.get(normalizeName(name))
|
||||
}
|
||||
|
||||
public formatValueForBlock(
|
||||
|
||||
@@ -117,7 +117,7 @@ export class ParallelResolver implements Resolver {
|
||||
// String handling
|
||||
if (typeof rawItems === 'string') {
|
||||
// Skip references - they should be resolved by the variable resolver
|
||||
if (rawItems.startsWith('<')) {
|
||||
if (rawItems.startsWith(REFERENCE.START)) {
|
||||
return []
|
||||
}
|
||||
|
||||
|
||||
@@ -1,12 +1,11 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { VariableManager } from '@/lib/workflows/variables/variable-manager'
|
||||
import { isReference, parseReferencePath, REFERENCE } from '@/executor/constants'
|
||||
import { isReference, normalizeName, parseReferencePath, REFERENCE } from '@/executor/constants'
|
||||
import {
|
||||
navigatePath,
|
||||
type ResolutionContext,
|
||||
type Resolver,
|
||||
} from '@/executor/variables/resolvers/reference'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
|
||||
const logger = createLogger('WorkflowResolver')
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import { and, eq, isNull } from 'drizzle-orm'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { loadWorkflowFromNormalizedTables } from '@/lib/workflows/persistence/utils'
|
||||
import { sanitizeForCopilot } from '@/lib/workflows/sanitization/json-sanitizer'
|
||||
import { escapeRegExp } from '@/executor/constants'
|
||||
import type { ChatContext } from '@/stores/panel/copilot/types'
|
||||
|
||||
export type AgentContextType =
|
||||
@@ -153,10 +154,6 @@ export async function processContextsServer(
|
||||
return filtered
|
||||
}
|
||||
|
||||
function escapeRegExp(input: string): string {
|
||||
return input.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')
|
||||
}
|
||||
|
||||
function sanitizeMessageForDocs(rawMessage: string, contexts: ChatContext[] | undefined): string {
|
||||
if (!rawMessage) return ''
|
||||
if (!Array.isArray(contexts) || contexts.length === 0) {
|
||||
|
||||
@@ -4,10 +4,10 @@ import {
|
||||
} from '@/lib/core/utils/response-format'
|
||||
import { getBlockOutputPaths } from '@/lib/workflows/blocks/block-outputs'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { useVariablesStore } from '@/stores/panel/variables/store'
|
||||
import type { Variable } from '@/stores/panel/variables/types'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
|
||||
export interface WorkflowContext {
|
||||
|
||||
@@ -16,8 +16,8 @@ import {
|
||||
type GetBlockOutputsResultType,
|
||||
} from '@/lib/copilot/tools/shared/schemas'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
const logger = createLogger('GetBlockOutputsClientTool')
|
||||
|
||||
@@ -12,6 +12,7 @@ import { isValidKey } from '@/lib/workflows/sanitization/key-validation'
|
||||
import { validateWorkflowState } from '@/lib/workflows/sanitization/validation'
|
||||
import { getAllBlocks, getBlock } from '@/blocks/registry'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { EDGE, normalizeName } from '@/executor/constants'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
import { TRIGGER_RUNTIME_SUBBLOCK_IDS } from '@/triggers/constants'
|
||||
|
||||
@@ -55,6 +56,7 @@ type SkippedItemType =
|
||||
| 'invalid_subblock_field'
|
||||
| 'missing_required_params'
|
||||
| 'invalid_subflow_parent'
|
||||
| 'duplicate_block_name'
|
||||
|
||||
/**
|
||||
* Represents an item that was skipped during operation application
|
||||
@@ -80,6 +82,21 @@ function logSkippedItem(skippedItems: SkippedItem[], item: SkippedItem): void {
|
||||
skippedItems.push(item)
|
||||
}
|
||||
|
||||
/**
|
||||
* Finds an existing block with the same normalized name.
|
||||
*/
|
||||
function findBlockWithDuplicateNormalizedName(
|
||||
blocks: Record<string, any>,
|
||||
name: string,
|
||||
excludeBlockId: string
|
||||
): [string, any] | undefined {
|
||||
const normalizedName = normalizeName(name)
|
||||
return Object.entries(blocks).find(
|
||||
([blockId, block]: [string, any]) =>
|
||||
blockId !== excludeBlockId && normalizeName(block.name || '') === normalizedName
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
* Result of input validation
|
||||
*/
|
||||
@@ -773,10 +790,10 @@ function validateSourceHandleForBlock(
|
||||
}
|
||||
|
||||
case 'condition': {
|
||||
if (!sourceHandle.startsWith('condition-')) {
|
||||
if (!sourceHandle.startsWith(EDGE.CONDITION_PREFIX)) {
|
||||
return {
|
||||
valid: false,
|
||||
error: `Invalid source handle "${sourceHandle}" for condition block. Must start with "condition-"`,
|
||||
error: `Invalid source handle "${sourceHandle}" for condition block. Must start with "${EDGE.CONDITION_PREFIX}"`,
|
||||
}
|
||||
}
|
||||
|
||||
@@ -792,12 +809,12 @@ function validateSourceHandleForBlock(
|
||||
}
|
||||
|
||||
case 'router':
|
||||
if (sourceHandle === 'source' || sourceHandle.startsWith('router-')) {
|
||||
if (sourceHandle === 'source' || sourceHandle.startsWith(EDGE.ROUTER_PREFIX)) {
|
||||
return { valid: true }
|
||||
}
|
||||
return {
|
||||
valid: false,
|
||||
error: `Invalid source handle "${sourceHandle}" for router block. Valid handles: source, router-{targetId}, error`,
|
||||
error: `Invalid source handle "${sourceHandle}" for router block. Valid handles: source, ${EDGE.ROUTER_PREFIX}{targetId}, error`,
|
||||
}
|
||||
|
||||
default:
|
||||
@@ -1387,7 +1404,39 @@ function applyOperationsToWorkflowState(
|
||||
block.type = params.type
|
||||
}
|
||||
}
|
||||
if (params?.name !== undefined) block.name = params.name
|
||||
if (params?.name !== undefined) {
|
||||
if (!normalizeName(params.name)) {
|
||||
logSkippedItem(skippedItems, {
|
||||
type: 'missing_required_params',
|
||||
operationType: 'edit',
|
||||
blockId: block_id,
|
||||
reason: `Cannot rename to empty name`,
|
||||
details: { requestedName: params.name },
|
||||
})
|
||||
} else {
|
||||
const conflictingBlock = findBlockWithDuplicateNormalizedName(
|
||||
modifiedState.blocks,
|
||||
params.name,
|
||||
block_id
|
||||
)
|
||||
|
||||
if (conflictingBlock) {
|
||||
logSkippedItem(skippedItems, {
|
||||
type: 'duplicate_block_name',
|
||||
operationType: 'edit',
|
||||
blockId: block_id,
|
||||
reason: `Cannot rename to "${params.name}" - conflicts with "${conflictingBlock[1].name}"`,
|
||||
details: {
|
||||
requestedName: params.name,
|
||||
conflictingBlockId: conflictingBlock[0],
|
||||
conflictingBlockName: conflictingBlock[1].name,
|
||||
},
|
||||
})
|
||||
} else {
|
||||
block.name = params.name
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Handle trigger mode toggle
|
||||
if (typeof params?.triggerMode === 'boolean') {
|
||||
@@ -1571,7 +1620,7 @@ function applyOperationsToWorkflowState(
|
||||
}
|
||||
|
||||
case 'add': {
|
||||
if (!params?.type || !params?.name) {
|
||||
if (!params?.type || !params?.name || !normalizeName(params.name)) {
|
||||
logSkippedItem(skippedItems, {
|
||||
type: 'missing_required_params',
|
||||
operationType: 'add',
|
||||
@@ -1582,6 +1631,27 @@ function applyOperationsToWorkflowState(
|
||||
break
|
||||
}
|
||||
|
||||
const conflictingBlock = findBlockWithDuplicateNormalizedName(
|
||||
modifiedState.blocks,
|
||||
params.name,
|
||||
block_id
|
||||
)
|
||||
|
||||
if (conflictingBlock) {
|
||||
logSkippedItem(skippedItems, {
|
||||
type: 'duplicate_block_name',
|
||||
operationType: 'add',
|
||||
blockId: block_id,
|
||||
reason: `Block name "${params.name}" conflicts with existing block "${conflictingBlock[1].name}"`,
|
||||
details: {
|
||||
requestedName: params.name,
|
||||
conflictingBlockId: conflictingBlock[0],
|
||||
conflictingBlockName: conflictingBlock[1].name,
|
||||
},
|
||||
})
|
||||
break
|
||||
}
|
||||
|
||||
// Special container types (loop, parallel) are not in the block registry but are valid
|
||||
const isContainerType = params.type === 'loop' || params.type === 'parallel'
|
||||
|
||||
|
||||
@@ -8,7 +8,6 @@ import {
|
||||
validateNumericId,
|
||||
validatePathSegment,
|
||||
validateUrlWithDNS,
|
||||
validateUUID,
|
||||
} from '@/lib/core/security/input-validation'
|
||||
import { sanitizeForLogging } from '@/lib/core/security/redaction'
|
||||
|
||||
@@ -194,54 +193,6 @@ describe('validatePathSegment', () => {
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateUUID', () => {
|
||||
describe('valid UUIDs', () => {
|
||||
it.concurrent('should accept valid UUID v4', () => {
|
||||
const result = validateUUID('550e8400-e29b-41d4-a716-446655440000')
|
||||
expect(result.isValid).toBe(true)
|
||||
})
|
||||
|
||||
it.concurrent('should accept UUID with uppercase letters', () => {
|
||||
const result = validateUUID('550E8400-E29B-41D4-A716-446655440000')
|
||||
expect(result.isValid).toBe(true)
|
||||
expect(result.sanitized).toBe('550e8400-e29b-41d4-a716-446655440000')
|
||||
})
|
||||
|
||||
it.concurrent('should normalize UUID to lowercase', () => {
|
||||
const result = validateUUID('550E8400-E29B-41D4-A716-446655440000')
|
||||
expect(result.sanitized).toBe('550e8400-e29b-41d4-a716-446655440000')
|
||||
})
|
||||
})
|
||||
|
||||
describe('invalid UUIDs', () => {
|
||||
it.concurrent('should reject non-UUID strings', () => {
|
||||
const result = validateUUID('not-a-uuid')
|
||||
expect(result.isValid).toBe(false)
|
||||
expect(result.error).toContain('valid UUID')
|
||||
})
|
||||
|
||||
it.concurrent('should reject UUID with wrong version', () => {
|
||||
const result = validateUUID('550e8400-e29b-31d4-a716-446655440000') // version 3
|
||||
expect(result.isValid).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should reject UUID with wrong variant', () => {
|
||||
const result = validateUUID('550e8400-e29b-41d4-1716-446655440000') // wrong variant
|
||||
expect(result.isValid).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should reject empty string', () => {
|
||||
const result = validateUUID('')
|
||||
expect(result.isValid).toBe(false)
|
||||
})
|
||||
|
||||
it.concurrent('should reject null', () => {
|
||||
const result = validateUUID(null)
|
||||
expect(result.isValid).toBe(false)
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('validateAlphanumericId', () => {
|
||||
it.concurrent('should accept alphanumeric IDs', () => {
|
||||
const result = validateAlphanumericId('user123')
|
||||
|
||||
@@ -171,46 +171,6 @@ export function validatePathSegment(
|
||||
return { isValid: true, sanitized: value }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates a UUID (v4 format)
|
||||
*
|
||||
* @param value - The UUID to validate
|
||||
* @param paramName - Name of the parameter for error messages
|
||||
* @returns ValidationResult
|
||||
*
|
||||
* @example
|
||||
* ```typescript
|
||||
* const result = validateUUID(workflowId, 'workflowId')
|
||||
* if (!result.isValid) {
|
||||
* return NextResponse.json({ error: result.error }, { status: 400 })
|
||||
* }
|
||||
* ```
|
||||
*/
|
||||
export function validateUUID(
|
||||
value: string | null | undefined,
|
||||
paramName = 'UUID'
|
||||
): ValidationResult {
|
||||
if (value === null || value === undefined || value === '') {
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} is required`,
|
||||
}
|
||||
}
|
||||
|
||||
// UUID v4 pattern
|
||||
const uuidPattern = /^[0-9a-f]{8}-[0-9a-f]{4}-4[0-9a-f]{3}-[89ab][0-9a-f]{3}-[0-9a-f]{12}$/i
|
||||
|
||||
if (!uuidPattern.test(value)) {
|
||||
logger.warn('Invalid UUID format', { paramName, value: value.substring(0, 50) })
|
||||
return {
|
||||
isValid: false,
|
||||
error: `${paramName} must be a valid UUID`,
|
||||
}
|
||||
}
|
||||
|
||||
return { isValid: true, sanitized: value.toLowerCase() }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates an alphanumeric ID (letters, numbers, hyphens, underscores only)
|
||||
*
|
||||
|
||||
@@ -1,8 +1,6 @@
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import {
|
||||
buildTraceSpans,
|
||||
stripCustomToolPrefix,
|
||||
} from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||
import { buildTraceSpans } from '@/lib/logs/execution/trace-spans/trace-spans'
|
||||
import { stripCustomToolPrefix } from '@/executor/constants'
|
||||
import type { ExecutionResult } from '@/executor/types'
|
||||
|
||||
describe('buildTraceSpans', () => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import type { ToolCall, TraceSpan } from '@/lib/logs/types'
|
||||
import { isWorkflowBlockType } from '@/executor/constants'
|
||||
import { isWorkflowBlockType, stripCustomToolPrefix } from '@/executor/constants'
|
||||
import type { ExecutionResult } from '@/executor/types'
|
||||
|
||||
const logger = createLogger('TraceSpans')
|
||||
@@ -769,7 +769,3 @@ function ensureNestedWorkflowsProcessed(span: TraceSpan): TraceSpan {
|
||||
|
||||
return processedSpan
|
||||
}
|
||||
|
||||
export function stripCustomToolPrefix(name: string) {
|
||||
return name.startsWith('custom_') ? name.replace('custom_', '') : name
|
||||
}
|
||||
|
||||
@@ -25,6 +25,8 @@ import type {
|
||||
McpTransport,
|
||||
} from '@/lib/mcp/types'
|
||||
import { MCP_CONSTANTS } from '@/lib/mcp/utils'
|
||||
import { REFERENCE } from '@/executor/constants'
|
||||
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
|
||||
|
||||
const logger = createLogger('McpService')
|
||||
|
||||
@@ -49,14 +51,17 @@ class McpService {
|
||||
* Resolve environment variables in strings
|
||||
*/
|
||||
private resolveEnvVars(value: string, envVars: Record<string, string>): string {
|
||||
const envMatches = value.match(/\{\{([^}]+)\}\}/g)
|
||||
const envVarPattern = createEnvVarPattern()
|
||||
const envMatches = value.match(envVarPattern)
|
||||
if (!envMatches) return value
|
||||
|
||||
let resolvedValue = value
|
||||
const missingVars: string[] = []
|
||||
|
||||
for (const match of envMatches) {
|
||||
const envKey = match.slice(2, -2).trim()
|
||||
const envKey = match
|
||||
.slice(REFERENCE.ENV_VAR_START.length, -REFERENCE.ENV_VAR_END.length)
|
||||
.trim()
|
||||
const envValue = envVars[envKey]
|
||||
|
||||
if (envValue === undefined) {
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { NextResponse } from 'next/server'
|
||||
import type { McpApiResponse } from '@/lib/mcp/types'
|
||||
import { isMcpTool, MCP } from '@/executor/constants'
|
||||
|
||||
/**
|
||||
* MCP-specific constants
|
||||
@@ -124,7 +125,7 @@ export function categorizeError(error: unknown): { message: string; status: numb
|
||||
* Create standardized MCP tool ID from server ID and tool name
|
||||
*/
|
||||
export function createMcpToolId(serverId: string, toolName: string): string {
|
||||
const normalizedServerId = serverId.startsWith('mcp-') ? serverId : `mcp-${serverId}`
|
||||
const normalizedServerId = isMcpTool(serverId) ? serverId : `${MCP.TOOL_PREFIX}${serverId}`
|
||||
return `${normalizedServerId}-${toolName}`
|
||||
}
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import { isUuid, sanitizeFileName } from '@/executor/constants'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
|
||||
/**
|
||||
@@ -15,7 +16,7 @@ export interface ExecutionContext {
|
||||
*/
|
||||
export function generateExecutionFileKey(context: ExecutionContext, fileName: string): string {
|
||||
const { workspaceId, workflowId, executionId } = context
|
||||
const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||
const safeFileName = sanitizeFileName(fileName)
|
||||
return `execution/${workspaceId}/${workflowId}/${executionId}/${safeFileName}`
|
||||
}
|
||||
|
||||
@@ -26,17 +27,7 @@ export function generateFileId(): string {
|
||||
return `file_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID pattern for validating execution context IDs
|
||||
*/
|
||||
const UUID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i
|
||||
|
||||
/**
|
||||
* Check if a string matches UUID pattern
|
||||
*/
|
||||
export function isUuid(str: string): boolean {
|
||||
return UUID_PATTERN.test(str)
|
||||
}
|
||||
export { isUuid }
|
||||
|
||||
/**
|
||||
* Check if a key matches execution file pattern
|
||||
|
||||
@@ -19,6 +19,7 @@ import {
|
||||
uploadFile,
|
||||
} from '@/lib/uploads/core/storage-service'
|
||||
import { getFileMetadataByKey, insertFileMetadata } from '@/lib/uploads/server/metadata'
|
||||
import { isUuid, sanitizeFileName } from '@/executor/constants'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
|
||||
const logger = createLogger('WorkspaceFileStorage')
|
||||
@@ -36,11 +37,6 @@ export interface WorkspaceFileRecord {
|
||||
uploadedAt: Date
|
||||
}
|
||||
|
||||
/**
|
||||
* UUID pattern for validating workspace IDs
|
||||
*/
|
||||
const UUID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i
|
||||
|
||||
/**
|
||||
* Workspace file key pattern: workspace/{workspaceId}/{timestamp}-{random}-{filename}
|
||||
*/
|
||||
@@ -73,7 +69,7 @@ export function parseWorkspaceFileKey(key: string): string | null {
|
||||
}
|
||||
|
||||
const workspaceId = match[1]
|
||||
return UUID_PATTERN.test(workspaceId) ? workspaceId : null
|
||||
return isUuid(workspaceId) ? workspaceId : null
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -83,7 +79,7 @@ export function parseWorkspaceFileKey(key: string): string | null {
|
||||
export function generateWorkspaceFileKey(workspaceId: string, fileName: string): string {
|
||||
const timestamp = Date.now()
|
||||
const random = Math.random().toString(36).substring(2, 9)
|
||||
const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||
const safeFileName = sanitizeFileName(fileName)
|
||||
return `workspace/${workspaceId}/${timestamp}-${random}-${safeFileName}`
|
||||
}
|
||||
|
||||
|
||||
@@ -8,6 +8,7 @@ import type {
|
||||
} from '@/lib/uploads/providers/blob/types'
|
||||
import type { FileInfo } from '@/lib/uploads/shared/types'
|
||||
import { sanitizeStorageMetadata } from '@/lib/uploads/utils/file-utils'
|
||||
import { sanitizeFileName } from '@/executor/constants'
|
||||
|
||||
type BlobServiceClientInstance = Awaited<
|
||||
ReturnType<typeof import('@azure/storage-blob').BlobServiceClient.fromConnectionString>
|
||||
@@ -79,7 +80,7 @@ export async function uploadToBlob(
|
||||
shouldPreserveKey = preserveKey ?? false
|
||||
}
|
||||
|
||||
const safeFileName = fileName.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
const safeFileName = sanitizeFileName(fileName)
|
||||
const uniqueKey = shouldPreserveKey ? fileName : `${Date.now()}-${safeFileName}`
|
||||
|
||||
const blobServiceClient = await getBlobServiceClient()
|
||||
@@ -357,7 +358,7 @@ export async function initiateMultipartUpload(
|
||||
containerName = BLOB_CONFIG.containerName
|
||||
}
|
||||
|
||||
const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||
const safeFileName = sanitizeFileName(fileName)
|
||||
const { v4: uuidv4 } = await import('uuid')
|
||||
const uniqueKey = `kb/${uuidv4()}-${safeFileName}`
|
||||
|
||||
|
||||
@@ -22,6 +22,7 @@ import {
|
||||
sanitizeFilenameForMetadata,
|
||||
sanitizeStorageMetadata,
|
||||
} from '@/lib/uploads/utils/file-utils'
|
||||
import { sanitizeFileName } from '@/executor/constants'
|
||||
|
||||
let _s3Client: S3Client | null = null
|
||||
|
||||
@@ -84,7 +85,7 @@ export async function uploadToS3(
|
||||
shouldSkipTimestamp = skipTimestampPrefix ?? false
|
||||
}
|
||||
|
||||
const safeFileName = fileName.replace(/\s+/g, '-') // Replace spaces with hyphens
|
||||
const safeFileName = sanitizeFileName(fileName)
|
||||
const uniqueKey = shouldSkipTimestamp ? fileName : `${Date.now()}-${safeFileName}`
|
||||
|
||||
const s3Client = getS3Client()
|
||||
@@ -223,7 +224,7 @@ export async function initiateS3MultipartUpload(
|
||||
const config = customConfig || { bucket: S3_KB_CONFIG.bucket, region: S3_KB_CONFIG.region }
|
||||
const s3Client = getS3Client()
|
||||
|
||||
const safeFileName = fileName.replace(/\s+/g, '-').replace(/[^a-zA-Z0-9.-]/g, '_')
|
||||
const safeFileName = sanitizeFileName(fileName)
|
||||
const { v4: uuidv4 } = await import('uuid')
|
||||
const uniqueKey = `kb/${uuidv4()}-${safeFileName}`
|
||||
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import type { Logger } from '@/lib/logs/console/logger'
|
||||
import type { StorageContext } from '@/lib/uploads'
|
||||
import { ACCEPTED_FILE_TYPES, SUPPORTED_DOCUMENT_EXTENSIONS } from '@/lib/uploads/utils/validation'
|
||||
import { isUuid } from '@/executor/constants'
|
||||
import type { UserFile } from '@/executor/types'
|
||||
|
||||
export interface FileAttachment {
|
||||
@@ -625,11 +626,9 @@ export function extractCleanFilename(urlOrPath: string): string {
|
||||
export function extractWorkspaceIdFromExecutionKey(key: string): string | null {
|
||||
const segments = key.split('/')
|
||||
|
||||
const UUID_PATTERN = /^[a-f0-9]{8}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{4}-[a-f0-9]{12}$/i
|
||||
|
||||
if (segments[0] === 'execution' && segments.length >= 5) {
|
||||
const workspaceId = segments[1]
|
||||
if (workspaceId && UUID_PATTERN.test(workspaceId)) {
|
||||
if (workspaceId && isUuid(workspaceId)) {
|
||||
return workspaceId
|
||||
}
|
||||
}
|
||||
|
||||
@@ -14,6 +14,8 @@ import {
|
||||
verifyProviderWebhook,
|
||||
} from '@/lib/webhooks/utils.server'
|
||||
import { executeWebhookJob } from '@/background/webhook-execution'
|
||||
import { REFERENCE } from '@/executor/constants'
|
||||
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
|
||||
|
||||
const logger = createLogger('WebhookProcessor')
|
||||
|
||||
@@ -170,12 +172,13 @@ export async function findWebhookAndWorkflow(
|
||||
* @returns String with all {{VARIABLE}} references replaced
|
||||
*/
|
||||
function resolveEnvVars(value: string, envVars: Record<string, string>): string {
|
||||
const envMatches = value.match(/\{\{([^}]+)\}\}/g)
|
||||
const envVarPattern = createEnvVarPattern()
|
||||
const envMatches = value.match(envVarPattern)
|
||||
if (!envMatches) return value
|
||||
|
||||
let resolvedValue = value
|
||||
for (const match of envMatches) {
|
||||
const envKey = match.slice(2, -2).trim()
|
||||
const envKey = match.slice(REFERENCE.ENV_VAR_START.length, -REFERENCE.ENV_VAR_END.length).trim()
|
||||
const envValue = envVars[envKey]
|
||||
if (envValue !== undefined) {
|
||||
resolvedValue = resolvedValue.replaceAll(match, envValue)
|
||||
|
||||
@@ -11,6 +11,7 @@ import {
|
||||
prepareBlockMetrics,
|
||||
} from '@/lib/workflows/autolayout/utils'
|
||||
import { BLOCK_DIMENSIONS, HANDLE_POSITIONS } from '@/lib/workflows/blocks/block-dimensions'
|
||||
import { EDGE } from '@/executor/constants'
|
||||
import type { BlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
const logger = createLogger('AutoLayout:Core')
|
||||
@@ -31,8 +32,8 @@ function getSourceHandleYOffset(block: BlockState, sourceHandle?: string | null)
|
||||
return HANDLE_POSITIONS.SUBFLOW_START_Y_OFFSET
|
||||
}
|
||||
|
||||
if (block.type === 'condition' && sourceHandle?.startsWith('condition-')) {
|
||||
const conditionId = sourceHandle.replace('condition-', '')
|
||||
if (block.type === 'condition' && sourceHandle?.startsWith(EDGE.CONDITION_PREFIX)) {
|
||||
const conditionId = sourceHandle.replace(EDGE.CONDITION_PREFIX, '')
|
||||
try {
|
||||
const conditionsValue = block.subBlocks?.conditions?.value
|
||||
if (typeof conditionsValue === 'string' && conditionsValue) {
|
||||
|
||||
@@ -97,41 +97,4 @@ export class BlockPathCalculator {
|
||||
|
||||
return accessibleMap
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets accessible block names for a specific block (for error messages).
|
||||
*
|
||||
* @param blockId - The block ID to get accessible names for
|
||||
* @param workflow - The serialized workflow
|
||||
* @param accessibleBlocksMap - Pre-calculated accessible blocks map
|
||||
* @returns Array of accessible block names and aliases
|
||||
*/
|
||||
static getAccessibleBlockNames(
|
||||
blockId: string,
|
||||
workflow: SerializedWorkflow,
|
||||
accessibleBlocksMap: Map<string, Set<string>>
|
||||
): string[] {
|
||||
const accessibleBlockIds = accessibleBlocksMap.get(blockId) || new Set<string>()
|
||||
const names: string[] = []
|
||||
|
||||
// Create a map of block IDs to blocks for efficient lookup
|
||||
const blockById = new Map(workflow.blocks.map((block) => [block.id, block]))
|
||||
|
||||
for (const accessibleBlockId of accessibleBlockIds) {
|
||||
const block = blockById.get(accessibleBlockId)
|
||||
if (block) {
|
||||
// Add both the actual name and the normalized name
|
||||
if (block.metadata?.name) {
|
||||
names.push(block.metadata.name)
|
||||
names.push(block.metadata.name.toLowerCase().replace(/\s+/g, ''))
|
||||
}
|
||||
names.push(accessibleBlockId)
|
||||
}
|
||||
}
|
||||
|
||||
// Add special aliases
|
||||
names.push('start') // Always allow start alias
|
||||
|
||||
return [...new Set(names)] // Remove duplicates
|
||||
}
|
||||
}
|
||||
|
||||
@@ -16,8 +16,10 @@ import {
|
||||
import { TriggerUtils } from '@/lib/workflows/triggers/triggers'
|
||||
import { updateWorkflowRunCounts } from '@/lib/workflows/utils'
|
||||
import { Executor } from '@/executor'
|
||||
import { REFERENCE } from '@/executor/constants'
|
||||
import type { ExecutionCallbacks, ExecutionSnapshot } from '@/executor/execution/snapshot'
|
||||
import type { ExecutionResult } from '@/executor/types'
|
||||
import { createEnvVarPattern } from '@/executor/utils/reference-validation'
|
||||
import { Serializer } from '@/serializer'
|
||||
import { mergeSubblockState } from '@/stores/workflows/server-utils'
|
||||
|
||||
@@ -190,11 +192,19 @@ export async function executeWorkflowCore(
|
||||
(subAcc, [key, subBlock]) => {
|
||||
let value = subBlock.value
|
||||
|
||||
if (typeof value === 'string' && value.includes('{{') && value.includes('}}')) {
|
||||
const matches = value.match(/{{([^}]+)}}/g)
|
||||
if (
|
||||
typeof value === 'string' &&
|
||||
value.includes(REFERENCE.ENV_VAR_START) &&
|
||||
value.includes(REFERENCE.ENV_VAR_END)
|
||||
) {
|
||||
const envVarPattern = createEnvVarPattern()
|
||||
const matches = value.match(envVarPattern)
|
||||
if (matches) {
|
||||
for (const match of matches) {
|
||||
const varName = match.slice(2, -2)
|
||||
const varName = match.slice(
|
||||
REFERENCE.ENV_VAR_START.length,
|
||||
-REFERENCE.ENV_VAR_END.length
|
||||
)
|
||||
const decryptedValue = decryptedEnvVars[varName]
|
||||
if (decryptedValue !== undefined) {
|
||||
value = (value as string).replace(match, decryptedValue)
|
||||
@@ -218,7 +228,7 @@ export async function executeWorkflowCore(
|
||||
(acc, [blockId, blockState]) => {
|
||||
if (blockState.responseFormat && typeof blockState.responseFormat === 'string') {
|
||||
const responseFormatValue = blockState.responseFormat.trim()
|
||||
if (responseFormatValue && !responseFormatValue.startsWith('<')) {
|
||||
if (responseFormatValue && !responseFormatValue.startsWith(REFERENCE.START)) {
|
||||
try {
|
||||
acc[blockId] = {
|
||||
...blockState,
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { resolveStartCandidates, StartBlockPath } from '@/lib/workflows/triggers/triggers'
|
||||
import { normalizeName, startsWithUuid } from '@/executor/constants'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { useWorkflowStore } from '@/stores/workflows/workflow/store'
|
||||
|
||||
@@ -74,13 +75,11 @@ export function getInputFormatExample(
|
||||
// Add streaming parameters if enabled and outputs are selected
|
||||
if (includeStreaming && selectedStreamingOutputs.length > 0) {
|
||||
exampleData.stream = true
|
||||
// Convert blockId_attribute format to blockName.attribute format for display
|
||||
const UUID_REGEX = /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}/i
|
||||
|
||||
const convertedOutputs = selectedStreamingOutputs
|
||||
.map((outputId) => {
|
||||
// If it starts with a UUID, convert to blockName.attribute format
|
||||
if (UUID_REGEX.test(outputId)) {
|
||||
if (startsWithUuid(outputId)) {
|
||||
const underscoreIndex = outputId.indexOf('_')
|
||||
if (underscoreIndex === -1) return null
|
||||
|
||||
@@ -90,9 +89,7 @@ export function getInputFormatExample(
|
||||
// Find the block by ID and get its name
|
||||
const block = blocks.find((b) => b.id === blockId)
|
||||
if (block?.name) {
|
||||
// Normalize block name: lowercase and remove spaces
|
||||
const normalizedBlockName = block.name.toLowerCase().replace(/\s+/g, '')
|
||||
return `${normalizedBlockName}.${attribute}`
|
||||
return `${normalizeName(block.name)}.${attribute}`
|
||||
}
|
||||
// Block not found (deleted), return null to filter out
|
||||
return null
|
||||
@@ -104,7 +101,7 @@ export function getInputFormatExample(
|
||||
const blockName = parts[0]
|
||||
// Check if a block with this name exists
|
||||
const block = blocks.find(
|
||||
(b) => b.name?.toLowerCase().replace(/\s+/g, '') === blockName.toLowerCase()
|
||||
(b) => b.name && normalizeName(b.name) === normalizeName(blockName)
|
||||
)
|
||||
if (!block) {
|
||||
// Block not found (deleted), return null to filter out
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
import { normalizeName, REFERENCE } from '@/executor/constants'
|
||||
|
||||
export const SYSTEM_REFERENCE_PREFIXES = new Set(['start', 'loop', 'parallel', 'variable'])
|
||||
|
||||
@@ -9,11 +9,11 @@ const LEADING_REFERENCE_PATTERN = /^[<>=!\s]*$/
|
||||
export function splitReferenceSegment(
|
||||
segment: string
|
||||
): { leading: string; reference: string } | null {
|
||||
if (!segment.startsWith('<') || !segment.endsWith('>')) {
|
||||
if (!segment.startsWith(REFERENCE.START) || !segment.endsWith(REFERENCE.END)) {
|
||||
return null
|
||||
}
|
||||
|
||||
const lastOpenBracket = segment.lastIndexOf('<')
|
||||
const lastOpenBracket = segment.lastIndexOf(REFERENCE.START)
|
||||
if (lastOpenBracket === -1) {
|
||||
return null
|
||||
}
|
||||
@@ -21,7 +21,7 @@ export function splitReferenceSegment(
|
||||
const leading = lastOpenBracket > 0 ? segment.slice(0, lastOpenBracket) : ''
|
||||
const reference = segment.slice(lastOpenBracket)
|
||||
|
||||
if (!reference.startsWith('<') || !reference.endsWith('>')) {
|
||||
if (!reference.startsWith(REFERENCE.START) || !reference.endsWith(REFERENCE.END)) {
|
||||
return null
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ export function isLikelyReferenceSegment(segment: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
const inner = reference.slice(1, -1)
|
||||
const inner = reference.slice(REFERENCE.START.length, -REFERENCE.END.length)
|
||||
|
||||
if (!inner) {
|
||||
return false
|
||||
@@ -58,10 +58,10 @@ export function isLikelyReferenceSegment(segment: string): boolean {
|
||||
return false
|
||||
}
|
||||
|
||||
if (inner.includes('.')) {
|
||||
const dotIndex = inner.indexOf('.')
|
||||
if (inner.includes(REFERENCE.PATH_DELIMITER)) {
|
||||
const dotIndex = inner.indexOf(REFERENCE.PATH_DELIMITER)
|
||||
const beforeDot = inner.substring(0, dotIndex)
|
||||
const afterDot = inner.substring(dotIndex + 1)
|
||||
const afterDot = inner.substring(dotIndex + REFERENCE.PATH_DELIMITER.length)
|
||||
|
||||
if (afterDot.includes(' ')) {
|
||||
return false
|
||||
@@ -82,7 +82,8 @@ export function extractReferencePrefixes(value: string): Array<{ raw: string; pr
|
||||
return []
|
||||
}
|
||||
|
||||
const matches = value.match(/<[^>]+>/g)
|
||||
const referencePattern = new RegExp(`${REFERENCE.START}[^${REFERENCE.END}]+${REFERENCE.END}`, 'g')
|
||||
const matches = value.match(referencePattern)
|
||||
if (!matches) {
|
||||
return []
|
||||
}
|
||||
@@ -105,8 +106,8 @@ export function extractReferencePrefixes(value: string): Array<{ raw: string; pr
|
||||
continue
|
||||
}
|
||||
|
||||
const inner = referenceSegment.slice(1, -1)
|
||||
const [rawPrefix] = inner.split('.')
|
||||
const inner = referenceSegment.slice(REFERENCE.START.length, -REFERENCE.END.length)
|
||||
const [rawPrefix] = inner.split(REFERENCE.PATH_DELIMITER)
|
||||
if (!rawPrefix) {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBlock } from '@/blocks/registry'
|
||||
import { isCustomTool, isMcpTool } from '@/executor/constants'
|
||||
import type { WorkflowState } from '@/stores/workflows/workflow/types'
|
||||
import { getTool } from '@/tools/utils'
|
||||
|
||||
@@ -299,11 +300,7 @@ export function validateToolReference(
|
||||
): string | null {
|
||||
if (!toolId) return null
|
||||
|
||||
// Check if it's a custom tool or MCP tool
|
||||
const isCustomTool = toolId.startsWith('custom_')
|
||||
const isMcpTool = toolId.startsWith('mcp-')
|
||||
|
||||
if (!isCustomTool && !isMcpTool) {
|
||||
if (!isCustomTool(toolId) && !isMcpTool(toolId)) {
|
||||
// For built-in tools, verify they exist
|
||||
const tool = getTool(toolId)
|
||||
if (!tool) {
|
||||
|
||||
@@ -458,10 +458,6 @@ export function hasWorkflowChanged(
|
||||
return false
|
||||
}
|
||||
|
||||
export function stripCustomToolPrefix(name: string) {
|
||||
return name.startsWith('custom_') ? name.replace('custom_', '') : name
|
||||
}
|
||||
|
||||
export const workflowHasResponseBlock = (executionResult: ExecutionResult): boolean => {
|
||||
if (
|
||||
!executionResult?.logs ||
|
||||
|
||||
@@ -3,6 +3,7 @@ import type { CompletionUsage } from 'openai/resources/completions'
|
||||
import { getEnv, isTruthy } from '@/lib/core/config/env'
|
||||
import { isHosted } from '@/lib/core/config/feature-flags'
|
||||
import { createLogger, type Logger } from '@/lib/logs/console/logger'
|
||||
import { isCustomTool } from '@/executor/constants'
|
||||
import {
|
||||
getComputerUseModels,
|
||||
getEmbeddingModelPricing,
|
||||
@@ -431,7 +432,7 @@ export async function transformBlockTool(
|
||||
|
||||
let toolConfig: any
|
||||
|
||||
if (toolId.startsWith('custom_') && getToolAsync) {
|
||||
if (isCustomTool(toolId) && getToolAsync) {
|
||||
toolConfig = await getToolAsync(toolId)
|
||||
} else {
|
||||
toolConfig = getTool(toolId)
|
||||
|
||||
@@ -3,6 +3,7 @@ import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { BlockPathCalculator } from '@/lib/workflows/blocks/block-path-calculator'
|
||||
import { getBlock } from '@/blocks'
|
||||
import type { SubBlockConfig } from '@/blocks/types'
|
||||
import { REFERENCE } from '@/executor/constants'
|
||||
import type { SerializedBlock, SerializedWorkflow } from '@/serializer/types'
|
||||
import type { BlockState, Loop, Parallel } from '@/stores/workflows/workflow/types'
|
||||
import { generateLoopBlocks, generateParallelBlocks } from '@/stores/workflows/workflow/utils'
|
||||
@@ -351,7 +352,7 @@ export class Serializer {
|
||||
const trimmedValue = responseFormat.trim()
|
||||
|
||||
// Check for variable references like <start.input>
|
||||
if (trimmedValue.startsWith('<') && trimmedValue.includes('>')) {
|
||||
if (trimmedValue.startsWith(REFERENCE.START) && trimmedValue.includes(REFERENCE.END)) {
|
||||
// Keep variable references as-is
|
||||
return trimmedValue
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import type {
|
||||
} from '@/stores/variables/types'
|
||||
import { useWorkflowRegistry } from '@/stores/workflows/registry/store'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import { normalizeName } from '@/stores/workflows/utils'
|
||||
|
||||
const logger = createLogger('VariablesModalStore')
|
||||
|
||||
@@ -303,8 +304,8 @@ export const useVariablesStore = create<VariablesStore>()(
|
||||
Object.entries(workflowValues).forEach(([blockId, blockValues]) => {
|
||||
Object.entries(blockValues as Record<string, any>).forEach(
|
||||
([subBlockId, value]) => {
|
||||
const oldVarName = oldVariableName.replace(/\s+/g, '').toLowerCase()
|
||||
const newVarName = newName.replace(/\s+/g, '').toLowerCase()
|
||||
const oldVarName = normalizeName(oldVariableName)
|
||||
const newVarName = normalizeName(newName)
|
||||
const regex = new RegExp(`<variable\\.${oldVarName}>`, 'gi')
|
||||
|
||||
updatedWorkflowValues[blockId][subBlockId] = updateReferences(
|
||||
|
||||
@@ -1,17 +1,7 @@
|
||||
// DEPRECATED: useEnvironmentStore import removed as autofill functions were removed
|
||||
|
||||
/**
|
||||
* Checks if a value is an environment variable reference in the format {{ENV_VAR}}
|
||||
* Re-exports env var utilities from executor constants for backward compatibility
|
||||
*/
|
||||
export const isEnvVarReference = (value: string): boolean => {
|
||||
// Check if the value looks like {{ENV_VAR}}
|
||||
return /^\{\{[a-zA-Z0-9_-]+\}\}$/.test(value)
|
||||
}
|
||||
|
||||
/**
|
||||
* Extracts the environment variable name from a reference like {{ENV_VAR}}
|
||||
*/
|
||||
export const extractEnvVarName = (value: string): string | null => {
|
||||
if (!isEnvVarReference(value)) return null
|
||||
return value.slice(2, -2)
|
||||
}
|
||||
export {
|
||||
extractEnvVarName,
|
||||
isEnvVarReference,
|
||||
} from '@/executor/constants'
|
||||
|
||||
@@ -1,15 +1,8 @@
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import { useSubBlockStore } from '@/stores/workflows/subblock/store'
|
||||
import type { BlockState, SubBlockState } from '@/stores/workflows/workflow/types'
|
||||
|
||||
/**
|
||||
* Normalizes a name for comparison by converting to lowercase and removing spaces.
|
||||
* Used for both block names and variable names to ensure consistent matching.
|
||||
* @param name - The name to normalize
|
||||
* @returns The normalized name
|
||||
*/
|
||||
export function normalizeName(name: string): string {
|
||||
return name.toLowerCase().replace(/\s+/g, '')
|
||||
}
|
||||
export { normalizeName }
|
||||
|
||||
/**
|
||||
* Generates a unique block name by finding the highest number suffix among existing blocks
|
||||
|
||||
@@ -594,18 +594,18 @@ describe('workflow store', () => {
|
||||
}
|
||||
)
|
||||
|
||||
it.concurrent('should handle edge cases with empty or whitespace-only names', () => {
|
||||
it.concurrent('should reject empty or whitespace-only names', () => {
|
||||
const { updateBlockName } = useWorkflowStore.getState()
|
||||
|
||||
const result1 = updateBlockName('block1', '')
|
||||
expect(result1.success).toBe(true)
|
||||
expect(result1.success).toBe(false)
|
||||
|
||||
const result2 = updateBlockName('block2', ' ')
|
||||
expect(result2.success).toBe(true)
|
||||
expect(result2.success).toBe(false)
|
||||
|
||||
const state = useWorkflowStore.getState()
|
||||
expect(state.blocks.block1.name).toBe('')
|
||||
expect(state.blocks.block2.name).toBe(' ')
|
||||
expect(state.blocks.block1.name).toBe('column ad')
|
||||
expect(state.blocks.block2.name).toBe('Employee Length')
|
||||
})
|
||||
|
||||
it.concurrent('should return false when trying to rename a non-existent block', () => {
|
||||
|
||||
@@ -671,23 +671,21 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
const oldBlock = get().blocks[id]
|
||||
if (!oldBlock) return { success: false, changedSubblocks: [] }
|
||||
|
||||
// Check for normalized name collisions
|
||||
const normalizedNewName = normalizeName(name)
|
||||
const currentBlocks = get().blocks
|
||||
|
||||
// Find any other block with the same normalized name
|
||||
const conflictingBlock = Object.entries(currentBlocks).find(([blockId, block]) => {
|
||||
return (
|
||||
blockId !== id && // Different block
|
||||
block.name && // Has a name
|
||||
normalizeName(block.name) === normalizedNewName // Same normalized name
|
||||
)
|
||||
})
|
||||
if (!normalizedNewName) {
|
||||
logger.error(`Cannot rename block to empty name`)
|
||||
return { success: false, changedSubblocks: [] }
|
||||
}
|
||||
|
||||
const currentBlocks = get().blocks
|
||||
const conflictingBlock = Object.entries(currentBlocks).find(
|
||||
([blockId, block]) => blockId !== id && normalizeName(block.name) === normalizedNewName
|
||||
)
|
||||
|
||||
if (conflictingBlock) {
|
||||
// Don't allow the rename - another block already uses this normalized name
|
||||
logger.error(
|
||||
`Cannot rename block to "${name}" - another block "${conflictingBlock[1].name}" already uses the normalized name "${normalizedNewName}"`
|
||||
`Cannot rename block to "${name}" - conflicts with "${conflictingBlock[1].name}"`
|
||||
)
|
||||
return { success: false, changedSubblocks: [] }
|
||||
}
|
||||
@@ -723,8 +721,8 @@ export const useWorkflowStore = create<WorkflowStore>()(
|
||||
|
||||
// Loop through subblocks and update references
|
||||
Object.entries(blockValues).forEach(([subBlockId, value]) => {
|
||||
const oldBlockName = oldBlock.name.replace(/\s+/g, '').toLowerCase()
|
||||
const newBlockName = name.replace(/\s+/g, '').toLowerCase()
|
||||
const oldBlockName = normalizeName(oldBlock.name)
|
||||
const newBlockName = normalizeName(name)
|
||||
const regex = new RegExp(`<${oldBlockName}\\.`, 'g')
|
||||
|
||||
// Use a recursive function to handle all object types
|
||||
|
||||
@@ -2,6 +2,7 @@ import { load as yamlParse } from 'js-yaml'
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { getBlock } from '@/blocks'
|
||||
import { normalizeName } from '@/executor/constants'
|
||||
import {
|
||||
type ConnectionsFormat,
|
||||
expandConditionInputs,
|
||||
@@ -172,6 +173,34 @@ function validateBlockTypes(yamlWorkflow: YamlWorkflow): { errors: string[]; war
|
||||
return { errors, warnings }
|
||||
}
|
||||
|
||||
/**
|
||||
* Validates block names are non-empty and unique (by normalized name).
|
||||
*/
|
||||
function validateBlockNames(blocks: Record<string, YamlBlock>): string[] {
|
||||
const errors: string[] = []
|
||||
const seen = new Map<string, string>()
|
||||
|
||||
for (const [blockId, block] of Object.entries(blocks)) {
|
||||
const normalized = normalizeName(block.name)
|
||||
|
||||
if (!normalized) {
|
||||
errors.push(`Block "${blockId}" has empty name`)
|
||||
continue
|
||||
}
|
||||
|
||||
const existingBlockId = seen.get(normalized)
|
||||
if (existingBlockId) {
|
||||
errors.push(
|
||||
`Block "${blockId}" has same name as "${existingBlockId}" (normalized: "${normalized}")`
|
||||
)
|
||||
} else {
|
||||
seen.set(normalized, blockId)
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate positions for blocks based on their connections
|
||||
* Uses a simple layered approach similar to the auto-layout algorithm
|
||||
@@ -334,18 +363,19 @@ export function convertYamlToWorkflow(yamlWorkflow: YamlWorkflow): ImportResult
|
||||
errors.push(...typeErrors)
|
||||
warnings.push(...typeWarnings)
|
||||
|
||||
// Validate block names (non-empty and unique)
|
||||
const nameErrors = validateBlockNames(yamlWorkflow.blocks)
|
||||
errors.push(...nameErrors)
|
||||
|
||||
if (errors.length > 0) {
|
||||
return { blocks: [], edges: [], errors, warnings }
|
||||
}
|
||||
|
||||
// Calculate positions
|
||||
const positions = calculateBlockPositions(yamlWorkflow)
|
||||
|
||||
// Convert blocks
|
||||
Object.entries(yamlWorkflow.blocks).forEach(([blockId, yamlBlock]) => {
|
||||
const position = positions[blockId] || { x: 100, y: 100 }
|
||||
|
||||
// Expand condition inputs from clean format to internal format
|
||||
const processedInputs =
|
||||
yamlBlock.type === 'condition'
|
||||
? expandConditionInputs(blockId, yamlBlock.inputs || {})
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import { v4 as uuidv4 } from 'uuid'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { EDGE } from '@/executor/constants'
|
||||
|
||||
const logger = createLogger('YamlParsingUtils')
|
||||
|
||||
@@ -110,7 +111,7 @@ export function generateBlockConnections(
|
||||
successTargets.push(edge.target)
|
||||
} else if (handle === 'error') {
|
||||
errorTargets.push(edge.target)
|
||||
} else if (handle.startsWith('condition-')) {
|
||||
} else if (handle.startsWith(EDGE.CONDITION_PREFIX)) {
|
||||
const rawConditionId = extractConditionId(handle)
|
||||
rawConditionIds.push(rawConditionId)
|
||||
|
||||
@@ -671,22 +672,22 @@ function createConditionHandle(blockId: string, conditionId: string, blockType?:
|
||||
if (blockType === 'condition') {
|
||||
// Map semantic condition IDs to the internal format the system expects
|
||||
const actualConditionId = `${blockId}-${conditionId}`
|
||||
return `condition-${actualConditionId}`
|
||||
return `${EDGE.CONDITION_PREFIX}${actualConditionId}`
|
||||
}
|
||||
// For other blocks that might have conditions, use a more explicit format
|
||||
return `condition-${blockId}-${conditionId}`
|
||||
return `${EDGE.CONDITION_PREFIX}${blockId}-${conditionId}`
|
||||
}
|
||||
|
||||
function extractConditionId(sourceHandle: string): string {
|
||||
// Extract condition ID from handle like "condition-blockId-semantic-key"
|
||||
// Example: "condition-e23e6318-bcdc-4572-a76b-5015e3950121-else-if-1752111795510"
|
||||
|
||||
if (!sourceHandle.startsWith('condition-')) {
|
||||
if (!sourceHandle.startsWith(EDGE.CONDITION_PREFIX)) {
|
||||
return sourceHandle
|
||||
}
|
||||
|
||||
// Remove "condition-" prefix
|
||||
const withoutPrefix = sourceHandle.substring('condition-'.length)
|
||||
// Remove condition prefix
|
||||
const withoutPrefix = sourceHandle.substring(EDGE.CONDITION_PREFIX.length)
|
||||
|
||||
// Special case: check if this ends with "-else" (the auto-added else condition)
|
||||
if (withoutPrefix.endsWith('-else')) {
|
||||
|
||||
@@ -3,6 +3,7 @@ import { generateRequestId } from '@/lib/core/utils/request'
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { parseMcpToolId } from '@/lib/mcp/utils'
|
||||
import { isCustomTool, isMcpTool } from '@/executor/constants'
|
||||
import type { ExecutionContext } from '@/executor/types'
|
||||
import type { ErrorInfo } from '@/tools/error-extractors'
|
||||
import { extractErrorMessage } from '@/tools/error-extractors'
|
||||
@@ -210,13 +211,13 @@ export async function executeTool(
|
||||
const normalizedToolId = normalizeToolId(toolId)
|
||||
|
||||
// If it's a custom tool, use the async version with workflowId
|
||||
if (normalizedToolId.startsWith('custom_')) {
|
||||
if (isCustomTool(normalizedToolId)) {
|
||||
const workflowId = params._context?.workflowId
|
||||
tool = await getToolAsync(normalizedToolId, workflowId)
|
||||
if (!tool) {
|
||||
logger.error(`[${requestId}] Custom tool not found: ${normalizedToolId}`)
|
||||
}
|
||||
} else if (normalizedToolId.startsWith('mcp-')) {
|
||||
} else if (isMcpTool(normalizedToolId)) {
|
||||
return await executeMcpTool(
|
||||
normalizedToolId,
|
||||
params,
|
||||
@@ -615,7 +616,7 @@ async function handleInternalRequest(
|
||||
|
||||
const fullUrl = fullUrlObj.toString()
|
||||
|
||||
if (toolId.startsWith('custom_') && tool.request.body) {
|
||||
if (isCustomTool(toolId) && tool.request.body) {
|
||||
const requestBody = tool.request.body(params)
|
||||
if (
|
||||
typeof requestBody === 'object' &&
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RedditCommentsParams, RedditCommentsResponse } from '@/tools/reddit/types'
|
||||
import { normalizeSubreddit } from '@/tools/reddit/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getCommentsTool: ToolConfig<RedditCommentsParams, RedditCommentsResponse> = {
|
||||
@@ -108,8 +109,7 @@ export const getCommentsTool: ToolConfig<RedditCommentsParams, RedditCommentsRes
|
||||
|
||||
request: {
|
||||
url: (params: RedditCommentsParams) => {
|
||||
// Sanitize inputs
|
||||
const subreddit = params.subreddit.trim().replace(/^r\//, '')
|
||||
const subreddit = normalizeSubreddit(params.subreddit)
|
||||
const sort = params.sort || 'confidence'
|
||||
const limit = Math.min(Math.max(1, params.limit || 50), 100)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RedditControversialParams, RedditPostsResponse } from '@/tools/reddit/types'
|
||||
import { normalizeSubreddit } from '@/tools/reddit/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getControversialTool: ToolConfig<RedditControversialParams, RedditPostsResponse> = {
|
||||
@@ -72,8 +73,7 @@ export const getControversialTool: ToolConfig<RedditControversialParams, RedditP
|
||||
|
||||
request: {
|
||||
url: (params: RedditControversialParams) => {
|
||||
// Sanitize inputs
|
||||
const subreddit = params.subreddit.trim().replace(/^r\//, '')
|
||||
const subreddit = normalizeSubreddit(params.subreddit)
|
||||
const limit = Math.min(Math.max(1, params.limit || 10), 100)
|
||||
|
||||
// Build URL with appropriate parameters using OAuth endpoint
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RedditPostsParams, RedditPostsResponse } from '@/tools/reddit/types'
|
||||
import { normalizeSubreddit } from '@/tools/reddit/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const getPostsTool: ToolConfig<RedditPostsParams, RedditPostsResponse> = {
|
||||
@@ -78,8 +79,7 @@ export const getPostsTool: ToolConfig<RedditPostsParams, RedditPostsResponse> =
|
||||
|
||||
request: {
|
||||
url: (params: RedditPostsParams) => {
|
||||
// Sanitize inputs
|
||||
const subreddit = params.subreddit.trim().replace(/^r\//, '')
|
||||
const subreddit = normalizeSubreddit(params.subreddit)
|
||||
const sort = params.sort || 'hot'
|
||||
const limit = Math.min(Math.max(1, params.limit || 10), 100)
|
||||
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RedditHotPostsResponse, RedditPost } from '@/tools/reddit/types'
|
||||
import { normalizeSubreddit } from '@/tools/reddit/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
interface HotPostsParams {
|
||||
@@ -41,8 +42,7 @@ export const hotPostsTool: ToolConfig<HotPostsParams, RedditHotPostsResponse> =
|
||||
|
||||
request: {
|
||||
url: (params) => {
|
||||
// Sanitize inputs and enforce limits
|
||||
const subreddit = params.subreddit.trim().replace(/^r\//, '')
|
||||
const subreddit = normalizeSubreddit(params.subreddit)
|
||||
const limit = Math.min(Math.max(1, params.limit || 10), 100)
|
||||
|
||||
return `https://oauth.reddit.com/r/${subreddit}/hot?limit=${limit}&raw_json=1`
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RedditPostsResponse, RedditSearchParams } from '@/tools/reddit/types'
|
||||
import { normalizeSubreddit } from '@/tools/reddit/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const searchTool: ToolConfig<RedditSearchParams, RedditPostsResponse> = {
|
||||
@@ -85,8 +86,7 @@ export const searchTool: ToolConfig<RedditSearchParams, RedditPostsResponse> = {
|
||||
|
||||
request: {
|
||||
url: (params: RedditSearchParams) => {
|
||||
// Sanitize inputs
|
||||
const subreddit = params.subreddit.trim().replace(/^r\//, '')
|
||||
const subreddit = normalizeSubreddit(params.subreddit)
|
||||
const sort = params.sort || 'relevance'
|
||||
const limit = Math.min(Math.max(1, params.limit || 10), 100)
|
||||
const restrict_sr = params.restrict_sr !== false // Default to true
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RedditSubmitParams, RedditWriteResponse } from '@/tools/reddit/types'
|
||||
import { normalizeSubreddit } from '@/tools/reddit/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const submitPostTool: ToolConfig<RedditSubmitParams, RedditWriteResponse> = {
|
||||
@@ -78,8 +79,7 @@ export const submitPostTool: ToolConfig<RedditSubmitParams, RedditWriteResponse>
|
||||
}
|
||||
},
|
||||
body: (params: RedditSubmitParams) => {
|
||||
// Sanitize subreddit
|
||||
const subreddit = params.subreddit.trim().replace(/^r\//, '')
|
||||
const subreddit = normalizeSubreddit(params.subreddit)
|
||||
|
||||
// Build form data
|
||||
const formData = new URLSearchParams({
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
import type { RedditSubscribeParams, RedditWriteResponse } from '@/tools/reddit/types'
|
||||
import { normalizeSubreddit } from '@/tools/reddit/utils'
|
||||
import type { ToolConfig } from '@/tools/types'
|
||||
|
||||
export const subscribeTool: ToolConfig<RedditSubscribeParams, RedditWriteResponse> = {
|
||||
@@ -53,8 +54,7 @@ export const subscribeTool: ToolConfig<RedditSubscribeParams, RedditWriteRespons
|
||||
throw new Error('action must be "sub" or "unsub"')
|
||||
}
|
||||
|
||||
// Sanitize subreddit
|
||||
const subreddit = params.subreddit.trim().replace(/^r\//, '')
|
||||
const subreddit = normalizeSubreddit(params.subreddit)
|
||||
|
||||
const formData = new URLSearchParams({
|
||||
action: params.action,
|
||||
|
||||
10
apps/sim/tools/reddit/utils.ts
Normal file
10
apps/sim/tools/reddit/utils.ts
Normal file
@@ -0,0 +1,10 @@
|
||||
const SUBREDDIT_PREFIX = /^r\//
|
||||
|
||||
/**
|
||||
* Normalizes a subreddit name by removing the 'r/' prefix if present and trimming whitespace.
|
||||
* @param subreddit - The subreddit name to normalize
|
||||
* @returns The normalized subreddit name without the 'r/' prefix
|
||||
*/
|
||||
export function normalizeSubreddit(subreddit: string): string {
|
||||
return subreddit.trim().replace(SUBREDDIT_PREFIX, '')
|
||||
}
|
||||
@@ -1,5 +1,6 @@
|
||||
import { getBaseUrl } from '@/lib/core/utils/urls'
|
||||
import { createLogger } from '@/lib/logs/console/logger'
|
||||
import { AGENT, isCustomTool } from '@/executor/constants'
|
||||
import { useCustomToolsStore } from '@/stores/custom-tools/store'
|
||||
import { useEnvironmentStore } from '@/stores/settings/environment/store'
|
||||
import { tools } from '@/tools/registry'
|
||||
@@ -286,10 +287,10 @@ export function getTool(toolId: string): ToolConfig | undefined {
|
||||
if (builtInTool) return builtInTool
|
||||
|
||||
// Check if it's a custom tool
|
||||
if (toolId.startsWith('custom_') && typeof window !== 'undefined') {
|
||||
if (isCustomTool(toolId) && typeof window !== 'undefined') {
|
||||
// Only try to use the sync version on the client
|
||||
const customToolsStore = useCustomToolsStore.getState()
|
||||
const identifier = toolId.replace('custom_', '')
|
||||
const identifier = toolId.slice(AGENT.CUSTOM_TOOL_PREFIX.length)
|
||||
|
||||
// Try to find the tool directly by ID first
|
||||
let customTool = customToolsStore.getTool(identifier)
|
||||
@@ -319,7 +320,7 @@ export async function getToolAsync(
|
||||
if (builtInTool) return builtInTool
|
||||
|
||||
// Check if it's a custom tool
|
||||
if (toolId.startsWith('custom_')) {
|
||||
if (isCustomTool(toolId)) {
|
||||
return getCustomTool(toolId, workflowId)
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user