Skip to main content

概述

频道扩展让 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"
    }
  }
}

插件入口

1

创建插件定义

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;
2

实现频道逻辑

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();
  },
};
3

设置运行时引用

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();
  });
});

发布扩展

1

准备发布

确保所有必需文件都已就绪:
extensions/my-channel/
├── package.json
├── index.ts
├── src/
   ├── channel.ts
   └── runtime.ts
├── README.md
└── LICENSE
2

发布到 npm

cd extensions/my-channel
npm publish --access public
3

用户安装

用户可以通过以下方式安装:
npm install @openclaw/my-channel
OpenClaw 会自动检测并加载扩展。

最佳实践

遵循命名约定

  • 包名使用 @openclaw/channel-name
  • 插件 ID 使用 kebab-case
  • 导出默认插件对象

健壮的错误处理

  • 捕获并记录所有错误
  • 实现重试逻辑
  • 提供有意义的错误消息

支持重连

  • 监听断线事件
  • 实现指数退避重连
  • 保持心跳检测

完善文档

  • 提供安装说明
  • 说明配置选项
  • 包含使用示例

相关资源