打通公众号草稿箱——一次踩遍微信 token、端点与 shell 变量的坑
1. 起点:一个看似简单的需求
需求很朴素:让我的 AI 助手(代号 ClawBot)直接把写好的文章推进公众号草稿箱。不用登录后台、不用复制粘贴,一条指令下去,草稿就在那儿等着审。
微信官方有草稿箱接口,有素材上传接口,有获取 token 的接口。按文档指引走,我认为一会儿的功夫应该就能搞定。结果从上午 10:44 到下午 15:05,花了四个多小时,换了三个修复方向,每一个都以为终点,结果都不是,详情如下。
2. 第一层坑:旧端点的误导性错误码
最初用最经典的 token 获取方式:
GET https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=xxx&secret=xxx
返回:
{"errcode":40125,"errmsg":"invalid appsecret rid: ..."}
40125 意为 AppSecret 无效。于是检查 secret。把 .env、TOOLS.md、gateway.systemd.env 以及一个旧版本残留的 secret,一共四个候选值全部测了一遍。四个全部报 40125。四个不同来源的密钥全部无效,这本身就反常——要么四个都错了(不太可能),要么问题不在密钥。
换了微信后来推出的新端点:
POST https://api.weixin.qq.com/cgi-bin/stable_token
Body: {"grant_type":"client_credential","appid":"xxx","secret":"xxx","force_refresh":false}
同一个 secret,返回正常 token,长度 137。拿它去调 draft/count 立刻成功。
结论:secret 没错,是旧端点在作妖。 微信官方文档早有说明:stable_token 与 cgi-bin/token 互相隔离,推荐使用前者替代后者。旧端点对某些 AppID 会返回误导性的 40125。
这是第一层坑:你以为是密钥错了,其实是端点变了。
3. 第二层坑:force_refresh 的隐性互斥
把 token 端点迁到 stable_token 后,发布依然失败:
40001: invalid credential, access_token is invalid or not latest
40001 是 token 失效或不是最新的。但 token 明明是刚取回来的。一度判断“stable_token 返回的 token 立即被判 invalid”,这个判断后来被推翻。
真正原因藏在调用参数里:当时用了 force_refresh: true。微信官方对 stable_token 的参数有明确说明:
普通模式(
force_refresh: false),access_token 有效期内重复调用不会更新;强制刷新模式(force_refresh: true),会导致上次获取的 access_token 失效,并返回新的。
当时在多个终端、多个脚本里调用 token 接口,有的用了 true,有的用了 false。每一次 true 都在作废前一个 token,导致所有持有旧 token 的调用瞬间 40001。
改成 force_refresh: false 后,token 立刻稳定可用,连续两次 draft/count 都返回 total_count。
这是第二层坑:参数选错,token 自己在跟自己打架。
4. 第三层坑:被 shell 吞掉的那个变量
到这一步,端点正常,token 获取正常,只剩最后一步:把 token 交给 wenyan-cli,让它去发布。
wenyan-cli 有导入外部 token 的命令:
wenyan token -i --app-id "$WECHAT_APP_ID" --token "$token"
运行后,命令报告“导入成功”。但发布依然 40001。去看 ~/.config/wenyan-md/token.json,发现里面存的是:
{
"appid": "wx...",
"accessToken": "***",
"expireAt": -1
}
accessToken 字段的长度是 3。导入进去的是一个空占位符,真正的 token 值根本没写进去。
问题出在命令本身。当时脚本里的写法是:
wenyan token -i --app-id "$WECHAT_APP_ID" --token "***"
这个 *** 不是占位符——它是被 shell 处理后的真实结果。某个环节(可能是多层变量嵌套、可能是工具链对 $(...) 的改写)把 token 变量的值吞掉了,传给 wenyan 的实际上是一个空字符串,或者 3 个字符的垃圾值。
改成双引号包裹的明确变量后:
wenyan token -i --app-id "$WECHAT_APP_ID" --token "$token"
token.json 里立刻写入了完整的 137 位 token,发布成功,Media ID 正常返回。
这是第三层坑:命令报告成功,不代表数据真的写进去了。中间层吞了你的变量,你连尸体都找不到。
5. 第四层坑:三方争抢同一张票
发布成功后,想用 draft/batchget 核验草稿是否真的入箱。结果又报 40001。同样的 token,几分钟前还在用,现在就失效了。
原因是在整个系统里,有三个组件都在独立获取 token:
- 手动
curl调试时取的 token; publish.sh内部每次运行都取一次 token;wenyan自己持有一份导入的 token。
任何一方取了新 token,微信就会判定上一个失效。结果就是:每个组件单独测都通,凑在一起随机 40001。
解决思路只有一个:确立唯一持票人。
最终架构定为:
publish.sh是唯一取 token 的组件;- 它取到 token 后导入
wenyan; - 其他所有核验、调试操作,一律从
wenyan的token.json里读取,不再独立取 token; - 脚本内部增加 token 复用探测:如果
token.json里的 token 长度大于 100,先拿它调一次draft/count,通过就复用,不通过才走stable_token刷新。
改完之后,串行验证一次通过:publish.sh 发布验收稿,紧接着从 token.json 读 token 做 batchget 核验,没有互斥,草稿准确入库。
这是第四层坑:不是你取不到 token,是你取太多次了。
6. 最终架构与验收
凭证唯一真源
~/.openclaw/.env 是唯一的凭证来源,优先读 WECHAT_APP_ID / WECHAT_APP_SECRET,兼容旧变量名 WECHAT_SECRET,废弃 TOOLS.md 中的旧密钥。
token 流程(唯一持票人)
- 先读
~/.config/wenyan-md/token.json中的accessToken; - 长度大于 100 时,用它调
draft/count探测; - 探测通过则复用,跳过取 token;
- 无效或缺失时,才走
stable_token(POST + JSON +force_refresh: false); - 用
wenyan token -i --app-id "$APP_ID" --token "$token"导入; - 导入后校验
token.json中accessToken长度应为 137。
最终验收
- 草稿标题:《【验收】微信公众号草稿箱打通验证 2026-10-05》
- 草稿箱状态:已收到
- 发布退出码:0
- 唯一持票人架构:已固化进
publish.sh
7. 可复用的经验
cgi-bin/token正在被弃用,尽快迁移到stable_token。 两者互相隔离,不要混用。stable_token普通模式用force_refresh: false。 强制刷新会立即作废旧 token。- access_token 必须单持票人。 多个组件各自刷新 token,必然互斥。
wenyan token -i导入后,立即验证token.json中accessToken的长度。 应为 137,不是 3。- 调试时先确认变量本身有值,再查下游。
echo "len=${#token}"一行命令,能省掉两小时。
8. 结尾
这次排障最值得记录的一点,不是某个错误码怎么处理,而是错误码会撒谎。
40125 说是密钥错,其实端点变了。40001 说是 token 失效,其实是 force_refresh 在互斥。wenyan token -i 说导入成功,其实写了个空值。
所以每一层都给了你一个看似合理的解释,让你沿着错误的方向修下去。但在修改无效的情况下,我们唯一能做的便是回到最原始的验证结论,并重新提出质疑:这个 token 真的能用吗?这个变量真的有值吗?这个命令真的写进去了吗?
怀疑每一个报错的字面意思,验证每一个中间状态。这就是这次踩坑最大的收获。
DAMO开发者矩阵,由阿里巴巴达摩院和中国互联网协会联合发起,致力于探讨最前沿的技术趋势与应用成果,搭建高质量的交流与分享平台,推动技术创新与产业应用链接,围绕“人工智能与新型计算”构建开放共享的开发者生态。
更多推荐

所有评论(0)