""" TAF/WUP 帧解码器 — 将二进制帧转为可读摘要,用于日志输出 """ from typing import Any, cast from .taf_protocol import TafInputStream, TafType from .wup_protocol import normalize_wup_payload # cmd 编号 → 名称 CMD_NAMES = { 0x03: "RPC_REQ", 0x04: "RPC_RSP", 0x0A: "AUTH", 0x0B: "PUSH1", 0x10: "HB_SEND", 0x11: "HB_RECV", 0x17: "CONFIRM", 0x18: "PUSH2", 0x21: "REGISTER", 0x22: "CONFIRM_RSP", } # TAIL_BYTES = 2c36004c5c6600 _TAIL = bytes.fromhex("2c36004c5c6600") def _strip_tail(body: bytes) -> bytes: """裁掉 body 末尾的 TAIL_BYTES""" if body.endswith(_TAIL): return body[: -len(_TAIL)] # 有时 TAIL 前还有 0c (ZERO tag) if ( len(body) > 1 and body[-len(_TAIL) - 1 : -len(_TAIL)] == b"\x0c" and body.endswith(_TAIL) ): return body[: -len(_TAIL) - 1] return body def _decode_taf_value(ins: TafInputStream, dtype: int, depth: int = 0) -> object: """递归解码单个 TAF 值""" if dtype == TafType.ZERO: return 0 if dtype in (TafType.INT8, TafType.INT16, TafType.INT32, TafType.INT64): return ins._read_int_value(dtype) if dtype in (TafType.FLOAT, TafType.DOUBLE): import struct as _s if dtype == TafType.FLOAT: return round(_s.unpack(">f", ins.buf.read(4))[0], 4) return round(_s.unpack(">d", ins.buf.read(8))[0], 6) if dtype == TafType.STRING1: ln = ins.buf.read(1)[0] return ins.buf.read(ln).decode("utf-8", errors="replace") if dtype == TafType.STRING4: import struct as _s ln = _s.unpack(">I", ins.buf.read(4))[0] return ins.buf.read(ln).decode("utf-8", errors="replace") if dtype == TafType.MAP: cnt = ins._read_int_len() m = {} for _ in range(cnt): _, kt = ins.read_head() k = _decode_taf_value(ins, kt, depth + 1) _, vt = ins.read_head() v = _decode_taf_value(ins, vt, depth + 1) m[k] = v return m if dtype == TafType.LIST: cnt = ins._read_int_len() items = [] for _ in range(cnt): _, it = ins.read_head() items.append(_decode_taf_value(ins, it, depth + 1)) return items if dtype == TafType.SIMPLE_LIST: ins.read_head() # element type INT8 ln = ins._read_int_len() return f"" if dtype == TafType.STRUCT_BEGIN: return _decode_taf_struct(ins, depth + 1) return f"<0x{dtype:02x}>" def _decode_taf_struct(ins: TafInputStream, depth: int = 0) -> dict: """解码一个 TAF 结构体 (已消费 STRUCT_BEGIN) → 有序字典""" fields = {} while True: try: tag, dtype = ins.peek_head() except EOFError: break if dtype == TafType.STRUCT_END: ins.read_head() break ins.read_head() # 限制递归深度 if depth < 5: try: val = _decode_taf_value(ins, dtype, depth) except Exception: val = f"" else: val = "<...>" try: ins.skip_field(dtype) except Exception: break key = f"tag{tag}" if key in fields: key = f"{key}_2" fields[key] = val return fields def _extract_wup(body: bytes) -> bytes: """从 WSS 响应 body 中提取干净的 WUP 包""" if len(body) < 5: return body # 大包格式: [1B prefix][4B wup_len][wup_body][tail] # prefix 可能是 0x00,必须优先于 4B total_len 判断。 wup_len = int.from_bytes(body[1:5], "big") if 5 + wup_len <= len(body) and body[5:7] == b"\x10\x03": return body[5 : 5 + wup_len] total_len = int.from_bytes(body[0:4], "big") if 8 <= total_len <= len(body) and body[4:6] == b"\x10\x03": return body[:total_len] if 5 + wup_len <= len(body): return body[5 : 5 + wup_len] return body def _decode_wup_body(body: bytes) -> dict: """解码 WUP 封装 (请求/响应通用) → {servant, func, data}""" result = {} wup = normalize_wup_payload(_extract_wup(body)) # 手动解析 WUP,WSS 大帧常在 tag10 后追加校验/尾部字节 try: ins = TafInputStream(wup) # 读 WUP 字段 tag1~tag10,读到 tag10 后停止(忽略尾部垃圾) sBuffer = b"" while True: try: tag, dtype = ins.peek_head() except EOFError: break # tag10 是最后一个合法 WUP 字段 if tag > 10: break ins.read_head() if tag == 1 or tag in (2, 3) or tag == 4: if dtype != TafType.ZERO: ins._read_int_value(dtype) elif tag == 5: result["servant"] = _decode_taf_value(ins, dtype) elif tag == 6: result["func"] = _decode_taf_value(ins, dtype) elif tag == 7: if dtype == TafType.SIMPLE_LIST: ins.read_head() ln = ins._read_int_len() sBuffer = ins.buf.read(ln) else: ins.skip_field(dtype) elif tag == 8: if dtype != TafType.ZERO: ins._read_int_value(dtype) elif tag in (9, 10): ins.skip_field(dtype) if tag == 10: break else: ins.skip_field(dtype) # 解析 sBuffer → newdata MAP if sBuffer: try: sins = TafInputStream(sBuffer) stag, sdt = sins.read_head() if stag == 0 and sdt == TafType.MAP: cnt = sins._read_int_len() for _ in range(cnt): _, kt = sins.read_head() k = _decode_taf_value(sins, kt) _, vt = sins.read_head() if vt == TafType.SIMPLE_LIST: sins.read_head() vln = sins._read_int_len() v = sins.buf.read(vln) if v: try: tins = TafInputStream(v) ttag, tdt = tins.peek_head() if tdt == TafType.STRUCT_BEGIN: tins.read_head() result[k] = _decode_taf_struct(tins) else: result[k] = f"<{len(v)}B>" except Exception: result[k] = f"<{len(v)}B>" else: result[k] = _decode_taf_value(sins, vt) except Exception: pass except Exception as e: result["err"] = str(e) return result def _truncate(obj, max_str=80): """截断过长的值,使日志紧凑""" if isinstance(obj, str): if len(obj) > max_str: return obj[:max_str] + f"...({len(obj)})" return obj if isinstance(obj, dict): return {k: _truncate(v, max_str) for k, v in obj.items()} if isinstance(obj, list): return [_truncate(v, max_str) for v in obj[:5]] return obj def _fmt_fields(fields: dict) -> str: """把字段字典格式化为一行摘要""" parts = [] for k, v in fields.items(): if isinstance(v, dict): parts.append(f"{k}={{...}}") elif isinstance(v, list): parts.append(f"{k}=[{len(v)}项]") else: sv = str(v) if len(sv) > 60: sv = sv[:60] + "..." parts.append(f"{k}={sv}") return ", ".join(parts) def format_wss_log(body: bytes, cmd: int, seq: int, direction: str) -> str: """格式化单条 WSS 帧的日志行 Args: body: 帧体 (不含 6B 头) cmd: 命令字 seq: 序列号 direction: "发" 或 "收" Returns: 可读日志字符串 """ prefix = f"[{direction}]" # 心跳精简 if cmd == 0x10: return f"{prefix} HB →" if cmd == 0x11: return f"{prefix} HB ←" # RPC 请求/响应 if cmd in (0x03, 0x04): decoded = _decode_wup_body(body) svc = decoded.get("servant", "") func = decoded.get("func", "") err = decoded.get("err") if err: return f"{prefix} RPC ERR: {err}" label = f"{svc}.{func}" if svc else "RPC" # 找 tRsp/tResp/tReq for key in ("tRsp", "tResp", "tReq"): data = decoded.get(key) if isinstance(data, dict): return f"{prefix} {label} {_fmt_fields(_truncate(data))}" return f"{prefix} {label}" # AUTH if cmd == 0x0A: text = _strip_tail(body).decode("utf-8", errors="replace") return f"{prefix} AUTH {text[:100]}{'...' if len(text) > 100 else ''}" # REGISTER / CONFIRM / PUSH 等 clean = _strip_tail(body) try: ins = TafInputStream(clean) # 看第一个 head tag, dtype = ins.peek_head() if dtype == TafType.STRUCT_BEGIN: ins.read_head() fields = _decode_taf_struct(ins) elif dtype == TafType.MAP: ins.read_head() cnt = ins._read_int_len() fields = {} for _ in range(cnt): _, kt = ins.read_head() k = _decode_taf_value(ins, kt) _, vt = ins.read_head() v = _decode_taf_value(ins, vt) fields[k] = v elif dtype == TafType.LIST: ins.read_head() cnt = ins._read_int_len() items = [] for _ in range(cnt): _, it = ins.read_head() items.append(_decode_taf_value(ins, it)) fields = {"items": items} else: fields = {} cmd_name = CMD_NAMES.get(cmd, f"0x{cmd:02x}") if fields: return f"{prefix} {cmd_name} {_fmt_fields(cast(dict[str, Any], _truncate(fields)))}" return f"{prefix} {cmd_name}" except Exception: cmd_name = CMD_NAMES.get(cmd, f"0x{cmd:02x}") return f"{prefix} {cmd_name} ({len(body)}B)"