目 录CONTENT

文章目录

微信 Bot 的 session 为什么会挂:iLink 的生命周期问题

过客
2026-11-15 / 0 评论 / 1 点赞 / 5 阅读 / 0 字

3 月 22 日,微信官方公众号宣布个人微信对 AI Agent 开放灰度测试后,像OpenClaw、Hermes Agent等AI智能体连接微信已经成常态了,之前也有些消息也会让Hermes发到微信上来,这几天发现微信收不到消息了。

检查发现,是​收不到回复​,不是收不到消息——用户发过来的消息入站一直是正常的,日志里每条入站都能看到。挂的是出站。

日志里的报错长这样:

iLink sendmessage session not ready: ret=-2

这个报错很含糊,只告诉你「session 没准备好」,但没说为什么没准备好、怎么恢复。踩这个坑的人大概率会先怀疑网络、怀疑 token 过期、怀疑账号被限流,其实都不是。

要理解这个报错,得先看 iLink 的通信模型。它不是普通微信号的「全程在线」思路,而是腾讯给 bot 身份设计的一套 CGI 接口:

  • 入站:bot 主动调 getUpdates 长轮询,服务端有新消息就返回
  • 出站:bot 主动调 sendmessage,但每条入站消息都会带一个 context_token,出站必须原样带上这个 token

也就是说,微信服务端不是「你连上就能发消息」,而是「你发消息时必须证明你手里有一条待回复的对话上下文」。context_token 就是那个上下文凭证。

看插件源码 src/api/types.ts,每条 WeixinMessage 都带 context_token,出站 SendMessageReq 里也强制写 context_token: contextToken ?? undefined。没有这个字段,服务端直接拒绝。

那 ret=-2 到底是什么

插件里有个 session-guard.ts,定义了 SESSION_EXPIRED_ERRCODE = -14,注释写着「Error code returned by the server when the bot session has expired」。但实际报错是 ret=-2,不是 -14。

这两个码的区别很关键:

  • -14:明确的 session 过期,插件会调 pauseSession() 把账号暂停 1 小时,等用户主动发消息再恢复
  • -2:sendmessage session not ready,​session 还没建立​,而不是过期

也就是说,token 还在,账号没被封,纯粹是当前没有可用的上下文可以发消息。

为什么会「session not ready」

我把源码翻了一遍,contextToken 的生命周期是这样的:

  1. 用户给 bot 发一条消息 → 服务端在消息里带 context_token
  2. 插件从消息里取出 token,存进内存的 contextTokenStore,同时持久化到磁盘 {accountId}.context-tokens.json
  3. 之后 bot 出站回复时,从 store 里取出这个 token 带上,服务端认了才让发

问题出在:​这个 token 是「会话级」的,不是「永久」的​。

  • Gateway 重启:内存里的 store 被清空,虽然磁盘上有持久化文件,但 restoreContextTokens() 只在 startAccount 时恢复一次。如果恢复时服务端那边 token 已经过期,就会拿到一个失效的 token
  • DNS 抖动 / IDC 切换:iLink 的 base URL 可能会被服务端要求重定向(scaned_but_redirect),token 跟 session 绑定,session 跟着机房走,换机房等于换 session
  • 长时间没人说话:服务端那边的会话上下文是有 TTL 的,没人说话就自然过期

一旦 token 失效,sendmessage 就报 ret=-2。插件里 session-guard.ts 的 assertSessionActive() 只在 pauseSession() 被调用后才拦截,而 ret=-2 走的是 apiPostFetch 里 HTTP 200 但 body 里 ret≠0 的分支——​插件目前没有对 ret=-2 做自动重连,也没有自动重新拉取 token 的逻辑​。

唯一的恢复方式

用户先发一条消息。

这不是 workaround,这是 iLink 的设计——用户发消息,服务端生成新的 context_token,插件收到后存下来,bot 就能出站了。所以你会看到一个反直觉的现象:​bot 失声之后,你发一句「在吗」,bot 立刻就能回了​。

我 11 月 15 日验证过:微信通道连续挂了一周,15 号从微信发消息给Hermes后,出站立刻恢复。

插件里已经做了什么防护

翻 monitor/monitor.ts 能看到几个防护机制,说明作者知道这个坑:

  • ​session-guard 暂停机制​:ret=-14 时暂停账号 1 小时,避免无效请求打爆服务端
  • ​contextToken 磁盘持久化​:inbound.ts 里 persistContextTokens() 把 token 写到 {accountId}.context-tokens.json,restoreContextTokens() 在 Gateway 启动时恢复
  • ​sync_buf 持久化​:storage/sync-buf.ts 把 getUpdates 的 get_updates_buf 存盘,重启后能从断点续上,不丢消息
  • ​失败退避​:连续 3 次失败退避 30 秒,避免雪崩

这些都是针对「入站」的防护。​出站的 ret=-2 没有自动恢复​,插件文档里也没写清楚这一点。

怎么办

短期:接受它。微信通道定位成「备用渠道」,主力推送走 飞书。让 bot 失声一周没问题,反正用户发消息就会自动激活。

中期:在 sendWeixinOutbound 里加一层 ret=-2 处理——遇到 session not ready 时,把消息暂存到一个 outbox,等下次入站消息带着新 token 进来时再补发。插件作者没做这件事,估计是因为「用户发消息就会激活」这个机制已经够用了。

长期:如果真的要求 7×24 出站稳定,别用 iLink bot,用企业微信或者普通微信号接入。iLink 这套模型从设计上就是「用户主动触发」优先的,不是给「bot 随时推送」用的。

说说我的用法

现在这套系统里,飞书 是主力通道,微信只做备用。每日简报、周总结等一些 cron 推送,飞书 发一份、微信发一份。微信挂了顶多备用通道失效,不影响主流程。

真正要记住的是:​iLink 的 contextToken 是消耗品,不是通行证​。看到 session not ready 别慌,先让用户发条消息,比什么排查都管用。这个报错从 11 月 7 号拖到 15 号才定位,原因就是一开始往「token 过期」「账号异常」的方向查,没想到去翻插件源码里 session-guard.ts 那段注释。教训很简单:​报错含糊的时候,源码比猜测靠谱​。

所以微信的iLink bot是要你先主动,你长时间不理,AI Agent的消息会推送不到微信上来。

1

评论区