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

作者:互联网

2026-03-27

⼤语⾔模型脚本

文档版本:1.1.0

最后更新:2026-03-26

GitHub: github.com/chungeplus/…

目录

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

概述

什么是 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; // 账户配置
}

开发环境搭建

环境要求

  • 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   [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 

# 重启 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 官方文档
  • OpenClaw SDK API 文档
  • 示例插件:飞书插件
  • Yeizi 插件源码

结语

开发一个 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 运行时环境