Appearance
第8章 通道实现深度剖析
"代码中最危险的假设是'这两个平台的行为应该一样'。Telegram 的已读回执是可选的,WhatsApp 的是强制的;Discord 的消息可以编辑,Signal 的不能。每一个'应该'都是一个等待爆炸的 bug。"
本章要点
- 逐一剖析六大通道实现:Telegram、Discord、WhatsApp、Signal、Slack、飞书
- 理解每个平台的独特约束与技术选型背后的权衡
- 掌握通道实现中的共性设计模式与差异化处理策略
- 对比六大通道的能力矩阵,指导实际选型
8.1 引言
想象一个已经上线的 Agent 的日常切面:一条 WhatsApp 语音从巴西圣保罗发来,同一秒,东京的开发者在 Discord 的 #support 频道里 @了它,伦敦的产品经理在 Slack 线程里追问部署进度。三条消息,三种截然不同的协议栈——但 Agent 的核心逻辑看到的是同一个 inbound() 调用。上一章定义的抽象合约在此刻兑现了它的承诺。
上一章,我们设计了通道的抽象合约——那是蓝图。本章走进施工现场,看看那些优雅的接口定义如何与混乱的现实短兵相接。每个平台都有自己的"脾气":Telegram 要求你在 webhook 和长轮询之间二选一;Discord 强制声明 intents,遗漏一个就收不到消息;WhatsApp 需要扫码登录,认证状态还会悄然过期;Signal 依赖外部 daemon 进程,通信走 JSON-RPC。
没有两个通道是一样的。但 OpenClaw 的通道插件,让它们看起来一样——正如没有两种语言的语法是一样的,但一个优秀的翻译让你忘记原文是什么语言。
这正是抽象的价值所在——不是消灭差异,而是将差异封装在边界之内,让边界之外的世界保持简洁与统一。
🔥 深度洞察:每个通道都是一部外交史
深入阅读本章时,你会发现每个通道实现都不只是"技术适配"——它是与一个平台长期博弈的结晶。Telegram 选 Bot API 而非 User API,是安全与功能的权衡;WhatsApp 的认证状态管理,是与端到端加密设计的妥协;Discord 的 Intent 声明机制,是隐私保护与功能需求的平衡。每一个看似"技术细节"的选择,背后都有平台政策、安全合规、用户体验的多方博弈。这就像外交——你不能只懂对方的语言,还要懂对方的文化、法律和政治红线。优秀的通道实现工程师,本质上是技术外交官。

图 8-1:通道插件与共享适配器模式
8.2 Telegram 通道实现
8.2.1 设计决策:为什么选择 Bot API 而非 User API?
深入实现之前,先讨论一个根本性的设计选择:Telegram 提供两种 API——Bot API 和 User API(MTProto)。OpenClaw 选择了 Bot API,这个决定并非不言自明。
User API 的诱惑:MTProto 允许以普通用户身份操作,可以加入任意群组、读取历史消息、使用 Telegram 的全部功能。一些竞品(如 Telethon 生态)选择了这条路。但它有三个致命问题:
- 违反 Telegram ToS。Telegram 明确禁止使用 User API 构建自动化工具。账号被封禁的风险实实在在——你的 Agent 可能在运行三个月后突然失去所有连接。
- 需要手机号。每个 User API 会话需要一个手机号登录,且需要接收验证码。对于想运行多个 Agent 的运营者,这意味着多个手机号——管理成本高,且手机号是个人身份信息。
- 安全边界模糊。以"用户"身份操作意味着 Agent 拥有与人类用户完全相同的权限——包括读取所有私聊、操作所有群组。没有权限隔离。
Bot API 的优势:Bot 是 Telegram 的一等公民,有官方支持的生命周期(创建、配置、撤销)。更重要的是,Bot 的权限天然受限——它只能看到 @它的消息或它管理的群组中的消息。这种"天然最小权限"恰好是 Agent 安全的理想基础。
关键概念:通道协议适配 每个消息平台都有独特的通信协议——Telegram 使用长轮询或 Webhook,Discord 使用 WebSocket Gateway,WhatsApp 需要扫码认证的持久连接,Signal 依赖外部 JSON-RPC daemon。通道插件的核心职责就是将这些异构协议适配为 OpenClaw 的统一消息接口,同时保留平台的独特能力。
⚠️ 注意:Telegram Bot API 在群组中默认只能看到 @它的消息和通过 /command 发送的命令。如果需要 Bot 读取群组中的所有消息,必须在 @BotFather 中关闭 Privacy Mode。但请谨慎使用——这会让 Bot 接收群组中的每一条消息,可能带来 Token 消耗和隐私方面的考量。
技术选型不只是"能不能"的问题,更是"应不应该"的问题。Bot API 的功能限制,在安全语境下反而是优势——就像消防员穿防护服不是因为灵活,而是因为安全。限制本身就是保护。
WhatsApp 为什么做了相反的选择? 因为 WhatsApp 没有官方 Bot API(仅有面向企业的 Cloud API,但需要 Facebook 商业验证)。OpenClaw 被迫使用 Web 客户端协议——这不是偏好,而是唯一可行的消费级方案。这种对比恰好说明:设计决策受平台约束的影响,和受技术偏好的影响一样大。
8.2.2 架构概览
Telegram 是 OpenClaw 支持最完善的通道之一。它支持 Bot API 的完整功能集,包括文本消息、媒体、投票、inline keyboard、话题(Topics)、webhook 和 polling 两种接收模式。
typescript
// extensions/telegram/src/channel.ts
export const telegramPlugin: ChannelPlugin<ResolvedTelegramAccount, TelegramProbe> = {
...createTelegramPluginBase({
setupWizard: telegramSetupWizard,
setup: telegramSetupAdapter,
}),
pairing: createTextPairingAdapter({
idLabel: "telegramUserId",
message: PAIRING_APPROVED_MESSAGE,
normalizeAllowEntry: createPairingPrefixStripper(/^(telegram|tg):/i),
notify: async ({ cfg, id, message }) => {
const { token } = getTelegramRuntime().channel.telegram.resolveTelegramToken(cfg);
if (!token) {
throw new Error("telegram token not configured");
}
await getTelegramRuntime().channel.telegram.sendMessageTelegram(id, message, { token });
},
}),
// ... 更多适配器
};8.2.3 配置解析
Telegram 的配置解析支持多种 token 来源:环境变量 TELEGRAM_BOT_TOKEN、配置文件中的 botToken 字段,以及多账户配置中的 accounts.<id>.botToken。
typescript
// extensions/telegram/src/accounts.ts
export function resolveTelegramAccount(params: {
cfg: OpenClawConfig; accountId?: string | null;
}): ResolvedTelegramAccount {
const accountId = normalizeAccountId(params.accountId);
const envToken = process.env.TELEGRAM_BOT_TOKEN?.trim() ?? "";
// 优先级:环境变量 → 配置文件 → 多账户配置
if (accountId === DEFAULT_ACCOUNT_ID && envToken) {
return { accountId, token: envToken, tokenSource: "env", /* ... */ };
}
const token = resolveTelegramTokenFromConfig(telegramConfig, accountId);
return { accountId, token: token ?? "", tokenSource: token ? "config" : "none", /* ... */ };
}8.2.4 出站消息发送
Telegram 的出站消息发送遵循 ChannelOutboundAdapter 合约,涵盖文本分块、媒体发送、投票、inline keyboard 等能力:
typescript
// extensions/telegram/src/channel.ts
outbound: {
deliveryMode: "direct", // 直接调用 Bot API
chunkerMode: "markdown", // 分块时尊重 Markdown 语法
textChunkLimit: 4000, // Telegram 消息上限约 4096 字符
sendPayload: async ({ cfg, to, payload, ...opts }) => {
const send = resolveOutboundSendDep(deps, "telegram") ?? sendMessageTelegram;
return attachChannelToResult("telegram",
await sendTelegramPayloadMessages({ send, to, payload, baseOpts: buildTelegramSendOptions(opts) }));
},
sendText: async (ctx) => await sendTelegramOutbound(ctx),
sendMedia: async (ctx) => await sendTelegramOutbound(ctx),
sendPoll: async ({ to, poll, ...opts }) => await sendPollTelegram(to, poll, opts),
},deliveryMode: "direct" 表示 Telegram 插件直接调用 Bot API,不需要通过 Gateway 代理。chunkerMode: "markdown" 表示消息分块时会尊重 Markdown 语法结构,避免在代码块中间断开。
8.2.5 Gateway 启动流程
Telegram 支持两种消息接收模式:Polling(轮询) 和 Webhook(推送)。
typescript
// extensions/telegram/src/channel.ts
gateway: {
startAccount: async (ctx) => {
const token = ctx.account.token.trim();
// 探测 bot 信息(@username),失败时静默降级
const probe = await probeTelegram(token, 2500, { /* proxy, network */ }).catch(() => null);
const botLabel = probe?.ok ? ` (@${probe.bot?.username})` : "";
ctx.log?.info(`[${ctx.account.accountId}] starting provider${botLabel}`);
// 有 webhookUrl 时用 Webhook 模式,否则用 Long Polling
return monitorTelegramProvider({
token, accountId: ctx.account.accountId,
config: ctx.cfg, runtime: ctx.runtime, abortSignal: ctx.abortSignal,
useWebhook: Boolean(ctx.account.config.webhookUrl),
// ... webhookUrl, webhookSecret, webhookPath 等
});
},
},当配置了 webhookUrl 时,monitorTelegramProvider 会注册 webhook;否则使用 getUpdates long polling。
8.2.6 话题(Topics)支持
Telegram 群组支持话题功能,每个话题有独立的 message_thread_id。OpenClaw 将话题映射为独立的会话:
typescript
// extensions/telegram/src/channel.ts — 话题路由
function resolveTelegramOutboundSessionRoute(params: {
cfg; agentId: string; target: string; threadId?: string | number | null;
}) {
const parsed = parseTelegramTarget(params.target);
const isGroup = parsed.chatType === "group" || /* ... */;
// 话题的 peerId = "chatId:threadId",群组始终独立会话
const peerId = isGroup && resolvedThreadId
? buildTelegramGroupPeerId(parsed.chatId, resolvedThreadId) : parsed.chatId;
const baseSessionKey = buildTelegramBaseSessionKey({ cfg, agentId, accountId, peer: { kind: isGroup ? "group" : "direct", id: peerId } });
const threadKeys = resolvedThreadId && !isGroup
? resolveThreadSessionKeys({ baseSessionKey, threadId: String(resolvedThreadId) }) : null;
return { sessionKey: threadKeys?.sessionKey ?? baseSessionKey, baseSessionKey, /* ... */ };
}8.2.7 Exec Approval 集成
Telegram 通道借助 execApprovals 适配器,将命令审批嵌入 inline keyboard 交互中:
typescript
// extensions/telegram/src/channel.ts
execApprovals: {
getInitiatingSurfaceState: ({ cfg, accountId }) =>
isTelegramExecApprovalClientEnabled({ cfg, accountId }) ? { kind: "enabled" } : { kind: "disabled" },
buildPendingPayload: ({ request, nowMs }) => {
// 构建审批消息 + Inline Keyboard(Approve / Deny 按钮)
const payload = buildExecApprovalPendingReplyPayload({
approvalId: request.id, command: resolveExecApprovalCommandDisplay(request.request).commandText,
expiresAtMs: request.expiresAtMs, nowMs, /* ... */
});
const buttons = buildTelegramExecApprovalButtons(request.id);
return buttons ? { ...payload, channelData: { telegram: { buttons } } } : payload;
},
},
图 8-2:Telegram Exec Approval 流程
8.2.8 Webhook vs. Polling 的深层权衡
表面上看,这是一个"推 vs. 拉"的简单选择。但深入分析,两种模式在不同部署场景下的适用性差异巨大:
| 考量维度 | Long Polling | Webhook |
|---|---|---|
| 部署复杂度 | 零——只需出站连接 | 需要公网 HTTPS 端点 |
| NAT 友好 | 是——出站请求穿越 NAT | 否——需要端口转发或反向代理 |
| 消息延迟 | ~1 秒(取决于轮询间隔) | 亚秒(Telegram 主动推送) |
| 资源消耗 | 持续 HTTP 连接 | 仅在有消息时消耗 |
| 可靠性 | 简单——失败就重试 | 复杂——webhook 失败时 Telegram 会指数退避重推 |
| 调试难度 | 低——可以在本地直接看到请求 | 高——需要 ngrok 等隧道工具 |
OpenClaw 默认使用 Long Polling 的原因正是其目标用户画像:个人部署者在家用服务器或 VPS 上运行,通常在 NAT 之后,不想(或不会)配置 SSL 证书和反向代理。渐进式复杂度的哲学在这里体现——默认零配置即可工作,需要低延迟时可以选择升级到 Webhook。
8.3 Discord 通道实现
8.3.1 设计决策:Intents 是安全特性而非技术障碍
Discord 使用 WebSocket Gateway 连接接收事件。但在讨论实现之前,必须理解 Discord 的 Gateway Intents 设计——这不只是技术限制,而是一个深思熟虑的隐私保护架构。
2020 年之前,Discord Bot 可以自动接收所有事件。这意味着一个被加入 1000 个服务器的 Bot 可以静默监控所有消息——即使它的功能只是播放音乐。Intents 的引入强制 Bot 声明"我需要看到什么",未声明的事件类型不会被推送。
这对 OpenClaw 的影响是双面的:
正面:Intents 天然限制了 Agent 的信息范围。一个只声明了 GUILD_MESSAGES intent 的 Bot 看不到用户的在线状态、语音状态或成员变更——这正是最小权限原则的平台级实现。