Skip to content

渠道适配器 ​

源码版本v2026.6.11

职责 ​

渠道适配器 (channel adapter) 是把"22 种不同协议的 chat 平台"翻译成 OpenClaw 内部统一事件流的胶水层。每个渠道实现一组适配器接口,网关只跟这组接口打交道,不关心下面是 Telegram polling 还是 Slack Socket Mode 还是 Discord Gateway。

适配器分三组:

  1. GatewayAdapter — startAccount / stopAccount,负责拉起一个账号的长连接 (polling / webhook / WebSocket) 并把平台原始事件喂进 agent 主循环;loginWithQrStart / loginWithQrWait / logoutAccount 处理 WhatsApp 这类需要扫码登录的渠道。
  2. MessageAdapter — send.text / send.media / send.payload / send.poll 四个出站方法,加 receive 的 ack 策略声明、live 的流式预览能力声明。这是 outbound 方向的统一发送接口。
  3. 细分能力 adapter — config / setup / pairing / security / groups / mentions / outbound / status / gateway / auth / approval / elevated / commands / lifecycle / secrets / allowlist / doctor / bindings / conversationBindings / streaming / threading / message / messaging / agentPrompt / directory / resolver / actions / heartbeat,每个都是可选的——渠道按能力实现,不实现的就留 undefined。

设计动机 ​

把 gateway / message / 一堆小 adapter 拆开,最直接原因是渠道之间的能力差异很大:

Telegram 既支持 webhook 也支持 long polling,WhatsApp 必须扫码,Slack 走 Socket Mode (WebSocket),Discord 走 Gateway (WebSocket),iMessage 走 AppleScript 本地桥。如果用一个统一 Channel 接口囊括,要么塞一堆可选字段,要么每个渠道实现大量空方法。

消息出站方向,有的渠道支持 replyToId,有的支持 threadId,有的支持 nativeQuote,有的支持流式预览 (draft preview)。这些能力是声明式的——适配器在 capabilities 里声明 nativeStreaming: true,核心代码看到才用,看不到就走降级路径。

startAccount 接收的 ChannelGatewayContext 里有 abortSignal、channelRuntime、setStatus、getStatus——这些是网关给渠道注入的能力,不是渠道自己造的。channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher 就是网关让渠道把 inbound 事件喂进 agent 主循环的入口。

所以适配器层的设计是:接口分细 + 全部可选 + 能力声明式。渠道按需实现,核心代码按声明的能力选用策略。

关键文件 ​

数据流 ​

渠道启动从 gateway.startAccount(ctx) 进入(telegram/channel.ts:993):

typescript
gateway: {
  startAccount: async (ctx) => {
    const account = ctx.account;
    // ... probe token、resolve bot info
    return startTelegramMonitor({
      token,
      accountId: account.accountId,
      cfg: ctx.cfg,
      runtime: ctx.runtime,
      channelRuntime: ctx.channelRuntime,
      abortSignal: ctx.abortSignal,
      useWebhook: Boolean(account.config.webhookUrl),
      webhookUrl: account.config.webhookUrl,
      webhookSecret: account.config.webhookSecret,
      // ...
    });
  },
  stopAccount: async ({ account, accountId, log }) => {
    const released = await releaseStoppedTelegramPollingLease({ token, accountId });
    if (released) {
      log?.info?.(`[${accountId}] released stopped Telegram polling lease`);
    }
  },
}

注意几点:ctx.account 是 渠道配置 解析后的结构化账号;ctx.channelRuntime 是网关注入的能力包,里面有 reply / routing / text / session / media / commands / groups / pairing 八个子模块;abortSignal 是网关生命周期信号,网关停时要让长连接立即断。

Slack 的 startAccount(slack/channel.ts:757)更薄,直接委托给 monitorSlackProvider:botToken + appToken 两个 token trim 后传入(appToken 是 Socket Mode 专用、botToken 是普通 bot API 用的),其余字段跟 telegram 类似——channelRuntime / abortSignal / setStatus / getStatus。Slack Socket Mode 是 WebSocket 长连接,实现在 monitorSlackProvider 内部;setStatus / getStatus 让 monitor 反馈账号运行状态到 渠道注册表 的快照里。

Discord 启动多了一个 rate-limit 防护(discord/channel.ts:690):

typescript
const startupDelayMs = resolveDiscordStartupDelayMs(ctx.cfg, account.accountId);
if (startupDelayMs > 0) {
  ctx.log?.info(
    `[${account.accountId}] delaying provider startup ${Math.round(startupDelayMs / 1000)}s to reduce Discord startup rate limits`,
  );
  try {
    await sleepWithAbort(startupDelayMs, ctx.abortSignal);
  } catch {
    return;
  }
}

sleepWithAbort 让等待期间也能响应 abortSignal——网关停了就立即放弃启动,不要傻等完再退出。Discord 的 token 状态也要先校验,configured_unavailable 抛错(discord/channel.ts:685),避免用未解析的 SecretRef 启动。

WhatsApp 的 QR 登录是两段式(whatsapp/channel.ts:345):loginWithQrStart 拉起 web login 返回二维码,loginWithQrWait 轮询等待用户扫码完成,logoutAccount 调 logoutWeb 清 authDir。两个方法配合让 setup 界面渲染二维码并阻塞等待结果,而不是把整个登录塞在一个超长 promise 里。

出站方向走 MessageAdapter(message/types.ts:306):

typescript
export type ChannelMessageSendAdapter<
  TConfig = OpenClawConfig,
  TSendResult extends ChannelMessageSendResult = ChannelMessageSendResult,
> = {
  text?: (ctx: ChannelMessageSendTextContext<TConfig>) => Promise<TSendResult>;
  media?: (ctx: ChannelMessageSendMediaContext<TConfig>) => Promise<TSendResult>;
  payload?: (ctx: ChannelMessageSendPayloadContext<TConfig>) => Promise<TSendResult>;
  poll?: (ctx: ChannelMessageSendPollContext<TConfig>) => Promise<TSendResult>;
  lifecycle?: ChannelMessageSendLifecycleAdapter<TConfig, TSendResult>;
};

四个方法对应四种出站 payload:text 纯文本、media 媒体、payload 富卡片、poll 投票。lifecycle 是 hook: beforeSend / afterSendSuccess / afterSendFailure / afterCommit,给渠道在发送前后插自己的逻辑(比如 Slack 要更新 parent message 的 reply 计数)。

接收方向 (inbound) 走 ack 策略(message/types.ts:374):

typescript
export type ChannelMessageReceiveAckPolicy =
  | "after_receive_record"
  | "after_agent_dispatch"
  | "after_durable_send"
  | "manual";

四种策略控制"什么时候向平台 ack 这条消息已收到"——after_receive_record 是最早、刚落库就 ack;after_agent_dispatch 是 agent 拿到事件后;after_durable_send 是回复 durably 投递成功后;manual 是渠道自己控制。defineChannelMessageAdapter 默认给 manual(message/adapter.ts:12),因为大多数渠道需要自己决定 ack 时机。

整个 inbound → outbound 链路:

边界与失败 ​

  • SecretRef 未解析就启动会抛:Discord 在 startAccount 里检查 tokenStatus === "configured_unavailable"(discord/channel.ts:685),直接抛错而不是用空 token 去连,避免日志泄漏"token 是空字符串"这种边界状态。
  • polling lease 释放:Telegram 的 stopAccount 必须调 releaseStoppedTelegramPollingLease(telegram/channel.ts:1094),否则重启时上一轮的 polling lease 还在,会撞 token 冲突。
  • rate limit 主动延迟:Discord 启动前 startupDelayMs 等待且能被 abortSignal 中断(discord/channel.ts:696)。这是把"防止 Discord 限制并发启动"做成显式 sleep,而不是失败重试。
  • bundled 加载失败只 warn:describeBundledChannelLoadError(bundled.ts:318)提示跑 openclaw doctor --fix,一个渠道挂了不影响其他渠道。
  • channelRuntime 可选:ChannelGatewayContext.channelRuntime 是可选字段,外部插件应该检查 undefined 再用(types.adapters.ts:313),否则 SDK 版本不对会 NPE。
  • ack 默认 manual:defineChannelMessageAdapter 把 receive 默认成 manual(message/adapter.ts:12),渠道不主动声明 ack 策略时核心代码不会替它自动 ack——防止"消息被以为已处理其实丢了"。
  • capabilities 声明式降级:durableFinalDeliveryCapabilities(message/types.ts:17)列出 12 个能力,渠道在 capabilities 里声明 true 的核心代码才会走对应可靠投递路径,没声明的走降级,不抛错。

小结 ​

适配器层把 22 个渠道的差异封装在一组细粒度、全部可选、能力声明式的接口背后。GatewayAdapter 管启停与登录,MessageAdapter 管出站发送与入站 ack,细分 adapter 各管一个能力切面。网关通过 channelRuntime 注入能力,通过 abortSignal 控制生命周期,通过 capabilities 声明式选择策略。

怎么从 渠道注册表 拿到 plugin 条目再调它的 startAccount,以及 agent 事件怎么到 MessageAdapter 发回平台,看 聊天广播。每个渠道在 openclaw.json 里怎么配 token、allowFrom、accounts,看 渠道配置。