ForcePilot/backend/package/yuxi/channels/adapters/yuanbao/proto_codec.py
Kris eb25707668 feat(yuanbao): 新增元宝渠道适配器完整实现
新增元宝(Yuanbao)渠道的完整适配器实现,包含以下核心模块:
- 基础适配器与导出入口
- 协议编解码与WebSocket帧处理
- 会话管理与路由逻辑
- 事件队列与出站消息队列
- 消息格式转换与发送重试
- 安全审计与权限校验
- 配置映射与账户管理
- 视觉分析与工具函数
- 文档生成与设置向导
2026-05-12 00:52:20 +08:00

149 lines
4.7 KiB
Python

from __future__ import annotations
import struct
from yuxi.utils.logging_config import logger
try:
from google.protobuf import json_format
from google.protobuf.message import Message as PbMessage
HAS_PROTOBUF = True
except ImportError:
HAS_PROTOBUF = False
class ProtoCodec:
MAGIC_BYTE = 0xFE
VERSION = 1
MSG_TYPE_BIZ = 1
MSG_TYPE_CONN = 2
MSG_TYPE_HEARTBEAT = 3
MSG_TYPE_AUTH = 4
@staticmethod
def is_protobuf(data: bytes) -> bool:
return len(data) > 0 and data[0] == ProtoCodec.MAGIC_BYTE
@staticmethod
def encode_biz(payload: dict) -> bytes:
body = ProtoCodec._encode_json(payload)
return ProtoCodec._build_frame(ProtoCodec.MSG_TYPE_BIZ, body)
@staticmethod
def encode_conn(payload: dict) -> bytes:
body = ProtoCodec._encode_json(payload)
return ProtoCodec._build_frame(ProtoCodec.MSG_TYPE_CONN, body)
@staticmethod
def encode_heartbeat() -> bytes:
return ProtoCodec._build_frame(ProtoCodec.MSG_TYPE_HEARTBEAT, b"")
@staticmethod
def encode_auth(token: str) -> bytes:
body = ProtoCodec._encode_json({"token": token})
return ProtoCodec._build_frame(ProtoCodec.MSG_TYPE_AUTH, body)
@staticmethod
def decode(data: bytes) -> dict | None:
if len(data) < 5:
return None
try:
magic = data[0]
if magic != ProtoCodec.MAGIC_BYTE:
return None
version = data[1]
msg_type = data[2]
length = struct.unpack(">H", data[3:5])[0]
body = data[5 : 5 + length]
return {
"version": version,
"msg_type": ProtoCodec._msg_type_name(msg_type),
"payload": ProtoCodec._decode_json(body),
}
except Exception as e:
logger.warning(f"[ProtoCodec] Decode error: {e}")
return None
@staticmethod
def encode_pb(message: PbMessage) -> bytes:
if not HAS_PROTOBUF:
raise ImportError("google.protobuf is not installed")
body = message.SerializeToString()
return ProtoCodec._build_frame(ProtoCodec.MSG_TYPE_BIZ, body)
@staticmethod
def decode_pb(data: bytes, message_class: type[PbMessage]) -> PbMessage | None:
if not HAS_PROTOBUF:
raise ImportError("google.protobuf is not installed")
decoded = ProtoCodec.decode(data)
if decoded is None:
return None
payload = decoded.get("payload", {})
try:
return json_format.ParseDict(payload, message_class())
except Exception as e:
logger.warning(f"[ProtoCodec] PB decode error: {e}")
return None
@staticmethod
def pb_to_frame(message: PbMessage, msg_type: int | None = None) -> bytes:
if not HAS_PROTOBUF:
raise ImportError("google.protobuf is not installed")
msg_type = msg_type or ProtoCodec.MSG_TYPE_BIZ
body = message.SerializeToString()
return ProtoCodec._build_frame(msg_type, body)
@staticmethod
def frame_to_pb(data: bytes, message_class: type[PbMessage]) -> PbMessage | None:
if not HAS_PROTOBUF:
raise ImportError("google.protobuf is not installed")
if len(data) < 5:
return None
try:
magic = data[0]
if magic != ProtoCodec.MAGIC_BYTE:
return None
length = struct.unpack(">H", data[3:5])[0]
body = data[5 : 5 + length]
message = message_class()
message.ParseFromString(body)
return message
except Exception as e:
logger.warning(f"[ProtoCodec] Frame-to-PB decode error: {e}")
return None
@staticmethod
def _build_frame(msg_type: int, body: bytes) -> bytes:
if len(body) > 65535:
raise ValueError(f"Body too large: {len(body)} bytes (max 65535)")
length = struct.pack(">H", len(body))
return bytes([ProtoCodec.MAGIC_BYTE, ProtoCodec.VERSION, msg_type]) + length + body
@staticmethod
def _encode_json(data: dict) -> bytes:
import json
return json.dumps(data, ensure_ascii=False).encode("utf-8")
@staticmethod
def _decode_json(data: bytes) -> dict:
import json
return json.loads(data.decode("utf-8"))
@staticmethod
def _msg_type_name(msg_type: int) -> str:
names = {
ProtoCodec.MSG_TYPE_BIZ: "biz",
ProtoCodec.MSG_TYPE_CONN: "conn",
ProtoCodec.MSG_TYPE_HEARTBEAT: "heartbeat",
ProtoCodec.MSG_TYPE_AUTH: "auth",
}
return names.get(msg_type, "unknown")
def is_protobuf_message(data: bytes) -> bool:
return ProtoCodec.is_protobuf(data)