/** * JRSoft Subway 统一WebSocket协议定义 * 版本: 1.0 * * 功能特性: * 1. 统一消息格式定义 * 2. 支持设备注册、命令执行、心跳等核心功能 * 3. 支持 Edge 代理模式 * * v1.6.0:单一 MessageStatus + CodeMeta + dispatchMessage(详见 code-meta.ts) */ // v1.6.0:从 code-meta 模块导入工具(用于 MessageFactory.dispatchMessage) import { resolveCodeMeta, MessageStatus } from './code-meta'; // 消息类型枚举 export enum MessageType { // 连接管理 REGISTER = 'REGISTER', REGISTER_ACK = 'REGISTER_ACK', UNREGISTER = 'UNREGISTER', UNREGISTER_ACK = 'UNREGISTER_ACK', // 心跳 HEARTBEAT = 'HEARTBEAT', HEARTBEAT_ACK = 'HEARTBEAT_ACK', // 命令执行 COMMAND = 'COMMAND', COMMAND_RESPONSE = 'COMMAND_RESPONSE', // 程序管理 PROGRAM = 'PROGRAM', PROGRAM_RESPONSE = 'PROGRAM_RESPONSE', // 进度更新 PROGRESS_UPDATE = 'PROGRESS_UPDATE', // 路由管理 UPDATE_ROUTES = 'UPDATE_ROUTES', UPDATE_ROUTES_ACK = 'UPDATE_ROUTES_ACK', // 接入授权(RFC 8628 适配) REGISTER_PENDING = 'REGISTER_PENDING', AUTHORIZATION_GRANTED = 'AUTHORIZATION_GRANTED', AUTHORIZATION_REJECTED = 'AUTHORIZATION_REJECTED', // 设备级审批(Edge ↔ Gateway) DEVICE_APPROVAL_REQUEST = 'DEVICE_APPROVAL_REQUEST', DEVICE_APPROVAL_RESPONSE = 'DEVICE_APPROVAL_RESPONSE', // Client ACL 失效通知(Gateway → Backend;client 授权变更时让缓存方清缓存) // v1.11.0 新增。设备端非接收方(只有 ACL 缓存方 Backend 消费),对设备端 N/A。 ACL_INVALIDATED = 'ACL_INVALIDATED', // 错误 ERROR = 'ERROR' } // 客户端类型 export enum ClientType { DEVICE = 'DEVICE', BACKEND = 'BACKEND', EDGE = 'EDGE', GATEWAY = 'GATEWAY', // 添加缺失的 GATEWAY 类型 API_CLIENT = 'API_CLIENT' // v1.11.0: 对外 /api/v1 客户端 (client 动态授权) } // 操作类型 // v1.7.4: 缩到 READ/WRITE 二值 (Scenario B 全删 dead spec) // - QUERY: 3 端 0 真实使用 → 删 // - UPDATE: 历史用途(配置文件更新)已下线;backend gateway.client.ts:556 唯一 1 处 `as any` cast 是 hack → 删 + backend 同步修 // - CONTROL: 3 端 0 真实使用 → 删 // 与 device-command-reference §3 "wire 接受 READ/WRITE" 一致 // (Consumer 设备端可选保留 [Obsolete] 别名作接收向后兼容缓冲) export enum OperationType { READ = 'READ', WRITE = 'WRITE' } // 优先级 export enum Priority { LOW = 'LOW', NORMAL = 'NORMAL', HIGH = 'HIGH', CRITICAL = 'CRITICAL', EMERGENCY = 'EMERGENCY' } // v1.6.0: 单一 MessageStatus enum 取代 CommandStatus / ProgressStatus // 字符串值与 v1.5.0 重叠的 4 值保持一致:'IN_PROGRESS' / 'COMPLETED' / 'FAILED' / 'CANCELLED' // 死代码值删除:CommandStatus.TIMEOUT / ProgressStatus.PENDING / PAUSED // TIMEOUT 语义迁移:status=FAILED + report.data.error.category='TIMEOUT' export { MessageStatus, type CodeMeta, type CodeSemantic, type CodeMetaContext, resolveCodeMeta, isKnownCode, EXPLICIT_META } from './code-meta'; // 基础消息接口 export interface BaseMessage { type: MessageType; timestamp: string; // ISO 8601 格式 version: string; // 消息格式版本,固定为 "1.0"(与包版本 PROTOCOL_VERSION 无关) } // 客户端信息(可选,用于扩展信息) export interface ClientInfo { name?: string; version?: string; platform?: string; capabilities?: string[]; deviceType?: string; // 设备类型,用于设备端 description?: string; metadata?: Record; physicalParams?: DevicePhysicalParams; // v1.4.13: DEVICE 客户端必填,描述屏幕物理属性与节目槽位上限 } // v1.4.13 设备物理参数(DEVICE 注册时必填,字段语义与 ProgramParameters 对齐) export interface DevicePhysicalParams { width: number; // 屏幕宽度(像素,整数 > 0),与 ProgramParameters.width 对齐 height: number; // 屏幕高度(像素,整数 > 0),与 ProgramParameters.height 对齐 direction: ProgramDirection; // 节目滚动方向,复用 ProgramDirection 枚举 maxProgramSlots: number; // 节目槽位上限(整数 1-10,表示容量)。programNo 合法范围 0..(maxProgramSlots-1) } // Edge 信息(设备端通过 Edge 注册时包含) export interface EdgeInfo { edgeId: string; edgeVersion?: string; connectionTime?: string; } // v1.12.0 (Phase 9a) deviceFingerprint — Pre-License 设备身份强化 // 详见 claude-docs/license-authorization-design.md §3.7 + discussions/upgrade-guide-v1.12.0.md // design lock 三方一致 (Owner + 协议方 + 设备端 R6.5 final ack 2026-05-31) export interface DeviceFingerprint { /** 仅允许 'installPubkey' (L3 strict, 无 L1/L2/L4 fallback); 详见 upgrade-guide §3 */ type: 'installPubkey'; /** "sha256:" — SHA-256 of DER-encoded SubjectPublicKeyInfo (P-256 91 bytes) */ value: string; /** base64 编码的 ECDSA-SHA256 签名 (用私钥签 proofPayload, 证明私钥在手) */ proof: string; /** 签名基串, 推荐 "${clientId}|${nonce}|${timestamp}", Edge 用同款规则重建后 verify */ proofPayload: string; /** PEM-encoded SPKI 公钥, 首次审批时必填 (Edge 持久化用于后续验签); 后续注册可省 */ publicKeyPem?: string; } // 注册消息基础结构 (v1.12.0 discriminated union 共享字段) interface BaseRegisterMessage extends BaseMessage { type: MessageType.REGISTER; clientId: string; // 客户端唯一标识符; v1.12.0e 规范 ^[a-z0-9._:-]{1,64}$ lower-case clientInfo?: ClientInfo; edgeInfo?: EdgeInfo; // 当设备通过 Edge 注册时包含 licenseToken?: string; // DEVICE 装 DAK; EDGE/BACKEND/API_CLIENT 装 License JWT } // v1.12.0 (Phase 9a Owner+协议方+设备端三方拍 schema 层 C 方案 discriminated union) // DEVICE clientType: deviceFingerprint **runtime required** (Edge 收到不填或 type ≠ installPubkey // 立即 reject FINGERPRINT_REQUIRED / FINGERPRINT_TYPE_UNSUPPORTED, grace period 内例外 LEGACY warn) export interface DeviceRegisterMessage extends BaseRegisterMessage { clientType: ClientType.DEVICE; deviceFingerprint: DeviceFingerprint; } // 非 DEVICE clientType (EDGE/BACKEND/GATEWAY): deviceFingerprint 不适用 // `?: never` 类型层挡 — 调用方误带 fingerprint 编译期就抓 export interface NonDeviceRegisterMessage extends BaseRegisterMessage { clientType: Exclude; deviceFingerprint?: never; } // 注册消息 — discriminated union by clientType export type RegisterMessage = DeviceRegisterMessage | NonDeviceRegisterMessage; // 注册确认消息 export interface RegisterAckMessage extends BaseMessage { type: MessageType.REGISTER_ACK; clientId: string; // 确认的客户端ID success: boolean; // 注册是否成功 sessionId?: string; // 成功时分配的会话ID error?: { // 失败时的错误信息 code: string; message: string; }; serverInfo?: { // 服务器信息 version: string; capabilities: string[]; currentLoad?: number; // 当前负载 maxClients?: number; // 最大客户端数 }; } // 注销消息 export interface UnregisterMessage extends BaseMessage { type: MessageType.UNREGISTER; clientId: string; // 客户端唯一标识符 reason?: string; // 注销原因 } // 注销确认消息 export interface UnregisterAckMessage extends BaseMessage { type: MessageType.UNREGISTER_ACK; clientId: string; // 客户端唯一标识符 success: boolean; // 注销是否成功 cleanupInfo?: { // 清理信息 messagesProcessed: number; // 已处理的消息数 pendingMessages: number; // 待处理的消息数 connectionDuration: number; // 连接持续时间(秒) }; } // 注册挂起消息(Gateway → Edge/Backend:请求已收到,等待管理员审批) export interface RegisterPendingMessage extends BaseMessage { type: MessageType.REGISTER_PENDING; requestId: string; // 审批请求唯一 ID pollInterval: number; // 建议客户端轮询间隔(秒) expiresIn: number; // 请求有效期(秒),超时自动拒绝 } // 授权通过消息(Gateway → Edge/Backend:审批通过,JWT 已颁发) export interface AuthorizationGrantedMessage extends BaseMessage { type: MessageType.AUTHORIZATION_GRANTED; requestId: string; licenseToken: string; // 签名后的 JWT } // 授权拒绝消息(Gateway → Edge/Backend:审批拒绝) export interface AuthorizationRejectedMessage extends BaseMessage { type: MessageType.AUTHORIZATION_REJECTED; requestId: string; reason?: string; } // 设备审批请求消息(Edge → Gateway:请求管理员审批设备接入) export interface DeviceApprovalRequestMessage extends BaseMessage { type: MessageType.DEVICE_APPROVAL_REQUEST; edgeId: string; requestId: string; deviceId: string; deviceInfo?: ClientInfo; sourceIp?: string; } // 设备审批响应消息(Gateway → Edge:审批结果) export interface DeviceApprovalResponseMessage extends BaseMessage { type: MessageType.DEVICE_APPROVAL_RESPONSE; requestId: string; deviceId: string; approved: boolean; deviceAccessKey?: string; // 批准时颁发的 DAK reason?: string; // 拒绝原因 action?: 'unblacklist' | 'revoke'; // 可选动作:移除黑名单 / 吊销已授权设备 } // 引用生成的强类型命令 import type { SpecificCommand as ImportedSpecificCommand, CommandTypeMap as ImportedCommandTypeMap } from './command-types'; export type SpecificCommand = ImportedSpecificCommand; export type CommandTypeMap = ImportedCommandTypeMap; // 基础命令接口 export interface BaseCommand { commandCode: string; parameters?: Record; } // 简单命令接口(点对点) export interface SimpleCommand extends BaseCommand { commandType: CommandType.SIMPLE; deviceId: number | string; // 必需:单个设备 deviceType: string; // 必需 operationType: OperationType; // 必需 } // 批量命令接口(多设备) export interface BatchCommand extends BaseCommand { commandType: CommandType.BATCH; deviceId: number[] | string; // 必需:设备数组或范围表达式 deviceType: string; // 必需 operationType: OperationType; // 必需 } // 复杂命令接口(持续响应) export interface ComplexCommand extends BaseCommand { commandType: CommandType.COMPLEX; deviceId?: number | number[] | string; // 可选 deviceType?: string; // 可选 operationType?: OperationType; // 可选 } // 通用命令接口(用于运行时动态命令) // 注意:字段是否必需由 MessageValidator 根据 commandType 在运行时验证 export interface GenericCommand extends BaseCommand { commandType?: CommandType; // 可选,默认为 SIMPLE deviceId?: number | number[] | string; // 可选,验证器会检查 deviceType?: string; // 可选,验证器会检查 operationType?: OperationType; // 可选,验证器会检查 } // 命令详情 - 支持强类型和通用类型 export type Command = SpecificCommand | SimpleCommand | BatchCommand | ComplexCommand | GenericCommand; // 命令类型 export enum CommandType { SIMPLE = 'SIMPLE', // 点对点命令 BATCH = 'BATCH', // 多设备命令 COMPLEX = 'COMPLEX' // 持续响应命令 } // 命令消息 export interface CommandMessage extends BaseMessage { type: MessageType.COMMAND; requestRef: string; targetClientId: string; // 目标客户端 ID (所有类型必需) command: Command; // 所有类型都使用command字段 priority: Priority; timeout: number; retryCount?: number; callback: string; // 回调地址,用于接收命令响应 metadata?: Record; } // 命令执行结果 export interface CommandResult { deviceType: string; deviceId: number | string; commandCode: string; operationType: OperationType; data: Record; } // 命令响应消息 export interface CommandResponseMessage extends BaseMessage { type: MessageType.COMMAND_RESPONSE; clientId: string; // 响应设备标识 requestRef: string; /** * COMMAND_RESPONSE wire 的 status 字段是**终态语义**, 必须 COMPLETED/FAILED/CANCELLED/TIMEOUT. * v1.8.5 spec invariant (反向约束, 设备端 Round 1 review §4.2 启发): * `IN_PROGRESS` 只属于 PROGRESS_UPDATE message type, 不可出现在 COMMAND_RESPONSE. * Edge v1.8.5 validator 对 status='IN_PROGRESS' 的 COMMAND_RESPONSE → REJECT * (fail code: ERROR_RESPONSE_NON_TERMINAL_STATUS_NOT_ALLOWED). * TypeScript literal type 编译期 catch. */ status: 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'TIMEOUT'; result?: CommandResult; // 命令执行结果 report?: ReportMessage; executionTime?: number; } // 注意:批量命令和复杂命令通过 progress_update 报告执行进度,最终返回简单的 command_response // 心跳消息 export interface HeartbeatMessage extends BaseMessage { type: MessageType.HEARTBEAT; clientId: string; // 客户端唯一标识符 sequence: number; // 序列号,用于匹配请求和响应 clientTime: string; // 客户端时间戳 } // 心跳确认消息 export interface HeartbeatAckMessage extends BaseMessage { type: MessageType.HEARTBEAT_ACK; clientId: string; // 客户端唯一标识符 sequence: number; // 原始序列号 clientTime: string; // 原始客户端时间 serverTime: string; // 服务器时间 latency?: number; // 服务器处理延迟(毫秒) serverStatus?: { // 服务器状态 healthy: boolean; activeConnections: number; messageQueueSize: number; cpuUsage?: number; memoryUsage?: number; }; } // 错误消息 export interface ErrorMessage extends BaseMessage { type: MessageType.ERROR; code: string; message: string; severity?: string; category?: string; context?: any; retryable?: boolean; } // 程序类型 export enum ProgramType { DYNAMIC = 'DYNAMIC', STATIC = 'STATIC' } // 程序方向 export enum ProgramDirection { LEFT_TO_RIGHT = 'LEFT_TO_RIGHT', RIGHT_TO_LEFT = 'RIGHT_TO_LEFT' } // 程序上传参数 export interface ImageProcessConfig { gamma?: { k: number; gamma: number; n: number }; histogram?: boolean; rgbCorrection?: { r: number; g: number; b: number; k: number }; } export interface ProgramParameters { deviceId: string; taskId: string; // Snowflake ID programId: string; // Snowflake ID programName: string; programNo: number; // 0-9 (v1.7.5 rename from programNumber — 设备 0-based + Snowflake programId 区分) programType: ProgramType; width: number; height: number; direction: ProgramDirection; publishTime?: string; // 上刊时间(可选,不填则立即生效) unpublishTime?: string; // 下刊时间(可选,不填则无限期) downloadUrl: string; checksum: string; hashAlgorithm: 'SHA256' | 'MD5'; fileSize?: number; imageProcessConfig?: ImageProcessConfig; // v1.12.1: 可选, 设备端 PROGRAM_PREPROCESS 使用. 不传=无图像处理 } // 程序消息 export interface ProgramMessage extends BaseMessage { type: MessageType.PROGRAM; requestRef: string; targetClientId: string; // 目标客户端 ID command: { commandCode: 'UPLOAD_PROGRAM'; parameters: ProgramParameters; }; priority: Priority; timeout: number; callback: string; // 回调地址,用于接收程序上传进度和结果 } // 程序响应消息 export interface ProgramResponseMessage extends BaseMessage { type: MessageType.PROGRAM_RESPONSE; clientId: string; // 响应设备标识 requestRef: string; /** * PROGRAM_RESPONSE wire 的 status 字段是**终态语义**, 必须 COMPLETED/FAILED/CANCELLED/TIMEOUT. * v1.8.5 spec invariant (反向约束, 设备端 Round 1 review §4.2 启发): * `IN_PROGRESS` 只属于 PROGRESS_UPDATE, 不可出现在 PROGRAM_RESPONSE. * Edge v1.8.5 validator 对 status='IN_PROGRESS' 的 PROGRAM_RESPONSE → REJECT * (fail code: ERROR_RESPONSE_NON_TERMINAL_STATUS_NOT_ALLOWED). */ status: 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'TIMEOUT'; context?: ProgramContext; // 上下文信息(可选) report?: ReportMessage; // 日志信息(可选) executionTime?: number; // 总执行时间(毫秒) } /** * 进度阶段(PROGRESS_UPDATE.phase) * * v1.4.11 重命名整理(不保留 deprecated): * - 节目处理 pipeline 改用 PROGRAM_ 前缀,与 SYNC_DETECT/SWITCH_DETECT 同风格 * - EDGE_CACHE_FETCH/READY 新增(Edge 缓存活动可见性) * - 一键检测的首尾边界 INITIALIZATION/COMPLETE 改名为 DETECT_INIT/DETECT_COMPLETE * - BATCH_EXECUTE 替代旧的 'executing' 字符串(消除与 Backend 内部状态字符串撞名) * - SYNC_EXPORT 替代 EXPORT(与 SYNC_DETECT/SYNC_RECOVER 同主语) * * v1.9.0 命名空间统一(不保留 deprecated): * - 一键检测 7 phase 加 QUICK_DETECTION_ 前缀(DETECT_INIT→QUICK_DETECTION_INIT 等) * - 镜像 PROGRAM_/EDGE_CACHE_/SYNC_/BATCH_ 族风格,闭合 v1.4.11 detection 族无前缀的设计空洞 * - 协议方主动升 minor — 不扩 prefix 打补丁, 治本设计(MEMORY rule #1) * - 详见 claude-docs/discussions/protocol-team-proposal-progressphase-detection-namespace-v1.9.0.md * * 注意:Backend 状态机的内部相位(TASK_ACCEPTED / TASK_RESOLVING_URL 等) * 不通过协议消息流转,**不**列在此 enum 中(属于 Backend 内部 audit 字符串)。 */ export enum ProgressPhase { // ---- 节目处理 pipeline(设备端)---- // v1.5.0: PROGRAM 命令统一生命周期 — 加 INIT 起始 / COMPLETE 终态边界 PROGRAM_INIT = 'PROGRAM_INIT', // v1.5.0 新增 — 任务初始化(参数校验、资源准备、并发检查) PROGRAM_FETCH = 'PROGRAM_FETCH', // 拉取节目源文件(OSS / Edge / 其他) PROGRAM_EXTRACT = 'PROGRAM_EXTRACT', // 解压归档(zip/tar/rar) PROGRAM_PREPROCESS = 'PROGRAM_PREPROCESS', // 图片预处理(缩放/滤镜等) PROGRAM_COMPILE = 'PROGRAM_COMPILE', // 编译为显示帧 PROGRAM_UPLOAD = 'PROGRAM_UPLOAD', // 上传到底层显示设备 PROGRAM_STATS = 'PROGRAM_STATS', // 上报统计信息 PROGRAM_COMPLETE = 'PROGRAM_COMPLETE', // v1.5.0 新增 — 终态边界(决定 ALL_SUCCESS / PARTIAL / FAILED 等) // ---- Edge 缓存可见性(v1.4.11 新增)---- // ⚠ Edge 服务端专用,设备端不发:仅 Edge 在 PROGRESS_UPDATE(sourceType=EDGE) 中 // emit,用于 dashboard 时间线展示 Edge 拉 OSS 进度。Edge 缓存错误归 EDGE_PROXY。 EDGE_CACHE_FETCH = 'EDGE_CACHE_FETCH', // Edge 从 OSS 拉文件中 EDGE_CACHE_READY = 'EDGE_CACHE_READY', // Edge 缓存就绪 // ---- 同步器数据 ---- SYNC_EXPORT = 'SYNC_EXPORT', // 同步器监播表导出 SYNC_MONITORING_TABLE // ---- 一键检测(v1.4.9,首尾边界标记 v1.4.11 重命名,v1.9.0 命名空间统一加 QUICK_DETECTION_ 前缀)---- // v1.7.2: 删除 RATER_DETECT — 与 ErrorPhase v1.7.1 删除保持一致(sg7-31-win 设备端 // v1.5.0 起架构性移除 RATER 检测路径;line 430 旧值漏改在 v1.7.1 发版时未被发现) // v1.9.0: 命名空间对齐 PROGRAM_/EDGE_CACHE_/SYNC_/BATCH_ 族 — 7 值统一加 QUICK_DETECTION_ 前缀 // (镜像设备端 sg7-31-win 提议 + Round 1-3 双方 ACCEPT, MEMORY rule #3 反向 invariant) QUICK_DETECTION_INIT = 'QUICK_DETECTION_INIT', // 一键检测流程初始化 (v1.9.0 ← DETECT_INIT) QUICK_DETECTION_SWITCH_DETECT = 'QUICK_DETECTION_SWITCH_DETECT', // 检测交换机在线状态 (v1.9.0 ← SWITCH_DETECT) QUICK_DETECTION_SWITCH_CONFIG_READ = 'QUICK_DETECTION_SWITCH_CONFIG_READ', // 读取交换机配置信息 (v1.9.0 ← SWITCH_CONFIG_READ) QUICK_DETECTION_SYNC_DETECT = 'QUICK_DETECTION_SYNC_DETECT', // 检测同步器状态 (v1.9.0 ← SYNC_DETECT) QUICK_DETECTION_BARGRAPH_DETECT = 'QUICK_DETECTION_BARGRAPH_DETECT', // 检测光柱节点在线状态 (v1.9.0 ← BARGRAPH_DETECT) QUICK_DETECTION_SYNC_RECOVER = 'QUICK_DETECTION_SYNC_RECOVER', // 恢复同步器原始状态 (v1.9.0 ← SYNC_RECOVER) QUICK_DETECTION_COMPLETE = 'QUICK_DETECTION_COMPLETE', // 一键检测全部阶段完成 (v1.9.0 ← DETECT_COMPLETE) // ---- 通用命令兜底 ---- BATCH_EXECUTE = 'BATCH_EXECUTE', // BATCH 类型命令执行中 } /** PROGRESS_UPDATE.sourceType 合法值(v1.4.11 新增 EDGE) */ export type ProgressSourceType = 'COMMAND' | 'SYSTEM' | 'EDGE'; // 设备操作记录 export interface DeviceOperationRecord { commandType: CommandType; // 命令类型 commandCode: string; // 命令代码 deviceType: string; // 设备类型 deviceId: number | string; // 设备ID operationType: OperationType; // 读写类型 result?: Record; // 命令执行结果对象 } // v1.6.0:原 ProgressStatus enum 删除——所有消息的 status 字段统一类型为 MessageStatus // 删除的死代码值:PENDING / PAUSED(两个值在 v1.5.0 内 0 引用,全协议历史从未真实发出) // 集成方:import { MessageStatus } from '@thejrsoft/subway-protocol' // 程序上下文信息 export interface ProgramContext { taskId: string; // Snowflake ID programId: string; // Snowflake ID programName: string; // 程序名称 programNo: number; // 0-9 (v1.7.5 rename from programNumber — 设备 0-based + Snowflake programId 区分) programType: ProgramType; // dynamic | static } // 报告级别 // v1.7.2: 删除 DEBUG / CRITICAL — 死代码清理(EXPLICIT_META / SUFFIX_RULES / 三端代码 0 处使用, // 与 v1.7.0 删 PENDING/PAUSED 同节奏)。⚠ 注意 Priority enum 的 CRITICAL 是命令优先级,与本 enum 无关, // 不要误删 — sg7-31-win 设备端 audit 提醒。 export enum ReportLevel { INFO = 'INFO', WARNING = 'WARNING', ERROR = 'ERROR' } // v1.5.0: ErrorCategory — 7 类 closed enum(v1.4.14 的 4 档扩展到 7 档) // - TRANSPORT: 通讯层失败 — 网络断、CAN 总线告警、HTTP 4xx/5xx、设备离线 // - TIMEOUT: 等待响应超时 — HTTP 504、设备无应答、命令响应等待超时 // - RESOURCE: 资源不足/耗尽 — 磁盘满、内存不足、设备槽位耗尽、文件句柄耗尽 // - BUSINESS: 业务逻辑/状态/异常(兜底)— 业务规则违反、状态机错误、未预期异常 // - CONFIGURATION: 配置缺失/错误(v1.5.0 新增)— Line.config 字段缺、ParamSet 不完整 // - PROTOCOL: 协议层错误(v1.5.0 新增)— JSON 解析失败、字段缺失、消息格式错 // - AUTHORIZATION: 凭证/权限失败(v1.5.0 新增)— DAK 失效、设备未审批、命令权限不足 export type ErrorCategory = | 'TRANSPORT' | 'TIMEOUT' | 'RESOURCE' | 'BUSINESS' | 'CONFIGURATION' | 'PROTOCOL' | 'AUTHORIZATION'; // v1.5.0: ErrorPhase — 错误归类的 phase 维度,与 ProgressPhase 是两个独立 closed set // · 共享主流子集:PROGRAM 8 + DETECT 7 + SYNC_EXPORT + BATCH_EXECUTE(即"业务阶段", // 既是进度时间线位置,也是错误归位) // · ErrorPhase 独有:EDGE_PROXY(代理层错误抽象层,仅作错误归类,不发为进度) // · ProgressPhase 独有:EDGE_CACHE_FETCH / EDGE_CACHE_READY(Edge 缓存可见性专用, // sourceType=EDGE 进度消息;Edge 缓存错误归 EDGE_PROXY,不在 ErrorPhase 中扩值) // v1.7.1: 删除 RATER_DETECT — sg7-31-win 设备端 v1.5.0 后架构性移除 RATER 检测路径 // v1.7.2: 注释订正 — 历史"ErrorPhase 是 ProgressPhase alias"措辞(v1.5.0–v1.7.1)已撤销 // 实际从来都不是 alias,是两套独立语义维度的 closed set。ProgressPhase 同步删 RATER_DETECT。 export type ErrorPhase = | 'PROGRAM_INIT' | 'PROGRAM_FETCH' | 'PROGRAM_EXTRACT' | 'PROGRAM_PREPROCESS' | 'PROGRAM_COMPILE' | 'PROGRAM_UPLOAD' | 'PROGRAM_STATS' | 'PROGRAM_COMPLETE' // v1.9.0: detection 族 7 值统一加 QUICK_DETECTION_ 前缀, 与 ProgressPhase 镜像 | 'QUICK_DETECTION_INIT' | 'QUICK_DETECTION_SWITCH_DETECT' | 'QUICK_DETECTION_SWITCH_CONFIG_READ' | 'QUICK_DETECTION_SYNC_DETECT' | 'QUICK_DETECTION_BARGRAPH_DETECT' | 'QUICK_DETECTION_SYNC_RECOVER' | 'QUICK_DETECTION_COMPLETE' | 'SYNC_EXPORT' | 'BATCH_EXECUTE' | 'EDGE_PROXY'; // v1.5.0: ErrorStep — 21 个值,按 phase 归属(详见 message-validator.ts 的 step-phase 映射) // v1.6.1:Edge 代理层新增 4 个 step(适用于 EDGE_PROXY phase) export type ErrorStep = // PROGRAM_INIT | 'ValidateParameters' | 'PrepareResources' | 'LoadConfig' | 'CheckConcurrency' // PROGRAM_FETCH | 'Download' | 'VerifyChecksum' // PROGRAM_EXTRACT | 'ExtractZip' // PROGRAM_PREPROCESS | 'ResizeImage' | 'AdjustColor' | 'GammaCorrect' | 'Histogram' | 'RGBCorrection' // PROGRAM_COMPILE | 'CompileFrame' | 'MoveFrame' | 'VerifyFrameCount' // PROGRAM_UPLOAD | 'DeviceCheck' | 'DataTransfer' | 'ForbiddenTable' | 'StatusRecovery' | 'RetryLimitExceeded' // PROGRAM_COMPLETE — v1.10.0 §1 新增 (镜像 v1.9.0 §B QUICK_DETECTION_COMPLETE 的 DetectionWrapup) | 'ProgramWrapup' // PROGRAM_COMPLETE (v1.10.0 §1 新增, 配 PROGRAM_COMPLETE_FINAL wire) // PROGRAM_STATS — v1.7.1 删除 CloudReport(云端 RabbitMQ 通道下线,dead spec 清理) // QUICKLY_DETECTION 族 step(v1.7.1 新增,按 sg7-31-win 设备端 final 命名) // v1.9.0: phase 注释加 QUICK_DETECTION_ 前缀 + 新增 2 值 (§B DetectionWrapup / §C CommBoardInfoRead) | 'DetectionInit' // QUICK_DETECTION_INIT | 'NetworkScan' // QUICK_DETECTION_SWITCH_DETECT | 'SwitchConfigRead' // QUICK_DETECTION_SWITCH_CONFIG_READ | 'CommBoardInfoRead' // QUICK_DETECTION_SWITCH_CONFIG_READ (v1.9.0 §C 新增, 配 _SWITCH_CONFIG_READ_COMM_BOARD_FAILED wire) | 'SyncDeviceCheck' // QUICK_DETECTION_SYNC_DETECT | 'BargraphNodeCheck' // QUICK_DETECTION_BARGRAPH_DETECT | 'SyncStatusRecover' // QUICK_DETECTION_SYNC_RECOVER | 'DetectionWrapup' // QUICK_DETECTION_COMPLETE (v1.9.0 §B 新增, 配 _COMPLETE_FINAL wire) // EDGE_PROXY(v1.6.1 新增) | 'ValidateCommandFormat' | 'CheckDeviceOnline' | 'DispatchToDevice' | 'AwaitDeviceResponse' // v1.8.2: BATCH_EXECUTE step (BATCH 子项 failure 路径, 修 v1.5.0 ERROR_STEP_BY_PHASE map 漏 key 残留) // DataTransfer: 复用 PROGRAM_UPLOAD step (设备端 commit c9bdc03 已用, BATCH 子项 CAN bus 通信失败) // SubItemTimeout: 子设备无响应超时 (v1.9.0 设备端 detail-aware step 派生用) // SubItemDeviceQuery: 子设备查询失败 (e.g. install offset 读取, READ_FAILED 路径试点) | 'SubItemTimeout' | 'SubItemDeviceQuery' // v1.8.2: SYNC_EXPORT step (监播表导出 failure 路径) // ReadDayData: 日数据读取失败 (设备端 SYNC_MONITORING_TABLE_READ_FAILED 路径) // AggregateExport: 聚合导出失败 (PARTIAL_SUCCESS 边界场景) | 'ReadDayData' | 'AggregateExport'; // v1.5.0: 统一失败信号 schema — 嵌套在 report.data.error 下,仅 ERROR / 终态失败消息携带 // v1.8.3: phase/step 改 optional — 适用范围按 wire data.code 本身的语义判定: // - orchestrator-coded wire (BATCH_* / PROGRAM_* / QUICK_DETECTION_* / // SYNC_EXPORT / SYNC_MONITORING_TABLE / EDGE_*) → phase/step **必填**, 必须 closed enum 合法值 // - leaf 码 wire (SYNC/RATER/BARGRAPH SIMPLE + Cured-with-leaf-wire) → phase/step **必须缺失**, // 出现任何值 (含 UNKNOWN) 即 REJECTED (clean break, 无 backward 通道) // - category / detail 永远必填 // v1.9.0: 删 BARGRAPH_CHECK_ONLINE_STATUS prefix (设备端 0 emit dead spec) + detection 单 prefix 统一 // 判据用 isOrchestratorCodedWire(wireCode) helper, 禁止用 command.commandType (BATCH 子项 commandType=SIMPLE 陷阱) // 详见: claude-docs/discussions/protocol-team-reply-2-to-proposal-v1.8.3.md (Round 11''') export interface ErrorInfo { phase?: ErrorPhase; // v1.8.3: optional (orchestrator-coded wire 必填; leaf wire 必须缺失) step?: ErrorStep; // v1.8.3: optional (同 phase) category: ErrorCategory; // 协议级横切分类(7 类)— 必填 detail: string; // 自由文本(英文,grep 友好)— 必填 } // v1.5.0: ReportData — 嵌套结构,error 仅在失败时存在;其余字段按命令业务字段平铺 export interface ReportData { error?: ErrorInfo; // v1.5.0 新增 — 失败信号 schema(level=ERROR / status=FAILED 时必填) [key: string]: any; // 业务字段平铺(programNo / fileSize / 等) } // v1.5.0: ReportMessage — 移除顶层 category 字段(下沉到 data.error.category) // - data.error 仅在错误时出现(level=ERROR / status=FAILED) // - 成功/进度消息:data 仍为平铺业务字段,无 error // v1.7.4: 删 messageEn 字段 — dead spec,3 端 0 真实使用 // - 设备端 i18n 实践 = 全英文 message(事实标准) // - messageEn 字段从 v1.5.0 引入到 v1.7.3 期间 0 处使用 export interface ReportMessage { level: ReportLevel; message: string; // 报告消息(推荐英文,作为 i18n 兜底 / 设备日志) code?: string; // 标准化的消息代码 data?: ReportData; // v1.5.0 — 改为 ReportData 嵌套结构(含 error) } // 进度更新消息 export interface ProgressUpdateMessage extends BaseMessage { type: MessageType.PROGRESS_UPDATE; clientId: string; // 上报设备标识 requestRef: string; /** * PROGRESS_UPDATE wire 的 status 字段**永远 'IN_PROGRESS'**. * v1.8.5 spec invariant — TypeScript literal type 编译期 catch 任何手写派生. * * 终态语义 (COMPLETED / FAILED / CANCELLED / TIMEOUT) 只属于 * COMMAND_RESPONSE / PROGRAM_RESPONSE message types. * * progress=100 on PROGRESS_UPDATE 表示"达到 100% 进度", 而非"命令完成". * 命令真正完成由 COMMAND_RESPONSE / PROGRAM_RESPONSE 单独承载. * * Edge v1.8.5 validator 对 status !== 'IN_PROGRESS' 的 PROGRESS_UPDATE → REJECT * (单步 ship, 0 grace mode, fail code: ERROR_PROGRESS_UPDATE_TERMINAL_STATUS_NOT_ALLOWED). */ status: 'IN_PROGRESS'; // v1.8.5: literal type 收紧 (v1.6.0 enum → v1.8.5 literal) phase: ProgressPhase | string; // 当前阶段(支持自定义阶段) progress: number; // 0-100 进度百分比 sourceType: ProgressSourceType; // 来源类型:COMMAND/SYSTEM/EDGE(v1.4.11 加 EDGE) context?: ProgramContext; // 上下文信息(用于程序上传相关的进度更新) command?: DeviceOperationRecord; // 设备操作记录(当涉及设备读写时) report?: ReportMessage; // 日志信息(可选) timestamp: string; // ISO 8601 时间戳 version: string; // 协议版本 // v1.8.1: 删除 metaCompliance 字段 — 该字段是 Edge 中间件 metadata, 不应污染 device-emit wire schema // 改用 EdgeForwardEnvelope (本文件下方定义) 在 Edge↔Gateway 内部协议层承载 // 详见 protocol-team-reply-to-consumer-audit-v1.8.0.md (Round 9) + Round 10 ack } /** * v1.8.1: Edge → Gateway 内部 forward envelope * * 用途: Edge 收到设备端 wire 消息后, 包一层 envelope 转发到 Gateway, * 把 Edge 校验结果 (metaCompliance / validationFailures) 放外层 annotation, * 不污染 device emit 的原始 payload。 * * 与 v1.8.0 mutate 模式的区别: * v1.8.0: Edge 直接 (message as any).metaCompliance = 'TOLERATED' 改写 payload * v1.8.1: Edge send EdgeForwardEnvelope, payload 字段保留 device emit 原样 * * 协议层使用范围: 仅 Edge↔Gateway WebSocket 内部协议 — 设备端 wire 不涉及。 */ export interface EdgeForwardEnvelope { /** 包装设备端 emit 的原始 wire payload (PROGRESS_UPDATE / COMMAND_RESPONSE / PROGRAM_RESPONSE) */ edgePayload: ProgressUpdateMessage | CommandResponseMessage | ProgramResponseMessage; /** Edge 添加的 metadata, 不污染 edgePayload */ edgeAnnotation: { /** META 校验 outcome — STRICT 通过 / TOLERATED 宽容放行 */ metaCompliance: 'STRICT' | 'TOLERATED'; /** 校验失败的具体原因 (TOLERATED 时 dashboard 展示用; STRICT 时通常空) */ validationFailures?: string[]; /** Edge 收到 wire 的时间 (ISO 8601, 与 device emit timestamp 区分) */ edgeReceivedAt: string; }; /** envelope 标识 — 让 Gateway 解析时能区分 v1.8.0 raw message vs v1.8.1 envelope (forward-compat) */ envelopeVersion: '1.8.1'; } /** * v1.8.1: 类型守卫 — 判断 Gateway 收到的 message 是 v1.8.1 envelope 还是 v1.8.0 raw payload * (向后兼容: 老 Edge 仍可能 send raw message) */ export function isEdgeForwardEnvelope(msg: unknown): msg is EdgeForwardEnvelope { return !!msg && typeof msg === 'object' && (msg as any).envelopeVersion === '1.8.1' && 'edgePayload' in (msg as any) && 'edgeAnnotation' in (msg as any); } // 路由更新消息(Edge -> Gateway) export interface UpdateRoutesMessage extends BaseMessage { type: MessageType.UPDATE_ROUTES; clientId: string; // Edge节点ID(统一使用clientId) devices: string[]; // 设备ID列表 } // 路由更新确认消息(Gateway -> Edge) export interface UpdateRoutesAckMessage extends BaseMessage { type: MessageType.UPDATE_ROUTES_ACK; clientId: string; // Edge节点ID(统一使用clientId) success: boolean; // 更新是否成功 message?: string; // 附加消息 routeCount?: number; // 成功更新的路由数量 } // ACL 失效通知(Gateway → Backend,v1.11.0) // admin 改/吊销某 client 的 ACL 时, Gateway 经常驻 WS 通道推此消息, 让 Backend 清缓存 → 近实时生效。 // 设备端非接收方, 不需实现 (仅 ACL 缓存方 Backend 消费)。 export interface AclInvalidatedMessage extends BaseMessage { type: MessageType.ACL_INVALIDATED; clientId?: string; // 受影响的 client (缺省 = 全部失效) jti?: string; // 受影响的具体 license (可选, 更细) reason?: string; // 变更原因 (edit / revoke 等) } // 类型守卫函数 export function isRegisterMessage(msg: any): msg is RegisterMessage { return msg && msg.type === MessageType.REGISTER; } export function isRegisterAckMessage(msg: any): msg is RegisterAckMessage { return msg && msg.type === MessageType.REGISTER_ACK; } export function isUnregisterMessage(msg: any): msg is UnregisterMessage { return msg && msg.type === MessageType.UNREGISTER; } export function isUnregisterAckMessage(msg: any): msg is UnregisterAckMessage { return msg && msg.type === MessageType.UNREGISTER_ACK; } export function isHeartbeatMessage(msg: any): msg is HeartbeatMessage { return msg && msg.type === MessageType.HEARTBEAT; } export function isHeartbeatAckMessage(msg: any): msg is HeartbeatAckMessage { return msg && msg.type === MessageType.HEARTBEAT_ACK; } export function isCommandMessage(msg: any): msg is CommandMessage { return msg && msg.type === MessageType.COMMAND; } export function isCommandResponseMessage(msg: any): msg is CommandResponseMessage { return msg && msg.type === MessageType.COMMAND_RESPONSE; } export function isProgramMessage(msg: any): msg is ProgramMessage { return msg && msg.type === MessageType.PROGRAM; } export function isProgramResponseMessage(msg: any): msg is ProgramResponseMessage { return msg && msg.type === MessageType.PROGRAM_RESPONSE; } export function isProgressUpdateMessage(msg: any): msg is ProgressUpdateMessage { return msg && msg.type === MessageType.PROGRESS_UPDATE; } export function isErrorMessage(msg: any): msg is ErrorMessage { return msg && msg.type === MessageType.ERROR; } export function isUpdateRoutesMessage(msg: any): msg is UpdateRoutesMessage { return msg && msg.type === MessageType.UPDATE_ROUTES; } export function isUpdateRoutesAckMessage(msg: any): msg is UpdateRoutesAckMessage { return msg && msg.type === MessageType.UPDATE_ROUTES_ACK; } export function isRegisterPendingMessage(msg: any): msg is RegisterPendingMessage { return msg && msg.type === MessageType.REGISTER_PENDING; } export function isAuthorizationGrantedMessage(msg: any): msg is AuthorizationGrantedMessage { return msg && msg.type === MessageType.AUTHORIZATION_GRANTED; } export function isAuthorizationRejectedMessage(msg: any): msg is AuthorizationRejectedMessage { return msg && msg.type === MessageType.AUTHORIZATION_REJECTED; } export function isDeviceApprovalRequestMessage(msg: any): msg is DeviceApprovalRequestMessage { return msg && msg.type === MessageType.DEVICE_APPROVAL_REQUEST; } export function isDeviceApprovalResponseMessage(msg: any): msg is DeviceApprovalResponseMessage { return msg && msg.type === MessageType.DEVICE_APPROVAL_RESPONSE; } export function isAclInvalidatedMessage(msg: any): msg is AclInvalidatedMessage { return msg && msg.type === MessageType.ACL_INVALIDATED; } // v1.5.0: ErrorCategory 类型守卫(消费方收到未知值时降级到 BUSINESS) export const VALID_ERROR_CATEGORIES: readonly ErrorCategory[] = [ 'TRANSPORT', 'TIMEOUT', 'RESOURCE', 'BUSINESS', 'CONFIGURATION', 'PROTOCOL', 'AUTHORIZATION', ] as const; export function isValidErrorCategory(value: any): value is ErrorCategory { return typeof value === 'string' && (VALID_ERROR_CATEGORIES as readonly string[]).includes(value); } // v1.5.0: normalizeErrorCategory — 未知值降级到 'BUSINESS'(forward-compatibility 标准做法) export function normalizeErrorCategory(value: any): ErrorCategory { return isValidErrorCategory(value) ? value : 'BUSINESS'; } // v1.5.0: ErrorStep ↔ ErrorPhase 归属映射(强校验:step 必须属于 phase) export const ERROR_STEP_BY_PHASE: Record = { PROGRAM_INIT: ['ValidateParameters', 'PrepareResources', 'LoadConfig', 'CheckConcurrency'] as const, PROGRAM_FETCH: ['Download', 'VerifyChecksum'] as const, PROGRAM_EXTRACT: ['ExtractZip'] as const, PROGRAM_PREPROCESS: ['ResizeImage', 'AdjustColor', 'GammaCorrect', 'Histogram', 'RGBCorrection'] as const, PROGRAM_COMPILE: ['CompileFrame', 'MoveFrame', 'VerifyFrameCount'] as const, PROGRAM_UPLOAD: ['DeviceCheck', 'DataTransfer', 'ForbiddenTable', 'StatusRecovery', 'RetryLimitExceeded'] as const, PROGRAM_STATS: [] as const, // v1.7.1: 删 CloudReport(RabbitMQ 通道下线,dead spec 清理) // v1.10.0 §1: 加 ProgramWrapup, 镜像 QUICK_DETECTION_COMPLETE: ['DetectionWrapup'] (v1.9.0 §B) PROGRAM_COMPLETE: ['ProgramWrapup'] as const, // v1.7.1: QUICKLY_DETECTION 族 step 归属(按 sg7-31-win 设备端 final 单 step 命名) // v1.9.0: 7 key 加 QUICK_DETECTION_ 前缀 + §C 加 CommBoardInfoRead + §B 加 DetectionWrapup QUICK_DETECTION_INIT: ['DetectionInit'] as const, QUICK_DETECTION_SWITCH_DETECT: ['NetworkScan'] as const, QUICK_DETECTION_SWITCH_CONFIG_READ: ['SwitchConfigRead', 'CommBoardInfoRead'] as const, QUICK_DETECTION_SYNC_DETECT: ['SyncDeviceCheck'] as const, QUICK_DETECTION_BARGRAPH_DETECT: ['BargraphNodeCheck'] as const, QUICK_DETECTION_SYNC_RECOVER: ['SyncStatusRecover'] as const, QUICK_DETECTION_COMPLETE: ['DetectionWrapup'] as const, // v1.6.1:Edge 代理层 4 个 step EDGE_PROXY: ['ValidateCommandFormat', 'CheckDeviceOnline', 'DispatchToDevice', 'AwaitDeviceResponse'] as const, // v1.8.2: 补 v1.5.0 ErrorPhase type 含 BATCH_EXECUTE+SYNC_EXPORT 但 map 漏的设计塌方残留 // 触发: 设备端 v1.8.1 staging Round 8' audit dashboard ⚠️ TOLERATED // (req_1778991911621_gjqlbi, BARGRAPH_MISALIGNMENT_READ_FAILED data.error.phase="BATCH_EXECUTE") // Round 9'' reply + Round 10'' ack 协商, 设备端 ErrorStep 5 候选全 ack (DataTransfer 必加 + 4 新 step) BATCH_EXECUTE: ['DataTransfer', 'SubItemTimeout', 'SubItemDeviceQuery'] as const, SYNC_EXPORT: ['ReadDayData', 'AggregateExport'] as const, } as const; export function isValidErrorStepForPhase(step: any, phase: any): boolean { if (typeof step !== 'string' || typeof phase !== 'string') return false; const allowed = ERROR_STEP_BY_PHASE[phase]; return Array.isArray(allowed) && (allowed as readonly string[]).includes(step); } // v1.8.3: 判定 wire data.code 是否携带 orchestrator phase 上下文. // // orchestrator-coded wire (返回 true): wire code 含 ErrorPhase enum 值前缀, error.phase/step 必填 // - BATCH_* → BATCH_EXECUTE phase // - PROGRAM_(INIT|FETCH|EXTRACT|...) → PROGRAM_* phase 8 阶段 // - QUICK_DETECTION_* → QUICK_DETECTION_* phase 7 阶段 (v1.9.0 单 prefix 统一) // - SYNC_MONITORING_TABLE / SYNC_EXPORT → SYNC_EXPORT phase // - EDGE_* → EDGE_PROXY phase (Edge 自合成 wire) // // v1.9.0: 由 7 prefix 降到 6 prefix — // · 删 BARGRAPH_CHECK_ONLINE_STATUS prefix (设备端 0 emit, 协议方误配 dead spec, Round 1-3 协商删除) // · detection 7 子族合并到单一 QUICK_DETECTION_ prefix (替代 v1.8.5 的 7 别名 regex) // // leaf 码 wire (返回 false): SYNC/RATER/BARGRAPH SIMPLE 命令失败 + Cured-with-leaf-wire, // wire code 无 orchestrator phase 语义, error.phase/step 必须缺失 (clean break, v1.8.3 onward) // // ⚠️ 红线: 任何 validator / 派生 / 路由 判据**必须**用此函数 (基于 wire data.code), // **禁止**用 command.commandType (BATCH 子项 commandType=SIMPLE 但 wire 是 BATCH_* 的陷阱). // // 与设备端 sg7-31-win C# helper IsOrchestratorCodedWire (commit F + v1.9.0 升级) 完全同源. // 加新 orchestrator phase 时**双方必须同步**加 prefix 判, 否则 Edge 与设备端兜底口径漂移会引入新 wart. // // 详见: claude-docs/discussions/protocol-team-reply-to-proposal-v1.8.3.md §3 + reply-2 §2 + // claude-docs/discussions/protocol-team-proposal-progressphase-detection-namespace-v1.9.0.md export function isOrchestratorCodedWire(wireCode: any): boolean { if (typeof wireCode !== 'string' || wireCode.length === 0) return false; // 1. BATCH 三态 + 子项 → BATCH_EXECUTE phase if (wireCode.startsWith('BATCH_')) return true; // 2. PROGRAM Upload 8 阶段 if (/^PROGRAM_(INIT|FETCH|EXTRACT|PREPROCESS|COMPILE|UPLOAD|STATS|COMPLETE)/.test(wireCode)) return true; // 3. QuickDetection 7 阶段 (v1.9.0 单 prefix — // QUICK_DETECTION_INIT / _SWITCH_DETECT / _SWITCH_CONFIG_READ / _SYNC_DETECT / // _BARGRAPH_DETECT / _SYNC_RECOVER / _COMPLETE) if (wireCode.startsWith('QUICK_DETECTION_')) return true; // 4. SYNC_MONITORING_TABLE / SYNC_EXPORT 系列 if (wireCode.startsWith('SYNC_MONITORING_TABLE') || wireCode.startsWith('SYNC_EXPORT')) return true; // 5. Edge 自合成 → EDGE_PROXY (v1.9.0 由 #6 升 #5, 原 #5 BARGRAPH_CHECK_ONLINE_STATUS 删除) if (wireCode.startsWith('EDGE_')) return true; return false; // leaf 码 (SYNC/RATER/BARGRAPH SIMPLE + Cured-with-leaf-wire) } // 消息工厂类 - 创建符合新协议的消息 export class MessageFactory { /** * 创建注册消息 (v1.12.0 discriminated union) * * DEVICE clientType: deviceFingerprint **必填** (runtime Edge 强制, schema 层 C 方案类型挡) * 非 DEVICE clientType: deviceFingerprint **禁带** (类型层 ?: never) */ static createRegisterMessage( clientId: string, clientType: ClientType.DEVICE, options: { deviceFingerprint: DeviceFingerprint; clientInfo?: ClientInfo; edgeInfo?: EdgeInfo; licenseToken?: string } ): DeviceRegisterMessage; static createRegisterMessage( clientId: string, clientType: Exclude, options?: { clientInfo?: ClientInfo; edgeInfo?: EdgeInfo; licenseToken?: string } ): NonDeviceRegisterMessage; static createRegisterMessage( clientId: string, clientType: ClientType, optionsOrClientInfo?: ClientInfo | { deviceFingerprint?: DeviceFingerprint; clientInfo?: ClientInfo; edgeInfo?: EdgeInfo; licenseToken?: string; } ): RegisterMessage { // 向后兼容: 第 3 参数若是 ClientInfo (v1.11 及更早调用方), 包成 options.clientInfo const options = optionsOrClientInfo && 'deviceFingerprint' in optionsOrClientInfo ? optionsOrClientInfo : optionsOrClientInfo && ('clientInfo' in optionsOrClientInfo || 'edgeInfo' in optionsOrClientInfo || 'licenseToken' in optionsOrClientInfo) ? optionsOrClientInfo : optionsOrClientInfo ? { clientInfo: optionsOrClientInfo as ClientInfo } : {}; if (clientType === ClientType.DEVICE) { const deviceOpts = options as { deviceFingerprint?: DeviceFingerprint; clientInfo?: ClientInfo; edgeInfo?: EdgeInfo; licenseToken?: string }; if (!deviceOpts.deviceFingerprint) { // runtime guard: DEVICE 必须带 deviceFingerprint (TypeScript overload 也已挡, 这是双保险) throw new Error('createRegisterMessage: DEVICE clientType requires deviceFingerprint (v1.12.0+)'); } return { type: MessageType.REGISTER, clientId, clientType: ClientType.DEVICE, deviceFingerprint: deviceOpts.deviceFingerprint, clientInfo: deviceOpts.clientInfo, edgeInfo: deviceOpts.edgeInfo, licenseToken: deviceOpts.licenseToken, timestamp: new Date().toISOString(), version: '1.0', }; } const nonDeviceOpts = options as { clientInfo?: ClientInfo; edgeInfo?: EdgeInfo; licenseToken?: string }; return { type: MessageType.REGISTER, clientId, clientType: clientType as Exclude, clientInfo: nonDeviceOpts.clientInfo, edgeInfo: nonDeviceOpts.edgeInfo, licenseToken: nonDeviceOpts.licenseToken, timestamp: new Date().toISOString(), version: '1.0', }; } /** * 创建命令消息 */ static createCommandMessage( requestRef: string, targetClientId: string, command: Command, callback: string, options?: { priority?: Priority; timeout?: number; retryCount?: number; } ): CommandMessage { return { type: MessageType.COMMAND, requestRef, targetClientId, command: { ...command, commandType: command.commandType || CommandType.SIMPLE }, priority: options?.priority || Priority.NORMAL, timeout: options?.timeout || DEFAULT_TIMEOUT, retryCount: options?.retryCount || 0, // 默认值 0 callback, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建心跳消息 */ static createHeartbeatMessage(clientId: string, sequence: number): HeartbeatMessage { const now = new Date().toISOString(); return { type: MessageType.HEARTBEAT, clientId, sequence, clientTime: now, timestamp: now, version: '1.0' }; } /** * 创建注销消息 */ static createUnregisterMessage(clientId: string, reason?: string): UnregisterMessage { return { type: MessageType.UNREGISTER, clientId, reason, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建注册挂起消息(Gateway → Edge/Backend) */ static createRegisterPendingMessage( requestId: string, pollInterval: number = 5, expiresIn: number = 86400 ): RegisterPendingMessage { return { type: MessageType.REGISTER_PENDING, requestId, pollInterval, expiresIn, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建授权通过消息(Gateway → Edge/Backend) */ static createAuthorizationGrantedMessage( requestId: string, licenseToken: string ): AuthorizationGrantedMessage { return { type: MessageType.AUTHORIZATION_GRANTED, requestId, licenseToken, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建授权拒绝消息(Gateway → Edge/Backend) */ static createAuthorizationRejectedMessage( requestId: string, reason?: string ): AuthorizationRejectedMessage { return { type: MessageType.AUTHORIZATION_REJECTED, requestId, reason, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建程序上传消息 */ static createProgramMessage( requestRef: string, targetClientId: string, parameters: ProgramParameters, callback: string, options?: { priority?: Priority; timeout?: number; } ): ProgramMessage { return { type: MessageType.PROGRAM, requestRef, targetClientId, command: { commandCode: 'UPLOAD_PROGRAM', parameters }, priority: options?.priority || Priority.NORMAL, timeout: options?.timeout || 1800000, // 30分钟 callback, timestamp: new Date().toISOString(), version: '1.0' }; } // ============================================================ // v1.7.0 BREAKING: 删除旧 createCommandResponseMessage / createProgramResponseMessage / // createProgressUpdateMessage 三个 helper。替代品是 MessageFactory.dispatchMessage(见下方) // —— status / level / 消息类型由 report.code 通过 CodeMeta 自动派生。 // ============================================================ /** * 创建错误消息 (Gateway & Backend 都需要) */ static createErrorMessage( code: string, message: string, requestRef?: string, options?: { level?: ReportLevel; data?: Record; severity?: string; category?: string; retryable?: boolean; } ): ErrorMessage { return { type: MessageType.ERROR, code, message, severity: options?.severity || 'ERROR', category: options?.category, context: { requestRef, data: options?.data }, retryable: options?.retryable, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建心跳确认消息 (Gateway 急需) */ static createHeartbeatAckMessage( sequence: number, clientId?: string, clientTime?: string, options?: { latency?: number; heartbeatReceivedTime?: number; // 收到心跳的时间戳(毫秒) } ): HeartbeatAckMessage { const now = new Date().toISOString(); const nowMs = Date.now(); // 如果提供了接收时间但没有 latency,自动计算处理延迟 const latency = options?.latency || (options?.heartbeatReceivedTime ? nowMs - options.heartbeatReceivedTime : undefined); return { type: MessageType.HEARTBEAT_ACK, clientId: clientId || '', sequence, clientTime: clientTime || now, // 使用传入的客户端时间或当前时间 serverTime: now, latency, timestamp: now, version: '1.0' }; } /** * 创建注册确认消息 (Gateway 需要) */ static createRegisterAckMessage( clientId: string, success: boolean, message?: string, sessionId?: string ): RegisterAckMessage { return { type: MessageType.REGISTER_ACK, clientId, success, sessionId, error: !success && message ? { code: 'REGISTRATION_FAILED', message } : undefined, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建注销确认消息 */ static createUnregisterAckMessage( clientId: string, success: boolean ): UnregisterAckMessage { return { type: MessageType.UNREGISTER_ACK, clientId, success, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建设备审批请求消息(Edge → Gateway) */ static createDeviceApprovalRequestMessage( edgeId: string, requestId: string, deviceId: string, deviceInfo?: ClientInfo, sourceIp?: string ): DeviceApprovalRequestMessage { return { type: MessageType.DEVICE_APPROVAL_REQUEST, edgeId, requestId, deviceId, deviceInfo, sourceIp, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建设备审批响应消息(Gateway → Edge) */ static createDeviceApprovalResponseMessage( requestId: string, deviceId: string, approved: boolean, deviceAccessKey?: string, reason?: string, action?: 'unblacklist' | 'revoke' ): DeviceApprovalResponseMessage { return { type: MessageType.DEVICE_APPROVAL_RESPONSE, requestId, deviceId, approved, deviceAccessKey, reason, action, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建路由更新消息 */ static createUpdateRoutesMessage( clientId: string, // 改为 clientId,统一命名 devices: string[] ): UpdateRoutesMessage { return { type: MessageType.UPDATE_ROUTES, clientId, // 使用 clientId devices, timestamp: new Date().toISOString(), version: '1.0' }; } /** * 创建路由更新确认消息 */ static createUpdateRoutesAckMessage( clientId: string, // 改为 clientId,统一命名 success: boolean, message?: string, routeCount?: number ): UpdateRoutesAckMessage { return { type: MessageType.UPDATE_ROUTES_ACK, clientId, // 使用 clientId success, message, routeCount, timestamp: new Date().toISOString(), version: '1.0' }; } /** * v1.6.0: 统一消息派发接口 * * 设备端发送终态消息(COMMAND_RESPONSE / PROGRAM_RESPONSE)或进度消息(PROGRESS_UPDATE)时, * 只需要提供 code 和业务字段,协议层通过 CodeMeta 自动推导 status / level / 消息类型。 * * 替代旧的 createCommandResponseMessage / createProgramResponseMessage / createProgressUpdateMessage。 * * @param params dispatch 参数 * @returns 完整的协议消息(具体类型由 code 派生的 isTerminal 决定) * @throws Error 当 code 要求 data.error 但 params.error 缺失时 */ static dispatchMessage(params: { clientId: string; requestRef: string; code: string; message: string; /** 终态消息:result 块;进度消息:业务平铺字段 */ data?: Record; /** v1.5.0 失败 schema — 当 code 要求时必填 */ error?: { phase: string; step: string; category: string; detail: string; }; /** 仅进度消息:phase 与 progress */ phase?: ProgressPhase | string; progress?: number; /** 进度消息 sourceType(默认 COMMAND)*/ sourceType?: ProgressSourceType; /** 终态消息 result 块(COMMAND_RESPONSE)*/ result?: any; /** 终态消息 context(PROGRAM_RESPONSE)*/ context?: ProgramContext; /** 终态消息 executionTime */ executionTime?: number; /** * v1.10.3: caller 可显式 override level (partial-failure 场景必传) * - 不传时: * - meta.level 是 string → 用 meta.level (向后兼容) * - meta.level 是 array → 用 array[0] 作为默认 (通常 'INFO') * - 传时: 必须命中 meta.level 接受值 (string 严格相等 / array includes) */ level?: ReportLevel; }): CommandResponseMessage | ProgramResponseMessage | ProgressUpdateMessage { const meta = resolveCodeMeta(params.code); // 强校验:requiresErrorBlock 时 data.error 必填 if (meta.requiresErrorBlock && !params.error) { throw new Error( `dispatchMessage: code "${params.code}" requires data.error (4 fields: phase/step/category/detail)`, ); } const reportData: Record = { ...(params.data || {}) }; if (params.error) { reportData.error = params.error; } // v1.10.3: level resolve — caller override 优先; 否则 array 取首元素, string 直接用 const resolvedLevel: ReportLevel = params.level ?? (Array.isArray(meta.level) ? meta.level[0] : meta.level as ReportLevel); const report: ReportMessage = { level: resolvedLevel, message: params.message, code: params.code, data: reportData, }; const timestamp = new Date().toISOString(); const version = '1.0'; if (meta.isTerminal) { // v1.8.5 spec invariant: 终态消息 status 必须 COMPLETED/FAILED/CANCELLED/TIMEOUT (不可 IN_PROGRESS). // helper 内部 runtime 守护 — CodeMeta 决策矩阵若返回非终态 status, 即 meta-data bug, 立刻 throw. const terminalStatus = meta.status as 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'TIMEOUT'; if (terminalStatus !== 'COMPLETED' && terminalStatus !== 'FAILED' && terminalStatus !== 'CANCELLED' && terminalStatus !== 'TIMEOUT') { throw new Error( `dispatchMessage v1.8.5 spec violation: code "${params.code}" isTerminal=true ` + `but meta.status="${meta.status}" is not a terminal value ` + `(must be 'COMPLETED' | 'FAILED' | 'CANCELLED' | 'TIMEOUT'). ` + `Fix CodeMeta SUFFIX_RULES for this code.`, ); } // 通过 code 前缀决定终态消息类型:PROGRAM_* → PROGRAM_RESPONSE,其余 → COMMAND_RESPONSE const isProgramTerminal = params.code.startsWith('PROGRAM_') && !params.code.startsWith('PROGRAM_FETCH') && !params.code.startsWith('PROGRAM_EXTRACT') && !params.code.startsWith('PROGRAM_PREPROCESS') && !params.code.startsWith('PROGRAM_COMPILE') && !params.code.startsWith('PROGRAM_STATS') && !params.code.startsWith('PROGRAM_INIT'); if (isProgramTerminal) { const msg: ProgramResponseMessage = { type: MessageType.PROGRAM_RESPONSE, clientId: params.clientId, requestRef: params.requestRef, status: terminalStatus, context: params.context, report, executionTime: params.executionTime, timestamp, version, }; return msg; } else { const msg: CommandResponseMessage = { type: MessageType.COMMAND_RESPONSE, clientId: params.clientId, requestRef: params.requestRef, status: terminalStatus, result: params.result, report, executionTime: params.executionTime, timestamp, version, }; return msg; } } // 进度消息 // v1.8.5 spec invariant: PROGRESS_UPDATE status 永远 'IN_PROGRESS' — helper 内强制写死, // 不依赖 meta.status (CodeMeta _PROGRESS/_PHASE_START 等后缀虽然也派生 IN_PROGRESS, // 但 helper 自己守护 invariant 更安全). const progressMsg: ProgressUpdateMessage = { type: MessageType.PROGRESS_UPDATE, clientId: params.clientId, requestRef: params.requestRef, status: 'IN_PROGRESS', phase: params.phase ?? '', progress: params.progress ?? 0, sourceType: params.sourceType ?? 'COMMAND', context: params.context, report, timestamp, version, }; return progressMsg; } } // 导出所有类型 export type AnyMessage = | RegisterMessage | RegisterAckMessage | UnregisterMessage | UnregisterAckMessage | CommandMessage | CommandResponseMessage | ProgramMessage | ProgramResponseMessage | HeartbeatMessage | HeartbeatAckMessage | ProgressUpdateMessage | UpdateRoutesMessage | UpdateRoutesAckMessage | RegisterPendingMessage | AuthorizationGrantedMessage | AuthorizationRejectedMessage | DeviceApprovalRequestMessage | DeviceApprovalResponseMessage | AclInvalidatedMessage | ErrorMessage; // 导出常量 /** * 协议 wire schema 版本号(与 package.json.version 解耦演进) * * Semantic versioning of the wire protocol schema: * - Major/minor bump: BREAKING wire change(e.g. v1.6.0 删 createCommandResponse) * - Patch bump on schema: 仅当 schema 行为有 backward-compat 增强时 * - package.json.version 可独立 patch(不动 PROTOCOL_VERSION)— 例如 v1.7.3 仅 * publish-time fix 无 wire 变化,PROTOCOL_VERSION 保持 '1.7.2' * * v1.7.3 patch: 修正 v1.7.2 publish-time 漏改(原值 '1.7.1'),同时申明 schema 版本 * 与 package 版本可解耦的设计意图。详见 README.md > Versioning Policy + CHANGELOG v1.7.3。 * * v1.7.4 patch: schema 真有变化(EXPLICIT_META rename + add + OperationType 缩 + messageEn 删) * 所以 schema 版本同步升 '1.7.3',package 版本 1.7.4。 * * v1.7.5 patch: schema BREAKING wire rename — ProgramParameters.programNumber / ProgramContext.programNumber * → programNo(对齐设备端 wire emit + 与 Snowflake programId 区分)。Phase 3.5 Category C escalation 仲裁结果。 * schema 版本 1.7.3 → 1.7.4,package 版本 1.7.4 → 1.7.5。 * * v1.7.6 patch: SUFFIX_RULES 新增 _READ_SUCCESS / _WRITE_SUCCESS 终态规则。 * 修复 v1.7.0–v1.7.5 协议大坑:SIMPLE 命令 (SYNC_FUNCTIONS_SWITCH / SYNC_PROGRAM_CONTROL / * DEVICE_DATETIME_INFORMATION / 等几十个) 的 _READ_SUCCESS / _WRITE_SUCCESS 终态响应被 * 误匹配 _SUCCESS 规则降为 IN_PROGRESS → Edge 协议校验拒收 → 30s 假 timeout。 * v1.7.5 wire 联调 req_1778811285514_oan6qa 实证暴露。 * schema 版本 1.7.4 → 1.7.5,package 版本 1.7.5 → 1.7.6。 * * v1.7.7 patch: SUFFIX_RULES 对称侧补完 — v1.7.6 只修了 SUCCESS 侧,留下 FAILED + BATCH 漏: * - _READ_FAILED / _WRITE_FAILED (SIMPLE 命令失败终态对称) * - _ALL_SUCCESS / _PARTIAL_SUCCESS / _ALL_FAILED (BATCH 三态终态) * 触发:v1.7.6 wire 联调 req_1778824929909_jlvome (BARGRAPH_MISALIGNMENT_READ_FAILED) * schema 版本 1.7.5 → 1.7.6,package 版本 1.7.6 → 1.7.7。 * * v1.7.8 patch: SUFFIX_RULES 第 3 轮对称补完 — v1.7.7 漏 ECAN 广播 WRITE 终态: * - _BROADCAST_SUCCESS / _BROADCAST_FAILED (5 个光柱命令的 WRITE 广播终态) * 触发:v1.7.7 wire 联调 req_1778824943659_4gaxtv (BARGRAPH_MISALIGNMENT_WRITE_BROADCAST_SUCCESS) * 流程改进:本版严守 Principle 5(Round 2 reply → 设备端 Round 5 final ack → Phase 4 implement) * 配套工具卡:scripts/check-consumer-ack.sh + escape-hatch-audit.jsonl 进 prepublishOnly hook * schema 版本 1.7.6 → 1.7.7,package 版本 1.7.7 → 1.7.8。 * * v1.8.2 patch: ERROR_STEP_BY_PHASE map drift 修复 — 补 v1.5.0 残留塌方: * 触发: 设备端 v1.8.1 staging Round 8' audit dashboard ⚠️ TOLERATED * (req_1778991911621_gjqlbi, BARGRAPH_MISALIGNMENT_READ_FAILED * data.error.phase="BATCH_EXECUTE" Edge validator 报"不在白名单内") * 根因: ErrorPhase type (line 506-524) 含 BATCH_EXECUTE + SYNC_EXPORT, * 但 ERROR_STEP_BY_PHASE map (line 770-789) 漏这 2 key * Edge VALID_ERROR_PHASES 派生自 map, 也缺 * 修复 (additive, 0 break): * - ErrorStep type + 4 候选 step (SubItemTimeout/SubItemDeviceQuery/ReadDayData/AggregateExport) * - ERROR_STEP_BY_PHASE map 补 BATCH_EXECUTE + SYNC_EXPORT 2 key * 设备端影响: 0 改动 (设备端已 emit BATCH_EXECUTE/DataTransfer, 修后 Edge STRICT 通过) * 设计协商: Round 9'' reply + Round 10'' ack (transition mode 第 2 次) * schema 版本 1.8.1 → 1.8.2, package 版本 1.8.1 → 1.8.2 * * v1.8.1 patch: envelope 重构 — 删 ProgressUpdateMessage.metaCompliance + 加 EdgeForwardEnvelope: * 触发: 设备端 Round 8 audit P1 finding — Edge mutate device wire payload 注入 metaCompliance, * dashboard 展开 payload 误判 "私自加字段" * 设计纠正: Edge↔Gateway forward 改为 envelope 包装, device payload immutable * - ProgressUpdateMessage.metaCompliance 字段删除 (设备端从不 emit, 0 影响) * - 加 EdgeForwardEnvelope { edgePayload, edgeAnnotation: { metaCompliance, validationFailures?, edgeReceivedAt } } * - 加 isEdgeForwardEnvelope() 类型守卫 (Gateway forward-compat 老 Edge raw message) * 设备端影响: 0 改动 (wire / C# / catalog 全 0 改) — 字段本来设备端就不发 * 第三方影响: 0 grep 验证 (全 repo 仅协议方 own Edge/Gateway 用 metaCompliance) * schema 版本 1.8.0 → 1.8.1, package 版本 1.8.0 → 1.8.1 * 设计协商: Round 9 protocol-team-reply-to-consumer-audit-v1.8.0.md → Round 10 device-team-final-ack-v1.8.0.md * * v1.8.0 minor: resolveCodeMeta(code, ctx?) 升维 — 上下文派生 (向后兼容, ctx=undefined 等同 v1.7.8): * - CodeMetaContext interface: messageType / parentCommandType / phase / sourceType / subCommandType * - applyContextRules() R1-R7 派生规则 (PROGRESS_UPDATE 终态降级 + BATCH/COMPLEX 子项 + EDGE_CACHE invariant + 命名空间合规) * - SUFFIX_RULES +2: _STEP_OK / _STEP_FAIL (BATCH/COMPLEX 子项进度显式后缀, Q9) * - EXPLICIT_META QUICK_DETECTION_ALL_OFFLINE: status COMPLETED → FAILED (Q5 语义打架修) * - ProgressUpdateMessage.metaCompliance?: 'STRICT'|'TOLERATED' (Edge 添加标记) * - CLI tool: npx @thejrsoft/subway-protocol resolve / --batch / --diff (Q6 + 4 项增强) * 触发:v1.7.x cycle 多轮 META 命名空间冲突 + 真实测试环境 17 条拒收数据归因 * 设计协商:upgrade-guide-v1.8.0.md (Round 0) → 12 题 4 轮 (Round 1-4) 全 ack 0 反驳 * schema 版本 1.7.7 → 1.8.0,package 版本 1.7.8 → 1.8.0。 * * 防再犯:本常量被 scripts/check-protocol-version.js 在 prepublishOnly hook 中校验。 */ export const PROTOCOL_VERSION = '1.12.0'; export const DEFAULT_TIMEOUT = 10000; export const DEFAULT_PRIORITY = Priority.NORMAL; // 导出强类型命令系统 export * from './command-types'; export * from './command-factory'; // 导出消息验证器 export { MessageValidator, ValidationResult } from './message-validator'; // 导出协议工具类 export { ProtocolUtils } from './protocol-utils'; // 导出 Gateway 扩展 (暂时禁用,有编译错误) // export * from './gateway-extensions'; // 导出 Edge 代理模式 (暂时禁用,有编译错误) // export * from './edge-proxy';