前言
OpenClaw 的架构设计秉持"一切皆插件"的理念。Telegram、Discord、Slack 等聊天平台的支持,本质上都是通过 Channel 插件实现的。当你需要接入一个 OpenClaw 尚未支持的聊天平台,或者需要创建一个完全自定义的消息通道时,就需要开发自己的 Channel 插件。
本文将从零开始,带你走完一个 Channel 插件从开发到发布的完整流程。
插件架构概览
Channel 插件的职责
一个 Channel 插件负责以下工作:
外部平台 ←→ Channel 插件 ←→ OpenClaw Core ←→ AI Agent
│
├── 接收外部消息,转换为 OpenClaw 内部格式
├── 将 AI 回复转换为外部平台的消息格式
├── 管理与外部平台的连接(WebSocket / HTTP / 轮询)
└── 处理平台特有的功能(按钮、卡片、媒体等)
插件目录结构
my-channel-plugin/
├── package.json
├── tsconfig.json
├── src/
│ ├── index.ts # 插件入口
│ ├── channel.ts # Channel 实现
│ ├── message.ts # 消息转换
│ ├── connection.ts # 连接管理
│ └── types.ts # 类型定义
├── test/
│ ├── channel.test.ts
│ └── message.test.ts
└── README.md
开发环境搭建
初始化项目
mkdir openclaw-channel-myplatform
cd openclaw-channel-myplatform
npm init -y
npm install @openclaw/plugin-sdk typescript
npm install -D @types/node vitest
配置 TypeScript
{
"compilerOptions": {
"target": "ES2022",
"module": "ESNext",
"moduleResolution": "bundler",
"outDir": "./dist",
"rootDir": "./src",
"strict": true,
"esModuleInterop": true,
"declaration": true
},
"include": ["src/**/*"]
}
package.json 插件元数据
{
"name": "openclaw-channel-myplatform",
"version": "1.0.0",
"description": "OpenClaw Channel plugin for MyPlatform",
"main": "dist/index.js",
"types": "dist/index.d.ts",
"openclaw": {
"type": "channel",
"name": "myplatform",
"displayName": "MyPlatform",
"version": "1.0.0",
"minGatewayVersion": "0.8.0"
}
}
实现 Channel 接口
核心接口定义
OpenClaw Plugin SDK 定义了 Channel 插件必须实现的接口:
// src/types.ts
import {
ChannelPlugin,
ChannelConfig,
IncomingMessage,
OutgoingMessage,
ChannelConnection,
ChannelCapabilities
} from "@openclaw/plugin-sdk";
export interface MyPlatformConfig extends ChannelConfig {
apiToken: string;
apiEndpoint?: string;
webhookPort?: number;
}
实现插件入口
// src/index.ts
import { PluginDefinition } from "@openclaw/plugin-sdk";
import { MyPlatformChannel } from "./channel";
const plugin: PluginDefinition = {
name: "myplatform",
displayName: "MyPlatform",
type: "channel",
// 配置项声明(用于Dashboard UI生成)
configSchema: {
apiToken: {
type: "string",
required: true,
secret: true,
label: "API Token",
description: "MyPlatform 的 Bot API Token"
},
apiEndpoint: {
type: "string",
required: false,
default: "https://api.myplatform.com",
label: "API 端点"
}
},
// 创建Channel实例
createChannel(config: MyPlatformConfig) {
return new MyPlatformChannel(config);
}
};
export default plugin;
实现 Channel 类
// src/channel.ts
import {
ChannelPlugin,
IncomingMessage,
OutgoingMessage,
ChannelCapabilities,
MessageHandler
} from "@openclaw/plugin-sdk";
import { MyPlatformConfig } from "./types";
import { MyPlatformConnection } from "./connection";
import { convertIncoming, convertOutgoing } from "./message";
export class MyPlatformChannel implements ChannelPlugin {
private config: MyPlatformConfig;
private connection: MyPlatformConnection;
private messageHandler: MessageHandler | null = null;
constructor(config: MyPlatformConfig) {
this.config = config;
this.connection = new MyPlatformConnection(config);
}
// 声明插件能力
getCapabilities(): ChannelCapabilities {
return {
supportsDM: true,
supportsGroup: true,
supportsMedia: true,
supportsButtons: true,
supportsEditing: true,
supportsReactions: false,
supportsThreads: false,
supportsVoice: false,
maxMessageLength: 4096,
supportedMediaTypes: ["image", "file", "video"]
};
}
// 启动连接
async start(handler: MessageHandler): Promise<void> {
this.messageHandler = handler;
// 连接到外部平台
await this.connection.connect();
// 监听收到的消息
this.connection.on("message", async (rawMessage) => {
const message = convertIncoming(rawMessage);
if (this.messageHandler) {
await this.messageHandler(message);
}
});
console.log("[MyPlatform] Channel started successfully");
}
// 发送消息
async sendMessage(message: OutgoingMessage): Promise<string> {
const platformMessage = convertOutgoing(message);
const result = await this.connection.send(platformMessage);
return result.messageId; // 返回平台消息ID
}
// 编辑已发送的消息
async editMessage(messageId: string, message: OutgoingMessage): Promise<void> {
const platformMessage = convertOutgoing(message);
await this.connection.edit(messageId, platformMessage);
}
// 停止连接
async stop(): Promise<void> {
await this.connection.disconnect();
console.log("[MyPlatform] Channel stopped");
}
// 健康检查
async healthCheck(): Promise<boolean> {
return this.connection.isConnected();
}
}
实现消息转换
// src/message.ts
import { IncomingMessage, OutgoingMessage } from "@openclaw/plugin-sdk";
// 将平台消息转换为OpenClaw内部格式
export function convertIncoming(raw: any): IncomingMessage {
return {
id: raw.id,
userId: raw.sender.id,
userName: raw.sender.name,
channelId: raw.chat.id,
channelType: raw.chat.type === "private" ? "dm" : "group",
content: raw.text || "",
// 媒体附件
attachments: (raw.attachments || []).map((a: any) => ({
type: a.type,
url: a.url,
filename: a.name,
mimeType: a.mimeType,
size: a.size
})),
// 原始数据(供高级用途)
rawData: raw,
timestamp: new Date(raw.timestamp)
};
}
// 将OpenClaw回复转换为平台消息格式
export function convertOutgoing(message: OutgoingMessage): any {
const result: any = {
chat_id: message.channelId,
text: message.content
};
// 处理富文本格式
if (message.format === "markdown") {
result.parse_mode = "Markdown";
}
// 处理媒体
if (message.attachments?.length) {
result.attachments = message.attachments.map(a => ({
type: a.type,
url: a.url,
caption: a.caption
}));
}
// 处理按钮
if (message.buttons?.length) {
result.keyboard = message.buttons.map(b => ({
text: b.label,
callback_data: b.action
}));
}
return result;
}
连接管理
// src/connection.ts
import { EventEmitter } from "events";
import { MyPlatformConfig } from "./types";
export class MyPlatformConnection extends EventEmitter {
private config: MyPlatformConfig;
private ws: WebSocket | null = null;
private connected = false;
constructor(config: MyPlatformConfig) {
super();
this.config = config;
}
async connect(): Promise<void> {
const endpoint = this.config.apiEndpoint || "https://api.myplatform.com";
// WebSocket 连接示例
this.ws = new WebSocket(`${endpoint}/ws?token=${this.config.apiToken}`);
this.ws.onopen = () => {
this.connected = true;
console.log("[MyPlatform] WebSocket connected");
};
this.ws.onmessage = (event) => {
const data = JSON.parse(event.data);
if (data.type === "message") {
this.emit("message", data.payload);
}
};
this.ws.onclose = () => {
this.connected = false;
// 自动重连
setTimeout(() => this.connect(), 5000);
};
}
async send(message: any): Promise<{ messageId: string }> {
const response = await fetch(
`${this.config.apiEndpoint}/api/messages`,
{
method: "POST",
headers: {
"Authorization": `Bearer ${this.config.apiToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(message)
}
);
return response.json();
}
async edit(messageId: string, message: any): Promise<void> {
await fetch(
`${this.config.apiEndpoint}/api/messages/${messageId}`,
{
method: "PUT",
headers: {
"Authorization": `Bearer ${this.config.apiToken}`,
"Content-Type": "application/json"
},
body: JSON.stringify(message)
}
);
}
async disconnect(): Promise<void> {
this.ws?.close();
this.connected = false;
}
isConnected(): boolean {
return this.connected;
}
}
测试
单元测试
// test/message.test.ts
import { describe, it, expect } from "vitest";
import { convertIncoming, convertOutgoing } from "../src/message";
describe("Message Conversion", () => {
it("should convert incoming DM message", () => {
const raw = {
id: "msg_001",
sender: { id: "user_1", name: "张三" },
chat: { id: "chat_1", type: "private" },
text: "你好",
timestamp: "2026-03-14T10:00:00Z"
};
const result = convertIncoming(raw);
expect(result.userId).toBe("user_1");
expect(result.channelType).toBe("dm");
expect(result.content).toBe("你好");
});
it("should convert outgoing message with buttons", () => {
const message = {
channelId: "chat_1",
content: "请选择:",
buttons: [
{ label: "选项A", action: "option_a" },
{ label: "选项B", action: "option_b" }
]
};
const result = convertOutgoing(message);
expect(result.keyboard).toHaveLength(2);
expect(result.keyboard[0].text).toBe("选项A");
});
});
集成测试
# 使用OpenClaw的插件测试框架
openclaw plugin test ./openclaw-channel-myplatform \
--mock-platform \
--scenario send-receive
本地调试
在 OpenClaw 中加载本地插件
{
plugins: {
local: [
// 从本地路径加载插件
"./plugins/openclaw-channel-myplatform"
]
},
channels: {
myplatform: {
enabled: true,
apiToken: "your-test-token"
}
}
}
# 以开发模式启动,支持热重载
openclaw start --dev --watch-plugins
发布插件
发布到 npm
# 构建
npm run build
# 发布
npm publish --access public
提交到 OpenClaw 插件市场
- 在 GitHub 上创建仓库,确保包含完整的 README 和 LICENSE
- 在 OpenClaw 插件市场提交 PR,包含插件元数据
- 通过审核后,用户可以通过以下方式安装:
openclaw plugin install openclaw-channel-myplatform
安装第三方插件
# 从npm安装
openclaw plugin install openclaw-channel-myplatform
# 从GitHub安装
openclaw plugin install github:username/openclaw-channel-myplatform
# 查看已安装插件
openclaw plugin list
生命周期钩子
Channel 插件还可以实现以下可选的生命周期钩子:
export class MyPlatformChannel implements ChannelPlugin {
// 插件加载时调用(初始化资源)
async onLoad(): Promise<void> { }
// 插件卸载时调用(清理资源)
async onUnload(): Promise<void> { }
// 配置更新时调用(热更新配置)
async onConfigUpdate(newConfig: MyPlatformConfig): Promise<void> { }
// Agent 上线时调用
async onAgentOnline(agentId: string): Promise<void> { }
// Agent 下线时调用
async onAgentOffline(agentId: string): Promise<void> { }
}
小结
OpenClaw 的插件架构让你可以将任何通信平台接入 AI Agent 生态系统。通过实现 ChannelPlugin 接口的几个核心方法——start、sendMessage、stop——你就完成了一个可用的 Channel 插件。消息转换层负责在平台格式和 OpenClaw 内部格式之间进行翻译,连接管理层负责维护与外部平台的通信链路。配合完善的测试框架、本地调试支持和插件市场发布流程,OpenClaw 让插件开发变得高效而有序。