mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 14:17:40 +00:00
fix(channels): extract and download images from Lark post messages
Post messages with embedded img tags were only producing [image] placeholder text — the actual image_key was discarded and never downloaded. Now extractPostImageKeys() collects image keys from post JSON, and resolvePostImages() downloads them via the existing Lark resource API. Also handles reply-to-post with images. Refactored: shared resolvePostElements() helper for language variant lookup, mediaMaxBytes() helper, and dedup via seen map.
This commit is contained in:
1 parent
9aaec59527
commit
072d98a856
3 files changed
+100
-14
No files matched your search
@@ -182,6 +182,10 @@ func (c *Channel) handleMessageEvent(ctx context.Context, event *MessageEvent) {
|
||||
switch mc.ContentType {
|
||||
case "image", "file", "audio", "video", "sticker":
|
||||
mediaList = append(mediaList, c.resolveMediaFromMessage(ctx, mc.MessageID, mc.ContentType, msg.Content)...)
|
||||
case "post":
|
||||
if imageKeys := extractPostImageKeys(msg.Content); len(imageKeys) > 0 {
|
||||
mediaList = append(mediaList, c.resolvePostImages(ctx, mc.MessageID, imageKeys)...)
|
||||
}
|
||||
}
|
||||
|
||||
// 11. Process media: STT transcription, document extraction, build tags
|
||||
@@ -305,19 +309,23 @@ func (c *Channel) fetchReplyContext(ctx context.Context, parentID string) (strin
|
||||
replyCtx = fmt.Sprintf("[Replying to %s]\n%s\n[/Replying]", senderName, body)
|
||||
}
|
||||
|
||||
// Download media from parent message (image, file, audio, video, sticker).
|
||||
// Download media from parent message (image, file, audio, video, sticker, post).
|
||||
var replyMedia []media.MediaInfo
|
||||
switch item.MsgType {
|
||||
case "image", "file", "audio", "video", "sticker":
|
||||
replyMedia = c.resolveMediaFromMessage(ctx, parentID, item.MsgType, item.Body.Content)
|
||||
for i := range replyMedia {
|
||||
replyMedia[i].FromReply = true
|
||||
}
|
||||
if len(replyMedia) > 0 {
|
||||
slog.Debug("feishu: resolved media from replied message",
|
||||
"parent_id", parentID, "media_count", len(replyMedia))
|
||||
case "post":
|
||||
if imageKeys := extractPostImageKeys(item.Body.Content); len(imageKeys) > 0 {
|
||||
replyMedia = c.resolvePostImages(ctx, parentID, imageKeys)
|
||||
}
|
||||
}
|
||||
for i := range replyMedia {
|
||||
replyMedia[i].FromReply = true
|
||||
}
|
||||
if len(replyMedia) > 0 {
|
||||
slog.Debug("feishu: resolved media from replied message",
|
||||
"parent_id", parentID, "media_count", len(replyMedia))
|
||||
}
|
||||
|
||||
return replyCtx, replyMedia
|
||||
}
|
||||
@@ -99,10 +99,12 @@ func parseMessageContent(rawContent, messageType string) string {
|
||||
}
|
||||
}
|
||||
|
||||
func parsePostContent(rawContent string) string {
|
||||
// resolvePostElements extracts the flattened element list from a Lark post message JSON.
|
||||
// Handles language variant lookup (zh_cn → en_us → any first key).
|
||||
func resolvePostElements(rawContent string) []any {
|
||||
var post map[string]any
|
||||
if err := json.Unmarshal([]byte(rawContent), &post); err != nil {
|
||||
return rawContent
|
||||
return nil
|
||||
}
|
||||
|
||||
var langContent any
|
||||
@@ -119,16 +121,24 @@ func parsePostContent(rawContent string) string {
|
||||
}
|
||||
}
|
||||
if langContent == nil {
|
||||
return rawContent
|
||||
return nil
|
||||
}
|
||||
|
||||
langMap, ok := langContent.(map[string]any)
|
||||
if !ok {
|
||||
return rawContent
|
||||
return nil
|
||||
}
|
||||
|
||||
contentArr, ok := langMap["content"].([]any)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return contentArr
|
||||
}
|
||||
|
||||
func parsePostContent(rawContent string) string {
|
||||
contentArr := resolvePostElements(rawContent)
|
||||
if contentArr == nil {
|
||||
return rawContent
|
||||
}
|
||||
|
||||
@@ -179,6 +189,40 @@ func parsePostContent(rawContent string) string {
|
||||
return strings.Join(textParts, "\n")
|
||||
}
|
||||
|
||||
// extractPostImageKeys parses post content JSON and returns deduplicated image_key
|
||||
// values from embedded img tags. Used to download images inline in post messages.
|
||||
func extractPostImageKeys(rawContent string) []string {
|
||||
contentArr := resolvePostElements(rawContent)
|
||||
if contentArr == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
seen := make(map[string]struct{})
|
||||
var keys []string
|
||||
for _, para := range contentArr {
|
||||
paraArr, ok := para.([]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
for _, elem := range paraArr {
|
||||
elemMap, ok := elem.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
tag, _ := elemMap["tag"].(string)
|
||||
if tag == "img" {
|
||||
if key, ok := elemMap["image_key"].(string); ok && key != "" {
|
||||
if _, dup := seen[key]; !dup {
|
||||
seen[key] = struct{}{}
|
||||
keys = append(keys, key)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
return keys
|
||||
}
|
||||
|
||||
// resolveMentions replaces mention placeholders (@_user_1, @_user_2, etc.) in content.
|
||||
// Bot mention is stripped entirely; other user mentions become @Name.
|
||||
func resolveMentions(text string, mentions []mentionInfo, botOpenID string) string {
|
||||
|
||||
@@ -12,13 +12,19 @@ import (
|
||||
"github.com/nextlevelbuilder/goclaw/internal/channels/media"
|
||||
)
|
||||
|
||||
// resolveMediaFromMessage extracts and downloads media from a Feishu message.
|
||||
// Returns a list of MediaInfo for each media item found.
|
||||
func (c *Channel) resolveMediaFromMessage(ctx context.Context, messageID, messageType, rawContent string) []media.MediaInfo {
|
||||
// mediaMaxBytes returns the configured per-file size limit in bytes.
|
||||
func (c *Channel) mediaMaxBytes() int64 {
|
||||
maxBytes := int64(c.cfg.MediaMaxMB) * 1024 * 1024
|
||||
if maxBytes <= 0 {
|
||||
maxBytes = int64(defaultMediaMaxMB) * 1024 * 1024
|
||||
}
|
||||
return maxBytes
|
||||
}
|
||||
|
||||
// resolveMediaFromMessage extracts and downloads media from a Feishu message.
|
||||
// Returns a list of MediaInfo for each media item found.
|
||||
func (c *Channel) resolveMediaFromMessage(ctx context.Context, messageID, messageType, rawContent string) []media.MediaInfo {
|
||||
maxBytes := c.mediaMaxBytes()
|
||||
|
||||
var results []media.MediaInfo
|
||||
|
||||
@@ -140,6 +146,34 @@ func (c *Channel) resolveMediaFromMessage(ctx context.Context, messageID, messag
|
||||
return results
|
||||
}
|
||||
|
||||
// resolvePostImages downloads images embedded in post messages by their image_key.
|
||||
// Uses continue-on-error so one failed image doesn't skip the rest.
|
||||
func (c *Channel) resolvePostImages(ctx context.Context, messageID string, imageKeys []string) []media.MediaInfo {
|
||||
maxBytes := c.mediaMaxBytes()
|
||||
|
||||
var results []media.MediaInfo
|
||||
for _, key := range imageKeys {
|
||||
data, _, err := c.downloadMessageResource(ctx, messageID, key, "image")
|
||||
if err != nil {
|
||||
slog.Debug("feishu download post image failed", "message_id", messageID, "image_key", key, "error", err)
|
||||
continue
|
||||
}
|
||||
if int64(len(data)) > maxBytes {
|
||||
slog.Debug("feishu post image too large", "size", len(data), "max", maxBytes)
|
||||
continue
|
||||
}
|
||||
path, err := saveMediaToTemp(data, "img", ".png")
|
||||
if err != nil {
|
||||
slog.Debug("feishu save post image failed", "error", err)
|
||||
continue
|
||||
}
|
||||
results = append(results, media.MediaInfo{
|
||||
Type: media.TypeImage, FilePath: path, ContentType: "image/png",
|
||||
})
|
||||
}
|
||||
return results
|
||||
}
|
||||
|
||||
// saveMediaToTemp writes media bytes to a temp file and returns the path.
|
||||
func saveMediaToTemp(data []byte, prefix, ext string) (string, error) {
|
||||
if ext == "" {
|
||||
|
||||
Reference in new issue
Block a user