feat(channel/qq): optimize message sending, seq generation, and message parsing

1. Define `kindType` as constants.
2. Set `seq` generation to global incremental, simplifying `seq` handling. (QQ platform validation rule is `chatID + replyMsgID + seq`, and `seq` does not affect display order.)
3. Increase API timeout from 5 seconds to 20 seconds to reduce send timeouts.
4. Prioritize sending messages using Markdown; fall back to plain text if Markdown sending fails. Also remove the `QQConfig.SendMarkdown` configuration option.
5. Restrict `InputNotify` to be sent only for direct messages.
6. Adjust message parsing to support emoji parsing and utilize the platform-provided ASR (Automatic Speech Recognition) content.
This commit is contained in:
aishannon 2026-04-03 00:31:40 +08:00
parent 0efe8bb444
commit 4ce07362b0
2 changed files with 11 additions and 12 deletions

View file

@ -643,7 +643,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
} }
if !c.IsAllowedSender(sender) { if !c.IsAllowedSender(sender) {
logger.Infof("qq", "Received group message from unauthorized sender", map[string]any{ logger.InfoCF("qq", "Received group message from unauthorized sender", map[string]any{
"sender": sender, "sender": sender,
}) })
return nil return nil
@ -1027,6 +1027,9 @@ func sanitizeURLs(text string) string {
} }
func getVoiceInfo(event *dto.WSPayload) (string, string) { func getVoiceInfo(event *dto.WSPayload) (string, string) {
if event == nil {
return "", ""
}
_raw, err := json.Marshal(event.Data) _raw, err := json.Marshal(event.Data)
if err != nil { if err != nil {
logger.ErrorCF("qq", "Failed to marshal event data", map[string]any{ logger.ErrorCF("qq", "Failed to marshal event data", map[string]any{

View file

@ -3,13 +3,11 @@ package qq
import ( import (
"bytes" "bytes"
"context" "context"
"encoding/base64"
"encoding/binary" "encoding/binary"
"encoding/json" "encoding/json"
"errors" "errors"
"os" "os"
"strings" "strings"
"sync/atomic"
"testing" "testing"
"time" "time"
@ -207,7 +205,6 @@ func TestSendMedia_UploadsLocalFileAsBase64(t *testing.T) {
ch.SetMediaStore(store) ch.SetMediaStore(store)
ch.chatType.Store("group-1", "group") ch.chatType.Store("group-1", "group")
ch.lastMsgID.Store("group-1", "msg-1") ch.lastMsgID.Store("group-1", "msg-1")
ch.msgSeqCounters.Store("group-1", new(atomic.Uint64))
_, err = ch.SendMedia(context.Background(), bus.OutboundMediaMessage{ _, err = ch.SendMedia(context.Background(), bus.OutboundMediaMessage{
ChatID: "group-1", ChatID: "group-1",
@ -234,9 +231,8 @@ func TestSendMedia_UploadsLocalFileAsBase64(t *testing.T) {
if upload.body.URL != "" { if upload.body.URL != "" {
t.Fatalf("upload URL = %q, want empty", upload.body.URL) t.Fatalf("upload URL = %q, want empty", upload.body.URL)
} }
wantBase64 := base64.StdEncoding.EncodeToString(content) if !bytes.Equal(upload.body.FileData, content) {
if upload.body.FileData != wantBase64 { t.Fatalf("upload file_data = %q, want %q", upload.body.FileData, content)
t.Fatalf("upload file_data = %q, want %q", upload.body.FileData, wantBase64)
} }
if upload.body.FileType != 1 { if upload.body.FileType != 1 {
t.Fatalf("upload file_type = %d, want 1", upload.body.FileType) t.Fatalf("upload file_type = %d, want 1", upload.body.FileType)
@ -415,7 +411,7 @@ func TestSendMedia_UsesRemoteURLUploadForC2C(t *testing.T) {
ctx: context.Background(), ctx: context.Background(),
} }
ch.SetRunning(true) ch.SetRunning(true)
ch.chatType.Store("user-1", "direct") ch.chatType.Store("user-1", kindDirect)
_, err := ch.SendMedia(context.Background(), bus.OutboundMediaMessage{ _, err := ch.SendMedia(context.Background(), bus.OutboundMediaMessage{
ChatID: "user-1", ChatID: "user-1",
@ -438,7 +434,7 @@ func TestSendMedia_UsesRemoteURLUploadForC2C(t *testing.T) {
if upload.body.URL != "https://cdn.example.com/report.pdf" { if upload.body.URL != "https://cdn.example.com/report.pdf" {
t.Fatalf("upload URL = %q", upload.body.URL) t.Fatalf("upload URL = %q", upload.body.URL)
} }
if upload.body.FileData != "" { if len(upload.body.FileData) > 0 {
t.Fatalf("upload file_data = %q, want empty", upload.body.FileData) t.Fatalf("upload file_data = %q, want empty", upload.body.FileData)
} }
if upload.body.FileType != 4 { if upload.body.FileType != 4 {
@ -511,7 +507,7 @@ func TestSendMedia_LocalFileUploadIncludesStoredFilename(t *testing.T) {
if upload.body.FileName != "report.pdf" { if upload.body.FileName != "report.pdf" {
t.Fatalf("upload file_name = %q, want report.pdf", upload.body.FileName) t.Fatalf("upload file_name = %q, want report.pdf", upload.body.FileName)
} }
if upload.body.FileData == "" { if len(upload.body.FileData) == 0 {
t.Fatal("upload file_data = empty, want base64 payload") t.Fatal("upload file_data = empty, want base64 payload")
} }
} }
@ -606,7 +602,7 @@ type fakeQQAPI struct {
type fakeTransportCall struct { type fakeTransportCall struct {
method string method string
url string url string
body qqMediaUpload body RichMediaMessage
} }
func (f *fakeQQAPI) WS( func (f *fakeQQAPI) WS(
@ -638,7 +634,7 @@ func (f *fakeQQAPI) PostC2CMessage(
} }
func (f *fakeQQAPI) Transport(_ context.Context, method, url string, body any) ([]byte, error) { func (f *fakeQQAPI) Transport(_ context.Context, method, url string, body any) ([]byte, error) {
upload, ok := body.(*qqMediaUpload) upload, ok := body.(*RichMediaMessage)
if !ok { if !ok {
return nil, errors.New("unexpected transport body type") return nil, errors.New("unexpected transport body type")
} }