Skip to content

チャネルアダプタ

源码版本v2026.6.11

責務

チャネルアダプタ (channel adapter) は「22 種の異なるプロトコルのチャットプラットフォーム」を OpenClaw 内部の統一イベントストリームに翻訳する接着層です。各チャネルは一組のアダプタインターフェースを実装し,ゲートウェイはこのインターフェース群とだけやり取りし,下が Telegram polling か Slack Socket Mode か Discord Gateway かを気にしません。

アダプタは 3 つのグループに分かれます:

  1. GatewayAdapterstartAccount / stopAccount はアカウントの長接続(polling / webhook / WebSocket)を立ち上げ,プラットフォームの生イベントを agent メインループに送ります。loginWithQrStart / loginWithQrWait / logoutAccount は WhatsApp のような QR スキャンログインが必要なチャネルを処理します。
  2. MessageAdaptersend.text / send.media / send.payload / send.poll の 4 つの outbound メソッド,加えて receive の ack ポリシー宣言,live のストリーミングプレビュー能力宣言。これは outbound 方向の統一送信インターフェースです。
  3. 細分能力 adapterconfig / 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 / 小アダプタ群を分ける最も直接的な理由は,チャネル間の能力差が大きいからです:

Telegram は webhook も long polling もサポートし,WhatsApp は QR スキャン必須,Slack は Socket Mode(WebSocket),Discord は Gateway(WebSocket),iMessage は AppleScript のローカルブリッジ。1 つの統一 Channel インターフェースで包もうとすると,オプションフィールドを山ほど詰めるか,各チャネルが大量の空メソッドを実装するかになります。

メッセージ outbound 方向では,あるチャネルは replyToId をサポートし,あるチャネルは threadId,あるチャネルは nativeQuote,あるチャネルはストリーミングプレビュー(draft preview)をサポートします。これらの能力は宣言的です——アダプタは capabilitiesnativeStreaming: true を宣言し,コアコードが見たら使い,見なければグレードダウンパスに走ります。

startAccount が受け取る ChannelGatewayContext には abortSignalchannelRuntimesetStatusgetStatus があります——これらはゲートウェイがチャネルに注入する能力で,チャネルが自作したものではありません。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 の 8 つのサブモジュールがあります。abortSignal はゲートウェイライフサイクルシグナルで,ゲートウェイ停止時に長接続を即座に切ります。

Slack の startAccount(slack/channel.ts:757)はさらに薄く,直接 monitorSlackProvider に委譲:botToken + appToken の 2 つの 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 ログインは 2 段式(whatsapp/channel.ts:345)です:loginWithQrStart が web login を立ち上げ QR コードを返し,loginWithQrWait がポーリングでユーザのスキャン完了を待ち,logoutAccountlogoutWeb を呼んで authDir を清理します。2 つのメソッドが協調して setup 画面に QR コードを描画させ,結果をブロック待ちします,ログイン全体を 1 つの超長 promise に詰め込みません。

outbound 方向は 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>;
};

4 つのメソッドは 4 種の outbound 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";

4 つのポリシーが「いつプラットフォームにこのメッセージを受信したと ack するか」を制御します——after_receive_record は最も早く,落庫直後に ack。after_agent_dispatch は agent がイベントを取得した後。after_durable_send は返信が耐性投递で成功した後。manual はチャネル自身が制御。defineChannelMessageAdapter はデフォルトで manual(message/adapter.ts:12)を与えます,なぜならほとんどのチャネルは自身で ack タイミングを決める必要があるからです。

inbound → outbound チェーン全体:

境界と失敗

  • SecretRef 未解析で起動するとスロー:Discord は startAccounttokenStatus === "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 の実行を促し,1 つのチャネルが壊れても他チャネルに影響しません。
  • 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 は outbound 送信と inbound ack を管理し,細分 adapter は各々 1 つの能力側面を管理します。ゲートウェイは channelRuntime で能力を注入し,abortSignal でライフサイクルを制御し,capabilities で宣言的にポリシーを選びます。

チャネル登録表 から plugin エントリを取得しその startAccount を呼ぶ方法や,agent イベントがどう MessageAdapter 経由でプラットフォームに送り返されるかは チャット放送 を参照。各チャネルが openclaw.json でどう token、allowFrom、accounts を設定するかは チャネル設定 を参照。