OpenClaw WebSocket Channel 开发实战:从零打造自定义 AI 通信通道
介绍基于 OpenClaw 框架开发自定义 WebSocket 通道的实战教程。涵盖项目架构设计(Vue 前端、Python 后端、Node.js 插件)、环境搭建步骤及核心代码实现。重点解析 Channel 插件的元数据定义、状态管理适配器和网关适配器逻辑,展示如何实现消息标准化、路由分发及 AI 回复回调,帮助开发者构建实时双向通信能力。

介绍基于 OpenClaw 框架开发自定义 WebSocket 通道的实战教程。涵盖项目架构设计(Vue 前端、Python 后端、Node.js 插件)、环境搭建步骤及核心代码实现。重点解析 Channel 插件的元数据定义、状态管理适配器和网关适配器逻辑,展示如何实现消息标准化、路由分发及 AI 回复回调,帮助开发者构建实时双向通信能力。

项目定位:这是一个学习性质的教学案例,帮助其他开发者快速上手 OpenClaw 插件开发。
前端层:Vue 3 + WebSocket ↓ 服务端:Python + aiohttp + uv ↓ 通道层:Node.js + ws + OpenClaw Plugin SDK ↓ AI 层:OpenClaw Gateway + LLM Provider
openclaw-websocket-channel/
├── websocket-service/ # Python WebSocket 服务端
│ ├── app.py # aiohttp 主程序
│ └── requirements.txt # Python 依赖
├── websocket-web/ # Vue 3 前端
│ ├── src/
│ │ └── App.vue # 主界面
│ └── package.json
└── websocket-channel/ # OpenClaw 通道插件
├── index.ts # 插件主逻辑
└── openclaw.plugin.json
# 进入服务端目录
cd websocket-service
# 使用 uv 安装依赖
uv sync
# 启动服务端
python app.py # 默认监听:ws://localhost:8765
# 进入前端目录
cd websocket-web
# 安装依赖
npm install
# 开发模式运行
npm run dev # 访问:http://localhost:3000
前端界面功能:
# 进入通道插件目录
cd websocket-channel
# 安装到 OpenClaw
openclaw plugins install .
# 验证安装
openclaw plugins list # 应该看到:websocket-channel
编辑 ~/.openclaw/config.json(或通过 Web UI):
{"channels":{"websocket-channel":{"enabled":true,"config":{"enabled":true,"wsUrl":"ws://localhost:8765/openclaw"}}}}
配置说明:
enabled: 启用通道wsUrl: WebSocket 服务端地址groupPolicy:默认就是开放模式# 如果使用 macOS 应用
# 点击菜单栏 OpenClaw → Restart Gateway
# 或命令行重启
pkill -f openclaw-gateway openclaw gateway run
http://localhost:3000预期效果:
你:你好,请介绍一下自己
AI:你好!我是你的个人 AI 助手,基于 OpenClaw 框架运行。我可以帮助你回答问题、编写代码、分析数据等。有什么我可以帮你的吗?😊
┌─────────────────┐
│ 用户浏览器 │
│ (Vue 前端) │
└────────┬────────┘
│ WebSocket │
ws://localhost:8765
▼
┌─────────────────┐
│ Python 服务端 │
│ (aiohttp) │
└────────┬────────┘
│ WebSocket │
长连接
▼
┌─────────────────┐
│ Node.js 通道 │
│ (ws 库) │
└────────┬────────┘
│ OpenClaw Plugin API
▼
┌─────────────────┐
│ OpenClaw │
│ Gateway │
└────────┬────────┘
│
▼
┌─────────────────┐
│ AI Provider │
│ (Qwen/Bailian) │
└─────────────────┘
1. 用户在 Vue 界面输入消息
↓
2. 前端通过 WebSocket 发送到 Python 服务端
↓
3. Python 服务端转发给 Node.js 通道
↓
4. Node.js 通道的 ws.on("message") 接收
↓
5. 标准化消息格式
↓
6. 调用 OpenClaw 框架 API
↓
7. Gateway 调用 AI Provider 生成回复
1. AI 生成回复文本
↓
2. OpenClaw 调用 deliver 回调
↓
3. Node.js 通道通过 WebSocket 发送给 Python 服务端
↓
4. Python 服务端转发给 Vue 前端
↓
5. 前端界面显示 AI 回复
# 创建插件目录
mkdir -p openclaw-websocket-channel/websocket-channel
cd openclaw-websocket-channel/websocket-channel
# 创建基础文件
touch index.ts openclaw.plugin.json package.json
index.ts:
import type { ReplyPayload } from "openclaw/auto-reply/types";
import type { ChannelPlugin, OpenClawConfig } from "openclaw/plugin-sdk";
import { createDefaultChannelRuntimeState } from "openclaw/plugin-sdk";
interface WebSocketChannelConnection {
ws: any;
accountId: string;
}
interface WebSocketChannelAccount {
accountId: string;
wsUrl: string;
enabled?: boolean;
configured?: boolean;
dmPolicy?: "pairing" | "allowlist" | "open" | "disabled";
}
const connections = new Map<string, WebSocketChannelConnection>();
let pluginRuntime: any = null;
const WebSocketChannel: ChannelPlugin<WebSocketChannelAccount> = {
id: "websocket-channel",
meta: {
id: "websocket-channel",
: ,
: ,
: ,
: ,
: [],
},
};
config: {
/**
* 列出所有配置的账户 ID
* @returns 固定返回 ["default"]
*/
listAccountIds: (cfg: OpenClawConfig) => {
return ["default"];
},
/**
* 解析账户配置
*/
resolveAccount: (cfg: OpenClawConfig, accountId: string) => {
const channelCfg = cfg.channels?.["websocket-channel"];
if (!channelCfg || !channelCfg.config) {
return undefined;
}
const config = channelCfg.config as any;
return {
accountId: "default",
wsUrl: config.wsUrl || "ws://localhost:8765/openclaw",
enabled: config.enabled !== false,
};
},
/**
* 检查账户是否已配置
*/
isConfigured: async (account, cfg) => {
return Boolean(account.wsUrl && account.wsUrl.trim() !== "");
},
}
status: {
/**
* 默认运行时状态模板
* ⚠️ 必须实现这个方法,否则 UI 会显示 "0/1 connected"
*/
defaultRuntime: createDefaultChannelRuntimeState("default", {
wsUrl: null,
connected: false,
groupPolicy: null,
}),
/**
* 构建通道摘要(用于 UI 显示)
*/
buildChannelSummary: ({ snapshot }) => ({
wsUrl: snapshot.wsUrl ?? null,
connected: snapshot.connected ?? null,
groupPolicy: snapshot.groupPolicy ?? null,
}),
/**
* 构建账户完整快照
*/
buildAccountSnapshot: ({ account, runtime }) => ({
accountId: account.accountId,
enabled: account.enabled,
configured: account.configured,
wsUrl: account.wsUrl,
running: runtime?.running ?? false,
connected: runtime?.connected ?? false,
groupPolicy: runtime?.groupPolicy ?? null,
lastStartAt: runtime?.lastStartAt ?? null,
lastStopAt: runtime?.lastStopAt ?? ,
: runtime?. ?? ,
}),
}
为什么需要 defaultRuntime?
OpenClaw 的 UI 通过读取通道的 defaultRuntime 来知道要跟踪哪些状态字段。如果没有这个配置:
connected 字段startAccount 中设置了 connected: true正确做法:
defaultRuntime 中声明要跟踪的字段(包括 connected: false)startAccount 开始时调用 ctx.setStatus({ connected: true })gateway: {
/**
* 启动 WebSocket 账户连接
*/
startAccount: async (ctx) => {
const { log, account, abortSignal, cfg } = ctx;
log?.info(`[websocket-channel] Starting WebSocket Channel for ${account.accountId}`);
// 获取 runtime API
const runtime = pluginRuntime;
// ⭐ 关键:设置初始状态为已连接
ctx.setStatus({
accountId: account.accountId,
wsUrl: account.wsUrl,
running: true,
connected: true,
});
log?.info(`[websocket-channel] Status set: connected=true, running=true`);
// 创建 WebSocket 连接
const WebSocketLib = await import("ws");
const ws = new (WebSocketLib.default as any)(account.wsUrl);
// 存储连接
connections.set(account.accountId, {
ws,
accountId: account.accountId,
});
// 监听消息事件
ws.on("message", async (data: Buffer) => {
try {
// 1. 解析原始消息
const rawData = data.();
eventData = .(rawData);
innerData = eventData. || {};
normalizedMessage = {
: ,
: ,
: account.,
: innerData. || eventData. || ,
: innerData. || eventData. || ,
: innerData. || innerData. || ,
: innerData. || .().(),
: ,
: ,
: [],
: {},
};
log?.(
,
);
route = runtime...({
cfg,
: ,
: account.,
: {
: ,
: normalizedMessage.,
},
});
ctxPayload = runtime...({
: normalizedMessage.,
: normalizedMessage.,
: normalizedMessage.,
: ,
: route.,
: route.,
: ,
: normalizedMessage.,
: normalizedMessage.,
: ,
: ,
: normalizedMessage.,
: .(),
});
runtime...({
: ctxPayload,
: cfg,
: {
: (: , { kind }) => {
log?.(
,
);
currentConn = connections.(account.);
(
!currentConn ||
!currentConn. ||
currentConn.. !==
) {
();
}
currentConn..(
.({
: ,
: payload. || ,
kind,
}),
);
},
: {
log?.(
,
);
},
},
});
log?.();
} (err) {
log?.(
,
);
}
});
ws.(, {
log?.();
connections.(account.);
(err);
});
ws.(, {
log?.();
connections.(account.);
();
});
abortSignal.(, {
log?.();
ws.();
();
});
.([
connectionPromise,
<>( {
abortSignal.(, ());
}),
]);
connections.(account.);
},
}
/**
* 注册插件入口
* @param api - 插件 API
*/
export default function register(api: any) {
console.log("[websocket-channel] Registering WebSocket Channel plugin");
pluginRuntime = api.runtime;
api.registerChannel({ plugin: WebSocketChannel });
}

微信公众号「极客日志」,在微信中扫描左侧二维码关注。展示文案:极客日志 zeeklog
生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online
基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online
将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online
将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online
将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online
将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online