/** * 消息验证器 - 提供全面的消息验证功能 */ import { MessageType, ClientType, Priority, MessageStatus, OperationType, CommandType, ProgressPhase, ReportLevel, ProgramType, ProgramDirection, BaseMessage, RegisterMessage, CommandMessage, ProgramMessage, ProgressUpdateMessage, HeartbeatMessage, ErrorMessage, isRegisterMessage, isCommandMessage, isProgramMessage, isProgressUpdateMessage, isHeartbeatMessage, isErrorMessage } from './index'; // 验证结果接口 export interface ValidationResult { valid: boolean; errors: string[]; warnings?: string[]; } // 验证模式定义 interface ValidationSchema { required: string[]; optional?: string[]; types: Record; validators?: Record boolean>; } /** * 消息验证器类 */ export class MessageValidator { /** * 通用验证方法,返回详细的验证结果 */ static validate(message: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; // 基本结构验证 if (!message || typeof message !== 'object') { errors.push('Message must be an object'); return { valid: false, errors }; } if (!message.type) { errors.push('Message type is required'); } else if (!this.isValidMessageType(message.type)) { errors.push(`Invalid message type: ${message.type}`); } // 根据消息类型进行特定验证 switch (message.type) { case MessageType.REGISTER: return this.validateRegisterMessage(message); case MessageType.COMMAND: return this.validateCommandMessage(message); case MessageType.PROGRAM: return this.validateProgramMessage(message); case MessageType.PROGRESS_UPDATE: return this.validateProgressUpdate(message); case MessageType.HEARTBEAT: return this.validateHeartbeatMessage(message); case MessageType.ERROR: return this.validateErrorMessage(message); default: // 其他消息类型的基本验证 if (!message.timestamp) { warnings.push('Timestamp is missing'); } if (!message.version) { warnings.push('Version is missing'); } } return { valid: errors.length === 0, errors, warnings: warnings.length > 0 ? warnings : undefined }; } /** * 验证消息基本结构 */ static validateStructure(message: any): boolean { if (!message || typeof message !== 'object') return false; if (!message.type || typeof message.type !== 'string') return false; return true; } /** * 验证命令消息 */ static validateCommandMessage(message: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!this.validateStructure(message)) { errors.push('Invalid message structure'); return { valid: false, errors }; } if (!isCommandMessage(message)) { errors.push('Not a valid command message'); return { valid: false, errors }; } // 必需字段验证 if (!message.requestRef) { errors.push('requestRef is required'); } if (!message.targetClientId) { errors.push('targetClientId is required'); } if (!message.callback) { errors.push('callback is required'); } if (!message.priority) { errors.push('priority is required'); } else if (!this.isValidPriority(message.priority)) { errors.push(`Invalid priority: ${message.priority}`); } if (message.timeout === undefined || message.timeout === null) { errors.push('timeout is required'); } else if (typeof message.timeout !== 'number' || isNaN(message.timeout) || message.timeout <= 0) { errors.push('timeout must be a positive number'); } // 命令字段验证 if (!message.command) { errors.push('command is required'); } else { if (!message.command.commandCode) { errors.push('command.commandCode is required'); } const commandType = message.command.commandType || CommandType.SIMPLE; switch (commandType) { case CommandType.SIMPLE: case CommandType.BATCH: if (!message.command.deviceType) { errors.push('command.deviceType is required for SIMPLE/BATCH commands'); } if (!message.command.operationType) { errors.push('command.operationType is required for SIMPLE/BATCH commands'); } else if (!this.isValidOperationType(message.command.operationType)) { errors.push(`Invalid operationType: ${message.command.operationType}`); } if (message.command.deviceId === undefined) { errors.push('command.deviceId is required for SIMPLE/BATCH commands'); } // 命令类型特定验证 if (commandType === CommandType.SIMPLE) { // SIMPLE 命令的 deviceId 必须是单个设备 if (Array.isArray(message.command.deviceId)) { errors.push('SIMPLE command deviceId must be a single value (number or string), not an array'); } } else if (commandType === CommandType.BATCH) { // BATCH 命令的 deviceId 必须是数组或范围字符串 if (!Array.isArray(message.command.deviceId) && typeof message.command.deviceId !== 'string') { errors.push('BATCH command deviceId must be an array or string (range expression)'); } } break; case CommandType.COMPLEX: // Complex 命令不需要 deviceType, deviceId, operationType break; default: errors.push(`Invalid commandType: ${commandType}`); } } // 时间戳和版本验证 if (!message.timestamp) { warnings.push('timestamp is missing'); } if (!message.version) { warnings.push('version is missing'); } return { valid: errors.length === 0, errors, warnings: warnings.length > 0 ? warnings : undefined }; } /** * 验证程序消息 */ static validateProgramMessage(message: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!this.validateStructure(message)) { errors.push('Invalid message structure'); return { valid: false, errors }; } if (!isProgramMessage(message)) { errors.push('Not a valid program message'); return { valid: false, errors }; } // 必需字段验证 if (!message.requestRef) { errors.push('requestRef is required'); } if (!message.targetClientId) { errors.push('targetClientId is required'); } if (!message.callback) { errors.push('callback is required'); } if (!message.priority) { errors.push('priority is required'); } else if (!this.isValidPriority(message.priority)) { errors.push(`Invalid priority: ${message.priority}`); } if (message.timeout === undefined || message.timeout === null) { errors.push('timeout is required'); } else if (typeof message.timeout !== 'number' || isNaN(message.timeout) || message.timeout <= 0) { errors.push('timeout must be a positive number'); } // 命令和参数验证 if (!message.command) { errors.push('command is required'); } else { if (message.command.commandCode !== 'UPLOAD_PROGRAM') { errors.push('command.commandCode must be UPLOAD_PROGRAM'); } if (!message.command.parameters) { errors.push('command.parameters is required'); } else { const params = message.command.parameters; // 验证程序参数 const requiredParams = [ 'deviceId', 'taskId', 'programId', 'programName', 'programNo', 'programType', 'width', 'height', 'direction', 'downloadUrl', 'checksum', 'hashAlgorithm' ]; for (const param of requiredParams) { if ((params as any)[param] === undefined || (params as any)[param] === null) { errors.push(`command.parameters.${param} is required`); } } // 验证特定字段的值 if (params.programType && !this.isValidProgramType(params.programType)) { errors.push(`Invalid programType: ${params.programType}`); } if (params.direction && !this.isValidProgramDirection(params.direction)) { errors.push(`Invalid direction: ${params.direction}`); } if (params.hashAlgorithm && !['SHA256', 'MD5'].includes(params.hashAlgorithm)) { errors.push('hashAlgorithm must be SHA256 or MD5'); } if (params.programNo !== undefined && params.programNo !== null) { if (typeof params.programNo !== 'number' || isNaN(params.programNo)) { errors.push('programNo must be a number'); } else if (params.programNo < 0 || params.programNo > 9) { errors.push('programNo must be between 0 and 9 (device 0-based)'); } } // 验证数值字段类型(防止 NaN 比较恒为 false 的问题) const numericParams: Array = ['width', 'height', 'fileSize']; for (const field of numericParams) { const value = (params as any)[field]; if (value !== undefined && value !== null) { if (typeof value !== 'number' || isNaN(value)) { errors.push(`command.parameters.${String(field)} must be a number`); } else if (value < 0) { errors.push(`command.parameters.${String(field)} must be non-negative`); } } } } } return { valid: errors.length === 0, errors, warnings: warnings.length > 0 ? warnings : undefined }; } /** * 验证进度更新消息 */ static validateProgressUpdate(message: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!this.validateStructure(message)) { errors.push('Invalid message structure'); return { valid: false, errors }; } if (!isProgressUpdateMessage(message)) { errors.push('Not a valid progress update message'); return { valid: false, errors }; } // 必需字段验证 if (!message.requestRef) { errors.push('requestRef is required'); } if (!message.status) { errors.push('status is required'); } else if (!this.isValidMessageStatus(message.status)) { errors.push(`Invalid status: ${message.status}`); } if (!message.phase) { errors.push('phase is required'); } else if (!this.isValidProgressPhase(message.phase)) { // v1.4.11: phase 强校验 — 必须是 ProgressPhase enum 合法值 errors.push(`Invalid phase: ${message.phase} (must be one of ProgressPhase enum)`); } if (message.progress === undefined || message.progress === null) { errors.push('progress is required'); } else if (typeof message.progress !== 'number' || isNaN(message.progress)) { errors.push('progress must be a number'); } else if (message.progress < 0 || message.progress > 100) { errors.push('progress must be between 0 and 100'); } if (!message.sourceType) { errors.push('sourceType is required'); } else if (!['COMMAND', 'SYSTEM', 'EDGE'].includes(message.sourceType)) { errors.push('sourceType must be COMMAND / SYSTEM / EDGE'); } // 可选字段验证 if (message.report) { if (!message.report.level || !this.isValidReportLevel(message.report.level)) { errors.push('Invalid report.level'); } if (!message.report.message) { errors.push('report.message is required when report is present'); } } return { valid: errors.length === 0, errors, warnings: warnings.length > 0 ? warnings : undefined }; } /** * 验证注册消息 */ static validateRegisterMessage(message: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!this.validateStructure(message)) { errors.push('Invalid message structure'); return { valid: false, errors }; } if (!isRegisterMessage(message)) { errors.push('Not a valid register message'); return { valid: false, errors }; } // 必需字段验证 if (!message.clientId) { errors.push('clientId is required'); } if (!message.clientType) { errors.push('clientType is required'); } else if (!this.isValidClientType(message.clientType)) { errors.push(`Invalid clientType: ${message.clientType}`); } // 可选字段验证 if (message.clientInfo) { if (message.clientInfo.capabilities && !Array.isArray(message.clientInfo.capabilities)) { errors.push('clientInfo.capabilities must be an array'); } } // v1.4.13: DEVICE 客户端必须上报 physicalParams if (message.clientType === ClientType.DEVICE) { const pp = message.clientInfo?.physicalParams; if (!pp) { errors.push('clientInfo.physicalParams is required for DEVICE clientType (v1.4.13)'); } else { if (!Number.isInteger(pp.width) || pp.width <= 0) { errors.push('physicalParams.width must be a positive integer'); } if (!Number.isInteger(pp.height) || pp.height <= 0) { errors.push('physicalParams.height must be a positive integer'); } if (pp.direction !== ProgramDirection.LEFT_TO_RIGHT && pp.direction !== ProgramDirection.RIGHT_TO_LEFT) { errors.push(`physicalParams.direction must be LEFT_TO_RIGHT or RIGHT_TO_LEFT, got: ${pp.direction}`); } if (!Number.isInteger(pp.maxProgramSlots) || pp.maxProgramSlots < 1 || pp.maxProgramSlots > 10) { errors.push('physicalParams.maxProgramSlots must be an integer in [1, 10]'); } } } if (message.edgeInfo) { if (!message.edgeInfo.edgeId) { errors.push('edgeInfo.edgeId is required when edgeInfo is present'); } } return { valid: errors.length === 0, errors, warnings: warnings.length > 0 ? warnings : undefined }; } /** * 验证心跳消息 */ static validateHeartbeatMessage(message: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!this.validateStructure(message)) { errors.push('Invalid message structure'); return { valid: false, errors }; } if (!isHeartbeatMessage(message)) { errors.push('Not a valid heartbeat message'); return { valid: false, errors }; } if (!message.clientId) { errors.push('clientId is required'); } if (message.sequence === undefined || message.sequence === null) { errors.push('sequence is required'); } else if (typeof message.sequence !== 'number' || isNaN(message.sequence)) { errors.push('sequence must be a number'); } if (!message.clientTime) { errors.push('clientTime is required'); } return { valid: errors.length === 0, errors, warnings: warnings.length > 0 ? warnings : undefined }; } /** * 验证错误消息 */ static validateErrorMessage(message: any): ValidationResult { const errors: string[] = []; const warnings: string[] = []; if (!this.validateStructure(message)) { errors.push('Invalid message structure'); return { valid: false, errors }; } if (!isErrorMessage(message)) { errors.push('Not a valid error message'); return { valid: false, errors }; } if (!message.code) { errors.push('code is required'); } if (!message.message) { errors.push('message is required'); } return { valid: errors.length === 0, errors, warnings: warnings.length > 0 ? warnings : undefined }; } // 字段验证辅助方法 /** * 验证消息类型 */ static isValidMessageType(type: string): boolean { return Object.values(MessageType).includes(type as MessageType); } /** * 验证客户端类型 */ static isValidClientType(clientType: string): boolean { return Object.values(ClientType).includes(clientType as ClientType); } /** * 验证优先级 */ static isValidPriority(priority: string): boolean { return Object.values(Priority).includes(priority as Priority); } /** * 验证命令状态 */ static isValidMessageStatus(status: string): boolean { return Object.values(MessageStatus).includes(status as MessageStatus); } /** * 验证操作类型 */ static isValidOperationType(operationType: string): boolean { return Object.values(OperationType).includes(operationType as OperationType); } /** * 验证命令类型 */ static isValidCommandType(commandType: string): boolean { return Object.values(CommandType).includes(commandType as CommandType); } /** * 验证进度阶段 */ static isValidProgressPhase(phase: string): boolean { return Object.values(ProgressPhase).includes(phase as ProgressPhase); } /** * 验证报告级别 */ static isValidReportLevel(level: string): boolean { return Object.values(ReportLevel).includes(level as ReportLevel); } /** * 验证程序类型 */ static isValidProgramType(type: string): boolean { return Object.values(ProgramType).includes(type as ProgramType); } /** * 验证程序方向 */ static isValidProgramDirection(direction: string): boolean { return Object.values(ProgramDirection).includes(direction as ProgramDirection); } /** * 获取验证模式 */ static getValidationSchema(messageType: MessageType): ValidationSchema | null { const schemas: Record = { [MessageType.REGISTER]: { required: ['type', 'clientId', 'clientType', 'timestamp', 'version'], optional: ['clientInfo', 'edgeInfo'], types: { type: 'string', clientId: 'string', clientType: 'string', timestamp: 'string', version: 'string', clientInfo: 'object', edgeInfo: 'object' } }, [MessageType.REGISTER_ACK]: { required: ['type', 'clientId', 'success', 'timestamp', 'version'], optional: ['sessionId', 'error', 'serverInfo'], types: { type: 'string', clientId: 'string', success: 'boolean', sessionId: 'string', error: 'object', serverInfo: 'object', timestamp: 'string', version: 'string' } }, [MessageType.COMMAND]: { required: ['type', 'requestRef', 'targetClientId', 'command', 'priority', 'timeout', 'callback', 'timestamp', 'version'], optional: ['retryCount', 'metadata'], types: { type: 'string', requestRef: 'string', targetClientId: 'string', command: 'object', priority: 'string', timeout: 'number', callback: 'string', retryCount: 'number', metadata: 'object', timestamp: 'string', version: 'string' } }, [MessageType.COMMAND_RESPONSE]: { required: ['type', 'requestRef', 'status', 'timestamp', 'version'], optional: ['result', 'report', 'executionTime'], types: { type: 'string', requestRef: 'string', status: 'string', result: 'object', report: 'object', executionTime: 'number', timestamp: 'string', version: 'string' } }, [MessageType.PROGRAM]: { required: ['type', 'requestRef', 'targetClientId', 'command', 'priority', 'timeout', 'callback', 'timestamp', 'version'], optional: [], types: { type: 'string', requestRef: 'string', targetClientId: 'string', command: 'object', priority: 'string', timeout: 'number', callback: 'string', timestamp: 'string', version: 'string' } }, [MessageType.PROGRAM_RESPONSE]: { required: ['type', 'requestRef', 'status', 'timestamp', 'version'], optional: ['context', 'report', 'executionTime'], types: { type: 'string', requestRef: 'string', status: 'string', context: 'object', report: 'object', executionTime: 'number', timestamp: 'string', version: 'string' } }, [MessageType.HEARTBEAT]: { required: ['type', 'clientId', 'sequence', 'clientTime', 'timestamp', 'version'], optional: [], types: { type: 'string', clientId: 'string', sequence: 'number', clientTime: 'string', timestamp: 'string', version: 'string' } }, [MessageType.HEARTBEAT_ACK]: { required: ['type', 'clientId', 'sequence', 'clientTime', 'serverTime', 'timestamp', 'version'], optional: ['latency', 'serverStatus'], types: { type: 'string', clientId: 'string', sequence: 'number', clientTime: 'string', serverTime: 'string', latency: 'number', serverStatus: 'object', timestamp: 'string', version: 'string' } }, [MessageType.PROGRESS_UPDATE]: { required: ['type', 'requestRef', 'status', 'phase', 'progress', 'sourceType', 'timestamp', 'version'], optional: ['context', 'command', 'report'], types: { type: 'string', requestRef: 'string', status: 'string', phase: 'string', progress: 'number', sourceType: 'string', context: 'object', command: 'object', report: 'object', timestamp: 'string', version: 'string' } }, [MessageType.ERROR]: { required: ['type', 'code', 'message', 'timestamp', 'version'], optional: ['severity', 'category', 'context', 'retryable'], types: { type: 'string', code: 'string', message: 'string', severity: 'string', category: 'string', context: 'any', retryable: 'boolean', timestamp: 'string', version: 'string' } }, [MessageType.UNREGISTER]: { required: ['type', 'clientId', 'timestamp', 'version'], optional: ['reason'], types: { type: 'string', clientId: 'string', reason: 'string', timestamp: 'string', version: 'string' } }, [MessageType.UPDATE_ROUTES]: { required: ['type', 'edgeId', 'devices', 'timestamp', 'version'], optional: [], types: { type: 'string', edgeId: 'string', devices: 'array', timestamp: 'string', version: 'string' } }, [MessageType.UPDATE_ROUTES_ACK]: { required: ['type', 'edgeId', 'success', 'timestamp', 'version'], optional: ['message', 'routeCount'], types: { type: 'string', edgeId: 'string', success: 'boolean', message: 'string', routeCount: 'number', timestamp: 'string', version: 'string' } }, [MessageType.UNREGISTER_ACK]: { required: ['type', 'clientId', 'success', 'timestamp', 'version'], optional: ['cleanupInfo'], types: { type: 'string', clientId: 'string', success: 'boolean', cleanupInfo: 'object', timestamp: 'string', version: 'string' } }, [MessageType.REGISTER_PENDING]: { required: ['type', 'requestId', 'pollInterval', 'expiresIn', 'timestamp', 'version'], optional: [], types: { type: 'string', requestId: 'string', pollInterval: 'number', expiresIn: 'number', timestamp: 'string', version: 'string' } }, [MessageType.AUTHORIZATION_GRANTED]: { required: ['type', 'requestId', 'licenseToken', 'timestamp', 'version'], optional: [], types: { type: 'string', requestId: 'string', licenseToken: 'string', timestamp: 'string', version: 'string' } }, [MessageType.AUTHORIZATION_REJECTED]: { required: ['type', 'requestId', 'timestamp', 'version'], optional: ['reason'], types: { type: 'string', requestId: 'string', reason: 'string', timestamp: 'string', version: 'string' } }, [MessageType.DEVICE_APPROVAL_REQUEST]: { required: ['type', 'edgeId', 'requestId', 'deviceId', 'timestamp', 'version'], optional: ['deviceInfo', 'sourceIp'], types: { type: 'string', edgeId: 'string', requestId: 'string', deviceId: 'string', sourceIp: 'string', timestamp: 'string', version: 'string' } }, [MessageType.DEVICE_APPROVAL_RESPONSE]: { required: ['type', 'requestId', 'deviceId', 'approved', 'timestamp', 'version'], optional: ['deviceAccessKey', 'reason', 'action'], types: { type: 'string', requestId: 'string', deviceId: 'string', approved: 'boolean', deviceAccessKey: 'string', reason: 'string', action: 'string', timestamp: 'string', version: 'string' } }, [MessageType.ACL_INVALIDATED]: { required: ['type', 'timestamp', 'version'], optional: ['clientId', 'jti', 'reason'], types: { type: 'string', clientId: 'string', jti: 'string', reason: 'string', timestamp: 'string', version: 'string' } } }; return schemas[messageType] || null; } }