fix(qq): route group replies correctly and track reply context

This commit is contained in:
westwind027 2026-02-25 13:50:49 +08:00
parent ec6da7a530
commit 0eade09809
2 changed files with 155 additions and 6 deletions

View file

@ -27,9 +27,32 @@ type QQChannel struct {
cancel context.CancelFunc
sessionManager botgo.SessionManager
processedIDs map[string]bool
lastMsgIDs map[string]string
msgSeqByChat map[string]uint32
chatKindByID map[string]string
mu sync.RWMutex
}
const (
qqChatKindDirect = "direct"
qqChatKindGroup = "group"
)
type qqC2CMessageToCreate struct {
Content string `json:"content,omitempty"`
MsgType int `json:"msg_type"`
MsgID string `json:"msg_id,omitempty"`
MsgSeq uint32 `json:"msg_seq,omitempty"`
}
func (m qqC2CMessageToCreate) GetEventID() string {
return ""
}
func (m qqC2CMessageToCreate) GetSendType() dto.SendType {
return dto.Text
}
func NewQQChannel(cfg config.QQConfig, messageBus *bus.MessageBus) (*QQChannel, error) {
base := NewBaseChannel("qq", cfg, messageBus, cfg.AllowFrom)
@ -37,6 +60,9 @@ func NewQQChannel(cfg config.QQConfig, messageBus *bus.MessageBus) (*QQChannel,
BaseChannel: base,
config: cfg,
processedIDs: make(map[string]bool),
lastMsgIDs: make(map[string]string),
msgSeqByChat: make(map[string]uint32),
chatKindByID: make(map[string]string),
}, nil
}
@ -117,15 +143,21 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
}
// construct message
msgToCreate := &dto.MessageToCreate{
Content: msg.Content,
}
msgToCreate := c.buildC2CMessage(msg.ChatID, msg.Content)
// send C2C message
_, err := c.api.PostC2CMessage(ctx, msg.ChatID, msgToCreate)
chatKind := c.resolveChatKind(msg.ChatID)
var err error
if chatKind == qqChatKindGroup {
_, err = c.api.PostGroupMessage(ctx, msg.ChatID, msgToCreate)
} else {
_, err = c.api.PostC2CMessage(ctx, msg.ChatID, msgToCreate)
}
if err != nil {
logger.ErrorCF("qq", "Failed to send C2C message", map[string]any{
logger.ErrorCF("qq", "Failed to send QQ message", map[string]any{
"error": err.Error(),
"chat": msg.ChatID,
"kind": chatKind,
})
return err
}
@ -169,6 +201,8 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
"peer_id": senderID,
}
c.recordInboundMessage(senderID, data.ID, qqChatKindDirect)
c.HandleMessage(senderID, senderID, content, []string{}, metadata)
return nil
@ -213,6 +247,8 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
"peer_id": data.GroupID,
}
c.recordInboundMessage(data.GroupID, data.ID, qqChatKindGroup)
c.HandleMessage(senderID, data.GroupID, content, []string{}, metadata)
return nil
@ -245,3 +281,47 @@ func (c *QQChannel) isDuplicate(messageID string) bool {
return false
}
func (c *QQChannel) recordInboundMessage(chatID, messageID, chatKind string) {
if chatID == "" || messageID == "" {
return
}
c.mu.Lock()
defer c.mu.Unlock()
c.lastMsgIDs[chatID] = messageID
c.msgSeqByChat[chatID] = 0
if chatKind != "" {
c.chatKindByID[chatID] = chatKind
}
}
func (c *QQChannel) buildC2CMessage(chatID, content string) *qqC2CMessageToCreate {
msg := &qqC2CMessageToCreate{
Content: content,
MsgType: int(dto.TextMsg),
}
c.mu.Lock()
defer c.mu.Unlock()
if lastMsgID := c.lastMsgIDs[chatID]; lastMsgID != "" {
c.msgSeqByChat[chatID]++
msg.MsgID = lastMsgID
msg.MsgSeq = c.msgSeqByChat[chatID]
}
return msg
}
func (c *QQChannel) resolveChatKind(chatID string) string {
c.mu.RLock()
defer c.mu.RUnlock()
if kind := c.chatKindByID[chatID]; kind != "" {
return kind
}
return qqChatKindDirect
}

69
pkg/channels/qq_test.go Normal file
View file

@ -0,0 +1,69 @@
package channels
import (
"testing"
"github.com/tencent-connect/botgo/dto"
"github.com/sipeed/picoclaw/pkg/config"
)
func TestQQChannel_RecordInboundAndBuildMessage(t *testing.T) {
channel, err := NewQQChannel(config.QQConfig{}, nil)
if err != nil {
t.Fatalf("failed to create qq channel: %v", err)
}
channel.recordInboundMessage("group-1", "msg-a", qqChatKindGroup)
if got := channel.resolveChatKind("group-1"); got != qqChatKindGroup {
t.Fatalf("expected chat kind %q, got %q", qqChatKindGroup, got)
}
first := channel.buildC2CMessage("group-1", "hello")
if first.MsgType != int(dto.TextMsg) {
t.Fatalf("expected msg_type %d, got %d", int(dto.TextMsg), first.MsgType)
}
if first.MsgID != "msg-a" {
t.Fatalf("expected msg_id msg-a, got %q", first.MsgID)
}
if first.MsgSeq != 1 {
t.Fatalf("expected first msg_seq 1, got %d", first.MsgSeq)
}
second := channel.buildC2CMessage("group-1", "hello again")
if second.MsgSeq != 2 {
t.Fatalf("expected second msg_seq 2, got %d", second.MsgSeq)
}
channel.recordInboundMessage("group-1", "msg-b", qqChatKindGroup)
third := channel.buildC2CMessage("group-1", "after new inbound")
if third.MsgID != "msg-b" {
t.Fatalf("expected latest msg_id msg-b, got %q", third.MsgID)
}
if third.MsgSeq != 1 {
t.Fatalf("expected msg_seq reset to 1 after new inbound, got %d", third.MsgSeq)
}
}
func TestQQChannel_DefaultChatKindAndMessageWithoutContext(t *testing.T) {
channel, err := NewQQChannel(config.QQConfig{}, nil)
if err != nil {
t.Fatalf("failed to create qq channel: %v", err)
}
if got := channel.resolveChatKind("unknown-chat"); got != qqChatKindDirect {
t.Fatalf("expected default chat kind %q, got %q", qqChatKindDirect, got)
}
msg := channel.buildC2CMessage("unknown-chat", "plain")
if msg.MsgType != int(dto.TextMsg) {
t.Fatalf("expected msg_type %d, got %d", int(dto.TextMsg), msg.MsgType)
}
if msg.MsgID != "" {
t.Fatalf("expected empty msg_id for chat without context, got %q", msg.MsgID)
}
if msg.MsgSeq != 0 {
t.Fatalf("expected msg_seq 0 for chat without context, got %d", msg.MsgSeq)
}
}