前言
在上一篇中,我们梳理了 Gateway 的启动、WebSocket 协议、方法处理与认证。Gateway 启动后会通过 ChannelManager 启动各消息渠道(WhatsApp、Telegram、Slack、Discord 等),消息进入后需根据渠道、账号、会话(peer/群组/频道)路由到对应的 Agent 与 Session Key,再交给自动回复与 Agent 执行。本文是《Clawdbot 源码解读》系列的第五篇,我们将深入 渠道与路由:渠道抽象层与注册(内置 vs 扩展)、消息标准化与渠道生命周期、路由系统(Session Key、绑定、群组/广播)、以及渠道插件开发(以 Microsoft Teams 为例)。
学习目标
- 理解渠道抽象层(ChannelPlugin、适配器)与注册表(registry、plugins)
- 掌握 ChannelManager 的启停与运行时快照
- 熟悉 Session Key 格式、路由解析(resolveAgentRoute)与绑定(bindings)
- 能够阅读渠道实现并完成「开发一个渠道插件」的检查清单
前置知识
- 已阅读系列前四篇(架构、CLI、配置、Gateway)
- 对消息平台(Bot API、Webhook、Socket 等)有基本概念
一、核心概念
1.1 渠道抽象层
Clawdbot 将「消息平台」抽象为 渠道(channel):每个渠道有唯一 ChannelId(如 whatsapp、telegram、msteams),由 ChannelPlugin 描述其元数据、配置、能力与各类 适配器(config、gateway、outbound、pairing、status 等)。内置渠道(telegram、whatsapp、discord、slack、signal、imessage、googlechat)在核心仓库的 channels/plugins/ 下实现;扩展渠道(如 msteams、matrix、zalo)在 extensions/ 下以独立包形式提供,通过插件加载器注册到 PluginRegistry.channels,与内置渠道统一由 listChannelPlugins() 暴露。
1.2 消息标准化与渠道生命周期
- 入站:各渠道的 monitor/Webhook 收到原始消息后,经 normalize 转成统一的内部格式(会话标识、文本、附件、线程等),再进入 auto-reply 与 routing。
- 出站:回复、转发、广播等通过渠道的 outbound 适配器发送,由 deliver 与 message-action-runner 等协调。
- 生命周期:ChannelManager 在 Gateway 启动时调用 startChannels(),对每个已注册渠道调用 plugin.gateway.startAccount;停止时 stopChannel 会 abort 并等待任务结束;markChannelLoggedOut 用于登录态失效时更新运行时状态。每个渠道可有多个 accountId(如多 Telegram bot、多 Slack workspace),由 config.listAccountIds、config.resolveAccount 与配置中的 channels. 决定。
1.3 路由系统:Session Key 与绑定
- Session Key:用于持久化、去重与并发的会话标识,格式为
agent:<agentId>:<mainKey>或agent:<agentId>:<channel>:<peerKind>:<peerId>(见 routing/session-key.ts 的 buildAgentMainSessionKey、buildAgentPeerSessionKey)。mainKey 默认为main;peerKind 为dm、group、channel;dmScope(main/per-peer/per-channel-peer)决定 DM 是否按联系人或按渠道+联系人隔离。 - 路由解析:resolveAgentRoute(cfg, channel, accountId, peer, guildId, teamId) 根据配置中的 bindings 与输入,决定 agentId、sessionKey、mainSessionKey 以及 matchedBy(binding.peer / binding.guild / binding.team / binding.account / binding.channel / default)。匹配顺序:peer → guildId → teamId → account(非
*)→ channel(accountId*)→ default(默认 agent)。 - 绑定:config.bindings 为 AgentBinding[],每项含 agentId 与 match(channel、accountId、可选 peer、guildId、teamId)。用于「某渠道某群/某 DM 用哪个 Agent」。
1.4 群组、广播与目录
- 群组/频道:Discord 的 guild、Slack 的 team、Teams 的 team/channel 等通过 peer.kind === "group" | "channel" 与 guildId、teamId 参与路由;allowlist、group policy(谁可触发 Agent)由各渠道的 groups、allowlist 等适配器提供。
- 广播:Gateway 的 broadcast(event, payload) 向所有已连接 WS 客户端推送;渠道侧「广播组」、多会话下发由 outbound 与 deliver 等配合完成。
- 目录:directory 适配器提供「可选的群组/联系人列表」(如从配置或 API 拉取),供 Control UI、CLI 与工具使用。
二、代码解析
2.1 渠道注册表:内置列表与插件列表
src/channels/registry.ts:
- CHAT_CHANNEL_ORDER、CHANNEL_IDS:内置渠道 id 顺序(telegram、whatsapp、discord、googlechat、slack、signal、imessage)。
- CHAT_CHANNEL_META:每个内置渠道的 ChannelMeta(id、label、selectionLabel、docsPath、blurb、systemImage 等)。
- normalizeChatChannelId(raw):将 raw 规范为 ChatChannelId,支持别名(如 imsg→imessage、google-chat→googlechat)。
- normalizeAnyChannelId(raw):在 requireActivePluginRegistry() 已初始化的前提下,从 registry.channels 中按 id/aliases 查找,返回任意已注册的 ChannelId(含扩展渠道)。
src/channels/plugins/index.ts:
- listChannelPlugins():从 requireActivePluginRegistry() 取 registry.channels,去重后按 CHAT_CHANNEL_ORDER 与 meta.order 排序返回 ChannelPlugin[]。
- getChannelPlugin(id):在 listChannelPlugins() 中按 id 查找。
- loadChannelPlugin(id)(load.ts):异步从 registry 按 id 取插件并缓存,避免重复加载。
2.2 ChannelPlugin 接口与适配器
src/channels/plugins/types.plugin.ts 定义 ChannelPlugin<ResolvedAccount>:
- id、meta(ChannelMeta)、capabilities(chatTypes、polls、threads、media 等)。
- config:ChannelConfigAdapter,必选。包含 listAccountIds、resolveAccount、defaultAccountId、setAccountEnabled、deleteAccount、isConfigured、describeAccount 等,用于配置读写与账号解析。
- gateway:ChannelGatewayAdapter,可选。包含 startAccount、stopAccount,供 ChannelManager 启停该渠道的「长跑」任务(如 WebSocket、Webhook 监听)。
- outbound:发送消息;pairing:配对与通知;status:状态与探活;groups:群组策略;mentions、threading、messaging、agentPrompt、directory、actions、heartbeat 等为可选能力扩展。
- gatewayMethods:该渠道在 Gateway 上注册的额外方法名列表。
- onboarding、setup、security、commands、streaming、auth、elevated、resolver、agentTools 等:CLI/向导/权限/工具等扩展点。
内置渠道的实现分布在 channels/plugins/ 下(如 normalize/telegram.ts、outbound/telegram.ts、onboarding/telegram.ts);扩展渠道在 extensions/<id>/ 下实现并通过 clawdbot.plugin.json 与插件加载器注册。
2.3 ChannelManager:启停与快照
src/gateway/server-channels.ts 中 createChannelManager(opts):
- getStore(channelId):按渠道维护 ChannelRuntimeStore(aborts、tasks、runtimes)。
- startChannel(channelId, accountId?):若未指定 accountId 则对 plugin.config.listAccountIds(cfg) 中每个账号执行;对每个账号检查 isEnabled、isConfigured,然后创建 AbortController、调用 plugin.gateway.startAccount,将返回的 Promise 放入 store.tasks,并在 setStatus 中更新 running、lastError 等。
- stopChannel(channelId, accountId?):对目标账号 abort、调用 plugin.gateway.stopAccount(若有)、await task,最后清空 aborts/tasks 并 setStatus(running: false)。
- startChannels():遍历 listChannelPlugins(),对每个 plugin.id 调用 startChannel(plugin.id)。
- getRuntimeSnapshot():遍历所有渠道与账号,结合 config.resolveAccount、isEnabled、isConfigured、store.runtimes 组装 ChannelRuntimeSnapshot(channels、channelAccounts),供 channels.status、Control UI 等使用。
- markChannelLoggedOut(channelId, cleared, accountId?):将对应 runtime 设为未连接、lastError 可选设为
"logged out"。
2.4 Session Key 与路由解析
src/routing/session-key.ts:
- buildAgentMainSessionKey({ agentId, mainKey }):产出
agent:<agentId>:<mainKey>,默认 mainKey 为main。 - buildAgentPeerSessionKey({ agentId, mainKey, channel, peerKind, peerId, dmScope, identityLinks }):
- peerKind === "dm" 且 dmScope === "main"(或未填 peerId)时,退化为 buildAgentMainSessionKey。
- dmScope === "per-peer" 时为
agent:<agentId>:dm:<peerId>。 - dmScope === "per-channel-peer" 时为
agent:<agentId>:<channel>:dm:<peerId>。 - group / channel 时为
agent:<agentId>:<channel>:<peerKind>:<peerId>。
- identityLinks:用于跨渠道同一用户映射,将不同 channel:peerId 归一为同一 canonical,再参与 sessionKey 生成。
- buildGroupHistoryKey、resolveThreadSessionKeys:群组历史与线程会话 key 的辅助。
src/routing/resolve-route.ts:
- resolveAgentRoute(input):input 含 cfg、channel、accountId、peer、guildId、teamId。
- 用 listBindings(cfg) 过滤出 match.channel 与 match.accountId 匹配的 binding。
- 按顺序尝试:matchesPeer → matchesGuild → matchesTeam → account 精确匹配 → accountId
*→ 最后 default 使用 resolveDefaultAgentId(cfg)。 - 调用 buildAgentSessionKey、buildAgentMainSessionKey 得到 sessionKey、mainSessionKey,pickFirstExistingAgentId 保证 agentId 在 agents.list 中存在。
- 返回 ResolvedAgentRoute(agentId、channel、accountId、sessionKey、mainSessionKey、matchedBy)。
2.5 绑定列表与工具
src/routing/bindings.ts:
- listBindings(cfg):返回 cfg.bindings ?? []。
- listBoundAccountIds(cfg, channelId):某渠道下所有非
*的 accountId 列表。 - resolveDefaultAgentBoundAccountId(cfg, channelId):默认 agent 在该渠道上绑定的第一个 accountId。
- buildChannelAccountBindings(cfg):得到 Map<channelId, Map<agentId, accountIds[]>>,便于按渠道/Agent 查绑定账号。
- resolvePreferredAccountId:在多个 accountId 中优先选「已绑定」的。
绑定类型 AgentBinding(config/types.agents.ts):agentId + match(channel、accountId?、peer?、guildId?、teamId?)。
2.6 渠道插件开发:以 Microsoft Teams 为例
extensions/msteams/:
- clawdbot.plugin.json:id: "msteams",channels: ["msteams"],configSchema 等,供插件加载器识别并注册到 registry.channels。
- src/channel.ts:导出 msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount>。
- meta:id、label、selectionLabel、docsPath、blurb、aliases、order。
- config:listAccountIds(单账号则
[DEFAULT_ACCOUNT_ID])、resolveAccount(从 cfg.channels.msteams 读 enabled、configured)、isConfigured(凭 token 等)、describeAccount、setAccountEnabled、deleteAccount。 - gateway:startAccount 启动 Bot Framework 适配器/Webhook 等;stopAccount 停止监听。
- onboarding、pairing、outbound、groups、threading、agentPrompt、reload、configSchema 等按需实现。
- src/ 下还有 inbound、outbound、send、monitor、directory-live、token、policy 等,对应入站解析、发送、探活、目录、鉴权与群组策略。
开发新渠道插件时:1)在 extensions/<id>/ 下新建包,package.json 中声明 clawdbot 为 peer/dev 依赖;2)clawdbot.plugin.json 中声明 id、channels、configSchema;3)实现 ChannelPlugin(至少 id、meta、capabilities、config,若需 Gateway 启停则实现 gateway);4)在配置 plugins.allow 或 plugins.entries 中启用,并配置 channels.<id>;5)运行 clawdbot channels status 与 clawdbot gateway run 验证启停与路由。
三、架构图解
3.1 渠道注册与 ChannelManager
graph TB
subgraph 注册
R[registry.ts CHANNEL_IDS + META]
P[PluginRegistry.channels]
end
R --> L[listChannelPlugins]
P --> L
L --> M[createChannelManager]
M --> S[startChannels / startChannel]
S --> G[plugin.gateway.startAccount]
G --> T[store.tasks + setStatus]
M --> Sn[getRuntimeSnapshot]
3.2 路由解析流程
graph LR
A[入站消息] --> B[channel, accountId, peer/guildId/teamId]
B --> C[resolveAgentRoute]
C --> D[listBindings 过滤 channel+accountId]
D --> E{匹配顺序}
E -->|1| F[peer]
E -->|2| G[guildId]
E -->|3| H[teamId]
E -->|4| I[account 精确]
E -->|5| J[accountId *]
E -->|6| K[default agent]
F --> L[buildAgentSessionKey]
G --> L
H --> L
I --> L
J --> L
K --> L
L --> M[ResolvedAgentRoute: agentId, sessionKey, mainSessionKey]
3.3 Session Key 与绑定
graph TB
subgraph 配置
B[bindings]
end
subgraph 输入
I[channel, accountId, peer, guildId, teamId]
end
I --> R[resolveAgentRoute]
B --> R
R --> O[agentId, sessionKey, mainSessionKey, matchedBy]
O --> SK[buildAgentPeerSessionKey / buildAgentMainSessionKey]
SK --> DM[dmScope: main / per-peer / per-channel-peer]
SK --> GR[group / channel peerId]
四、实践建议
4.1 如何阅读渠道实现
- 从 registry 与 listChannelPlugins 入手:确认渠道 id 与 meta 是否在 CHAT_CHANNEL_ORDER 或 registry.channels 中。
- 看 config 适配器:listAccountIds、resolveAccount、isConfigured 如何读 cfg.channels.<id>,多账号时如何区分。
- 看 gateway 适配器:startAccount 里启动的是什么(WebSocket、HTTP Webhook、轮询),stopAccount 如何 abort 与清理。
- 看 normalize/outbound:入站如何转成内部格式、出站如何调平台 API;allowlist、groups 如何限制目标与群组。
- 看 onboarding/pairing:向导步骤、配对流程与通知。
4.2 如何配置路由与绑定
- 默认单 Agent:不配 bindings 时,resolveAgentRoute 始终返回 resolveDefaultAgentId(cfg),sessionKey 由 buildAgentSessionKey 根据 session.dmScope、peer 等生成。
- 按渠道/账号绑定:bindings: [{ agentId: "main", match: { channel: "telegram", accountId: "work" } }] 表示 telegram 的 work 账号用 main agent。
- 按群/频道绑定:match 中加 guildId(Discord)、teamId(Slack/Teams)或 peer: { kind: "group", id: "xxx" },该会话会命中对应 agentId。
- accountId *:match: { channel: "whatsapp", accountId: "*" } 表示该渠道任意账号都用该 agent,常用于单账号渠道。
4.3 渠道插件开发检查清单
- extensions/<id>/package.json:name、version、peer/dev 依赖 clawdbot。
- clawdbot.plugin.json:id、channels、configSchema(若需)。
- ChannelPlugin:id、meta、capabilities、config(listAccountIds、resolveAccount、defaultAccountId、isConfigured、describeAccount 等)。
- gateway.startAccount / stopAccount:若渠道需长跑任务(WebSocket/Webhook)。
- outbound:发送文本/附件/卡片等;normalize 入站格式(若在核心侧有统一入口)。
- config 与 channels.<id>:在 config 校验(Zod/JSON Schema)中允许 channels.<id>,并在 validateConfigObjectWithPlugins 的渠道 id 白名单中加入新 id。
- plugins.allow 或 plugins.entries.<id>:启用插件;clawdbot channels status、clawdbot gateway run 验证。
4.4 常见问题
Q: 新渠道在 channels.status 里不显示?
A: 确认插件已加载(plugins.allow 或 plugins.entries)、registry.channels 中有该插件;config.listAccountIds 在当前配置下应返回至少一个 id;若 isConfigured 为 false 会显示为 not configured。
Q: 消息没有路由到预期的 Agent?
A: 检查 bindings 的 match 与 resolveAgentRoute 的输入是否一致(channel、accountId、peer、guildId、teamId);matchedBy 可在日志或调试中查看,确认命中的是哪条 binding 或 default。
Q: Session Key 冲突或重复?
A: dmScope 决定 DM 是否会按 peer 或 channel+peer 隔离;identityLinks 会把多个 id 映射到同一 canonical,从而共用同一 sessionKey;检查 buildAgentPeerSessionKey 的入参与配置的 session.dmScope、session.identityLinks。
五、总结
5.1 本文要点
- 渠道抽象:ChannelPlugin 通过 config、gateway、outbound、pairing、status、groups 等适配器与核心交互;内置渠道在 channels/plugins/,扩展在 extensions/,统一由 listChannelPlugins() 暴露。
- ChannelManager:startChannels/ startChannel 调用 plugin.gateway.startAccount,stopChannel 做 abort 与 stopAccount;getRuntimeSnapshot 汇总各渠道各账号状态;markChannelLoggedOut 更新登录态。
- Session Key:buildAgentMainSessionKey、buildAgentPeerSessionKey 根据 agentId、channel、peerKind、peerId、dmScope、identityLinks 生成;格式为 agent:<agentId>:…。
- 路由:resolveAgentRoute 按 bindings 匹配顺序(peer → guildId → teamId → account → channel * → default)得到 agentId、sessionKey、mainSessionKey、matchedBy。
- 插件开发:clawdbot.plugin.json + ChannelPlugin 实现(config 必选,gateway/outbound 等按需);配置中启用插件与 channels.<id>,并通过校验白名单。
参考资源
- 项目仓库:github.com/clawdbot/cl…
- 官方文档:docs.clawd.bot
- 渠道注册与列表:
src/channels/registry.ts、src/channels/plugins/index.ts、src/channels/plugins/load.ts - 渠道插件类型:
src/channels/plugins/types.plugin.ts、src/channels/plugins/types.core.ts - ChannelManager:
src/gateway/server-channels.ts - 路由与 Session Key:
src/routing/resolve-route.ts、src/routing/session-key.ts、src/routing/bindings.ts - 绑定类型:
src/config/types.agents.ts(AgentBinding) - 渠道插件示例:
extensions/msteams/(clawdbot.plugin.json、src/channel.ts)