Passer au contenu principal

Documentation Index

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

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

Le noyau de tour de canal est la machine d’état entrante partagée qui transforme un événement de plateforme normalisé en tour d’agent. Les plugins de canal fournissent les faits de plateforme et le rappel de livraison. Le cœur possède l’orchestration : ingestion, classification, pré-vérification, résolution, autorisation, assemblage, enregistrement, dispatch et finalisation. Utilisez-le lorsque votre plugin se trouve sur le chemin critique des messages entrants. Pour les événements qui ne sont pas des messages (commandes slash, modales, interactions de boutons, événements de cycle de vie, réactions, état vocal), gardez-les locaux au plugin. Le noyau ne possède que les événements susceptibles de devenir un tour textuel d’agent.
Le noyau est atteint via le runtime de plugin injecté avec runtime.channel.turn.*. Le type de runtime de plugin est exporté depuis openclaw/plugin-sdk/core, afin que les plugins natifs tiers puissent utiliser ces points d’entrée de la même manière que les plugins de canal intégrés.

Pourquoi un noyau partagé

Les plugins de canal répètent le même flux entrant : normaliser, router, contrôler, construire un contexte, enregistrer les métadonnées de session, dispatcher le tour d’agent, finaliser l’état de livraison. Sans noyau partagé, une modification du contrôle des mentions, des réponses visibles uniquement pour les outils, des métadonnées de session, de l’historique en attente ou de la finalisation du dispatch doit être appliquée canal par canal. Le noyau garde délibérément quatre concepts séparés :
  • ConversationFacts : d’où vient le message
  • RouteFacts : quel agent et quelle session doivent le traiter
  • ReplyPlanFacts : où les réponses visibles doivent aller
  • MessageFacts : quel corps et quel contexte supplémentaire l’agent doit voir
Les DM Slack, les sujets Telegram, les fils Matrix et les sessions de sujet Feishu les distinguent tous en pratique. Les traiter comme un seul identifiant provoque une dérive au fil du temps.

Cycle de vie des étapes

Le noyau exécute le même pipeline fixe quel que soit le canal :
  1. ingest — l’adaptateur convertit un événement brut de plateforme en NormalizedTurnInput
  2. classify — l’adaptateur déclare si cet événement peut démarrer un tour d’agent
  3. preflight — l’adaptateur effectue la déduplication, l’auto-écho, l’hydratation, le debounce, le déchiffrement et le préremplissage partiel des faits
  4. resolve — l’adaptateur renvoie un tour entièrement assemblé (route, plan de réponse, message, livraison)
  5. authorize — la politique de DM, groupe, mention et commande est appliquée aux faits assemblés
  6. assembleFinalizedMsgContext est construit à partir des faits via buildContext
  7. record — les métadonnées de session entrante et la dernière route sont persistées
  8. dispatch — le tour d’agent est exécuté via le dispatcher de blocs mis en tampon
  9. finalizeonFinalize de l’adaptateur s’exécute même en cas d’erreur de dispatch
Chaque étape émet un événement de journal structuré lorsqu’un rappel log est fourni. Voir Observabilité.

Types d’admission

Le noyau ne lève pas d’exception lorsqu’un tour est bloqué. Il renvoie un ChannelTurnAdmission :
TypeQuand
dispatchLe tour est admis. Le tour d’agent s’exécute et le chemin de réponse visible est exercé.
observeOnlyLe tour s’exécute de bout en bout, mais l’adaptateur de livraison n’envoie rien de visible. Utilisé pour les agents observateurs de diffusion et les autres flux multi-agents passifs.
handledUn événement de plateforme a été consommé localement (cycle de vie, réaction, bouton, modale). Le noyau ignore le dispatch.
dropChemin ignoré. Facultativement, recordHistory: true conserve le message dans l’historique de groupe en attente afin qu’une future mention dispose du contexte.
L’admission peut provenir de classify (la classe d’événement a indiqué qu’elle ne peut pas démarrer un tour), de preflight (déduplication, auto-écho, mention manquante avec enregistrement de l’historique) ou de resolveTurn lui-même.

Points d’entrée

Le runtime expose trois points d’entrée préférés afin que les adaptateurs puissent s’intégrer au niveau correspondant au canal.
runtime.channel.turn.run(...)             // adapter-driven full pipeline
runtime.channel.turn.runAssembled(...)    // already-built context + delivery adapter
runtime.channel.turn.runPrepared(...)     // channel owns dispatch; kernel runs record + finalize
runtime.channel.turn.buildContext(...)    // pure facts to FinalizedMsgContext mapping
Deux anciens assistants de runtime restent disponibles pour la compatibilité du Plugin SDK :
runtime.channel.turn.runResolved(...)      // deprecated compatibility alias; prefer run
runtime.channel.turn.dispatchAssembled(...) // deprecated compatibility alias; prefer runAssembled

run

À utiliser lorsque votre canal peut exprimer son flux entrant comme un ChannelTurnAdapter<TRaw>. L’adaptateur dispose de rappels pour ingest, classify facultatif, preflight facultatif, resolveTurn obligatoire et onFinalize facultatif.
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 est la bonne forme lorsque le canal a une petite logique d’adaptateur et gagne à posséder le cycle de vie via des hooks.

runAssembled

À utiliser lorsque le canal a déjà résolu le routage, construit un FinalizedMsgContext, et n’a besoin que de l’ordre partagé d’enregistrement, de pipeline de réponse, de dispatch et de finalisation. C’est la forme préférée pour les chemins entrants intégrés simples qui répéteraient autrement le code standard createChannelMessageReplyPipeline(...) et runPrepared(...).
await runtime.channel.turn.runAssembled({
  cfg,
  channel: "irc",
  accountId,
  agentId: route.agentId,
  routeSessionKey: route.sessionKey,
  storePath,
  ctxPayload,
  recordInboundSession: runtime.channel.session.recordInboundSession,
  dispatchReplyWithBufferedBlockDispatcher:
    runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher,
  delivery: {
    deliver: async (payload) => {
      await sendPlatformReply(payload);
    },
    onError: (err, info) => {
      runtime.error?.(`reply ${info.kind} failed: ${String(err)}`);
    },
  },
});
Choisissez runAssembled plutôt que runPrepared lorsque le seul comportement de dispatch possédé par le canal est la livraison finale de la charge utile, plus éventuellement l’indicateur de saisie, les options de réponse, la livraison durable ou la journalisation des erreurs.

runPrepared

À utiliser lorsque le canal possède un dispatcher local complexe avec aperçus, tentatives, modifications ou amorçage de fil qui doit rester possédé par le canal. Le noyau enregistre tout de même la session entrante avant le dispatch et expose un DispatchedChannelTurnResult uniforme.
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();
  },
});
Les canaux riches (Matrix, Mattermost, Microsoft Teams, Feishu, QQ Bot) utilisent runPrepared parce que leur dispatcher orchestre un comportement propre à la plateforme que le noyau ne doit pas apprendre.

buildContext

Une fonction pure qui mappe des ensembles de faits vers FinalizedMsgContext. Utilisez-la lorsque votre canal implémente manuellement une partie du pipeline, mais veut une forme de contexte cohérente.
const ctxPayload = runtime.channel.turn.buildContext({
  channel: "googlechat",
  accountId,
  messageId,
  timestamp,
  from,
  sender,
  conversation,
  route,
  reply,
  message,
  access,
  media,
  supplemental,
});
buildContext est aussi utile dans les rappels resolveTurn lors de l’assemblage d’un tour pour run.
Les assistants SDK obsolètes comme dispatchInboundReplyWithBase passent encore par un assistant de tour assemblé. Le nouveau code de plugin doit utiliser run ou runPrepared.

Types de faits

Les faits que le noyau consomme depuis votre adaptateur sont indépendants de la plateforme. Traduisez les objets de plateforme dans ces formes avant de les transmettre au noyau.

NormalizedTurnInput

ChampObjectif
idIdentifiant de message stable utilisé pour la déduplication et les journaux
timestampMillisecondes d’époque facultatives
rawTextCorps reçu depuis la plateforme
textForAgentCorps nettoyé facultatif pour l’agent (suppression de mention, trim de saisie)
textForCommandsCorps facultatif utilisé pour l’analyse de /command
rawRéférence de transit facultative pour les rappels d’adaptateur qui ont besoin de l’original

ChannelEventClass

ChampObjectif
kindmessage, command, interaction, reaction, lifecycle, unknown
canStartAgentTurnSi faux, le noyau renvoie { kind: "handled" }
requiresImmediateAckIndication pour les adaptateurs qui doivent ACK avant le dispatch

SenderFacts

ChampObjectif
idIdentifiant stable de l’expéditeur sur la plateforme
nameNom d’affichage
usernameIdentifiant si distinct de name
tagDiscriminateur de style Discord ou tag de plateforme
rolesIdentifiants de rôles, utilisés pour la correspondance de liste d’autorisation par rôle membre
isBotVrai lorsque l’expéditeur est un bot connu (le noyau l’utilise pour le rejet)
isSelfVrai lorsque l’expéditeur est l’agent configuré lui-même
displayLabelLibellé pré-rendu pour le texte d’enveloppe

ConversationFacts

ChampObjectif
kinddirect, group ou channel
idIdentifiant de conversation utilisé pour le routage
labelLibellé humain pour l’enveloppe
spaceIdIdentifiant d’espace externe facultatif (espace de travail Slack, homeserver Matrix)
parentIdIdentifiant de conversation externe lorsque ceci est un fil
threadIdIdentifiant de fil lorsque ce message se trouve dans un fil
nativeChannelIdIdentifiant de canal natif de la plateforme lorsqu’il diffère de l’identifiant de routage
routePeerPair utilisé pour la recherche resolveAgentRoute

RouteFacts

ChampObjectif
agentIdAgent qui doit traiter ce tour
accountIdRemplacement facultatif (canaux multi-comptes)
routeSessionKeyClé de session utilisée pour le routage
dispatchSessionKeyClé de session utilisée à l’expédition lorsqu’elle diffère de la clé de routage
persistedSessionKeyClé de session écrite dans les métadonnées de session persistées
parentSessionKeyParent pour les sessions ramifiées/enfilées
modelParentSessionKeyParent côté modèle pour les sessions ramifiées
mainSessionKeyÉpingle du propriétaire de DM principal pour les conversations directes
createIfMissingAutoriser l’étape d’enregistrement à créer une ligne de session manquante

ReplyPlanFacts

ChampObjectif
toCible logique de réponse écrite dans le contexte To
originatingToCible de contexte d’origine (OriginatingTo)
nativeChannelIdIdentifiant de canal natif à la plateforme pour la livraison
replyTargetDestination finale de la réponse visible si elle diffère de to
deliveryTargetRemplacement de livraison de niveau inférieur
replyToIdIdentifiant du message cité/ancré
replyToIdFullIdentifiant cité en forme complète lorsque la plateforme a les deux
messageThreadIdIdentifiant du fil au moment de la livraison
threadParentIdIdentifiant du message parent du fil
sourceReplyDeliveryModethread, reply, channel, direct ou none

AccessFacts

AccessFacts transporte les booléens dont l’étape d’autorisation a besoin. La correspondance d’identité reste dans le canal : le noyau ne consomme que le résultat.
ChampObjectif
dmDécision autoriser/appairer/refuser pour les DM et liste allowFrom
groupPolitique de groupe, autorisation de route, autorisation d’expéditeur, liste d’autorisation, exigence de mention
commandsAutorisation des commandes parmi les autorisateurs configurés
mentionsIndique si la détection de mention est possible et si l’agent a été mentionné

MessageFacts

ChampObjectif
bodyCorps final de l’enveloppe (formaté)
rawBodyCorps entrant brut
bodyForAgentCorps vu par l’agent
commandBodyCorps utilisé pour l’analyse des commandes
envelopeFromLibellé d’expéditeur pré-rendu pour l’enveloppe
senderLabelRemplacement facultatif pour l’expéditeur rendu
previewCourt aperçu caviardé pour les journaux
inboundHistoryEntrées récentes de l’historique entrant lorsque le canal conserve un tampon

SupplementalContextFacts

Le contexte supplémentaire couvre le contexte de citation, de transfert et d’amorçage de fil. Le noyau applique la politique contextVisibility configurée. L’adaptateur de canal fournit uniquement les faits et les indicateurs senderAllowed afin que la politique inter-canaux reste cohérente.

InboundMediaFacts

Les médias sont structurés comme des faits. Le téléchargement plateforme, l’authentification, la politique SSRF, les règles CDN et le déchiffrement restent propres au canal. Le noyau mappe les faits vers MediaPath, MediaUrl, MediaType, MediaPaths, MediaUrls, MediaTypes et MediaTranscribedIndexes.

Contrat d’adaptateur

Pour run complet, la forme de l’adaptateur est :
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 renvoie un ChannelTurnResolved, qui est un AssembledChannelTurn avec un type d’admission facultatif. Renvoyer { admission: { kind: "observeOnly" } } exécute le tour sans produire de sortie visible. L’adaptateur reste propriétaire du rappel de livraison ; il devient simplement un no-op pour ce tour. onFinalize s’exécute pour chaque résultat, y compris les erreurs d’expédition. Utilisez-le pour effacer l’historique de groupe en attente, supprimer les réactions d’accusé de réception, arrêter les indicateurs d’état et vider l’état local.

Adaptateur de livraison

Le noyau n’appelle pas directement la plateforme. Le canal fournit au noyau un 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 est appelé une fois par fragment de réponse mis en mémoire tampon. Pendant la migration du cycle de vie des messages, la livraison des tours de canal assemblés appartient par défaut au canal : un champ durable omis signifie que le noyau doit appeler directement deliver et ne doit pas passer par la livraison sortante générique. Définissez durable seulement après avoir audité le canal pour prouver que le chemin d’envoi générique préserve l’ancien comportement de livraison, y compris les cibles de réponse/fil, la gestion des médias, les caches de messages envoyés/d’écho de soi, le nettoyage d’état et les identifiants de messages renvoyés. durable: false reste une écriture de compatibilité pour « utiliser le rappel appartenant au canal », mais les canaux non migrés ne devraient pas avoir besoin de l’ajouter. Renvoyez les identifiants de messages de la plateforme lorsque le canal les possède afin que le répartiteur puisse préserver les ancres de fil et modifier les fragments ultérieurs ; les chemins de livraison plus récents devraient aussi renvoyer receipt afin que la récupération, la finalisation d’aperçu et la suppression des doublons puissent se détacher de messageIds. Pour les tours en observation seule, renvoyez { visibleReplySent: false } ou utilisez createNoopChannelTurnDeliveryAdapter(). Les canaux utilisant runPrepared avec un répartiteur entièrement détenu par le canal n’ont pas de ChannelTurnDeliveryAdapter. Ces répartiteurs ne sont pas durables par défaut. Ils doivent conserver leur chemin de livraison direct jusqu’à ce qu’ils optent explicitement pour le nouveau contexte d’envoi avec une cible complète, un adaptateur sûr pour la relecture, un contrat de reçu et des hooks d’effets de bord côté canal. Les helpers de compatibilité publics tels que recordInboundSessionAndDispatchReply, dispatchInboundReplyWithBase et les helpers de DM directs doivent préserver le comportement pendant la migration. Ils ne doivent pas appeler la livraison durable générique avant les rappels deliver ou reply détenus par l’appelant.

Options d’enregistrement

L’étape d’enregistrement encapsule recordInboundSession. La plupart des canaux peuvent utiliser les valeurs par défaut. Remplacez via record :
record: {
  groupResolution,
  createIfMissing: true,
  updateLastRoute,
  onRecordError: (err) => log.warn("record failed", err),
  trackSessionMetaTask: (task) => pendingTasks.push(task),
}
Le répartiteur attend l’étape d’enregistrement. Si l’enregistrement lève une exception, le noyau exécute onPreDispatchFailure (lorsqu’il est fourni à runPrepared) puis relance l’exception.

Observabilité

Chaque étape émet un événement structuré lorsqu’un rappel log est fourni :
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,
    });
  },
});
Étapes journalisées : ingest, classify, preflight, resolve, authorize, assemble, record, dispatch, finalize. Évitez de journaliser les corps bruts ; utilisez MessageFacts.preview pour de courts aperçus caviardés.

Ce qui reste propre au canal

Le noyau possède l’orchestration. Le canal possède toujours :
  • Transports de plateforme (Gateway, REST, websocket, polling, webhooks)
  • Résolution d’identité et correspondance des noms d’affichage
  • Commandes natives, commandes slash, autocomplétion, modales, boutons, état vocal
  • Rendu de cartes, de modales et de cartes adaptatives
  • Authentification des médias, règles CDN, médias chiffrés, transcription
  • API de modification, réaction, caviardage et présence
  • Récupération rétroactive et récupération de l’historique côté plateforme
  • Flux d’appairage qui exigent une vérification propre à la plateforme
Si deux canaux commencent à avoir besoin du même helper pour l’un de ces éléments, extrayez un helper SDK partagé au lieu de le pousser dans le noyau.

Stabilité

runtime.channel.turn.* fait partie de la surface publique d’exécution des plugins. Les types de faits (SenderFacts, ConversationFacts, RouteFacts, ReplyPlanFacts, AccessFacts, MessageFacts, SupplementalContextFacts, InboundMediaFacts) et les formes d’admission (ChannelTurnAdmission, ChannelEventClass) sont accessibles via PluginRuntime depuis openclaw/plugin-sdk/core. Les règles de rétrocompatibilité s’appliquent : les nouveaux champs de faits sont additifs, les types d’admission ne sont pas renommés, et les noms de points d’entrée restent stables. Les nouveaux besoins de canal qui exigent une modification non additive doivent passer par le processus de migration du SDK de plugin.

Connexe