Skip to content

本页完整指南用于为 OpenClaw 开发渠道插件。从 package.json 和 manifest 配置起步,创建 ChannelPlugin 对象(含安全、配对、线程、出站),通过 defineChannelPluginEntry 接入入口,添加 setup-entry.ts 实现引导时轻量加载,处理入站 Webhook 消息,并编写 colocated 测试。完成后你就能将 OpenClaw 连接到任意消息平台,支持 DM 安全、配对审批、回复线程和出站发送。

OpenClaw 渠道插件开发全指南:从零搭建消息平台集成

本指南带你一步步为 OpenClaw 构建渠道插件,将其接入任意消息平台。完成后你将拥有一个支持 DM 安全策略、配对流程、回复线程和出站消息的完整渠道。

如果你此前没有构建过任何 OpenClaw 插件,请先阅读插件开发入门了解基础包结构和 manifest 配置。

渠道插件工作原理

渠道插件不需要自己实现 send/edit/react 工具。OpenClaw 在 core 中维护一个共享的 message 工具。你的插件负责:

  • Config — 账号解析和 Setup 向导
  • Security — DM 策略和白名单
  • Pairing — DM 审批流程
  • Session grammar — Provider 特定对话 id 到 base chat/thread/parent 的映射
  • Outbound — 向平台发送文本、媒体和轮询
  • Threading — 回复如何构成线程

Core 持有共享 message 工具、Prompt 接线、外层 session-key 形状、通用 :thread: bookkeeping 和调度。

如果你的平台在对话 id 中存储了额外作用域,将解析逻辑放在插件的 messaging.resolveSessionConversation(...) 中——这是将 rawId 映射到 base conversation id、可选 thread id 和 parentConversationCandidates 的规范 hook。

审批与渠道能力

大多数渠道插件不需要审批专用代码:

  • Core 负责同聊天 /approve、共享审批按钮载荷和通用回退交付
  • 渠道需要审批特定行为时,在渠道插件上提供一个 approvalCapability 对象
  • approvalCapability.authorizeActorActionapprovalCapability.getActionAvailabilityState 是规范的审批认证接缝
  • 如果渠道暴露原生 exec 审批,即使原生传输完全在 approvalCapability.native 下,也要实现 getActionAvailabilityState

开发步骤

步骤 1:创建包和 manifest

package.json

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": "通过 OpenClaw 接入 Acme Chat。"
    }
  }
}

openclaw.plugin.json

json
{
  "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 验证 plugins.entries.acme-chat.config,用于插件自有设置(非渠道账号配置)。channelConfigs 验证 channels.acme-chat,是 cold-path 来源,在插件 runtime 加载前被配置 schema、setup 和 UI 界面使用。

步骤 2:构建渠道插件对象

ChannelPlugin 接口有很多可选的适配器接口。从最小配置开始——idsetup——然后按需添加适配器。

创建 src/channel.ts

typescript
import {
  createChatChannelPlugin,
  createChannelPluginBase,
} from "openclaw/plugin-sdk/channel-core";
import type { OpenClawConfig } from "openclaw/plugin-sdk/channel-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 安全:谁可以给机器人发消息
  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 接受声明式选项并自动组合:

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

你还可以直接传递原始适配器对象代替声明式选项以获得完全控制。

原始出站适配器可以定义一个 chunker(text, limit, ctx) 函数。可选的 ctx.formatting 携带着交付时的格式化决策(如 maxLinesPerMessage);在发送前应用它,这样回复线程和分块边界由共享出站交付一次性解析。发送上下文还包含 replyToIdSourceimplicitexplicit),当原生回复目标被解析后,载荷助手可以在不消耗隐式单次回复槽的情况下保留显式回复标签。

步骤 3:接线入口文件

创建 index.ts

typescript
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 管理");
      },
      {
        descriptors: [
          {
            name: "acme-chat",
            description: "Acme Chat 管理",
            hasSubcommands: false,
          },
        ],
      },
    );
  },
  registerFull(api) {
    api.registerGatewayMethod(/* ... */);
  },
});

将渠道自有的 CLI 描述符放在 registerCliMetadata(...) 中,这样 OpenClaw 无需激活完整渠道 runtime 也能在根 help 中展示它们。registerFull(...) 只用于 runtime 专有工作。如果 registerFull(...) 注册了 gateway RPC 方法,请使用插件特定的前缀。核心管理 namespace(config.*exec.approvals.*wizard.*update.*)保持保留,且始终解析为 operator.admindefineChannelPluginEntry 自动处理注册模式分离。

步骤 4:添加 setup entry

创建 setup-entry.ts(引导时轻量加载):

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

export default defineSetupPluginEntry(acmeChatPlugin);

渠道禁用或未配置时,OpenClaw 加载此文件而非完整入口,避免在 Setup 流程中引入繁重的 runtime 代码。捆绑的工作区渠道如果希望将 setup-safe 的导出拆分为侧边栏模块,也可以使用来自 openclaw/plugin-sdk/channel-entry-contractdefineBundledChannelSetupEntry(...)

步骤 5:处理入站消息

你的插件需要接收来自平台的消息并转发给 OpenClaw。典型模式是一个 Webhook,验证请求后通过渠道入站处理器调度:

typescript
registerFull(api) {
  api.registerHttpRoute({
    path: "/acme-chat/webhook",
    auth: "plugin", // 插件管理的认证(自行验证签名)
    handler: async (req, res) => {
      const event = parseWebhookPayload(req);
      // 你的入站处理器将消息发送给 OpenClaw。具体接线取决于平台 SDK——
      // 查看内置的 Microsoft Teams 或 Google Chat 插件包获取真实示例。
      await handleAcmeChatInbound(api, event);
      res.statusCode = 200;
      res.end("ok");
      return true;
    },
  });
}

入站消息处理是渠道特定的。每个渠道插件拥有自己的入站管道。可参考内置渠道插件(如 Microsoft Teams 或 Google Chat 插件包)查看真实示例。

步骤 6:编写测试

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 -- <bundled-plugin-root>/acme-chat/

文件结构

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

进阶主题

主题说明
线程选项固定、账号作用域或自定义回复模式(参见入口点文档
消息工具集成describeMessageTool 和动作发现(参见架构
目标解析inferTargetChatTypelooksLikeIdresolveTarget(参见内部架构
运行时辅助api.runtime 下的 TTS、STT、媒体、子智能体(参见SDK Runtime
渠道 turn 内核共享入站事件生命周期:摄取、解析、记录、调度、最终化(参见渠道 turn

一些捆绑的辅助接缝仍然存在用于捆绑插件维护和兼容性。它们不是新渠道插件的推荐模式;除非你直接维护该捆绑插件系列,否则优先使用来自通用 SDK 表面的通用 channel/setup/reply/runtime 子路径。

下一步

常见问题

渠道插件和工具插件的入口有什么区别?

渠道插件必须使用 defineChannelPluginEntry(来自 openclaw/plugin-sdk/channel-core),它会自动调用 api.registerChannel({ plugin }),并处理注册模式分离(full/setup-only/cli-metadata)。工具插件用 definePluginEntry(来自 openclaw/plugin-sdk/plugin-entry)。

我的渠道需要处理原生审批(用户点击按钮批准)怎么做?

ChannelPlugin 上添加 approvalCapability 对象,并实现 getActionAvailabilityState(告诉 core 原生审批是否可用)和 approvalCapability.native(原生传输逻辑)。使用 createChannelNativeApprovalRuntime 等来自 openclaw/plugin-sdk/approval-runtime 的共享 helper,core 负责请求过滤、路由、去重和超时管理。具体参见英文原文的“审批与渠道能力”章节。

渠道插件可以不提供 setup-entry.ts 吗?

可以,但不推荐。没有 setup-entry.ts 时,渠道禁用或未配置时 OpenClaw 会加载完整入口,引入不必要的 runtime 依赖。对于有一定复杂度的渠道,建议始终提供 setup-entry.ts