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

OpenClaw WebSocket Channel 开发实战:构建自定义 AI 通信通道

综述由AI生成介绍基于 OpenClaw 框架开发 WebSocket 通道的实战过程。项目采用 Python aiohttp 作为服务端,Vue 3 作为前端,Node.js 编写 OpenClaw 插件。核心内容包括插件初始化、元数据定义、状态管理适配器实现以及网关适配器的消息收发逻辑。通过 WebSocket 建立长连接,实现用户浏览器到 AI 服务的实时双向通信。文章提供了完整的项目结构、配置步骤及代码示例,帮助开发者快速掌握 OpenClaw 插件开发技能并构建自定义 AI 通信通道。

FrontendX发布于 2026/4/6更新于 2026/5/2429 浏览
OpenClaw WebSocket Channel 开发实战:构建自定义 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 

快速开始

项目源码:https://gitee.com/impl/openclaw-websocket-channel

项目结构
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); // Removed due to undefined scope
    });

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

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

    // 保持连接运行
    await Promise.race([
      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 源码
  • 通道开发指南
  • Channel 插件开发指导手册
示例项目
  • 本项目 Gitee 仓库
  • Feishu Channel
  • iMessage Channel

目录

  1. 项目背景
  2. 为什么做这个项目?
  3. 技术栈
  4. 快速开始
  5. 项目结构
  6. 1. 启动 Python WebSocket 服务端
  7. 进入服务端目录
  8. 使用 uv 安装依赖
  9. 启动服务端
  10. 默认监听:ws://localhost:8765
  11. 2. 启动 Vue 前端
  12. 进入前端目录
  13. 安装依赖
  14. 开发模式运行
  15. 访问:http://localhost:3000
  16. 3. 安装 WebSocket Channel
  17. 进入通道插件目录
  18. 安装到 OpenClaw
  19. 验证安装
  20. 应该看到:websocket-channel
  21. 4. 配置 OpenClaw
  22. 5. 重启 OpenClaw Gateway
  23. 如果使用 macOS 应用
  24. 点击菜单栏 OpenClaw → Restart Gateway
  25. 或命令行重启
  26. 6. 测试
  27. 程序架构
  28. 整体架构图
  29. 数据流详解
  30. 入站消息(前端 → AI)
  31. 出站消息(AI → 前端)
  32. Channel 开发详解
  33. 1. 项目初始化
  34. 创建插件目录
  35. 创建基础文件
  36. 2. 定义插件元数据
  37. 3. 实现配置适配器
  38. 4. 实现状态管理适配器 ⭐关键
  39. 5. 实现网关适配器(核心)
  40. 6. 注册插件入口
  41. 参考链接
  42. 官方文档
  43. 示例项目
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 字节跳动前端开发面试核心考点与实战指南
  • 多智能体协作驱动的多模态医疗大模型系统:RAG-KAG 双路径知识增强与架构设计
  • 企业接入大语言模型的关键步骤与数据准备策略
  • 前端请求分层与自动接口生成方案
  • 数据结构入门:顺序表的实现与原理
  • Python+AI 实战:搭建智能问答机器人
  • OpenClaw Windows10 本地 AI 智能体一键部署教程
  • C/C++ 动态规划:二维路径问题实战
  • ChatGPT 学术版驱动的智能学术写作工具应用
  • 开源 AI 编程工具选型对比:OpenCode 与 GitHub Copilot
  • LangChain+LLaMA:AI 原生应用上下文理解的最佳技术组合
  • Qwen2.5-7B 文案生成实测:5 类商业场景效果对比
  • AIGC 产品经理转行指南:核心技能体系与实战路径
  • 基于 Rust 与 DeepSeek 构建高性能 Text-to-SQL 数据库代理
  • Python NumPy 入门:数据处理与科学计算基础
  • Vivado RAM IP 核配置与读写时序仿真
  • WhisperLiveKit 实时语音识别指南:从安装到生产部署
  • Floweb 超轻量浮动浏览器功能介绍
  • 渗透测试工程师常用的10款漏洞扫描工具(2023版)
  • Gazebo 机器人三维物理仿真平台

相关免费在线工具

  • 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