跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像AI 生图工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
TypeScriptNode.jsAI

OpenClaw WebSocket Channel 开发实战:从零打造自定义 AI 通信通道

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

路由之心发布于 2026/4/5更新于 2026/7/661 浏览
OpenClaw WebSocket Channel 开发实战:从零打造自定义 AI 通信通道

项目背景

技术目标
  • 理解多通道 AI 网关的架构模式
  • 掌握 OpenClaw 插件化开发技能
  • 实践 WebSocket 实时双向通信
  • 为社区贡献一个实用的教学案例

项目定位:这是一个学习性质的教学案例,帮助其他开发者快速上手 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
1. 启动 Python WebSocket 服务端
# 进入服务端目录
cd websocket-service
# 使用 uv 安装依赖
uv sync
# 启动服务端
python app.py # 默认监听:ws://localhost:8765
2. 启动 Vue 前端
# 进入前端目录
cd websocket-web
# 安装依赖
npm install
# 开发模式运行
npm run dev # 访问:http://localhost:3000

前端界面功能:

  • 实时聊天窗口
  • 连接状态显示
  • 消息收发日志
3. 安装 WebSocket Channel
# 进入通道插件目录
cd websocket-channel
# 安装到 OpenClaw
openclaw plugins install .
# 验证安装
openclaw plugins list # 应该看到:websocket-channel
4. 配置 OpenClaw

编辑 ~/.openclaw/config.json(或通过 Web UI):

{"channels":{"websocket-channel":{"enabled":true,"config":{"enabled":true,"wsUrl":"ws://localhost:8765/openclaw"}}}}

配置说明:

  • enabled: 启用通道
  • wsUrl: WebSocket 服务端地址
  • 无需 groupPolicy:默认就是开放模式
5. 重启 OpenClaw Gateway
# 如果使用 macOS 应用
# 点击菜单栏 OpenClaw → Restart Gateway
# 或命令行重启
pkill -f openclaw-gateway openclaw gateway run
6. 测试
  1. 打开浏览器访问前端:http://localhost:3000
  2. 点击'连接'按钮
  3. 发送消息:'你好,请介绍一下自己'
  4. 等待 AI 回复…

预期效果:

你:你好,请介绍一下自己
AI:你好!我是你的个人 AI 助手,基于 OpenClaw 框架运行。我可以帮助你回答问题、编写代码、分析数据等。有什么我可以帮你的吗?😊

程序架构

整体架构图
┌─────────────────┐
│ 用户浏览器 │
│ (Vue 前端) │
└────────┬────────┘
│ WebSocket │
ws://localhost:8765
▼
┌─────────────────┐
│ Python 服务端 │
│ (aiohttp) │
└────────┬────────┘
│ WebSocket │
长连接
▼
┌─────────────────┐
│ Node.js 通道 │
│ (ws 库) │
└────────┬────────┘
│ OpenClaw Plugin API
▼
┌─────────────────┐
│ OpenClaw │
│ Gateway │
└────────┬────────┘
│
▼
┌─────────────────┐
│ AI Provider │
│ (Qwen/Bailian) │
└─────────────────┘
数据流详解
入站消息(前端 → AI)
1. 用户在 Vue 界面输入消息
↓
2. 前端通过 WebSocket 发送到 Python 服务端
↓
3. Python 服务端转发给 Node.js 通道
↓
4. Node.js 通道的 ws.on("message") 接收
↓
5. 标准化消息格式
↓
6. 调用 OpenClaw 框架 API
↓
7. Gateway 调用 AI Provider 生成回复
出站消息(AI → 前端)
1. AI 生成回复文本
↓
2. OpenClaw 调用 deliver 回调
↓
3. Node.js 通道通过 WebSocket 发送给 Python 服务端
↓
4. Python 服务端转发给 Vue 前端
↓
5. 前端界面显示 AI 回复

Channel 开发详解

1. 项目初始化
# 创建插件目录
mkdir -p openclaw-websocket-channel/websocket-channel
cd openclaw-websocket-channel/websocket-channel
# 创建基础文件
touch index.ts openclaw.plugin.json package.json
2. 定义插件元数据

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",
    label: "Websocket Channel",
    selectionLabel: "Websocket Channel (Custom)",
    docsPath: "/channels/websocket-channel",
    blurb: "WebSocket based messaging channel.",
    aliases: ["ws"],
  },
  // ... 其他配置
};
3. 实现配置适配器
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() !== "");
  },
}
4. 实现状态管理适配器 ⭐关键
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 ?? null,
    lastError: runtime?.lastError ?? null,
  }),
}

为什么需要 defaultRuntime?

OpenClaw 的 UI 通过读取通道的 defaultRuntime 来知道要跟踪哪些状态字段。如果没有这个配置:

  • UI 不知道要显示 connected 字段
  • 即使你在 startAccount 中设置了 connected: true
  • UI 也只会显示'0/1 connected'

正确做法:

  1. 在 defaultRuntime 中声明要跟踪的字段(包括 connected: false)
  2. 在 startAccount 开始时调用 ctx.setStatus({ connected: true })
  3. UI 就会正确显示'1/1 connected'
5. 实现网关适配器(核心)
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.toString();
        const eventData = JSON.parse(rawData);
        const innerData = eventData.data || {};

        // 2. 标准化消息
        const normalizedMessage = {
          id: `${eventData.source || "websocket"}-${Date.now()}`,
          channel: "websocket-channel",
          accountId: account.accountId,
          senderId: innerData.source || eventData.source || "unknown",
          senderName: innerData.source || eventData.source || "Unknown",
          text: innerData.content || innerData.text || "",
          timestamp: innerData.timestamp || Date.now().toISOString(),
          isGroup: false,
          groupId: undefined,
          attachments: [],
          metadata: {},
        };

        log?.info(
          `[websocket-channel] 📨 Received: "${normalizedMessage.text}" from ${normalizedMessage.senderId}`,
        );

        // 3. 解析路由
        const route = runtime.channel.routing.resolveAgentRoute({
          cfg,
          channel: "websocket-channel",
          accountId: account.accountId,
          peer: {
            kind: "direct",
            id: normalizedMessage.senderId,
          },
        });

        // 4. 构建消息上下文
        const ctxPayload = runtime.channel.reply.finalizeInboundContext({
          Body: normalizedMessage.text,
          BodyForAgent: normalizedMessage.text,
          From: normalizedMessage.senderId,
          To: undefined,
          SessionKey: route.sessionKey,
          AccountId: route.accountId,
          ChatType: "direct",
          SenderName: normalizedMessage.senderName,
          SenderId: normalizedMessage.senderId,
          Provider: "websocket-channel",
          Surface: "websocket-channel",
          MessageSid: normalizedMessage.id,
          Timestamp: Date.now(),
        });

        // 5. 调用框架调度器
        await runtime.channel.reply.dispatchReplyWithBufferedBlockDispatcher({
          ctx: ctxPayload,
          cfg: cfg,
          dispatcherOptions: {
            deliver: async (payload: ReplyPayload, { kind }) => {
              log?.info(
                `[websocket-channel] Delivering ${kind} reply via WebSocket...`,
              );
              const currentConn = connections.get(account.accountId);
              if (
                !currentConn ||
                !currentConn.ws ||
                currentConn.ws.readyState !== 1
              ) {
                throw new Error("No WebSocket connection available");
              }

              // 发送 AI 回复
              currentConn.ws.send(
                JSON.stringify({
                  type: "reply",
                  content: payload.text || "",
                  kind,
                }),
              );
            },
            onError: (err, { kind }) => {
              log?.error(
                `[websocket-channel] Delivery error for ${kind}: ${err.message}`,
              );
            },
          },
        });

        log?.info(`[websocket-channel] Message dispatched successfully`);
      } catch (err) {
        log?.error(
          `[websocket-channel] Failed to process message: ${err.message}`,
        );
      }
    });

    // 监听错误和关闭
    ws.on("error", (err: Error) => {
      log?.error(`[websocket-channel] ❌ WebSocket error: ${err.message}`);
      connections.delete(account.accountId);
      reject(err);
    });

    ws.on("close", () => {
      log?.info(`[websocket-channel] 🔴 Connection closed`);
      connections.delete(account.accountId);
      resolve();
    });

    // 监听中止信号
    abortSignal.addEventListener("abort", () => {
      log?.info(`[websocket-channel] ⏹️ Abort requested`);
      ws.close();
      resolve();
    });

    // 保持连接运行
    await Promise.race([
      connectionPromise,
      new Promise<void>((resolve) => {
        abortSignal.addEventListener("abort", () => resolve());
      }),
    ]);

    connections.delete(account.accountId);
  },
}
6. 注册插件入口
/**
 * 注册插件入口
 * @param api - 插件 API
 */
export default function register(api: any) {
  console.log("[websocket-channel] Registering WebSocket Channel plugin");
  pluginRuntime = api.runtime;
  api.registerChannel({ plugin: WebSocketChannel });
}

目录

  1. 项目背景
  2. 技术目标
  3. 技术栈
  4. 快速开始
  5. 项目结构
  6. 1. 启动 Python WebSocket 服务端
  7. 进入服务端目录
  8. 使用 uv 安装依赖
  9. 启动服务端
  10. 2. 启动 Vue 前端
  11. 进入前端目录
  12. 安装依赖
  13. 开发模式运行
  14. 3. 安装 WebSocket Channel
  15. 进入通道插件目录
  16. 安装到 OpenClaw
  17. 验证安装
  18. 4. 配置 OpenClaw
  19. 5. 重启 OpenClaw Gateway
  20. 如果使用 macOS 应用
  21. 点击菜单栏 OpenClaw → Restart Gateway
  22. 或命令行重启
  23. 6. 测试
  24. 程序架构
  25. 整体架构图
  26. 数据流详解
  27. 入站消息(前端 → AI)
  28. 出站消息(AI → 前端)
  29. Channel 开发详解
  30. 1. 项目初始化
  31. 创建插件目录
  32. 创建基础文件
  33. 2. 定义插件元数据
  34. 3. 实现配置适配器
  35. 4. 实现状态管理适配器 ⭐关键
  36. 5. 实现网关适配器(核心)
  37. 6. 注册插件入口
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • CISP 核心知识点思维导图汇总:应急响应、安全攻击与防护等
  • Spring Boot与MySQL接口结合Redis和Caffeine多级缓存实践
  • Claude Code 高级编程技巧与实战详解
  • Rust 异步代码测试与调试实战指南
  • Rust 核心基础数据类型与变量系统详解
  • GitHub 仓库创建与本地 Git 配置指南
  • Qt Creator 引入第三方库 OpenCV 配置指南
  • CentOS 使用本地光盘源安装 MySQL 实战
  • 搜索引擎收录加速与外链建设策略
  • Spring AI 实现 RAG 检索增强生成详解与实践
  • Rust 异步编程的错误处理艺术
  • LeetCode 滑动窗口算法入门
  • Rust 异步编程进阶:Tokio 运行时、任务调度与高性能网络架构
  • 程序员转行方向推荐:数据分析师、AI 大模型工程师、产品经理与云计算工程师
  • Rust 异步编程实战:构建高性能网络服务
  • VSCode Copilot 文档自动化流程与配置指南
  • Rust 异步编程实战:构建高性能网络应用
  • Topaz Photo AI v1.3.3 中文便携版:智能图片降噪与无损放大工具
  • Rust 异步缓存系统的设计与实现
  • Rust 异步 Web 框架 Axum:核心原理与实战进阶

相关免费在线工具

  • RSA密钥对生成器

    生成新的随机RSA私钥和公钥pem证书。 在线工具,RSA密钥对生成器在线工具,online

  • Mermaid 预览与可视化编辑

    基于 Mermaid.js 实时预览流程图、时序图等图表,支持源码编辑与即时渲染。 在线工具,Mermaid 预览与可视化编辑在线工具,online

  • 随机西班牙地址生成器

    随机生成西班牙地址(支持马德里、加泰罗尼亚、安达卢西亚、瓦伦西亚筛选),支持数量快捷选择、显示全部与下载。 在线工具,随机西班牙地址生成器在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online