技术支持 wechatapi.net

做微信机器人这件事,最容易低估的往往不是“接回调”,而是“回调到底该怎么正确解释”。

在这里插入图片描述

我最近把微信接到了 OpenClaw,上线前我以为最麻烦的是:

  • 回调地址配置
  • token 校验
  • 发消息接口

真正做起来以后才发现,最容易踩坑的地方其实是:

  • 这是不是自己发的消息
  • 这是不是群消息
  • 群里真正发消息的人到底是谁

如果这三件事处理不对,后面不管你接什么大模型,逻辑都会不稳定。

很多人一开始会写成这样

raw_content = data["Data"]["Content"]["string"]
from_user = data["Data"]["FromUserName"]["string"]

text = raw_content.strip()
sender = from_user

看起来没问题,但在群聊场景下,这种写法经常会直接把群 ID 当成发送人

为什么会这样

根据实际回调文档,微信群消息并不是一个“字段天然就分好了”的结构。

你至少要分清楚这几个角色:

  • Wxid:归属微信,也就是当前登录账号
  • Data.FromUserName.string:消息发送方
  • Data.ToUserName.string:消息接收方
  • Data.Content.string:消息内容
  • Data.MsgType:消息类型

重点在这里:

判断是不是自己发的

文档给得很清楚:

可通过消息发送人 $.Data.FromUserName.string 与所属微信 $.Wxid 是否一致进行判断。

所以判断自己发送应该是:

is_self = bool(wxid and from_user == wxid)

判断是不是群消息

文档里也明确区分了两种情况:

  • 别人发群:FromUserName.string@chatroom 结尾
  • 自己发群:ToUserName.string@chatroom 结尾

所以不能只看一个字段。

is_group = from_user.endswith("@chatroom") or to_user.endswith("@chatroom")

判断群里真正是谁发的

这一步最关键。

群聊里,真正的发言人经常藏在 Content.string 前半段:

wxid_xxx:
消息内容

所以还要再拆一次。

我后来用的是这套解析方式

def parse_wechat_payload(data):
    wxid = str(data.get("Wxid", "") or "").strip()
    msg_data = data.get("Data", {}) or {}

    from_user = (msg_data.get("FromUserName", {}) or {}).get("string", "")
    to_user = (msg_data.get("ToUserName", {}) or {}).get("string", "")
    raw_content = (msg_data.get("Content", {}) or {}).get("string", "")
    msg_type = msg_data.get("MsgType")

    is_self = bool(wxid and from_user == wxid)
    is_group = from_user.endswith("@chatroom") or to_user.endswith("@chatroom")

    if from_user.endswith("@chatroom"):
        chat_id = from_user
    elif to_user.endswith("@chatroom"):
        chat_id = to_user
    else:
        chat_id = from_user if not is_self else to_user

    sender_wxid = from_user
    actual_text = (raw_content or "").strip()

    if is_group and raw_content and ":\n" in raw_content:
        possible_sender, possible_text = raw_content.split(":\n", 1)
        possible_sender = possible_sender.strip()
        if possible_sender.startswith("wxid_") or possible_sender.startswith("v1_") or possible_sender.startswith("gh_"):
            sender_wxid = possible_sender
            actual_text = possible_text.strip()

    return {
        "wxid": wxid,
        "msg_type": msg_type,
        "chat_id": chat_id,
        "sender_wxid": sender_wxid,
        "actual_text": actual_text,
        "is_group": is_group,
        "is_self": is_self,
    }

这段逻辑看起来比简单脚本复杂不少,但它的好处是:

  • 私聊和群聊都能统一得到 chat_id
  • 群里真正说话的人能拆出来
  • 机器人自己发出去的消息能直接过滤
  • 后面 session 设计会清晰很多

自发消息不过滤,后果会很严重

这一点一定要单独说。

如果不忽略自己发出的消息,就很容易形成:

  1. 机器人收到用户消息
  2. 生成回复
  3. 把回复发到微信
  4. 微信又把这条回复回调回来
  5. 程序把自己的消息再送进 AI
  6. 最后循环

所以我后面在入口层直接做了:

if parsed.get("is_self"):
    logger.info("忽略自己发送的消息: %s", parsed.get("msg_id"))
    return {"status": "ignored_self"}

这是一个很小的判断,但实际价值非常大。

群聊和私聊的 session 也不能一样

如果回调解析错了,session 就一定跟着错。

我现在的 session 规则是这样的:

私聊

wechat_dm_{chat_id}

群共享上下文

wechat_group_{chat_id}

群成员独立上下文

wechat_group_{chat_id}_user_{sender_wxid}

代码长这样:

def build_session_id(chat_id: str, sender_wxid: str, is_group: bool, config: dict) -> str:
    def norm(s: str) -> str:
        return re.sub(r"[^a-zA-Z0-9_-]", "_", str(s or "").strip())

    if not is_group:
        return f"wechat_dm_{norm(chat_id)}"

    if config["GROUP_SESSION_MODE"] == "per_user":
        return f"wechat_group_{norm(chat_id)}_user_{norm(sender_wxid)}"

    return f"wechat_group_{norm(chat_id)}"

如果 chat_idsender_wxid 本身就没判断对,后面整个上下文系统都会出问题。

为什么我说“回调解析不是难点,判断才是难点”

因为接一个 URL 并不难。
真正难的是:

  • 正确理解每个字段的含义
  • 不凭想当然做映射
  • 在群消息、自发消息、系统消息之间区分清楚

尤其微信这种场景,很多时候“发送人”和“会话所在位置”不是一个概念。

我最后的建议

如果你也在做微信机器人,不管底层是 OpenClaw、LangChain、Dify 还是自己写的 Agent,都建议先把这三件事做扎实:

  1. 自发消息识别
  2. 群消息识别
  3. 群内真实 sender 识别

这三步做对了,后面的 AI 接入才不会乱。

一句话总结:

回调地址只是入口,字段判断才是系统稳定性的底座。

Logo

AtomGit 是由开放原子开源基金会联合 CSDN 等生态伙伴共同推出的新一代开源与人工智能协作平台。平台坚持“开放、中立、公益”的理念,把代码托管、模型共享、数据集托管、智能体开发体验和算力服务整合在一起,为开发者提供从开发、训练到部署的一站式体验。

更多推荐