【OpenClaw 】Channel 插件开发实战指南

818 阅读11分钟

从零到一,开发你的第一个 OpenClaw 消息通道插件 文档版本:1.1.0

最后更新:2026-03-26

GitHub: github.com/chungeplus/…

image.png

目录

  1. 概述
  2. 系统架构
  3. 核心概念
  4. Plugin 与 Channel 的区别
  5. 开发环境搭建
  6. 插件项目结构
  7. 核心代码实现
  8. 插件配置与安装
  9. 调试与测试
  10. 实战案例:Yeizi 插件
  11. 常见问题
  12. API 端点
  13. 技术选型
  14. 术语表

概述

什么是 OpenClaw?

OpenClaw 是一个开源的 AI 智能代理框架,支持通过插件扩展消息通道。开发者可以编写 Channel Plugin 来对接各种消息平台(如飞书、微信、Slack 等),使 AI 代理能够接收和回复消息。

Channel Plugin 的作用

Channel Plugin 是 OpenClaw 的扩展模块,负责:

  • 与外部消息平台建立连接
  • 接收用户消息
  • 将消息传递给 OpenClaw 进行 AI 处理
  • 将 AI 回复发送回用户
┌─────────────┐      ┌─────────────┐      ┌─────────────┐
│   用户      │ ───► │  Channel    │ ───► │  OpenClaw  │
│  (飞书/微信)│      │   Plugin    │      │   AI       │
└─────────────┘      └─────────────┘      └─────────────┘
     ▲                                          │
     │                                          ▼
     └──────────────────────────────────────────┘
                    (AI 回复)

插件类型

OpenClaw 支持多种插件类型:

类型说明
Channel Plugin对接消息平台,接收/发送消息
Skill Plugin扩展 AI 技能
Tool Plugin添加 AI 工具

本指南主要讲解 Channel Plugin 的开发。


系统架构

整体架构图

┌────────────────────────────────────────────────────────────────┐
│                         用户浏览器                               │
├────────────────────────────────────────────────────────────────┤
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    Web 前端(对话页面)                      │  │
│  │  • 用户输入消息                                             │  │
│  │  • 显示 AI 回复                                             │  │
│  │  • WebSocket 实时通信                                        │  │
│  │  • 插件配置显示                                             │  │
│  └──────────────────────────────────────────────────────────┘  │
└────────────────────────────────────────────────────────────────┘
           ▲                                    │
           │                                    │ WebSocket
     AI 回复                                    │ 用户消息
           │                                    ▼
           └────────────────────────────────────────────────►
                                 │
                                 ▼
┌────────────────────────────────────────────────────────────────┐
│                          Web 后端服务                            │
├────────────────────────────────────────────────────────────────┤
│  • 鉴权服务(AppKey + AppSecret)                               │
│  • WebSocket 服务                                              │
│  • 消息路由                                                    │
│  • 配置查询 API (/api/config)                                   │
└────────────────────────────────────────────────────────────────┘
           ▲                                    │
           │                                    │ WebSocket
     AI 回复                                    │ 消息转发
           │                                    ▼
           └────────────────────────────────────────────────►
                                 │
                                 ▼
┌────────────────────────────────────────────────────────────────┐
│                        OpenClaw 插件                            │
├────────────────────────────────────────────────────────────────┤
│  • WebSocket 客户端(接收/发送消息)                             │
│  • dispatchReplyWithBufferedBlockDispatcher                     │
│  • ChannelDock 配置                                            │
│  • YeiziDock 定义插件能力                                       │
└────────────────────────────────────────────────────────────────┘
                                 │
                            AI 回复
                         (OpenClaw API)

消息流程

消息发送流程(用户 → OpenClaw)

  1. 用户输入消息
  2. 前端通过 WebSocket 发送消息到后端
  3. 后端转发消息到 OpenClaw 插件
  4. 插件构建 ctxPayload
  5. 调用 finalizeInboundContext
  6. 调用 dispatchReplyWithBufferedBlockDispatcher 触发 AI 处理

消息回复流程(OpenClaw → 用户)

  1. OpenClaw AI 处理完成
  2. dispatchReplyWithBufferedBlockDispatcher 的 deliver 回调触发
  3. 插件通过 WebSocket 发送回复到后端
  4. 后端通过 WebSocket 推送到前端
  5. 前端显示 AI 回复

核心概念

Channel

Channel 是 OpenClaw 中的消息通道概念,代表一个具体的消息来源或发送目标。每个 Channel Plugin 实现一个 Channel。

ChannelDock

ChannelDock 定义了 Channel 的能力和元数据:

export const yeiziDock: ChannelDock = {
    id: "yeizi",
    capabilities: {
        chatTypes: ["direct"],      // 支持私聊
        blockStreaming: true,       // 支持流式响应
    },
};

ChannelPlugin

ChannelPlugin 是插件的核心实现,包含:

  • meta: 插件元数据(名称、描述、文档链接)
  • capabilities: 能力配置(支持的聊天类型、媒体支持等)
  • config: 账户管理配置
  • security: 安全策略
  • status: 状态管理
  • outbound: 出站消息处理
  • gateway: 网关配置(启动账户,处理消息)

Runtime

Runtime 是 OpenClaw 提供的运行时环境,插件通过 Runtime 与 OpenClaw 核心交互:

import { getRuntime } from './runtime';

const runtime = getRuntime();

// 使用 runtime 进行消息处理
runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({...});

账户(Account)

一个 Channel 可以配置多个账户,每个账户代表一个独立的连接:

interface ResolvedAccount {
    accountId: string;      // 账户 ID
    enabled: boolean;       // 是否启用
    configured: boolean;    // 是否已配置
    name?: string;         // 账户名称
    config: AccountConfig; // 账户配置
}

Plugin 与 Channel 的区别

两个核心对象

在 Channel Plugin 中,有两个核心对象需要理解:

对象命名示例说明ID 作用域
PluginyeiziPluginOpenClaw 插件入口,注册 Channel 到 OpenClawplugins.installsplugins.allow
ChannelyeiziChannelChannel 实现,包含消息处理逻辑channelsbindings

插件结构图

yeizi-plugin/
├── index.ts           # 插件入口,定义 yeiziPlugin
└── src/
    ├── channel.ts     # Channel 实现,定义 yeiziChannel
    ├── runtime.ts     # 运行时存储
    └── ...

yeiziPlugin - 插件对象

export const yeiziPlugin = {
    id: "yeizi",           // 插件标识符,用于 plugins.installs / plugins.allow
    name: "Yeizi",
    description: "Yeizi Channel 插件",
    register(api) {
        api.registerChannel({
            channel: yeiziChannel,  // 注册 Channel
            dock: yeiziDock
        });
    }
};

用途

  • 定义插件元数据(名称、描述)
  • 向 OpenClaw 注册 Channel
  • 配置文件:openclaw.jsonplugins 层级

yeiziChannel - Channel 对象

export const yeiziChannel: ChannelPlugin<ResolvedAccount> = {
    id: 'yeizi',           // Channel 标识符,用于 channels / bindings
    meta: { ... },         // 显示信息
    capabilities: { ... }, // 能力配置
    config: { ... },       // 账户管理
    gateway: { ... },      // 消息处理
    outbound: { ... },     // 出站消息
    // ...
};

用途

  • 定义 Channel 的完整功能实现
  • 处理消息收发逻辑
  • 配置文件:openclaw.jsonchannels 层级

yeiziDock - Channel 能力声明

export const yeiziDock: ChannelDock = {
    id: "yeizi",
    capabilities: {
        chatTypes: ["direct"],      // 支持私聊
        blockStreaming: true,        // 支持流式响应
    },
};

用途

  • 轻量级的能力声明
  • 插件注册时快速告诉 OpenClaw 支持的功能
  • yeiziChannel.capabilities 是完整版,yeiziDock.capabilities 是精简版

完整配置示例

{
    "plugins": {
        "allow": ["yeizi"],           // ← 使用 yeiziPlugin.id
        "installs": {
            "yeizi": {                // ← 使用 yeiziPlugin.id
                "source": "path"
            }
        }
    },
    "channels": {
        "yeizi": {                    // ← 使用 yeiziChannel.id
            "enabled": true,
            "appKey": "..."
        }
    },
    "bindings": [{
        "agentId": "main",
        "match": {
            "channel": "yeizi"        // ← 使用 yeiziChannel.id
        }
    }]
}

类比理解

概念类比说明
yeiziPlugin公司整体注册、认证
yeiziChannel销售部具体业务处理
yeiziDock资质证书能力声明

开发环境搭建

环境要求

  • Node.js: >= 18.0.0
  • npmpnpm
  • OpenClaw: >= 2026.3.12
  • 代码编辑器(推荐 VS Code)

创建插件项目

# 创建项目目录
mkdir my-channel-plugin
cd my-channel-plugin

# 初始化 npm 项目
npm init -y

# 安装 TypeScript
npm install -D typescript @types/node

# 安装 OpenClaw SDK
npm install openclaw

# 安装其他依赖(如 ws 用于 WebSocket)
npm install ws
npm install -D @types/ws

配置 tsconfig.json

{
    "compilerOptions": {
        "target": "ES2022",
        "module": "ESNext",
        "moduleResolution": "bundler",
        "lib": ["ES2022"],
        "outDir": "./dist",
        "rootDir": "./src",
        "strict": true,
        "esModuleInterop": true,
        "skipLibCheck": true,
        "forceConsistentCasingInFileNames": true,
        "declaration": true,
        "declarationMap": true,
        "sourceMap": true
    },
    "include": ["src/**/*"],
    "exclude": ["node_modules", "dist"]
}

插件项目结构

一个标准的 Channel Plugin 项目结构:

my-channel-plugin/
├── src/
│   ├── channel.ts          # 核心插件实现
│   ├── accounts.ts         # 账户管理工具
│   ├── config-schema.ts    # 配置验证 Schema
│   ├── runtime.ts          # 运行时存储
│   ├── types.ts           # 类型定义
│   └── websocket-client.ts # WebSocket 客户端
├── index.ts               # 插件入口
├── package.json            # 项目配置
├── tsconfig.json          # TypeScript 配置
└── openclaw.plugin.json   # 插件元数据

Yeizi 项目完整结构

Yeizi 是一个完整的 Web Channel 插件项目,包含 Web 前端、Web 后端和 OpenClaw 插件三个部分:

yeizi/
├── web-channel/                    # Web 端项目
│   ├── frontend/                   # 前端项目(Vue 3 + TypeScript)
│   │   ├── src/
│   │   │   ├── components/       # Vue 组件
│   │   │   │   ├── ChatInput.vue       # 消息输入组件
│   │   │   │   ├── ChatMessage.vue     # 消息显示组件
│   │   │   │   ├── ChatWindow.vue     # 聊天窗口组件
│   │   │   │   ├── ConnectionStatus.vue # 连接状态组件
│   │   │   │   └── SettingsPanel.vue   # 插件配置页面
│   │   │   ├── composables/
│   │   │   │   └── useWebSocket.ts     # WebSocket 钩子
│   │   │   ├── stores/
│   │   │   │   └── chat.ts             # 聊天状态管理
│   │   │   └── App.vue                 # 主应用组件
│   │   └── package.json
│   ├── backend/                   # 后端项目(Express.js)
│   │   ├── src/
│   │   │   ├── routes/
│   │   │   │   ├── auth.ts            # 鉴权路由
│   │   │   │   └── config.ts          # 配置查询路由
│   │   │   ├── services/
│   │   │   │   ├── auth.ts            # 鉴权服务
│   │   │   │   ├── config.ts          # 配置服务
│   │   │   │   └── websocket.ts       # WebSocket 管理
│   │   │   └── index.ts               # 服务入口
│   │   ├── .env                      # 环境变量配置
│   │   └── package.json
│   └── package.json
│
└── yeizi-plugin/                  # OpenClaw 插件项目
    ├── src/
    │   ├── accounts.ts              # 账户管理工具
    │   ├── channel.ts              # Channel Plugin 实现
    │   ├── config-schema.ts        # 配置 Schema 定义
    │   ├── runtime.ts              # 运行时存储管理
    │   ├── types.ts                # 类型定义
    │   └── websocket-client.ts     # WebSocket 客户端封装
    ├── scripts/
    │   ├── setup.mjs              # 安装脚本
    │   └── README.md               # 安装说明
    ├── index.ts                   # 插件运行时入口
    ├── openclaw.plugin.json       # 插件元数据
    ├── package.json
    └── tsconfig.json

核心代码实现

类型定义 (types.ts)

/**
 * WebSocket 消息类型
 */
export interface WebSocketMessage {
    type: string;
    text?: string;
    to?: string;
    from?: string;
    messageId?: string;
    payload?: {
        content?: string;
        messageId?: string;
        to?: string;
    };
}

/**
 * 账户配置类型
 */
export interface AccountConfig {
    name?: string;
    appKey: string;
    appSecret: string;
    baseUrl: string;
    websocketUrl: string;
    enabled?: boolean;
}

/**
 * 已解析的账户类型
 */
export interface ResolvedAccount {
    accountId: string;
    enabled: boolean;
    configured: boolean;
    name?: string;
    config: AccountConfig;
}

配置验证 Schema (config-schema.ts)

import { z } from 'zod';

/**
 * 账户配置 Schema
 */
const AccountConfigSchema = z.object({
    name: z.string().optional(),
    appKey: z.string(),
    appSecret: z.string(),
    baseUrl: z.string().url(),
    websocketUrl: z.string(),
    enabled: z.boolean().optional(),
});

/**
 * 配置 Schema
 */
export const ConfigSchema = z.object({
    appKey: z.string(),
    appSecret: z.string(),
    baseUrl: z.string().url(),
    websocketUrl: z.string(),
    enabled: z.boolean().optional(),
    accounts: z.record(z.string(), AccountConfigSchema).optional(),
});

export type ConfigType = z.infer<typeof ConfigSchema>;

运行时存储 (runtime.ts)

import type { PluginRuntime } from "openclaw/plugin-sdk";

let runtime: PluginRuntime | null = null;

export function setRuntime(next: PluginRuntime) {
    runtime = next;
}

export function getRuntime(): PluginRuntime {
    if (!runtime) {
        throw new Error("Plugin runtime not initialized");
    }
    return runtime;
}

WebSocket 客户端 (websocket-client.ts)

import WebSocket from 'ws';
import type { WebSocketMessage } from './types.js';

export interface WebSocketClientOptions {
    url: string;
    token?: string;
    onMessage: (message: WebSocketMessage) => void;
    onError?: (error: Error) => void;
    onClose?: () => void;
    onOpen?: () => void;
}

export class WebSocketClient {
    private ws: WebSocket | null = null;
    private options: WebSocketClientOptions;
    private reconnectTimer: NodeJS.Timeout | null = null;
    private maxReconnectAttempts = 5;
    private reconnectAttempts = 0;

    constructor(options: WebSocketClientOptions) {
        this.options = options;
    }

    connect(): void {
        const url = this.options.token
            ? `${this.options.url}?token=${this.options.token}`
            : this.options.url;

        this.ws = new WebSocket(url);

        this.ws.on('open', () => {
            this.options.onOpen?.();
        });

        this.ws.on('message', (data) => {
            try {
                const message = JSON.parse(data.toString()) as WebSocketMessage;
                this.options.onMessage(message);
            } catch (error) {
                console.error('[WebSocketClient] Failed to parse message:', error);
            }
        });

        this.ws.on('close', () => {
            this.options.onClose?.();
        });
    }

    send(message: WebSocketMessage): boolean {
        if (!this.ws || this.ws.readyState !== WebSocket.OPEN) {
            return false;
        }
        this.ws.send(JSON.stringify(message));
        return true;
    }

    isConnected(): boolean {
        return this.ws !== null && this.ws.readyState === WebSocket.OPEN;
    }

    disconnect(): void {
        if (this.ws) {
            this.ws.close();
            this.ws = null;
        }
    }
}

账户管理 (accounts.ts)

import type { OpenClawConfig } from 'openclaw/plugin-sdk';
import { DEFAULT_ACCOUNT_ID, normalizeAccountId } from 'openclaw/plugin-sdk/account-resolution';
import type { Config, AccountConfig, ResolvedAccount } from './types.js';

/**
 * 列出所有账户 ID
 */
export function listAccountIds(cfg: OpenClawConfig): string[] {
    const channelConfig = (cfg.channels as any)?.mychannel;
    if (!channelConfig?.accounts) {
        return [DEFAULT_ACCOUNT_ID];
    }
    return Object.keys(channelConfig.accounts);
}

/**
 * 检查账户是否已配置
 */
export function isAccountConfigured(config: AccountConfig): boolean {
    return !!(config.appKey && config.appSecret && config.baseUrl);
}

/**
 * 解析完整的账户信息
 */
export function resolveAccount(
    cfg: OpenClawConfig,
    accountId?: string | null
): ResolvedAccount {
    const id = normalizeAccountId(accountId);
    const channelConfig = (cfg.channels as any)?.mychannel;
    const accountConfig = channelConfig?.accounts?.[id] || channelConfig || {};

    return {
        accountId: id,
        enabled: accountConfig.enabled ?? true,
        configured: isAccountConfigured(accountConfig),
        name: accountConfig.name,
        config: {
            appKey: accountConfig.appKey || channelConfig?.appKey,
            appSecret: accountConfig.appSecret || channelConfig?.appSecret,
            baseUrl: accountConfig.baseUrl || channelConfig?.baseUrl,
            websocketUrl: accountConfig.websocketUrl || channelConfig?.websocketUrl,
        },
    };
}

核心插件实现 (channel.ts)

import type { ChannelDock, ChannelGatewayContext, ChannelPlugin } from 'openclaw/plugin-sdk';
import { buildChannelConfigSchema } from 'openclaw/plugin-sdk';
import type { ResolvedAccount, WebSocketMessage } from './types.js';
import { ConfigSchema } from './config-schema.js';
import { getRuntime } from './runtime.js';
import { WebSocketClient } from './websocket-client.js';
import { listAccountIds, resolveAccount, isAccountConfigured } from './accounts.js';

// 账户连接映射
const accountConnections = new Map<string, WebSocketClient>();

// ChannelDock 定义
export const myChannelDock: ChannelDock = {
    id: "mychannel",
    capabilities: {
        chatTypes: ["direct"],
        blockStreaming: true,
    },
};

// ChannelPlugin 实现
export const plugin: ChannelPlugin<ResolvedAccount> = {
    id: 'mychannel',
    meta: {
        id: 'mychannel',
        label: 'My Channel',
        selectionLabel: 'My Channel',
        docsPath: '/channels/mychannel',
        docsLabel: 'mychannel',
        blurb: 'My Channel Plugin',
        aliases: [],
        order: 100,
    },
    capabilities: {
        chatTypes: ['direct'],
        media: false,
        reactions: false,
        threads: false,
        polls: false,
        nativeCommands: false,
        blockStreaming: true,
    },
    reload: {
        configPrefixes: ['channels.mychannel']
    },
    configSchema: buildChannelConfigSchema(ConfigSchema),
    config: {
        listAccountIds: (cfg) => listAccountIds(cfg),
        resolveAccount: (cfg, accountId) => resolveAccount(cfg, accountId),
        isConfigured: (account) => isAccountConfigured(account.config),
        describeAccount: (account) => ({
            accountId: account.accountId,
            name: account.name ?? 'My Channel Account',
            enabled: account.enabled,
            configured: account.configured,
        }),
    },
    security: {
        resolveDmPolicy: () => ({
            resolve: async () => ({ allow: true }),
        }),
    },
    status: {
        buildAccountSnapshot: async ({ account }) => ({
            label: 'Connected',
            value: 'connected',
        }),
    },
    outbound: {
        deliveryMode: 'direct',
        chunker: (text) => [text],
        textChunkLimit: 4096,
        sendText: async ({ to, text, accountId }) => {
            const wsClient = accountConnections.get(accountId ?? 'default');
            const messageId = Date.now().toString();
            
            if (!wsClient || !wsClient.isConnected()) {
                return { channel: 'mychannel', ok: false, messageId };
            }

            const sent = wsClient.send({
                type: 'response',
                payload: { content: text, messageId, to },
            });

            return { channel: 'mychannel', ok: sent, messageId };
        },
    },
    gateway: {
        startAccount: async (ctx: ChannelGatewayContext) => {
            const { account, accountId, cfg, log, abortSignal } = ctx;
            
            // 1. HTTP 鉴权
            const authResponse = await fetch(`${account.config.baseUrl}/api/auth/token`, {
                method: 'POST',
                headers: { 'Content-Type': 'application/json' },
                body: JSON.stringify({
                    appKey: account.config.appKey,
                    appSecret: account.config.appSecret,
                }),
            });

            if (!authResponse.ok) {
                throw new Error('Authentication failed');
            }

            const { token } = await authResponse.json();
            log?.info(`[MyChannel] Auth success, token: ${token.substring(0, 10)}...`);

            // 2. 建立 WebSocket 连接
            const wsClient = new WebSocketClient({
                url: `${account.config.websocketUrl}/ws/plugin`,
                token,
                onMessage: async (message: WebSocketMessage) => {
                    if (message.type === 'message') {
                        await handleMessage(message, accountId, cfg, log, wsClient);
                    }
                },
                onOpen: () => log?.info(`[MyChannel] WebSocket connected`),
                onError: (error) => log?.error(`[MyChannel] WebSocket error: ${error.message}`),
                onClose: () => log?.info(`[MyChannel] WebSocket disconnected`),
            });

            wsClient.connect();
            accountConnections.set(accountId, wsClient);

            // 3. 等待 abort 信号
            return new Promise<void>((resolve) => {
                abortSignal?.addEventListener('abort', () => {
                    log?.info(`[MyChannel] Stopping account: ${accountId}`);
                    wsClient.disconnect();
                    accountConnections.delete(accountId);
                    resolve();
                });
            });
        },
    },
};

// 消息处理函数
async function handleMessage(
    message: WebSocketMessage,
    accountId: string,
    cfg: any,
    log: any,
    wsClient: WebSocketClient
) {
    const runtime = getRuntime();

    // 从 bindings 中查找 AgentId
    const binding = cfg.bindings?.find(
        (b: any) => b.match?.channel === 'mychannel' && b.match?.accountId === accountId
    );
    const agentId = binding?.agentId ?? 'main';

    log?.info(`[MyChannel] Received message: ${JSON.stringify(message)}`);

    // 构建 ctxPayload
    const ctxPayload = {
        Body: message.text ?? '',
        BodyForAgent: message.text ?? '',
        RawBody: JSON.stringify(message),
        From: message.from ?? 'unknown',
        To: message.to ?? 'mychannel',
        ChatType: 'dm',
        Provider: 'mychannel',
        Surface: 'mychannel',
        AgentId: agentId,
        Timestamp: Date.now(),
        AccountId: accountId,
        MessageSid: message.messageId ?? Date.now().toString(),
        OriginatingChannel: 'mychannel',
        OriginatingTo: message.to ?? 'mychannel',
    };

    // 完成入站上下文
    const finalized = runtime.channel.reply.finalizeInboundContext(ctxPayload);

    // 分发消息到 AI 处理
    await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
        ctx: finalized,
        cfg,
        dispatcherOptions: {
            deliver: async (payload: any) => {
                const textOut = String(payload.text ?? payload.body ?? '');
                const target = message.from;

                if (!target || !textOut.trim()) {
                    return;
                }

                log?.info(`[MyChannel] AI reply: ${textOut}`);

                wsClient.send({
                    type: 'response',
                    payload: {
                        content: textOut,
                        messageId: message.messageId,
                        to: target,
                    },
                });
            },
        },
    });
}

插件入口 (index.ts)

import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { plugin, myChannelDock } from "./src/channel.js";
import { setRuntime } from "./src/runtime.js";

export { plugin } from "./src/channel.js";

const myChannel = {
    id: "mychannel",
    name: "My Channel",
    description: "My Channel Plugin",
    configSchema: emptyPluginConfigSchema(),
    register(api: OpenClawPluginApi) {
        setRuntime(api.runtime);
        api.registerChannel({ plugin, dock: myChannelDock });
    },
};

export function register(api: OpenClawPluginApi) {
    myChannel.register(api);
}

export function activate(api: OpenClawPluginApi) {
    register(api);
}

export default myChannel;

插件配置与安装

openclaw.json 配置示例

完整的 OpenClaw 配置文件示例:

{
    "channels": {
        "yeizi": {
            "enabled": true,
            "appKey": "yeizi-app-key-2026",
            "appSecret": "yeizi-app-secret-2026",
            "baseUrl": "http://localhost:3000",
            "websocketUrl": "ws://localhost:3000",
            "accounts": {
                "default": {
                    "name": "默认账户",
                    "enabled": true,
                    "appKey": "yeizi-app-key-2026",
                    "appSecret": "yeizi-app-secret-2026",
                    "baseUrl": "http://localhost:3000",
                    "websocketUrl": "ws://localhost:3000"
                }
            }
        }
    },
    "plugins": {
        "allow": ["yeizi"],
        "entries": {
            "yeizi": { "enabled": true }
        },
        "installs": {
            "yeizi": {
                "source": "path",
                "sourcePath": "/path/to/yeizi-plugin",
                "installPath": "/path/to/.openclaw/extensions/yeizi",
                "version": "1.0.0"
            }
        }
    },
    "bindings": [
        {
            "agentId": "main",
            "match": {
                "channel": "yeizi",
                "accountId": "default"
            }
        }
    ],
    "models": {
        "providers": {
            "siliconflow": {
                "baseUrl": "https://api.siliconflow.cn/v1",
                "apiKey": "YOUR_API_KEY",
                "api": "openai-completions",
                "models": [
                    { "id": "Pro/moonshotai/Kimi-K2.5" }
                ]
            }
        }
    },
    "agents": {
        "defaults": {
            "model": {
                "primary": "siliconflow/Pro/moonshotai/Kimi-K2.5"
            }
        }
    }
}

WebSocket 消息格式

前端 → 插件(用户消息)

{
    "type": "message",
    "text": "用户消息内容",
    "from": "user_xxx",
    "to": "yeizi",
    "messageId": "1700000000000",
    "chatType": "dm"
}

插件 → 前端(AI 回复)

{
    "type": "response",
    "payload": {
        "content": "AI 回复内容",
        "messageId": "1700000000000",
        "to": "user_xxx"
    }
}

openclaw.plugin.json

插件元数据文件:

{
    "id": "mychannel",
    "channels": ["mychannel"],
    "skills": [],
    "configSchema": {
        "type": "object",
        "additionalProperties": false,
        "properties": {}
    }
}

package.json 配置

{
    "name": "@openclaw/mychannel",
    "version": "1.0.0",
    "description": "My Channel Plugin",
    "type": "module",
    "main": "./dist/index.js",
    "exports": {
        ".": {
            "import": "./dist/index.js",
            "types": "./dist/index.d.ts"
        }
    },
    "scripts": {
        "build": "tsc",
        "dev": "tsc --watch"
    },
    "peerDependencies": {
        "openclaw": ">=2026.3.12"
    },
    "openclaw": {
        "extensions": ["./index.ts"],
        "channel": {
            "id": "mychannel",
            "label": "My Channel",
            "selectionLabel": "My Channel",
            "docsPath": "/channels/mychannel",
            "docsLabel": "mychannel",
            "blurb": "My Channel Plugin",
            "aliases": ["mychannel"],
            "order": 100
        }
    }
}

安装脚本

// scripts/setup.mjs
import fs from 'node:fs/promises';
import path from 'node:path';
import os from 'node:os';
import { execSync } from 'node:child_process';
import { fileURLToPath } from 'node:url';

const PLUGIN_NAME = 'mychannel';
const CONFIG_FILE = 'openclaw.json';

async function main() {
    const args = process.argv.slice(2);
    
    if (args.length < 2) {
        console.error('Usage: node setup.mjs <app_key> <app_secret> [base_url]');
        process.exit(1);
    }

    const [appKey, appSecret, baseUrl = 'http://localhost:3000'] = args;
    const __dirname = path.dirname(fileURLToPath(import.meta.url));
    const home = path.join(os.homedir(), '.openclaw');
    const target = path.join(home, 'extensions', PLUGIN_NAME);
    const configPath = path.join(home, CONFIG_FILE);

    // 复制文件
    await fs.mkdir(path.dirname(target), { recursive: true });
    await fs.cp(path.resolve(__dirname, '..'), target, {
        recursive: true,
        filter: (src) => !src.includes('node_modules')
    });

    // 安装依赖
    execSync('npm install', { cwd: target, stdio: 'inherit' });

    // 更新配置
    let config = {};
    if (await fs.access(configPath).then(() => true).catch(() => false)) {
        config = JSON.parse(await fs.readFile(configPath, 'utf8'));
    }

    config.channels = config.channels || {};
    config.channels[PLUGIN_NAME] = {
        enabled: true,
        appKey,
        appSecret,
        baseUrl,
        websocketUrl: baseUrl.replace('http', 'ws'),
    };

    config.plugins = config.plugins || {};
    config.plugins.allow = config.plugins.allow || [];
    if (!config.plugins.allow.includes(PLUGIN_NAME)) {
        config.plugins.allow.push(PLUGIN_NAME);
    }
    config.plugins.installs = config.plugins.installs || {};
    config.plugins.installs[PLUGIN_NAME] = {
        source: 'path',
        installPath: target,
        version: '1.0.0'
    };

    config.bindings = config.bindings || [];
    config.bindings.push({
        agentId: 'main',
        match: { channel: PLUGIN_NAME, accountId: 'default' }
    });

    await fs.writeFile(configPath, JSON.stringify(config, null, 4));

    console.log('Installation complete!');
}

main().catch(console.error);

安装命令

# 构建插件
npm run build

# 安装插件
node scripts/setup.mjs your-app-key your-app-secret http://localhost:3000

# 重启 OpenClaw
openclaw restart

调试与测试

日志输出

使用 OpenClaw 提供的 log 对象进行日志输出:

log?.info(`[MyChannel] Message received`);
log?.warn(`[MyChannel] Warning message`);
log?.error(`[MyChannel] Error: ${error.message}`);

常见问题排查

问题 1: deliver 回调不触发

可能原因:

  1. OpenClaw 版本 < 2026.3.12
  2. AI 模型未正确配置
  3. blockStreaming 配置问题

解决方案:

  • 确保 OpenClaw 版本 >= 2026.3.12
  • 检查 AI 模型配置是否正确
  • 确保 blockStreaming: true

问题 2: WebSocket 连接失败

检查项:

  • 后端服务是否运行
  • AppKey/AppSecret 是否正确
  • 网络连接是否正常

问题 3: AI 不回复

检查项:

  • AI 模型 API Key 是否正确
  • 模型是否支持
  • 网络是否能访问 AI 服务

测试建议

  1. 先在本地测试后端服务
  2. 使用简单的消息测试
  3. 逐步添加复杂功能
  4. 使用日志追踪问题

实战案例:Yeizi 插件

Yeizi 是一个 Web Channel 插件,用于通过 WebSocket 连接 Web 前端与 OpenClaw。

项目结构

yeizi-plugin/
├── src/
│   ├── channel.ts          # 核心实现
│   ├── accounts.ts        # 账户管理
│   ├── config-schema.ts   # 配置验证
│   ├── runtime.ts         # 运行时
│   ├── types.ts          # 类型定义
│   └── websocket-client.ts # WebSocket
├── scripts/
│   └── setup.mjs         # 安装脚本
├── index.ts              # 入口
└── package.json

关键实现

Yeizi 插件的核心在于:

  1. 通过 WebSocket 接收前端消息
  2. 构建 ctxPayload 调用 dispatchReplyWithBufferedBlockDispatcher
  3. 在 deliver 回调中发送 AI 回复

Web 后端

Yeizi 插件需要一个 Web 后端服务,提供:

  • /api/auth/token - 鉴权接口
  • /api/config - 配置查询接口
  • /ws/plugin - 插件 WebSocket 端点
  • /ws - 前端 WebSocket 端点

常见问题

Q1: 如何选择 Channel ID?

Channel ID 应该:

  • 唯一标识插件
  • 使用小写字母和数字
  • 避免与现有插件冲突
  • 简短易记

Q2: 如何支持多个账户?

在配置中添加多个账户配置:

{
    "channels": {
        "mychannel": {
            "accounts": {
                "default": { ... },
                "backup": { ... }
            }
        }
    }
}

Q3: 如何处理流式响应?

设置 blockStreaming: true

capabilities: {
    blockStreaming: true,
}

Q4: 如何发布插件到 NPM?

# 1. 登录 NPM
npm login

# 2. 发布
npm publish --access public

Q5: 如何调试插件?

  1. 使用 log?.info() 输出日志
  2. 检查 OpenClaw 日志
  3. 使用断点调试
  4. 逐步测试功能

参考资源


结语

开发一个 OpenClaw Channel Plugin 需要理解其核心概念和架构。通过本指南,您应该能够:

  • 理解 OpenClaw 的插件系统
  • 掌握 Channel Plugin 的开发流程
  • 实现一个完整的消息通道插件
  • 调试和发布插件

祝您开发愉快!



API 端点

后端 API

端点方法说明
/api/configGET获取插件配置信息
/api/auth/tokenPOST插件鉴权获取 token
/healthGET健康检查
/wsWebSocket前端连接端点
/ws/pluginWebSocket插件连接端点

配置查询响应

{
    "success": true,
    "data": {
        "appKey": "...",
        "appSecret": "...",
        "baseUrl": "http://localhost:3000",
        "websocketUrl": "ws://localhost:3000"
    }
}

技术选型

Web 端

技术说明
前端框架Vue 3 + TypeScript
构建工具Vite
样式方案Tailwind CSS
状态管理Pinia
WebSocket原生 WebSocket API
后端框架Express.js
WebSocket 服务ws 库
环境配置dotenv

OpenClaw 插件

技术说明
语言TypeScript
Node.js>= 18.0.0
OpenClaw SDK>= 2026.3.12
配置验证Zod

开发里程碑

阶段状态说明
需求确认✅ 完成需求文档、技术方案、项目结构
Web 端开发✅ 完成前端对话界面、后端服务
插件开发✅ 完成Channel 实现、消息处理
集成测试🔄 进行中端到端测试、问题修复

术语表

术语说明
AppKey应用唯一标识符
AppSecret应用密钥
WebSocket双向通信协议
ChannelOpenClaw 中的消息通道
ChannelDock通道能力定义
dispatchReplyWithBufferedBlockDispatcherOpenClaw SDK 核心 API,用于消息分发和 AI 处理
deliverAI 回复回调函数
ctxPayload入站上下文载荷
RuntimeOpenClaw 运行时环境