Skip to content

第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-1:通道插件与共享适配器模式

8.2 Telegram 通道实现

8.2.1 设计决策:为什么选择 Bot API 而非 User API?

深入实现之前,先讨论一个根本性的设计选择:Telegram 提供两种 API——Bot APIUser API(MTProto)。OpenClaw 选择了 Bot API,这个决定并非不言自明。

User API 的诱惑:MTProto 允许以普通用户身份操作,可以加入任意群组、读取历史消息、使用 Telegram 的全部功能。一些竞品(如 Telethon 生态)选择了这条路。但它有三个致命问题:

  1. 违反 Telegram ToS。Telegram 明确禁止使用 User API 构建自动化工具。账号被封禁的风险实实在在——你的 Agent 可能在运行三个月后突然失去所有连接。
  2. 需要手机号。每个 User API 会话需要一个手机号登录,且需要接收验证码。对于想运行多个 Agent 的运营者,这意味着多个手机号——管理成本高,且手机号是个人身份信息。
  3. 安全边界模糊。以"用户"身份操作意味着 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 执行审批流程

图 8-2:Telegram Exec Approval 流程

8.2.8 Webhook vs. Polling 的深层权衡

表面上看,这是一个"推 vs. 拉"的简单选择。但深入分析,两种模式在不同部署场景下的适用性差异巨大:

考量维度Long PollingWebhook
部署复杂度零——只需出站连接需要公网 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 看不到用户的在线状态、语音状态或成员变更——这正是最小权限原则的平台级实现。

基于 VitePress 构建