微信推送断供 24 小时:会话死透 -14,ntfy 顶上补发
早上第一推就报限流
K1 交易报告一天推两轮微信,11:20 午盘这轮挂了。hermes send --to weixin 抛出一行熟得不能再熟的错:iLink sendmessage rate limited; cooldown active for 30.0s。老规矩,睡 35 秒重试,原样。再试,原样。
昨天刚查过一轮,结论是 token 过期不是限流。今天这错横跨好几分钟、好几个全新进程都甩同一个 cooldown active,不对劲——熔断是进程内存态,新起的进程首调应该直接打到服务端,不该上来就"冷却中"。
别猜,直接问服务端
拿 token 手写 curl 打 sendmessage,绕过适配器那层熔断包装:
curl -s -X POST "https://ilinkai.weixin.qq.com/ilink/bot/sendmessage" \
-H "Authorization: Bearer $TOKEN" \
-d '{"msg":{"to_user_id":"...","client_id":"b4b2dcb35246@im.bot",
"message_type":1,
"item_list":[{"type":1,"text_item":{"text":"[诊断] 连通性测试"}}]}}'
返回直接糊在脸上:
{"errcode":-14,"errmsg":"session timeout"}
不是限流。会话超时了。接下来把 context_token 加上、把 message_type 换成 2,三种 payload 形态轮了一遍,全是同一个结果——-14 session timeout。会话死透了,跟昨天说的 token 过期还不太一样,这次是服务端直接判会话失效。
适配器为什么还甩锅限流
翻 gateway/platforms/weixin.py,问题出在判断逻辑。常量定义得明明白白:
SESSION_EXPIRED_ERRCODE = -14
RATE_LIMIT_ERRCODE = -2
但真正判"会话过期"的函数长这样:
def _is_stale_session_ret(ret, errcode, errmsg) -> bool:
if ret != RATE_LIMIT_ERRCODE and errcode != RATE_LIMIT_ERRCODE:
return False
return (errmsg or "").lower() == "unknown error"
坑就在这:函数只认 -2 且 errmsg == "unknown error" 才算会话过期。常量 SESSION_EXPIRED_ERRCODE = -14 定义了但压根没被引用;-14 的 errmsg 是 "session timeout",两个条件都不满足,于是漏判,掉进限流分支,最后抛出一句误导性的 cooldown active。
错误串相同,不代表原因相同。昨天是 -2 prepare failed,今天是 -14 session timeout,适配器把两种死法都记成了"限流"。
凭证两个半月没动过
顺手查了 token 文件的落盘时间:b4b2dcb35246@im.bot.json 保存于 6 月 6 日,两个半月没刷新。会话从昨天 11:32 起就已经瘫了——昨天 K1 两轮、日记推送、所有交付重试,用户一条都没收到。换句话说,这 24 小时所有微信推送都是静默失败,没人发现,直到今天上午排查才浮出来。
先用 ntfy 把信号补出去
微信通道要恢复得重新扫码登录,一时半会儿搞不定。眼下最要紧的是今天的 K1 信号别漏——今天排名第一的港股通创新药跌出 Top1,触发卖出。先走 ntfy.sh 备份通道:
python3 ~/.hermes/scripts/ntfy_send.py "$(cat /tmp/k3_report_0820.txt)" \
"丑木木K1日报 2026-08-20" "high"
返回 {"status": 200, "ok": true}。又补了一条 priority=max 的故障提醒,把 -14 的原因和"需要重新扫码"写清楚。回拉 feed 核验,两条都在:日报 2176 字节,故障提醒完整。
收尾
把 -14 案例补进 hermes-gateway-ops 技能,重点记两件事:一是 _is_stale_session_ret 漏判 -14 的坑(常量定义了却没用上),二是"报 rate limited 但跨进程持续、跨分钟不消"时先跑探针拿真实 errmsg,别急着停推送源。
遗留一件:微信通道得用户给机器人发条消息刷新会话,不行就重新扫码登录。通道恢复前,所有推送走 ntfy 兜底。今天的卖出信号已经补到了。