从 openclaw-qqbot 抽取并改写的 QQ Bot API Java SDK,用于在 Java 应用中直接调用 QQ Bot 官方 API(鉴权、Gateway 连接、消息收发、富媒体、定时消息等),不依赖 OpenClaw 本体。
| 模块 | 说明 |
|---|---|
qqbot.sdk.api.Api |
QQ Bot HTTP API 封装:获取 AccessToken、获取 Gateway URL、发送文本 / 图片 / 语音 / 视频 / 文件、后台 Token 刷新(多 AppID 隔离缓存) |
qqbot.sdk.gateway.Gateway |
Gateway 入口:自动重连、会话恢复(Identify/Resume)、心跳、限流重试、统一的入站消息事件 |
qqbot.sdk.gateway.QQGatewayClient |
WebSocket 协议实现:Hello / Identify / Resume / Heartbeat、事件分发、按用户并发消息队列 |
qqbot.sdk.gateway.InboundMessageEvent |
入站消息统一结构:C2C / 频道 / 群聊,附带附件列表、本地路径映射、可选富化内容 |
qqbot.sdk.outbound.Outbound |
出站消息封装:被动回复 / 主动消息 / 富媒体 / 定时消息(Cron 载荷) |
qqbot.sdk.session_store.SessionStore |
Session 持久化:在本地保存 Gateway sessionId + lastSeq,支持 5 分钟过期和节流写盘 |
qqbot.sdk.utils.* |
工具集合:跨平台路径处理、文件读写与大小校验、音频转换、上传缓存、QQ 表情标签解析等 |
qqbot.sdk.refindex.RefIndexStore |
REFIDX 引用索引存储:持久化 QQ REFIDX_* 索引 → 消息摘要,供后续引用消息解析 |
Java SDK 只负责“对接 QQ Bot API”,不涉及 OpenClaw 的插件框架和命令行逻辑,但在设计上与
openclaw-qqbotJS 插件保持一一对应,便于阅读 JS 源码时进行功能比对。
- JDK 21+
- 构建工具推荐:Maven / Gradle
- 依赖:
- Jackson(
com.fasterxml.jackson.core:jackson-databind等,用于 JSON 序列化/反序列化) org.java-websocket:Java-WebSocket(用于 Gateway WebSocket 连接)
- Jackson(
以 Maven 为例(坐标请根据你实际发布到的仓库调整,这里仅示意):
<dependency>
<groupId>qqbot</groupId>
<artifactId>qqbot-sdk</artifactId>
<version>1.0.0</version>
</dependency>import qqbot.sdk.gateway.Gateway;
import qqbot.sdk.gateway.GatewayContext;
import qqbot.sdk.gateway.GatewayMessageHandler;
import qqbot.sdk.gateway.InboundMessageEvent;
import qqbot.sdk.types.ResolvedQQBotAccount;
import java.util.concurrent.atomic.AtomicBoolean;
public class Demo {
public static void main(String[] args) {
// 1. 构造账号配置(通常来自你自己的配置文件)
ResolvedQQBotAccount account = new ResolvedQQBotAccount();
account.setAccountId("default");
account.setAppId(System.getenv("QQBOT_APPID"));
account.setClientSecret(System.getenv("QQBOT_SECRET"));
account.setMarkdownSupport(true);
// 2. 构造 Gateway 上下文
AtomicBoolean abort = new AtomicBoolean(false);
GatewayContext ctx = new GatewayContext();
ctx.setAccount(account);
ctx.setAbortSignal(abort::get);
ctx.setLog(new GatewayContext.GatewayLog() {
@Override public void info(String msg) { System.out.println(msg); }
@Override public void error(String msg) { System.err.println(msg); }
@Override public void debug(String msg) { System.out.println("[DEBUG] " + msg); }
});
ctx.setMessageHandler(new GatewayMessageHandler() {
@Override
public void onMessage(GatewayContext context, InboundMessageEvent event) {
// event.getContent() 已包含表情解析与可选的语音转写
System.out.println("收到消息: type=" + event.getType()
+ ", from=" + event.getSenderId()
+ ", content=" + event.getContent());
}
});
// 3. 启动 Gateway(内部自动重连与会话恢复)
Gateway gateway = new Gateway(ctx);
gateway.startGateway();
}
}import qqbot.sdk.api.Api;
public class SendOnceDemo {
public static void main(String[] args) throws Exception {
String appId = System.getenv("QQBOT_APPID");
String secret = System.getenv("QQBOT_SECRET");
String openid = "USER_OPENID";
// 仅调用 HTTP API 时,建议先初始化 markdown 支持开关
Api.initApiConfig(true);
String token = Api.getAccessToken(appId, secret);
Api.sendC2CMessage(token, openid, "你好,这是一条来自 Java SDK 的消息", null);
}
}QQ 的引用消息事件中不会直接携带被引用消息的正文,而是通过 REFIDX_* 索引标识;服务端在发送消息成功时,也会在响应的 ext_info.ref_idx 字段里返回当前消息的索引。
Java SDK 按照 JS 版本的设计,提供了完整的引用索引链路:
-
入站侧:
C2CMessageEvent/GuildMessageEvent/GroupMessageEvent都映射了message_scene.ext数组。QQGatewayClient会从ext中解析:ref_msg_idx=REFIDX_xxx→ 当前消息引用的那条历史消息(被引用消息的索引)。msg_idx=REFIDX_yyy→ 当前消息自身的索引。
- 解析结果注入到
InboundMessageEvent:event.getRefMsgIdx():被引用消息的索引。event.getMsgIdx():当前消息自身的索引。
-
出站侧:
MessageResponse映射了响应体中的ext_info.ref_idx。OutboundResult增加了refIdx字段,Outbound在内部从 HTTP 返回中提取赋值。
-
持久化存储:
qqbot.sdk.refindex.RefIndexStore- 存储文件位置:
Platform.getDataDir("data")目录下的ref-index.jsonl,通常为:- Linux/macOS:
~/.qqbot-sdk/data/ref-index.jsonl - Windows:
%USERPROFILE%\.qqbot-sdk\data\ref-index.jsonl
- Linux/macOS:
- 设计:
- 内存 Map + JSONL 追加写,进程重启后自动加载恢复。
- 7 天 TTL,超过后自动失效并在适当时机 compact。
- 最大 50,000 条记录,超出后按时间自动淘汰最旧的数据。
- 核心 API:
setRefIndex(String refIdx, RefIndexEntry entry):写入索引。RefIndexEntry getRefIndex(String refIdx):读取被引用消息摘要。String formatRefEntryForAgent(RefIndexEntry entry):格式化为适合注入大模型上下文的一段说明文字。
- 存储文件位置:
应用层可以据此实现“带引用上下文的对话”,示意代码:
import qqbot.sdk.refindex.RefIndexStore;
public void onMessage(GatewayContext ctx, InboundMessageEvent event) {
String userContent = event.getContent();
// 如果用户引用了历史消息
String quotedSummary = null;
if (event.getRefMsgIdx() != null) {
RefIndexStore.RefIndexEntry ref = RefIndexStore.getRefIndex(event.getRefMsgIdx());
if (ref != null) {
quotedSummary = RefIndexStore.formatRefEntryForAgent(ref);
}
}
// 将 quotedSummary 注入到你构造的 prompt 中,交给大模型
}注意:Java SDK 不强制自动写入“出站消息索引”,你可以在自己封装的发送逻辑中,基于
OutboundResult.getRefIdx()手动调用RefIndexStore.setRefIndex,或在需要时新增一层封装完成这一工作,从而完全复刻 JS 版本的行为。
-
Token 管理(
qqbot.sdk.api.Api)- AccessToken 以
appId为 key 保存在内存 Map 中,带 5 分钟安全缓冲。 - 采用“singleflight”模式:同一
appId下并发刷新 Token 时只会发出一次 HTTP 请求,其他调用复用同一个Future。 - 支持后台刷新:
startBackgroundTokenRefresh(appId, clientSecret, options)/stopBackgroundTokenRefresh(appId)。
- AccessToken 以
-
Session 持久化(
qqbot.sdk.session_store.SessionStore+Gateway)- Gateway 启动时会尝试从本地加载上次保存的
sessionId和lastSeq,尽量使用 Resume 而非重新 Identify。 - Session 写盘有 5 分钟过期和 1 秒节流机制,避免频繁 IO。
- Gateway 启动时会尝试从本地加载上次保存的
仓库中包含若干脚本与测试用例(位于 src/test/java 和 src/main/java/qqbot/sdk/scripts),涵盖:
- 仅使用 HTTP API 单次发消息。
- 启动 Gateway 并打印入站事件。
- 使用 Cron 载荷(
QQBOT_CRON:...)发送定时提醒消息。
你可以基于这些示例快速扩展为:
- 自己的 QQ Bot 后端服务(Spring Boot / Jakarta EE 等)。
- 将 QQ 作为入口,对接你已有的对话/工作流系统。
本 SDK 源自 openclaw-qqbot 的实现思路与协议封装,保持开源协议一致(MIT)。如在使用过程中遇到问题或发现与 JS 版本行为不一致,欢迎在原仓库或你自己的仓库中提 Issue。