OpenClaw 微信早报定时任务排障复盘
这次排障表面上看像是“Cron 正常、日志正常、系统也显示 delivered”,但真正的问题根本不在任务有没有执行,而在于:执行完成后,消息是否真的稳定送达当前微信会话。
一、问题现象:后台看起来正常,用户侧却不稳定可见
这次排查的对象主要是两条早报任务:
- GitHub 早报
- 油价早报
从 OpenClaw 后台状态来看,它们一开始都不像“坏掉了”的任务:
- Cron 在正常运行
- 日志里多次出现
status=ok - 甚至还能看到
deliveryStatus=delivered
如果只看这些信息,很容易下结论:
任务已经成功执行,问题不大。
但用户侧的实际体验完全不是这样。消息并没有稳定出现在当前微信会话里,或者即使有“送达”,内容也不一定是用户真正需要的最终早报。
这一步首先说明了一个很重要的问题:
在 OpenClaw 里,
status=ok和deliveryStatus=delivered只能说明“系统认为这次运行完成并进行了投递”,但不能直接等同于“用户已经在当前会话里看到了正确结果”。
所以,这类用户可见的定时任务,最终验证标准必须从“后台状态正确”切换到“当前微信会话真实可见”。
二、尝试过的几条链路,为什么都不够稳
排查过程中,先后尝试了几种不同的任务链路:
main + systemEventcurrent / sessionKey绑定delivery.mode = noneisolated + announce
这些方案看起来都“像是能发消息”,但实际语义并不完全一样。
1)main + systemEvent
这条路更像是把事件投递回主会话,适合需要主会话上下文接力处理的场景。
但它的关键问题是:
它不天然等于“把最终内容稳定发回当前微信会话”。
在定时任务场景里,尤其是要求“到点就给用户发一条最终可读消息”,这条链路并不够直接。
2)current / sessionKey 绑定
这种方式更偏向:
- 绑定上下文
- 绑定会话归属
- 绑定后续处理位置
但它解决的是“任务归谁处理”的问题,不是“最终一定如何投递给用户”的问题。
也就是说:
current/sessionKey更像上下文路由,而不是稳定送达保证。
3)delivery.mode = none
这次排障里,delivery.mode=none 非常容易被误解。
很多时候看到 none,会下意识理解为:
- 先内部跑完
- 再自动回到当前会话
但它的真实语义其实更接近:
只内部运行,不做对外投递。
也就是说,它不是“静默执行后自动回微信”,而是“执行归执行,但不负责外发”。
所以对于“定时给用户发消息”这种任务,none 并不能解决最终送达问题。
三、最后确认:真正符合这类场景的还是 isolated + announce
排到后面,最终确认下来,真正符合 OpenClaw 官方设计思路的路径,仍然是:
isolated + announce- 并显式指定:
channeltoaccountId
原因很简单:
isolated让任务在独立运行环境里完成announce让 cron runner 自己负责最终投递channel/to/accountId把目标会话明确到不能再模糊
这条链路的核心优势在于:
它不是“任务跑完了再看看要不要发”,而是“这个任务从设计上就是为了产出一个可直接投递的最终结果”。
但这还不是全部。
真正最容易踩坑的地方,其实在下面这一步。
四、最大的坑不在配置,而在“输出协议”
这次最关键的坑,不是哪个字段写错了,而是:
模型输出了过程性话术,系统却把过程性文本当成最终 summary 投递了。
在 isolated + announce 模式下,如果 prompt 约束不够强,模型非常容易输出类似这种内容:
- “我先查一下今天的数据”
- “我再整理一下结果”
- “拿到更多信息后我再判断是否发送”
- “我先验证一下来源是否可靠”
从对话角度看,这些话没问题;但从定时任务角度看,它们是致命的。
因为在 announce 链路里,OpenClaw 并不会替你判断“这句话到底是不是最终要发给用户的内容”。它只会拿到当前轮次产出的文本,并把它当成最终 summary 去投递。
结果就是:
- 系统记录为“已送达”
- 但用户收到的可能只是过程说明
- 甚至收到一条看起来不像早报的废话
- 最差情况下,用户会觉得“根本没发对内容”
这就是为什么后台明明显示 delivered,但用户感知依然是“这任务不稳定”——因为真正送到用户眼前的内容,不是正确的最终早报,而是模型中间态输出。
五、修复关键:不要再换链路,要先锁死“最终摘要协议”
排到这里,真正的修复方向就清楚了:
不是继续无止境地换链路,而是先把输出协议收紧。
这次修复的关键约束有三条:
1)如果输出,第一行必须直接是最终标题
不能先寒暄,不能先铺垫,不能先解释自己在做什么。
也就是说,任务一旦决定输出,开头就必须像这样:
【GitHub 早报】
或者:
【今日油价早报】
用户第一眼看到的就必须是可读结果,而不是过程说明。
2)禁止任何过程性话术
例如:
- 我先查一下
- 我再确认一下
- 稍后发送
- 还需要判断
- 我先整理一下
这些在普通对话里没问题,但在定时早报里都不应该出现。
因为一旦出现,它们就很可能被系统当成“最终可发送文本”。
3)如果没有足够信息,就直接不输出
这是一个很重要但经常被忽略的原则。
与其发出一条半成品、解释性、过程性的内容,不如在信息不足时直接保持静默。
因为定时摘要的目标不是“展示模型正在努力”,而是:
只在准备好最终结果时,输出最终结果。
六、最终跑通方式:一条保留,一条重建
最终处理结果不是两条任务完全一样地硬改,而是区别对待:
GitHub 早报
- 保留原本可用链路
- 重点放在输出协议收紧
- 避免过程性文本被误投递
油价早报
因为原有链路问题更多,最终采用了更干净的重建方式:
- 新建 clean job
- 固定来源白名单
- 使用
announce投递 - 增加“必须直接输出最终摘要”的强约束协议
这样处理的好处是:
- 不把旧任务历史包袱继续带下去
- 新链路语义更清晰
- 验证标准也更明确
七、这次复盘真正说明了什么
这次排障最后得到的结论,其实不只适用于“微信早报”,而适用于所有 用户可见的定时任务:
1)不要只看后台日志
后台 ok、delivered 只能作为参考,不能作为最终验收标准。
2)定时任务的终点是“用户真实可见”
真正的成功标准不是:
- run 成功
- 日志正常
- 系统没报错
而是:
当前会话里,用户能否真实看到一条格式正确、内容正确、时机正确的消息。
3)任务输出必须有“最终结果协议”
对于 announce 场景,最怕的不是没输出,而是输出了过程性文本却被系统当作最终结果发送。
所以这类任务必须把“输出格式”和“中间态禁止输出”写进 prompt 本身,而不是依赖模型自行理解。
八、这次排障沉淀下来的 4 条经验
1)delivered=true 不等于用户已经收到有效消息
它只能证明系统完成了一次投递动作,不能证明用户看到了正确结果。
2)delivery.mode = none 不是“自动回当前会话”
它的语义更接近“内部运行,不负责投递”。
3)current/sessionKey 解决的是上下文绑定,不是稳定送达
不要把会话绑定能力误当成最终消息送达保证。
4)isolated + announce 的真正风险在输出协议
链路本身并没有错,错的是如果不限制模型输出,它就可能把中间话术送给用户。
九、当前可复用的做法
如果以后再做这类“用户可见的定时摘要任务”,建议默认按下面这套思路设计:
- 优先使用
isolated + announce - 显式指定:
channeltoaccountId
- 输出协议必须强约束:
- 第一行就是最终标题
- 禁止过程说明
- 信息不足就不输出
- 验证标准只看一条:
- 用户当前会话里是否真实可见
做到这四点,类似“系统说发了,但用户感觉没收到”的问题,基本就能少掉一大半。
结语
这次排障最值得记住的一句话是:
用户可见的定时任务,最终要对“用户看到什么”负责,而不是只对“后台跑了什么”负责。
一旦把这个标准立住,很多判断都会变得更直接:
- 不是看内部有没有执行
- 不是看字段有没有报错
- 而是看结果是否真正出现在用户眼前
这才是这次 OpenClaw 微信早报排障复盘的真正价值。