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-03-29 16:54:59 +08:00
parent 861f37b7c6
commit 42838138b0

View file

@ -328,7 +328,6 @@ func (c *QQChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage)
return channels.ErrNotRunning return channels.ErrNotRunning
} }
chatKind := c.getChatKind(msg.ChatID) chatKind := c.getChatKind(msg.ChatID)
var err error
for _, part := range msg.Parts { for _, part := range msg.Parts {
fileInfo, err := c.uploadMedia(ctx, chatKind, msg.ChatID, part) fileInfo, err := c.uploadMedia(ctx, chatKind, msg.ChatID, part)
if err != nil { if err != nil {
@ -349,10 +348,10 @@ func (c *QQChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage)
"chat_id": msg.ChatID, "chat_id": msg.ChatID,
"error": err.Error(), "error": err.Error(),
}) })
continue return fmt.Errorf("qq send media: %w", channels.ErrTemporary)
} }
} }
return err return nil
} }
func (c *QQChannel) uploadMedia(ctx context.Context, func (c *QQChannel) uploadMedia(ctx context.Context,
@ -582,8 +581,7 @@ func (c *QQChannel) handleC2CMessage() event.C2CMessageEventHandler {
return nil return nil
} }
scope := channels.BuildMediaScope("qq", senderID, data.ID) content, mediaPaths := c.decodeMessage(context.Background(), senderID, event, (*dto.Message)(data))
content, mediaPaths := c.decodeMessage(context.Background(), event, (*dto.Message)(data), scope)
if content == "" && len(mediaPaths) == 0 { if content == "" && len(mediaPaths) == 0 {
logger.DebugC("qq", "Received empty C2C message with no content/attachments, ignoring") logger.DebugC("qq", "Received empty C2C message with no content/attachments, ignoring")
return nil return nil
@ -648,8 +646,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
return nil return nil
} }
scope := channels.BuildMediaScope("qq", data.GroupID, data.ID) content, mediaPaths := c.decodeMessage(context.Background(), senderID, event, (*dto.Message)(data))
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
@ -697,7 +694,7 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
} }
func (c *QQChannel) extractInboundAttachments(chatID, messageID string, func (c *QQChannel) extractInboundAttachments(chatID, messageID string,
attachments []*dto.MessageAttachment) ([]string, []string) { attachments []MessageAttachment) ([]string, []string) {
if len(attachments) == 0 { if len(attachments) == 0 {
return nil, nil return nil, nil
@ -707,7 +704,7 @@ func (c *QQChannel) extractInboundAttachments(chatID, messageID string,
mediaPaths := make([]string, 0, len(attachments)) mediaPaths := make([]string, 0, len(attachments))
notes := make([]string, 0, len(attachments)) notes := make([]string, 0, len(attachments))
storeMedia := func(localPath string, attachment *dto.MessageAttachment) string { storeMedia := func(localPath string, attachment MessageAttachment) string {
if store := c.GetMediaStore(); store != nil { if store := c.GetMediaStore(); store != nil {
ref, err := store.Store(localPath, media.MediaMeta{ ref, err := store.Store(localPath, media.MediaMeta{
Filename: qqAttachmentFilename(attachment), Filename: qqAttachmentFilename(attachment),
@ -723,17 +720,12 @@ func (c *QQChannel) extractInboundAttachments(chatID, messageID string,
} }
for _, attachment := range attachments { for _, attachment := range attachments {
if attachment == nil {
continue
}
filename := qqAttachmentFilename(attachment) filename := qqAttachmentFilename(attachment)
if localPath := c.downloadAttachment(attachment.URL, filename); localPath != "" { if localPath := c.downloadAttachment(attachment.URL, filename); localPath != "" {
mediaPaths = append(mediaPaths, storeMedia(localPath, attachment)) mediaPaths = append(mediaPaths, storeMedia(localPath, attachment))
} else if attachment.URL != "" { } else if attachment.URL != "" {
mediaPaths = append(mediaPaths, attachment.URL) mediaPaths = append(mediaPaths, attachment.URL)
} }
notes = append(notes, qqAttachmentNote(attachment)) notes = append(notes, qqAttachmentNote(attachment))
} }
@ -776,10 +768,7 @@ func (c *QQChannel) downloadHeaders() map[string]string {
return headers return headers
} }
func qqAttachmentFilename(attachment *dto.MessageAttachment) string { func qqAttachmentFilename(attachment MessageAttachment) string {
if attachment == nil {
return "attachment"
}
if attachment.FileName != "" { if attachment.FileName != "" {
return attachment.FileName return attachment.FileName
} }
@ -803,11 +792,7 @@ func qqAttachmentFilename(attachment *dto.MessageAttachment) string {
} }
} }
func qqAttachmentKind(attachment *dto.MessageAttachment) string { func qqAttachmentKind(attachment MessageAttachment) string {
if attachment == nil {
return "file"
}
contentType := strings.ToLower(attachment.ContentType) contentType := strings.ToLower(attachment.ContentType)
filename := strings.ToLower(attachment.FileName) filename := strings.ToLower(attachment.FileName)
@ -832,7 +817,7 @@ func qqAttachmentKind(attachment *dto.MessageAttachment) string {
} }
} }
func qqAttachmentNote(attachment *dto.MessageAttachment) string { func qqAttachmentNote(attachment MessageAttachment) string {
filename := qqAttachmentFilename(attachment) filename := qqAttachmentFilename(attachment)
switch qqAttachmentKind(attachment) { switch qqAttachmentKind(attachment) {
@ -841,6 +826,9 @@ func qqAttachmentNote(attachment *dto.MessageAttachment) string {
case "audio": case "audio":
return fmt.Sprintf("[audio: %s]", filename) return fmt.Sprintf("[audio: %s]", filename)
case "video": case "video":
if attachment.AsrReferText != "" {
return fmt.Sprintf("[video: %s]", attachment.AsrReferText)
}
return fmt.Sprintf("[video: %s]", filename) return fmt.Sprintf("[video: %s]", filename)
default: default:
return fmt.Sprintf("[file: %s]", filename) return fmt.Sprintf("[file: %s]", filename)
@ -945,10 +933,9 @@ func (c *QQChannel) getReplyExtInfo(ctx context.Context, chatID string) (replyID
return replyID, seq return replyID, seq
} }
func (c *QQChannel) decodeMessage(ctx context.Context, event *dto.WSPayload, data *dto.Message, func (c *QQChannel) decodeMessage(ctx context.Context, chatID string, event *dto.WSPayload,
scope string) (content string, mediaPaths []string) { data *dto.Message) (string, []string) {
content := parseEmojiText(data.Content)
content = parseEmojiText(data.Content)
wavURL, asrReferText := getVoiceInfo(event) wavURL, asrReferText := getVoiceInfo(event)
var attachments []MessageAttachment var attachments []MessageAttachment
for _, att := range data.Attachments { for _, att := range data.Attachments {
@ -967,63 +954,11 @@ func (c *QQChannel) decodeMessage(ctx context.Context, event *dto.WSPayload, dat
FileName: att.FileName, FileName: att.FileName,
}) })
} }
processedPaths, attachmentContent := c.processAttachments(ctx, attachments, scope) processedPaths, attachmentContents := c.extractInboundAttachments(chatID, data.ID, attachments)
mediaPaths = processedPaths for _, note := range attachmentContents {
if content != "" { content = appendContent(content, note)
content += "\n"
} }
content += attachmentContent return content, processedPaths
return content, mediaPaths
}
// processAttachments processes all attachments in a message
func (c *QQChannel) processAttachments(ctx context.Context, attachments []MessageAttachment,
scope string) (mediaPaths []string, content string) {
// Helper to register a local file with the media store
storeMedia := func(localPath, filename string) string {
store := c.GetMediaStore()
if store == nil {
logger.ErrorCF("qq", "media store is nil", map[string]any{
"scope": scope,
})
return ""
}
ref, err := store.Store(localPath, media.MediaMeta{Filename: filename, Source: "qq"}, scope)
if err == nil {
logger.InfoCF("qq", "Stored media", map[string]any{
"scope": scope,
"localPath": localPath,
"filename": filename,
})
return ref
}
logger.ErrorCF("qq", "Stored media err ", map[string]any{
"scope": scope,
"localPath": localPath,
"err": err.Error(),
})
return localPath
}
for _, attachment := range attachments {
attachmentType := c.getAttachmentType(attachment)
ref := attachment.URL
localPath := c.downloadAttachment(attachment.URL, attachment.FileName)
if localPath != "" {
if ref = storeMedia(localPath, attachment.FileName); ref == "" {
ref = localPath
}
}
if attachmentType == "audio" && attachment.AsrReferText != "" {
content = appendContent(content, fmt.Sprintf("[audio: %s]", attachment.AsrReferText))
} else {
content = appendContent(content, fmt.Sprintf("[%v: %s]", attachmentType, ref))
}
mediaPaths = append(mediaPaths, ref)
}
return mediaPaths, content
} }
// getAttachmentType determines the type of attachment (image, audio, video, file) // getAttachmentType determines the type of attachment (image, audio, video, file)