mirror of
https://github.com/simstudioai/sim.git
synced 2026-09-24 15:45:35 +08:00
feat(error-handling): created error path and upgraded ports
This commit is contained in:
+3
-3
@@ -84,7 +84,7 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP
|
||||
)
|
||||
|
||||
return (
|
||||
<div className="absolute -left-[240px] top-0 space-y-2 flex flex-col items-end w-[220px] max-h-[400px] overflow-y-auto">
|
||||
<div className="absolute -left-[162px] top-0 space-y-2 flex flex-col items-end max-h-[400px] overflow-y-auto">
|
||||
{sortedConnections.map((connection) => (
|
||||
<div key={connection.id} className="space-y-2">
|
||||
{Array.isArray(connection.outputType) ? (
|
||||
@@ -103,7 +103,7 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, connection, field)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-fit"
|
||||
className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-max"
|
||||
>
|
||||
<div className="text-sm">
|
||||
<span className="font-medium leading-none">
|
||||
@@ -119,7 +119,7 @@ export function ConnectionBlocks({ blockId, setIsConnecting }: ConnectionBlocksP
|
||||
draggable
|
||||
onDragStart={(e) => handleDragStart(e, connection)}
|
||||
onDragEnd={handleDragEnd}
|
||||
className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-fit"
|
||||
className="group flex items-center rounded-lg border bg-card p-2 shadow-sm transition-colors hover:bg-accent/50 cursor-grab active:cursor-grabbing w-max"
|
||||
>
|
||||
<div className="text-sm">
|
||||
<span className="font-medium leading-none">
|
||||
|
||||
@@ -224,13 +224,20 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
position={horizontalHandles ? Position.Left : Position.Top}
|
||||
id="target"
|
||||
className={cn(
|
||||
'!w-3.5 !h-3.5',
|
||||
'!bg-white !rounded-full !border !border-gray-200',
|
||||
'group-hover:!border-blue-500',
|
||||
'!w-[7px] !h-5',
|
||||
'!bg-slate-300 dark:!bg-slate-500 !rounded-[2px] !border-none',
|
||||
'!z-[-1]',
|
||||
'group-hover:!shadow-[0_0_0_3px_rgba(156,163,175,0.15)]',
|
||||
horizontalHandles
|
||||
? 'hover:!w-[10px] hover:!left-[-10px] hover:!rounded-l-full hover:!rounded-r-none'
|
||||
: 'hover:!h-[10px] hover:!translate-y-[-3px] hover:!rounded-t-full hover:!rounded-b-none',
|
||||
'!cursor-crosshair',
|
||||
'transition-[border-color] duration-150',
|
||||
'transition-all duration-150',
|
||||
horizontalHandles ? '!left-[-7px]' : '!top-[-7px]'
|
||||
)}
|
||||
style={{
|
||||
...(horizontalHandles ? { top: '50%', transform: 'translateY(-50%)' } : {}),
|
||||
}}
|
||||
data-nodeid={id}
|
||||
data-handleid="target"
|
||||
isConnectableStart={false}
|
||||
@@ -362,24 +369,74 @@ export function WorkflowBlock({ id, data }: NodeProps<WorkflowBlockProps>) {
|
||||
|
||||
{/* Output Handle */}
|
||||
{type !== 'condition' && (
|
||||
<Handle
|
||||
type="source"
|
||||
position={horizontalHandles ? Position.Right : Position.Bottom}
|
||||
id="source"
|
||||
className={cn(
|
||||
'!w-3.5 !h-3.5',
|
||||
'!bg-white !rounded-full !border !border-gray-200',
|
||||
'group-hover:!border-blue-500',
|
||||
'!cursor-crosshair',
|
||||
'transition-[border-color] duration-150',
|
||||
horizontalHandles ? '!right-[-7px]' : '!bottom-[-7px]'
|
||||
<>
|
||||
<Handle
|
||||
type="source"
|
||||
position={horizontalHandles ? Position.Right : Position.Bottom}
|
||||
id="source"
|
||||
className={cn(
|
||||
'!w-[7px] !h-5',
|
||||
'!bg-slate-300 dark:!bg-slate-500 !rounded-[2px] !border-none',
|
||||
'!z-[-1]',
|
||||
'group-hover:!shadow-[0_0_0_3px_rgba(156,163,175,0.15)]',
|
||||
horizontalHandles
|
||||
? 'hover:!w-[10px] hover:!right-[-10px] hover:!rounded-r-full hover:!rounded-l-none'
|
||||
: 'hover:!h-[10px] hover:!translate-y-[3px] hover:!rounded-b-full hover:!rounded-t-none',
|
||||
'!cursor-crosshair',
|
||||
'transition-all duration-150',
|
||||
horizontalHandles ? '!right-[-7px]' : '!bottom-[-7px]'
|
||||
)}
|
||||
style={{
|
||||
...(horizontalHandles ? { top: '50%', transform: 'translateY(-50%)' } : {}),
|
||||
}}
|
||||
data-nodeid={id}
|
||||
data-handleid="source"
|
||||
isConnectableStart={true}
|
||||
isConnectableEnd={false}
|
||||
isValidConnection={(connection) => connection.target !== id}
|
||||
/>
|
||||
|
||||
{/* Error Handle - Don't show for starter blocks */}
|
||||
{type !== 'starter' && (
|
||||
<Handle
|
||||
type="source"
|
||||
position={horizontalHandles ? Position.Right : Position.Bottom}
|
||||
id="error"
|
||||
className={cn(
|
||||
'!w-[7px] !h-5',
|
||||
'!bg-red-400 dark:!bg-red-500 !rounded-[2px] !border-none',
|
||||
'!z-[-1]',
|
||||
'group-hover:!shadow-[0_0_0_3px_rgba(248,113,113,0.15)]',
|
||||
horizontalHandles
|
||||
? 'hover:!w-[10px] hover:!right-[-10px] hover:!rounded-r-full hover:!rounded-l-none'
|
||||
: 'hover:!h-[10px] hover:!translate-y-[3px] hover:!rounded-b-full hover:!rounded-t-none',
|
||||
'!cursor-crosshair',
|
||||
'transition-all duration-150'
|
||||
)}
|
||||
style={{
|
||||
position: 'absolute',
|
||||
...(horizontalHandles
|
||||
? {
|
||||
right: '-8px',
|
||||
top: 'auto',
|
||||
bottom: '30px',
|
||||
transform: 'translateY(0)',
|
||||
}
|
||||
: {
|
||||
bottom: '-7px',
|
||||
left: 'auto',
|
||||
right: '30px',
|
||||
transform: 'translateX(0)',
|
||||
}),
|
||||
}}
|
||||
data-nodeid={id}
|
||||
data-handleid="error"
|
||||
isConnectableStart={true}
|
||||
isConnectableEnd={false}
|
||||
isValidConnection={(connection) => connection.target !== id}
|
||||
/>
|
||||
)}
|
||||
data-nodeid={id}
|
||||
data-handleid="source"
|
||||
isConnectableStart={true}
|
||||
isConnectableEnd={false}
|
||||
isValidConnection={(connection) => connection.target !== id}
|
||||
/>
|
||||
</>
|
||||
)}
|
||||
</Card>
|
||||
</div>
|
||||
|
||||
+96
-3
@@ -392,6 +392,10 @@ export class Executor {
|
||||
} else {
|
||||
const allDependenciesMet = incomingConnections.every((conn) => {
|
||||
const sourceExecuted = executedBlocks.has(conn.source)
|
||||
const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source)
|
||||
const sourceBlockState = context.blockStates.get(conn.source)
|
||||
const hasSourceError = sourceBlockState?.output?.error !== undefined ||
|
||||
sourceBlockState?.output?.response?.error !== undefined
|
||||
|
||||
// For condition blocks, check if this is the selected path
|
||||
if (conn.sourceHandle?.startsWith('condition-')) {
|
||||
@@ -411,7 +415,6 @@ export class Executor {
|
||||
}
|
||||
|
||||
// For router blocks, check if this is the selected target
|
||||
const sourceBlock = this.workflow.blocks.find((b) => b.id === conn.source)
|
||||
if (sourceBlock?.metadata?.id === 'router') {
|
||||
const selectedTarget = context.decisions.router.get(conn.source)
|
||||
|
||||
@@ -424,6 +427,16 @@ export class Executor {
|
||||
return sourceExecuted && conn.target === selectedTarget
|
||||
}
|
||||
|
||||
// For error connections, check if the source had an error
|
||||
if (conn.sourceHandle === 'error') {
|
||||
return sourceExecuted && hasSourceError
|
||||
}
|
||||
|
||||
// For regular connections, check if the source was executed without error
|
||||
if (conn.sourceHandle === 'source' || !conn.sourceHandle) {
|
||||
return sourceExecuted && !hasSourceError
|
||||
}
|
||||
|
||||
// If source is not in active path, consider this dependency met
|
||||
// This allows blocks with multiple inputs to execute even if some inputs are from inactive paths
|
||||
if (!context.activeExecutionPath.has(conn.source)) {
|
||||
@@ -556,6 +569,7 @@ export class Executor {
|
||||
blockLog.durationMs =
|
||||
new Date(blockLog.endedAt).getTime() - new Date(blockLog.startedAt).getTime()
|
||||
|
||||
// Log the error even if we'll continue execution through error path
|
||||
context.blockLogs.push(blockLog)
|
||||
addConsole({
|
||||
output: {},
|
||||
@@ -571,6 +585,34 @@ export class Executor {
|
||||
blockType: block.metadata?.id || 'unknown',
|
||||
})
|
||||
|
||||
// Create error output with appropriate structure
|
||||
const errorOutput: NormalizedBlockOutput = {
|
||||
response: {
|
||||
error: this.extractErrorMessage(error),
|
||||
status: error.status || 500,
|
||||
},
|
||||
error: this.extractErrorMessage(error),
|
||||
}
|
||||
|
||||
// Set block state with error output
|
||||
context.blockStates.set(blockId, {
|
||||
output: errorOutput,
|
||||
executed: true,
|
||||
executionTime: blockLog.durationMs,
|
||||
})
|
||||
|
||||
// Check for error connections and follow them if they exist
|
||||
const hasErrorPath = this.activateErrorPath(blockId, context)
|
||||
|
||||
// Console.error the error for visibility
|
||||
logger.error(`Error executing block ${block.metadata?.name || blockId}:`, this.sanitizeError(error))
|
||||
|
||||
// If there are error paths to follow, return error output instead of throwing
|
||||
if (hasErrorPath) {
|
||||
// Return the error output to allow execution to continue along error path
|
||||
return errorOutput
|
||||
}
|
||||
|
||||
// Create a proper error message that is never undefined
|
||||
let errorMessage = error.message
|
||||
|
||||
@@ -590,6 +632,39 @@ export class Executor {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Activates error paths from a block that had an error.
|
||||
* Checks for connections from the block's "error" handle and adds them to the active execution path.
|
||||
*
|
||||
* @param blockId - ID of the block that had an error
|
||||
* @param context - Current execution context
|
||||
* @returns Whether there was an error path to follow
|
||||
*/
|
||||
private activateErrorPath(blockId: string, context: ExecutionContext): boolean {
|
||||
// Skip for starter blocks which don't have error handles
|
||||
const block = this.workflow.blocks.find((b) => b.id === blockId)
|
||||
if (block?.metadata?.id === 'starter' || block?.metadata?.id === 'condition') {
|
||||
return false
|
||||
}
|
||||
|
||||
// Look for connections from this block's error handle
|
||||
const errorConnections = this.workflow.connections.filter(
|
||||
(conn) => conn.source === blockId && conn.sourceHandle === 'error'
|
||||
)
|
||||
|
||||
if (errorConnections.length === 0) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Add all error connection targets to the active execution path
|
||||
for (const conn of errorConnections) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
logger.info(`Activated error path from ${blockId} to ${conn.target}`)
|
||||
}
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalizes a block output to ensure it has the expected structure.
|
||||
* Handles different block types with appropriate response formats.
|
||||
@@ -599,8 +674,26 @@ export class Executor {
|
||||
* @returns Normalized output with consistent structure
|
||||
*/
|
||||
private normalizeBlockOutput(output: any, block: SerializedBlock): NormalizedBlockOutput {
|
||||
// Handle error outputs
|
||||
if (output && typeof output === 'object' && output.error) {
|
||||
return {
|
||||
response: {
|
||||
error: output.error,
|
||||
status: output.status || 500,
|
||||
},
|
||||
error: output.error,
|
||||
};
|
||||
}
|
||||
|
||||
if (output && typeof output === 'object' && 'response' in output) {
|
||||
return output as NormalizedBlockOutput
|
||||
// If response already contains an error, maintain it
|
||||
if (output.response && output.response.error) {
|
||||
return {
|
||||
...output,
|
||||
error: output.response.error
|
||||
};
|
||||
}
|
||||
return output as NormalizedBlockOutput;
|
||||
}
|
||||
|
||||
const blockType = block.metadata?.id
|
||||
@@ -830,4 +923,4 @@ export class Executor {
|
||||
|
||||
return { message: `Unexpected error type: ${typeof error}` }
|
||||
}
|
||||
}
|
||||
}
|
||||
+21
-2
@@ -96,13 +96,32 @@ export class PathTracker {
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// For regular blocks, activate all outgoing connections
|
||||
// For regular blocks, activate all outgoing connections based on success or error status
|
||||
const blockState = context.blockStates.get(blockId)
|
||||
const hasError = blockState?.output?.error !== undefined || blockState?.output?.response?.error !== undefined
|
||||
|
||||
// Get all outgoing connections
|
||||
const outgoingConnections = this.workflow.connections.filter(
|
||||
(conn) => conn.source === blockId
|
||||
)
|
||||
|
||||
for (const conn of outgoingConnections) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
// For error connections, only activate them on error
|
||||
if (conn.sourceHandle === 'error') {
|
||||
if (hasError) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
}
|
||||
}
|
||||
// For regular (source) connections, only activate them if there's no error
|
||||
else if (conn.sourceHandle === 'source' || !conn.sourceHandle) {
|
||||
if (!hasError) {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
}
|
||||
}
|
||||
// All other types of connections (e.g., from condition blocks) follow their own rules
|
||||
else {
|
||||
context.activeExecutionPath.add(conn.target)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -32,7 +32,9 @@ export interface NormalizedBlockOutput {
|
||||
data?: any // Response data from API calls
|
||||
status?: number // HTTP status code
|
||||
headers?: Record<string, string> // HTTP headers
|
||||
error?: string // Error message if block execution failed
|
||||
}
|
||||
error?: string // Top-level error field for easy error checking
|
||||
[key: string]: any // Additional properties
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user