Day 04 / 共 20 天 · 第 1 周 入门与全景

一条消息的完整旅程

前三天分别看了全景、启动、Gateway。今天把它们串成一条线:你在 Telegram 发"北京天气?",到收到回复,中间穿过了哪七站、每站在哪段代码。这是理解 OpenClaw 的主轴,也是第 1 周的收束——后面每周都在放大其中一段。

📍 你在整门课的位置(第 1 周 · 入门与全景)
Day1 全景 Day2 入口启动 Day3 Gateway Day4 消息旅程 Day5 配置· W2 大脑· W3 技能/渠道· W4 安全/部署
L01

全景七站

🤔 痛点:功能各自看懂了,但它们怎么接力? 单看渠道、Gateway、大脑都懂了,可一条真实消息进来,是谁先接、传给谁、结果又怎么回到你手机?不把这条"接力棒"看清,就永远是零散的知识点。
💡 本质:一条消息 = 一根接力棒,跑过七站 进(收到+归一化)→ 路由 → 想+做(Agent,可多轮)→ 出(选渠道+投递)。贯穿全程的两样东西:MsgContext(把各平台方言翻成通用语)和 OriginatingChannel(回执地址,保证原路返回)。记住这根接力棒,20 天就有了主轴。
渠道收到:Telegram 监听拿到原生消息 src/telegram/monitor.ts
归一化:转成统一 MsgContext + 清洗 finalizeInboundContext
路由:决定哪个 agent/会话处理 src/routing/resolve-route.ts
Agent 想+做:故障转移 → Pi 循环 agent-runner-execution.tsrunEmbeddedPiAgent
订阅输出:观察 Pi 事件流转成回调 subscribeEmbeddedPiSession
选渠道:按 OriginatingChannel 决定发回哪 channel-selection.ts
投递:调 outbound adapter 发回 Telegram outbound/deliver.ts
读法:七站 = 进(1-2)→ 路由(3)→ 想+做(4-5)→ 出(6-7)。每站都在某周被放大:归一化/渠道在 W3、Agent 在 W2、配置在 Day 05。今天走一遍全程,建立整体感。
📥 进站 🧠 想+做(可循环多轮) 📤 出站 ①收到telegram/monitor.ts ②归一化→ MsgContext ③路由resolve-route.ts ④Agent 想Pi ReAct 循环 ⑤订阅输出旁听事件→流式回调 🔧 weather调工具·拿结果 ⑥选渠道读OriginatingChannel ⑦投递deliver.ts→sendText 虚线:工具结果回喂给模型再想一轮
"北京天气?"的完整旅程:从 Telegram 进,路由到 agent,模型调 weather 工具再想,最后原路发回 Telegram。

像调试器单步执行一样,跟踪这条消息每一站"手里的关键状态"怎么变:

此刻发生什么关键状态的值
①收到Telegram 长轮询拿到原生消息raw = "北京天气?"(Telegram 格式)
②归一化翻译成统一 MsgContext + 记回执地址OriginatingChannel = "telegram"
③路由查绑定,决定交给谁matchedBy = "default", agent = 默认
④想喂给 LLM,模型决定调工具toolCall = weather("北京")
④想(第2轮)工具结果回喂,模型再想toolResult = "晴 25°C"
⑥选渠道读回执地址决定发回哪telegram(=②记的)
⑦投递只加载 telegram outbound,sendTextreply = "北京今天晴,25°C"
⚠️ 常见误解:以为"想+做"只跑一次。其实第④站是个循环——模型想一步、调个工具、看结果、再想,可能来回好几轮(表里第 4/5 行就是两轮),直到它觉得"够答了"才产出回复(Day 08 细讲)。
L02

① 渠道收到

Gateway 启动时拉起的渠道监听(Day 03 L04)在后台等消息。Telegram 用 grammY runner 长轮询(src/telegram/monitor.ts:77),有新消息就触发处理。

读法:每个渠道有自己的"收消息"方式(Telegram 轮询、Slack/Discord WebSocket,Day 14),但收到后都进同一条后续管线。这一步是平台特有的,之后就统一了。
L03

② 归一化

渠道把平台原生消息映射成统一的 MsgContexttemplating.ts:14,~170 字段),再调 finalizeInboundContextinbound-context.ts:37)统一清洗(换行、剥系统标签、安全默认)。

为什么这步至关重要? 这是"把二十多种平台的方言翻译成一种通用语"的时刻(Day 15 详讲)。翻译完成后,后面所有代码(路由、Agent、回复)就只需认识 MsgContext 这一种格式,不用管消息来自哪个平台。还会记下 OriginatingChannel(从哪来)——第 6 站要靠它决定"回哪去"。清洗步骤(剥离恶意系统标签、默认拒绝命令授权)是第一道安全关。
L04

③ 路由

src/routing/resolve-route.ts:26-59ResolveAgentRouteInputResolvedAgentRoute,决定"这条消息该由哪个 agent、哪个会话处理"。matchedBy 支持多级绑定:binding.peer(按对端)/ binding.guild+roles(Discord 服务器+角色)/ binding.team(Slack 团队)/ binding.channel / default

为什么需要"路由"? 你可能配了多个 agent(工作助理、家庭助理),或不同群走不同人设。路由就是"接线员":根据消息来自谁、哪个群、什么角色,把它转接给对的 agent 和会话。比如"老板发的消息用正式 agent,家人群用轻松 agent"。default 兜底——没匹配到特殊规则就用默认 agent。会话(session)则决定用哪段对话历史(记忆连续性)。
📝 举个例子:三条消息、三种路由结果 • 老板在私聊发消息 → 命中 binding.peer(按对端)→ 转「正式工作 agent」。
• 家人群(Discord)里 @它 → 命中 binding.guild+roles → 转「轻松家庭 agent」。
• 一个没配过规则的新群发消息 → 全都不匹配 → 落到 default 默认 agent。
同一个人昨天/今天在同一会话说话 → 命中同一 session,接得上昨天的上下文。
L05

④ Agent 想+做

路由定了 agent,就进大脑(第 2 周主角):agent-runner-execution.ts:202 启动两层故障转移(Day 07)→ runEmbeddedPiAgent(Day 08)→ Pi 的 session.prompt() 跑 ReAct 循环(想 → 调工具 → 再想)。

读法:这一站是整个系列的核心,第 2 周(Day 06-10)整整五天在讲它内部:Pi 内核、故障转移、执行循环、提示词、记忆。今天只需知道"消息在这里被喂给 LLM、模型可能调工具(如 weather)、最终产出回复"。
L06

⑤ 订阅输出

Pi 循环跑的同时,subscribeEmbeddedPiSessionpi-embedded-subscribe.ts:34)订阅它的事件流,把"模型吐的文字增量、工具调用/结果"转成 OpenClaw 回调(onPartialReply/onToolResult…)。

读法:这是"观察者模式"(Day 08):不打断 Pi 循环,只旁听它发生的每件事,转成流式回复推给渠道。让用户看到"正在查天气""答复逐字蹦出"的实时体验(呼应 Day 15 的流式发送)。
L07

⑥⑦ 回复原路返回

// ⑥ 选渠道 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)
"原路返回"怎么做到的? 还记得第 2 站归一化时记下的 OriginatingChannel="telegram" 吗?现在回复要发出去,就读这个字段——"哦,这消息从 Telegram 来,那答复也发回 Telegram"。然后只加载 Telegram 的 outbound adapter(不加载它的收消息代码,省内存),调 sendText 把"北京今天晴,25°C"发回你的 Telegram。一条消息的旅程闭环完成。整个过程渠道、路由、大脑各司其职,靠 MsgContext 这个通用语和 OriginatingChannel 这个"回执地址"串起来。
L08

今日小结 + 动手

🧠 今天你应该能回答

  • 一条消息的七站分别是什么?
  • 归一化为什么关键?记下的 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          # 投递
明天预告 · Day 05(第1周收官)配置系统与 workspace——配置怎么加载(env 优先级链 → openclaw.json)、workspace 工作区是什么、bootstrap 文件(SOUL/MEMORY.md)如何塑造助理。
← Day 03 Gateway Day 05 · 配置系统与 workspace →