diff --git a/pkg/channels/qq.go b/pkg/channels/qq.go index b10776db6..acd51a96e 100644 --- a/pkg/channels/qq.go +++ b/pkg/channels/qq.go @@ -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 +} diff --git a/pkg/channels/qq_test.go b/pkg/channels/qq_test.go new file mode 100644 index 000000000..bc7d5f1bf --- /dev/null +++ b/pkg/channels/qq_test.go @@ -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) + } +}