Skip to content

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Repository files navigation

onebot-sdk (Go)

这是一个面向 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 契约

message_id 在两个后端的数值语义不同(NapCat 槽位含义与 SnowLuma 完全不可比),SDK 只保证:

  • 它是不透明的外部定位符,只能做等值比较,禁止排序、大小比较或当水位用。
  • 正常路径下保证非零;消息事件中缺失时 SDK 记日志警告,该消息无法用于回复、撤回或去重。
  • NapCat 可能上报负数 ID 的边缘情况 SDK 照常接受,不假设递增。

HTTP 调用

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)
	}
}

正向 WebSocket 事件循环

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 不缓存或记录密钥。

原始 action 兜底

当后端新增接口但 SDK 还未生成强类型方法时,可以直接调用原始 action:

var result map[string]any
err := client.Call(ctx, "some_new_action", map[string]any{"value": 1}, &result)

按方言的 action 适用范围

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 或多帧文件传输。

重新生成 API

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 账号权限、在线发送效果、分页方向或长期运行。

About

No description, website, or topics provided.

Resources

Stars

2 stars

Watchers

0 watching

Forks

Releases

Packages

Contributors

Languages