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

OpenClaw WebSocket 通道开发:从零构建自定义 AI 通信

OpenClaw WebSocket 通道开发教程涵盖项目背景、技术栈、快速开始步骤及核心代码实现。重点讲解 Python 服务端、Vue 前端与 Node.js 插件 SDK 的集成方式,包括状态管理、消息路由及回调处理。适合希望扩展 OpenClaw 多通道支持的开发者。

板砖工程师发布于 2026/4/7更新于 2026/5/2213 浏览
OpenClaw WebSocket 通道开发:从零构建自定义 AI 通信

项目背景

为什么做这个项目?

OpenClaw 是一个强大的个人 AI 助手网关,支持接入 WhatsApp、Telegram、Discord 等 15+ 个消息平台。本项目旨在深入学习其架构设计。

学习目标:

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

项目定位:这是一个学习性质的教学案例,帮助其他开发者快速上手 OpenClaw 插件开发。

技术栈

前端层:Vue 3 + WebSocket
服务端:Python + aiohttp + uv
通道层:Node.js + ws + OpenClaw Plugin SDK
AI 层:OpenClaw Gateway + LLM Provider

快速开始

本项目 Gitee 仓库

项目结构

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

# 进入通道插件目录
 websocket-channel

openclaw plugins install .

openclaw plugins list 
cd
# 安装到 OpenClaw
# 验证安装
# 应该看到: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,
  });
}

参考链接

官方文档

  • OpenClaw 官方文档
  • Plugin SDK 源码
  • 通道开发指南

目录

  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. 注册插件入口
  38. 参考链接
  39. 官方文档
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • OpenClaw 配置 Codex 5.3 个人 AI 编程方案实战
  • 基于 MCP 协议的 JS 逆向自动化工具构建与实践
  • 2025 具身智能端侧芯片与云边协同:主流方案深度解析
  • Ace-Translate 本地离线 AI 翻译工具使用指南
  • 二分查找算法详解与常见变种实战
  • Java Web 开发入门:基础概念、环境搭建与 Servlet/JSP
  • Claude 4.6 深度解读:Agent Teams 功能与 Python 接入指南
  • 通义万相 2.1 模型能力解析与部署指南
  • 通过 Ambari API 添加服务时解决 CSRF 保护错误的方法
  • 基于 Java Swing 的个人所得税计算模拟器源码解析
  • Python 核心知识点与面试常见问题汇总
  • Realistic Vision V1.4 技术解析:如何提升 AI 绘画真实感
  • C++ 异常处理机制:从基础到实践的全面解析
  • OpenClaw WebSocket Channel 开发实战:从零打造自定义 AI 通信通道
  • 无人机路径规划算法详解
  • 国内如何升级 GitHub Copilot 到专业版
  • 前端 SSG 详解:静态站点生成最佳实践
  • C++ STL 容器适配器详解:stack、queue 与 priority_queue 原理
  • Linux 高频指令详解:which alias man echo cd cp mv
  • Stable Diffusion WebUI 本地部署教程(AUTOMATIC1111 版)

相关免费在线工具

  • 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