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
}
}
关键字段:
id: 插件唯一标识channels: 声明这个插件提供的渠道名称(跟openclaw.json里channels.qqbot对应)skills: 插件附带的 Skill 目录capabilities: 声明支持的能力
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 的被动回复限制:
- 同一 message_id 1小时内最多回复 4 次
- 超出后自动降级为主动消息(有月度配额:4条/用户/群)
- 插件内部维护
messageReplyTrackerMap 做限流
富媒体处理
| 媒体 | 收 | 发 | 特殊处理 |
|---|---|---|---|
| 图片 | ✅ 下载→本地→传 AI | ✅ 本地图床 HTTP 服务 | 启动 image-server (port 18765) |
| 语音 | ✅ SILK→WAV→STT | ✅ TTS→MP3→SILK | silk-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 SDK:openclaw/plugin-sdk 提供了 ChannelPlugin、OpenClawPluginApi、emptyPluginConfigSchema 等接口,是写插件的 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