Skip to content

Kanal-Adapter ​

源码版本v2026.6.11

Verantwortung ​

Ein Kanal-Adapter (channel adapter) ist die Klebstoffschicht, die „22 Chat-Plattformen unterschiedlicher Protokolle" in den einheitlichen OpenClaw-internen Ereignisstrom übersetzt. Jeder Kanal implementiert eine Reihe von Adapter-Schnittstellen; das Gateway verhandelt nur mit diesen Schnittstellen und interessiert sich nicht, ob darunter Telegram-Polling, Slack Socket Mode oder Discord Gateway steckt.

Die Adapter fallen in drei Gruppen:

  1. GatewayAdapter — startAccount / stopAccount, verantwortlich, eine lange Verbindung eines Kontos hochzuziehen (Polling / Webhook / WebSocket) und Plattform-Rohereignisse in die Agent-Hauptschleife einzuspeisen; loginWithQrStart / loginWithQrWait / logoutAccount behandeln Kanäle wie WhatsApp, die QR-Scan-Login brauchen.
  2. MessageAdapter — vier Outbound-Methoden send.text / send.media / send.payload / send.poll, plus receive-Ack-Strategie-Deklaration, live-Streaming-Vorschau-Fähigkeitsdeklaration. Das ist die einheitliche Sende-Schnittstelle in Outbound-Richtung.
  3. Feinkörnige Fähigkeitsadapter — 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, jeder optional — der Kanal implementiert nach Fähigkeit, was fehlt, bleibt undefined.

Designmotivation ​

Warum gateway / message / eine Reihe kleiner Adapter aufteilen? Weil die Fähigkeiten zwischen Kanälen stark differieren:

Telegram unterstützt Webhook und Long-Polling, WhatsApp muss QR-Scan, Slack geht über Socket Mode (WebSocket), Discord über Gateway (WebSocket), iMessage über eine lokale AppleScript-Brücke. Eine vereinheitlichte Channel-Schnittstelle müsste entweder viele optionale Felder stopfen oder jeden Kanal viele Leer-Methoden implementieren lassen.

In Outbound-Richtung unterstützt mancher Kanal replyToId, mancher threadId, mancher nativeQuote, mancher Streaming-Vorschau (draft preview). Diese Fähigkeiten sind deklarativ — der Adapter deklariert in capabilities z. B. nativeStreaming: true; der Kerncode verwendet sie nur, wenn er sie sieht, sonst geht er in den Degradationspfad.

startAccount erhält im ChannelGatewayContext abortSignal, channelRuntime, setStatus, getStatus — das sind Fähigkeiten, die das Gateway dem Kanal injiziert, nicht solche, die sich der Kanal selbst baut. channelRuntime.reply.dispatchReplyWithBufferedBlockDispatcher ist der Eintritt, über den das Gateway den Kanal Inbound-Ereignisse in die Agent-Hauptschleife einspeisen lässt.

Das Design der Adapterschicht lautet also: feinkörnige Schnittstellen + alles optional + Fähigkeiten deklarativ. Der Kanal implementiert nach Bedarf, der Kerncode wählt die Strategie nach deklarierter Fähigkeit.

Schlüsseldateien ​

Datenfluss ​

Der Kanalstart tritt in gateway.startAccount(ctx) ein (telegram/channel.ts:993):

typescript
gateway: {
  startAccount: async (ctx) => {
    const account = ctx.account;
    // ... Token anpingen, Bot-Info auflösen
    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`);
    }
  },
}

Beachten Sie: ctx.account ist die strukturierte Account aus der Kanal-Konfiguration; ctx.channelRuntime ist die vom Gateway injizierte Fähigkeits-Tasche mit acht Submodulen reply / routing / text / session / media / commands / groups / pairing; abortSignal ist das Lebenszyklus-Signal des Gateways — beim Stop des Gateways muss die lange Verbindung sofort reißen.

Slacks startAccount (slack/channel.ts:757) ist dünner und delegiert direkt an monitorSlackProvider: botToken + appToken werden nach Trim übergeben (appToken für Socket Mode, botToken für normale Bot-API); die restlichen Felder ähneln Telegram — channelRuntime / abortSignal / setStatus / getStatus. Slack Socket Mode ist eine WebSocket-Langverbindung, implementiert in monitorSlackProvider; setStatus / getStatus lassen den Monitor den Kontostand in den Snapshot der Kanal-Registry zurückspeisen.

Discord startet mit einem zusätzlichen Rate-Limit-Schutz (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 lässt das Warten auch auf abortSignal reagieren — stoppt das Gateway, bricht der Start sofort ab, statt doof abzuwarten. Discords Token-Status wird vorab geprüft; configured_unavailable wirft (discord/channel.ts:685), um nicht mit unaufgelöstem SecretRef zu starten.

WhatsApps QR-Login ist zweiphasig (whatsapp/channel.ts:345): loginWithQrStart startet den Web-Login und liefert den QR-Code zurück; loginWithQrWait pollt, bis der Nutzer den Scan abschließt; logoutAccount ruft logoutWeb und räumt authDir auf. Die zwei Methoden erlauben dem Setup-UI, den QR-Code zu rendern und auf das Ergebnis zu warten, statt den gesamten Login in einem überlangen Promise zu vergraben.

In Outbound-Richtung läuft der 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>;
};

Die vier Methoden entsprechen vier Outbound-Payloads: text Plain-Text, media Medien, payload Rich-Karte, poll Umfrage. lifecycle ist ein Hook: beforeSend / afterSendSuccess / afterSendFailure / afterCommit, damit der Kanal rund ums Senden eigene Logik einhakt (z. B. Slack aktualisiert den Reply-Zähler der Parent-Message).

Inbound-Richtung läuft über Ack-Strategie (message/types.ts:374):

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

Vier Strategien steuern „wann der Plattform ackt wird, dass diese Nachricht empfangen wurde" — after_receive_record ist der früheste, direkt nach Persistenz; after_agent_dispatch nach Erhalt des Agenten; after_durable_send nach erfolgreicher zuverlässiger Zustellung der Antwort; manual der Kanal steuert selbst. defineChannelMessageAdapter defaultet auf manual (message/adapter.ts:12), weil die meisten Kanäle ihren Ack-Zeitpunkt selbst bestimmen müssen.

Die gesamte Inbound → Outbound-Kette:

Grenzen und Fehler ​

  • SecretRef unaufgelöst starten wirft: Discord prüft in startAccount auf tokenStatus === "configured_unavailable" (discord/channel.ts:685), wirft statt mit leerem Token zu verbinden; verhindert, dass in Logs der Randzustand „Token ist leerer String" sickert.
  • Polling-Lease-Freigabe: Telegrams stopAccount muss releaseStoppedTelegramPollingLease (telegram/channel.ts:1094) rufen, sonst hält die vorherige Polling-Lease noch und es knallt am Token-Konflikt.
  • Rate-Limit aktiv verzögern: Discord startet vor startupDelayMs Warten, das durch abortSignal abbrechbar ist (discord/channel.ts:696). Das macht „Discord-Concurrent-Start-Limit vermeiden" zu explizitem Sleep statt Fehler-Wiederholung.
  • Bundled-Laden fehlschlägt nur warn: describeBundledChannelLoadError (bundled.ts:318) empfiehlt openclaw doctor --fix; ein Kanal, der stirbt, beeinträchtigt die anderen nicht.
  • channelRuntime optional: ChannelGatewayContext.channelRuntime ist optional; externe Plugins sollten auf undefined prüfen (types.adapters.ts:313), sonst droht bei SDK-Versionsabweichung NPE.
  • Ack defaultet manual: defineChannelMessageAdapter setzt receive defaultet auf manual (message/adapter.ts:12); deklariert der Kanal keine Ack-Strategie, ackt der Kerncode nicht automatisch — verhindert „Nachricht gilt als bearbeitet, ist aber verloren".
  • capabilities deklarativ degradieren: durableFinalDeliveryCapabilities (message/types.ts:17) listet 12 Fähigkeiten; nur was der Kanal in capabilities auf true setzt, lässt den Kerncode den jeweiligen zuverlässigen Zustellungspfad gehen, der Rest degradiert ohne Throw.

Zusammenfassung ​

Die Adapterschicht kapselt die Unterschiede von 22 Kanälen hinter einer feinkörnigen, alles optionalen, deklarativen Fähigkeits-Schnittstelle. GatewayAdapter verwaltet Start/Stopp und Login, MessageAdapter verwaltet Outbound-Senden und Inbound-Ack, die feinkörnigen Adapter je einen Fähigkeitsaspekt. Das Gateway injiziert Fähigkeiten über channelRuntime, steuert den Lebenszyklus über abortSignal, wählt Strategien deklarativ über capabilities.

Wie aus der Kanal-Registry der Plugin-Eintrag geholt und sein startAccount gerufen wird und wie Agent-Ereignisse zum MessageAdapter zurück zur Plattform fließen, siehe Chat-Broadcast. Wie jeder Kanal in openclaw.json Token, allowFrom, accounts konfiguriert, siehe Kanal-Konfiguration.