一条消息的完整旅程
前三天分别看了全景、启动、Gateway。今天把它们串成一条线:你在 Telegram 发"北京天气?",到收到回复,中间穿过了哪七站、每站在哪段代码。这是理解 OpenClaw 的主轴,也是第 1 周的收束——后面每周都在放大其中一段。
全景七站
MsgContext(把各平台方言翻成通用语)和 OriginatingChannel(回执地址,保证原路返回)。记住这根接力棒,20 天就有了主轴。src/telegram/monitor.tsMsgContext + 清洗 finalizeInboundContextsrc/routing/resolve-route.tsagent-runner-execution.ts → runEmbeddedPiAgentsubscribeEmbeddedPiSessionOriginatingChannel 决定发回哪 channel-selection.tsoutbound/deliver.ts像调试器单步执行一样,跟踪这条消息每一站"手里的关键状态"怎么变:
| 站 | 此刻发生什么 | 关键状态的值 |
|---|---|---|
| ①收到 | Telegram 长轮询拿到原生消息 | raw = "北京天气?"(Telegram 格式) |
| ②归一化 | 翻译成统一 MsgContext + 记回执地址 | OriginatingChannel = "telegram" |
| ③路由 | 查绑定,决定交给谁 | matchedBy = "default", agent = 默认 |
| ④想 | 喂给 LLM,模型决定调工具 | toolCall = weather("北京") |
| ④想(第2轮) | 工具结果回喂,模型再想 | toolResult = "晴 25°C" |
| ⑥选渠道 | 读回执地址决定发回哪 | 回 telegram(=②记的) |
| ⑦投递 | 只加载 telegram outbound,sendText | reply = "北京今天晴,25°C" |
① 渠道收到
Gateway 启动时拉起的渠道监听(Day 03 L04)在后台等消息。Telegram 用 grammY runner 长轮询(src/telegram/monitor.ts:77),有新消息就触发处理。
② 归一化
渠道把平台原生消息映射成统一的 MsgContext(templating.ts:14,~170 字段),再调 finalizeInboundContext(inbound-context.ts:37)统一清洗(换行、剥系统标签、安全默认)。
OriginatingChannel(从哪来)——第 6 站要靠它决定"回哪去"。清洗步骤(剥离恶意系统标签、默认拒绝命令授权)是第一道安全关。③ 路由
src/routing/resolve-route.ts:26-59:ResolveAgentRouteInput → ResolvedAgentRoute,决定"这条消息该由哪个 agent、哪个会话处理"。matchedBy 支持多级绑定:binding.peer(按对端)/ binding.guild+roles(Discord 服务器+角色)/ binding.team(Slack 团队)/ binding.channel / default。
default 兜底——没匹配到特殊规则就用默认 agent。会话(session)则决定用哪段对话历史(记忆连续性)。binding.peer(按对端)→ 转「正式工作 agent」。• 家人群(Discord)里 @它 → 命中
binding.guild+roles → 转「轻松家庭 agent」。• 一个没配过规则的新群发消息 → 全都不匹配 → 落到
default 默认 agent。同一个人昨天/今天在同一会话说话 → 命中同一
session,接得上昨天的上下文。④ Agent 想+做
路由定了 agent,就进大脑(第 2 周主角):agent-runner-execution.ts:202 启动两层故障转移(Day 07)→ runEmbeddedPiAgent(Day 08)→ Pi 的 session.prompt() 跑 ReAct 循环(想 → 调工具 → 再想)。
⑤ 订阅输出
Pi 循环跑的同时,subscribeEmbeddedPiSession(pi-embedded-subscribe.ts:34)订阅它的事件流,把"模型吐的文字增量、工具调用/结果"转成 OpenClaw 回调(onPartialReply/onToolResult…)。
⑥⑦ 回复原路返回
// ⑥ 选渠道 src/infra/outbound/channel-selection.ts:86 resolveMessageChannelSelection
// 从 sessionCtx.OriginatingChannel/OriginatingTo 读回"从哪来"→ 决定"回哪去"
// 多渠道并存时必须显式指定(防发错)
// ⑦ 投递 src/infra/outbound/deliver.ts:141 createChannelHandler
// → loadChannelOutboundAdapter(id)(轻量 loader,只取 outbound)
// → 调 sendPayload/sendText/sendMedia(Day 13 的 outbound adapter)
OriginatingChannel="telegram" 吗?现在回复要发出去,就读这个字段——"哦,这消息从 Telegram 来,那答复也发回 Telegram"。然后只加载 Telegram 的 outbound adapter(不加载它的收消息代码,省内存),调 sendText 把"北京今天晴,25°C"发回你的 Telegram。一条消息的旅程闭环完成。整个过程渠道、路由、大脑各司其职,靠 MsgContext 这个通用语和 OriginatingChannel 这个"回执地址"串起来。今日小结 + 动手
🧠 今天你应该能回答
- 一条消息的七站分别是什么?
- 归一化为什么关键?记下的 OriginatingChannel 后面干嘛用?
- 路由按什么把消息分给不同 agent?
- ④⑤两站分别对应第几周的哪些天?
- 回复怎么做到"原路返回"?为什么用轻量 loader?
MsgContext(通用语)+ OriginatingChannel(回执地址)。记住这句,整条主轴就在脑子里了。✋ 动手
cd /Users/bitmart/work/codes/github/openclaw
sed -n '26,59p' src/routing/resolve-route.ts # 路由
grep -n 'runWithModelFallback\|runEmbeddedPiAgent' src/auto-reply/reply/agent-runner-execution.ts | head
sed -n '86,120p' src/infra/outbound/channel-selection.ts # 选渠道
sed -n '141,160p' src/infra/outbound/deliver.ts # 投递