From 50c32a4d018d665f5c4f4fe7df266d804cdcd8cf Mon Sep 17 00:00:00 2001 From: viettranx Date: Tue, 3 Mar 2026 09:23:58 +0700 Subject: [PATCH] feat: Add support for sending and receiving media attachments in the Feishu channel. --- internal/channels/feishu/bot.go | 11 ++++- internal/channels/feishu/feishu.go | 67 +++++++++++++++++------------- internal/channels/feishu/media.go | 40 ++++++++++++++++++ 3 files changed, 88 insertions(+), 30 deletions(-) diff --git a/internal/channels/feishu/bot.go b/internal/channels/feishu/bot.go index b13fece2..355eedb3 100644 --- a/internal/channels/feishu/bot.go +++ b/internal/channels/feishu/bot.go @@ -145,8 +145,15 @@ func (c *Channel) handleMessageEvent(ctx context.Context, event *MessageEvent) { } } - // 10. Publish to bus - c.HandleMessage(mc.SenderID, chatID, content, nil, metadata, peerKind) + // 10. Resolve inbound media (image, file, audio, video, sticker) + var mediaPaths []string + switch mc.ContentType { + case "image", "file", "audio", "video", "sticker": + mediaPaths = c.resolveMediaFromMessage(ctx, mc.MessageID, mc.ContentType, msg.Content) + } + + // 11. Publish to bus + c.HandleMessage(mc.SenderID, chatID, content, mediaPaths, metadata, peerKind) // Clear pending history after sending to agent. if mc.ChatType == "group" { diff --git a/internal/channels/feishu/feishu.go b/internal/channels/feishu/feishu.go index 69cb24b1..91c65c89 100644 --- a/internal/channels/feishu/feishu.go +++ b/internal/channels/feishu/feishu.go @@ -136,39 +136,50 @@ func (c *Channel) Send(ctx context.Context, msg bus.OutboundMessage) error { return fmt.Errorf("empty chat ID for feishu send") } - text := msg.Content - if text == "" { - return nil - } - - // Resolve render mode - renderMode := c.cfg.RenderMode - if renderMode == "" { - renderMode = "auto" - } - - useCard := false - switch renderMode { - case "card": - useCard = true - case "auto": - useCard = shouldUseCard(text) - } - - chunkLimit := c.cfg.TextChunkLimit - if chunkLimit <= 0 { - chunkLimit = defaultTextChunkLimit - } - // Determine receive_id_type receiveIDType := resolveReceiveIDType(chatID) - // Send as card or text - if useCard { - return c.sendMarkdownCard(ctx, chatID, receiveIDType, text, nil) + // Send text content + text := msg.Content + if text != "" { + // Resolve render mode + renderMode := c.cfg.RenderMode + if renderMode == "" { + renderMode = "auto" + } + + useCard := false + switch renderMode { + case "card": + useCard = true + case "auto": + useCard = shouldUseCard(text) + } + + chunkLimit := c.cfg.TextChunkLimit + if chunkLimit <= 0 { + chunkLimit = defaultTextChunkLimit + } + + if useCard { + if err := c.sendMarkdownCard(ctx, chatID, receiveIDType, text, nil); err != nil { + return err + } + } else { + if err := c.sendChunkedText(ctx, chatID, receiveIDType, text, chunkLimit); err != nil { + return err + } + } } - return c.sendChunkedText(ctx, chatID, receiveIDType, text, chunkLimit) + // Send media attachments + for _, media := range msg.Media { + if err := c.sendMediaAttachment(ctx, chatID, receiveIDType, media); err != nil { + slog.Warn("feishu send media failed", "url", media.URL, "error", err) + } + } + + return nil } // --- Connection modes --- diff --git a/internal/channels/feishu/media.go b/internal/channels/feishu/media.go index 736572a9..5a26f103 100644 --- a/internal/channels/feishu/media.go +++ b/internal/channels/feishu/media.go @@ -9,6 +9,8 @@ import ( "path/filepath" "strings" "time" + + "github.com/nextlevelbuilder/goclaw/internal/bus" ) // --- Image download --- @@ -65,6 +67,44 @@ func (c *Channel) sendFile(ctx context.Context, chatID, receiveIDType, fileKey, return nil } +// --- Outbound media --- + +// sendMediaAttachment uploads and sends a media attachment (image or file). +func (c *Channel) sendMediaAttachment(ctx context.Context, chatID, receiveIDType string, media bus.MediaAttachment) error { + filePath := media.URL + if filePath == "" { + return nil + } + + f, err := os.Open(filePath) + if err != nil { + return fmt.Errorf("open media file %s: %w", filePath, err) + } + defer f.Close() + + ct := media.ContentType + if isImageContentType(ct) { + imageKey, err := c.uploadImage(ctx, f) + if err != nil { + return fmt.Errorf("upload image: %w", err) + } + return c.sendImage(ctx, chatID, receiveIDType, imageKey) + } + + // Everything else: upload as file + fileName := filepath.Base(filePath) + fileType := detectFileType(fileName) + fileKey, err := c.uploadFile(ctx, f, fileName, fileType, 0) + if err != nil { + return fmt.Errorf("upload file: %w", err) + } + return c.sendFile(ctx, chatID, receiveIDType, fileKey, "file") +} + +func isImageContentType(ct string) bool { + return strings.HasPrefix(ct, "image/") || ct == "image" +} + // --- Media helpers --- // saveMediaToTemp writes media bytes to a temp file and returns the path.