一、起点:一个看似简单的需求
事情的开头很朴素:我希望让运行在 OpenClaw 上的 AI Agent(代号 ClawBot,我管它叫"龙虾")能直接把写好的文章推进我公众号的草稿箱里。不用登录后台、不用复制粘贴,一条指令下去,草稿就在那儿等着我审。
需求本身不复杂。微信官方有草稿箱接口cgi-bin/draft/add,有素材上传接口,有获取 token 的接口。按文档走,一小会儿应该就能搞定。
结果从上午 10:44 到下午 15:05,整整花了四个多小时。中间换了四个方向的修复,每一个都以为是终点,结果都不是。
以下的复盘内容,就是这四个坑的实战记录。
二、第一个坑:旧端点的误导性错误码
最初的publish.sh用的是微信最经典的 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,返回:
{"access_token":"...","expires_in":7200}Token 长度 137,拿它去调draft/count立刻成功。
结论很清楚:secret 没错,是旧端点在作妖。
微信官方文档其实早有说明:stable_token与cgi-bin/token互相隔离,推荐使用前者替代后者。旧端点对某些 AppID 会返回误导性的40125,而真正的凭证验证在新端点上是完全正常的。
这是第一个坑:你以为是密钥错了,其实是端点变了。
三、第二个坑:force_refresh 的隐性互斥
把publish.sh的 token 端点迁到stable_token后,我以为通了一半。结果发布依然失败:
40001: invalid credential, access_token is invalid or not latest40001是 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 自己在跟自己打架。
四、第三个坑:被 shell 吞掉的那个变量
到这一步,stable_token端点正常,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 正常返回。
这是第三个坑:命令报告成功,不代表数据真的写进去了。中间层吞了你的变量,你连尸体都找不到。
五、第四个坑:三方争抢同一张票
到这里,如果你以为问题全解决了,那还太早。
发布成功后,我想用draft/batchget核验一下草稿是否真的入箱。结果又报40001。
同样的 token,几分钟前还在用,现在就失效了。
原因是在整个系统里,有三个组件都在独立获取 token:
- 我手动
curl调试时取的 token; publish.sh内部每次运行都取一次 token;wenyan自己持有一份导入的 token(虽然expireAt: -1已禁用自管,但它仍然拿着旧的那份)。
任何一方取了新 token,微信就会判定上一个失效。结果就是:每个组件单独测都通,凑在一起随机40001。
这跟微信官方推荐的做法直接冲突。官方文档明确写过:
建议开发者使用中控服务器统一获取和刷新 access_token,其他业务逻辑服务器所使用的 access_token 均来自于该中控服务器,不应该各自去刷新,否则容易造成冲突。
解决思路只有一个:确立唯一持票人。
最终架构定为:
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,是你取太多次了。
六、最终架构与验收
凭证唯一真源
~/.openclaw/.env是唯一的凭证来源:
- 优先读
WECHAT_APP_ID/WECHAT_APP_SECRET; - 兼容旧变量名
WECHAT_SECRET,但命中时输出 deprecation 提示; - 废弃
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-09-29》
- 草稿箱状态:已收到
- 发布退出码:0
- 唯一持票人架构:已固化进
publish.sh - 日志归档:
skills/wechat-publisher/reports/2026-09-29-selfcheck/
七、实战检验:鲁迅风亲子稿
链路打通后,我做了一次真实测试:让 ClawBot 用鲁迅的文笔,写一篇关于"女儿掉门牙、英语发音漏风"的文章,不少于 700 字,直接推到草稿箱。
它写出来的题目是《缺了门牙的日子,是时间漏出的光》。主线是"掉门牙 → 英语发音漏风 → 全家和师生的笑 → 新牙长出的怅惘",把孩子的成长写成"嘴里开着的一扇小窗",结尾落在"漏风的年月,反成了心上最完好的一颗牙"。
文章入箱,后台确认通过。这是公众号里第一篇由 AI 协助完成的鲁迅风亲子稿。
技术上的意义是:链路不只是能跑通 demo,它更能承接真实的写作任务,并产出可以人工验收的成品。
八、可复用的经验
如果只记五条:
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}"一行命令,能省掉两小时。
九、结尾
这次排障最值得记录的一点,不是某个错误码怎么处理,而是错误码会撒谎。
40125说是密钥错,其实端点变了。40001说是 token 失效,其实是force_refresh在互斥。wenyan token -i说导入成功,其实写了个空值。
每一层都给你一个看似合理的解释,让你沿着错误的方向修下去。唯一能做的,是每一层都回到最原始的验证:这个 token 真的能用吗?这个变量真的有值吗?这个命令真的写进去了吗?与其抱怨,不如养成一个习惯:怀疑每一个报错的字面意思,验证每一个中间态。
技术排障写到这里——通过实战验证获得的那篇《缺了门牙的日子,是时间漏出的光》公众号文章,能落到一个七岁孩子漏风的笑声里,也算是诠释了技术进步的别样浪漫。