napcat: NapCatQQ (OneBot 11) Go SDK, initial release

This commit is contained in:
2026-10-18 20:20:29 +08:00
commit 7f1738a987
14 changed files with 2879 additions and 0 deletions

200
segment.go Normal file
View File

@ -0,0 +1,200 @@
package napcat
import (
"encoding/json"
"fmt"
"strconv"
)
// Segment 是 OneBot 11 的消息段:{"type": "...", "data": {...}}。
// 收到的事件里 data 值可能是数字或字符串,因此统一用 map 承载,
// 发送时用本包的构造函数生成,避免字段名拼错。
type Segment struct {
Type string `json:"type"`
Data map[string]any `json:"data"`
}
// With 为消息段附加一个 data 字段,可链式调用:
//
// napcat.Image("1.jpg").With("summary", "看图")
func (s Segment) With(key string, value any) Segment {
if s.Data == nil {
s.Data = map[string]any{}
}
s.Data[key] = value
return s
}
func seg(t string, kv ...any) Segment {
data := map[string]any{}
for i := 0; i+1 < len(kv); i += 2 {
key, ok := kv[i].(string)
if !ok {
panic(fmt.Sprintf("napcat: segment key must be string, got %T", kv[i]))
}
data[key] = kv[i+1]
}
return Segment{Type: t, Data: data}
}
// Text 构造纯文本消息段。
func Text(text string) Segment { return seg("text", "text", text) }
// Face 构造 QQ 系统表情消息段。
func Face(id int) Segment { return seg("face", "id", id) }
// Image 构造图片消息段。file 支持本地路径、http(s) URL、base64://。
func Image(file string) Segment { return seg("image", "file", file) }
// Record 构造语音消息段。
func Record(file string) Segment { return seg("record", "file", file) }
// Video 构造视频消息段。
func Video(file string) Segment { return seg("video", "file", file) }
// At 构造 @ 消息段。
func At(qq int64) Segment { return seg("at", "qq", strconv.FormatInt(qq, 10)) }
// AtAll 构造 @全体成员 消息段。
func AtAll() Segment { return seg("at", "qq", "all") }
// Reply 构造回复消息段。
func Reply(messageID int64) Segment { return seg("reply", "id", messageID) }
// Forward 构造合并转发消息段(引用一条已存在的合并转发)。
func Forward(id string) Segment { return seg("forward", "id", id) }
// NodeSeg 引用一条已有消息作为合并转发节点。
// 注意:与 api.go 里的 Node 结构体(构造转发节点参数)区分开。
func NodeSeg(messageID int64) Segment {
return seg("node", "id", messageID)
}
// CustomNode 构造伪造发送者的合并转发节点。
func CustomNode(userID int64, nickname string, content []Segment) Segment {
return seg("node",
"user_id", strconv.FormatInt(userID, 10),
"nickname", nickname,
"content", content,
)
}
// JSON 构造 JSON 卡片消息段(json/lightapp 卡片)。
func JSON(payload string) Segment { return seg("json", "data", payload) }
// Music 构造音乐分享消息段,musicType 取值 qq/163/kugou/migu/kuwo。
func Music(musicType, id string) Segment {
return seg("music", "type", musicType, "id", id)
}
// CustomMusic 构造自定义音乐分享消息段。
func CustomMusic(url, audio, title string) Segment {
return seg("music", "type", "custom", "url", url, "audio", audio, "title", title)
}
// Contact 构造好友/群推荐卡片,contactType 取值 qq/group。
func Contact(contactType, id string) Segment {
return seg("contact", "type", contactType, "id", id)
}
// Markdown 构造 markdown 消息段(NapCat 下需置于合并转发内发送)。
func Markdown(content string) Segment { return seg("markdown", "content", content) }
// Poke 构造戳一戳消息段。
func Poke(qq int64) Segment { return seg("poke", "qq", strconv.FormatInt(qq, 10)) }
// Dice 构造随机骰子消息段。
func Dice() Segment { return Segment{Type: "dice", Data: map[string]any{}} }
// RPS 构造猜拳消息段。
func RPS() Segment { return Segment{Type: "rps", Data: map[string]any{}} }
// Shake 构造窗口抖动消息段(仅私聊可用)。
func Shake() Segment { return Segment{Type: "shake", Data: map[string]any{}} }
// FileSegment 构造文件消息段(一般推荐使用 UploadGroupFile/UploadPrivateFile)。
func FileSegment(file, name string) Segment {
return seg("file", "file", file, "name", name)
}
// DecodeMessage 将事件或 API 返回中的 message 字段解码为消息段数组,
// 兼容段数组和 CQ 码字符串两种形态。
func DecodeMessage(raw json.RawMessage) ([]Segment, error) {
if len(raw) == 0 {
return nil, nil
}
trimmed := trimSpaceBytes(raw)
if len(trimmed) > 0 && trimmed[0] == '[' {
var segs []Segment
if err := json.Unmarshal(trimmed, &segs); err != nil {
return nil, fmt.Errorf("napcat: decode message segments: %w", err)
}
return segs, nil
}
var s string
if err := json.Unmarshal(trimmed, &s); err != nil {
return nil, fmt.Errorf("napcat: message is neither array nor string: %w", err)
}
return ParseCQ(s), nil
}
// SegmentsText 拼接所有 text 段的文本内容。
func SegmentsText(segs []Segment) string {
out := ""
for _, s := range segs {
if s.Type == "text" {
out += strOf(s.Data["text"])
}
}
return out
}
func trimSpaceBytes(b []byte) []byte {
start, end := 0, len(b)
for start < end && (b[start] == ' ' || b[start] == '\t' || b[start] == '\r' || b[start] == '\n') {
start++
}
for end > start && (b[end-1] == ' ' || b[end-1] == '\t' || b[end-1] == '\r' || b[end-1] == '\n') {
end--
}
return b[start:end]
}
// strOf 把 data 里的值统一转成字符串(兼容 float64/string/json.Number 等)。
func strOf(v any) string {
switch t := v.(type) {
case nil:
return ""
case string:
return t
case float64:
if t == float64(int64(t)) {
return strconv.FormatInt(int64(t), 10)
}
return strconv.FormatFloat(t, 'f', -1, 64)
case json.Number:
return t.String()
case bool:
return strconv.FormatBool(t)
default:
return fmt.Sprint(t)
}
}
// intOf 把 data 里的值尽量转成 int64(兼容 float64/string)。
func intOf(v any) int64 {
switch t := v.(type) {
case nil:
return 0
case float64:
return int64(t)
case string:
n, _ := strconv.ParseInt(t, 10, 64)
return n
case json.Number:
n, _ := t.Int64()
return n
default:
return 0
}
}