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

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

项目背景 为什么做这个项目? 最近 OpenClaw 特别火,这是一个强大的个人 AI 助手网关,支持接入 WhatsApp、Telegram、Discord 等 15+ 个消息平台。作为一个技术爱好者,我决定深入学习一下它的架构设计。 **学习目标**: 理解多通道 AI 网关的架构模式 掌握 OpenClaw 插件化开发技能 实践 WebSocket 实时双向通信 为社区贡献一个实用的教学案例…

嘘发布于 2026/4/6更新于 2026/7/244.8K 浏览
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

快速开始

本项目 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}`);
      }
    });

    // 监听错误和关闭
    const connectionPromise = new Promise<void>((resolve, reject) => {
      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();
      });

      // 保持连接运行
      Promise.race([
        connectionPromise,
        new Promise<void>((res) => {
          abortSignal.addEventListener("abort", () => res());
        }),
      ]).then(() => {
        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. 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. 官方文档
  40. 示例项目
  • 免费图片AI生成工具免费生成了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 免费图片视频在线生成30秒,将你的创意变成现实开始设计
  • X/Twitter免费视频下载器免登陆无限额度免费视频解析下载了解详情
  • 100+免费在线小游戏爽一把
极客日志微信公众号二维码

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

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

更多推荐文章

查看全部
  • 飞牛 NAS 配置 IPv6 动态解析至阿里云域名教程
  • MySQL 主键与外键的区别及设计最佳实践
  • PaddleOCR-VL 0.9B 本地一键部署教程
  • OpenClaw 接入飞书机器人并集成 Ollama 本地大模型实战
  • C++ 核心基础特性详解:重载、引用、内联、auto 与 nullptr
  • AI 智能编码工具深度测评:GitHub Copilot 与国产新秀对比
  • Python 编码风格与规范
  • Flutter TabBar 标签系统实战:从基础到高级定制
  • 用于数据增强的十个 Python 库
  • 使用本地大模型从发票文本中提取结构化信息
  • Ubuntu 20.04 本地部署 Ollama 及 Open WebUI 图形界面指南
  • 知识库问答机器人:基于 SpringAI+RAG 的完整实现
  • Milvus 部署架构选型与 Linux Docker 实战:Standalone 模式及 WebUI 使用
  • LLM 大模型训练原理与 Transformer 结构详解
  • Llama-Factory 使用指南:从入门到实战
  • 知识图谱与向量数据库融合提升 RAG 检索效果的技术实践
  • 移动端 H5 开发与 PC 端 Web 开发的区别
  • 时空反向传播 (STBP) 算法原理与硬件协同设计
  • 基于 ResNet18 的轻量级 OCR WebUI 实战指南
  • MCP 服务集成实战:browser-tools-mcp 配置教程

相关免费在线工具

  • curl 转代码

    解析常见 curl 参数并生成 fetch、axios、PHP curl 或 Python requests 示例代码。 在线工具,curl 转代码在线工具,online

  • Base64 字符串编码/解码

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

  • Base64 文件转换器

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

  • Markdown转HTML

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

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online

  • JSON 压缩

    通过删除不必要的空白来缩小和压缩JSON。 在线工具,JSON 压缩在线工具,online