Ir al contenido

Creación de complementos de canales

Esta guía explica cómo crear un complemento de canal que conecte OpenClaw con una plataforma de mensajería. Al final, tendrá un canal funcional con seguridad de MD, emparejamiento, hilos de respuesta y mensajería saliente.

Los complementos de canal no necesitan sus propias herramientas de envío/edición/reacción. OpenClaw mantiene una herramienta message compartida en el núcleo. Tu complemento es dueño de:

  • Configuración — resolución de cuenta y asistente de configuración
  • Seguridad — política de MD y listas de permitidos
  • Emparejamiento — flujo de aprobación de MD
  • Gramática de sesión — cómo se asignan los ids de conversación específicos del proveedor a chats base, ids de hilo y reservas principales
  • Saliente — envío de texto, medios y encuestas a la plataforma
  • Hilos — cómo se hilvanan las respuestas

El núcleo es dueño de la herramienta de mensaje compartida, el cableado del prompt, la forma de la clave de sesión externa, la contabilidad :thread: genérica y el despacho.

Si tu plataforma almacena un alcance adicional dentro de los ids de conversación, mantén ese análisis en el complemento con messaging.resolveSessionConversation(...). Ese es el enlace canónico para asignar rawId al id de conversación base, id de hilo opcional, baseConversationId explícito y cualquier parentConversationCandidates. Cuando devuelvas parentConversationCandidates, mantenlos ordenados del principal más estrecho al más amplio/conversación base.

Los complementos empaquetados que necesitan el mismo análisis antes de que se inicie el registro del canal también pueden exponer un archivo session-key-api.ts de nivel superior con una exportación resolveSessionConversation(...) coincidente. El núcleo usa esa superficie segura para el arranque solo cuando el registro de complementos en tiempo de ejecución aún no está disponible.

messaging.resolveParentConversationCandidates(...) permanece disponible como una reserva de compatibilidad heredada cuando un complemento solo necesita reservas principales encima del id genérico/sin procesar. Si existen ambos enlaces, el núcleo usa resolveSessionConversation(...).parentConversationCandidates primero y solo recurre a resolveParentConversationCandidates(...) cuando el enlace canónico los omite.

La mayoría de los complementos de canal no necesitan código específico para aprobaciones.

  • El núcleo es propietario de /approve en el mismo chat, cargas útiles de botones de aprobación compartidas y entrega de reserva genérica.
  • Usa auth.authorizeActorAction o auth.getActionAvailabilityState solo cuando la autenticación de aprobación difiera de la autenticación de chat normal.
  • Use outbound.shouldSuppressLocalPayloadPrompt o outbound.beforeDeliverPayload para el comportamiento del ciclo de vida de la carga útil específico del canal, como ocultar avisos de aprobación local duplicados o enviar indicadores de escritura antes de la entrega.
  • Use approvals.delivery solo para el enrutamiento nativo de aprobaciones o la supresión de retorno (fallback).
  • Use approvals.render solo cuando un canal realmente necesite cargas útiles de aprobación personalizadas en lugar del procesador compartido.
  • Si un canal puede inferir identidades de MD estables tipo propietario desde la configuración existente, use createResolvedApproverActionAuthAdapter de openclaw/plugin-sdk/approval-runtime para restringir el /approve del mismo chat sin añadir lógica central específica para aprobaciones.

Para Slack, Matrix, Microsoft Teams y canales de chat similares, la ruta predeterminada suele ser suficiente: el núcleo maneja las aprobaciones y el complemento solo expone capacidades normales de salida y autenticación.

  1. Paquete y manifiesto

    Cree los archivos de complemento estándar. El campo channel en package.json es lo que convierte a esto en un complemento de canal:

    {
    "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": {
    "acme-chat": {
    "type": "object",
    "properties": {
    "token": { "type": "string" },
    "allowFrom": {
    "type": "array",
    "items": { "type": "string" }
    }
    }
    }
    }
    }
    }
  2. Construir el objeto del complemento de canal

    La interfaz ChannelPlugin tiene muchas superficies de adaptador opcionales. Comience con lo mínimo — id y setup — y agregue adaptadores a medida que los necesite.

    Cree src/channel.ts:

    import {
    createChatChannelPlugin,
    createChannelPluginBase,
    } from "openclaw/plugin-sdk/core";
    import type { OpenClawConfig } from "openclaw/plugin-sdk/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

    )?.[“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

    ({ base: createChannelPluginBase({ id: “acme-chat”, setup: { resolveAccount, inspectAccount(cfg, accountId) { const section = (cfg.channels as Record

    )?.[“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);
    },
    },
    },
    });
    ```
    Lo que hace createChatChannelPlugin por usted

    En lugar de implementar interfaces de adaptador de bajo nivel manualmente, usted pasa opciones declarativas y el constructor las compone:

    OpciónLo que conecta
    security.dmSolucionador de seguridad DM con alcance desde campos de configuración
    pairing.textFlujo de emparejamiento DM basado en texto con intercambio de código
    threadingSolucionador de modo de respuesta (fijo, con alcance de cuenta o personalizado)
    outbound.attachedResultsFunciones de envío que devuelven metadatos de resultado (ID de mensajes)

    También puede pasar objetos de adaptador brutos en lugar de las opciones declarativas si necesita control total.

  3. Conectar el punto de entrada

    Cree index.ts:

    import { defineChannelPluginEntry } from "openclaw/plugin-sdk/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(/* ... */);
    },
    });

    Coloque descriptores de CLI propiedad del canal en registerCliMetadata(...) para que OpenClaw pueda mostrarlos en la ayuda raíz sin activar el tiempo de ejecución completo del canal, mientras que las cargas completas normales aún recogen los mismos descriptores para el registro real de comandos. Mantenga registerFull(...) para el trabajo solo de tiempo de ejecución. defineChannelPluginEntry maneja la división del modo de registro automáticamente. Vea Puntos de entrada para todas las opciones.

  4. Agregar una entrada de configuración

    Cree setup-entry.ts para una carga ligera durante la incorporación:

    import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
    import { acmeChatPlugin } from "./src/channel.js";
    export default defineSetupPluginEntry(acmeChatPlugin);

    OpenClaw carga esto en lugar de la entrada completa cuando el canal está deshabilitado o sin configurar. Esto evita introducir código pesado de tiempo de ejecución durante los flujos de configuración. Vea Configuración y Config para obtener detalles.

  5. Manejar mensajes entrantes

    Tu complemento necesita recibir mensajes de la plataforma y reenviarlos a OpenClaw. El patrón típico es un webhook que verifica la solicitud y la despacha a través del controlador de entrada de tu canal:

    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;
    },
    });
    }
  6. Prueba

    Escribe pruebas ubicadas en src/channel.test.ts:

    ```typescript 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);
    });
    });
    ```
    ```bash
    pnpm test --

    /acme-chat/ ```

    Para ver los asistentes de pruebas compartidos, consulta [Testing](/en/plugins/sdk-testing).
<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)
Opciones de hilos

Modos de respuesta fijos, de ámbito de cuenta o personalizados

Integración de herramientas de mensajes

describeMessageTool y descubrimiento de acciones

Resolución de objetivos

inferTargetChatType, looksLikeId, resolveTarget

Asistentes de tiempo de ejecución

TTS, STT, medios, subagente a través de api.runtime