概述
频道扩展让 OpenClaw 能够连接到不同的消息平台。通过创建自定义频道扩展,你可以让 AI 助手在任何消息平台上工作。OpenClaw 核心支持 Telegram、Discord、Slack、Signal、iMessage 等频道。你可以通过扩展添加更多平台支持。
频道扩展示例
OpenClaw 提供了多个开箱即用的频道扩展:Discord
通过 Discord.js 集成
npm install @openclaw/discord
Matrix
开放协议,端到端加密
npm install @openclaw/matrix
Mattermost
开源团队协作平台
npm install @openclaw/mattermost
MS Teams
微软团队协作工具
npm install @openclaw/msteams
创建频道扩展
扩展结构
一个最小的频道扩展包含以下文件:extensions/my-channel/
├── package.json
├── index.ts # 插件入口
└── src/
├── channel.ts # 频道实现
└── runtime.ts # 运行时设置
package.json
extensions/my-channel/package.json
{
"name": "@openclaw/my-channel",
"version": "2026.3.2",
"description": "OpenClaw My Channel plugin",
"type": "module",
"dependencies": {
"my-channel-sdk": "^1.0.0"
},
"openclaw": {
"extensions": ["./index.ts"],
"channel": {
"id": "my-channel",
"label": "My Channel",
"selectionLabel": "My Channel (plugin)",
"docsPath": "/channels/my-channel",
"docsLabel": "my-channel",
"blurb": "Custom messaging platform",
"order": 100,
"quickstartAllowFrom": true
},
"install": {
"npmSpec": "@openclaw/my-channel",
"localPath": "extensions/my-channel",
"defaultChoice": "npm"
}
}
}
插件入口
创建插件定义
extensions/my-channel/index.ts
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/my-channel";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/my-channel";
import { myChannelPlugin } from "./src/channel.js";
import { setMyChannelRuntime } from "./src/runtime.js";
const plugin = {
id: "my-channel",
name: "My Channel",
description: "My Channel integration",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
// 设置运行时引用
setMyChannelRuntime(api.runtime);
// 注册频道
api.registerChannel({ plugin: myChannelPlugin });
},
};
export default plugin;
实现频道逻辑
extensions/my-channel/src/channel.ts
import type { ChannelPlugin } from "openclaw/plugin-sdk/my-channel";
export const myChannelPlugin: ChannelPlugin = {
name: "my-channel",
description: "My Channel integration",
// 初始化频道
async init(config) {
// 连接到消息平台
const client = await connectToMyChannel(config);
return { client };
},
// 发送消息
async sendMessage(context, message) {
const { client } = context;
await client.send({
channelId: message.channelId,
text: message.text,
attachments: message.attachments,
});
},
// 接收消息
async onMessage(context, handler) {
const { client } = context;
client.on("message", async (msg) => {
await handler({
id: msg.id,
text: msg.text,
author: msg.author,
channelId: msg.channelId,
timestamp: msg.timestamp,
});
});
},
// 清理资源
async cleanup(context) {
const { client } = context;
await client.disconnect();
},
};
设置运行时引用
extensions/my-channel/src/runtime.ts
import type { OpenClawRuntime } from "openclaw/plugin-sdk/my-channel";
let runtime: OpenClawRuntime | null = null;
export function setMyChannelRuntime(rt: OpenClawRuntime): void {
runtime = rt;
}
export function getMyChannelRuntime(): OpenClawRuntime {
if (!runtime) {
throw new Error("Runtime not initialized");
}
return runtime;
}
真实示例:Discord 扩展
让我们看看 Discord 扩展的实际实现:extensions/discord/index.ts
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/discord";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/discord";
import { discordPlugin } from "./src/channel.js";
import { setDiscordRuntime } from "./src/runtime.js";
import { registerDiscordSubagentHooks } from "./src/subagent-hooks.js";
const plugin = {
id: "discord",
name: "Discord",
description: "Discord channel plugin",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
// 设置运行时
setDiscordRuntime(api.runtime);
// 注册频道
api.registerChannel({ plugin: discordPlugin });
// 注册子代理钩子(用于高级功能)
registerDiscordSubagentHooks(api);
},
};
export default plugin;
真实示例:Matrix 扩展
Matrix 扩展展示了如何处理加密消息:extensions/matrix/index.ts
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/matrix";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk/matrix";
import { matrixPlugin } from "./src/channel.js";
import { ensureMatrixCryptoRuntime } from "./src/matrix/deps.js";
import { setMatrixRuntime } from "./src/runtime.js";
const plugin = {
id: "matrix",
name: "Matrix",
description: "Matrix channel plugin (matrix-js-sdk)",
configSchema: emptyPluginConfigSchema(),
register(api: OpenClawPluginApi) {
// 设置运行时
setMatrixRuntime(api.runtime);
// 初始化加密运行时
void ensureMatrixCryptoRuntime({ log: api.logger.info })
.catch((err) => {
const message = err instanceof Error ? err.message : String(err);
api.logger.warn?.(`matrix: crypto runtime bootstrap failed: ${message}`);
});
// 注册频道
api.registerChannel({ plugin: matrixPlugin });
},
};
export default plugin;
频道插件 API
必需方法
所有频道插件必须实现以下方法:init
初始化频道连接
async init(config: ChannelConfig) {
const client = await connect(config);
return { client };
}
sendMessage
发送消息到平台
async sendMessage(context, message) {
await context.client.send(message);
}
onMessage
监听新消息
async onMessage(context, handler) {
context.client.on("message", handler);
}
cleanup
清理资源和连接
async cleanup(context) {
await context.client.disconnect();
}
可选方法
你可以实现这些可选方法以支持高级功能:export const myChannelPlugin: ChannelPlugin = {
// 必需方法...
name: "my-channel",
init: async (config) => { /* ... */ },
sendMessage: async (context, message) => { /* ... */ },
onMessage: async (context, handler) => { /* ... */ },
cleanup: async (context) => { /* ... */ },
// 可选方法
// 编辑已发送的消息
async editMessage(context, messageId, newContent) {
await context.client.edit(messageId, newContent);
},
// 删除消息
async deleteMessage(context, messageId) {
await context.client.delete(messageId);
},
// 添加表情反应
async addReaction(context, messageId, emoji) {
await context.client.react(messageId, emoji);
},
// 处理附件
async uploadAttachment(context, file) {
return await context.client.upload(file);
},
// 获取频道信息
async getChannelInfo(context, channelId) {
return await context.client.getChannel(channelId);
},
};
消息格式
发送消息
interface OutgoingMessage {
channelId: string;
text: string;
attachments?: Array<{
url: string;
filename?: string;
mimeType?: string;
}>;
replyTo?: string; // 回复特定消息
metadata?: Record<string, unknown>;
}
接收消息
interface IncomingMessage {
id: string;
text: string;
author: {
id: string;
username: string;
displayName?: string;
};
channelId: string;
timestamp: number;
attachments?: Array<{
url: string;
filename?: string;
mimeType?: string;
size?: number;
}>;
replyTo?: string;
metadata?: Record<string, unknown>;
}
配置架构
如果你的频道需要配置,可以定义配置架构:import { Type } from "@sinclair/typebox";
const MyChannelConfigSchema = Type.Object({
apiKey: Type.String({ description: "API Key" }),
serverUrl: Type.Optional(Type.String({ description: "Server URL" })),
enableWebhooks: Type.Optional(Type.Boolean({ description: "Enable webhooks" })),
});
const myChannelConfigSchema = {
parse(value: unknown) {
return MyChannelConfigSchema.parse(value);
},
uiHints: {
apiKey: { label: "API Key", sensitive: true },
serverUrl: { label: "Server URL", placeholder: "https://api.example.com" },
enableWebhooks: { label: "Enable Webhooks", advanced: true },
},
};
const plugin = {
id: "my-channel",
name: "My Channel",
configSchema: myChannelConfigSchema,
// ...
};
错误处理
实现健壮的错误处理:export const myChannelPlugin: ChannelPlugin = {
// ...
async sendMessage(context, message) {
try {
await context.client.send(message);
} catch (err) {
if (err.code === "RATE_LIMIT") {
// 等待并重试
await sleep(err.retryAfter);
return await this.sendMessage(context, message);
}
if (err.code === "CHANNEL_NOT_FOUND") {
throw new Error(`Channel ${message.channelId} not found`);
}
// 记录并抛出未知错误
context.logger.error("Send message failed:", err);
throw err;
}
},
async onMessage(context, handler) {
context.client.on("message", async (msg) => {
try {
await handler(msg);
} catch (err) {
context.logger.error("Message handler failed:", err);
}
});
context.client.on("error", (err) => {
context.logger.error("Client error:", err);
// 尝试重新连接
void this.reconnect(context);
});
},
};
高级功能
子代理钩子
为频道添加自定义子代理功能:extensions/my-channel/src/subagent-hooks.ts
import type { OpenClawPluginApi } from "openclaw/plugin-sdk/my-channel";
export function registerMyChannelSubagentHooks(api: OpenClawPluginApi) {
api.on("before_agent_start", async (event) => {
// 在 AI 助手启动前执行的逻辑
if (event.channel === "my-channel") {
// 添加频道特定的上下文
return {
prependContext: "<my-channel-context>频道特定信息</my-channel-context>",
};
}
});
api.on("agent_end", async (event) => {
// 在对话结束后执行的逻辑
if (event.channel === "my-channel") {
// 记录或处理对话结果
}
});
}
媒体处理
处理图片、视频等媒体文件:export const myChannelPlugin: ChannelPlugin = {
// ...
async uploadAttachment(context, file) {
const { client } = context;
// 检查文件大小
if (file.size > 10 * 1024 * 1024) {
throw new Error("File too large (max 10MB)");
}
// 检查文件类型
const allowedTypes = ["image/jpeg", "image/png", "image/gif", "video/mp4"];
if (!allowedTypes.includes(file.mimeType)) {
throw new Error(`Unsupported file type: ${file.mimeType}`);
}
// 上传文件
const url = await client.upload(file.data, {
filename: file.filename,
mimeType: file.mimeType,
});
return { url, filename: file.filename, mimeType: file.mimeType };
},
async sendMessage(context, message) {
const { client } = context;
// 处理附件
const attachments = [];
if (message.attachments) {
for (const attachment of message.attachments) {
if (attachment.url.startsWith("http")) {
// 已经是 URL,直接使用
attachments.push(attachment);
} else {
// 本地文件,需要上传
const uploaded = await this.uploadAttachment(context, {
data: await readFile(attachment.url),
filename: attachment.filename,
mimeType: attachment.mimeType,
});
attachments.push(uploaded);
}
}
}
// 发送消息
await client.send({
channelId: message.channelId,
text: message.text,
attachments,
});
},
};
连接管理
实现自动重连和心跳:export const myChannelPlugin: ChannelPlugin = {
// ...
async init(config) {
const client = await connectToMyChannel(config);
// 设置心跳
const heartbeat = setInterval(() => {
client.ping().catch((err) => {
console.error("Heartbeat failed:", err);
});
}, 30000);
// 监听断线
client.on("disconnect", async () => {
console.log("Disconnected, attempting to reconnect...");
await this.reconnect({ client, heartbeat, config });
});
return { client, heartbeat, config };
},
async reconnect(context) {
const { config } = context;
let retries = 0;
const maxRetries = 5;
while (retries < maxRetries) {
try {
const client = await connectToMyChannel(config);
context.client = client;
console.log("Reconnected successfully");
return;
} catch (err) {
retries++;
console.error(`Reconnect attempt ${retries} failed:`, err);
await sleep(1000 * Math.pow(2, retries)); // 指数退避
}
}
throw new Error("Failed to reconnect after max retries");
},
async cleanup(context) {
const { client, heartbeat } = context;
clearInterval(heartbeat);
await client.disconnect();
},
};
测试
为你的频道扩展编写测试:extensions/my-channel/src/channel.test.ts
import { describe, it, expect, beforeEach, vi } from "vitest";
import { myChannelPlugin } from "./channel.js";
describe("My Channel Plugin", () => {
let context: any;
let mockClient: any;
beforeEach(() => {
mockClient = {
send: vi.fn(),
on: vi.fn(),
disconnect: vi.fn(),
};
context = {
client: mockClient,
logger: { error: vi.fn() },
};
});
it("should send message", async () => {
await myChannelPlugin.sendMessage(context, {
channelId: "123",
text: "Hello",
});
expect(mockClient.send).toHaveBeenCalledWith({
channelId: "123",
text: "Hello",
});
});
it("should handle message", async () => {
const handler = vi.fn();
await myChannelPlugin.onMessage(context, handler);
expect(mockClient.on).toHaveBeenCalledWith("message", expect.any(Function));
});
it("should cleanup resources", async () => {
await myChannelPlugin.cleanup(context);
expect(mockClient.disconnect).toHaveBeenCalled();
});
});
发布扩展
准备发布
确保所有必需文件都已就绪:
extensions/my-channel/
├── package.json
├── index.ts
├── src/
│ ├── channel.ts
│ └── runtime.ts
├── README.md
└── LICENSE
最佳实践
遵循命名约定
- 包名使用
@openclaw/channel-name - 插件 ID 使用 kebab-case
- 导出默认插件对象
健壮的错误处理
- 捕获并记录所有错误
- 实现重试逻辑
- 提供有意义的错误消息
支持重连
- 监听断线事件
- 实现指数退避重连
- 保持心跳检测
完善文档
- 提供安装说明
- 说明配置选项
- 包含使用示例