Zum Hauptinhalt springen

Documentation Index

Fetch the complete documentation index at: https://docs.openclaw.ai/llms.txt

Use this file to discover all available pages before exploring further.

Der Kanal-Turn-Kernel ist die gemeinsame eingehende Zustandsmaschine, die ein normalisiertes Plattformereignis in einen Agenten-Turn umwandelt. Kanal-Plugins stellen die Plattformfakten und den Delivery-Callback bereit. Core besitzt die Orchestrierung: erfassen, klassifizieren, vorprüfen, auflösen, autorisieren, zusammensetzen, aufzeichnen, dispatchen und abschließen. Verwenden Sie dies, wenn Ihr Plugin im Hot Path für eingehende Nachrichten liegt. Für Nicht-Nachrichtenereignisse (Slash-Commands, Modals, Button-Interaktionen, Lifecycle-Ereignisse, Reaktionen, Voice State) bleiben diese Plugin-lokal. Der Kernel besitzt nur Ereignisse, die zu einem Agenten-Text-Turn werden können.
Der Kernel wird über die injizierte Plugin-Runtime als runtime.channel.turn.* erreicht. Der Plugin-Runtime-Typ wird aus openclaw/plugin-sdk/core exportiert, sodass native Drittanbieter-Plugins diese Einstiegspunkte genauso verwenden können wie gebündelte Kanal-Plugins.

Warum ein gemeinsamer Kernel

Kanal-Plugins wiederholen denselben eingehenden Ablauf: normalisieren, routen, gaten, einen Kontext aufbauen, Sitzungsmetadaten aufzeichnen, den Agenten-Turn dispatchen, den Delivery-Status abschließen. Ohne gemeinsamen Kernel muss eine Änderung an Mention-Gating, nur für Tools sichtbaren Antworten, Sitzungsmetadaten, ausstehender Historie oder Dispatch-Abschluss pro Kanal angewendet werden. Der Kernel hält vier Konzepte bewusst getrennt:
  • ConversationFacts: woher die Nachricht kam
  • RouteFacts: welcher Agent und welche Sitzung sie verarbeiten sollen
  • ReplyPlanFacts: wohin sichtbare Antworten gehen sollen
  • MessageFacts: welchen Body und Zusatzkontext der Agent sehen soll
Slack-DMs, Telegram-Themen, Matrix-Threads und Feishu-Themensitzungen unterscheiden diese in der Praxis alle. Sie als eine Kennung zu behandeln, verursacht mit der Zeit Drift.

Stage-Lifecycle

Der Kernel führt unabhängig vom Kanal dieselbe feste Pipeline aus:
  1. ingest — Adapter wandelt ein rohes Plattformereignis in NormalizedTurnInput um
  2. classify — Adapter deklariert, ob dieses Ereignis einen Agenten-Turn starten kann
  3. preflight — Adapter erledigt Deduplizierung, Self-Echo, Hydration, Debounce, Entschlüsselung, teilweise Vorbefüllung von Fakten
  4. resolve — Adapter gibt einen vollständig zusammengesetzten Turn zurück (Route, Antwortplan, Nachricht, Delivery)
  5. authorize — DM-, Gruppen-, Mention- und Command-Policy wird auf die zusammengesetzten Fakten angewendet
  6. assembleFinalizedMsgContext wird aus den Fakten über buildContext aufgebaut
  7. record — eingehende Sitzungsmetadaten und letzte Route werden persistiert
  8. dispatch — Agenten-Turn wird über den gepufferten Block-Dispatcher ausgeführt
  9. finalize — Adapter-onFinalize läuft selbst bei einem Dispatch-Fehler
Jede Stage gibt ein strukturiertes Logereignis aus, wenn ein log-Callback bereitgestellt wird. Siehe Observability.

Admission-Arten

Der Kernel wirft keinen Fehler, wenn ein Turn gegatet wird. Er gibt eine ChannelTurnAdmission zurück:
ArtWann
dispatchTurn wird zugelassen. Agenten-Turn läuft und der sichtbare Antwortpfad wird ausgeführt.
observeOnlyTurn läuft vollständig durch, aber der Delivery-Adapter sendet nichts Sichtbares. Wird für Broadcast-Beobachteragenten und andere passive Multi-Agent-Flows verwendet.
handledEin Plattformereignis wurde lokal verarbeitet (Lifecycle, Reaktion, Button, Modal). Kernel überspringt Dispatch.
dropÜbersprungener Pfad. Optional behält recordHistory: true die Nachricht in der ausstehenden Gruppenhistorie, damit eine zukünftige Mention Kontext hat.
Admission kann aus classify kommen (Ereignisklasse sagte, dass sie keinen Turn starten kann), aus preflight (Deduplizierung, Self-Echo, fehlende Mention mit Historienaufzeichnung) oder aus resolveTurn selbst.

Einstiegspunkte

Die Runtime stellt drei bevorzugte Einstiegspunkte bereit, sodass Adapter auf der Ebene einsteigen können, die zum Kanal passt.
runtime.channel.turn.run(...)             // adapter-driven full pipeline
runtime.channel.turn.runPrepared(...)     // channel owns dispatch; kernel runs record + finalize
runtime.channel.turn.buildContext(...)    // pure facts to FinalizedMsgContext mapping
Zwei ältere Runtime-Helfer bleiben für Plugin-SDK-Kompatibilität verfügbar:
runtime.channel.turn.runResolved(...)      // deprecated compatibility alias; prefer run
runtime.channel.turn.dispatchAssembled(...) // deprecated compatibility alias; prefer run or runPrepared

run

Verwenden Sie dies, wenn Ihr Kanal seinen eingehenden Ablauf als ChannelTurnAdapter<TRaw> ausdrücken kann. Der Adapter hat Callbacks für ingest, optional classify, optional preflight, verpflichtend resolveTurn und optional onFinalize.
await runtime.channel.turn.run({
  channel: "tlon",
  accountId,
  raw: platformEvent,
  adapter: {
    ingest(raw) {
      return {
        id: raw.messageId,
        timestamp: raw.timestamp,
        rawText: raw.body,
        textForAgent: raw.body,
      };
    },
    classify(input) {
      return { kind: "message", canStartAgentTurn: input.rawText.length > 0 };
    },
    async preflight(input, eventClass) {
      if (await isDuplicate(input.id)) {
        return { admission: { kind: "drop", reason: "dedupe" } };
      }
      return {};
    },
    resolveTurn(input) {
      return buildAssembledTurn(input);
    },
    onFinalize(result) {
      clearPendingGroupHistory(result);
    },
  },
});
run ist die richtige Form, wenn der Kanal kleine Adapterlogik hat und davon profitiert, den Lifecycle über Hooks zu besitzen.

runPrepared

Verwenden Sie dies, wenn der Kanal einen komplexen lokalen Dispatcher mit Vorschauen, Wiederholungen, Bearbeitungen oder Thread-Bootstrap hat, der im Besitz des Kanals bleiben muss. Der Kernel zeichnet weiterhin die eingehende Sitzung vor dem Dispatch auf und stellt ein einheitliches DispatchedChannelTurnResult bereit.
const { dispatchResult } = await runtime.channel.turn.runPrepared({
  channel: "matrix",
  accountId,
  routeSessionKey,
  storePath,
  ctxPayload,
  recordInboundSession,
  record: {
    onRecordError,
    updateLastRoute,
  },
  onPreDispatchFailure: async (err) => {
    await stopStatusReactions();
  },
  runDispatch: async () => {
    return await runMatrixOwnedDispatcher();
  },
});
Reichhaltige Kanäle (Matrix, Mattermost, Microsoft Teams, Feishu, QQ Bot) verwenden runPrepared, weil ihr Dispatcher plattformspezifisches Verhalten orchestriert, über das der Kernel nichts lernen darf.

buildContext

Eine reine Funktion, die Faktenbündel auf FinalizedMsgContext abbildet. Verwenden Sie sie, wenn Ihr Kanal einen Teil der Pipeline selbst erstellt, aber eine konsistente Kontextform möchte.
const ctxPayload = runtime.channel.turn.buildContext({
  channel: "googlechat",
  accountId,
  messageId,
  timestamp,
  from,
  sender,
  conversation,
  route,
  reply,
  message,
  access,
  media,
  supplemental,
});
buildContext ist auch innerhalb von resolveTurn-Callbacks nützlich, wenn ein Turn für run zusammengesetzt wird.
Veraltete SDK-Helfer wie dispatchInboundReplyWithBase bridgen weiterhin über einen Helfer für zusammengesetzte Turns. Neuer Plugin-Code sollte run oder runPrepared verwenden.

Faktentypen

Die Fakten, die der Kernel von Ihrem Adapter konsumiert, sind plattformagnostisch. Übersetzen Sie Plattformobjekte in diese Formen, bevor Sie sie an den Kernel übergeben.

NormalizedTurnInput

FeldZweck
idStabile Nachrichten-ID für Deduplizierung und Logs
timestampOptionale Epochenzeit in ms
rawTextBody, wie von der Plattform empfangen
textForAgentOptional bereinigter Body für den Agenten (Mention entfernen, Eingabe trimmen)
textForCommandsOptionaler Body für das Parsen von /command
rawOptionale Pass-through-Referenz für Adapter-Callbacks, die das Original benötigen

ChannelEventClass

FeldZweck
kindmessage, command, interaction, reaction, lifecycle, unknown
canStartAgentTurnWenn false, gibt der Kernel { kind: "handled" } zurück
requiresImmediateAckHinweis für Adapter, die vor dem Dispatch ein ACK senden müssen

SenderFacts

FeldZweck
idStabile Plattform-Sender-ID
nameAnzeigename
usernameHandle, wenn verschieden von name
tagDiscord-artiger Discriminator oder Plattform-Tag
rolesRollen-IDs, verwendet für Allowlist-Abgleich von Mitgliedsrollen
isBotTrue, wenn der Sender ein bekannter Bot ist (Kernel nutzt dies zum Verwerfen)
isSelfTrue, wenn der Sender der konfigurierte Agent selbst ist
displayLabelVorgerendertes Label für Envelope-Text

ConversationFacts

FeldZweck
kinddirect, group oder channel
idKonversations-ID für Routing
labelMenschliches Label für den Envelope
spaceIdOptionale äußere Space-Kennung (Slack-Workspace, Matrix-Homeserver)
parentIdÄußere Konversations-ID, wenn dies ein Thread ist
threadIdThread-ID, wenn diese Nachricht in einem Thread liegt
nativeChannelIdPlattformeigene Kanal-ID, wenn sie sich von der Routing-ID unterscheidet
routePeerPeer, der für die resolveAgentRoute-Suche verwendet wird

RouteFacts

FeldZweck
agentIdAgent, der diesen Turn verarbeiten soll
accountIdOptionale Überschreibung (Multi-Account-Kanäle)
routeSessionKeySitzungsschlüssel für Routing
dispatchSessionKeySitzungsschlüssel für Dispatch, wenn verschieden vom Route-Schlüssel
persistedSessionKeySitzungsschlüssel, der in persistierte Sitzungsmetadaten geschrieben wird
parentSessionKeyParent für verzweigte/Thread-Sitzungen
modelParentSessionKeyModellseitiger Parent für verzweigte Sitzungen
mainSessionKeyHaupt-DM-Owner-Pin für direkte Konversationen
createIfMissingErlaubt dem Aufzeichnungsschritt, eine fehlende Sitzungszeile zu erstellen

ReplyPlanFacts

FeldZweck
toLogisches Antwortziel, das in den Kontext To geschrieben wird
originatingToUrsprüngliches Kontextziel (OriginatingTo)
nativeChannelIdPlattformnativer Kanal-ID für die Zustellung
replyTargetEndgültiges sichtbares Antwortziel, wenn es sich von to unterscheidet
deliveryTargetZustellungsüberschreibung auf niedrigerer Ebene
replyToIdZitierte/verankerte Nachrichten-ID
replyToIdFullVollständige zitierte ID, wenn die Plattform beides hat
messageThreadIdThread-ID zum Zustellungszeitpunkt
threadParentIdID der übergeordneten Nachricht des Threads
sourceReplyDeliveryModethread, reply, channel, direct oder none

AccessFacts

AccessFacts enthält die booleschen Werte, die die Autorisierungsphase benötigt. Der Identitätsabgleich bleibt im Kanal: Der Kernel verbraucht nur das Ergebnis.
FeldZweck
dmDM-Entscheidung zu Zulassen/Koppeln/Ablehnen und allowFrom-Liste
groupGruppenrichtlinie, Routenfreigabe, Absenderfreigabe, Allowlist, Erwähnungserfordernis
commandsBefehlsautorisierung über konfigurierte Autorisierer hinweg
mentionsOb Erwähnungserkennung möglich ist und ob der Agent erwähnt wurde

MessageFacts

FeldZweck
bodyEndgültiger Envelope-Body (formatiert)
rawBodyRohdaten des eingehenden Bodys
bodyForAgentBody, den der Agent sieht
commandBodyFür das Parsen von Befehlen verwendeter Body
envelopeFromVorab gerendertes Absenderlabel für den Envelope
senderLabelOptionale Überschreibung für den gerenderten Absender
previewKurze redigierte Vorschau für Logs
inboundHistoryAktuelle eingehende Verlaufseinträge, wenn der Kanal einen Puffer führt

SupplementalContextFacts

Zusätzlicher Kontext umfasst Zitat-, Weiterleitungs- und Thread-Bootstrap-Kontext. Der Kernel wendet die konfigurierte contextVisibility-Richtlinie an. Der Kanaladapter stellt nur Fakten und senderAllowed-Flags bereit, damit die kanalübergreifende Richtlinie konsistent bleibt.

InboundMediaFacts

Medien sind als Fakten modelliert. Plattformdownload, Authentifizierung, SSRF-Richtlinie, CDN-Regeln und Entschlüsselung bleiben kanallokal. Der Kernel ordnet Fakten MediaPath, MediaUrl, MediaType, MediaPaths, MediaUrls, MediaTypes und MediaTranscribedIndexes zu.

Adaptervertrag

Für vollständiges run hat der Adapter diese Form:
type ChannelTurnAdapter<TRaw> = {
  ingest(raw: TRaw): Promise<NormalizedTurnInput | null> | NormalizedTurnInput | null;
  classify?(input: NormalizedTurnInput): Promise<ChannelEventClass> | ChannelEventClass;
  preflight?(
    input: NormalizedTurnInput,
    eventClass: ChannelEventClass,
  ): Promise<PreflightFacts | ChannelTurnAdmission | null | undefined>;
  resolveTurn(
    input: NormalizedTurnInput,
    eventClass: ChannelEventClass,
    preflight: PreflightFacts,
  ): Promise<ChannelTurnResolved> | ChannelTurnResolved;
  onFinalize?(result: ChannelTurnResult): Promise<void> | void;
};
resolveTurn gibt ein ChannelTurnResolved zurück, also ein AssembledChannelTurn mit optionaler Admission-Art. Die Rückgabe von { admission: { kind: "observeOnly" } } führt den Turn aus, ohne sichtbare Ausgabe zu erzeugen. Der Adapter besitzt weiterhin den Zustellungs-Callback; er wird für diesen Turn lediglich zu einem No-op. onFinalize wird für jedes Ergebnis ausgeführt, einschließlich Dispatch-Fehlern. Verwenden Sie es, um ausstehende Gruppenverläufe zu löschen, Ack-Reaktionen zu entfernen, Statusindikatoren zu stoppen und lokalen Zustand zu flushen.

Zustellungsadapter

Der Kernel ruft die Plattform nicht direkt auf. Der Kanal übergibt dem Kernel einen ChannelTurnDeliveryAdapter:
type ChannelTurnDeliveryAdapter = {
  deliver(payload: ReplyPayload, info: ChannelDeliveryInfo): Promise<ChannelDeliveryResult | void>;
  onError?(err: unknown, info: { kind: string }): void;
  durable?: false | DurableInboundReplyDeliveryOptions;
};

type ChannelDeliveryResult = {
  messageIds?: string[];
  receipt?: MessageReceipt;
  threadId?: string;
  replyToId?: string;
  visibleReplySent?: boolean;
};
deliver wird einmal pro gepuffertem Antwort-Chunk aufgerufen. Während der Migration des Nachrichtenlebenszyklus ist die Zustellung zusammengesetzter Kanal-Turns standardmäßig kanaleigen: Ein ausgelassenes durable-Feld bedeutet, dass der Kernel deliver direkt aufrufen muss und nicht über die generische ausgehende Zustellung routen darf. Setzen Sie durable erst, nachdem der Kanal geprüft wurde, um nachzuweisen, dass der generische Sendepfad das alte Zustellungsverhalten beibehält, einschließlich Antwort-/Thread-Zielen, Medienverarbeitung, Caches für gesendete Nachrichten/Selbst-Echos, Statusbereinigung und zurückgegebenen Nachrichten-IDs. durable: false bleibt eine Kompatibilitätsschreibweise für „kanaleigenen Callback verwenden“, aber nicht migrierte Kanäle sollten es nicht hinzufügen müssen. Geben Sie Plattform-Nachrichten-IDs zurück, wenn der Kanal sie hat, damit der Dispatcher Thread-Anker erhalten und spätere Chunks bearbeiten kann; neuere Zustellungspfade sollten außerdem receipt zurückgeben, damit Wiederherstellung, Vorschau-Finalisierung und Duplikatunterdrückung von messageIds weg migrieren können. Für reine Beobachtungs-Turns geben Sie { visibleReplySent: false } zurück oder verwenden Sie createNoopChannelTurnDeliveryAdapter(). Kanäle, die runPrepared mit einem vollständig kanaleigenen Dispatcher verwenden, haben keinen ChannelTurnDeliveryAdapter. Diese Dispatcher sind standardmäßig nicht durable. Sie sollten ihren direkten Zustellungspfad beibehalten, bis sie sich ausdrücklich für den neuen Sendekontext mit vollständigem Ziel, replay-sicherem Adapter, Receipt-Vertrag und kanalseitigen Nebenwirkungs-Hooks entscheiden. Öffentliche Kompatibilitäts-Helper wie recordInboundSessionAndDispatchReply, dispatchInboundReplyWithBase und Direct-DM-Helper müssen während der Migration verhaltenserhaltend bleiben. Sie dürfen die generische durable Zustellung nicht vor aufrufereigenen deliver- oder reply-Callbacks aufrufen.

Aufzeichnungsoptionen

Die Aufzeichnungsphase umschließt recordInboundSession. Die meisten Kanäle können die Standardwerte verwenden. Überschreiben Sie sie über record:
record: {
  groupResolution,
  createIfMissing: true,
  updateLastRoute,
  onRecordError: (err) => log.warn("record failed", err),
  trackSessionMetaTask: (task) => pendingTasks.push(task),
}
Der Dispatcher wartet auf die Aufzeichnungsphase. Wenn die Aufzeichnung eine Exception auslöst, führt der Kernel onPreDispatchFailure aus (wenn für runPrepared bereitgestellt) und wirft erneut.

Observability

Jede Phase gibt ein strukturiertes Ereignis aus, wenn ein log-Callback bereitgestellt wird:
await runtime.channel.turn.run({
  channel: "twitch",
  accountId,
  raw,
  adapter,
  log: (event) => {
    runtime.log?.debug?.(`turn.${event.stage}:${event.event}`, {
      channel: event.channel,
      accountId: event.accountId,
      messageId: event.messageId,
      sessionKey: event.sessionKey,
      admission: event.admission,
      reason: event.reason,
    });
  },
});
Protokollierte Phasen: ingest, classify, preflight, resolve, authorize, assemble, record, dispatch, finalize. Vermeiden Sie das Loggen roher Bodys; verwenden Sie MessageFacts.preview für kurze redigierte Vorschauen.

Was kanallokal bleibt

Der Kernel besitzt die Orchestrierung. Der Kanal besitzt weiterhin:
  • Plattformtransporte (Gateway, REST, WebSocket, Polling, Webhooks)
  • Identitätsauflösung und Abgleich von Anzeigenamen
  • Native Befehle, Slash-Befehle, Autocomplete, Modals, Buttons, Voice-Status
  • Rendering von Karten, Modals und Adaptive Cards
  • Medienauthentifizierung, CDN-Regeln, verschlüsselte Medien, Transkription
  • Bearbeitungs-, Reaktions-, Redaktions- und Presence-APIs
  • Backfill und plattformseitiger Verlaufsabruf
  • Kopplungsabläufe, die plattformspezifische Verifizierung erfordern
Wenn zwei Kanäle denselben Helper für einen dieser Punkte benötigen, extrahieren Sie einen gemeinsamen SDK-Helper, anstatt ihn in den Kernel zu verschieben.

Stabilität

runtime.channel.turn.* ist Teil der öffentlichen Plugin-Runtime-Oberfläche. Die Faktentypen (SenderFacts, ConversationFacts, RouteFacts, ReplyPlanFacts, AccessFacts, MessageFacts, SupplementalContextFacts, InboundMediaFacts) und Admission-Formen (ChannelTurnAdmission, ChannelEventClass) sind über PluginRuntime aus openclaw/plugin-sdk/core erreichbar. Es gelten Regeln für Abwärtskompatibilität: Neue Faktenfelder sind additiv, Admission-Arten werden nicht umbenannt, und die Einstiegspunktnamen bleiben stabil. Neue Kanalanforderungen, die eine nicht additive Änderung erfordern, müssen den Migrationsprozess des Plugin-SDK durchlaufen.

Verwandt