Adaptateurs de canaux
Responsabilités
L'adaptateur de canal (channel adapter) est la couche de colle qui traduit « 22 plateformes de chat aux protocoles différents » en un flux d'événements unifié interne à OpenClaw. Chaque canal implémente un ensemble d'interfaces d'adaptateur; la passerelle ne discute qu'avec cet ensemble, sans se soucier de savoir si dessous c'est du Telegram polling, du Slack Socket Mode ou du Discord Gateway.
Les adaptateurs se répartissent en trois groupes:
- GatewayAdapter —
startAccount/stopAccount, responsable de tirer une connexion longue (polling / webhook / WebSocket) pour un compte et d'injecter les événements bruts de la plateforme dans la boucle principale de l'agent;loginWithQrStart/loginWithQrWait/logoutAccountgèrent les canaux nécessitant un login par QR code comme WhatsApp. - MessageAdapter — quatre méthodes outbound
send.text/send.media/send.payload/send.poll, plus la déclaration de stratégie ack pourreceive, et la déclaration de capacité de streaming pourlive. C'est l'interface d'envoi unifiée en direction outbound. - Adaptateurs de capacités fines —
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, tous facultatifs — le canal implémente selon ses capacités, ce qui n'est pas implémenté reste undefined.
Motivation de conception
Si l'on sépare gateway / message / un tas de petits adapters, la raison immédiate est que les capacités diffèrent beaucoup d'un canal à l'autre.
Telegram supporte à la fois webhook et long polling; WhatsApp impose le login par QR; Slack passe par Socket Mode (WebSocket); Discord passe par Gateway (WebSocket); iMessage passe par un pont AppleScript local. Si l'on voulait tout faire tenir dans une interface Channel unique, il faudrait soit bourrer un tas de champs facultatifs, soit each canal implémente beaucoup de méthodes vides.
Côté outbound, certains canaux supportent replyToId, d'autres threadId, d'autres nativeQuote, d'autres la prévisualisation en streaming (draft preview). Ces capacités sont déclaratives — l'adaptateur déclare nativeStreaming: true dans capabilities, le code cœur ne l'utilise que s'il le voit, sinon il passe par un chemin dégradé.
startAccount reçoit dans son ChannelGatewayContext un abortSignal, un channelRuntime, un setStatus, un getStatus — ce sont des capacités injectées par la passerelle au canal, pas forgées par le canal lui-même. channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher est l'entrée par laquelle la passerelle laisse le canal injecter les événements inbound dans la boucle principale de l'agent.
La conception de la couche adaptateur est donc: interfaces fines + toutes facultatives + capacités déclaratives. Le canal implémente à la demande, le code cœur choisit ses stratégies selon les capacités déclarées.
Fichiers clés
types.plugin.ts:66-111— Type racineChannelPlugin, agrège toutes les surfaces d'adaptateur.types.adapters.ts:244-359—ChannelGatewayContextetChannelGatewayAdapter, définissentstartAccount/stopAccount/loginWithQrStartetc.message/types.ts:306-410—ChannelMessageAdapterShapecomposé des quatre facets send / durableFinal / live / receive.message/adapter.ts:11-32— HelperdefineChannelMessageAdapter, receive par défaut enmanualack.telegram/channel.ts:992-1106— Adaptateurgatewayde Telegram,startAccountgère à la fois webhook et polling.slack/channel.ts:756-776— Slackgateway.startAccountdélègue àmonitorSlackProvider.discord/channel.ts:682-721— Discord ajoutestartupDelayMspour éviter le rate limit.whatsapp/channel.ts:345-368— WhatsApploginWithQrStart/loginWithQrWait/logoutAccount.
Flux de données
Le démarrage du canal entre par 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`);
}
},
}Plusieurs points: ctx.account est le compte structuré issu du parsing de la configuration de canal; ctx.channelRuntime est le paquet de capacités injecté par la passerelle, contenant huit sous-modules reply / routing / text / session / media / commands / groups / pairing; abortSignal est le signal de cycle de vie de la passerelle, qui doit rompre la connexion longue quand la passerelle s'arrête.
Le startAccount de Slack (slack/channel.ts:757) est plus mince et délègue directement à monitorSlackProvider: deux tokens botToken + appToken sont passés après trim (appToken est spécifique à Socket Mode, botToken sert à l'API bot normale), les autres champs sont similaires à Telegram — channelRuntime / abortSignal / setStatus / getStatus. Slack Socket Mode est une connexion longue WebSocket, implémentée à l'intérieur de monitorSlackProvider; setStatus / getStatus permettent au monitor de refaire remonter l'état runtime du compte dans le snapshot du registre des canaux.
Discord ajoute une protection contre le rate limit au démarrage (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 permet à l'attente de répondre à abortSignal — si la passerelle s'arrête, on abandonne le démarrage immédiatement plutôt que d'attendre bêtement la fin. Le token Discord doit aussi être validé au préalable; un configured_unavailable est jeté (discord/channel.ts:685), pour éviter de démarrer avec un SecretRef non résolu.
Le login QR de WhatsApp se fait en deux étapes (whatsapp/channel.ts:345): loginWithQrStart tire un web login et renvoie le QR code, loginWithQrWait sonde en boucle l'attente de scan par l'utilisateur, logoutAccount appelle logoutWeb pour nettoyer authDir. Les deux méthodes coordonnées permettent à l'interface setup de rendre le QR code et d'attendre bloquant le résultat, plutôt que d'empiler tout le login dans une seule promesse trop longue.
Côté outbound, on passe par le 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>;
};Les quatre méthodes correspondent à quatre types de payload outbound: text texte brut, media média, payload carte riche, poll sondage. lifecycle est un hook: beforeSend / afterSendSuccess / afterSendFailure / afterCommit, pour laisser le canal insérer sa logique avant/après envoi (par exemple Slack met à jour le compteur de replies du message parent).
Côté réception (inbound), on passe par la stratégie ack (message/types.ts:374):
export type ChannelMessageReceiveAckPolicy =
| "after_receive_record"
| "after_agent_dispatch"
| "after_durable_send"
| "manual";Quatre stratégies contrôlent « quand acker la plateforme pour signaler que le message a été reçu » — after_receive_record est la plus précoce, ack dès l'enregistrement; after_agent_dispatch après que l'agent a reçu l'événement; after_durable_send après livraison fiable de la reply; manual le canal contrôle lui-même. defineChannelMessageAdapter met par défaut manual (message/adapter.ts:12), car la plupart des canaux veulent décider eux-mêmes du moment de l'ack.
Toute la chaîne inbound → outbound:
Limites et modes d'échec
- SecretRef non résolu au démarrage jette: Discord vérifie dans
startAccountquetokenStatus === "configured_unavailable"(discord/channel.ts:685), et jette plutôt que de se connecter avec un token vide — évite que les logs leakent l'état limite « token est une chaîne vide ». - Polling lease release: le
stopAccountde Telegram doit appelerreleaseStoppedTelegramPollingLease(telegram/channel.ts:1094), sinon au redémarrage la polling lease du tour précédent est encore là et entre en conflit de token. - Rate limit délai proactif: Discord attend
startupDelayMsavant le démarrage et cet attend peut être interrompu parabortSignal(discord/channel.ts:696). Il s'agit de transformer « empêcher Discord de limiter le démarrage concurrent » en un sleep explicite, plutôt qu'en retry après échec. - Échec de chargement bundled n'émet qu'un warn:
describeBundledChannelLoadError(bundled.ts:318) suggère de lanceropenclaw doctor --fix; un canal en panne n'impacte pas les autres. - channelRuntime facultatif:
ChannelGatewayContext.channelRuntimeest un champ facultatif; les plugins externes devraient vérifierundefinedavant utilisation (types.adapters.ts:313), sinon une version SDK incompatible donnera une NPE. - ack par défaut manual:
defineChannelMessageAdaptermet receive par défaut àmanual(message/adapter.ts:12); tant que le canal ne déclare pas sa stratégie ack, le code cœur ne l'acquitte pas automatiquement — pour éviter « message cru traité mais en fait perdu ». - Capacités déclaratives pour dégradation:
durableFinalDeliveryCapabilities(message/types.ts:17) liste 12 capacités; le code cœur n'emprunte le chemin de livraison fiable correspondant que pour celles déclarées true danscapabilities, les autres passent par un chemin dégradé sans jeter.
Résumé
La couche adaptateur encapsule les différences des 22 canaux derrière un ensemble d'interfaces à granularité fine, toutes facultatives, déclaratives en capacités. GatewayAdapter gère le démarrage/arrêt et le login; MessageAdapter gère l'envoi outbound et l'ack inbound; les adaptateurs fins gèrent chacun une facette de capacité. La passerelle injecte les capacités via channelRuntime, contrôle le cycle de vie via abortSignal, choisit les stratégies de façon déclarative via capabilities.
Comment on obtient l'entrée plugin depuis le registre des canaux pour appeler son startAccount, et comment les événements agent atteignent le MessageAdapter pour être renvoyés à la plateforme, voir diffusion de chat. Comment chaque canal configure dans openclaw.json son token, allowFrom, accounts, voir configuration des canaux.