这是一个面向 OneBot 11 的纯 Go 统一接入层 SDK,同时支持 NapCat 和 SnowLuma 两种后端。SDK 以 NapCat 官方 OpenAPI 4.18.42 为来源生成强类型 API 方法,通过事件管线和根包门面处理方言差异。API() 提供遵循 NapCat 规范的底层绑定;跨后端行为优先使用根包统一方法。提供 HTTP、正向 WebSocket、反向 WebSocket server、事件解析和消息段构造能力。
- 基于方言表的双后端归一化:连接建立后自动检测后端(
get_version_info的app_name),未知后端降级为通用 OneBot 11 模式(零补拉、零扩展)。 - 归一化事件流
Events():强类型事件 + 类型化消息段 + 方言字段补齐 + 合并转发补拉。 - 原始事件旁路
RawEvents():透传未解析的原始 JSON 帧,与Events()并存。 - 基于 NapCat 4.18.42 OpenAPI 生成 204 个 action 绑定;对 SnowLuma 调用不存在的 action 会得到携带 retcode 1404 的
*onebot.APIError(后端返回UNKNOWN_ACTION)。 - HTTP 调用基于标准库
net/http;WebSocket 调用基于 coder/websocket,支持 echo 匹配、有界事件流和终止原因查询。 - JSON 编解码使用 Sonic 的标准库兼容配置。
- 不内置插件、命令或路由框架。
go get github.com/zjutjh/onebot-sdk(v1.1.0 起 module 路径由 github.com/zjutjh/napcat-sdk 改名为 github.com/zjutjh/onebot-sdk,属破坏性变更;旧路径的历史版本仍可通过 GitHub 重定向拉取。)
| 行为 | NapCat | SnowLuma | SDK 归一化 |
|---|---|---|---|
| 方言检测 | 自报 app_name = NapCat.Onebot |
自报 app_name = SnowLuma |
自动检测;未知 → Generic |
| forward 段内容 | parseMultMsg 默认 false,可能仅有 ID |
通常仅上报资源 ID | 两端都由管线补拉缺失内容;已有内嵌内容跳过 |
| 表情回应读取 | get_msg 的 emoji_likes_list(likes_cnt 为字符串) |
独立 action get_msg_emoji_likes |
GetMessageReactions 双路径归一为 {EmojiID, Count} |
私聊 target_id |
有,值为会话对端(收到的私聊等于发送者) | 仅自发消息上报 | 缺失时按对端补齐,统一为「会话对端」 |
set_friend_add_request 的 remark |
支持 | 不支持 | SnowLuma 不传 remark |
| 最新历史 | 无锚点读取,显式开启转发内嵌 | 无锚点读取 | GetGroupHistory / GetPrivateHistory,保留后端顺序 |
| 在线设备 | 当前可能仅返回空数组 | {clients:[...]} |
GetOnlineClients 返回设备数组,保留原字段 |
| RKey 列表 | 字符串 type、created_at | 数字 type、create_time | GetRKeys 统一类别与创建时间 |
| 回应通知 | is_add、user_id | sub_type、operator_id | 管线统一 add/remove、is_add 与已知操作者 |
message_id 数值语义 |
31 位正数(MD5 清符号位) | 有符号 int32(SHA-1 截断) | 无法归一,见下方契约 |
检测时机:WebSocket(正向/反向)在连接建立时同步检测(超时 5 秒,失败降级 Generic 并记日志);HTTP client 在首次需要方言时懒检测,检测失败不缓存、下次调用重试,只有识别出的后端和明确判定为未知后端这两种稳定结果才会缓存。WithBackend(...) 可显式指定后端跳过检测,取值在 onebot.BackendNapCat、onebot.BackendSnowLuma、onebot.BackendGeneric 三选一(默认 BackendAuto 自动检测)。
message_id 在两个后端的数值语义不同(NapCat 槽位含义与 SnowLuma 完全不可比),SDK 只保证:
- 它是不透明的外部定位符,只能做等值比较,禁止排序、大小比较或当水位用。
- 正常路径下保证非零;消息事件中缺失时 SDK 记日志警告,该消息无法用于回复、撤回或去重。
- NapCat 可能上报负数 ID 的边缘情况 SDK 照常接受,不假设递增。
package main
import (
"context"
"fmt"
"os"
"github.com/zjutjh/onebot-sdk"
"github.com/zjutjh/onebot-sdk/api"
"github.com/zjutjh/onebot-sdk/message"
)
func main() {
ctx := context.Background()
userID := message.ID(123456)
client := onebot.NewHTTPClient("http://127.0.0.1:3000", onebot.WithToken(os.Getenv("ONEBOT_TOKEN")))
login, err := client.API().GetLoginInfo(ctx, api.GetLoginInfoRequest{})
if err != nil {
panic(err)
}
fmt.Println("当前账号:", login.UserID, "后端:", client.Dialect(ctx).Name)
msg, err := api.NewOB11Message(message.Text("pong"))
if err != nil {
panic(err)
}
_, err = client.API().SendPrivateMsg(ctx, api.SendPrivateMsgRequest{
UserID: userID,
Message: msg,
})
if err != nil {
panic(err)
}
}package main
import (
"context"
"errors"
"fmt"
"os"
"time"
"github.com/zjutjh/onebot-sdk"
"github.com/zjutjh/onebot-sdk/api"
"github.com/zjutjh/onebot-sdk/event"
"github.com/zjutjh/onebot-sdk/message"
)
func main() {
ctx := context.Background()
client, err := onebot.DialWebSocket(
ctx,
"ws://127.0.0.1:3001",
onebot.WithToken(os.Getenv("ONEBOT_TOKEN")),
onebot.WithEventBuffer(1024),
onebot.WithEventDeliveryTimeout(time.Second),
)
if err != nil {
panic(err)
}
defer client.Close()
fmt.Println("后端方言:", client.Dialect(ctx).Name)
pong, err := api.NewOB11Message(message.Text("pong"))
if err != nil {
panic(err)
}
for ev := range client.Events() {
switch e := ev.(type) {
case *event.PrivateMessage:
if e.Message.Text() != "/ping" {
continue
}
_, _ = client.API().SendPrivateMsg(ctx, api.SendPrivateMsgRequest{
UserID: e.UserID,
Message: pong,
})
}
}
if err := client.Err(); err != nil && !errors.Is(err, onebot.ErrClosed) {
panic(err)
}
}事件流默认经过归一化管线:单 goroutine 串行处理(强类型解析 → 方言归一 → 投递),投递顺序与到达顺序一致。普通事件(不含 forward 段)零 API 调用快速通过;两端缺内容的顶层 forward 段触发补拉时有独立超时(10 秒),补拉失败照常投递(ForwardData.Content 为空、FetchErr 暴露错误)。已有内嵌内容跳过,不递归补拉嵌套转发。补拉会延迟后续事件交付,原始队列满时仍按既有规则丢弃新事件。
event.Parse 只解析线上的字段;Events() 还会补齐私聊对端、临时私聊来源群和回应动作。新增强类型通知为 event.GroupMessageReaction 与 event.InputStatus。私聊 AutoReply 为 *bool,nil 表示后端未提供,不能据此判断是否人工消息。邀请人 InvitorID 与 OperatorID 分开保留,不互相代替。
需要强类型消息段时,将 NapCat 的 messagePostFormat 保持为默认的 array。后端返回 CQ 字符串时,SDK 把整个字符串保留为一个 text 段,不解析 CQ。消息段支持文件大小、文件来源、商城表情标记和转发内嵌引用等已核实字段;其余事件字段可通过 Raw() 读取。
背压语义只有一跳(管线到用户):
- 未配置
WithEventDeliveryTimeout(默认):队列满时丢弃新事件并记日志(配置 logger 时)。 - 配置正数超时后:用户持续不消费超过该时长会以
ErrEventBackpressure终止连接。归一化补拉的 API 往返不计入交付超时,不会因后端响应慢误杀连接。
传输层读循环永不阻塞:它是 action(echo) 响应唯一的路由方,阻塞会让在途调用(包括管线自己的补拉请求)永远等不到响应,所以传输到管线这一跳固定为非阻塞投递,队列满一律丢弃并记日志。事件队列有界。
事件 channel 关闭后可通过 Client.Err() 获取固定的首次终止原因:主动关闭及对端正常关闭返回 ErrClosed,网络读取失败返回 TransportError。HTTP client 的 Err() 始终返回 nil。
RawEvents() 返回原始事件 JSON 的旁路流,供绕过归一化管线自行解析(新事件类型、特殊字段、自管解析逻辑):
raw := client.RawEvents()
for frame := range raw {
// frame 是未解析的原始 JSON 字节
}与 Events() 并存而非二选一:每个事件帧同时进入两者。注意:旁路在首次调用 RawEvents() 时才启用,须在事件开始到达前调用,之后才调用的帧不会被回放。旁路队列与事件队列同容量,满时丢弃旁路帧并记日志,不影响主事件流。HTTP client 没有事件流,Events() 与 RawEvents() 返回已关闭通道,range 立即结束。
少量方言差异通过手写门面方法归一(生成方法之外):
// 表情回应:NapCat 走 get_msg.emoji_likes_list,SnowLuma 走 get_msg_emoji_likes
reactions, err := client.GetMessageReactions(ctx, messageID)
// 合并转发内容补拉
nodes, err := client.GetForwardContent(ctx, resID)
// 好友申请处理:remark 仅 NapCat 支持,SnowLuma 下静默忽略
err = client.SetFriendRequest(ctx, flag, true, "备注名")
// 最新一页历史:count=0 使用20,其他取值为1至200,不支持统一锚点分页
groupMessages, err := client.GetGroupHistory(ctx, groupID, 20)
privateMessages, err := client.GetPrivateHistory(ctx, userID, 20)
// 消息详情:直接使用已修正消息体和 ID 类型的生成方法
detail, err := client.API().GetMsg(ctx, api.GetMsgRequest{MessageID: message.ID(-42)})
// 按消息 ID 标记其会话已读,负消息 ID 合法
err = client.MarkMessageRead(ctx, message.ID(-42))
devices, err := client.GetOnlineClients(ctx)
keys, err := client.GetRKeys(ctx)这些读取方法和 GetForwardContent 都不会触发事件管线式自动补拉。历史结果复用 api.GetMsgResponse,消息体为 message.Chain;不承诺后端返回数量一定达到 count,不修改后端顺序。
带锚点历史仍通过底层绑定或 Call 使用:NapCat 的参数名是 message_seq,实际先按消息短 ID 定位;SnowLuma 使用 message_id。不要直接把事件 MessageSeq 当作通用锚点,也不要按消息 ID 大小翻页。两端的分页方向尚未做真实 QQ 联调。
GetMessageReactions 统一返回形态,但 SnowLuma 依赖已观测的表情种类,不保证完整历史快照。NapCat 在线设备当前可能仅返回空数组;SnowLuma 最近会话接口为恒空占位,不能据此推断没有会话或未读消息。MarkMessageRead 不代表所有服务器会话已读。RKey 属于临时下载凭据,SDK 不缓存或记录密钥。
当后端新增接口但 SDK 还未生成强类型方法时,可以直接调用原始 action:
var result map[string]any
err := client.Call(ctx, "some_new_action", map[string]any{"value": 1}, &result)204 个生成方法来自 NapCat 官方 OpenAPI:
- 标准 OneBot 11 核心接口(send_msg、get_group_member_info 等)两端都可用。
- 同名扩展接口也可能有不同参数或响应;历史、在线设备和 RKey 应使用根包门面。SnowLuma 缺少的接口由后端返回 retcode 1404(
UNKNOWN_ACTION),SDK 不伪造成功。 - SnowLuma 扩展 action 通过
Call逃生口调用。
4.18.42 新增26个 QZone action,其中24个在 SnowLuma 2.4.2 有同名入口。Public 系列将完整 feed/comment/reply 对象和游标原样传回,不自行按 tid 拼造对象。get_qzone_comment_list 与 get_qzone_comment_replies 仅 NapCat 提供;不自动换成语义近似接口。content 应使用普通文本或显式富文本对象数组,形似 JSON 数组的字符串在两端可能有不同解释。
生成绑定包含部分多帧文件 action,但当前 Call 只接收一个响应,不能收齐流式下载;RawEvents() 也不会接收后续带 echo 的流式帧。本版不提供 SSE 或多帧文件传输。
go generate ./...生成器读取 internal/openapi/4.18.42/openapi.json,把 action、client 和所有模型输出到 api/。生成文件头会标明“请勿手动修改”。
白名单内的非 nullable 标量 ID(user_id、group_id、message_id、self_id、operator_id、sender_id、target_id),包括纯数字/字符串联合类型,统一生成 message.ID:解码接受字符串与整数,编码为数字。可选字段为值类型并保留 omitempty;ID 数组、包含 null 的联合类型和动态 map 保留规范形态。
官方规范原件不改写。生成器定点将 GetMsgResponse.Message 修正为 message.Chain,因为官方 4.18.42 规范把真实的数组/字符串消息体误写成 object。
api.GetMsgRequest{MessageID: ...}等纯数值 ID Union 改为直接赋值message.ID,不再手工编码 Raw JSON。api.GetMsgResponse.Message从 map 改为message.Chain,使用类型化段或Text()读取。message.File("https://example.com/file.txt")现在编码data.file。参数是可发送的文件来源,不是文件 ID;SnowLuma 文件 ID 发送可显式构造message.Segment{Type: "file", Data: message.FileData{FileID: id}},这不保证适用于 NapCat。EmojiReaction与事件条目共用结构,JSON 字段名为emoji_id/count。- NapCat 的
set_group_leave(is_dismiss=true)现在真正解散群。 SDK 原样传递参数,不进行静默降级;检查调用方既有用法。
离线验收基于固定源码及本地 HTTP/WebSocket 回放。样本来源见 testdata/README.md;尚未验证真实 QQ 账号权限、在线发送效果、分页方向或长期运行。