feat: Add support for sending and receiving media attachments in the Feishu channel.

This commit is contained in:
viettranx committed 2026-03-03 09:23:58 +07:00
1 parent 278a98ba74
commit 50c32a4d01
3 files changed
+88 -30

No files matched your search

+9 -2
View File
@@ -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" {
+39 -28
View File
@@ -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 ---
+40
View File
@@ -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.