OpenClaw 设计解析(四):多通道架构——从 WhatsApp 到 Discord 的统一抽象

OpenClaw 多通道架构:将不同消息平台统一到 ChannelPlugin 接口。

本篇介绍 OpenClaw 的多通道(multi-channel)架构——让同一个 AI 助手接入 WhatsApp、Telegram、Discord、Signal、Slack、iMessage、Matrix、IRC 等平台。


1. 为什么多通道是核心设计

OpenClaw 的目标是把 AI 助手放到已有的对话场景。每个即时通讯平台的接入方式差异很大:

平台 接入方式 难点
Telegram Bot API(HTTP long-polling / webhook) 最简单,注册 BotFather 即用
Discord Bot API(WebSocket Gateway) Guild/Channel/Thread 层级复杂
WhatsApp WebSocket 逆向(Baileys) 无官方 Bot API,需 QR 配对
Signal signal-cli(REST wrapper) 链接设备、身份验证复杂
Slack Socket Mode OAuth 应用注册 + 事件订阅
Matrix Client-Server API 去中心化,homeserver 多样

OpenClaw 用 ChannelPlugin 体系统一这些差异。


2. Channel 抽象层

通道注册表

系统维护一个有序的通道 ID 列表,定义了内置通道的优先级排序,直接影响 CLI onboarding 时的通道选择顺序(Telegram 排第一,因为最容易上手):

1
2
3
4
5
6
7
8
9
10
11
// 内置通道优先级(影响 CLI 引导顺序)
const CHAT_CHANNEL_ORDER = [
"telegram",
"whatsapp",
"discord",
"irc",
"googlechat",
"slack",
"signal",
"imessage",
]

每个通道都有元数据描述,包含 UI 展示名、文档路径、简介说明、图标等信息,用于 onboarding 向导展示:

1
2
3
4
5
6
7
8
9
// 每个通道的元数据(以 Telegram 为例)
{
id: "telegram",
label: "Telegram", // UI 展示名
selectionLabel: "Telegram (Bot API)", // 选择列表中的名称
docsPath: "/channels/telegram", // 文档路径
blurb: "simplest way to get started...", // onboarding 简介
systemImage: "paperplane", // SF Symbols 图标
}

通道 ID 归一化

通道 ID 支持别名(如 imsgimessagegchatgooglechat)。对于扩展通道(如 Matrix、MS Teams),归一化时会同时查询插件注册表,在已注册插件的 idaliases 中匹配,从而统一处理内置通道和动态加载的扩展通道。


3. ChannelPlugin 接口:30+ 个 adapter 的协议

OpenClaw 通道体系的核心是 ChannelPlugin 类型。它是一个包含 30 多个可选 adapter 的大接口——每个 adapter 负责通道行为的一个切面:

flowchart TB subgraph CP["ChannelPlugin 接口(所有 adapter 可选,按需实现)"] direction LR B["onboarding\n引导向导"] ~~~ C["config\n账号解析"] ~~~ D["security\nDM策略"] ~~~ E["outbound\n发送消息"] ~~~ F["gateway\n启动/停止"] G["pairing\n配对授权"] ~~~ H["mentions\n@提及"] ~~~ I["streaming\n流式输出"] ~~~ J["threading\n线程/话题"] ~~~ K["messaging\n消息格式化"] L["auth\n认证"] ~~~ M["groups\n群组策略"] ~~~ N["actions\n消息操作"] ~~~ O["heartbeat\n心跳检测"] ~~~ P["agentTools\n通道专属工具"] end Q["极简通道: 只需 id + meta + capabilities + config"] R["完整通道(如 Telegram): 实现几乎所有 adapter"] CP ~~~ Q ~~~ R

核心结构如下(伪代码):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
type ChannelPlugin = {
id: ChannelId // 通道标识
meta: ChannelMeta // 元数据(名称、图标等)
capabilities: ChannelCapabilities // 能力声明

// --- 30+ 个可选适配器,按需实现 ---
onboarding? // CLI 引导向导
config // 账号配置解析(唯一必选)
pairing? // 配对/授权
security? // DM 策略、访问控制
gateway? // 启动/停止连接
outbound? // 发送消息
streaming? // 流式输出
threading? // 线程/话题
messaging? // 消息格式化
mentions? // @提及处理
actions? // 消息操作(回应等)
heartbeat? // 心跳检测
agentTools? // 通道专属 Agent 工具
// ... 以及 auth, groups, directory, commands 等
}

capabilities 声明通道支持的特性:

1
2
3
4
5
6
7
capabilities: {
chatTypes: ["direct", "group", "thread"],
polls: true, // 投票
reactions: true, // 表情回应
threads: true, // 线程
media: true, // 媒体文件
}

设计要点:

  • 所有 adapter 都是可选的——通道只需实现自己支持的部分
  • 一个极简通道可能只有 idmetacapabilitiesconfig
  • Telegram 作为最成熟的通道实现了几乎所有 adapter
  • 支持热重载——通过 reload.configPrefixes 配置,当用户修改相关配置时无需重启 gateway

4. 内置通道 vs 扩展通道

OpenClaw 的通道分两层:

graph TB subgraph builtin["内置通道 (src/, 随核心发布)"] B1["Telegram — 最易上手\nWhatsApp — QR 配对\nDiscord — Bot API"] B2["Slack — Socket Mode\nSignal — signal-cli\niMessage — macOS only"] end subgraph ext["扩展通道 (extensions/, 独立 npm 包)"] E1["Matrix · MS Teams · 飞书\nIRC · Google Chat · LINE"] E2["Nostr · Twitch · Mattermost\nZalo · BlueBubbles · ..."] end builtin --> MC["统一 MsgContext"] ext --> MC MC --> AGENT["Agent 运行时"]

扩展通道是独立的 npm 包,runtime 通过 jiti 别名加载 openclaw/plugin-sdk,无需在 dependencies 中声明 workspace:*(那会破坏 npm install)。这意味着社区可以在不 fork 核心代码的情况下添加新通道


5. 通道实现剖析:以 Telegram 为例

Telegram 是最成熟的内置通道,也是推荐的入门通道。

Bot 初始化

Bot 启动时需要配置以下关键参数:

1
2
3
4
5
6
7
8
// Telegram Bot 核心配置
{
token: string // Bot API token
allowFrom: string[] // 白名单:谁能使用这个 bot
mediaMaxMb: number // 媒体下载大小限制
requireMention: boolean // 群聊是否需要 @提及才响应
updateOffset: number // update ID 水位线,防止重启后重复处理
}

updateOffset(水位线)是生产环境的关键细节——它持久化了已处理的最新 update ID,确保 bot 重启后不会重复处理消息。

消息处理流程

Telegram 的消息处理采用工厂模式 + 依赖注入

flowchart LR A["收到 Telegram Update"] --> B["聚合 Media Group"] B --> C["构建统一 MsgContext"] C --> D["Agent 处理"] D --> E["响应发回通道"]

初始化时通过工厂函数注入所有依赖(config、logger、stream mode 等),返回一个纯函数处理器。这种模式方便测试——可以 mock 任何依赖而不需要启动真正的 Telegram bot。

Media Group 聚合:Telegram 会把一次发送的多张图片拆成多个独立 update,处理器需要先将它们聚合回一条消息。


6. 扩展通道剖析:以 Matrix 为例

Matrix 通道展示了 ChannelPlugin 接口的完整实现模式。以下是核心结构(省略具体实现):

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
const matrixPlugin: ChannelPlugin = {
id: "matrix",
meta: {
/* 名称、图标、文档链接等 */
},

capabilities: {
chatTypes: ["direct", "group", "thread"],
polls: true,
reactions: true,
threads: true,
media: true,
},

// 热重载:修改 channels.matrix.* 配置时无需重启
reload: { configPrefixes: ["channels.matrix"] },

// 配对:陌生人需审批后才能对话
pairing: {
normalizeAllowEntry: (entry) => entry.replace(/^matrix:/i, ""),
notifyApproval: (id) => sendMessage(id, "已批准"),
},

// 账号配置:列出、解析、描述账号
config: {
listAccountIds,
resolveAccount,
defaultAccountId,
isConfigured: (account) => account.configured,
},

// 安全策略:默认 pairing 模式
security: {
resolveDmPolicy: ({ account }) => ({
policy: account.config.dm?.policy ?? "pairing",
allowFrom: account.config.dm?.allowFrom ?? [],
}),
},

// 线程:将 Matrix 线程 ID 映射到统一 context
threading: { resolveReplyToMode, buildToolContext },

// 消息:判断输入是否为 Matrix ID(如 @user:matrix.org)
messaging: {
targetResolver: {
looksLikeId: (raw) => /^(matrix:)?[!#@]/.test(raw),
},
},

// ... gateway, status, directory 等
}

设计要点:

  • security 默认 "pairing" 模式,陌生人需先获批准才能对话,比 "open" 更安全
  • messaging.targetResolver 用正则判断输入是否为 Matrix ID(如 @user:matrix.org
  • threading 将 Matrix 线程 ID 映射到统一的 tool context,让 Agent 在正确的线程中回复

7. 消息路由:7 层绑定匹配

当一条消息进来时,OpenClaw 需要决定交给哪个 Agent 处理

为什么需要分层路由

假设你有多个 Agent:support-agent 负责客服、admin-agent 负责管理、general-agent 做通用聊天。你在配置里写了一堆规则(bindings),每条规则说”满足条件 X 的消息交给 Agent Y”。

问题是:一条消息可能同时匹配多条规则。Discord 的 #support 频道里的消息,既匹配”这个频道”的规则,也匹配”这个 Guild”的规则,还匹配”所有 Discord”的规则。谁优先?

OpenClaw 用 7 层优先级解决这个冲突——从最精确到最宽泛,第一个匹配的就生效

flowchart TD MSG["入站消息"] --> N["归一化\nchannel → 小写\naccountId → 默认 default\npeer.kind → dm 转 direct"] N --> R{"按优先级逐层匹配"} R -->|"① peer"| P1["精确匹配对话对象\n例:#support 频道"] R -->|"② peer.parent"| P2["父级匹配\n例:#support 下的线程"] R -->|"③ guild+roles"| P3["Guild + 角色\n例:Guild-X 的 admin"] R -->|"④ guild"| P4["Guild 匹配\n例:Guild-X 所有消息"] R -->|"⑤ team"| P5["团队匹配\n例:Slack workspace"] R -->|"⑥ account"| P6["Bot 账号匹配\n例:bot-1 的所有消息"] R -->|"⑦ channel"| P7["通道匹配(最宽泛)\n例:所有 Discord 消息"] R -->|"ⓧ 都没匹配"| P8["兜底 → 默认 Agent"] P1 & P2 & P3 & P4 & P5 & P6 & P7 & P8 --> KEY["构建 sessionKey\n决定会话隔离粒度"] KEY --> RES["返回 { agentId, sessionKey, matchedBy }"]

具体例子:4 条消息的路由过程

假设你的 Discord 服务器配了这些规则:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
bindings:
# 规则 A: #support 频道 → support-agent
- agentId: support-agent
match: { channel: discord, peer: { kind: channel, id: "C-SUPPORT" } }

# 规则 B: Guild-X 中有 admin 角色的用户 → admin-agent
- agentId: admin-agent
match: { channel: discord, guildId: "Guild-X", roles: ["admin-role"] }

# 规则 C: Guild-X 所有消息 → community-agent
- agentId: community-agent
match: { channel: discord, guildId: "Guild-X" }

# 规则 D: 所有 Discord 消息(通配 accountId: "*")→ general-agent
- agentId: general-agent
match: { channel: discord, accountId: "*" }

现在来了 4 条不同的消息,看它们各自怎么被路由:

flowchart TD subgraph msg1["消息 1: #support 频道里有人提问"] direction LR M1["peer=C-SUPPORT\nguild=Guild-X\nroles=无"] M1 -->|"① peer 匹配规则 A ✓"| R1["→ support-agent"] end subgraph msg2["消息 2: admin 在 #general 发消息"] direction LR M2["peer=C-GENERAL\nguild=Guild-X\nroles=admin-role"] M2 -->|"① peer 没匹配\n② parent 没有\n③ guild+roles 匹配规则 B ✓"| R2["→ admin-agent"] end
flowchart TD subgraph msg3["消息 3: 普通用户在 #general 发消息"] direction LR M3["peer=C-GENERAL\nguild=Guild-X\nroles=无"] M3 -->|"①②③ 都没匹配\n④ guild 匹配规则 C ✓"| R3["→ community-agent"] end subgraph msg4["消息 4: 另一个 Guild 的消息"] direction LR M4["peer=C-OTHER\nguild=Guild-Y\nroles=无"] M4 -->|"①②③④⑤⑥ 都没匹配\n⑦ channel 匹配规则 D ✓"| R4["→ general-agent"] end

关键细节:规则 A 同时指定了 peer 和隐含的 channel,但它只在 Tier 1(peer)尝试匹配。如果 peer 不匹配,它不会”降级”到更宽泛的层级去碰运气——每条规则只在它最精确的层级参与匹配。

线程继承(Tier 2: peer.parent)

Discord 的线程(Thread)带来一个有趣问题:

flowchart LR subgraph 没有Tier2["没有 Tier 2 的情况"] direction TB CH1["#support\nid=C-SUPPORT"] -->|"用户创建线程"| TH1["线程: 讨论 Bug-123\nid=thread-456"] TH1 -->|"① peer: thread-456 ✗\n③④⑤⑥⑦ 逐层匹配..."| WRONG["可能落到 community-agent\n而不是 support-agent"] end subgraph 有Tier2["有 Tier 2 的情况"] direction TB CH2["#support\nid=C-SUPPORT"] -->|"用户创建线程"| TH2["线程: 讨论 Bug-123\nid=thread-456\nparentPeer=C-SUPPORT"] TH2 -->|"① peer: thread-456 ✗\n② parent: C-SUPPORT ✓"| RIGHT["→ support-agent ✓\n线程继承父频道的 Agent"] end 没有Tier2 ~~~ 有Tier2

线程的 parentPeer 指向它的父频道。Tier 2 用父频道的 ID 去匹配规则,这样线程里的消息也交给正确的 Agent。注意,session key 仍然用线程自己的 ID,所以每个线程有独立的会话历史——只是 Agent 选择继承自父频道。

角色匹配(Tier 3: guild+roles)

角色匹配采用 OR 逻辑——只要用户拥有规则中列出的任意一个角色就算匹配:

1
2
3
4
规则:roles: ["moderator", "admin"]

用户角色: ["member", "admin"] → admin ∈ roles → 匹配 ✓
用户角色: ["member"] → 无交集 → 不匹配 ✗

Session Key:路由之后的会话隔离

路由决定了”消息交给谁”,而 session key 决定了”在哪个对话里”。

群聊/频道的 session key 是固定的——每个频道一个独立会话:

1
agent:support-agent:discord:channel:C-SUPPORT

私聊(DM) 则通过 dmScope 配置提供 4 种隔离粒度:

flowchart TD DM["收到私聊消息\nagent=main, channel=discord, peer=alice"] --> SCOPE{"dmScope 配置?"} SCOPE -->|"main(默认)"| S1["session key: agent:main:main\n所有私聊共享一个会话\nAlice 和 Bob 看到相同上下文"] SCOPE -->|"per-peer"| S2["session key: agent:main:direct:alice\n每人独立会话\n但跨平台的 alice 共享"] SCOPE -->|"per-channel-peer"| S3["session key: agent:main:discord:direct:alice\n每个平台每人独立\nTelegram 和 Discord 的 alice 分开"] SCOPE -->|"per-account-channel-peer"| S4["session key: agent:main:discord:bot1:direct:alice\n最细粒度\n每个 bot 账号 × 平台 × 人"]

大多数多用户场景用 per-channel-peer(每个平台每个用户一个独立会话)。默认的 main 适合个人用——只有你一个人私聊 bot,所有消息在同一个上下文里。

跨平台身份合并(Identity Links)

如果 Alice 同时在 Telegram(ID: 111111)和 Discord(ID: 222222)私聊 bot,默认是两个独立会话。通过 identityLinks 可以合并:

1
2
3
4
session:
dmScope: per-channel-peer
identityLinks:
alice: ["telegram:111111", "discord:222222"]
flowchart LR subgraph before["没有 Identity Links"] TG1["Telegram:111111"] --> SK1["session key:\nagent:main:telegram:direct:111111"] DC1["Discord:222222"] --> SK2["session key:\nagent:main:discord:direct:222222"] SK1 -.- NOTE1["两个独立会话\nAlice 换平台后丢失上下文"] end subgraph after["有 Identity Links"] TG2["Telegram:111111"] --> LINK["查 identityLinks\n111111 → alice"] DC2["Discord:222222"] --> LINK LINK --> SK3["peerId 替换为 alice\n两个平台共享会话上下文"] end

Alice 在 Telegram 聊到一半切到 Discord 继续,bot 仍然记得之前的对话。

防回环:Bot 不会回复自己

在群聊场景中,Agent 回复的消息会被平台重新推送给 bot 自身。如果不处理,就会出现 bot 回复自己、自己再回复、无限循环的情况。OpenClaw 在消息进入路由之前就做了过滤,确保 bot 的回复永远不会进入 7 层匹配:

flowchart LR U["用户发消息"] --> AGENT["Agent 处理并回复"] AGENT --> CH["回复发到频道"] CH --> PUSH["平台推送这条回复\n给 bot 自身"] PUSH --> FILTER{"是自己发的消息?"} FILTER -->|"是"| DROP["丢弃,不进入路由"] FILTER -->|"否"| ROUTE["进入 7 层路由"]

各平台的过滤机制不同,但效果一致:

平台 自身消息过滤 其他 Bot 消息
Telegram 平台保证:Bot API 根本不推送 bot 自己的消息 群聊中平台也不推送其他 bot 的消息
Discord 检查 author.id === botUserId → 丢弃 默认丢弃(allowBots 可开启)
WhatsApp 检查 fromMe 标记 → 丢弃
Slack 检查 bot_id + user === botUserId → 丢弃 默认丢弃(allowBots 可开启)
Signal 对比发送者手机号/UUID → 丢弃

Discord 和 Slack 提供了 allowBots 配置——如果你故意想让 Agent 响应其他 bot 的消息(比如 bot 之间协作),可以打开这个开关。但 bot 回复自己是永远屏蔽的,不可配置。


8. 会话记录与消息归一化

路由完成后,消息需要被记录到 session store。核心流程分两步:

flowchart LR MSG["入站消息"] --> A["① 记录 session 元数据\n(发送者、通道、时间)"] MSG --> B["② 更新 last route\n(最后消息的投递上下文)"] A -.- NOTE1["异步执行,不阻塞消息处理"] B --> C["Agent 主动发消息时\n知道该通过哪个通道投递"]

last route 记录了”最后一次收到消息的通道和目标”。当 Agent 需要主动发送消息时,就知道该通过哪个通道投递。

归一化细节:session key 统一做 trim().toLowerCase() 处理,确保 Telegram:123telegram:123 指向同一个 session。


9. 访问控制

安全是多通道架构不可忽略的一环。访问控制的核心判断逻辑:

flowchart LR REQ["收到消息"] --> CHECK{"白名单\n是否为空?"} CHECK -->|"空"| POLICY["由 DM 策略决定"] CHECK -->|"有条目"| WILD{"包含\n通配符 * ?"} WILD -->|"是"| ALLOW["放行"] WILD -->|"否"| MATCH{"senderId\n在列表中?"} MATCH -->|"是"| ALLOW MATCH -->|"否"| DENY["拒绝"]

白名单来源有两个:配置文件中明确写的 allowFrompairing store 中已配对的用户。两者合并后作为最终白名单。

DM 策略有三种模式:

  • "open" — 任何人都可以发消息
  • "pairing" — 陌生人需要配对审批,已配对用户自动放行(合并 config + pairing store)
  • "allowlist" — 只看配置中明确列出的用户(不合并 pairing store)

10. WhatsApp Web 的特殊实现

WhatsApp 没有官方 Bot API,OpenClaw 通过 Baileys 库(@whiskeysockets/baileys)实现 WebSocket 逆向接入。

QR 码配对

sequenceDiagram participant User as 用户手机 participant OC as OpenClaw participant WA as WhatsApp 服务器 OC->>WA: 创建 WebSocket 连接 WA-->>OC: 返回 QR 码数据 OC->>OC: 生成 QR 码(有效期 3 分钟) User->>OC: 扫描 QR 码 OC->>WA: 完成配对 WA-->>OC: 连接成功 Note over OC,WA: 若收到 515 错误码,自动重连一次

关键设计:

  • QR 有效期 3 分钟(WhatsApp 限制),超时后需重新生成
  • 515 错误处理:WhatsApp 有时在配对成功后要求重连,系统会自动重试一次
  • 并发安全:用 Map 管理多个账号的登录状态,通过 login ID 防止过期回调覆盖新连接

消息监听与防碎片化

WhatsApp 用户经常连发多条消息,每条都触发 AI 回复会导致碎片化响应。系统通过 Debouncer 解决:

flowchart LR subgraph debounce["debounceMs 时间窗口内"] M1["你好"] --> AGG["聚合"] M2["帮我"] --> AGG M3["查下天气"] --> AGG end AGG --> MERGED["合并后的消息:\n你好\n帮我\n查下天气"] MERGED --> AGENT["Agent 处理"]

聚合策略:

  • 聚合键 = accountId + conversationKey + senderKey(同一发送者、同一对话)
  • 群聊中按发送者独立聚合,不会混淆不同人的消息
  • 单条消息直接透传,多条消息拼接 body 并合并 @提及 列表
  • 这样 AI 能看到完整的上下文,而不是一条条碎片

以上覆盖了 ChannelPlugin 接口、7 层绑定路由、内置与扩展通道、访问控制以及 WhatsApp 的 QR 配对和消息合并实现。