sliverp/qqbot:OpenClaw QQ 渠道插件源码分析——如何写一个 OpenClaw Channel Plugin

> 来源: https://github.com/sliverp/qqbot

> NPM: https://www.npmjs.com/package/@sliverp/qqbot

> 版本: v1.6.1

> 作者: sliverp

> 日期: 2026-03-15

📌 一句话总结

这是一个完整的 OpenClaw 渠道插件,实现了 QQ 官方 Bot API v2 的接入,支持 C2C 私聊、群聊、频道消息、富媒体(图片/语音/视频/文件)、STT/TTS。代码量大(gateway.ts 一个文件 141KB),但架构清晰,是学习"如何写 OpenClaw Channel Plugin"的最佳参考。

🏗️ OpenClaw Channel Plugin 架构

一个插件需要实现什么?


openclaw.plugin.json          ← 插件清单(告诉 OpenClaw 你是谁)
index.ts                      ← 入口(注册插件)
src/
  channel.ts                  ← ChannelPlugin 接口(核心)
  config.ts                   ← 配置解析
  gateway.ts                  ← WebSocket/长连接(收消息)
  outbound.ts                 ← 发消息
  api.ts                      ← 第三方 API 封装
  types.ts                    ← 类型定义

Step 1: 插件清单 `openclaw.plugin.json`


{
  "id": "qqbot",
  "name": "QQ Bot Channel",
  "description": "QQ Bot channel plugin",
  "channels": ["qqbot"],
  "skills": ["skills/qqbot-cron", "skills/qqbot-media"],
  "capabilities": {
    "proactiveMessaging": true,
    "cronJobs": true
  }
}

关键字段:

Step 2: 入口 `index.ts`


import type { OpenClawPluginApi } from "openclaw/plugin-sdk";
import { emptyPluginConfigSchema } from "openclaw/plugin-sdk";
import { qqbotPlugin } from "./src/channel.js";

const plugin = {
  id: "qqbot",
  name: "QQ Bot",
  description: "QQ Bot channel plugin",
  configSchema: emptyPluginConfigSchema(),
  register(api: OpenClawPluginApi) {
    // 保存 runtime 引用(用于访问框架能力)
    setQQBotRuntime(api.runtime);
    // 注册渠道插件
    api.registerChannel({ plugin: qqbotPlugin });
  },
};
export default plugin;

核心api.registerChannel() 把你的 ChannelPlugin 对象注册到 OpenClaw。

Step 3: ChannelPlugin 接口(最重要)

ChannelPlugin 是 OpenClaw 定义的渠道接口,需要实现以下模块:

3.1 `meta` — 元信息


meta: {
  id: "qqbot",
  label: "QQ Bot",
  selectionLabel: "QQ Bot",
  docsPath: "/docs/channels/qqbot",
  blurb: "Connect to QQ via official QQ Bot API",
  order: 50,
}

3.2 `capabilities` — 声明能力


capabilities: {
  chatTypes: ["direct", "group"],  // 支持私聊+群聊
  media: true,                     // 支持富媒体
  reactions: false,                // 不支持表情反应
  threads: false,                  // 不支持线程
  blockStreaming: false,           // 不使用块流式
}

3.3 `config` — 配置管理


config: {
  // 列出所有配置的账户 ID
  listAccountIds: (cfg) => listQQBotAccountIds(cfg),
  // 解析账户配置(appId, clientSecret 等)
  resolveAccount: (cfg, accountId) => resolveQQBotAccount(cfg, accountId),
  // 检查是否已配置
  isConfigured: (account) => Boolean(account?.appId && account?.clientSecret),
  // 描述账户状态
  describeAccount: (account) => ({ ... }),
  // 解析 allowFrom(授权发送者列表)
  resolveAllowFrom: ({ cfg, accountId }) => { ... },
}

3.4 `setup` — CLI 向导


setup: {
  validateInput: ({ input }) => {
    if (!input.token) return "需要 --token (格式: appId:clientSecret)";
    return null;
  },
  applyAccountConfig: ({ cfg, accountId, input }) => {
    // 解析 token,写入配置
    const [appId, clientSecret] = input.token.split(":");
    return applyQQBotAccountConfig(cfg, accountId, { appId, clientSecret });
  },
}

3.5 `messaging` — 目标地址解析


messaging: {
  normalizeTarget: (target) => {
    // "qqbot:c2c:OPENID" → 私聊
    // "qqbot:group:GROUP_ID" → 群聊
    // "qqbot:channel:CHANNEL_ID" → 频道
  },
  targetResolver: {
    looksLikeId: (id) => /^qqbot:(c2c|group|channel):/.test(id),
    hint: "格式: qqbot:c2c:openid 或 qqbot:group:groupid",
  },
}

3.6 `outbound` — 发消息(最关键)


outbound: {
  deliveryMode: "direct",        // 直接发送(非队列)
  chunker: chunkText,            // 长文本分块函数
  chunkerMode: "markdown",       // 在 markdown 换行处分块
  textChunkLimit: 2000,          // QQ 单条消息最大 2000 字符
  
  sendText: async ({ to, text, accountId, replyToId, cfg }) => {
    const account = resolveQQBotAccount(cfg, accountId);
    const result = await sendText({ to, text, accountId, replyToId, account });
    return {
      channel: "qqbot",
      messageId: result.messageId,
      error: result.error ? new Error(result.error) : undefined,
    };
  },
  
  sendMedia: async ({ to, text, mediaUrl, accountId, replyToId, cfg }) => {
    // 类似 sendText,但附带媒体 URL
  },
}

3.7 `gateway` — 收消息(WebSocket 长连接)


gateway: {
  startAccount: async (ctx) => {
    const { account, abortSignal, cfg, log } = ctx;
    
    await startGateway({
      account,
      abortSignal,
      cfg,
      log,
      onReady: () => {
        ctx.setStatus({ running: true, connected: true });
      },
      onError: (error) => {
        ctx.setStatus({ lastError: error.message });
      },
    });
  },
}

3.8 `status` — 状态上报


status: {
  defaultRuntime: {
    accountId: "default",
    running: false,
    connected: false,
    lastConnectedAt: null,
    lastError: null,
  },
  buildAccountSnapshot: ({ account, runtime }) => ({
    running: runtime?.running ?? false,
    connected: runtime?.connected ?? false,
    // ...
  }),
}

🔌 QQ Bot API 实现细节

鉴权流程


AppID + AppSecret
    ↓ POST https://bots.qq.com/app/getAppAccessToken
    ↓
Access Token (有效期 7200 秒)
    ↓ 缓存 Map<appId, token>
    ↓ Singleflight 防并发重复请求
    ↓
API 调用 → https://api.sgroup.qq.com/...
    Header: Authorization: QQBot <access_token>

WebSocket Gateway


1. 获取 Gateway URL → wss://api.sgroup.qq.com/gateway
2. 连接 WebSocket
3. 收到 HELLO → 发送 IDENTIFY (token + intents)
4. 权限三级 fallback:
   - full (群聊+私信+频道)
   - group+channel (群聊+频道)
   - channel-only (仅频道)
5. 心跳维护 (间隔由服务器指定)
6. 断线自动重连 (指数退避: 1s→2s→5s→10s→30s→60s,最多100次)

消息处理队列


收到消息 → 入队 (max 1000 全局 / 20 per user)
    ↓
异步消费 (max 10 并发用户)
    ↓
调用 OpenClaw runtime.inbound() → AI 处理 → 回调 outbound

限流机制

QQ Bot API 的被动回复限制:

富媒体处理

媒体特殊处理
图片✅ 下载→本地→传 AI✅ 本地图床 HTTP 服务启动 image-server (port 18765)
语音✅ SILK→WAV→STT✅ TTS→MP3→SILKsilk-wasm + mpg123-decoder
视频✅ 下载✅ URL/本地大文件进度条
文件✅ 下载→OCR/读取✅ max 20MB支持任意格式

SILK 编解码是 QQ 语音的核心难点——QQ 用 SILK 格式(类似微信),需要 silk-wasm 做 WAV↔SILK 转换。

📝 如果我们要写一个 OpenClaw 渠道插件

最小骨架


my-channel/
├── openclaw.plugin.json       # 清单
├── index.ts                   # 入口
├── package.json               # NPM 包
├── src/
│   ├── channel.ts             # ChannelPlugin 实现
│   ├── config.ts              # 配置
│   ├── gateway.ts             # 收消息
│   ├── outbound.ts            # 发消息
│   └── types.ts               # 类型
└── tsconfig.json

必须实现的接口

1. meta — 插件元信息

2. capabilities — 声明能力

3. config.resolveAccount — 解析凭证

4. outbound.sendText — 发文本消息

5. gateway.startAccount — 启动消息接收

可选但建议实现

6. outbound.sendMedia — 发图片/文件

7. messaging.normalizeTarget — 目标地址解析

8. setup.validateInput — CLI 向导

9. status — 状态监控

package.json 关键字段


{
  "openclaw": {
    "extensions": ["./index.ts"]
  },
  "peerDependencies": {
    "openclaw": "*"
  }
}

安装方式


# NPM 发布后
openclaw plugins install @yourname/my-channel

# 本地开发
cd my-channel && openclaw plugins install .

🔍 qqbot 插件的亮点与坑

亮点

1. 多账号支持 — 一个 OpenClaw 实例跑多个 QQ Bot,配置隔离、Token 隔离

2. 权限 fallback — 三级 intent 自动降级,不会因为没申请到群聊权限就启动失败

3. Singleflight Token — 并发安全的 Token 刷新机制

4. 消息队列 — 异步处理防止阻塞心跳,per-user 限流防刷

5. 被动→主动降级 — 超出回复限制自动降级为主动消息

6. 引用索引ref-index-store 缓存消息引用关系,支持 QQ 的引用回复

1. gateway.ts 141KB — 一个文件太大了,应该拆分(消息处理、STT/TTS、图床分开)

2. QQ SILK 编解码 — 必须用 silk-wasm,纯 JS 实现,不能用系统级音频库

3. QQ URL 限制 — 群聊不能直接发 URL,只有私聊可以

4. 消息被动回复 1h/4次 — QQ 官方限制,需要处理降级逻辑

5. 图床服务器 — QQ 发图需要公网可访问的 URL → 插件自建了 HTTP 图床

💡 与我们的关联

1. QQ 接入参考:如果我们要把 OpenClaw 接入 QQ,直接 openclaw plugins install @sliverp/qqbot 就行,不需要自己写

2. 插件开发模板:如果要接入其他 IM(比如钉钉、飞书企业版、LINE),这个插件是最好的参考

3. OpenClaw Plugin SDKopenclaw/plugin-sdk 提供了 ChannelPluginOpenClawPluginApiemptyPluginConfigSchema 等接口,是写插件的 API 文档

4. 我们已有 Discord:当前架构已经很完善,QQ 作为补充渠道可以考虑

📊 评分

维度评分(/10)
代码质量7.5 — 架构清晰但 gateway.ts 太大
功能完整度9.0 — 私聊/群聊/频道/富媒体/STT/TTS 全覆盖
文档质量8.5 — 中英双语 README + 功能截图
实用价值8.0 — 直接可用的 QQ 接入方案
对我们的参考价值9.0 — 最佳的 OpenClaw 插件开发教程
**综合****8.5**

报告由深度研究助手自动生成 | 2026-03-15

来源: https://github.com/sliverp/qqbot