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:
parent
cd0d708ba0
commit
36991c5330
3 changed files with 19 additions and 87 deletions
|
|
@ -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)
|
||||||
|
|
|
||||||
|
|
@ -402,7 +402,6 @@ type QQConfig struct {
|
||||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty" yaml:"-"`
|
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty" yaml:"-"`
|
||||||
MaxMessageLength int `json:"max_message_length" yaml:"-" env:"PICOCLAW_CHANNELS_QQ_MAX_MESSAGE_LENGTH"`
|
MaxMessageLength int `json:"max_message_length" yaml:"-" env:"PICOCLAW_CHANNELS_QQ_MAX_MESSAGE_LENGTH"`
|
||||||
MaxBase64FileSizeMiB int64 `json:"max_base64_file_size_mib" yaml:"-" env:"PICOCLAW_CHANNELS_QQ_MAX_BASE64_FILE_SIZE_MIB"`
|
MaxBase64FileSizeMiB int64 `json:"max_base64_file_size_mib" yaml:"-" env:"PICOCLAW_CHANNELS_QQ_MAX_BASE64_FILE_SIZE_MIB"`
|
||||||
SendMarkdown bool `json:"send_markdown" yaml:"-" env:"PICOCLAW_CHANNELS_QQ_SEND_MARKDOWN"`
|
|
||||||
ReasoningChannelID string `json:"reasoning_channel_id" yaml:"-" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"`
|
ReasoningChannelID string `json:"reasoning_channel_id" yaml:"-" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
||||||
|
|
@ -145,7 +145,6 @@ type qqConfigV0 struct {
|
||||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||||
MaxMessageLength int `json:"max_message_length" env:"PICOCLAW_CHANNELS_QQ_MAX_MESSAGE_LENGTH"`
|
MaxMessageLength int `json:"max_message_length" env:"PICOCLAW_CHANNELS_QQ_MAX_MESSAGE_LENGTH"`
|
||||||
MaxBase64FileSizeMiB int64 `json:"max_base64_file_size_mib" env:"PICOCLAW_CHANNELS_QQ_MAX_BASE64_FILE_SIZE_MIB"`
|
MaxBase64FileSizeMiB int64 `json:"max_base64_file_size_mib" env:"PICOCLAW_CHANNELS_QQ_MAX_BASE64_FILE_SIZE_MIB"`
|
||||||
SendMarkdown bool `json:"send_markdown" env:"PICOCLAW_CHANNELS_QQ_SEND_MARKDOWN"`
|
|
||||||
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"`
|
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_QQ_REASONING_CHANNEL_ID"`
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
@ -157,7 +156,6 @@ func (v *qqConfigV0) ToQQConfig() QQConfig {
|
||||||
GroupTrigger: v.GroupTrigger,
|
GroupTrigger: v.GroupTrigger,
|
||||||
MaxMessageLength: v.MaxMessageLength,
|
MaxMessageLength: v.MaxMessageLength,
|
||||||
MaxBase64FileSizeMiB: v.MaxBase64FileSizeMiB,
|
MaxBase64FileSizeMiB: v.MaxBase64FileSizeMiB,
|
||||||
SendMarkdown: v.SendMarkdown,
|
|
||||||
ReasoningChannelID: v.ReasoningChannelID,
|
ReasoningChannelID: v.ReasoningChannelID,
|
||||||
AppSecret: *NewSecureString(v.AppSecret),
|
AppSecret: *NewSecureString(v.AppSecret),
|
||||||
}
|
}
|
||||||
|
|
|
||||||
Loading…
Add table
Reference in a new issue