チャネルアダプタ
責務
チャネルアダプタ (channel adapter) は「22 種の異なるプロトコルのチャットプラットフォーム」を OpenClaw 内部の統一イベントストリームに翻訳する接着層です。各チャネルは一組のアダプタインターフェースを実装し,ゲートウェイはこのインターフェース群とだけやり取りし,下が Telegram polling か Slack Socket Mode か Discord Gateway かを気にしません。
アダプタは 3 つのグループに分かれます:
- GatewayAdapter —
startAccount/stopAccountはアカウントの長接続(polling / webhook / WebSocket)を立ち上げ,プラットフォームの生イベントを agent メインループに送ります。loginWithQrStart/loginWithQrWait/logoutAccountは WhatsApp のような QR スキャンログインが必要なチャネルを処理します。 - MessageAdapter —
send.text/send.media/send.payload/send.pollの 4 つの outbound メソッド,加えてreceiveの ack ポリシー宣言,liveのストリーミングプレビュー能力宣言。これは outbound 方向の統一送信インターフェースです。 - 細分能力 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 / 小アダプタ群を分ける最も直接的な理由は,チャネル間の能力差が大きいからです:
Telegram は webhook も long polling もサポートし,WhatsApp は QR スキャン必須,Slack は Socket Mode(WebSocket),Discord は Gateway(WebSocket),iMessage は AppleScript のローカルブリッジ。1 つの統一 Channel インターフェースで包もうとすると,オプションフィールドを山ほど詰めるか,各チャネルが大量の空メソッドを実装するかになります。
メッセージ outbound 方向では,あるチャネルは replyToId をサポートし,あるチャネルは threadId,あるチャネルは nativeQuote,あるチャネルはストリーミングプレビュー(draft preview)をサポートします。これらの能力は宣言的です——アダプタは capabilities で nativeStreaming: true を宣言し,コアコードが見たら使い,見なければグレードダウンパスに走ります。
startAccount が受け取る ChannelGatewayContext には abortSignal、channelRuntime、setStatus、getStatus があります——これらはゲートウェイがチャネルに注入する能力で,チャネルが自作したものではありません。channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher がゲートウェイがチャネルに inbound イベントを agent メインループに送らせる入口です。
だからアダプタ層の設計は:インターフェース細分化 + すべてオプショナル + 能力宣言的です。チャネルは必要に応じて実装し,コアコードは宣言された能力に従ってポリシーを選びます。
主要ファイル
types.plugin.ts:66-111—ChannelPluginルート型,すべての adapter surface を組み合わせ。types.adapters.ts:244-359—ChannelGatewayContextとChannelGatewayAdapter,startAccount/stopAccount/loginWithQrStartなどを定義。message/types.ts:306-410—ChannelMessageAdapterShapeは send / durableFinal / live / receive の 4 facet で構成。message/adapter.ts:11-32—defineChannelMessageAdapterヘルパー,デフォルト receive はmanualack。telegram/channel.ts:992-1106— Telegram のgatewayadapter,startAccountは webhook と polling を同時に処理。slack/channel.ts:756-776— Slack のgateway.startAccountはmonitorSlackProviderに委譲。discord/channel.ts:682-721— Discord 起動はstartupDelayMsで rate limit を防止。whatsapp/channel.ts:345-368— WhatsApp のloginWithQrStart/loginWithQrWait/logoutAccount。
データフロー
チャネル起動は gateway.startAccount(ctx) から入ります(telegram/channel.ts:993):
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)が加わります:
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 がポーリングでユーザのスキャン完了を待ち,logoutAccount は logoutWeb を呼んで authDir を清理します。2 つのメソッドが協調して setup 画面に QR コードを描画させ,結果をブロック待ちします,ログイン全体を 1 つの超長 promise に詰め込みません。
outbound 方向は MessageAdapter(message/types.ts:306)を走ります:
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)を走ります:
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 は
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の実行を促し,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 を設定するかは チャネル設定 を参照。