技术支持 wechatapi.net
最近在把微信接到 OpenClaw,想做一个真正能在微信里使用的 Agent 入口。
这件事看起来好像只是:
收消息 -> 调 OpenClaw -> 发回去但实际做起来,坑比想象中多得多。
下面我把目前踩过的 7 个最典型的坑总结一下,给后面要做类似事情的人少踩一些雷。
坑 1:回调打通不等于功能可用
这是最容易误判的地方。
很多人只要看到:
/wechat/callback能收到 POST- 消息能打印出来
- 日志里有内容
就以为这件事差不多完成了。
实际上,回调打通只代表:
入口层能接收到数据。
而真正决定系统是否可用的,是:
- 回调解析对不对
- 群消息识别对不对
- 自己发的消息有没有过滤
- session 是否合理
- 并发模型会不会乱
也就是说,“能收到消息”和“这个系统能用”,中间还隔着很多细节。
坑 2:微信群消息不能按私聊逻辑来理解
这类接口里,群消息最容易写错的地方,是把“群 ID”和“真实发送人 ID”混在一起。
我一开始也踩了这个坑,后来才按回调结构拆清楚。
正确判断群消息的逻辑类似这样:
is_group=from_user.endswith("@chatroom")orto_user.endswith("@chatroom")而真实发送人有时候还要从内容里拆:
ifis_groupandraw_contentand":\n"inraw_content:possible_sender,possible_text=raw_content.split(":\n",1)ifpossible_sender.startswith("wxid_"):sender_wxid=possible_sender actual_text=possible_text.strip()如果这块一开始没处理好,后面:
- 白名单判断会乱
- 群上下文会乱
- 机器人会把群 ID 当成人 ID
- session 会错得很离谱
坑 3:不忽略自己发的消息,机器人会自回环
这件事看起来小,但真的很致命。
如果你不做这层判断,机器人就会这样:
- 收到用户消息
- 调 OpenClaw 得到回复
- 发回微信
- 又收到自己刚刚发出去的回复
- 再把这条回复当成新消息送给 OpenClaw
最后形成一个非常难排查的自回环。
所以我后面强制加了这一步:
is_self=bool(wxidandfrom_user==wxid)ifparsed.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所以最后我把它收敛成更保守的格式,只保留字母、数字、下划线、短横线:
defbuild_session_id(chat_id:str,sender_wxid:str,is_group:bool,config:dict)->str:defnorm(s:str)->str:returnre.sub(r"[^a-zA-Z0-9_-]","_",str(sor"").strip())ifnotis_group:returnf"wechat_dm_{norm(chat_id)}"ifconfig["GROUP_SESSION_MODE"]=="per_user":returnf"wechat_group_{norm(chat_id)}_user_{norm(sender_wxid)}"returnf"wechat_group_{norm(chat_id)}"这一步虽然小,但能省掉很多奇怪的会话问题。
坑 6:不要一开始就接所有消息类型
微信消息类型很多:
- 文本
- 图片
- 语音
- 文件
- 名片
- 引用
- 小程序
- 系统通知
- 撤回
- 拍一拍
如果你一上来就想全吃,复杂度会直接爆炸。
我后来很快就收敛到一个策略:
第一阶段只处理文本消息。
代码里也直接限制:
ifmsg_type!=1:logger.info("📭 暂时只处理文本消息,已忽略 MsgType=%s",msg_type)return{"status":"ignored_msg_type"}这么做的好处很明显:
- 回路先跑稳
- 日志更好看
- 逻辑更容易定位
- 后续再逐步放开图片、文件等能力
坑 7:如果没有并发设计,群一热闹就堵住
只用一个全局队列 + 一个 worker,是很多 demo 项目的默认写法。
但只要一进群聊场景,它的问题就会立刻暴露:
- 所有会话都串行
- 一个慢请求拖垮后面所有消息
- 用户体感会非常差
所以我后来改成了:
- 不同 session 可以并发
- 同一个 session 固定路由到同一个 worker
- 保证顺序的同时提升吞吐
核心逻辑:
defshard_index_for_session(session_id:str,worker_count:int)->int:h=int(hashlib.md5(session_id.encode("utf-8")).hexdigest(),16)returnh%worker_count然后:
worker_queues[shard_idx].put_nowait(task)这一步非常重要,它本质上决定了这个系统是“玩具”,还是“入口网关”。
我后来做了哪些修正
把这些坑踩完以后,我后面做了几件真正有价值的改动:
1. 首次运行改成命令行初始化
不让用户先打开配置文件手改。
2. 自动生成带注释配置文件
方便后面自己继续改参数。
3. 加白名单
私聊不是谁都能直接触发。
4. 加群触发词
避免群里所有消息都打进来。
5. 默认关闭“正在处理中”
不然体验很像机器人在刷屏。
6. 失败提示收口
不要把 OpenClaw 的底层报错直接吐给最终用户。
我现在的判断
把微信接到 OpenClaw,绝对不是不值得做。
但它真正难的地方,从来不是“把接口接通”,而是:
把这条链路做到像一个产品,而不是一个脚本。
如果你现在也在做类似事情,我最想给的建议是:
不要一开始就追求“全功能”,先把最小稳定闭环做出来。
只要这个闭环稳定了,后面的能力都可以继续叠加。
最后
我现在更愿意把这件事看成:
- OpenClaw 负责能力层
- 微信负责入口层
- 网关负责把两者真正接起来
而不是简单地理解成“消息转发脚本”。
如果你也在做这一类方向,欢迎交流。