""" 企微 XML/字段解析 → InboundMessage。 本文件职责:从解密后的明文抽出 sender_id / message_id / content。 禁止:无 sender_id 时仍组装「可入业务」消息(调用方应丢弃);禁止再请求企微 userid。 """ from __future__ import annotations import xml.etree.ElementTree as ET from typing import Mapping from agent.channel.wecom.models import InboundMessage def _text(root: ET.Element, tag: str) -> str: node = root.find(tag) if node is None or node.text is None: return "" return node.text.strip() def parse_plain_xml(xml_text: str, *, default_agent_id: str = "") -> InboundMessage: """ 解析企微回调明文 XML。 调用时机:验签解密成功后、入 inbox 前。 返回:InboundMessage;MsgType 非 text 时 content 可能为空,由 handler 决定是否忽略。 """ root = ET.fromstring(xml_text) msg_type = _text(root, "MsgType") or "unknown" sender_id = _text(root, "FromUserName") msg_id = _text(root, "MsgId") or _text(root, "MsgID") create_time_raw = _text(root, "CreateTime") create_time = int(create_time_raw) if create_time_raw.isdigit() else 0 agent_id = _text(root, "AgentID") or _text(root, "AgentId") or default_agent_id content = _text(root, "Content") if msg_type == "text" else "" # 事件类可带 Event Key;骨架阶段仅记 raw,不进 Graph event = _text(root, "Event") if not msg_id: # 无 MsgId(如部分事件)时用发送人+时间拼幂等键,避免空键撞车 msg_id = f"{sender_id}:{create_time}:{msg_type}:{event}" chat_type = "single" chat_id = "" # 企业微信群聊应用消息偶发带 ChatId;无则保持私聊语义 chat_id = _text(root, "ChatId") or _text(root, "ChatID") if chat_id: chat_type = "group" return InboundMessage( sender_id=sender_id, message_id=msg_id, content=content, msg_type=msg_type, chat_id=chat_id, chat_type=chat_type, agent_id=agent_id, create_time=create_time, media={}, raw={"event": event, "xml": xml_text}, ) def inbound_from_inject(payload: Mapping[str, object], *, default_agent_id: str = "") -> InboundMessage: """ 本机联调注入:不经企微密文,直接构造 InboundMessage。 用途:并存期公网回调仍进现网时,用 POST /internal/dev/inject-text 测出站回复。 约束:必须带 sender_id(企微 userid),否则不得入队。 """ sender_id = str(payload.get("sender_id") or "").strip() content = str(payload.get("content") or "").strip() message_id = str(payload.get("message_id") or "").strip() if not message_id: message_id = f"inject:{sender_id}:{int(payload.get('create_time') or 0)}:{hash(content) & 0xFFFFFFFF}" return InboundMessage( sender_id=sender_id, message_id=message_id, content=content, msg_type="text", chat_type="single", agent_id=str(payload.get("agent_id") or default_agent_id), create_time=int(payload.get("create_time") or 0), raw={"source": "dev_inject"}, )