From e91c68ed3492761726686a8055c5377cd513538c Mon Sep 17 00:00:00 2001 From: Together Date: Sat, 21 Feb 2026 01:27:28 +0800 Subject: [PATCH 1/3] fix: remove premature media file cleanup in channel handlers Media files (photos, voice, audio, documents) downloaded by channel handlers were immediately deleted via defer when handleMessage returned. However, HandleMessage publishes to an async message bus, so the agent goroutine would attempt to access already-deleted files. Remove the eager defer cleanup from telegram, discord, slack, and line channels. Temp files in os.TempDir()/picoclaw_media/ are managed by the OS temp directory lifecycle. Fixes media/voice/document processing being non-functional across all affected channels. --- pkg/channels/discord.go | 20 ++++---------------- pkg/channels/line.go | 20 ++++---------------- pkg/channels/slack.go | 19 ++++--------------- pkg/channels/telegram.go | 21 ++++----------------- 4 files changed, 16 insertions(+), 64 deletions(-) diff --git a/pkg/channels/discord.go b/pkg/channels/discord.go index 342ddb478..deacbc7d5 100644 --- a/pkg/channels/discord.go +++ b/pkg/channels/discord.go @@ -3,7 +3,6 @@ package channels import ( "context" "fmt" - "os" "strings" "sync" "time" @@ -210,19 +209,10 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag content := m.Content content = c.stripBotMention(content) mediaPaths := make([]string, 0, len(m.Attachments)) - localFiles := make([]string, 0, len(m.Attachments)) - - // Ensure temp files are cleaned up when function returns - defer func() { - for _, file := range localFiles { - if err := os.Remove(file); err != nil { - logger.DebugCF("discord", "Failed to cleanup temp file", map[string]any{ - "file": file, - "error": err.Error(), - }) - } - } - }() + // Note: media files in os.TempDir()/picoclaw_media/ are not cleaned up here + // because HandleMessage publishes to an async message bus. The consumer + // goroutine may still need these files after this function returns. + // Temp files are managed by OS temp directory lifecycle. for _, attachment := range m.Attachments { isAudio := utils.IsAudioFile(attachment.Filename, attachment.ContentType) @@ -230,8 +220,6 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag if isAudio { localPath := c.downloadAttachment(attachment.URL, attachment.Filename) if localPath != "" { - localFiles = append(localFiles, localPath) - transcribedText := "" if c.transcriber != nil && c.transcriber.IsAvailable() { ctx, cancel := context.WithTimeout(c.getContext(), transcriptionTimeout) diff --git a/pkg/channels/line.go b/pkg/channels/line.go index 9f7d2bde0..ec7ac9e72 100644 --- a/pkg/channels/line.go +++ b/pkg/channels/line.go @@ -10,7 +10,6 @@ import ( "fmt" "io" "net/http" - "os" "strings" "sync" "time" @@ -307,18 +306,10 @@ func (c *LINEChannel) processEvent(event lineEvent) { var content string var mediaPaths []string - localFiles := []string{} - - defer func() { - for _, file := range localFiles { - if err := os.Remove(file); err != nil { - logger.DebugCF("line", "Failed to cleanup temp file", map[string]interface{}{ - "file": file, - "error": err.Error(), - }) - } - } - }() + // Note: media files in os.TempDir()/picoclaw_media/ are not cleaned up here + // because HandleMessage publishes to an async message bus. The consumer + // goroutine may still need these files after this function returns. + // Temp files are managed by OS temp directory lifecycle. switch msg.Type { case "text": @@ -330,21 +321,18 @@ func (c *LINEChannel) processEvent(event lineEvent) { case "image": localPath := c.downloadContent(msg.ID, "image.jpg") if localPath != "" { - localFiles = append(localFiles, localPath) mediaPaths = append(mediaPaths, localPath) content = "[image]" } case "audio": localPath := c.downloadContent(msg.ID, "audio.m4a") if localPath != "" { - localFiles = append(localFiles, localPath) mediaPaths = append(mediaPaths, localPath) content = "[audio]" } case "video": localPath := c.downloadContent(msg.ID, "video.mp4") if localPath != "" { - localFiles = append(localFiles, localPath) mediaPaths = append(mediaPaths, localPath) content = "[video]" } diff --git a/pkg/channels/slack.go b/pkg/channels/slack.go index 0060972ed..f7a337195 100644 --- a/pkg/channels/slack.go +++ b/pkg/channels/slack.go @@ -3,7 +3,6 @@ package channels import ( "context" "fmt" - "os" "strings" "sync" "time" @@ -232,19 +231,10 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) { content = c.stripBotMention(content) var mediaPaths []string - localFiles := []string{} // 跟踪需要清理的本地文件 - - // 确保临时文件在函数返回时被清理 - defer func() { - for _, file := range localFiles { - if err := os.Remove(file); err != nil { - logger.DebugCF("slack", "Failed to cleanup temp file", map[string]interface{}{ - "file": file, - "error": err.Error(), - }) - } - } - }() + // Note: media files in os.TempDir()/picoclaw_media/ are not cleaned up here + // because HandleMessage publishes to an async message bus. The consumer + // goroutine may still need these files after this function returns. + // Temp files are managed by OS temp directory lifecycle. if ev.Message != nil && len(ev.Message.Files) > 0 { for _, file := range ev.Message.Files { @@ -252,7 +242,6 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) { if localPath == "" { continue } - localFiles = append(localFiles, localPath) mediaPaths = append(mediaPaths, localPath) if utils.IsAudioFile(file.Name, file.Mimetype) && c.transcriber != nil && c.transcriber.IsAvailable() { diff --git a/pkg/channels/telegram.go b/pkg/channels/telegram.go index 20bbf6830..c64fb7126 100644 --- a/pkg/channels/telegram.go +++ b/pkg/channels/telegram.go @@ -221,19 +221,10 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes content := "" mediaPaths := []string{} - localFiles := []string{} // 跟踪需要清理的本地文件 - - // 确保临时文件在函数返回时被清理 - defer func() { - for _, file := range localFiles { - if err := os.Remove(file); err != nil { - logger.DebugCF("telegram", "Failed to cleanup temp file", map[string]interface{}{ - "file": file, - "error": err.Error(), - }) - } - } - }() + // Note: media files in os.TempDir()/picoclaw_media/ are not cleaned up here + // because HandleMessage publishes to an async message bus. The consumer + // goroutine may still need these files after this function returns. + // Temp files are managed by OS temp directory lifecycle. if message.Text != "" { content += message.Text @@ -250,7 +241,6 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes photo := message.Photo[len(message.Photo)-1] photoPath := c.downloadPhoto(ctx, photo.FileID) if photoPath != "" { - localFiles = append(localFiles, photoPath) mediaPaths = append(mediaPaths, photoPath) if content != "" { content += "\n" @@ -262,7 +252,6 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes if message.Voice != nil { voicePath := c.downloadFile(ctx, message.Voice.FileID, ".ogg") if voicePath != "" { - localFiles = append(localFiles, voicePath) mediaPaths = append(mediaPaths, voicePath) transcribedText := "" @@ -297,7 +286,6 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes if message.Audio != nil { audioPath := c.downloadFile(ctx, message.Audio.FileID, ".mp3") if audioPath != "" { - localFiles = append(localFiles, audioPath) mediaPaths = append(mediaPaths, audioPath) if content != "" { content += "\n" @@ -309,7 +297,6 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes if message.Document != nil { docPath := c.downloadFile(ctx, message.Document.FileID, "") if docPath != "" { - localFiles = append(localFiles, docPath) mediaPaths = append(mediaPaths, docPath) if content != "" { content += "\n" From 7836f21621f364cdff7da6ddfcce578ad43a66b0 Mon Sep 17 00:00:00 2001 From: Together Date: Sat, 21 Feb 2026 01:36:14 +0800 Subject: [PATCH 2/3] fix: remove premature cleanup from onebot, fix discord audio file leak - Remove defer cleanup of LocalFiles in onebot channel handler, same async race condition as the other channels. - Remove the now-unused LocalFiles field from parseMessageResult. - Add downloaded audio files to mediaPaths in discord channel so they are tracked (previously only tracked via the now-removed localFiles). --- pkg/channels/discord.go | 1 + pkg/channels/onebot.go | 31 ++++++++----------------------- 2 files changed, 9 insertions(+), 23 deletions(-) diff --git a/pkg/channels/discord.go b/pkg/channels/discord.go index deacbc7d5..dc92b6256 100644 --- a/pkg/channels/discord.go +++ b/pkg/channels/discord.go @@ -220,6 +220,7 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag if isAudio { localPath := c.downloadAttachment(attachment.URL, attachment.Filename) if localPath != "" { + mediaPaths = append(mediaPaths, localPath) transcribedText := "" if c.transcriber != nil && c.transcriber.IsAvailable() { ctx, cancel := context.WithTimeout(c.getContext(), transcriptionTimeout) diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go index 06186f783..726970fdf 100644 --- a/pkg/channels/onebot.go +++ b/pkg/channels/onebot.go @@ -4,7 +4,6 @@ import ( "context" "encoding/json" "fmt" - "os" "strconv" "strings" "sync" @@ -570,9 +569,8 @@ func parseJSONString(raw json.RawMessage) string { type parseMessageResult struct { Text string IsBotMentioned bool - Media []string - LocalFiles []string - ReplyTo string + Media []string + ReplyTo string } func (c *OneBotChannel) parseMessageSegments(raw json.RawMessage, selfID int64) parseMessageResult { @@ -603,7 +601,6 @@ func (c *OneBotChannel) parseMessageSegments(raw json.RawMessage, selfID int64) mentioned := false selfIDStr := strconv.FormatInt(selfID, 10) var media []string - var localFiles []string var replyTo string for _, seg := range segments { @@ -642,7 +639,6 @@ func (c *OneBotChannel) parseMessageSegments(raw json.RawMessage, selfID int64) }) if localPath != "" { media = append(media, localPath) - localFiles = append(localFiles, localPath) textParts = append(textParts, fmt.Sprintf("[%s]", segType)) } } @@ -656,7 +652,6 @@ func (c *OneBotChannel) parseMessageSegments(raw json.RawMessage, selfID int64) LoggerPrefix: "onebot", }) if localPath != "" { - localFiles = append(localFiles, localPath) if c.transcriber != nil && c.transcriber.IsAvailable() { tctx, tcancel := context.WithTimeout(c.ctx, 30*time.Second) result, err := c.transcriber.Transcribe(tctx, localPath) @@ -702,9 +697,8 @@ func (c *OneBotChannel) parseMessageSegments(raw json.RawMessage, selfID int64) return parseMessageResult{ Text: strings.TrimSpace(strings.Join(textParts, "")), IsBotMentioned: mentioned, - Media: media, - LocalFiles: localFiles, - ReplyTo: replyTo, + Media: media, + ReplyTo: replyTo, } } @@ -824,19 +818,10 @@ func (c *OneBotChannel) handleMessage(raw *oneBotRawEvent) { } } - // Clean up temp files when done - if len(parsed.LocalFiles) > 0 { - defer func() { - for _, f := range parsed.LocalFiles { - if err := os.Remove(f); err != nil { - logger.DebugCF("onebot", "Failed to remove temp file", map[string]interface{}{ - "path": f, - "error": err.Error(), - }) - } - } - }() - } + // Note: media files in os.TempDir()/picoclaw_media/ are not cleaned up here + // because HandleMessage publishes to an async message bus. The consumer + // goroutine may still need these files after this function returns. + // Temp files are managed by OS temp directory lifecycle. if c.isDuplicate(messageID) { logger.DebugCF("onebot", "Duplicate message, skipping", map[string]interface{}{ From 56a03afb5255ad6a321a3f2fb4c7d0da9b266a9f Mon Sep 17 00:00:00 2001 From: Together Date: Sat, 21 Feb 2026 12:44:40 +0800 Subject: [PATCH 3/3] style: fix struct field alignment per gofmt --- pkg/channels/onebot.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/pkg/channels/onebot.go b/pkg/channels/onebot.go index 726970fdf..feeb930af 100644 --- a/pkg/channels/onebot.go +++ b/pkg/channels/onebot.go @@ -569,8 +569,8 @@ func parseJSONString(raw json.RawMessage) string { type parseMessageResult struct { Text string IsBotMentioned bool - Media []string - ReplyTo string + Media []string + ReplyTo string } func (c *OneBotChannel) parseMessageSegments(raw json.RawMessage, selfID int64) parseMessageResult { @@ -697,8 +697,8 @@ func (c *OneBotChannel) parseMessageSegments(raw json.RawMessage, selfID int64) return parseMessageResult{ Text: strings.TrimSpace(strings.Join(textParts, "")), IsBotMentioned: mentioned, - Media: media, - ReplyTo: replyTo, + Media: media, + ReplyTo: replyTo, } }