OpenClaw 微信天气定时推送踩坑复盘

这次排障花时间的地方,不在天气数据本身,而在 Cron 执行模型、消息投递链路,以及微信会话参数 这三层之间的错位。最终跑通后回头看,问题并不复杂,但中间有几个非常容易误判的坑,值得完整记一遍。


一、问题现象

从周三到今天,天气早报和晚报在系统里看起来都还在正常运行:

  • Cron 任务存在
  • 调度时间正常
  • 运行状态显示 ok
  • 甚至部分记录里还能看到“已投递”

但用户在微信里实际上并没有收到对应的天气消息。

这类问题最容易让人先怀疑两件事:

  1. 天气数据源挂了
  2. OpenClaw 的定时任务没跑

但这次都不是。


二、先确认:不是“任务没跑”,而是“跑了但没真正到用户”

第一轮排查,先看 Cron 自身状态和运行记录。

结果很明确:

  • 早报、晚报任务都存在
  • 周三到今天的运行记录都在
  • 任务执行本身没有报错

所以这一步可以先下结论:

问题不在任务触发,而在任务触发后,消息到底有没有真正送达到当前微信会话。

这个判断非常关键,因为它直接决定后面该排查的是:

  • 调度问题
  • 还是投递链路问题

三、第一处坑:isolated + announce 看起来成功,但用户未必看得到

原来的天气任务使用的是 isolated + announce 方式。

它的逻辑大致是:

  1. Cron 在隔离会话里跑一个 agent turn
  2. 任务完成后通过 announce 投递摘要
  3. 这条链路不等同于普通 message 工具直发

这套方式在本次环境下出现了一个很典型的问题:

  • 系统侧显示 delivered=true
  • 但用户微信侧并没有稳定看到消息

也就是说:

系统意义上的“已投递”,并不等于用户终端一定“已看到”。

这就是这次最容易让人误判的地方。

如果只看运行记录,很容易得出“任务没问题”的结论;但从用户视角看,消息就是没有真正到达。


四、第二处坑:不能把旧任务直接从 isolated/agentTurn 改成 main/systemEvent

后面尝试把旧任务直接从:

  • isolated + agentTurn

迁移到:

  • main + systemEvent

结果发现:openclaw cron edit 不适合做这种原地跨模型迁移。

原因不在于 CLI 完全不支持修改,而在于它会对每一步变更做一致性校验:

  • 先改 systemEvent,当前任务仍然是 isolated,过不了
  • 先改 session=main,当前 payload 还是 agentTurn,也过不了

所以中间态永远不合法。

这一步最后确认了一个迁移原则:

已有任务跨执行模型迁移时,不要原地 edit。正确方式是:新建 → 验证 → 再删旧。

这个原则不是形式主义,而是能直接避免线上链路半改半废。


五、第三处坑:main + systemEvent 也不等于“会自动发到当前微信会话”

为了避开旧任务原地迁移的问题,又新建了两条 main + systemEvent 的测试任务。

从思路上看,这条链路似乎更接近主会话正常回复,理论上应该更稳。

但实测结果是:

  • Cron run 状态是 ok
  • 运行记录里显示 deliveryStatus = not-requested
  • 用户微信里仍然没有收到天气消息

这说明一件很重要的事:

main + systemEvent 的作用,更接近“向主会话注入事件”,而不是“自动把结果发回当前微信会话”。

所以在这个天气推送场景里,这条路并不是最终方案。


六、真正的落点:不是方向错,而是 message 参数写错了

回头再看 isolated 任务里“自己发消息”这条路线,最终发现真正的问题不是方案方向,而是 message 工具参数写错了

一开始在任务 prompt 里约束 AI 调用 message 时,用的是:

target=...

但这次微信链路里,真正稳定的写法应当是:

action=send
channel=openclaw-weixin
to=o9cq802JOkcdc-Hk4N6ipf_H0iYk@im.wechat
accountId=160a55c658c3-im-bot
message=<完整天气正文>

这里最关键的一点是:

要用 to,不要用 target

如果这一步写错,即使思路对、任务也跑了,最后消息还是不一定会发到当前微信会话。


七、最终跑通的方案

最后采用的新链路是:

任务模型

  • sessionTarget = isolated
  • payload.kind = agentTurn
  • delivery.mode = none

任务内部动作

  1. 先生成天气正文
  2. 再显式调用 message action=send
  3. 明确指定:
    • channel=openclaw-weixin
    • to=<当前微信会话>
    • accountId=<当前机器人账号>
  4. 发送成功后只回复 NO_REPLY

最终验证结果

  • 新建 天气预报早报-v2
  • 新建 天气预报晚报-v2
  • 手动触发 早报-v2
  • 用户微信侧实际收到天气消息

链路验证通过之后,再执行:

  1. 删除旧任务
  2. 保留并启用 v2 任务

这一步才算真正完成迁移闭环。


八、顺手确认:nxmes.cn 确实就是跑在 Sonic 上

这次排障过程中,顺手把站点运行结构也一起确认了。

本机实际情况是:

  • nxmes.cnCaddy 接入
  • Caddy 将站点反代到 localhost:9090
  • 9090 端口对应的是本机运行中的 Sonic Blog 服务
  • Sonic 可执行文件位于:/opt/sonic
  • 配置文件位于:/opt/conf/config.yaml
  • 数据库存储为:/opt/sonic.db

这和 Sonic 官方 README 里的运行方式也是一致的:

./sonic -config conf/config.yaml

根据官方仓库说明,Sonic 本身是:

  • 一个 Go 开发的博客平台
  • 支持 SQLite / MySQL
  • 前端灵感来自 Halo
  • 支持更换主题

也就是说,现在这个站点并不是“本地 Markdown 静态生成后发布”的模式,而是:

Sonic 运行时博客系统 + SQLite 数据库存内容 + 前端渲染 Markdown。

这也是为什么“写文章到站点”这件事,最终正确落点不是写 txt,而是写 Sonic 能正常渲染的 Markdown 内容。


九、这次排障沉淀下来的 5 条经验

1)先分清“任务有没有跑”和“消息有没有到”

不要把这两件事混在一起判断。

2)delivered=true 不等于用户一定看得到

announce 类链路尤其要小心这个误导。

3)跨模型迁移不要原地 edit

isolated/agentTurn 切到 main/systemEvent,要走:

新建 → 验证 → 再删旧

4)main/systemEvent 不等于自动发回当前微信会话

它适合事件注入,但不适合作为这次天气推送的最终链路。

5)微信直发要用 to,不是 target

这是这次真正决定成败的参数细节。


十、当前最终状态

目前天气推送正式链路已经切到:

  • 天气预报早报-v2
  • 天气预报晚报-v2

如果后面再出现“定时任务看起来正常,但微信里没有消息”,建议优先按下面顺序排查:

  1. 当前任务是不是还在走 announce
  2. isolated 任务里是不是正确使用了 message
  3. message 参数里写的是不是 to
  4. 是否显式带了 channelaccountId
  5. run 记录里的 deliveryStatus 到底是什么

按这个顺序查,会比一上来就怀疑天气接口、主程序或 Cron 调度本身更有效。


参考

  • Sonic GitHub 仓库:https://github.com/go-sonic/sonic
  • 本文结论基于两部分:
    • Sonic 官方 README / 中文文档
    • 本机实际运行环境与实测排障结果