package snowluma 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 字段,可链式调用: // // snowluma.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("snowluma: 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 消息段(SnowLuma 下需置于合并转发内发送)。 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("snowluma: decode message segments: %w", err) } return segs, nil } var s string if err := json.Unmarshal(trimmed, &s); err != nil { return nil, fmt.Errorf("snowluma: 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 } }