UNPKG

@lobehub/chat

Version:

Lobe Chat - an open-source, high-performance chatbot framework that supports speech synthesis, multimodal, and extensible Function Call plugin system. Supports one-click free deployment of your private ChatGPT/LLM web application.

539 lines (483 loc) • 18.6 kB
/** * Error Handler Service * * Centralized error handling for ComfyUI runtime * Maps internal errors to framework errors */ import { AgentRuntimeError, AgentRuntimeErrorType, ILobeAgentRuntimeErrorType, } from '@lobechat/model-runtime'; import { TRPCError } from '@trpc/server'; import { SYSTEM_COMPONENTS } from '@/server/services/comfyui/config/systemComponents'; import { ConfigError, ServicesError, UtilsError, WorkflowError, isComfyUIInternalError, } from '@/server/services/comfyui/errors'; import { ModelResolverError } from '@/server/services/comfyui/errors/modelResolverError'; import { getComponentInfo } from '@/server/services/comfyui/utils/componentInfo'; interface ComfyUIError { code?: number | string; details?: any; message: string; missingFileName?: string; missingFileType?: 'model' | 'component'; status?: number; type?: string; userGuidance?: string; } interface ParsedError { error: ComfyUIError; errorType: ILobeAgentRuntimeErrorType; } /** * Generate user guidance message based on missing file info * Server-side version with access to full component information * @param fileName - The missing file name * @param fileType - The type of missing file * @returns User-friendly guidance message */ function generateUserGuidance(fileName: string, fileType: 'model' | 'component'): string { if (fileType === 'component') { const componentInfo = getComponentInfo(fileName); if (componentInfo) { return `Missing ${componentInfo.displayName}: ${fileName}. Please download and place it in the ${componentInfo.folderPath} folder.`; } // Fallback for unknown components return `Missing component file: ${fileName}. Please download and place it in the appropriate ComfyUI models folder.`; } // Main model files return `Missing model file: ${fileName}. Please download and place it in the models/checkpoints folder.`; } /** * Extract missing file information from error message * Server-side version with access to SYSTEM_COMPONENTS * @param message - Error message that may contain file names * @returns Object with extracted file name and type, or null if no file found */ function extractMissingFileInfo(message: string): { fileName: string; fileType: 'model' | 'component'; } | null { if (!message) return null; // Check for "Expected one of:" pattern from enhanced model errors const expectedPattern = /expected one of:\s*([^.]+\.(?:safetensors|ckpt|pt|pth))/i; const expectedMatch = message.match(expectedPattern); if (expectedMatch) { // Extract the first file from the match const fileName = expectedMatch[1].trim().split(',')[0].trim(); if (fileName) { return { fileName, fileType: 'model', }; } } // Common model file extensions - allow dots in filename const modelFilePattern = /([\w.-]+\.(?:safetensors|ckpt|pt|pth))\b/gi; const fileMatch = message.match(modelFilePattern); if (fileMatch) { const fileName = fileMatch[0]; // Use server-side SYSTEM_COMPONENTS to check if it's a system component if (fileName in SYSTEM_COMPONENTS) { return { fileName, fileType: 'component', }; } // If not found in SYSTEM_COMPONENTS, treat as main model return { fileName, fileType: 'model', }; } return null; } /** * Check if the error is a model-related error * @param error - Error object * @param message - Pre-extracted message * @returns Whether it's a model error */ function isModelError(error: any, message?: string): boolean { const errorMessage = message || error?.message || String(error); const lowerMessage = errorMessage.toLowerCase(); // Check for explicit model error patterns const hasModelErrorPattern = lowerMessage.includes('model not found') || lowerMessage.includes('checkpoint not found') || lowerMessage.includes('model file not found') || lowerMessage.includes('ckpt_name') || lowerMessage.includes('no models available') || lowerMessage.includes('safetensors') || lowerMessage.includes('.ckpt') || lowerMessage.includes('.pt') || lowerMessage.includes('.pth') || error?.code === 'MODEL_NOT_FOUND'; // Also check if the error contains a model file that's missing if (!hasModelErrorPattern) { const fileInfo = extractMissingFileInfo(errorMessage); return fileInfo !== null; // Any missing model file is considered a model error } return hasModelErrorPattern; } /** * Check if the error is a ComfyUI SDK custom error * @param error - Error object * @returns Whether it's a SDK custom error */ function isSDKCustomError(error: any): boolean { if (!error) return false; // Check for SDK error class names const errorName = error?.name || error?.constructor?.name || ''; const sdkErrorTypes = [ // Base error class 'CallWrapperError', // Actual SDK error classes from comfyui-sdk 'WentMissingError', 'FailedCacheError', 'EnqueueFailedError', 'DisconnectedError', 'ExecutionFailedError', 'CustomEventError', 'ExecutionInterruptedError', 'MissingNodeError', ]; if (sdkErrorTypes.includes(errorName)) { return true; } // Check for SDK error messages patterns const message = error?.message || String(error); const lowerMessage = message.toLowerCase(); return ( lowerMessage.includes('sdk error:') || lowerMessage.includes('call wrapper') || lowerMessage.includes('execution interrupted') || lowerMessage.includes('missing node type') || lowerMessage.includes('invalid model configuration') || lowerMessage.includes('workflow validation failed') || lowerMessage.includes('sdk timeout') || lowerMessage.includes('sdk configuration error') ); } /** * Check if the error is a network connection error (including WebSocket) * @param error - Error object * @param message - Pre-extracted message * @param code - Pre-extracted code * @returns Whether it's a network connection error */ function isNetworkError(error: any, message?: string, code?: string | number): boolean { const errorMessage = message || error?.message || String(error); const lowerMessage = errorMessage.toLowerCase(); const errorCode = code || error?.code; return ( // Basic network errors errorMessage === 'fetch failed' || lowerMessage.includes('econnrefused') || lowerMessage.includes('enotfound') || lowerMessage.includes('etimedout') || lowerMessage.includes('network error') || lowerMessage.includes('connection refused') || lowerMessage.includes('connection timeout') || errorCode === 'ECONNREFUSED' || errorCode === 'ENOTFOUND' || errorCode === 'ETIMEDOUT' || // WebSocket specific errors lowerMessage.includes('websocket') || lowerMessage.includes('ws connection') || lowerMessage.includes('connection lost to comfyui server') || errorCode === 'WS_CONNECTION_FAILED' || errorCode === 'WS_TIMEOUT' || errorCode === 'WS_HANDSHAKE_FAILED' ); } /** * Check if the error is a ComfyUI workflow error * @param error - Error object * @param message - Pre-extracted message * @returns Whether it's a workflow error */ function isWorkflowError(error: any, message?: string): boolean { const errorMessage = message || error?.message || String(error); const lowerMessage = errorMessage.toLowerCase(); // Check for structured workflow error fields if ( error && typeof error === 'object' && (error.node_id || error.nodeId || error.node_type || error.nodeType) ) { return true; } return ( lowerMessage.includes('node') || lowerMessage.includes('workflow') || lowerMessage.includes('execution') || lowerMessage.includes('prompt') || lowerMessage.includes('queue') || lowerMessage.includes('invalid input') || lowerMessage.includes('missing required') || lowerMessage.includes('node execution failed') || lowerMessage.includes('workflow validation') || error?.type === 'workflow_error' ); } /** * Simple ComfyUI error parser * Extracts error information and determines error type */ function parseComfyUIErrorMessage(error: any): ParsedError { // Default error info let message = 'Unknown error'; let status: number | undefined; let code: string | undefined; let missingFileName: string | undefined; let missingFileType: 'model' | 'component' | undefined; let userGuidance: string | undefined; let errorType: ILobeAgentRuntimeErrorType = AgentRuntimeErrorType.ComfyUIBizError; // Check for JSON parsing errors (indicates non-ComfyUI service) if ( error instanceof SyntaxError || (error && typeof error === 'object' && error.name === 'SyntaxError') ) { const syntaxMessage = error?.message || String(error); if (syntaxMessage.includes('JSON') || syntaxMessage.includes('Unexpected token')) { // JSON parsing failed - service is not ComfyUI return { error: { message: 'Service is not ComfyUI - received non-JSON response', type: 'SyntaxError', userGuidance: 'The service at this URL is not a ComfyUI server. Please check your baseURL configuration.', }, errorType: AgentRuntimeErrorType.InvalidProviderAPIKey, // Trigger auth dialog }; } } // Extract message if (typeof error === 'string') { message = error; } else if (error instanceof Error) { message = error.message; code = (error as any).code; } else if (error && typeof error === 'object') { // Extract message from various possible sources (matching original logic) const possibleMessage = [ error.exception_message, // ComfyUI specific field (highest priority) error.error?.exception_message, // Nested ComfyUI exception message error.error?.error, // Deeply nested error.error.error path error.message, error.error?.message, error.data?.message, error.body?.message, error.response?.data?.message, error.response?.data?.error?.message, error.response?.text, error.response?.body, error.statusText, ].find(Boolean); // Use the message or fallback to a generic error if (!possibleMessage) { message = 'Unknown error occurred'; } else { message = possibleMessage; } // Extract status code from various possible locations const possibleStatus = [ error.status, error.statusCode, error.details?.status, // ServicesError puts status in details error.response?.status, error.response?.statusCode, error.error?.status, error.error?.statusCode, ].find(Number.isInteger); status = possibleStatus; code = error.code || error.error?.code || error.response?.data?.code; } // Extract missing file information and generate guidance const fileInfo = extractMissingFileInfo(message); if (fileInfo) { missingFileName = fileInfo.fileName; missingFileType = fileInfo.fileType; userGuidance = generateUserGuidance(fileInfo.fileName, fileInfo.fileType); } // Determine error type based on status code if (status) { switch (status) { case 400: case 401: case 404: { errorType = AgentRuntimeErrorType.InvalidProviderAPIKey; break; } case 403: { errorType = AgentRuntimeErrorType.PermissionDenied; break; } default: { if (status >= 500) { errorType = AgentRuntimeErrorType.ComfyUIServiceUnavailable; } } } } // Check for more specific error types only if it's still a generic ComfyUIBizError if (errorType === AgentRuntimeErrorType.ComfyUIBizError) { if (isSDKCustomError(error)) { // SDK errors remain as ComfyUIBizError errorType = AgentRuntimeErrorType.ComfyUIBizError; } else if (isNetworkError(error, message, code)) { errorType = AgentRuntimeErrorType.ComfyUIServiceUnavailable; } else if (isWorkflowError(error, message)) { errorType = AgentRuntimeErrorType.ComfyUIWorkflowError; } else if (isModelError(error, message)) { errorType = AgentRuntimeErrorType.ModelNotFound; } } const result = { error: { code, message, missingFileName, missingFileType, status, type: error?.name || error?.type, userGuidance, }, errorType, }; return result; } /** * Error Handler Service * Provides unified error handling and transformation */ export class ErrorHandlerService { /** * Handle and transform any error into framework error * Enhanced to preserve more debugging information while maintaining compatibility * @param error - The error to handle * @throws {TRPCError} Always throws a properly formatted error with cause */ handleError(error: unknown): never { // 1. If already a framework error, wrap in TRPCError if (error && typeof error === 'object' && 'errorType' in error) { throw new TRPCError({ cause: error, code: 'INTERNAL_SERVER_ERROR', message: 'ComfyUI service error', }); } // 2. Handle ComfyUI internal errors - enhance information preservation if (isComfyUIInternalError(error)) { const errorType = this.mapInternalErrorToRuntimeError(error); // Enhanced: preserve more context information const enhancedError = { details: error.details || {}, message: error.message, // Preserve original error type and reason originalErrorType: error.constructor.name, originalReason: error.reason, // Note: Removed originalError to avoid serialization issues }; const agentError = AgentRuntimeError.createImage({ error: enhancedError, errorType: errorType as ILobeAgentRuntimeErrorType, provider: 'comfyui', }); throw new TRPCError({ cause: agentError, code: 'INTERNAL_SERVER_ERROR', message: error.message, }); } // 3. Parse other errors - use enhanced parser with more information const { error: parsedError, errorType } = parseComfyUIErrorMessage(error); // Enhanced: add more context const enhancedParsedError = { ...parsedError, // Add timestamp for debugging timestamp: new Date().toISOString(), // Note: Removed originalError to avoid serialization issues }; const agentError = AgentRuntimeError.createImage({ error: enhancedParsedError, errorType, provider: 'comfyui', }); throw new TRPCError({ cause: agentError, code: 'INTERNAL_SERVER_ERROR', message: parsedError.message || 'ComfyUI service error', }); } /** * Map internal ComfyUI errors to runtime error types */ private mapInternalErrorToRuntimeError( error: ConfigError | WorkflowError | UtilsError | ServicesError | ModelResolverError, ): string { if (error instanceof ConfigError) { const mapping: Record<string, string> = { [ConfigError.Reasons.INVALID_CONFIG]: AgentRuntimeErrorType.ComfyUIBizError, [ConfigError.Reasons.MISSING_CONFIG]: AgentRuntimeErrorType.ComfyUIBizError, [ConfigError.Reasons.CONFIG_PARSE_ERROR]: AgentRuntimeErrorType.ComfyUIBizError, [ConfigError.Reasons.REGISTRY_ERROR]: AgentRuntimeErrorType.ComfyUIBizError, }; return mapping[error.reason] || AgentRuntimeErrorType.ComfyUIBizError; } if (error instanceof WorkflowError) { const mapping: Record<string, string> = { [WorkflowError.Reasons.INVALID_CONFIG]: AgentRuntimeErrorType.ComfyUIWorkflowError, [WorkflowError.Reasons.MISSING_COMPONENT]: AgentRuntimeErrorType.ComfyUIModelError, [WorkflowError.Reasons.MISSING_ENCODER]: AgentRuntimeErrorType.ComfyUIModelError, [WorkflowError.Reasons.UNSUPPORTED_MODEL]: AgentRuntimeErrorType.ModelNotFound, [WorkflowError.Reasons.INVALID_PARAMS]: AgentRuntimeErrorType.ComfyUIWorkflowError, }; return mapping[error.reason] || AgentRuntimeErrorType.ComfyUIWorkflowError; } if (error instanceof ServicesError) { // If error already has parsed errorType in details, use it directly if (error.details?.errorType) { return error.details.errorType; } // Otherwise use mapping table const mapping: Record<string, string> = { [ServicesError.Reasons.INVALID_ARGS]: AgentRuntimeErrorType.InvalidComfyUIArgs, [ServicesError.Reasons.INVALID_AUTH]: AgentRuntimeErrorType.InvalidProviderAPIKey, [ServicesError.Reasons.INVALID_CONFIG]: AgentRuntimeErrorType.InvalidComfyUIArgs, [ServicesError.Reasons.CONNECTION_FAILED]: AgentRuntimeErrorType.InvalidProviderAPIKey, // Trigger auth dialog for connection issues [ServicesError.Reasons.UPLOAD_FAILED]: AgentRuntimeErrorType.ComfyUIBizError, [ServicesError.Reasons.EXECUTION_FAILED]: AgentRuntimeErrorType.ComfyUIWorkflowError, [ServicesError.Reasons.MODEL_NOT_FOUND]: AgentRuntimeErrorType.ModelNotFound, [ServicesError.Reasons.EMPTY_RESULT]: AgentRuntimeErrorType.ComfyUIBizError, [ServicesError.Reasons.IMAGE_FETCH_FAILED]: AgentRuntimeErrorType.ComfyUIBizError, [ServicesError.Reasons.IMAGE_TOO_LARGE]: AgentRuntimeErrorType.ComfyUIBizError, [ServicesError.Reasons.UNSUPPORTED_PROTOCOL]: AgentRuntimeErrorType.ComfyUIBizError, [ServicesError.Reasons.MODEL_VALIDATION_FAILED]: AgentRuntimeErrorType.ModelNotFound, [ServicesError.Reasons.WORKFLOW_BUILD_FAILED]: AgentRuntimeErrorType.ComfyUIWorkflowError, }; return mapping[error.reason] || AgentRuntimeErrorType.ComfyUIBizError; } if (error instanceof UtilsError || error instanceof ModelResolverError) { const mapping: Record<string, string> = { CONNECTION_ERROR: AgentRuntimeErrorType.ComfyUIServiceUnavailable, DETECTION_FAILED: AgentRuntimeErrorType.ComfyUIBizError, INVALID_API_KEY: AgentRuntimeErrorType.InvalidProviderAPIKey, INVALID_MODEL_FORMAT: AgentRuntimeErrorType.ComfyUIBizError, MODEL_NOT_FOUND: AgentRuntimeErrorType.ModelNotFound, NO_BUILDER_FOUND: AgentRuntimeErrorType.ComfyUIWorkflowError, PERMISSION_DENIED: AgentRuntimeErrorType.PermissionDenied, ROUTING_FAILED: AgentRuntimeErrorType.ComfyUIWorkflowError, SERVICE_UNAVAILABLE: AgentRuntimeErrorType.ComfyUIServiceUnavailable, }; return mapping[error.reason] || AgentRuntimeErrorType.ComfyUIBizError; } return AgentRuntimeErrorType.ComfyUIBizError; } }