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

从零到一,开发你的第一个 OpenClaw 消息通道插件

文档版本:1.1.0

最后更新:2026-03-26

GitHub: github.com/chungeplus/...

目录

  1. 概述
  2. 系统架构
  3. 核心概念
  4. 开发环境搭建
  5. 插件项目结构
  6. 核心代码实现
  7. 插件配置与安装
  8. 调试与测试
  9. [实战案例:Yeizi 插件](#实战案例:Yeizi 插件 "#%E5%AE%9E%E6%88%98%E6%A1%88%E4%BE%8Byeizi-%E6%8F%92%E4%BB%B6")
  10. 常见问题
  11. [API 端点](#API 端点 "#api-%E7%AB%AF%E7%82%B9")
  12. 技术选型
  13. 术语表

概述

什么是 OpenClaw?

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

Channel Plugin 的作用

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

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

插件类型

OpenClaw 支持多种插件类型:

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

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


系统架构

整体架构图

javascript 复制代码
┌────────────────────────────────────────────────────────────────┐
│                         用户浏览器                               │
├────────────────────────────────────────────────────────────────┤
│  ┌──────────────────────────────────────────────────────────┐  │
│  │                    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 的能力和元数据:

typescript 复制代码
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 核心交互:

typescript 复制代码
import { getRuntime } from './runtime';

const runtime = getRuntime();

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

账户(Account)

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

typescript 复制代码
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)

创建插件项目

bash 复制代码
# 创建项目目录
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

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 项目结构:

perl 复制代码
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 插件三个部分:

bash 复制代码
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)

typescript 复制代码
/**
 * 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)

typescript 复制代码
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)

typescript 复制代码
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)

typescript 复制代码
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)

typescript 复制代码
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)

typescript 复制代码
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)

typescript 复制代码
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 配置文件示例:

json 复制代码
{
    "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 消息格式

前端 → 插件(用户消息)

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

插件 → 前端(AI 回复)

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

openclaw.plugin.json

插件元数据文件:

json 复制代码
{
    "id": "mychannel",
    "channels": ["mychannel"],
    "skills": [],
    "configSchema": {
        "type": "object",
        "additionalProperties": false,
        "properties": {}
    }
}

package.json 配置

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
        }
    }
}

安装脚本

javascript 复制代码
// 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);

安装命令

bash 复制代码
# 构建插件
npm run build

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

# 重启 OpenClaw
openclaw restart

调试与测试

日志输出

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

typescript 复制代码
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。

项目结构

bash 复制代码
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: 如何支持多个账户?

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

json 复制代码
{
    "channels": {
        "mychannel": {
            "accounts": {
                "default": { ... },
                "backup": { ... }
            }
        }
    }
}

Q3: 如何处理流式响应?

设置 blockStreaming: true

typescript 复制代码
capabilities: {
    blockStreaming: true,
}

Q4: 如何发布插件到 NPM?

bash 复制代码
# 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/config GET 获取插件配置信息
/api/auth/token POST 插件鉴权获取 token
/health GET 健康检查
/ws WebSocket 前端连接端点
/ws/plugin WebSocket 插件连接端点

配置查询响应

json 复制代码
{
    "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 双向通信协议
Channel OpenClaw 中的消息通道
ChannelDock 通道能力定义
dispatchReplyWithBufferedBlockDispatcher OpenClaw SDK 核心 API,用于消息分发和 AI 处理
deliver AI 回复回调函数
ctxPayload 入站上下文载荷
Runtime OpenClaw 运行时环境

相关推荐
ryrhhhh2 小时前
多平台同步优化技术:矩阵跃动小陌GEO如何实现一次配置、全端搜索曝光
人工智能·线性代数·矩阵
qq_452396232 小时前
【模型手术室】第四篇:全流程实战 —— 使用 LLaMA-Factory 开启你的第一个微调任务
人工智能·python·ai·llama
another heaven2 小时前
【深度学习 超参调优】lr0与lrf 的关系
人工智能·深度学习
放下华子我只抽RuiKe52 小时前
深度学习全景指南:硬核实战版
人工智能·深度学习·神经网络·算法·机器学习·自然语言处理·数据挖掘
天空之城_tsf2 小时前
通用多模态检索——大模型微调
人工智能·深度学习·计算机视觉
财迅通Ai2 小时前
天立国际携手电子科技大学对话凯文・凯利,共探科技与教育未来
人工智能·科技·天立国际控股
zhojiew3 小时前
在RAG系统中对FAISS,HNSW,BM25向量检索引擎选型的问题
人工智能·机器学习·faiss
深藏功yu名3 小时前
Day24:向量数据库 Chroma_FAISS 入门
数据库·人工智能·python·ai·agent·faiss·chroma
OpenBayes贝式计算3 小时前
教程上新|低门槛部署英伟达最新 Physical AI 模型,覆盖人形机器人/人体运动生成/扩散模型微调等
人工智能·深度学习·机器学习