Building plugins
Creare Plugin di canale
Questa guida illustra come creare un Plugin di canale che connette OpenClaw a una piattaforma di messaggistica. Alla fine avrai un canale funzionante con sicurezza per DM, associazione, threading delle risposte e messaggistica in uscita.
Come funzionano i Plugin di canale
I Plugin di canale non hanno bisogno di propri strumenti di invio/modifica/reazione. OpenClaw mantiene uno
strumento message condiviso nel core. Il tuo Plugin gestisce:
- Configurazione - risoluzione dell'account e procedura guidata di configurazione
- Sicurezza - policy DM e allowlist
- Associazione - flusso di approvazione DM
- Grammatica della sessione - come gli id di conversazione specifici del provider si mappano a chat di base, id di thread e fallback padre
- In uscita - invio di testo, contenuti multimediali e sondaggi alla piattaforma
- Threading - come vengono organizzate in thread le risposte
- Digitazione Heartbeat - segnali opzionali di digitazione/occupato per i target di consegna Heartbeat
Il core gestisce lo strumento di messaggistica condiviso, il collegamento al prompt, la forma esterna della chiave di sessione,
la contabilità generica :thread: e il dispatch.
I nuovi Plugin di canale dovrebbero anche esporre un adapter message con
defineChannelMessageAdapter da openclaw/plugin-sdk/channel-outbound. L'
adapter dichiara quali capacità durevoli di invio finale sono effettivamente supportate dal trasporto nativo
e indirizza gli invii di testo/media alle stesse funzioni di trasporto dell'
adapter outbound legacy. Dichiara una capacità solo quando un test di contratto
prova l'effetto collaterale nativo e la ricevuta restituita.
Per il contratto API completo, esempi, matrice delle capacità, regole sulle ricevute, finalizzazione dell'anteprima live,
policy di ack in ricezione, test e tabella di migrazione, vedi
API di outbound dei canali.
Se l'adapter outbound esistente ha già i metodi di invio corretti e
metadati di capacità, usa createChannelMessageAdapterFromOutbound(...) per
derivare l'adapter message invece di scrivere manualmente un altro bridge.
Gli invii dell'adapter dovrebbero restituire valori MessageReceipt. Quando il codice di compatibilità
ha ancora bisogno di id legacy, derivali con listMessageReceiptPlatformIds(...)
o resolveMessageReceiptPrimaryId(...) invece di mantenere campi
messageIds paralleli nel nuovo codice del ciclo di vita.
I canali con supporto alle anteprime dovrebbero anche dichiarare message.live.capabilities con
l'esatto ciclo di vita live che possiedono, come draftPreview,
previewFinalization, progressUpdates, nativeStreaming o
quietFinalization. I canali che finalizzano un'anteprima bozza in loco dovrebbero
dichiarare anche message.live.finalizer.capabilities, come finalEdit,
normalFallback, discardPending, previewReceipt e
retainOnAmbiguousFailure, e instradare la logica runtime tramite
defineFinalizableLivePreviewAdapter(...) più
deliverWithFinalizableLivePreviewAdapter(...). Mantieni queste capacità supportate
da test verifyChannelMessageLiveCapabilityAdapterProofs(...) e
verifyChannelMessageLiveFinalizerProofs(...) in modo che anteprima nativa,
avanzamento, modifica, fallback/conservazione, cleanup e comportamento delle ricevute non possano divergere
silenziosamente.
I receiver inbound che rinviano gli acknowledge della piattaforma dovrebbero dichiarare
message.receive.defaultAckPolicy e supportedAckPolicies invece di nascondere
il timing degli ack nello stato locale del monitor. Copri ogni policy dichiarata con
verifyChannelMessageReceiveAckPolicyAdapterProofs(...).
Gli helper legacy per le risposte come createChannelTurnReplyPipeline,
dispatchInboundReplyWithBase e recordInboundSessionAndDispatchReply
rimangono disponibili per i dispatcher di compatibilità. Non usare questi nomi per il nuovo
codice di canale; i nuovi Plugin dovrebbero iniziare con l'adapter message, le ricevute e
gli helper del ciclo di vita di ricezione/invio in openclaw/plugin-sdk/channel-outbound.
I canali che migrano l'autorizzazione inbound possono usare il sottopercorso sperimentale
openclaw/plugin-sdk/channel-ingress-runtime dai percorsi di ricezione runtime.
Il sottopercorso mantiene nel Plugin la ricerca della piattaforma e gli effetti collaterali, condividendo
al contempo la risoluzione dello stato delle allowlist, le decisioni di route/sender/command/event/activation,
la diagnostica redatta e la mappatura di ammissione dei turni. Mantieni la
normalizzazione dell'identità del Plugin nel descriptor che passi al resolver; non
serializzare i valori grezzi di match dallo stato o dalla decisione risolti. Vedi
API di ingresso dei canali per il design dell'API,
il confine di ownership e le aspettative sui test.
Se il tuo canale supporta indicatori di digitazione al di fuori delle risposte inbound, esponi
heartbeat.sendTyping(...) sul Plugin di canale. Il core lo chiama con il
target di consegna Heartbeat risolto prima dell'avvio dell'esecuzione del modello Heartbeat e
usa il ciclo di vita condiviso di keepalive/cleanup della digitazione. Aggiungi heartbeat.clearTyping(...)
quando la piattaforma richiede un segnale esplicito di stop.
Se il tuo canale aggiunge parametri dello strumento di messaggistica che trasportano sorgenti multimediali, esponi questi
nomi di parametri tramite describeMessageTool(...).mediaSourceParams. Il core usa
questo elenco esplicito per la normalizzazione dei percorsi sandbox e la policy di accesso ai media in uscita,
quindi i Plugin non hanno bisogno di casi speciali nel core condiviso per parametri specifici del provider
relativi ad avatar, allegati o immagini di copertina.
Preferisci restituire una mappa indicizzata per azione come
{ "set-profile": ["avatarUrl", "avatarPath"] } così le azioni non correlate non
ereditano gli argomenti media di un'altra azione. Un array piatto funziona comunque per i parametri che
sono intenzionalmente condivisi tra tutte le azioni esposte.
I canali che devono esporre un URL pubblico temporaneo per un recupero media lato piattaforma
possono usare createHostedOutboundMediaStore(...) da
openclaw/plugin-sdk/outbound-media con gli store di stato del Plugin. Mantieni
nel Plugin di canale il parsing delle route di piattaforma e l'applicazione dei token; l'helper condiviso
gestisce solo caricamento dei media, metadati di scadenza, righe dei chunk e cleanup.
Se il tuo canale ha bisogno di shaping specifico del provider per message(action="send"),
preferisci actions.prepareSendPayload(...). Inserisci card native, blocchi, embed o
altri dati durevoli sotto payload.channelData.<channel> e lascia che il core esegua
l'invio effettivo tramite l'adapter outbound/message. Usa
actions.handleAction(...) per l'invio solo come fallback di compatibilità per
payload che non possono essere serializzati e ritentati.
Se la tua piattaforma archivia scope aggiuntivo dentro gli id di conversazione, mantieni quel parsing
nel Plugin con messaging.resolveSessionConversation(...). Questo è l'hook
canonico per mappare rawId all'id della conversazione di base, all'id di thread opzionale,
a baseConversationId esplicito e a qualsiasi parentConversationCandidates.
Quando restituisci parentConversationCandidates, mantienili ordinati dal
padre più ristretto alla conversazione più ampia/di base.
Usa openclaw/plugin-sdk/channel-route quando il codice del Plugin deve normalizzare
campi simili a route, confrontare un thread figlio con la sua route padre o costruire una
chiave di deduplicazione stabile da { channel, to, accountId, threadId }. L'helper
normalizza gli id di thread numerici nello stesso modo del core, quindi i Plugin dovrebbero preferirlo
a confronti ad hoc String(threadId).
I Plugin con grammatica target specifica del provider dovrebbero esporre
messaging.resolveOutboundSessionRoute(...) così il core ottiene identità di sessione
e thread native del provider senza usare shim di parsing.
I Plugin in bundle che hanno bisogno dello stesso parsing prima dell'avvio del registro dei canali
possono anche esporre un file di primo livello session-key-api.ts con un export
resolveSessionConversation(...) corrispondente. Il core usa questa superficie sicura per il bootstrap
solo quando il registro dei Plugin runtime non è ancora disponibile.
messaging.resolveParentConversationCandidates(...) rimane disponibile come
fallback di compatibilità legacy quando un Plugin ha bisogno solo di fallback padre sopra
l'id generico/grezzo. Se entrambi gli hook esistono, il core usa prima
resolveSessionConversation(...).parentConversationCandidates e ricorre a
resolveParentConversationCandidates(...) solo quando l'hook canonico
li omette.
Approvazioni e capacità dei canali
La maggior parte dei Plugin di canale non ha bisogno di codice specifico per le approvazioni.
- Core possiede
/approvenella stessa chat, i payload dei pulsanti di approvazione condivisi e il recapito di fallback generico. - Preferisci un unico oggetto
approvalCapabilitysul plugin del canale quando il canale richiede un comportamento specifico per le approvazioni. ChannelPlugin.approvalsviene rimosso. Inserisci i fatti di recapito/native/render/auth delle approvazioni inapprovalCapability.plugin.authè solo login/logout; core non legge più gli hook di auth delle approvazioni da quell'oggetto.approvalCapability.authorizeActorActioneapprovalCapability.getActionAvailabilityStatesono il seam canonico per l'auth delle approvazioni.- Usa
approvalCapability.getActionAvailabilityStateper la disponibilità dell'auth delle approvazioni nella stessa chat. Mantieni disponibili gli approvatori configurati per/approveanche quando il recapito nativo è disabilitato; usa invece lo stato della superficie di avvio nativa per la guida a recapito/configurazione. - Se il tuo canale espone approvazioni exec native, usa
approvalCapability.getExecInitiatingSurfaceStateper lo stato della superficie di avvio/client nativo quando differisce dall'auth delle approvazioni nella stessa chat. Core usa quell'hook specifico per exec per distinguereenableddadisabled, decidere se il canale di avvio supporta approvazioni exec native e includere il canale nella guida di fallback del client nativo.createApproverRestrictedNativeApprovalCapability(...)lo compila per il caso comune. - Usa
outbound.shouldSuppressLocalPayloadPromptooutbound.beforeDeliverPayloadper comportamenti del ciclo di vita dei payload specifici del canale, come nascondere prompt di approvazione locali duplicati o inviare indicatori di digitazione prima del recapito. - Usa
approvalCapability.deliverysolo per l'instradamento delle approvazioni native o la soppressione del fallback. - Usa
approvalCapability.nativeRuntimeper i fatti delle approvazioni native posseduti dal canale. Mantienilo lazy sugli entrypoint caldi del canale concreateLazyChannelApprovalNativeRuntimeAdapter(...), che può importare il modulo runtime su richiesta pur consentendo a core di assemblare il ciclo di vita delle approvazioni. - Usa
approvalCapability.rendersolo quando un canale ha davvero bisogno di payload di approvazione personalizzati invece del renderer condiviso. - Usa
approvalCapability.describeExecApprovalSetupquando il canale vuole che la risposta del percorso disabilitato spieghi le esatte opzioni di configurazione necessarie per abilitare le approvazioni exec native. L'hook riceve{ channel, channelLabel, accountId }; i canali con account nominati dovrebbero renderizzare percorsi con ambito sull'account comechannels.<channel>.accounts.<id>.execApprovals.*invece di default di primo livello. - Usa
approvalCapability.describePluginApprovalSetupquando la guida per gli errori di approvazione Plugin è sicura da mostrare per gli errori di assenza di route e timeout delle approvazioni Plugin.createApproverRestrictedNativeApprovalCapability(...)non lo deduce dadescribeExecApprovalSetup; passa esplicitamente lo stesso helper solo quando le approvazioni Plugin ed exec usano davvero la stessa configurazione nativa. - Se un canale può dedurre identità DM stabili simili al proprietario dalla configurazione esistente, usa
createResolvedApproverActionAuthAdapterdaopenclaw/plugin-sdk/approval-runtimeper limitare/approvenella stessa chat senza aggiungere logica core specifica per le approvazioni. - Se l'auth personalizzata delle approvazioni consente intenzionalmente solo il fallback nella stessa chat, restituisci
markImplicitSameChatApprovalAuthorization({ authorized: true })daopenclaw/plugin-sdk/approval-auth-runtime; altrimenti core tratta il risultato come autorizzazione esplicita dell'approvatore. - Se una callback nativa posseduta dal canale risolve direttamente le approvazioni, usa
isImplicitSameChatApprovalAuthorization(...)prima di risolvere, così il fallback implicito passa comunque attraverso la normale autorizzazione attore del canale. - Se un canale richiede il recapito di approvazioni native, mantieni il codice del canale concentrato sulla normalizzazione del target più i fatti di trasporto/presentazione. Usa
createChannelExecApprovalProfile,createChannelNativeOriginTargetResolver,createChannelApproverDmTargetResolverecreateApproverRestrictedNativeApprovalCapabilitydaopenclaw/plugin-sdk/approval-runtime. Metti i fatti specifici del canale dietroapprovalCapability.nativeRuntime, idealmente tramitecreateChannelApprovalNativeRuntimeAdapter(...)ocreateLazyChannelApprovalNativeRuntimeAdapter(...), così core può assemblare l'handler e possedere filtro delle richieste, instradamento, deduplicazione, scadenza, sottoscrizione Gateway e avvisi di recapito altrove.nativeRuntimeè suddiviso in alcuni seam più piccoli: - Usa
createNativeApprovalChannelRouteGatesdaopenclaw/plugin-sdk/approval-native-runtimequando un canale supporta sia il recapito nativo dall'origine di sessione sia target espliciti di inoltro delle approvazioni. L'helper centralizza selezione della configurazione delle approvazioni, gestione dimode, filtri agente/sessione, binding dell'account, corrispondenza del target di sessione e corrispondenza dell'elenco target, mentre i chiamanti possiedono ancora id del canale, modalità di inoltro predefinita, lookup dell'account, controllo di trasporto abilitato, normalizzazione del target e risoluzione del target della sorgente del turno. Non usarlo per creare default di policy del canale posseduti da core; passa esplicitamente la modalità predefinita documentata del canale. createChannelNativeOriginTargetResolverusa per impostazione predefinita il matcher condiviso delle route di canale per target{ to, accountId, threadId }. PassatargetsMatchsolo quando un canale ha regole di equivalenza specifiche del provider, come la corrispondenza del prefisso timestamp di Slack.- Passa
normalizeTargetForMatchacreateChannelNativeOriginTargetResolverquando il canale deve canonizzare gli id del provider prima che venga eseguito il matcher di route predefinito o una callbacktargetsMatchpersonalizzata, preservando però il target originale per il recapito. UsanormalizeTargetsolo quando il target di recapito risolto deve essere esso stesso canonizzato. availability- se l'account è configurato e se una richiesta deve essere gestitapresentation- mappa il view model di approvazione condiviso in payload nativi pending/resolved/expired o azioni finalitransport- prepara i target e invia/aggiorna/elimina messaggi di approvazione nativiinteractions- hook opzionali bind/unbind/clear-action per pulsanti o reazioni native, più un hook opzionalecancelDelivered. ImplementacancelDeliveredquandodeliverPendingregistra stato in-process o persistente (come uno store di target di reazione), così quello stato può essere rilasciato se l'arresto di un handler annulla il recapito prima chebindPendingvenga eseguito o quandobindPendingnon restituisce alcun handleobserve- hook opzionali di diagnostica del recapito- Se il canale richiede oggetti posseduti dal runtime come un client, token, app Bolt o ricevitore webhook, registrali tramite
openclaw/plugin-sdk/channel-runtime-context. Il registro generico del contesto runtime consente a core di avviare handler guidati dalle capability dallo stato di avvio del canale senza aggiungere colla wrapper specifica per le approvazioni. - Usa il livello più basso
createChannelApprovalHandlerocreateChannelNativeApprovalRuntimesolo quando il seam guidato dalle capability non è ancora sufficientemente espressivo. - I canali di approvazione nativa devono instradare sia
accountIdsiaapprovalKindtramite quegli helper.accountIdmantiene la policy di approvazione multi-account nell'ambito dell'account bot corretto, eapprovalKindmantiene disponibile al canale il comportamento delle approvazioni exec rispetto a quelle Plugin senza branch hardcoded in core. - Core ora possiede anche gli avvisi di reinstradamento delle approvazioni. I plugin di canale non dovrebbero inviare i propri messaggi di follow-up "approvazione inviata ai DM / a un altro canale" da
createChannelNativeApprovalRuntime; invece, esponi un instradamento accurato origine + DM dell'approvatore tramite gli helper condivisi di capability di approvazione e lascia che core aggreghi i recapiti effettivi prima di pubblicare qualsiasi avviso nella chat di avvio. - Preserva end-to-end il tipo di id dell'approvazione recapitata. I client nativi non dovrebbero indovinare o riscrivere l'instradamento delle approvazioni exec rispetto a quelle Plugin dallo stato locale del canale.
- Tipi di approvazione diversi possono esporre intenzionalmente superfici native diverse.
Esempi bundled correnti:
- Slack mantiene disponibile l'instradamento nativo delle approvazioni sia per id exec sia Plugin.
- Matrix mantiene lo stesso instradamento nativo DM/canale e la stessa UX di reazione per approvazioni exec e Plugin, consentendo comunque all'auth di differire per tipo di approvazione.
createApproverRestrictedNativeApprovalAdapteresiste ancora come wrapper di compatibilità, ma il nuovo codice dovrebbe preferire il builder di capability ed esporreapprovalCapabilitysul plugin.
Per gli entrypoint caldi del canale, preferisci i sottopercorsi runtime più ristretti quando hai bisogno solo di una parte di quella famiglia:
openclaw/plugin-sdk/approval-auth-runtimeopenclaw/plugin-sdk/approval-client-runtimeopenclaw/plugin-sdk/approval-delivery-runtimeopenclaw/plugin-sdk/approval-gateway-runtimeopenclaw/plugin-sdk/approval-handler-adapter-runtimeopenclaw/plugin-sdk/approval-handler-runtimeopenclaw/plugin-sdk/approval-native-runtimeopenclaw/plugin-sdk/approval-reply-runtimeopenclaw/plugin-sdk/channel-runtime-context
Allo stesso modo, preferisci openclaw/plugin-sdk/setup-runtime,
openclaw/plugin-sdk/setup-runtime,
openclaw/plugin-sdk/reply-runtime,
openclaw/plugin-sdk/reply-dispatch-runtime,
openclaw/plugin-sdk/reply-reference e
openclaw/plugin-sdk/reply-chunking quando non ti serve la superficie ombrello
più ampia.
Per setup in particolare:
openclaw/plugin-sdk/setup-runtimecopre gli helper di setup sicuri per il runtime:createSetupTranslator, adattatori patch di setup import-safe (createPatchedAccountSetupAdapter,createEnvPatchedAccountSetupAdapter,createSetupInputPresenceValidator), output delle note di lookup,promptResolvedAllowFrom,splitSetupEntriese i builder delegati setup-proxyopenclaw/plugin-sdk/setup-runtimeinclude il seam dell'adapter env-aware percreateEnvPatchedAccountSetupAdapteropenclaw/plugin-sdk/channel-setupcopre i builder di setup optional-install più alcune primitive sicure per il setup:createOptionalChannelSetupSurface,createOptionalChannelSetupAdapter,
Se il tuo canale supporta setup o auth guidati da env e i flussi generici di avvio/configurazione
devono conoscere quei nomi env prima che il runtime venga caricato, dichiarali nel
manifest del plugin con channelEnvVars. Mantieni gli envVars del runtime del canale o le costanti locali
solo per il testo rivolto agli operatori.
Se il tuo canale può apparire in status, channels list, channels status o
nelle scansioni SecretRef prima che il runtime del plugin venga avviato, aggiungi openclaw.setupEntry in
package.json. Quell'entrypoint dovrebbe essere sicuro da importare nei percorsi dei comandi
sola lettura e dovrebbe restituire i metadati del canale, l'adapter di configurazione sicuro per il setup, l'adapter di stato
e i metadati dei target secret del canale necessari per quei riepiloghi. Non
avviare client, listener o runtime di trasporto dall'entry di setup.
Mantieni stretto anche il percorso di import dell'entry principale del canale. Discovery può valutare
l'entry e il modulo del plugin di canale per registrare capability senza attivare
il canale. File come channel-plugin-api.ts dovrebbero esportare l'oggetto plugin del canale
senza importare wizard di setup, client di trasporto, listener socket,
launcher di subprocess o moduli di avvio del servizio. Metti quei pezzi runtime
in moduli caricati da registerFull(...), setter runtime o adapter di capability
lazy.
createOptionalChannelSetupWizard, DEFAULT_ACCOUNT_ID,
createTopLevelChannelDmPolicy, setSetupChannelEnabled e
splitSetupEntries
- usa il seam più ampio
openclaw/plugin-sdk/setupsolo quando hai bisogno anche degli helper condivisi di setup/config più pesanti comemoveSingleAccountChannelSectionToDefaultAccount(...)
Se il tuo canale vuole solo pubblicizzare "installa prima questo plugin" nelle superfici
di setup, preferisci createOptionalChannelSetupSurface(...). L'adapter/wizard generato
fallisce chiuso sulle scritture di configurazione e sulla finalizzazione, e riusa
lo stesso messaggio install-required tra validazione, finalizzazione e testo del link
alla documentazione.
Per altri percorsi caldi del canale, preferisci gli helper stretti rispetto alle superfici legacy più ampie:
openclaw/plugin-sdk/account-core,openclaw/plugin-sdk/account-id,openclaw/plugin-sdk/account-resolutioneopenclaw/plugin-sdk/account-helpersper la configurazione multi-account e il fallback dell'account predefinitoopenclaw/plugin-sdk/inbound-envelopeeopenclaw/plugin-sdk/channel-inboundper il route/envelope in ingresso e il cablaggio record-and-dispatchopenclaw/plugin-sdk/channel-targetsper gli helper di parsing dei targetopenclaw/plugin-sdk/outbound-mediaper il caricamento dei media eopenclaw/plugin-sdk/channel-outboundper i delegati di identità/invio in uscita e la pianificazione dei payloadbuildThreadAwareOutboundSessionRoute(...)daopenclaw/plugin-sdk/channel-corequando una route in uscita deve preservare unreplyToId/threadIdesplicito o recuperare la sessione:thread:corrente dopo che la chiave di sessione di base corrisponde ancora. I Plugin provider possono sovrascrivere la precedenza, il comportamento dei suffissi e la normalizzazione dell'ID thread quando la loro piattaforma ha semantiche native di consegna dei thread.openclaw/plugin-sdk/thread-bindings-runtimeper il ciclo di vita del binding dei thread e la registrazione degli adapteropenclaw/plugin-sdk/agent-media-payloadsolo quando è ancora richiesto un layout legacy dei campi payload agente/mediaopenclaw/plugin-sdk/telegram-command-configper la normalizzazione dei comandi personalizzati Telegram, la validazione di duplicati/conflitti e un contratto di configurazione comandi stabile rispetto ai fallback
I canali solo auth di solito possono fermarsi al percorso predefinito: il core gestisce le approvazioni e il Plugin espone solo le capability in uscita/auth. I canali di approvazione nativi come Matrix, Slack, Telegram e trasporti chat personalizzati dovrebbero usare gli helper nativi condivisi invece di implementare un proprio ciclo di vita delle approvazioni.
Policy sulle menzioni in ingresso
Mantieni la gestione delle menzioni in ingresso divisa in due livelli:
- raccolta delle evidenze di proprietà del Plugin
- valutazione della policy condivisa
Usa openclaw/plugin-sdk/channel-mention-gating per le decisioni sulla policy delle menzioni.
Usa openclaw/plugin-sdk/channel-inbound solo quando hai bisogno del barrel helper in ingresso
più ampio.
Adatto alla logica locale del Plugin:
- rilevamento delle risposte al bot
- rilevamento del bot citato
- controlli di partecipazione al thread
- esclusioni dei messaggi di servizio/sistema
- cache native della piattaforma necessarie per dimostrare la partecipazione del bot
Adatto all'helper condiviso:
requireMention- risultato di menzione esplicita
- allowlist delle menzioni implicite
- bypass dei comandi
- decisione finale di skip
Flusso consigliato:
- Calcola i fatti locali sulle menzioni.
- Passa quei fatti a
resolveInboundMentionDecision({ facts, policy }). - Usa
decision.effectiveWasMentioned,decision.shouldBypassMentionedecision.shouldSkipnel tuo gate in ingresso.
implicitMentionKindWhen, matchesMentionWithExplicit, resolveInboundMentionDecision,} from "openclaw/plugin-sdk/channel-inbound"; const mentionMatch = matchesMentionWithExplicit(text, { mentionRegexes, mentionPatterns,}); const facts = { canDetectMention: true, wasMentioned: mentionMatch.matched, hasAnyMention: mentionMatch.hasExplicitMention, implicitMentionKinds: [ ...implicitMentionKindWhen("reply_to_bot", isReplyToBot), ...implicitMentionKindWhen("quoted_bot", isQuoteOfBot), ],}; const decision = resolveInboundMentionDecision({ facts, policy: { isGroup, requireMention, allowedImplicitMentionKinds: requireExplicitMention ? [] : ["reply_to_bot", "quoted_bot"], allowTextCommands, hasControlCommand, commandAuthorized, },}); if (decision.shouldSkip) return;api.runtime.channel.mentions espone gli stessi helper condivisi per le menzioni per i
Plugin canale inclusi che dipendono già dall'iniezione runtime:
buildMentionRegexesmatchesMentionPatternsmatchesMentionWithExplicitimplicitMentionKindWhenresolveInboundMentionDecision
Se ti servono solo implicitMentionKindWhen e
resolveInboundMentionDecision, importa da
openclaw/plugin-sdk/channel-mention-gating per evitare di caricare helper runtime
in ingresso non correlati.
Usa resolveInboundMentionDecision({ facts, policy }) per il gating delle menzioni.
Procedura guidata
Package e manifest
Crea i file standard del Plugin. Il campo channel in package.json è
ciò che lo rende un Plugin canale. Per l'intera superficie dei metadati del package,
consulta Configurazione e setup dei Plugin:
{"name": "@myorg/openclaw-acme-chat","version": "1.0.0","type": "module","openclaw": { "extensions": ["./index.ts"], "setupEntry": "./setup-entry.ts", "channel": { "id": "acme-chat", "label": "Acme Chat", "blurb": "Connect OpenClaw to Acme Chat." }}}{"id": "acme-chat","kind": "channel","channels": ["acme-chat"],"name": "Acme Chat","description": "Acme Chat channel plugin","configSchema": { "type": "object", "additionalProperties": false, "properties": {}},"channelConfigs": { "acme-chat": { "schema": { "type": "object", "additionalProperties": false, "properties": { "token": { "type": "string" }, "allowFrom": { "type": "array", "items": { "type": "string" } } } }, "uiHints": { "token": { "label": "Bot token", "sensitive": true } } }}}configSchema valida plugins.entries.acme-chat.config. Usalo per
impostazioni di proprietà del Plugin che non sono la configurazione dell'account del canale. channelConfigs
valida channels.acme-chat ed è la sorgente del cold path usata dallo schema di configurazione,
dal setup e dalle superfici UI prima che il runtime del Plugin venga caricato.
Costruisci l'oggetto Plugin canale
L'interfaccia ChannelPlugin ha molte superfici adapter opzionali. Inizia con
il minimo - id e setup - e aggiungi adapter quando ti servono.
Crea src/channel.ts:
import { createChatChannelPlugin, createChannelPluginBase,} from "openclaw/plugin-sdk/channel-core";import type { OpenClawConfig } from "openclaw/plugin-sdk/channel-core";import { acmeChatApi } from "./client.js"; // your platform API client type ResolvedAccount = { accountId: string | null; token: string; allowFrom: string[]; dmPolicy: string | undefined;}; function resolveAccount( cfg: OpenClawConfig, accountId?: string | null,): ResolvedAccount { const section = (cfg.channels as Record<string, any>)?.["acme-chat"]; const token = section?.token; if (!token) throw new Error("acme-chat: token is required"); return { accountId: accountId ?? null, token, allowFrom: section?.allowFrom ?? [], dmPolicy: section?.dmSecurity, };} export const acmeChatPlugin = createChatChannelPlugin<ResolvedAccount>({ base: createChannelPluginBase({ id: "acme-chat", setup: { resolveAccount, inspectAccount(cfg, accountId) { const section = (cfg.channels as Record<string, any>)?.["acme-chat"]; return { enabled: Boolean(section?.token), configured: Boolean(section?.token), tokenStatus: section?.token ? "available" : "missing", }; }, }, }), // DM security: who can message the bot security: { dm: { channelKey: "acme-chat", resolvePolicy: (account) => account.dmPolicy, resolveAllowFrom: (account) => account.allowFrom, defaultPolicy: "allowlist", }, }, // Pairing: approval flow for new DM contacts pairing: { text: { idLabel: "Acme Chat username", message: "Send this code to verify your identity:", notify: async ({ target, code }) => { await acmeChatApi.sendDm(target, `Pairing code: ${code}`); }, }, }, // Threading: how replies are delivered threading: { topLevelReplyToMode: "reply" }, // Outbound: send messages to the platform outbound: { attachedResults: { sendText: async (params) => { const result = await acmeChatApi.sendMessage( params.to, params.text, ); return { messageId: result.id }; }, }, base: { sendMedia: async (params) => { await acmeChatApi.sendFile(params.to, params.filePath); }, }, },});Per i canali che accettano sia chiavi DM canoniche di primo livello sia chiavi annidate legacy, usa gli helper da plugin-sdk/channel-config-helpers: resolveChannelDmAccess, resolveChannelDmPolicy, resolveChannelDmAllowFrom e normalizeChannelDmPolicy mantengono i valori locali dell'account prima dei valori root ereditati. Abbina lo stesso resolver alla riparazione doctor tramite normalizeLegacyDmAliases così runtime e migrazione leggono lo stesso contratto.
Cosa fa createChatChannelPlugin per te
Invece di implementare manualmente interfacce adapter di basso livello, passi opzioni dichiarative e il builder le compone:
| Opzione | Cosa collega |
|---|---|
security.dm |
Resolver di sicurezza DM con scope dai campi di configurazione |
pairing.text |
Flusso di pairing DM basato su testo con scambio di codice |
threading |
Resolver della modalità reply-to (fisso, con scope account o personalizzato) |
outbound.attachedResults |
Funzioni di invio che restituiscono metadati del risultato (ID messaggio) |
Puoi anche passare oggetti adapter raw invece delle opzioni dichiarative se hai bisogno di pieno controllo.
Gli adapter in uscita raw possono definire una funzione chunker(text, limit, ctx).
Il valore opzionale ctx.formatting porta decisioni di formattazione al momento della consegna
come maxLinesPerMessage; applicalo prima dell'invio così il threading delle risposte
e i confini dei chunk vengono risolti una sola volta dalla consegna in uscita condivisa.
I contesti di invio includono anche replyToIdSource (implicit o explicit)
quando è stato risolto un target di risposta nativo, così gli helper payload possono preservare
tag di risposta espliciti senza consumare uno slot di risposta implicito monouso.
Collega l'entry point
Crea index.ts:
import { defineChannelPluginEntry } from "openclaw/plugin-sdk/channel-core";import { acmeChatPlugin } from "./src/channel.js"; export default defineChannelPluginEntry({ id: "acme-chat", name: "Acme Chat", description: "Acme Chat channel plugin", plugin: acmeChatPlugin, registerCliMetadata(api) { api.registerCli( ({ program }) => { program .command("acme-chat") .description("Acme Chat management"); }, { descriptors: [ { name: "acme-chat", description: "Acme Chat management", hasSubcommands: false, }, ], }, ); }, registerFull(api) { api.registerGatewayMethod(/* ... */); },});Inserisci i descrittori CLI di proprietà del canale in registerCliMetadata(...) così OpenClaw
può mostrarli nell'aiuto root senza attivare il runtime completo del canale,
mentre i normali caricamenti completi raccolgono comunque gli stessi descrittori per la registrazione
effettiva dei comandi. Mantieni registerFull(...) per il lavoro solo runtime.
Se registerFull(...) registra metodi RPC del Gateway, usa un
prefisso specifico del plugin. I namespace amministrativi core (config.*,
exec.approvals.*, wizard.*, update.*) restano riservati e si risolvono sempre
in operator.admin.
defineChannelPluginEntry gestisce automaticamente la suddivisione della modalità di registrazione. Consulta
Punti di ingresso per tutte
le opzioni.
Aggiungi una voce di configurazione
Crea setup-entry.ts per il caricamento leggero durante l'onboarding:
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";import { acmeChatPlugin } from "./src/channel.js"; export default defineSetupPluginEntry(acmeChatPlugin);OpenClaw carica questo invece della voce completa quando il canale è disabilitato o non configurato. Evita di importare codice runtime pesante durante i flussi di configurazione. Consulta Configurazione e config per i dettagli.
I canali workspace in bundle che separano le esportazioni sicure per la configurazione in moduli
sidecar possono usare defineBundledChannelSetupEntry(...) da
openclaw/plugin-sdk/channel-entry-contract quando hanno bisogno anche di un
setter runtime esplicito al momento della configurazione.
Gestisci i messaggi in ingresso
Il tuo plugin deve ricevere messaggi dalla piattaforma e inoltrarli a OpenClaw. Il pattern tipico è un Webhook che verifica la richiesta e la invia tramite il gestore in ingresso del tuo canale:
registerFull(api) { api.registerHttpRoute({ path: "/acme-chat/webhook", auth: "plugin", // plugin-managed auth (verify signatures yourself) handler: async (req, res) => { const event = parseWebhookPayload(req); // Your inbound handler dispatches the message to OpenClaw. // The exact wiring depends on your platform SDK - // see a real example in the bundled Microsoft Teams or Google Chat plugin package. await handleAcmeChatInbound(api, event); res.statusCode = 200; res.end("ok"); return true; }, });}Test
Scrivi test colocati in src/channel.test.ts:
import { describe, it, expect } from "vitest";import { acmeChatPlugin } from "./channel.js"; describe("acme-chat plugin", () => { it("resolves account from config", () => { const cfg = { channels: { "acme-chat": { token: "test-token", allowFrom: ["user1"] }, }, } as any; const account = acmeChatPlugin.setup!.resolveAccount(cfg, undefined); expect(account.token).toBe("test-token"); }); it("inspects account without materializing secrets", () => { const cfg = { channels: { "acme-chat": { token: "test-token" } }, } as any; const result = acmeChatPlugin.setup!.inspectAccount!(cfg, undefined); expect(result.configured).toBe(true); expect(result.tokenStatus).toBe("available"); }); it("reports missing config", () => { const cfg = { channels: {} } as any; const result = acmeChatPlugin.setup!.inspectAccount!(cfg, undefined); expect(result.configured).toBe(false); });});pnpm test -- <bundled-plugin-root>/acme-chat/Per gli helper di test condivisi, consulta Test.
Struttura dei file
<bundled-plugin-root>/acme-chat/├── package.json # openclaw.channel metadata├── openclaw.plugin.json # Manifest with config schema├── index.ts # defineChannelPluginEntry├── setup-entry.ts # defineSetupPluginEntry├── api.ts # Public exports (optional)├── runtime-api.ts # Internal runtime exports (optional)└── src/ ├── channel.ts # ChannelPlugin via createChatChannelPlugin ├── channel.test.ts # Tests ├── client.ts # Platform API client └── runtime.ts # Runtime store (if needed)Argomenti avanzati
Modalità di risposta fisse, con ambito account o personalizzate
describeMessageTool e individuazione delle azioni
inferTargetChatType, looksLikeId, reservedLiterals, resolveTarget
TTS, STT, media, subagent tramite api.runtime
Ciclo di vita condiviso degli eventi in ingresso: acquisizione, risoluzione, registrazione, dispatch, finalizzazione
Passaggi successivi
- Plugin Provider - se il tuo plugin fornisce anche modelli
- Panoramica SDK - riferimento completo alle importazioni dei sottopercorsi
- Test SDK - utilità di test e test di contratto
- Manifest Plugin - schema completo del manifest