把微信接到 OpenClaw,我踩过的 7 个坑

26 阅读6分钟

技术支持 wechatapi.net

ddc6f24821de97d8a3c30bf6739e93a9.jpg 最近在把微信接到 OpenClaw,想做一个真正能在微信里使用的 Agent 入口。
这件事看起来好像只是:

收消息 -> 调 OpenClaw -> 发回去

但实际做起来,坑比想象中多得多。
下面我把目前踩过的 7 个最典型的坑总结一下,给后面要做类似事情的人少踩一些雷。

坑 1:回调打通不等于功能可用

这是最容易误判的地方。

很多人只要看到:

  • /wechat/callback 能收到 POST
  • 消息能打印出来
  • 日志里有内容

就以为这件事差不多完成了。

实际上,回调打通只代表:

入口层能接收到数据。

而真正决定系统是否可用的,是:

  • 回调解析对不对
  • 群消息识别对不对
  • 自己发的消息有没有过滤
  • session 是否合理
  • 并发模型会不会乱

也就是说,“能收到消息”和“这个系统能用”,中间还隔着很多细节。

坑 2:微信群消息不能按私聊逻辑来理解

这类接口里,群消息最容易写错的地方,是把“群 ID”和“真实发送人 ID”混在一起。

我一开始也踩了这个坑,后来才按回调结构拆清楚。

正确判断群消息的逻辑类似这样:

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

而真实发送人有时候还要从内容里拆:

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

如果这块一开始没处理好,后面:

  • 白名单判断会乱
  • 群上下文会乱
  • 机器人会把群 ID 当成人 ID
  • session 会错得很离谱

坑 3:不忽略自己发的消息,机器人会自回环

这件事看起来小,但真的很致命。

如果你不做这层判断,机器人就会这样:

  1. 收到用户消息
  2. 调 OpenClaw 得到回复
  3. 发回微信
  4. 又收到自己刚刚发出去的回复
  5. 再把这条回复当成新消息送给 OpenClaw

最后形成一个非常难排查的自回环。

所以我后面强制加了这一步:

is_self = bool(wxid and from_user == wxid)

if parsed.get("is_self"):
    return {"status": "ignored_self"}

这一步不做,后面一切都不稳。

坑 4:只要底层还是 CLI,每条消息都会有冷启动成本

我一开始的调用方式很直白:

cmd = [self.bin, "agent", "--session-id", sid, "--message", message.strip()]
res = subprocess.run(
    cmd,
    capture_output=True,
    text=True,
    stdin=subprocess.DEVNULL,
    timeout=self.chat_timeout
)

逻辑没问题,但体感问题非常明显。
因为它意味着每条消息都在做这些事情:

  • 起一个新进程
  • 恢复 session
  • 加载 provider
  • 初始化环境
  • 请求模型
  • 退出

这就是为什么很多时候:

  • OpenClaw 前端 10 秒左右能出结果
  • 接到微信里,可能就拉到 20~30 秒甚至更久

所以如果你也想做这种接入,最好先接受一个事实:

微信这层可以优化,但只要底层还是 OpenClaw CLI 单次调用,延迟就很难像原生前端那样顺滑。

坑 5:session_id 不是随便拼就行

我一开始为了看着直观,session_id 设计成这种:

wechat:dm:wxid_xxx
wechat:group:chatroom_xxx
wechat:group:chatroom_xxx:user:wxid_yyy

结果后面很快就踩到兼容性问题,有些场景下会报:

Invalid session ID

所以最后我把它收敛成更保守的格式,只保留字母、数字、下划线、短横线:

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)}"

这一步虽然小,但能省掉很多奇怪的会话问题。

坑 6:不要一开始就接所有消息类型

微信消息类型很多:

  • 文本
  • 图片
  • 语音
  • 文件
  • 名片
  • 引用
  • 小程序
  • 系统通知
  • 撤回
  • 拍一拍

如果你一上来就想全吃,复杂度会直接爆炸。

我后来很快就收敛到一个策略:

第一阶段只处理文本消息。

代码里也直接限制:

if msg_type != 1:
    logger.info("📭 暂时只处理文本消息,已忽略 MsgType=%s", msg_type)
    return {"status": "ignored_msg_type"}

这么做的好处很明显:

  • 回路先跑稳
  • 日志更好看
  • 逻辑更容易定位
  • 后续再逐步放开图片、文件等能力

坑 7:如果没有并发设计,群一热闹就堵住

只用一个全局队列 + 一个 worker,是很多 demo 项目的默认写法。

但只要一进群聊场景,它的问题就会立刻暴露:

  • 所有会话都串行
  • 一个慢请求拖垮后面所有消息
  • 用户体感会非常差

所以我后来改成了:

  • 不同 session 可以并发
  • 同一个 session 固定路由到同一个 worker
  • 保证顺序的同时提升吞吐

核心逻辑:

def shard_index_for_session(session_id: str, worker_count: int) -> int:
    h = int(hashlib.md5(session_id.encode("utf-8")).hexdigest(), 16)
    return h % worker_count

然后:

worker_queues[shard_idx].put_nowait(task)

这一步非常重要,它本质上决定了这个系统是“玩具”,还是“入口网关”。

我后来做了哪些修正

把这些坑踩完以后,我后面做了几件真正有价值的改动:

1. 首次运行改成命令行初始化

不让用户先打开配置文件手改。

2. 自动生成带注释配置文件

方便后面自己继续改参数。

3. 加白名单

私聊不是谁都能直接触发。

4. 加群触发词

避免群里所有消息都打进来。

5. 默认关闭“正在处理中”

不然体验很像机器人在刷屏。

6. 失败提示收口

不要把 OpenClaw 的底层报错直接吐给最终用户。

我现在的判断

把微信接到 OpenClaw,绝对不是不值得做。
但它真正难的地方,从来不是“把接口接通”,而是:

把这条链路做到像一个产品,而不是一个脚本。

如果你现在也在做类似事情,我最想给的建议是:

不要一开始就追求“全功能”,先把最小稳定闭环做出来。

只要这个闭环稳定了,后面的能力都可以继续叠加。

最后

我现在更愿意把这件事看成:

  • OpenClaw 负责能力层
  • 微信负责入口层
  • 网关负责把两者真正接起来

而不是简单地理解成“消息转发脚本”。

如果你也在做这一类方向,欢迎交流。