Skip to content

构建渠道插件(Channel Plugins)

本指南将带你完整地构建一个渠道插件,将 OpenClaw 接入某个消息平台。读完后,你将拥有一个具备 DM 安全策略、配对流程、回复线程和出站消息的可用渠道——相当于给你的"小龙虾"安装了一个新的聊天入口。

信息: 如果你还没有构建过任何 OpenClaw 插件,请先阅读 快速入门,了解基础包结构和 manifest 设置。

渠道插件的工作原理

渠道插件无需自己实现 send/edit/react 工具——OpenClaw 的 core 层维护了一个共享的 message 工具。你的插件负责:

  • 配置 — 账户解析和安装向导
  • 安全 — DM 策略和白名单
  • 配对 — DM 审批流程
  • 出站 — 向平台发送文本、媒体和投票
  • 线程 — 回复如何串联

Core 层负责共享消息工具、Prompt 接线、会话记账和调度。

开发步骤

第一步:创建包和 manifest

创建标准插件文件。package.json 中的 channel 字段是渠道插件的标志:

json
// package.json
{
  "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."
    }
  }
}
json
// openclaw.plugin.json
{
  "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" }
          }
        }
      }
    }
  }
}

第二步:构建 channel plugin 对象

ChannelPlugin 接口有很多可选的适配器接口。从最小配置开始——idsetup——根据需要逐步添加适配器。

创建 src/channel.ts

typescript
// src/channel.ts
import {
  createChatChannelPlugin,
  createChannelPluginBase,
} from "openclaw/plugin-sdk/core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/core";
import { acmeChatApi } from "./client.js"; // 你的平台 API 客户端

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 安全:谁可以给 bot 发消息
  security: {
    dm: {
      channelKey: "acme-chat",
      resolvePolicy: (account) => account.dmPolicy,
      resolveAllowFrom: (account) => account.allowFrom,
      defaultPolicy: "allowlist",
    },
  },

  // 配对:新 DM 联系人的审批流程
  pairing: {
    text: {
      idLabel: "Acme Chat 用户名",
      message: "发送此代码以验证你的身份:",
      notify: async ({ target, code }) => {
        await acmeChatApi.sendDm(target, `配对码:${code}`);
      },
    },
  },

  // 线程:回复的投递方式
  threading: { topLevelReplyToMode: "reply" },

  // 出站:向平台发送消息
  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);
      },
    },
  },
});

createChatChannelPlugin 为你做了什么:

你只需传入声明式选项,builder 会自动组合适配器,而不用手动实现底层接口:

选项接线内容
security.dm从配置字段派生的 DM 安全解析器
pairing.text基于文本的 DM 配对流程(含验证码交换)
threading回复模式解析器(固定、账户级别或自定义)
outbound.attachedResults返回结果元数据(消息 ID)的发送函数

如需完全控制,也可以直接传入原始适配器对象。

第三步:接线入口点

创建 index.ts

typescript
// 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,
  registerFull(api) {
    api.registerCli(
      ({ program }) => {
        program
          .command("acme-chat")
          .description("Acme Chat management");
      },
      { commands: ["acme-chat"] },
    );
  },
});

defineChannelPluginEntry 自动处理 setup/full 注册拆分。所有选项见入口点文档

第四步:添加 setup entry

创建 setup-entry.ts,用于引导流程中的轻量加载:

typescript
// setup-entry.ts
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/core";
import { acmeChatPlugin } from "./src/channel.js";

export default defineSetupPluginEntry(acmeChatPlugin);

当渠道被禁用或未配置时,OpenClaw 会加载这个文件而不是完整入口,避免在安装流程中引入重型运行时代码。详见 Setup and Config

第五步:处理入站消息

你的插件需要从平台接收消息并转发给 OpenClaw。典型做法是设置一个 Webhook,验证请求后通过渠道的入站处理器进行分发:

typescript
registerFull(api) {
  api.registerHttpRoute({
    path: "/acme-chat/webhook",
    auth: "plugin", // 插件自管理认证(自行验证签名)
    handler: async (req, res) => {
      const event = parseWebhookPayload(req);

      // 入站处理器将消息分发给 OpenClaw
      // 具体接线方式取决于你的平台 SDK——
      // 可参考 extensions/msteams 或 extensions/googlechat 中的真实示例
      await handleAcmeChatInbound(api, event);

      res.statusCode = 200;
      res.end("ok");
      return true;
    },
  });
}

注意: 入站消息处理是渠道专属的,每个渠道插件都有自己的入站流水线。可参考内置渠道插件(如 extensions/msteamsextensions/googlechat)中的真实模式。

第六步:编写测试

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("从配置中解析账户", () => {
    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("在不暴露密钥的情况下检查账户", () => {
    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("报告缺失配置", () => {
    const cfg = { channels: {} } as any;
    const result = acmeChatPlugin.setup!.inspectAccount!(cfg, undefined);
    expect(result.configured).toBe(false);
  });
});
bash
pnpm test -- extensions/acme-chat/

共享测试工具见 Testing

文件结构

extensions/acme-chat/
├── package.json              # openclaw.channel 元数据
├── openclaw.plugin.json      # 含 config schema 的 manifest
├── index.ts                  # defineChannelPluginEntry
├── setup-entry.ts            # defineSetupPluginEntry
├── api.ts                    # 公共导出(可选)
├── runtime-api.ts            # 内部运行时导出(可选)
└── src/
    ├── channel.ts            # 通过 createChatChannelPlugin 构建的 ChannelPlugin
    ├── channel.test.ts       # 测试
    ├── client.ts             # 平台 API 客户端
    └── runtime.ts            # 运行时 store(如需要)

进阶主题

下一步