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 Channel-Turn-Kernel ist die gemeinsame eingehende Zustandsmaschine, die ein normalisiertes Plattformereignis in einen Agent-Turn umwandelt. Channel-Plugins liefern die Plattformfakten und den Delivery-Callback. Core übernimmt die Orchestrierung: Ingest, Klassifizierung, Preflight, Auflösung, Autorisierung, Zusammenstellung, Aufzeichnung, Dispatch und Finalisierung. Verwenden Sie dies, wenn Ihr Plugin im Hot Path für eingehende Nachrichten liegt. Halten Sie nicht nachrichtenbezogene Ereignisse (Slash-Commands, Modals, Button-Interaktionen, Lifecycle-Ereignisse, Reaktionen, Voice State) Plugin-lokal. Der Kernel übernimmt nur Ereignisse, die zu einem Text-Turn des Agents 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 Channel-Plugins.

Warum ein gemeinsamer Kernel

Channel-Plugins wiederholen denselben eingehenden Ablauf: normalisieren, routen, sperren, einen Kontext erstellen, Sitzungsmetadaten aufzeichnen, den Agent-Turn dispatchen, den Delivery-Status finalisieren. Ohne gemeinsamen Kernel muss eine Änderung an Mention-Gating, sichtbaren Tool-only-Antworten, Sitzungsmetadaten, ausstehender Historie oder Dispatch-Finalisierung pro Channel angewendet werden. Der Kernel hält vier Konzepte bewusst getrennt:
  • ConversationFacts: woher die Nachricht kam
  • RouteFacts: welcher Agent und welche Sitzung sie verarbeiten soll
  • ReplyPlanFacts: wohin sichtbare Antworten gehen sollen
  • MessageFacts: welchen Inhalt und Zusatzkontext der Agent sehen soll
Slack-DMs, Telegram-Themen, Matrix-Threads und Feishu-Themensitzungen unterscheiden diese in der Praxis alle. Sie als einen einzigen Bezeichner zu behandeln, führt im Lauf der Zeit zu Abweichungen.

Stage-Lifecycle

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

Zulassungsarten

Der Kernel wirft keinen Fehler, wenn ein Turn gesperrt wird. Er gibt eine ChannelTurnAdmission zurück:
ArtWann
dispatchTurn wird zugelassen. Agent-Turn läuft, und der sichtbare Antwortpfad wird ausgeführt.
observeOnlyTurn läuft End-to-End, aber der Delivery-Adapter sendet nichts Sichtbares. Wird für Broadcast-Beobachter-Agents und andere passive Multi-Agent-Flows genutzt.
handledEin Plattformereignis wurde lokal verarbeitet (Lifecycle, Reaktion, Button, Modal). Kernel überspringt Dispatch.
dropÜberspringpfad. Optional hält recordHistory: true die Nachricht in der ausstehenden Gruppenhistorie, damit eine zukünftige Mention Kontext hat.
Die Zulassung 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, damit Adapter sich auf der Ebene einklinken können, die zum Channel 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-Hilfen 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 Channel 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 passende Form, wenn der Channel kleine Adapterlogik hat und davon profitiert, den Lifecycle über Hooks zu steuern.

runPrepared

Verwenden Sie dies, wenn der Channel einen komplexen lokalen Dispatcher mit Vorschauen, Wiederholungen, Edits oder Thread-Bootstrap hat, der Channel-eigen 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 Channels (Matrix, Mattermost, Microsoft Teams, Feishu, QQ Bot) verwenden runPrepared, weil ihr Dispatcher plattformspezifisches Verhalten orchestriert, das der Kernel nicht kennen darf.

buildContext

Eine reine Funktion, die Faktenbündel auf FinalizedMsgContext abbildet. Verwenden Sie sie, wenn Ihr Channel einen Teil der Pipeline manuell implementiert, 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 zusammengestellt wird.
Veraltete SDK-Hilfen wie dispatchInboundReplyWithBase überbrücken weiterhin über eine Hilfe für zusammengestellte 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 Epoch-ms
rawTextInhalt, wie von der Plattform empfangen
textForAgentOptional bereinigter Inhalt für den Agent (Mention entfernen, Typing-Trim)
textForCommandsOptionaler Inhalt 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-Absender-ID
nameAnzeigename
usernameHandle, falls von name verschieden
tagDiscord-artiger Diskriminator oder Plattform-Tag
rolesRollen-IDs, verwendet für Allowlist-Abgleich von Mitgliedsrollen
isBotTrue, wenn der Absender ein bekannter Bot ist (Kernel nutzt dies zum Verwerfen)
isSelfTrue, wenn der Absender der konfigurierte Agent selbst ist
displayLabelVorgerendertes Label für Hülltext

ConversationFacts

FeldZweck
kinddirect, group oder channel
idConversation-ID, die für Routing verwendet wird
labelMenschliches Label für die Hülle
spaceIdOptionaler äußerer Space-Bezeichner (Slack-Workspace, Matrix-Homeserver)
parentIdÄußere Conversation-ID, wenn dies ein Thread ist
threadIdThread-ID, wenn diese Nachricht in einem Thread liegt
nativeChannelIdPlattformnative Channel-ID, wenn sie von der Routing-ID abweicht
routePeerPeer, der für die resolveAgentRoute-Suche verwendet wird

RouteFacts

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

ReplyPlanFacts

FeldZweck
toLogisches Antwortziel, das in den Kontext To geschrieben wird
originatingToUrsprüngliches Kontextziel (OriginatingTo)
nativeChannelIdPlattformnative Kanal-ID für die Zustellung
replyTargetEndgültiges sichtbares Antwortziel, falls es sich von to unterscheidet
deliveryTargetUntergeordnete Zustellungsüberschreibung
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 Autorisierungsstufe benötigt. Der Identitätsabgleich bleibt im Kanal: Der Kernel verbraucht nur das Ergebnis.
FeldZweck
dmDM-Zulassen-/Koppeln-/Ablehnen-Entscheidung und allowFrom-Liste
groupGruppenrichtlinie, Routenzulassung, Absenderzulassung, Allowlist, Erwähnungspflicht
commandsBefehlsautorisierung über konfigurierte Autorisierer hinweg
mentionsOb Erwähnungserkennung möglich ist und ob der Agent erwähnt wurde

MessageFacts

FeldZweck
bodyEndgültiger Envelope-Text (formatiert)
rawBodyRoher eingehender Text
bodyForAgentText, den der Agent sieht
commandBodyFür die Befehlsanalyse verwendeter Text
envelopeFromVorgerendertes 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

Ergänzender Kontext umfasst Kontext für Zitate, Weiterleitungen und Thread-Bootstrapping. 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 geformt. 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. Das Zurückgeben 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 läuft für jedes Ergebnis, einschließlich Dispatch-Fehlern. Verwenden Sie es, um ausstehenden Gruppenverlauf 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;
};

type ChannelDeliveryResult = {
  messageIds?: string[];
  threadId?: string;
  replyToId?: string;
  visibleReplySent?: boolean;
};
deliver wird einmal pro gepuffertem Antwort-Chunk aufgerufen. Geben Sie Plattform-Nachrichten-IDs zurück, wenn der Kanal sie hat, damit der Dispatcher Thread-Anker beibehalten und spätere Chunks bearbeiten kann. Geben Sie für observe-only-Turns { visibleReplySent: false } zurück oder verwenden Sie createNoopChannelTurnDeliveryAdapter().

Aufzeichnungsoptionen

Die Aufzeichnungsstufe kapselt 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 Aufzeichnungsstufe. Wenn die Aufzeichnung einen Fehler auslöst, führt der Kernel onPreDispatchFailure aus (wenn es an runPrepared übergeben wurde) und wirft den Fehler erneut.

Beobachtbarkeit

Jede Stufe 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 Stufen: ingest, classify, preflight, resolve, authorize, assemble, record, dispatch, finalize. Vermeiden Sie das Protokollieren roher Texte; 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 Anzeigenamenabgleich
  • Native Befehle, Slash-Befehle, Autovervollständigung, Modale, Buttons, Sprachstatus
  • Rendering von Karten, Modalen und Adaptive Cards
  • Medienauthentifizierung, CDN-Regeln, verschlüsselte Medien, Transkription
  • APIs für Bearbeitung, Reaktion, Redigierung und Präsenz
  • Backfill und plattformseitiger Verlaufsabruf
  • Kopplungsabläufe, die plattformspezifische Verifizierung erfordern
Wenn zwei Kanäle beginnen, denselben Helper für einen dieser Punkte zu benötigen, extrahieren Sie stattdessen einen gemeinsamen SDK-Helper, anstatt ihn in den Kernel zu verschieben.

Stabilität

runtime.channel.turn.* ist Teil der öffentlichen Plugin-Laufzeitoberflä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. Regeln zur Abwärtskompatibilität gelten: Neue Faktenfelder sind additiv, Admission-Arten werden nicht umbenannt, und die Namen der Einstiegspunkte bleiben stabil. Neue Kanalanforderungen, die eine nicht-additive Änderung erfordern, müssen den Migrationsprozess des Plugin-SDK durchlaufen.

Verwandt