从零到一,开发你的第一个 OpenClaw 消息通道插件
文档版本:1.1.0
最后更新:2026-03-26
GitHub: github.com/chungeplus/...
目录
- 概述
- 系统架构
- 核心概念
- 开发环境搭建
- 插件项目结构
- 核心代码实现
- 插件配置与安装
- 调试与测试
- [实战案例:Yeizi 插件](#实战案例:Yeizi 插件 "#%E5%AE%9E%E6%88%98%E6%A1%88%E4%BE%8Byeizi-%E6%8F%92%E4%BB%B6")
- 常见问题
- [API 端点](#API 端点 "#api-%E7%AB%AF%E7%82%B9")
- 技术选型
- 术语表
概述
什么是 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)
- 用户输入消息
- 前端通过 WebSocket 发送消息到后端
- 后端转发消息到 OpenClaw 插件
- 插件构建 ctxPayload
- 调用 finalizeInboundContext
- 调用 dispatchReplyWithBufferedBlockDispatcher 触发 AI 处理
消息回复流程(OpenClaw → 用户)
- OpenClaw AI 处理完成
- dispatchReplyWithBufferedBlockDispatcher 的 deliver 回调触发
- 插件通过 WebSocket 发送回复到后端
- 后端通过 WebSocket 推送到前端
- 前端显示 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
- npm 或 pnpm
- 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 回调不触发
可能原因:
- OpenClaw 版本 < 2026.3.12
- AI 模型未正确配置
blockStreaming配置问题
解决方案:
- 确保 OpenClaw 版本 >= 2026.3.12
- 检查 AI 模型配置是否正确
- 确保
blockStreaming: true
问题 2: WebSocket 连接失败
检查项:
- 后端服务是否运行
- AppKey/AppSecret 是否正确
- 网络连接是否正常
问题 3: AI 不回复
检查项:
- AI 模型 API Key 是否正确
- 模型是否支持
- 网络是否能访问 AI 服务
测试建议
- 先在本地测试后端服务
- 使用简单的消息测试
- 逐步添加复杂功能
- 使用日志追踪问题
实战案例: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 插件的核心在于:
- 通过 WebSocket 接收前端消息
- 构建 ctxPayload 调用 dispatchReplyWithBufferedBlockDispatcher
- 在 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: 如何调试插件?
- 使用
log?.info()输出日志 - 检查 OpenClaw 日志
- 使用断点调试
- 逐步测试功能
参考资源
结语
开发一个 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 运行时环境 |