Appearance
本页是 OpenClaw 渠道插件(Channel Plugin)的完整开发指南,适合需要将 OpenClaw 接入自定义消息平台的开发者。从创建 package.json 和 manifest 文件开始,到构建 ChannelPlugin 对象、接线入口文件、添加 setup entry、处理入站消息,再到编写测试,六步走完成一个可运行的渠道插件。
构建 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.authorizeActorAction和approvalCapability.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": {
"acme-chat": {
"type": "object",
"properties": {
"token": { "type": "string" },
"allowFrom": {
"type": "array",
"items": { "type": "string" }
}
}
}
}
}
}步骤 2:构建渠道插件对象
ChannelPlugin 接口有很多可选的适配器接口。从最小配置开始——id 和 setup——然后按需添加适配器。
创建 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) |
步骤 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 专有工作。
步骤 4:添加 setup entry
创建 setup-entry.ts(引导时轻量加载):
typescript
import { defineSetupPluginEntry } from "openclaw/plugin-sdk/channel-core";
import { acmeChatPlugin } from "./src/channel.js";
export default defineSetupPluginEntry(acmeChatPlugin);渠道禁用或未配置时,OpenClaw 加载此文件而非完整入口,避免在 Setup 流程中引入繁重的 runtime 代码。
步骤 5:处理入站消息
你的插件需要接收来自平台的消息并转发给 OpenClaw。典型模式是一个 Webhook,验证请求后通过渠道入站处理器调度:
typescript
registerFull(api) {
api.registerHttpRoute({
path: "/acme-chat/webhook",
auth: "plugin", // 插件管理的认证(自行验证签名)
handler: async (req, res) => {
const event = parseWebhookPayload(req);
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(如需要)相关文档
- Provider 插件 — 如果插件还需要提供模型
- SDK 概览 — 完整子路径导入参考
- 插件 Manifest — 完整 manifest schema
- 运行时辅助工具 —
api.runtime命名空间
常见问题
Q: 渠道插件和工具插件的入口有什么区别?
A: 渠道插件必须使用 defineChannelPluginEntry(来自 openclaw/plugin-sdk/channel-core),它会自动调用 api.registerChannel({ plugin }),并处理注册模式分离(full/setup-only/cli-metadata)。工具插件用 definePluginEntry(来自 openclaw/plugin-sdk/plugin-entry)。
Q: 我的渠道需要处理原生审批(用户点击按钮批准)怎么做?
A: 在 ChannelPlugin 上添加 approvalCapability 对象,并实现 getActionAvailabilityState(告诉 core 原生审批是否可用)和 approvalCapability.native(原生传输逻辑)。使用 createChannelNativeApprovalRuntime 等来自 openclaw/plugin-sdk/approval-runtime 的共享 helper,core 负责请求过滤、路由、去重和超时管理。
Q: 渠道插件可以不提供 setup-entry.ts 吗?
A: 可以,但不推荐。没有 setup-entry.ts 时,渠道禁用或未配置时 OpenClaw 会加载完整入口,引入不必要的 runtime 依赖。对于有一定复杂度的渠道,建议始终提供 setup-entry.ts。