Clawdbot 源码解读 5:抽象、消息分发与插件开发

3 阅读9分钟

前言

在上一篇中,我们梳理了 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(如 whatsapptelegrammsteams),由 ChannelPlugin 描述其元数据、配置、能力与各类 适配器(config、gateway、outbound、pairing、status 等)。内置渠道(telegram、whatsapp、discord、slack、signal、imessage、googlechat)在核心仓库的 channels/plugins/ 下实现;扩展渠道(如 msteamsmatrixzalo)在 extensions/ 下以独立包形式提供,通过插件加载器注册到 PluginRegistry.channels,与内置渠道统一由 listChannelPlugins() 暴露。

1.2 消息标准化与渠道生命周期

  • 入站:各渠道的 monitor/Webhook 收到原始消息后,经 normalize 转成统一的内部格式(会话标识、文本、附件、线程等),再进入 auto-replyrouting
  • 出站:回复、转发、广播等通过渠道的 outbound 适配器发送,由 delivermessage-action-runner 等协调。
  • 生命周期ChannelManager 在 Gateway 启动时调用 startChannels(),对每个已注册渠道调用 plugin.gateway.startAccount;停止时 stopChannelabort 并等待任务结束;markChannelLoggedOut 用于登录态失效时更新运行时状态。每个渠道可有多个 accountId(如多 Telegram bot、多 Slack workspace),由 config.listAccountIdsconfig.resolveAccount 与配置中的 channels. 决定。

1.3 路由系统:Session Key 与绑定

  • Session Key:用于持久化、去重与并发的会话标识,格式为 agent:<agentId>:<mainKey>agent:<agentId>:<channel>:<peerKind>:<peerId>(见 routing/session-key.tsbuildAgentMainSessionKeybuildAgentPeerSessionKey)。mainKey 默认为 mainpeerKinddmgroupchanneldmScope(main/per-peer/per-channel-peer)决定 DM 是否按联系人或按渠道+联系人隔离。
  • 路由解析resolveAgentRoute(cfg, channel, accountId, peer, guildId, teamId) 根据配置中的 bindings 与输入,决定 agentIdsessionKeymainSessionKey 以及 matchedBy(binding.peer / binding.guild / binding.team / binding.account / binding.channel / default)。匹配顺序:peerguildIdteamIdaccount(非 *)→ channel(accountId *)→ default(默认 agent)。
  • 绑定config.bindingsAgentBinding[],每项含 agentIdmatchchannelaccountId、可选 peerguildIdteamId)。用于「某渠道某群/某 DM 用哪个 Agent」。

1.4 群组、广播与目录

  • 群组/频道:Discord 的 guild、Slack 的 team、Teams 的 team/channel 等通过 peer.kind === "group" | "channel"guildIdteamId 参与路由;allowlistgroup policy(谁可触发 Agent)由各渠道的 groupsallowlist 等适配器提供。
  • 广播:Gateway 的 broadcast(event, payload) 向所有已连接 WS 客户端推送;渠道侧「广播组」、多会话下发由 outbounddeliver 等配合完成。
  • 目录directory 适配器提供「可选的群组/联系人列表」(如从配置或 API 拉取),供 Control UI、CLI 与工具使用。

二、代码解析

2.1 渠道注册表:内置列表与插件列表

src/channels/registry.ts

  • CHAT_CHANNEL_ORDERCHANNEL_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_ORDERmeta.order 排序返回 ChannelPlugin[]
  • getChannelPlugin(id):在 listChannelPlugins() 中按 id 查找。
  • loadChannelPlugin(id)load.ts):异步从 registry 按 id 取插件并缓存,避免重复加载。

2.2 ChannelPlugin 接口与适配器

src/channels/plugins/types.plugin.ts 定义 ChannelPlugin<ResolvedAccount>

  • idmeta(ChannelMeta)、capabilities(chatTypes、polls、threads、media 等)。
  • configChannelConfigAdapter,必选。包含 listAccountIdsresolveAccountdefaultAccountIdsetAccountEnableddeleteAccountisConfigureddescribeAccount 等,用于配置读写与账号解析。
  • gatewayChannelGatewayAdapter,可选。包含 startAccountstopAccount,供 ChannelManager 启停该渠道的「长跑」任务(如 WebSocket、Webhook 监听)。
  • outbound:发送消息;pairing:配对与通知;status:状态与探活;groups:群组策略;mentionsthreadingmessagingagentPromptdirectoryactionsheartbeat 等为可选能力扩展。
  • gatewayMethods:该渠道在 Gateway 上注册的额外方法名列表。
  • onboardingsetupsecuritycommandsstreamingauthelevatedresolveragentTools 等:CLI/向导/权限/工具等扩展点。

内置渠道的实现分布在 channels/plugins/ 下(如 normalize/telegram.tsoutbound/telegram.tsonboarding/telegram.ts);扩展渠道在 extensions/<id>/ 下实现并通过 clawdbot.plugin.json 与插件加载器注册。

2.3 ChannelManager:启停与快照

src/gateway/server-channels.tscreateChannelManager(opts)

  • getStore(channelId):按渠道维护 ChannelRuntimeStore(aborts、tasks、runtimes)。
  • startChannel(channelId, accountId?):若未指定 accountId 则对 plugin.config.listAccountIds(cfg) 中每个账号执行;对每个账号检查 isEnabledisConfigured,然后创建 AbortController、调用 plugin.gateway.startAccount,将返回的 Promise 放入 store.tasks,并在 setStatus 中更新 runninglastError 等。
  • stopChannel(channelId, accountId?):对目标账号 abort、调用 plugin.gateway.stopAccount(若有)、await task,最后清空 aborts/tasks 并 setStatus(running: false)
  • startChannels():遍历 listChannelPlugins(),对每个 plugin.id 调用 startChannel(plugin.id)
  • getRuntimeSnapshot():遍历所有渠道与账号,结合 config.resolveAccountisEnabledisConfiguredstore.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 生成。
  • buildGroupHistoryKeyresolveThreadSessionKeys:群组历史与线程会话 key 的辅助。

src/routing/resolve-route.ts

  • resolveAgentRoute(input)inputcfgchannelaccountIdpeerguildIdteamId
    • listBindings(cfg) 过滤出 match.channelmatch.accountId 匹配的 binding。
    • 按顺序尝试:matchesPeermatchesGuildmatchesTeam → account 精确匹配 → accountId * → 最后 default 使用 resolveDefaultAgentId(cfg)
    • 调用 buildAgentSessionKeybuildAgentMainSessionKey 得到 sessionKeymainSessionKeypickFirstExistingAgentId 保证 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 中优先选「已绑定」的。

绑定类型 AgentBindingconfig/types.agents.ts):agentId + matchchannelaccountId?、peer?、guildId?、teamId?)。

2.6 渠道插件开发:以 Microsoft Teams 为例

extensions/msteams/

  • clawdbot.plugin.jsonid: "msteams"channels: ["msteams"]configSchema 等,供插件加载器识别并注册到 registry.channels
  • src/channel.ts:导出 msteamsPlugin: ChannelPlugin<ResolvedMSTeamsAccount>
    • meta:id、label、selectionLabel、docsPath、blurb、aliases、order。
    • configlistAccountIds(单账号则 [DEFAULT_ACCOUNT_ID])、resolveAccount(从 cfg.channels.msteams 读 enabled、configured)、isConfigured(凭 token 等)、describeAccountsetAccountEnableddeleteAccount
    • gatewaystartAccount 启动 Bot Framework 适配器/Webhook 等;stopAccount 停止监听。
    • onboardingpairingoutboundgroupsthreadingagentPromptreloadconfigSchema 等按需实现。
  • src/ 下还有 inboundoutboundsendmonitordirectory-livetokenpolicy 等,对应入站解析、发送、探活、目录、鉴权与群组策略。

开发新渠道插件时:1)在 extensions/<id&gt/ 下新建包,package.json 中声明 clawdbot 为 peer/dev 依赖;2)clawdbot.plugin.json 中声明 idchannelsconfigSchema;3)实现 ChannelPlugin(至少 idmetacapabilitiesconfig,若需 Gateway 启停则实现 gateway);4)在配置 plugins.allowplugins.entries 中启用,并配置 channels.<id>;5)运行 clawdbot channels statusclawdbot 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 如何阅读渠道实现

  1. 从 registry 与 listChannelPlugins 入手:确认渠道 id 与 meta 是否在 CHAT_CHANNEL_ORDERregistry.channels 中。
  2. 看 config 适配器listAccountIdsresolveAccountisConfigured 如何读 cfg.channels.<id>,多账号时如何区分。
  3. 看 gateway 适配器startAccount 里启动的是什么(WebSocket、HTTP Webhook、轮询),stopAccount 如何 abort 与清理。
  4. 看 normalize/outbound:入站如何转成内部格式、出站如何调平台 API;allowlistgroups 如何限制目标与群组。
  5. 看 onboarding/pairing:向导步骤、配对流程与通知。

4.2 如何配置路由与绑定

  • 默认单 Agent:不配 bindings 时,resolveAgentRoute 始终返回 resolveDefaultAgentId(cfg)sessionKeybuildAgentSessionKey 根据 session.dmScopepeer 等生成。
  • 按渠道/账号绑定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.jsonidchannelsconfigSchema(若需)。
  • ChannelPluginidmetacapabilitiesconfig(listAccountIds、resolveAccount、defaultAccountId、isConfigured、describeAccount 等)。
  • gateway.startAccount / stopAccount:若渠道需长跑任务(WebSocket/Webhook)。
  • outbound:发送文本/附件/卡片等;normalize 入站格式(若在核心侧有统一入口)。
  • configchannels.<id>:在 config 校验(Zod/JSON Schema)中允许 channels.<id>,并在 validateConfigObjectWithPlugins 的渠道 id 白名单中加入新 id。
  • plugins.allowplugins.entries.<id>:启用插件;clawdbot channels statusclawdbot gateway run 验证。

4.4 常见问题

Q: 新渠道在 channels.status 里不显示?
A: 确认插件已加载(plugins.allowplugins.entries)、registry.channels 中有该插件;config.listAccountIds 在当前配置下应返回至少一个 id;若 isConfigured 为 false 会显示为 not configured。

Q: 消息没有路由到预期的 Agent?
A: 检查 bindingsmatchresolveAgentRoute 的输入是否一致(channel、accountId、peer、guildId、teamId);matchedBy 可在日志或调试中查看,确认命中的是哪条 binding 或 default。

Q: Session Key 冲突或重复?
A: dmScope 决定 DM 是否会按 peer 或 channel+peer 隔离;identityLinks 会把多个 id 映射到同一 canonical,从而共用同一 sessionKey;检查 buildAgentPeerSessionKey 的入参与配置的 session.dmScopesession.identityLinks


五、总结

5.1 本文要点

  • 渠道抽象ChannelPlugin 通过 configgatewayoutboundpairingstatusgroups 等适配器与核心交互;内置渠道在 channels/plugins/,扩展在 extensions/,统一由 listChannelPlugins() 暴露。
  • ChannelManagerstartChannels/ startChannel 调用 plugin.gateway.startAccountstopChannel 做 abort 与 stopAccountgetRuntimeSnapshot 汇总各渠道各账号状态;markChannelLoggedOut 更新登录态。
  • Session KeybuildAgentMainSessionKeybuildAgentPeerSessionKey 根据 agentId、channel、peerKind、peerId、dmScope、identityLinks 生成;格式为 agent:<agentId>:…
  • 路由resolveAgentRoutebindings 匹配顺序(peer → guildId → teamId → account → channel * → default)得到 agentIdsessionKeymainSessionKeymatchedBy
  • 插件开发clawdbot.plugin.json + ChannelPlugin 实现(config 必选,gateway/outbound 等按需);配置中启用插件与 channels.<id>,并通过校验白名单。

参考资源

  • 项目仓库:github.com/clawdbot/cl…
  • 官方文档:docs.clawd.bot
  • 渠道注册与列表:src/channels/registry.tssrc/channels/plugins/index.tssrc/channels/plugins/load.ts
  • 渠道插件类型:src/channels/plugins/types.plugin.tssrc/channels/plugins/types.core.ts
  • ChannelManager:src/gateway/server-channels.ts
  • 路由与 Session Key:src/routing/resolve-route.tssrc/routing/session-key.tssrc/routing/bindings.ts
  • 绑定类型:src/config/types.agents.ts(AgentBinding)
  • 渠道插件示例:extensions/msteams/clawdbot.plugin.jsonsrc/channel.ts