fix cr
This commit is contained in:
parent
5af7904834
commit
c8b6523542
1 changed files with 37 additions and 42 deletions
|
|
@ -5,10 +5,7 @@ import (
|
||||||
"encoding/base64"
|
"encoding/base64"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"github.com/sipeed/picoclaw/pkg/media"
|
"math"
|
||||||
"github.com/sipeed/picoclaw/pkg/utils"
|
|
||||||
"github.com/tidwall/gjson"
|
|
||||||
"math/rand"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"regexp"
|
"regexp"
|
||||||
|
|
@ -17,6 +14,10 @@ import (
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"github.com/sipeed/picoclaw/pkg/media"
|
||||||
|
"github.com/sipeed/picoclaw/pkg/utils"
|
||||||
|
"github.com/tidwall/gjson"
|
||||||
|
|
||||||
"github.com/tencent-connect/botgo"
|
"github.com/tencent-connect/botgo"
|
||||||
"github.com/tencent-connect/botgo/dto"
|
"github.com/tencent-connect/botgo/dto"
|
||||||
"github.com/tencent-connect/botgo/event"
|
"github.com/tencent-connect/botgo/event"
|
||||||
|
|
@ -36,9 +37,11 @@ const (
|
||||||
dedupInterval = 60 * time.Second
|
dedupInterval = 60 * time.Second
|
||||||
dedupMaxSize = 10000 // hard cap on dedup map entries
|
dedupMaxSize = 10000 // hard cap on dedup map entries
|
||||||
typingResend = 8 * time.Second
|
typingResend = 8 * time.Second
|
||||||
typingSeconds = 10
|
typingSeconds = 20
|
||||||
)
|
)
|
||||||
|
|
||||||
|
var emojiRegexp = regexp.MustCompile(`<[^<]*?ext="([^"]+)"[^<]*?faceType=(\d+)[^<]*?>|<[^<]*?faceType=(\d+)[^<]*?ext="([^"]+)"[^<]*?>`)
|
||||||
|
|
||||||
type QQChannel struct {
|
type QQChannel struct {
|
||||||
*channels.BaseChannel
|
*channels.BaseChannel
|
||||||
config config.QQConfig
|
config config.QQConfig
|
||||||
|
|
@ -54,8 +57,8 @@ type QQChannel struct {
|
||||||
// Passive reply: store last inbound message ID per chat.
|
// Passive reply: store last inbound message ID per chat.
|
||||||
lastMsgID sync.Map // chatID → string
|
lastMsgID sync.Map // chatID → string
|
||||||
|
|
||||||
// msg_seq: per-chat atomic counter for multi-part replies.
|
replySeq atomic.Uint32
|
||||||
msgSeqCounters sync.Map // chatID → *atomic.Uint32
|
seqLock sync.Mutex
|
||||||
|
|
||||||
// Time-based dedup replacing the unbounded map.
|
// Time-based dedup replacing the unbounded map.
|
||||||
dedup map[string]time.Time
|
dedup map[string]time.Time
|
||||||
|
|
@ -86,7 +89,6 @@ func (c *QQChannel) Start(ctx context.Context) error {
|
||||||
return fmt.Errorf("QQ app_id and app_secret not configured")
|
return fmt.Errorf("QQ app_id and app_secret not configured")
|
||||||
}
|
}
|
||||||
|
|
||||||
botgo.SetLogger(logger.NewLogger("botgo"))
|
|
||||||
logger.InfoC("qq", "Starting QQ bot (WebSocket mode)")
|
logger.InfoC("qq", "Starting QQ bot (WebSocket mode)")
|
||||||
|
|
||||||
// Reinitialize shutdown signal for clean restart.
|
// Reinitialize shutdown signal for clean restart.
|
||||||
|
|
@ -193,14 +195,13 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
|
|
||||||
chatKind := c.getChatKind(msg.ChatID)
|
chatKind := c.getChatKind(msg.ChatID)
|
||||||
textMsg, mdMsg := c.genReplyMsg(ctx, msg, chatKind)
|
textMsg, mdMsg := c.genReplyMsg(ctx, msg, chatKind)
|
||||||
|
var err error
|
||||||
for _, _v := range []dto.MessageToCreate{mdMsg, textMsg} {
|
for _, replyMsg := range []dto.MessageToCreate{mdMsg, textMsg} {
|
||||||
var err error
|
|
||||||
var replyMsgID *dto.Message
|
var replyMsgID *dto.Message
|
||||||
if chatKind == "group" {
|
if chatKind == "group" {
|
||||||
replyMsgID, err = c.api.PostGroupMessage(ctx, msg.ChatID, _v)
|
replyMsgID, err = c.api.PostGroupMessage(ctx, msg.ChatID, replyMsg)
|
||||||
} else {
|
} else {
|
||||||
replyMsgID, err = c.api.PostC2CMessage(ctx, msg.ChatID, _v)
|
replyMsgID, err = c.api.PostC2CMessage(ctx, msg.ChatID, replyMsg)
|
||||||
}
|
}
|
||||||
if err == nil {
|
if err == nil {
|
||||||
logger.InfoCF("qq", "Sent message", map[string]any{"postrsp ": replyMsgID})
|
logger.InfoCF("qq", "Sent message", map[string]any{"postrsp ": replyMsgID})
|
||||||
|
|
@ -214,7 +215,7 @@ func (c *QQChannel) Send(ctx context.Context, msg bus.OutboundMessage) error {
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *QQChannel) genReplyMsg(ctx context.Context, msg bus.OutboundMessage, chatKind string) (dto.MessageToCreate,
|
func (c *QQChannel) genReplyMsg(ctx context.Context, msg bus.OutboundMessage, chatKind string) (dto.MessageToCreate,
|
||||||
|
|
@ -241,14 +242,15 @@ func (c *QQChannel) getReplyExtInfo(ctx context.Context, chatID string) (replyID
|
||||||
replyID = msgID
|
replyID = msgID
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
// Increment msg_seq atomically for multi-part replies.
|
|
||||||
if counterVal, ok := c.msgSeqCounters.Load(chatID); ok {
|
// Attach msg_seq for active reply.
|
||||||
if counter, ok := counterVal.(*atomic.Uint32); ok {
|
c.seqLock.Lock()
|
||||||
seq = counter.Add(1)
|
defer c.seqLock.Unlock()
|
||||||
}
|
seq = c.replySeq.Add(1)
|
||||||
} else {
|
if seq > math.MaxInt32 {
|
||||||
seq = rand.Uint32()
|
c.replySeq.Store(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
return replyID, seq
|
return replyID, seq
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -449,7 +451,7 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
|
||||||
|
|
||||||
scope := channels.BuildMediaScope("qq", senderID, data.ID)
|
scope := channels.BuildMediaScope("qq", senderID, data.ID)
|
||||||
|
|
||||||
content, mediaPaths := c.decodeMesasge(context.Background(), event, (*dto.Message)(data), scope)
|
content, mediaPaths := c.decodeMessage(context.Background(), event, (*dto.Message)(data), scope)
|
||||||
if content == "" {
|
if content == "" {
|
||||||
logger.DebugC("qq", "Received empty C2C message, ignoring")
|
logger.DebugC("qq", "Received empty C2C message, ignoring")
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -464,12 +466,7 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
|
||||||
c.chatType.Store(senderID, "direct")
|
c.chatType.Store(senderID, "direct")
|
||||||
c.lastMsgID.Store(senderID, data.ID)
|
c.lastMsgID.Store(senderID, data.ID)
|
||||||
|
|
||||||
// Reset msg_seq counter for new inbound message.
|
metadata := map[string]string{}
|
||||||
c.msgSeqCounters.Store(senderID, new(atomic.Uint32))
|
|
||||||
|
|
||||||
metadata := map[string]string{
|
|
||||||
"account_id": senderID,
|
|
||||||
}
|
|
||||||
|
|
||||||
sender := bus.SenderInfo{
|
sender := bus.SenderInfo{
|
||||||
Platform: "qq",
|
Platform: "qq",
|
||||||
|
|
@ -514,7 +511,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
|
||||||
}
|
}
|
||||||
scope := channels.BuildMediaScope("qq", data.GroupID, data.ID)
|
scope := channels.BuildMediaScope("qq", data.GroupID, data.ID)
|
||||||
|
|
||||||
content, mediaPaths := c.decodeMesasge(context.Background(), event, (*dto.Message)(data), scope)
|
content, mediaPaths := c.decodeMessage(context.Background(), event, (*dto.Message)(data), scope)
|
||||||
if content == "" {
|
if content == "" {
|
||||||
logger.DebugC("qq", "Received empty group message, ignoring")
|
logger.DebugC("qq", "Received empty group message, ignoring")
|
||||||
return nil
|
return nil
|
||||||
|
|
@ -535,9 +532,9 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
|
||||||
// Store chat routing context using GroupID as chatID.
|
// Store chat routing context using GroupID as chatID.
|
||||||
c.chatType.Store(data.GroupID, "group")
|
c.chatType.Store(data.GroupID, "group")
|
||||||
c.lastMsgID.Store(data.GroupID, data.ID)
|
c.lastMsgID.Store(data.GroupID, data.ID)
|
||||||
|
|
||||||
metadata := map[string]string{
|
metadata := map[string]string{
|
||||||
"account_id": senderID,
|
"group_id": data.GroupID,
|
||||||
"group_id": data.GroupID,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sender := bus.SenderInfo{
|
sender := bus.SenderInfo{
|
||||||
|
|
@ -621,7 +618,8 @@ func (c *QQChannel) dedupJanitor() {
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *QQChannel) decodeMesasge(ctx context.Context, event *dto.WSPayload, data *dto.Message, scope string) (content string, mediaPaths []string) {
|
func (c *QQChannel) decodeMessage(ctx context.Context, event *dto.WSPayload, data *dto.Message,
|
||||||
|
scope string) (content string, mediaPaths []string) {
|
||||||
|
|
||||||
content = parseEmojiText(data.Content)
|
content = parseEmojiText(data.Content)
|
||||||
wavURL, asrReferText := getVoiceInfo(event)
|
wavURL, asrReferText := getVoiceInfo(event)
|
||||||
|
|
@ -658,9 +656,8 @@ func (c *QQChannel) decodeMesasge(ctx context.Context, event *dto.WSPayload, dat
|
||||||
}
|
}
|
||||||
|
|
||||||
// processAttachments processes all attachments in a message
|
// processAttachments processes all attachments in a message
|
||||||
func (c *QQChannel) processAttachments(ctx context.Context, attachments []MessageAttachment, scope string) ([]string, string) {
|
func (c *QQChannel) processAttachments(ctx context.Context, attachments []MessageAttachment,
|
||||||
mediaPaths := []string{}
|
scope string) (mediaPaths []string, content string) {
|
||||||
content := ""
|
|
||||||
|
|
||||||
// Helper to register a local file with the media store
|
// Helper to register a local file with the media store
|
||||||
storeMedia := func(localPath, filename string) string {
|
storeMedia := func(localPath, filename string) string {
|
||||||
|
|
@ -672,7 +669,7 @@ func (c *QQChannel) processAttachments(ctx context.Context, attachments []Messag
|
||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
ref, err := store.Store(localPath, media.MediaMeta{Filename: filename, Source: "qq"}, scope)
|
ref, err := store.Store(localPath, media.MediaMeta{Filename: filename, Source: "qq"}, scope)
|
||||||
if err != nil {
|
if err == nil {
|
||||||
logger.InfoCF("qq", "Stored media", map[string]any{
|
logger.InfoCF("qq", "Stored media", map[string]any{
|
||||||
"scope": scope,
|
"scope": scope,
|
||||||
"localPath": localPath,
|
"localPath": localPath,
|
||||||
|
|
@ -680,10 +677,10 @@ func (c *QQChannel) processAttachments(ctx context.Context, attachments []Messag
|
||||||
})
|
})
|
||||||
return ref
|
return ref
|
||||||
}
|
}
|
||||||
logger.ErrorCF("qq", "Stored media", map[string]any{
|
logger.ErrorCF("qq", "Stored media err ", map[string]any{
|
||||||
"scope": scope,
|
"scope": scope,
|
||||||
"localPath": localPath,
|
"localPath": localPath,
|
||||||
"ref": ref,
|
"err": err.Error(),
|
||||||
})
|
})
|
||||||
return localPath
|
return localPath
|
||||||
}
|
}
|
||||||
|
|
@ -810,10 +807,8 @@ func parseEmojiText(content string) string {
|
||||||
content = strings.ReplaceAll(content, "\\u003e", ">")
|
content = strings.ReplaceAll(content, "\\u003e", ">")
|
||||||
content = strings.ReplaceAll(content, `\"`, `"`)
|
content = strings.ReplaceAll(content, `\"`, `"`)
|
||||||
|
|
||||||
combinedRegexp := regexp.MustCompile(`<[^<]*?ext="([^"]+)"[^<]*?faceType=(\d+)[^<]*?>|<[^<]*?faceType=(\d+)[^<]*?ext="([^"]+)"[^<]*?>`)
|
contentParts := emojiRegexp.Split(content, -1)
|
||||||
|
matches := emojiRegexp.FindAllString(content, -1)
|
||||||
contentParts := combinedRegexp.Split(content, -1)
|
|
||||||
matches := combinedRegexp.FindAllString(content, -1)
|
|
||||||
|
|
||||||
var result strings.Builder
|
var result strings.Builder
|
||||||
for i, part := range contentParts {
|
for i, part := range contentParts {
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue