OpenClaw 源码解析(九):Channel 接入机制与消息路由流程

OpenClaw 源码解析(九):Channel 接入机制与消息路由流程 1. 本期目标前几期我们已经分析了 CLI、Gateway、Agent 执行链路和 Session 会话模型。到这里OpenClaw 的核心骨架已经比较清楚CLI / UI / Channel / Node ↓ Gateway ↓ Agent / Session / Tool / Skill这一期继续分析一个非常关键的问题Telegram、Slack、Discord、WhatsApp、WebChat 等外部消息 到底是如何进入 OpenClaw 的OpenClaw 的 Channel 不是简单的“第三方平台适配器”。它承担了外部消息接入、访问控制、会话路由、Agent 绑定、回复投递、去重、防抖、群聊激活等一整套逻辑。官方消息流程文档把高层链路概括为Inbound message → routing/bindings → session key → queue → agent run → outbound replies。(OpenClaw)本期主要解决几个问题1. Channel 在 OpenClaw 架构中处于什么位置 2. 一个 Channel 是如何通过配置启用的 3. 外部消息进入后如何判断是否允许处理 4. 消息如何映射到 agentId 和 sessionKey 5. DM、群聊、频道、线程的路由有什么区别 6. 为什么需要 dedupe、debounce、mention gating 7. 源码应该从哪些文件开始读2. Channel 是什么在 OpenClaw 中Channel 可以理解为“外部消息平台接入层”。例如Telegram Slack Discord WhatsApp Signal iMessage Matrix WebChat Google Chat Microsoft Teams Mattermost这些平台的消息格式、用户 ID、群聊 ID、线程机制、权限模型都不一样。OpenClaw 需要把它们转换成统一的内部消息结构然后交给 Gateway 和 Agent 处理。所以 Channel 的作用可以概括为外部平台消息 ↓ Channel adapter ↓ 统一内部消息事件 ↓ Gateway 路由 ↓ Session / Agent ↓ 回复投递回原平台也就是说Channel 解决的是“外部世界如何接入 OpenClaw”的问题。3. Channel 和 Gateway 的关系Channel 不是独立运行的模型调用服务而是由 Gateway 统一管理。官方配置文档说明每个 Channel 都有自己的配置段通常位于channels.provider下如果某个 channel 配置存在它会自动启动除非显式设置enabled: false。(OpenClaw)可以这样理解openclaw.json ↓ channels.telegram / channels.discord / channels.slack ... ↓ Gateway 启动时读取配置 ↓ 加载对应 Channel ↓ Channel 开始监听外部消息所以Channel 的生命周期受 Gateway 管理Gateway 启动 ↓ 读取配置 ↓ 发现已配置的 channels ↓ 启动 channel runtime ↓ 接收外部消息 ↓ 交给 Gateway 内部消息管线这也是为什么我们前面一直强调 Gateway 是 OpenClaw 的控制平面。Channel 接入、Session 管理、Agent 运行和回复投递最终都会回到 Gateway。4. Channel 配置入口Channel 配置通常写在~/.openclaw/openclaw.json中。一个简化的 Telegram 配置可能类似{ channels: { telegram: { enabled: true, botToken: 123:abc, dmPolicy: pairing, allowFrom: [tg:123] } } }官方配置文档中也给出了类似模式每个 Channel 放在channels.provider下DM 访问通过dmPolicy控制例如pairing、allowlist、open和disabled。(OpenClaw)这里有三个字段尤其重要enabled 是否启用该 Channel。 dmPolicy 私聊消息如何授权。 allowFrom 允许哪些用户或来源给 Agent 发消息。从源码阅读角度看Channel 配置不是孤立的。它会影响Channel 是否启动 谁能发 DM 哪些群聊允许触发 是否需要 mention 回复投递到哪里 是否启用多账号 是否允许频道内命令写配置。5. DM 和 Group 的访问控制Channel 接入的第一道门槛是访问控制。官方 Channel 配置文档明确区分了 DM policy 和 Group policyDM policy pairing / allowlist / open / disabled Group policy allowlist / open / disabled其中pairing是默认 DM 策略未知发送者会收到一次性配对码需要 owner 批准allowlist表示只允许白名单open表示允许所有入站 DM但需要配合allowFrom: [*]disabled表示忽略 DM。群聊默认是allowlist也可以设置为open或disabled。(OpenClaw)可以这样理解外部消息进来 ↓ 先判断来自 DM 还是群聊 ↓ DM 走 dmPolicy 群聊走 groupPolicy ↓ 不满足策略则丢弃或进入 pairing ↓ 满足策略才继续路由这一层非常重要。因为 OpenClaw 不是普通聊天机器人它可以调用工具、访问本地环境、连接工作区。如果任何人都能随便给它发消息风险会非常高。6. pairing首次私聊为什么需要配对pairing是 OpenClaw 的默认 DM 策略。它的基本思路是未知用户第一次私聊 Agent ↓ 系统生成 pairing code ↓ owner 在本地批准 ↓ 该用户才被允许继续对话这解决了一个问题你的 Telegram bot、Discord bot 或其他 Channel 可能被陌生人找到。如果默认直接开放陌生人就能驱动你的 Agent 做事情。通过 pairingOpenClaw 把“第一次接入”变成显式授权流程。从博客解释角度可以这样写pairing 不是聊天体验功能而是安全边界。 它决定外部身份是否可以进入 OpenClaw 的 Agent 运行管线。7. Channel 源码目录怎么看从源码结构看src/channels是 Channel 相关的核心目录。当前目录中可以看到allowlists、inbound-event、message-access、message、plugins、status、transport、turn等子目录以及channel-config.ts、conversation-resolution.ts、mention-gating.ts、model-overrides.ts、native-command-session-targets.ts等文件。(GitHub)可以先建立一个目录级理解src/channels/ ├─ allowlists/ 白名单与允许来源 ├─ inbound-event/ 入站事件结构或处理 ├─ message-access/ 消息访问控制 ├─ message/ 消息抽象与通用处理 ├─ plugins/ Channel 插件接入 ├─ status/ Channel 状态 ├─ transport/ 传输层抽象 ├─ turn/ 与一次 Agent turn 相关的 Channel 逻辑 ├─ channel-config.ts Channel 配置解析 ├─ conversation-resolution.ts ├─ mention-gating.ts ├─ model-overrides.ts └─ native-command-session-targets.ts初学者不要一上来就钻进某个平台的实现。更推荐先看这些公共模块因为它们体现了 OpenClaw 对 Channel 的统一抽象。8. 一条消息进入 OpenClaw 的高层流程可以先用下面这张图理解外部平台消息 ↓ Channel plugin / adapter 接收 ↓ 标准化成内部 inbound message ↓ 访问控制dmPolicy / groupPolicy / allowlist / pairing ↓ 群聊激活mention gating / activation ↓ 路由匹配bindings / peer / guild / team / account / default agent ↓ 生成 sessionKey ↓ 进入 Session store ↓ Agent run ↓ 生成 reply payload ↓ Channel delivery ↓ 外部平台收到回复官方消息文档同样强调入站消息会经过 session resolution、queueing、streaming、tool execution 和 reasoning visibility 等管线。(OpenClaw)所以 Channel 并不是“收到消息后直接调用模型”。它前面有访问控制和路由后面还有回复投递和平台限制处理。9. 消息如何选择 AgentOpenClaw 支持多个 Agent因此一条外部消息进入后系统必须先决定“由哪个 Agent 处理”。官方 Channel Routing 文档列出了入站消息选择 Agent 的匹配顺序1. 精确 peer 匹配 2. parent peer 匹配也就是 thread inheritance 3. Discord guild roles 匹配 4. Discord guild 匹配 5. Slack team 匹配 6. accountId 匹配 7. channel 匹配 8. 默认 agent如果一个 binding 同时包含多个匹配字段例如 peer、guildId、teamId、roles那么这些字段都必须满足该 binding 才会生效匹配到的 Agent 会决定使用哪个 workspace 和 session store。(OpenClaw)这说明 Agent 路由不是简单地“所有消息都给 main”。更准确地说外部消息 ↓ 根据 channel / account / peer / guild / team / roles 等信息匹配 binding ↓ 得到 agentId ↓ 用该 agentId 找 workspace 和 session store示例{ agents: { list: [ { id: support, name: Support, workspace: ~/.openclaw/workspace-support } ] }, bindings: [ { match: { channel: slack, teamId: T123 }, agentId: support }, { match: { channel: telegram, peer: { kind: group, id: -100123 } }, agentId: support } ] }这类设计让 OpenClaw 可以做到某个 Slack workspace 交给 support agent 某个 Telegram 群交给 research agent 某个 Discord guild 交给 community agent 没有匹配时回到默认 main agent。10. 消息如何生成 sessionKey上一期我们讲过Session 是上下文边界。Channel 进来的消息最终也要落到某个sessionKey。官方 Channel Routing 文档给出了几类典型 session key 形态Direct messages agent:agentId:mainKey 默认是 agent:main:main Groups agent:agentId:channel:group:id Channels / rooms agent:agentId:channel:channel:id Slack / Discord threads 在基础 key 后追加 :thread:threadId Telegram forum topics 在 group key 中嵌入 :topic:topicId例如agent:main:telegram:group:-1001234567890:topic:42 agent:main:discord:channel:123456:thread:987654这些格式来自官方 Channel Routing 文档。(OpenClaw)可以这样理解agentId 决定由哪个 Agent 处理 channel 决定来自哪个平台 group/channel/direct 决定消息场景 id 决定具体对话空间 thread/topic 决定更细粒度的上下文分支。也就是说Channel 路由最终要落到 Session 路由外部平台身份 ↓ 内部 agentId ↓ 内部 sessionKey ↓ sessionId / transcript11. DM 为什么默认进入 main session官方文档中有一个重要默认行为Direct messages 默认会折叠到 Agent 的 main session key例如agent:main:main。同时群组和频道会按 channel 维度隔离形成各自的 sessionKey。(OpenClaw)这个设计有它的使用逻辑单用户个人助手场景 用户从 Telegram、WebChat、CLI 私聊同一个 Agent 希望它们共享一个主上下文。 多人或多渠道场景 需要打开 dmScope 隔离否则不同人的 DM 可能进入同一个上下文。所以可以在博客里强调DM 默认合并主会话适合“个人助手”场景 多人开放接入时需要特别关注 session.dmScope。上一期已经讲过 DM isolation这一期可以把它放到 Channel 里理解Channel 是外部身份进入系统的入口Session 是这些身份进入上下文的边界。12. Group / Channel 为什么要隔离群聊和频道默认隔离是合理的。因为一个群或一个频道往往有自己的话题、成员和上下文。如果所有群聊都进入 main session会造成明显的上下文污染。例如Telegram A 群在讨论代码问题 Discord B 频道在讨论项目管理 Slack C 频道在讨论部署故障。如果三者共用同一个 sessionAgent 很可能把 A 群的上下文带到 B 频道把 C 频道的故障信息带回 Telegram。所以 OpenClaw 用不同 sessionKey 把它们隔离agent:main:telegram:group:-100123 agent:main:discord:channel:123456 agent:main:slack:channel:C123一句话理解DM 默认强调个人连续性 群聊和频道默认强调上下文隔离。13. Thread 和 Topic 为什么还要细分Slack / Discord 有 threadTelegram 有 forum topic。这些结构在用户体验上是同一群或同一频道下的子对话但在上下文管理上通常应该分开。所以 OpenClaw 会在 sessionKey 里继续追加:thread:threadId :topic:topicId这样能把一个大频道中的不同讨论隔开。例如agent:main:discord:channel:123456:thread:987654表示main Agent Discord 平台 某个频道 某个具体 thread这种结构使 OpenClaw 能做到同一个频道中不同 thread 各有上下文 thread 内连续讨论不会污染整个频道 Agent 回复可以回到正确 thread。14. Mention gating群聊里为什么不能每句话都回复在群聊中如果 Agent 对每条消息都自动回复体验会很糟糕。所以 OpenClaw 支持群聊激活规则。官方配置文档中提到群消息默认需要 mention可以通过 agent 的groupChat.mentionPatterns和 channel 的 group 配置来控制触发方式还可以把 groupChat 的 visible replies 设置成message_tool让可见输出必须通过 message 工具显式发送。(OpenClaw)可以这样理解群聊消息进入 ↓ 判断是否来自允许群 ↓ 判断是否 mention / 触发关键词 ↓ 未触发只作为 quiet context 或直接忽略 ↓ 触发进入 Agent run这层机制主要解决两个问题第一避免 Agent 在群里刷屏。 第二避免群聊中无关消息不断消耗模型资源和上下文。从源码路径看可以重点关注src/channels/mention-gating.ts src/auto-reply/group-activation.ts src/auto-reply/inbound.test.tssrc/channels目录中确实包含mention-gating.tssrc/auto-reply目录中也包含group-activation.ts和inbound.test.ts等与入站消息和群聊激活相关的文件。(GitHub)15. Dedupe为什么要入站去重外部平台经常会重投消息。例如网络断开后重连 Webhook 重试 Channel adapter 重启 平台 SDK 重放事件 Gateway 恢复连接。如果没有去重同一条消息可能触发多次 Agent run。官方消息文档说明OpenClaw 会保留一个短期缓存并以 channel、account、peer、session、message id 等信息为 key避免重复投递触发重复 Agent run。(OpenClaw)可以写成同一条外部消息第一次到达 ↓ 生成 dedupe key ↓ 没有见过继续处理 同一条外部消息再次到达 ↓ dedupe key 命中 ↓ 跳过不再触发 Agent这对于 Channel 系统非常重要因为外部平台的可靠投递机制通常会带来“至少一次投递”的问题而 OpenClaw 需要把它变成“尽量只处理一次”。16. Debounce为什么要把连续消息合并人在聊天软件中经常会连续发几条短消息你帮我看一下 这个日志 好像有错误 最后一行特别奇怪如果每条都触发一次 Agent run就会浪费资源而且 Agent 可能在用户话还没说完时就开始回答。官方消息文档说明OpenClaw 支持入站 debouncing同一 sender 的快速连续消息可以合并成一个 Agent turndebounce 按 channel conversation 作用域生效并且媒体、附件会立即 flush控制命令会绕过 debounce。(OpenClaw)可以理解为短时间内多条文本消息 ↓ 先暂存 ↓ 等待 debounceMs ↓ 合并成一个 Agent turn示例配置{ messages: { inbound: { debounceMs: 2000, byChannel: { whatsapp: 5000, slack: 1500, discord: 1500 } } } }这让 OpenClaw 更像真实聊天助手而不是“每收到一个事件就机械响应一次”。17. Channel model override不同频道可以用不同模型Channel 还可以影响模型选择。官方 Channel 配置文档说明可以使用channels.modelByChannel为特定 channel ID 绑定模型这个映射会在 session 没有已有模型 override 时生效。例如 Discord 某个频道用 Claude OpusSlack 某个频道用 GPTTelegram 某个群或 topic 用另一个模型。(OpenClaw)示意{ channels: { modelByChannel: { discord: { 123456789012345678: anthropic/claude-opus-4-6 }, slack: { C1234567890: openai/gpt-5.5 }, telegram: { -1001234567890: openai/gpt-5.4-mini, -1001234567890:topic:99: anthropic/claude-sonnet-4-6 } } } }这说明 Channel 路由不仅影响“谁来处理”和“上下文在哪里”还可能影响“用什么模型处理”。可以总结为Agent binding 决定 agentId sessionKey 决定上下文 modelByChannel 决定默认模型 send/delivery 逻辑决定回复位置。18. Outbound delivery回复如何回到原平台入站消息进入 Agent 后最终会产生回复。这个回复还要回到原来的 Channel。可以用下面的链路理解Agent 生成回复 ↓ Gateway 得到 reply payload ↓ 根据 session / lastRoute / delivery plan 找到目标 Channel ↓ Channel adapter 转成平台 API 调用 ↓ 发送到 Telegram / Slack / Discord / WhatsApp ...这里要注意一点回复不是简单打印到终端而是要根据原消息来源、线程、群聊、频道、replyTo 等信息回到正确位置。官方 Channel Routing 文档也提到入站 replies 可以携带ReplyToId、ReplyToBody、ReplyToSender等上下文信息。(OpenClaw)所以 Outbound delivery 关心的问题包括回复哪个 Channel 回复哪个 chat / room / channel / group 是否需要回复到 thread 是否引用原消息 消息过长是否要 chunk 平台是否支持图片 / 文件 / markdown 是否需要发送 typing / progress draft这也是 Channel 系统复杂的原因。19. WebChat 的特殊性WebChat 也是一种 Channel但它和 Telegram、Slack 这类外部平台不完全一样。官方 Channel Routing 文档说明WebChat 会附着到选中的 Agent并默认使用该 Agent 的 main session因此 WebChat 可以看到该 Agent 的跨 Channel 上下文。(OpenClaw)这说明 WebChat 更像是 Gateway / Agent 的本地可视化聊天入口而不是一个完全隔离的外部社交平台。可以这样理解Telegram / Slack / Discord 更偏外部消息来源。 WebChat 更偏 Gateway 控制界面中的聊天入口。所以读 WebChat 相关代码时要特别注意它和 main session 的关系。20. Multi-account同一个 Channel 可以有多个账号OpenClaw 的 Channel 不只是“一种平台一个账号”。官方 Channel 配置文档说明多账号配置适用于所有 Channel每个账号有自己的accountId例如 Telegram 可以配置default和alerts两个 bot token。(OpenClaw)示意{ channels: { telegram: { accounts: { default: { name: Primary bot, botToken: 123456:ABC... }, alerts: { name: Alerts bot, botToken: 987654:XYZ... } } } } }这样一来路由时就不能只看channeltelegram还可能要看accountId。因此完整路由字段可能包括channel accountId peer.kind peer.id guildId teamId roles threadId topicId这也解释了为什么 OpenClaw 的 binding 匹配顺序比较细。21. Channel 和配置校验Channel 配置会进入 OpenClaw 的严格配置校验体系。官方配置文档明确说明OpenClaw 只接受完全符合 schema 的配置未知字段、类型错误或非法值会导致 Gateway 拒绝启动配置失败时只能使用 doctor、logs、health、status 等诊断命令。(OpenClaw)这对 Channel 很重要。因为 Channel 配置往往包含token allowlist group policy accountId binding model override command permissions native commands这些字段如果写错可能导致Channel 启动失败 消息无法路由 群聊策略失效 模型 override 不生效 配置写操作被拒绝 Gateway 热重载跳过新配置。所以调试 Channel 时不能只看日志还要用openclaw doctor openclaw config validate openclaw channels status openclaw gateway status22. Channel 相关源码阅读路线这一期建议按下面顺序看源码第一组Channel 基础结构 src/channels/channel-config.ts src/channels/status/ src/channels/transport/ src/channels/plugins/ 第二组访问控制 src/channels/allowlists/ src/channels/message-access/ src/channels/allow-from.ts src/channels/allowlist-match.ts 第三组会话和路由 src/channels/conversation-resolution.ts src/channels/conversation-binding-context.ts src/channels/conversation-label.ts src/channels/native-command-session-targets.ts 第四组群聊激活 src/channels/mention-gating.ts src/auto-reply/group-activation.ts 第五组入站消息处理 src/channels/inbound-event/ src/auto-reply/inbound-debounce.ts src/auto-reply/dispatch.ts src/auto-reply/reply/ 第六组Gateway 管理 src/gateway/server-methods/channels.ts src/gateway/server-methods/chat.ts src/gateway/server-methods/agent.ts从目录结构看src/gateway/server-methods中确实包含channels.ts、chat.ts和agent.ts这说明 Gateway 把 Channel 管理、Chat 消息和 Agent run 分到了不同方法模块。(GitHub)23. 一条 Telegram 群消息的示例链路假设有一条 Telegram 群消息openclaw 帮我总结一下今天的部署日志它进入 OpenClaw 后大致会经过Telegram bot 收到消息 ↓ telegram channel adapter 标准化消息 ↓ 检查 groupPolicy / groupAllowFrom ↓ 检查是否满足 mention gating ↓ 根据 bindings 匹配 agentId ↓ 生成 sessionKey agent:agentId:telegram:group:groupId ↓ 如果是 forum topic嵌入 :topic:topicId ↓ 检查 dedupe ↓ 检查 debounce ↓ 进入 Agent run ↓ Agent 读取对应 transcript ↓ 模型生成回复 ↓ Channel delivery 发送回 Telegram 群或 topic可以看到这里面模型调用只是中间一环。真正复杂的是前后的工程链路。24. 一条 Slack Thread 消息的示例链路Slack thread 的链路会多一个 thread 维度Slack message in thread ↓ Slack channel adapter 标准化消息 ↓ 识别 teamId / channelId / threadTs ↓ bindings 匹配 agentId ↓ 生成 sessionKey agent:agentId:slack:channel:channelId:thread:threadId ↓ 进入独立 thread session ↓ Agent run ↓ 回复到同一个 Slack thread这种设计的好处是同一 Slack 频道内不同 thread 不会混淆上下文。25. 本期重点理解这一期可以总结为五点第一Channel 是 OpenClaw 连接外部消息平台的接入层负责把 Telegram、Slack、Discord、WhatsApp 等平台消息转换为内部消息事件。 第二Channel 由 Gateway 统一管理通常通过 openclaw.json 中的 channels.provider 配置启用。 第三外部消息进入后先经过 dmPolicy、groupPolicy、allowlist、pairing 和 mention gating 等访问控制与激活逻辑。 第四消息会通过 bindings 匹配到具体 agentId并生成对应 sessionKey从而进入正确上下文。 第五Channel 还负责去重、连续消息防抖、模型覆盖、回复投递、线程 / topic 路由和多账号场景。一句话概括OpenClaw 的 Channel 不是简单平台适配器而是外部消息进入 Agent 系统前的访问控制层、路由层和上下文边界生成层。26. 本期小结本期主要分析了 OpenClaw 的 Channel 接入机制。Channel 负责把 Telegram、Slack、Discord、WhatsApp、WebChat 等外部平台消息接入 Gateway并经过访问控制、群聊激活、Agent binding、sessionKey 生成、去重、防抖、队列和投递等流程最终触发 Agent run 并把回复发送回原平台。OpenClaw 通过channels.provider配置启用不同平台通过dmPolicy和groupPolicy控制谁能发消息通过bindings决定哪个 Agent 处理消息通过结构化sessionKey维护不同 DM、群聊、频道、thread 和 topic 的上下文边界。这一期可以用一句话总结Channel 是 OpenClaw 从“本地 Agent”走向“多平台个人助手”的关键入口它把外部消息平台、Gateway、Session 和 Agent Runtime 串成了一条完整消息链路。下一期可以继续分析OpenClaw 源码解析十消息队列、去重与防抖机制下一期重点看dedupe、debounce、active run queue、steering queue、abort、retry、streaming 和 chunking理解 OpenClaw 如何在真实聊天环境中处理重复消息、连续消息、长任务和平台消息限制。