‹ 返回笔记 · Back to notes

现场手记

业务通知 vs 系统通知:通知不是一种语义,得分两路发

Business Notifications vs System Notifications: Notification Isn't One Semantic — Split Into Two Channels

一天之内连修两版,才搞定一个看起来不大的通知问题——飞书里苏晚的早报草稿被 watchdog 标题盖住,发件人又被 fallback 到了赵子龙。两版修完才学到:业务交付和系统健康根本是两种通知。

OpenClawStudio通知设计身份隔离飞书public-safe
水彩手绘:手机上两条飞书消息——一条是「苏晚 · 早报草稿」,一条是「赵子龙·控制塔 · 系统告警」,标题清楚分开

OpenClaw Studio · 通知设计

业务交付(苏晚写了一篇早报)和系统健康(watchdog 跑完一轮健康检查)根本不是一种通知。混走同一个通道,业务一定会被淹没。

这件事,trial 第一天连摔两次我才真的接受。第一次摔,是看到飞书里全是 watchdog(看门狗,事故时自动救场的兜底进程)的标题,看不到苏晚交了什么稿。第二次摔,是改完标题之后才发现——标题已经是「苏晚 · 早报草稿」了,发件人却还是赵子龙。

一天之内连修两版。中间隔了半小时。回头看,这两版加在一起才是一个完整的设计原则。少了任何一版,业务和系统都还会粘在一起。

前一天刚修完 PATH,以为通知层就稳了

前一天凌晨修完一个 LaunchAgent(macOS 的常驻任务调度器)PATH 缺失的老坑——runner(任务执行器)跑到一半找不到工具就死掉,5 个早班任务没发出来。当时连夜补了 PATH,又给 runner 套上一层 watchdog wrapper(包装层),让它在任何中断的时候都能兜出一个 alert(告警)。

我以为就此稳了——能跑、不掉、有兜底。第二天早上打开飞书,才发现通知层比我想的复杂得多。不是「能不能发出来」,是「发出来的东西人能不能用」。这层之前根本没考虑过。

v2.4 之前的样子:watchdog 盖住了所有人

12 点多打开飞书,看到的是这样一串:

  • 「OpenClaw trial watchdog checkpoint complete · task_id=xxx · status=PASS · manual_review_required=yes」
  • 「OpenClaw trial watchdog checkpoint complete · task_id=xxx · status=PASS · manual_review_required=yes」
  • 「OpenClaw trial watchdog checkpoint complete · task_id=xxx · status=PASS · manual_review_required=yes」

一条接一条。watchdog wrapper 套上之后,所有任务的通知都被它包了一层——苏晚交了早报、霍锐发了研究 memo(备忘),全部被改写成「watchdog checkpoint complete」的格式。任务的 owner(拥有者,比如苏晚拥有早报草稿)是谁、交付物是什么、内容写了啥,全埋在一堆字段后面。

更糟的是——Codex(OpenAI 的命令行 agent,智能体)每跑完一个 checkpoint(检查点),不管 PASS 还是 FAIL 都发一条。99% 的 checkpoint 都是 PASS。99% 只是「健康检查通过了」——这种事根本不需要打扰我。但它跟苏晚的早报混在一起发,结果业务被淹没在系统消息的刷屏里。

那一刻才意识到:业务交付和系统健康根本就是两种通知。业务交付是要被人读、被人用、被人回复的东西;系统健康 99% 的时候是 PASS,PASS 的时候根本不该出声。把它们走同一个通道,等于让兜底机制把主交付吃掉了。

v2.4:拆「内容」——给每个 owner 自己的标题和正文

上午 12:32 推了 v2.4(commit f62382b)。这一版只解决一件事——让业务交付的通知,长得像业务交付。

四个改动一起落地。

第一个:per-owner delivery renderer(每个 owner 自己的交付渲染器,把数据变成消息的那层代码)。每一种交付物都有自己的标题模板,不再共用一个通用的「watchdog 格式」。

  • 「苏晚 · 早报草稿」「苏晚 · 专栏草稿」「苏晚 · 晚报草稿」
  • 「霍锐 · 早盘研究早报」「霍锐 · 午盘小结」「霍锐 · 收盘总结」「霍锐 · 收盘研究复盘」「霍锐 · 深度研究复盘」「霍锐 · 周总结」「霍锐 · 下阶段研究建议」
  • 「沈知行 · 每日 source package(信源包)」

标题一眼看过去就知道是谁、交了什么。不用再往下翻 task_id。

第二个:真正的正文摘要(snippet)。之前正文是把 task_id、status、output_path 全 dump(堆出来)一遍,最后才挂一个文件路径。改完之后,renderer 直接读 output.md,从 ## content_body 这个 anchor(锚点)之后提取真正的正文,发 1500 字摘要、800 字段落底。我看到的是稿子本身,不是稿子的元数据。

第三个:Codex watchdog 静默原则。checkpoint 读自己生成的 JSON 里的 overall_status——

  • PASS / PASS_WITH_WARNINGS → 沉默。只写文件,不发飞书。state 里记录一行 skipped=true、reason=checkpoint_pass_silent。
  • WARN / FAIL / UNKNOWN → 才发 alert。

PASS 是默认状态,不该有声音。只有 PASS 不再是 PASS 的时候,才值得打扰人。

第四个:统一 alert 标题。所有系统告警一律用「OpenClaw Watchdog Alert」做前缀。不再让 watchdog 套用业务标题的格式。这样从标题就能一眼分出来:这是业务,那是系统。

跑了个 FAKE_PASS_SMOKE 测试——人为让 Codex 返回 PASS 看 silence path(沉默路径)有没有真生效。notification log 行数从 13 进去、13 出来——没发出去。state 里多了一条 skipped=true reason=checkpoint_pass_silent。沉默路径走通了。

然后把 5 条 Day 1 被 watchdog 标题盖掉的业务交付重发了一遍。这次进来的是「苏晚 · 早报草稿」+ 真正的正文。我以为这就完了。

v2.5:拆「身份」——发件人必须是自己

问题没解决。

v2.4 推出去后看了一眼新消息——标题确实变成「苏晚 · 早报草稿」了,可飞书消息列表里,发件人头像和名字还是「赵子龙·控制塔」。

这是 impersonation(冒名顶替)。标题写着苏晚,发件人是赵子龙——心理上就是赵子龙在冒充苏晚说话。

我去追底层。openclaw message send(OpenClaw 平台的消息发送命令)调用时没带 --account,gateway(网关,对外暴露服务的入口)直接 fallback(兜底)到了 zhao_zilong——他是平台的默认 account(账号)。所以所有消息不管标题写谁,最终都是从赵子龙的 bot(机器人)发出来的。

这个问题之前并不是没意识到——一直以为是「内容里写明是谁」就够了。v2.4 改完看到效果,才明白根本不够。通知的「发件人身份」和「内容标题」不能分离。一旦分离,人看到的就是冒名顶替,不是协作。

6 个 Agent,必须各自从自己的飞书 bot 发。每条消息要从对应的 peer(同事级别的对象)进入我的对话框,而不是全部挤在赵子龙一个窗口里。这是身份隔离,不是 UI 美化。

下午 13:00,v2.5(commit 5129a83)。

第一个改动:AGENT_FEISHU_ROUTES 显式映射。建一张 routing table(路由表)把每个 agent 钉死到具体的 profile(配置档案)+ account + 显示名:

  • suwan → openclaw-studio / su_wan / 苏晚
  • huorui → openclaw-studio / huo_rui / 霍锐
  • shenzhixing → openclaw-jiyanran / shen_zhixing / 沈知行
  • watchdog → openclaw-studio / zhao_zilong / 赵子龙·控制塔

注意第三条——沈知行不在 openclaw-studio 这个 profile 里,他属于 openclaw-jiyanran。其实 routing 不只是选 account,profile 也得跨过去。这是早期我没想到的复杂度,但 routing table 把它显式写出来之后,跨 profile 也只是表里多一列。

第二个改动:send_feishu_message(message, route=...) + resolve_feishu_personal_target(profile, account)。把 profile 和 account 一路传进每一次 openclaw 调用——不再让 gateway 猜。每条消息从 owning agent 自己的 bot 发出去。飞书界面上是 4 个不同的 open_id(飞书内部的用户唯一标识)进入我的对话框,每个 Agent 一个独立窗口。

第三个改动:「文章 first」投递模板。v2.4 的正文摘要是「头部 dump 一堆字段 + 1500 字截断的正文」。v2.5 改成正文 first——业务交付物本身就是消息内容,不需要 header dump(消息头部把一堆字段全堆出来)。苏晚的文章整篇过,最多 12000 字(不再 1500 字截断,因为正文就是交付物)。霍锐按 ## 段落 parse(解析),12 个禁止词在投递时重新扫一遍——避免合规问题被 renderer 放过去。

第四个改动:watchdog alert 改人话。之前 watchdog alert 是字段 / 路径 dump,长得也像机器在自言自语。改成「什么事 / 影响 / 已做 / 需要」四段中文。我看到 alert 不再需要解读,知道发生了什么、谁受影响、watchdog 已经做了什么、需要我做什么。

两版加起来才是真正的「语义分离」

v2.4 和 v2.5 解决的不是同一个问题。

v2.4 解决的是「内容看起来像谁」——标题、摘要、沉默规则,让业务消息看起来是业务,让系统告警看起来是系统。

v2.5 解决的是「消息从谁那儿发出来」——routing table、profile + account 显式传参、独立 bot,让每个 Agent 真的是自己在说话。

单做 v2.4,标题对了但发件人错——心理上是 zhao_zilong 在冒名顶替苏晚。单做 v2.5,发件人对了但内容还是 header dump——苏晚的 bot 发出来的还是机器味的字段拼接。两个一起做完,业务和系统才彻底走两条管道:内容上分开、身份上也分开。

回头看,这是「业务通知 vs 系统通知」的完整设计。一个通知系统要能真正分两路,至少要在两层上同时分——标题和正文一层(看起来是谁),发件身份一层(从哪儿发出来)。少一层都不算分。

这个设计能推广到哪里

这件事在 OpenClaw Studio 里是飞书 + 多 Agent,但原则跟工具无关。任何「业务结果 + 系统健康」共用一个通知通道的场景都会撞上同样的问题:

  • CI/CD(持续集成 / 持续交付)——构建成功和系统监控走同一个 Slack 频道,构建成功被监控的 PASS 心跳淹掉。
  • 监控告警——业务指标异常和基础设施健康用同一种标题模板,业务异常被基础设施 PASS 刷屏。
  • 定时任务、爬虫——任务交付和调度器健康混在一个通知队列,交付内容很快被「调度器跑完一轮」的回执盖住。
  • 多 Agent 系统——所有 Agent 走同一个 bot,每条消息看起来都是同一个发件人在说话,协作感被压平成单兵。

判断标准很简单:打开通知列表,能不能一眼分出「这条是给我看的内容」和「这条是机器在打卡」?分不出来,就该拆两路。拆两路不只是分频道、加 tag——要从内容模板、沉默规则、发件身份三个地方一起拆。

还有一条:PASS 通知和 FAIL 通知不该用同一种标题模板。PASS 是默认状态,默认状态不该有声音。让 FAIL 自己有一套显眼的标题前缀(比如「OpenClaw Watchdog Alert」),人一眼能从 100 条消息里抓出来。这件事跟分两路是配套的——分了两路还让 PASS 出声,业务管道照样会被淹。

v2.4 + v2.5 跑过几天,6 个 Agent 各自从自己的 bot 发消息,PASS 不出声,FAIL 显眼。业务交付暂时没被系统心跳吃掉,这一层算是稳了。

但每加一个新 Agent,AGENT_FEISHU_ROUTES 这张表都要手动加一行。下一步想做成自动注册——agent 启动时自己声明 route,不再依赖人维护一张全局表。Codex watchdog 的「沉默阈值」还在调,PASS_WITH_WARNINGS 算 PASS 还是算 WARN,目前还有几个边缘 case 没收口。霍锐的研究复盘有时候特别长,按 ## 段落 parse 还会丢边角内容——12000 字上限对苏晚够用,对霍锐紧一点。

通知层的设计永远没有「做完」这一说。每加一个 Agent、加一种交付物、加一条系统告警,这两层都得重新过一遍。现在习惯了——通知不是一种语义,它至少有两种,而且会继续分裂下去。

把这篇记录接到下一步

读完以后,可以继续追问这篇文章,也可以回到策展目录,或通过标签追同一条线索。

追问这篇 回到目录 浏览标签