2026-02-20 15:25:44 +00:00
|
|
|
//go:build amd64 || arm64 || riscv64 || mips64 || ppc64
|
|
|
|
|
|
|
|
|
|
package feishu
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"context"
|
|
|
|
|
"encoding/json"
|
|
|
|
|
"fmt"
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
"io"
|
2026-03-09 14:45:01 +00:00
|
|
|
"math/rand"
|
2026-03-03 08:43:04 +00:00
|
|
|
"net/http"
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
"os"
|
|
|
|
|
"path/filepath"
|
2026-03-19 15:46:17 +00:00
|
|
|
"strings"
|
2026-02-20 15:25:44 +00:00
|
|
|
"sync"
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
"sync/atomic"
|
2026-04-08 06:26:17 +00:00
|
|
|
"time"
|
2026-02-20 15:25:44 +00:00
|
|
|
|
|
|
|
|
lark "github.com/larksuite/oapi-sdk-go/v3"
|
2026-03-03 08:43:04 +00:00
|
|
|
larkcore "github.com/larksuite/oapi-sdk-go/v3/core"
|
2026-02-20 15:25:44 +00:00
|
|
|
larkdispatcher "github.com/larksuite/oapi-sdk-go/v3/event/dispatcher"
|
|
|
|
|
larkim "github.com/larksuite/oapi-sdk-go/v3/service/im/v1"
|
|
|
|
|
larkws "github.com/larksuite/oapi-sdk-go/v3/ws"
|
|
|
|
|
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/bus"
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/channels"
|
|
|
|
|
"github.com/sipeed/picoclaw/pkg/config"
|
2026-02-22 22:56:48 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/identity"
|
2026-02-20 15:25:44 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/logger"
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/media"
|
2026-02-20 15:25:44 +00:00
|
|
|
"github.com/sipeed/picoclaw/pkg/utils"
|
|
|
|
|
)
|
|
|
|
|
|
2026-03-18 11:07:49 +00:00
|
|
|
// errCodeTenantTokenInvalid is the Feishu API error code for an expired/revoked
|
|
|
|
|
// tenant_access_token. The Lark SDK's built-in retry does not clear its cache
|
|
|
|
|
// on this error, so we do it ourselves.
|
|
|
|
|
const errCodeTenantTokenInvalid = 99991663
|
|
|
|
|
|
2026-02-20 15:25:44 +00:00
|
|
|
type FeishuChannel struct {
|
|
|
|
|
*channels.BaseChannel
|
2026-04-11 16:57:26 +00:00
|
|
|
bc *config.Channel
|
|
|
|
|
config *config.FeishuSettings
|
2026-03-18 11:07:49 +00:00
|
|
|
client *lark.Client
|
|
|
|
|
wsClient *larkws.Client
|
|
|
|
|
tokenCache *tokenCache // custom cache that supports invalidation
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
|
2026-04-08 06:26:17 +00:00
|
|
|
botOpenID atomic.Value // stores string; populated lazily for @mention detection
|
|
|
|
|
messageCache sync.Map // caches fetched messages (messageID -> *larkim.Message)
|
2026-02-20 15:25:44 +00:00
|
|
|
|
|
|
|
|
mu sync.Mutex
|
|
|
|
|
cancel context.CancelFunc
|
2026-04-23 02:35:50 +00:00
|
|
|
|
|
|
|
|
progress *channels.ToolFeedbackAnimator
|
|
|
|
|
deleteMessageFn func(context.Context, string, string) error
|
2026-05-11 23:45:01 +00:00
|
|
|
sendMediaPartFn func(context.Context, string, bus.MediaPart, media.MediaStore) error
|
|
|
|
|
sendTextFn func(context.Context, string, string) (string, error)
|
2026-02-20 15:25:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 06:26:17 +00:00
|
|
|
type cachedMessage struct {
|
|
|
|
|
msg *larkim.Message
|
|
|
|
|
expiry time.Time
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 16:57:26 +00:00
|
|
|
func NewFeishuChannel(bc *config.Channel, cfg *config.FeishuSettings, bus *bus.MessageBus) (*FeishuChannel, error) {
|
|
|
|
|
base := channels.NewBaseChannel("feishu", cfg, bus, bc.AllowFrom,
|
|
|
|
|
channels.WithGroupTrigger(bc.GroupTrigger),
|
|
|
|
|
channels.WithReasoningChannelID(bc.ReasoningChannelID),
|
refactor(channels): standardize group chat trigger filtering (Phase 8)
Add unified ShouldRespondInGroup to BaseChannel, replacing scattered
per-channel group filtering logic. Introduce GroupTriggerConfig (with
mention_only + prefixes), TypingConfig, and PlaceholderConfig types.
Migrate Discord MentionOnly, OneBot checkGroupTrigger, and LINE
hardcoded mention-only to the shared mechanism. Add group trigger
entry points for Slack, Telegram, QQ, Feishu, DingTalk, and WeCom.
Legacy config fields are preserved with automatic migration.
2026-02-22 20:11:11 +00:00
|
|
|
)
|
2026-02-20 15:25:44 +00:00
|
|
|
|
2026-03-18 11:07:49 +00:00
|
|
|
tc := newTokenCache()
|
2026-03-18 16:29:55 +00:00
|
|
|
opts := []lark.ClientOptionFunc{lark.WithTokenCache(tc)}
|
|
|
|
|
if cfg.IsLark {
|
|
|
|
|
opts = append(opts, lark.WithOpenBaseUrl(lark.LarkBaseUrl))
|
|
|
|
|
}
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
ch := &FeishuChannel{
|
2026-02-20 15:25:44 +00:00
|
|
|
BaseChannel: base,
|
2026-04-11 16:57:26 +00:00
|
|
|
bc: bc,
|
2026-03-03 08:43:04 +00:00
|
|
|
config: cfg,
|
2026-03-18 11:07:49 +00:00
|
|
|
tokenCache: tc,
|
2026-03-27 16:03:34 +00:00
|
|
|
client: lark.NewClient(cfg.AppID, cfg.AppSecret.String(), opts...),
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
2026-04-23 02:35:50 +00:00
|
|
|
ch.deleteMessageFn = ch.deleteMessageAPI
|
2026-05-11 23:45:01 +00:00
|
|
|
ch.sendMediaPartFn = ch.sendMediaPart
|
|
|
|
|
ch.sendTextFn = ch.sendText
|
2026-04-23 02:35:50 +00:00
|
|
|
ch.progress = channels.NewToolFeedbackAnimator(ch.EditMessage)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
ch.SetOwner(ch)
|
|
|
|
|
return ch, nil
|
2026-02-20 15:25:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) Start(ctx context.Context) error {
|
2026-03-27 16:03:34 +00:00
|
|
|
if c.config.AppID == "" || c.config.AppSecret.String() == "" {
|
2026-02-20 15:25:44 +00:00
|
|
|
return fmt.Errorf("feishu app_id or app_secret is empty")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-03 08:43:04 +00:00
|
|
|
// Fetch bot open_id via API for reliable @mention detection.
|
|
|
|
|
if err := c.fetchBotOpenID(ctx); err != nil {
|
|
|
|
|
logger.ErrorCF("feishu", "Failed to fetch bot open_id, @mention detection may not work", map[string]any{
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
}
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
|
2026-03-27 16:03:34 +00:00
|
|
|
dispatcher := larkdispatcher.NewEventDispatcher(c.config.VerificationToken.String(), c.config.EncryptKey.String()).
|
2026-02-20 15:25:44 +00:00
|
|
|
OnP2MessageReceiveV1(c.handleMessageReceive)
|
|
|
|
|
|
|
|
|
|
runCtx, cancel := context.WithCancel(ctx)
|
|
|
|
|
|
|
|
|
|
c.mu.Lock()
|
|
|
|
|
c.cancel = cancel
|
2026-03-18 16:29:55 +00:00
|
|
|
domain := lark.FeishuBaseUrl
|
|
|
|
|
if c.config.IsLark {
|
|
|
|
|
domain = lark.LarkBaseUrl
|
|
|
|
|
}
|
2026-02-20 15:25:44 +00:00
|
|
|
c.wsClient = larkws.NewClient(
|
2026-03-03 08:43:04 +00:00
|
|
|
c.config.AppID,
|
2026-03-27 16:03:34 +00:00
|
|
|
c.config.AppSecret.String(),
|
2026-02-20 15:25:44 +00:00
|
|
|
larkws.WithEventHandler(dispatcher),
|
2026-03-18 16:29:55 +00:00
|
|
|
larkws.WithDomain(domain),
|
2026-02-20 15:25:44 +00:00
|
|
|
)
|
|
|
|
|
wsClient := c.wsClient
|
|
|
|
|
c.mu.Unlock()
|
|
|
|
|
|
|
|
|
|
c.SetRunning(true)
|
|
|
|
|
logger.InfoC("feishu", "Feishu channel started (websocket mode)")
|
|
|
|
|
|
|
|
|
|
go func() {
|
|
|
|
|
if err := wsClient.Start(runCtx); err != nil {
|
2026-02-21 08:35:56 +00:00
|
|
|
logger.ErrorCF("feishu", "Feishu websocket stopped with error", map[string]any{
|
2026-02-20 15:25:44 +00:00
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) Stop(ctx context.Context) error {
|
|
|
|
|
c.mu.Lock()
|
|
|
|
|
if c.cancel != nil {
|
|
|
|
|
c.cancel()
|
|
|
|
|
c.cancel = nil
|
|
|
|
|
}
|
|
|
|
|
c.wsClient = nil
|
|
|
|
|
c.mu.Unlock()
|
2026-04-23 02:35:50 +00:00
|
|
|
if c.progress != nil {
|
|
|
|
|
c.progress.StopAll()
|
|
|
|
|
}
|
2026-02-20 15:25:44 +00:00
|
|
|
|
|
|
|
|
c.SetRunning(false)
|
|
|
|
|
logger.InfoC("feishu", "Feishu channel stopped")
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// Send sends a message using Interactive Card format for markdown rendering.
|
2026-03-19 15:46:17 +00:00
|
|
|
// Falls back to plain text message if card sending fails (e.g., table limit exceeded).
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) ([]string, error) {
|
2026-02-20 15:25:44 +00:00
|
|
|
if !c.IsRunning() {
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
return nil, channels.ErrNotRunning
|
2026-02-20 15:25:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if msg.ChatID == "" {
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-23 02:35:50 +00:00
|
|
|
isToolFeedback := outboundMessageIsToolFeedback(msg)
|
|
|
|
|
if isToolFeedback {
|
|
|
|
|
if msgID, handled, err := c.progress.Update(ctx, msg.ChatID, msg.Content); handled {
|
|
|
|
|
if err != nil {
|
|
|
|
|
// Feishu can fall back to plain text for a previous progress
|
|
|
|
|
// message, and those messages cannot be patched through the card
|
|
|
|
|
// edit API. Drop the stale tracker and recreate the progress
|
|
|
|
|
// message so later tool feedback is not blocked.
|
|
|
|
|
c.resetTrackedToolFeedbackAfterEditFailure(ctx, msg.ChatID)
|
|
|
|
|
} else {
|
|
|
|
|
return []string{msgID}, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
} else {
|
|
|
|
|
if msgIDs, handled := c.FinalizeToolFeedbackMessage(ctx, msg); handled {
|
|
|
|
|
return msgIDs, nil
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
trackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(msg.ChatID)
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// Build interactive card with markdown content
|
2026-04-23 02:35:50 +00:00
|
|
|
sendContent := msg.Content
|
|
|
|
|
if isToolFeedback {
|
|
|
|
|
sendContent = channels.InitialAnimatedToolFeedbackContent(msg.Content)
|
|
|
|
|
}
|
|
|
|
|
cardContent, err := buildMarkdownCard(sendContent)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
if err != nil {
|
2026-03-19 15:46:17 +00:00
|
|
|
// If card build fails, fall back to plain text
|
2026-04-23 02:35:50 +00:00
|
|
|
msgID, sendErr := c.sendText(ctx, msg.ChatID, sendContent)
|
|
|
|
|
if sendErr != nil {
|
|
|
|
|
return nil, sendErr
|
|
|
|
|
}
|
|
|
|
|
if isToolFeedback {
|
|
|
|
|
c.RecordToolFeedbackMessage(msg.ChatID, msgID, msg.Content)
|
|
|
|
|
} else if hasTrackedMsg {
|
|
|
|
|
c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
|
|
|
|
|
}
|
|
|
|
|
return []string{msgID}, nil
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
2026-03-19 15:46:17 +00:00
|
|
|
|
|
|
|
|
// First attempt: try sending as interactive card
|
2026-04-23 02:35:50 +00:00
|
|
|
msgID, err := c.sendCard(ctx, msg.ChatID, cardContent)
|
2026-03-19 15:46:17 +00:00
|
|
|
if err == nil {
|
2026-04-23 02:35:50 +00:00
|
|
|
if isToolFeedback {
|
|
|
|
|
c.RecordToolFeedbackMessage(msg.ChatID, msgID, msg.Content)
|
|
|
|
|
} else if hasTrackedMsg {
|
|
|
|
|
c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
|
|
|
|
|
}
|
|
|
|
|
return []string{msgID}, nil
|
2026-03-19 15:46:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Check if error is due to card table limit (error code 11310)
|
|
|
|
|
// See: https://open.feishu.cn/document/server-docs/im-api/message-content-description/create_json
|
|
|
|
|
errMsg := err.Error()
|
|
|
|
|
isCardLimitError := strings.Contains(errMsg, "11310")
|
|
|
|
|
|
|
|
|
|
if isCardLimitError {
|
|
|
|
|
logger.WarnCF("feishu", "Card send failed (table limit), falling back to text message", map[string]any{
|
|
|
|
|
"chat_id": msg.ChatID,
|
|
|
|
|
"error": errMsg,
|
|
|
|
|
})
|
|
|
|
|
|
|
|
|
|
// Second attempt: fall back to plain text message
|
2026-04-23 02:35:50 +00:00
|
|
|
msgID, textErr := c.sendText(ctx, msg.ChatID, sendContent)
|
2026-03-19 15:46:17 +00:00
|
|
|
if textErr == nil {
|
2026-04-23 02:35:50 +00:00
|
|
|
if isToolFeedback {
|
|
|
|
|
c.RecordToolFeedbackMessage(msg.ChatID, msgID, msg.Content)
|
|
|
|
|
} else if hasTrackedMsg {
|
|
|
|
|
c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
|
|
|
|
|
}
|
|
|
|
|
return []string{msgID}, nil
|
2026-03-19 15:46:17 +00:00
|
|
|
}
|
|
|
|
|
// If text also fails, return the text error
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
return nil, textErr
|
2026-03-19 15:46:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// For other errors, return the original card error
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
return nil, err
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// EditMessage implements channels.MessageEditor.
|
|
|
|
|
// Uses Message.Patch to update an interactive card message.
|
|
|
|
|
func (c *FeishuChannel) EditMessage(ctx context.Context, chatID, messageID, content string) error {
|
|
|
|
|
cardContent, err := buildMarkdownCard(content)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("feishu edit: card build failed: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := larkim.NewPatchMessageReqBuilder().
|
|
|
|
|
MessageId(messageID).
|
|
|
|
|
Body(larkim.NewPatchMessageReqBodyBuilder().Content(cardContent).Build()).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.Message.Patch(ctx, req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("feishu edit: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !resp.Success() {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(resp.Code)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return fmt.Errorf("feishu edit api error (code=%d msg=%s)", resp.Code, resp.Msg)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 02:35:50 +00:00
|
|
|
// DeleteMessage implements channels.MessageDeleter.
|
|
|
|
|
func (c *FeishuChannel) DeleteMessage(ctx context.Context, chatID, messageID string) error {
|
|
|
|
|
deleteFn := c.deleteMessageFn
|
|
|
|
|
if deleteFn == nil {
|
|
|
|
|
deleteFn = c.deleteMessageAPI
|
|
|
|
|
}
|
|
|
|
|
return deleteFn(ctx, chatID, messageID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) deleteMessageAPI(ctx context.Context, chatID, messageID string) error {
|
|
|
|
|
req := larkim.NewDeleteMessageReqBuilder().
|
|
|
|
|
MessageId(messageID).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.Message.Delete(ctx, req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("feishu delete: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !resp.Success() {
|
|
|
|
|
c.invalidateTokenOnAuthError(resp.Code)
|
|
|
|
|
return fmt.Errorf("feishu delete api error (code=%d msg=%s)", resp.Code, resp.Msg)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// SendPlaceholder implements channels.PlaceholderCapable.
|
|
|
|
|
// Sends an interactive card with placeholder text and returns its message ID.
|
|
|
|
|
func (c *FeishuChannel) SendPlaceholder(ctx context.Context, chatID string) (string, error) {
|
2026-04-11 16:57:26 +00:00
|
|
|
if !c.bc.Placeholder.Enabled {
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
logger.DebugCF("feishu", "Placeholder disabled, skipping", map[string]any{
|
|
|
|
|
"chat_id": chatID,
|
|
|
|
|
})
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-11 16:57:26 +00:00
|
|
|
text := c.bc.Placeholder.GetRandomText()
|
2026-02-20 15:25:44 +00:00
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
cardContent, err := buildMarkdownCard(text)
|
2026-02-20 15:25:44 +00:00
|
|
|
if err != nil {
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return "", fmt.Errorf("feishu placeholder: card build failed: %w", err)
|
2026-02-20 15:25:44 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
req := larkim.NewCreateMessageReqBuilder().
|
2026-06-04 21:19:04 +00:00
|
|
|
ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).
|
2026-02-20 15:25:44 +00:00
|
|
|
Body(larkim.NewCreateMessageReqBodyBuilder().
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
ReceiveId(chatID).
|
|
|
|
|
MsgType(larkim.MsgTypeInteractive).
|
|
|
|
|
Content(cardContent).
|
2026-02-20 15:25:44 +00:00
|
|
|
Build()).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.Message.Create(ctx, req)
|
|
|
|
|
if err != nil {
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return "", fmt.Errorf("feishu placeholder send: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !resp.Success() {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(resp.Code)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return "", fmt.Errorf("feishu placeholder api error (code=%d msg=%s)", resp.Code, resp.Msg)
|
2026-02-20 15:25:44 +00:00
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
if resp.Data != nil && resp.Data.MessageId != nil {
|
|
|
|
|
return *resp.Data.MessageId, nil
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-04-23 02:35:50 +00:00
|
|
|
func outboundMessageIsToolFeedback(msg bus.OutboundMessage) bool {
|
|
|
|
|
if len(msg.Context.Raw) == 0 {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
return strings.EqualFold(strings.TrimSpace(msg.Context.Raw["message_kind"]), "tool_feedback")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) currentToolFeedbackMessage(chatID string) (string, bool) {
|
|
|
|
|
if c.progress == nil {
|
|
|
|
|
return "", false
|
|
|
|
|
}
|
|
|
|
|
return c.progress.Current(chatID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) takeToolFeedbackMessage(chatID string) (string, string, bool) {
|
|
|
|
|
if c.progress == nil {
|
|
|
|
|
return "", "", false
|
|
|
|
|
}
|
|
|
|
|
return c.progress.Take(chatID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) RecordToolFeedbackMessage(chatID, messageID, content string) {
|
|
|
|
|
if c.progress == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.progress.Record(chatID, messageID, content)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) ClearToolFeedbackMessage(chatID string) {
|
|
|
|
|
if c.progress == nil {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.progress.Clear(chatID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) DismissToolFeedbackMessage(ctx context.Context, chatID string) {
|
|
|
|
|
msgID, ok := c.currentToolFeedbackMessage(chatID)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.dismissTrackedToolFeedbackMessage(ctx, chatID, msgID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) resetTrackedToolFeedbackAfterEditFailure(ctx context.Context, chatID string) {
|
|
|
|
|
msgID, ok := c.currentToolFeedbackMessage(chatID)
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.dismissTrackedToolFeedbackMessage(ctx, chatID, msgID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) dismissTrackedToolFeedbackMessage(ctx context.Context, chatID, messageID string) {
|
|
|
|
|
if strings.TrimSpace(chatID) == "" || strings.TrimSpace(messageID) == "" {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
c.ClearToolFeedbackMessage(chatID)
|
|
|
|
|
deleteFn := c.deleteMessageFn
|
|
|
|
|
if deleteFn == nil {
|
|
|
|
|
deleteFn = c.deleteMessageAPI
|
|
|
|
|
}
|
|
|
|
|
_ = deleteFn(ctx, chatID, messageID)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) finalizeTrackedToolFeedbackMessage(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
chatID string,
|
|
|
|
|
content string,
|
|
|
|
|
editFn func(context.Context, string, string, string) error,
|
|
|
|
|
) ([]string, bool) {
|
|
|
|
|
msgID, baseContent, ok := c.takeToolFeedbackMessage(chatID)
|
|
|
|
|
if !ok || editFn == nil {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
if err := editFn(ctx, chatID, msgID, content); err != nil {
|
|
|
|
|
c.RecordToolFeedbackMessage(chatID, msgID, baseContent)
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
return []string{msgID}, true
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (c *FeishuChannel) FinalizeToolFeedbackMessage(ctx context.Context, msg bus.OutboundMessage) ([]string, bool) {
|
|
|
|
|
if outboundMessageIsToolFeedback(msg) {
|
|
|
|
|
return nil, false
|
|
|
|
|
}
|
|
|
|
|
return c.finalizeTrackedToolFeedbackMessage(ctx, msg.ChatID, msg.Content, c.EditMessage)
|
|
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// ReactToMessage implements channels.ReactionCapable.
|
2026-03-06 04:53:47 +00:00
|
|
|
// Adds a reaction (randomly chosen from config) and returns an undo function to remove it.
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) {
|
2026-03-28 15:36:49 +00:00
|
|
|
// Get emoji list from config (Feishu emoji_type keys, e.g. Pin, THUMBSUP).
|
|
|
|
|
// Ignore empty entries so a list like ["", "Pin"] does not randomly pick "" (API 231001).
|
|
|
|
|
var candidates []string
|
|
|
|
|
for _, e := range c.config.RandomReactionEmoji {
|
|
|
|
|
e = strings.TrimSpace(e)
|
|
|
|
|
if e != "" {
|
|
|
|
|
candidates = append(candidates, e)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
chosenEmoji := "Pin"
|
|
|
|
|
if len(candidates) > 0 {
|
|
|
|
|
chosenEmoji = candidates[rand.Intn(len(candidates))]
|
2026-03-08 09:30:50 +00:00
|
|
|
}
|
2026-03-06 04:53:47 +00:00
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
req := larkim.NewCreateMessageReactionReqBuilder().
|
|
|
|
|
MessageId(messageID).
|
|
|
|
|
Body(larkim.NewCreateMessageReactionReqBodyBuilder().
|
2026-03-06 04:53:47 +00:00
|
|
|
ReactionType(larkim.NewEmojiBuilder().EmojiType(chosenEmoji).Build()).
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
Build()).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.MessageReaction.Create(ctx, req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.ErrorCF("feishu", "Failed to add reaction", map[string]any{
|
2026-03-06 04:53:48 +00:00
|
|
|
"emoji": chosenEmoji,
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
"message_id": messageID,
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
return func() {}, fmt.Errorf("feishu react: %w", err)
|
|
|
|
|
}
|
2026-02-20 15:25:44 +00:00
|
|
|
if !resp.Success() {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(resp.Code)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
logger.ErrorCF("feishu", "Reaction API error", map[string]any{
|
2026-03-06 04:53:47 +00:00
|
|
|
"emoji": chosenEmoji,
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
"message_id": messageID,
|
|
|
|
|
"code": resp.Code,
|
|
|
|
|
"msg": resp.Msg,
|
|
|
|
|
})
|
|
|
|
|
return func() {}, fmt.Errorf("feishu react api error (code=%d msg=%s)", resp.Code, resp.Msg)
|
2026-02-20 15:25:44 +00:00
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
var reactionID string
|
|
|
|
|
if resp.Data != nil && resp.Data.ReactionId != nil {
|
|
|
|
|
reactionID = *resp.Data.ReactionId
|
|
|
|
|
}
|
|
|
|
|
if reactionID == "" {
|
|
|
|
|
return func() {}, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var undone atomic.Bool
|
|
|
|
|
undo := func() {
|
|
|
|
|
if !undone.CompareAndSwap(false, true) {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
delReq := larkim.NewDeleteMessageReactionReqBuilder().
|
|
|
|
|
MessageId(messageID).
|
|
|
|
|
ReactionId(reactionID).
|
|
|
|
|
Build()
|
|
|
|
|
_, _ = c.client.Im.V1.MessageReaction.Delete(context.Background(), delReq)
|
|
|
|
|
}
|
|
|
|
|
return undo, nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SendMedia implements channels.MediaSender.
|
|
|
|
|
// Uploads images/files via Feishu API then sends as messages.
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
func (c *FeishuChannel) SendMedia(ctx context.Context, msg bus.OutboundMediaMessage) ([]string, error) {
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
if !c.IsRunning() {
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
return nil, channels.ErrNotRunning
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
2026-04-23 02:35:50 +00:00
|
|
|
trackedMsgID, hasTrackedMsg := c.currentToolFeedbackMessage(msg.ChatID)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
|
2026-03-02 17:27:39 +00:00
|
|
|
if msg.ChatID == "" {
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
return nil, fmt.Errorf("chat ID is empty: %w", channels.ErrSendFailed)
|
2026-03-02 17:27:39 +00:00
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
store := c.GetMediaStore()
|
|
|
|
|
if store == nil {
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
return nil, fmt.Errorf("no media store available: %w", channels.ErrSendFailed)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
2026-05-11 23:04:26 +00:00
|
|
|
caption := firstMediaCaption(msg.Parts)
|
|
|
|
|
sentAny := false
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
for _, part := range msg.Parts {
|
2026-05-11 23:45:01 +00:00
|
|
|
if err := c.sendMediaPartFn(ctx, msg.ChatID, part, store); err != nil {
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
return nil, err
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
2026-05-11 23:04:26 +00:00
|
|
|
sentAny = true
|
|
|
|
|
}
|
|
|
|
|
if sentAny && caption != "" {
|
2026-05-11 23:45:01 +00:00
|
|
|
if _, err := c.sendTextFn(ctx, msg.ChatID, caption); err != nil {
|
2026-05-11 23:04:26 +00:00
|
|
|
return nil, err
|
|
|
|
|
}
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
2026-02-20 15:25:44 +00:00
|
|
|
|
2026-04-23 02:35:50 +00:00
|
|
|
if hasTrackedMsg {
|
|
|
|
|
c.dismissTrackedToolFeedbackMessage(ctx, msg.ChatID, trackedMsgID)
|
|
|
|
|
}
|
|
|
|
|
|
feat(channels): make Channel.Send return delivered message IDs (#2190)
* feat(channels): Channel.Send and MediaSender.SendMedia return delivered message IDs
Change Channel.Send signature from (ctx, msg) error to (ctx, msg) ([]string, error)
and MediaSender.SendMedia similarly, so callers can capture platform message IDs
for threading, reactions, and history annotation.
Adapters that return real IDs: Telegram (per-chunk MessageID), Discord (Message.ID),
Slack Send (ts), QQ (sentMsg.ID), Matrix (EventID). Slack SendMedia returns nil
because UploadFileV2 does not expose the posted message timestamp in its response.
All other adapters return nil IDs.
preSend and sendWithRetry in manager.go updated to propagate ([]string, bool).
README examples updated for both English and Chinese docs.
* style: apply golangci-lint fixes (golines)
* docs: fix Send migration guide — restore old error-only signature in before/after example
2026-03-31 03:07:32 +00:00
|
|
|
return nil, nil
|
2026-02-20 15:25:44 +00:00
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// sendMediaPart resolves and sends a single media part.
|
|
|
|
|
func (c *FeishuChannel) sendMediaPart(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
chatID string,
|
|
|
|
|
part bus.MediaPart,
|
|
|
|
|
store media.MediaStore,
|
|
|
|
|
) error {
|
|
|
|
|
localPath, err := store.Resolve(part.Ref)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.ErrorCF("feishu", "Failed to resolve media ref", map[string]any{
|
|
|
|
|
"ref": part.Ref,
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
return nil // skip this part
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
file, err := os.Open(localPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.ErrorCF("feishu", "Failed to open media file", map[string]any{
|
|
|
|
|
"path": localPath,
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
return nil // skip this part
|
|
|
|
|
}
|
|
|
|
|
defer file.Close()
|
|
|
|
|
|
|
|
|
|
switch part.Type {
|
|
|
|
|
case "image":
|
|
|
|
|
err = c.sendImage(ctx, chatID, file)
|
|
|
|
|
default:
|
|
|
|
|
filename := part.Filename
|
|
|
|
|
if filename == "" {
|
|
|
|
|
filename = "file"
|
|
|
|
|
}
|
|
|
|
|
err = c.sendFile(ctx, chatID, file, filename, part.Type)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.ErrorCF("feishu", "Failed to send media", map[string]any{
|
|
|
|
|
"type": part.Type,
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
return fmt.Errorf("feishu send media: %w", channels.ErrTemporary)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
2026-05-11 23:04:26 +00:00
|
|
|
func firstMediaCaption(parts []bus.MediaPart) string {
|
|
|
|
|
for _, part := range parts {
|
|
|
|
|
if caption := strings.TrimSpace(part.Caption); caption != "" {
|
|
|
|
|
return caption
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// --- Inbound message handling ---
|
|
|
|
|
|
2026-02-22 22:03:23 +00:00
|
|
|
func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
|
2026-02-20 15:25:44 +00:00
|
|
|
if event == nil || event.Event == nil || event.Event.Message == nil {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
message := event.Event.Message
|
|
|
|
|
sender := event.Event.Sender
|
|
|
|
|
|
|
|
|
|
chatID := stringValue(message.ChatId)
|
|
|
|
|
if chatID == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
senderID := extractFeishuSenderID(sender)
|
|
|
|
|
if senderID == "" {
|
|
|
|
|
senderID = "unknown"
|
|
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
messageType := stringValue(message.MessageType)
|
|
|
|
|
messageID := stringValue(message.MessageId)
|
|
|
|
|
rawContent := stringValue(message.Content)
|
|
|
|
|
|
2026-03-02 17:27:39 +00:00
|
|
|
// Check allowlist early to avoid downloading media for rejected senders.
|
|
|
|
|
// BaseChannel.HandleMessage will check again, but this avoids wasted network I/O.
|
|
|
|
|
senderInfo := bus.SenderInfo{
|
|
|
|
|
Platform: "feishu",
|
|
|
|
|
PlatformID: senderID,
|
|
|
|
|
CanonicalID: identity.BuildCanonicalID("feishu", senderID),
|
|
|
|
|
}
|
|
|
|
|
if !c.IsAllowedSender(senderInfo) {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// Extract content based on message type
|
|
|
|
|
content := extractContent(messageType, rawContent)
|
|
|
|
|
|
|
|
|
|
// Handle media messages (download and store)
|
|
|
|
|
var mediaRefs []string
|
|
|
|
|
if store := c.GetMediaStore(); store != nil && messageID != "" {
|
|
|
|
|
mediaRefs = c.downloadInboundMedia(ctx, chatID, messageID, messageType, rawContent, store)
|
|
|
|
|
}
|
|
|
|
|
|
Feat/feishu card parsing (#1534)
* feat(feishu): add interactive card message parsing
Add support for parsing inbound Feishu interactive card messages.
When a user sends a card message, the text content is now extracted
and passed to the LLM for processing.
- Add extractCardText() to recursively extract text from card JSON
- Support both JSON 1.0 (legacy) and JSON 2.0 schema formats
- Handle nested elements: header, body, actions, columns
- Extract text from markdown, lark_md, and plain_text elements
- Add comprehensive unit tests for card parsing
Fixes #<issue_number>
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* feat(feishu): extract and download images from interactive cards
When receiving interactive card messages, extract embedded images
(img_key, src, icon_key) and download them for LLM processing.
- Add extractCardImageKeys() to recursively extract image keys from card JSON
- Support img elements (img_key, src) and icon elements (icon_key)
- Update downloadInboundMedia() to handle MsgTypeInteractive
- Add comprehensive unit tests for image extraction
Images are downloaded and stored via MediaStore, then appended to
the message content as [image: photo] tags for LLM visibility.
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): simplify card parsing - pass raw JSON, only extract images
Address review feedback: text extraction cannot exhaustively handle all
card formats (i18n_elements, div.fields, etc.). Pass raw JSON to LLM
instead - same approach as MsgTypePost. Only image extraction remains
as images must be downloaded for LLM to process.
- Remove extractCardText() and helper functions
- extractContent() now returns raw JSON for MsgTypeInteractive
- Keep extractCardImageKeys() for downloading embedded images
- Update tests to expect raw JSON for interactive cards
* fix(feishu): don't append media tags to interactive card JSON
Appending media tags like "[attachment]" to raw JSON content produces
invalid JSON format. For interactive cards, the JSON already contains
image information and media refs are downloaded separately.
- Skip appendMediaTags for MsgTypeInteractive to preserve valid JSON
- Add test case for interactive card with images
* fix(feishu): filter out external URLs from card image extraction
Only Feishu-hosted image keys (img_xxx, icon_xxx) can be downloaded via
the Feishu API. External URLs in src field (https://...) should be
filtered out to avoid download failures.
- Add isFeishuImageKey() to detect Feishu-hosted keys vs external URLs
- Update extractImageKeysRecursive to skip external URLs in src field
- Add tests for external URL filtering and mixed scenarios
* feat(feishu): support downloading external images from interactive cards
Previously only Feishu-hosted images (img_key, icon_key) could be
downloaded. Now external URLs in src field are also downloaded via
HTTP and made available to the LLM.
- extractCardImageKeys now returns two slices: Feishu keys and external URLs
- Add downloadExternalImage to download images from HTTP URLs
- Update downloadInboundMedia to handle both Feishu API and HTTP downloads
- Update tests for new function signature
* fix(feishu): use HTTP client with timeout for external image downloads
Replaced http.DefaultClient with a client that has a 30-second timeout
to prevent hanging on unresponsive external URLs.
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): resolve lint errors for shadow and formatting
- Rename err variables to avoid shadowing in downloadExternalImage
- Fix struct field alignment in TestExtractCardImageKeys
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* refactor(feishu): pass external image URLs to LLM instead of downloading
Instead of downloading external images from interactive cards, pass
the URLs directly to LLM. This reduces network overhead and lets
vision-capable models fetch images as needed.
- Remove downloadExternalImage function
- Append external URLs to card content for LLM processing
- Only download Feishu-hosted images via API
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): add blank line between functions for gci formatting
* fix(feishu): keep interactive card content as valid JSON
2026-03-20 04:59:43 +00:00
|
|
|
// For interactive cards, pass external image URLs via media refs.
|
|
|
|
|
// Keep content as valid raw JSON for downstream parsing.
|
|
|
|
|
if messageType == larkim.MsgTypeInteractive {
|
|
|
|
|
_, externalURLs := extractCardImageKeys(rawContent)
|
|
|
|
|
if len(externalURLs) > 0 {
|
|
|
|
|
mediaRefs = append(mediaRefs, externalURLs...)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// Append media tags to content (like Telegram does)
|
|
|
|
|
content = appendMediaTags(content, messageType, mediaRefs)
|
|
|
|
|
|
2026-02-20 15:25:44 +00:00
|
|
|
if content == "" {
|
|
|
|
|
content = "[empty message]"
|
|
|
|
|
}
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
chatType := stringValue(message.ChatType)
|
2026-04-08 06:26:17 +00:00
|
|
|
metadata := buildInboundMetadata(message, sender)
|
2026-02-20 15:25:44 +00:00
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
var (
|
|
|
|
|
inboundChatType string
|
|
|
|
|
isMentioned bool
|
|
|
|
|
)
|
2026-02-20 15:25:44 +00:00
|
|
|
if chatType == "p2p" {
|
2026-04-01 12:56:48 +00:00
|
|
|
inboundChatType = "direct"
|
2026-02-20 15:25:44 +00:00
|
|
|
} else {
|
2026-04-01 12:56:48 +00:00
|
|
|
inboundChatType = "group"
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
|
|
|
|
|
// Check if bot was mentioned
|
2026-04-01 12:56:48 +00:00
|
|
|
isMentioned = c.isBotMentioned(message)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
|
|
|
|
|
// Strip mention placeholders from content before group trigger check
|
|
|
|
|
if len(message.Mentions) > 0 {
|
|
|
|
|
content = stripMentionPlaceholders(content, message.Mentions)
|
|
|
|
|
}
|
|
|
|
|
|
refactor(channels): standardize group chat trigger filtering (Phase 8)
Add unified ShouldRespondInGroup to BaseChannel, replacing scattered
per-channel group filtering logic. Introduce GroupTriggerConfig (with
mention_only + prefixes), TypingConfig, and PlaceholderConfig types.
Migrate Discord MentionOnly, OneBot checkGroupTrigger, and LINE
hardcoded mention-only to the shared mechanism. Add group trigger
entry points for Slack, Telegram, QQ, Feishu, DingTalk, and WeCom.
Legacy config fields are preserved with automatic migration.
2026-02-22 20:11:11 +00:00
|
|
|
// In group chats, apply unified group trigger filtering
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
respond, cleaned := c.ShouldRespondInGroup(isMentioned, content)
|
refactor(channels): standardize group chat trigger filtering (Phase 8)
Add unified ShouldRespondInGroup to BaseChannel, replacing scattered
per-channel group filtering logic. Introduce GroupTriggerConfig (with
mention_only + prefixes), TypingConfig, and PlaceholderConfig types.
Migrate Discord MentionOnly, OneBot checkGroupTrigger, and LINE
hardcoded mention-only to the shared mechanism. Add group trigger
entry points for Slack, Telegram, QQ, Feishu, DingTalk, and WeCom.
Legacy config fields are preserved with automatic migration.
2026-02-22 20:11:11 +00:00
|
|
|
if !respond {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
content = cleaned
|
2026-02-20 15:25:44 +00:00
|
|
|
}
|
|
|
|
|
|
2026-04-08 06:26:17 +00:00
|
|
|
if replyTargetID(message) != "" || stringValue(message.ThreadId) != "" {
|
|
|
|
|
content, mediaRefs = c.prependReplyContext(ctx, message, chatID, content, mediaRefs)
|
|
|
|
|
}
|
|
|
|
|
if content == "" {
|
|
|
|
|
content = "[empty message]"
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-21 08:35:56 +00:00
|
|
|
logger.InfoCF("feishu", "Feishu message received", map[string]any{
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
"sender_id": senderID,
|
|
|
|
|
"chat_id": chatID,
|
|
|
|
|
"message_id": messageID,
|
|
|
|
|
"preview": utils.Truncate(content, 80),
|
2026-02-20 15:25:44 +00:00
|
|
|
})
|
2026-04-08 06:26:17 +00:00
|
|
|
logger.InfoCF("feishu", "Feishu reply linkage", map[string]any{
|
|
|
|
|
"message_id": messageID,
|
|
|
|
|
"parent_id": stringValue(message.ParentId),
|
|
|
|
|
"root_id": stringValue(message.RootId),
|
|
|
|
|
"thread_id": stringValue(message.ThreadId),
|
|
|
|
|
})
|
2026-02-20 15:25:44 +00:00
|
|
|
|
2026-04-01 12:56:48 +00:00
|
|
|
inboundCtx := bus.InboundContext{
|
|
|
|
|
Channel: "feishu",
|
|
|
|
|
ChatID: chatID,
|
|
|
|
|
ChatType: inboundChatType,
|
|
|
|
|
SenderID: senderID,
|
|
|
|
|
MessageID: messageID,
|
|
|
|
|
Mentioned: isMentioned,
|
|
|
|
|
Raw: metadata,
|
|
|
|
|
}
|
|
|
|
|
if sender != nil && sender.TenantKey != nil && *sender.TenantKey != "" {
|
|
|
|
|
inboundCtx.SpaceType = "tenant"
|
|
|
|
|
inboundCtx.SpaceID = *sender.TenantKey
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.HandleInboundContext(ctx, chatID, content, mediaRefs, inboundCtx, senderInfo)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// --- Internal helpers ---
|
|
|
|
|
|
2026-03-03 08:43:04 +00:00
|
|
|
// fetchBotOpenID calls the Feishu bot info API to retrieve and store the bot's open_id.
|
|
|
|
|
func (c *FeishuChannel) fetchBotOpenID(ctx context.Context) error {
|
|
|
|
|
resp, err := c.client.Do(ctx, &larkcore.ApiReq{
|
|
|
|
|
HttpMethod: http.MethodGet,
|
|
|
|
|
ApiPath: "/open-apis/bot/v3/info",
|
|
|
|
|
SupportedAccessTokenTypes: []larkcore.AccessTokenType{larkcore.AccessTokenTypeTenant},
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("bot info request: %w", err)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var result struct {
|
|
|
|
|
Code int `json:"code"`
|
|
|
|
|
Bot struct {
|
|
|
|
|
OpenID string `json:"open_id"`
|
|
|
|
|
} `json:"bot"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal(resp.RawBody, &result); err != nil {
|
|
|
|
|
return fmt.Errorf("bot info parse: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if result.Code != 0 {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(result.Code)
|
2026-03-03 08:43:04 +00:00
|
|
|
return fmt.Errorf("bot info api error (code=%d)", result.Code)
|
|
|
|
|
}
|
|
|
|
|
if result.Bot.OpenID == "" {
|
|
|
|
|
return fmt.Errorf("bot info: empty open_id")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.botOpenID.Store(result.Bot.OpenID)
|
|
|
|
|
logger.InfoCF("feishu", "Fetched bot open_id from API", map[string]any{
|
|
|
|
|
"open_id": result.Bot.OpenID,
|
|
|
|
|
})
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// isBotMentioned checks if the bot was @mentioned in the message.
|
|
|
|
|
func (c *FeishuChannel) isBotMentioned(message *larkim.EventMessage) bool {
|
|
|
|
|
if message.Mentions == nil {
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
2026-06-06 13:16:55 +00:00
|
|
|
knownID, ok := c.botOpenID.Load().(string)
|
|
|
|
|
if !ok || knownID == "" {
|
2026-03-03 08:43:04 +00:00
|
|
|
logger.DebugCF("feishu", "Bot open_id unknown, cannot detect @mention", nil)
|
|
|
|
|
return false
|
|
|
|
|
}
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
|
|
|
|
|
for _, m := range message.Mentions {
|
|
|
|
|
if m.Id == nil {
|
|
|
|
|
continue
|
|
|
|
|
}
|
2026-03-03 08:43:04 +00:00
|
|
|
if m.Id.OpenId != nil && *m.Id.OpenId == knownID {
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return true
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
return false
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// extractContent extracts text content from different message types.
|
|
|
|
|
func extractContent(messageType, rawContent string) string {
|
|
|
|
|
if rawContent == "" {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
switch messageType {
|
|
|
|
|
case larkim.MsgTypeText:
|
|
|
|
|
var textPayload struct {
|
|
|
|
|
Text string `json:"text"`
|
|
|
|
|
}
|
|
|
|
|
if err := json.Unmarshal([]byte(rawContent), &textPayload); err == nil {
|
|
|
|
|
return textPayload.Text
|
|
|
|
|
}
|
|
|
|
|
return rawContent
|
|
|
|
|
|
|
|
|
|
case larkim.MsgTypePost:
|
|
|
|
|
// Pass raw JSON to LLM — structured rich text is more informative than flattened plain text
|
|
|
|
|
return rawContent
|
|
|
|
|
|
Feat/feishu card parsing (#1534)
* feat(feishu): add interactive card message parsing
Add support for parsing inbound Feishu interactive card messages.
When a user sends a card message, the text content is now extracted
and passed to the LLM for processing.
- Add extractCardText() to recursively extract text from card JSON
- Support both JSON 1.0 (legacy) and JSON 2.0 schema formats
- Handle nested elements: header, body, actions, columns
- Extract text from markdown, lark_md, and plain_text elements
- Add comprehensive unit tests for card parsing
Fixes #<issue_number>
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* feat(feishu): extract and download images from interactive cards
When receiving interactive card messages, extract embedded images
(img_key, src, icon_key) and download them for LLM processing.
- Add extractCardImageKeys() to recursively extract image keys from card JSON
- Support img elements (img_key, src) and icon elements (icon_key)
- Update downloadInboundMedia() to handle MsgTypeInteractive
- Add comprehensive unit tests for image extraction
Images are downloaded and stored via MediaStore, then appended to
the message content as [image: photo] tags for LLM visibility.
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): simplify card parsing - pass raw JSON, only extract images
Address review feedback: text extraction cannot exhaustively handle all
card formats (i18n_elements, div.fields, etc.). Pass raw JSON to LLM
instead - same approach as MsgTypePost. Only image extraction remains
as images must be downloaded for LLM to process.
- Remove extractCardText() and helper functions
- extractContent() now returns raw JSON for MsgTypeInteractive
- Keep extractCardImageKeys() for downloading embedded images
- Update tests to expect raw JSON for interactive cards
* fix(feishu): don't append media tags to interactive card JSON
Appending media tags like "[attachment]" to raw JSON content produces
invalid JSON format. For interactive cards, the JSON already contains
image information and media refs are downloaded separately.
- Skip appendMediaTags for MsgTypeInteractive to preserve valid JSON
- Add test case for interactive card with images
* fix(feishu): filter out external URLs from card image extraction
Only Feishu-hosted image keys (img_xxx, icon_xxx) can be downloaded via
the Feishu API. External URLs in src field (https://...) should be
filtered out to avoid download failures.
- Add isFeishuImageKey() to detect Feishu-hosted keys vs external URLs
- Update extractImageKeysRecursive to skip external URLs in src field
- Add tests for external URL filtering and mixed scenarios
* feat(feishu): support downloading external images from interactive cards
Previously only Feishu-hosted images (img_key, icon_key) could be
downloaded. Now external URLs in src field are also downloaded via
HTTP and made available to the LLM.
- extractCardImageKeys now returns two slices: Feishu keys and external URLs
- Add downloadExternalImage to download images from HTTP URLs
- Update downloadInboundMedia to handle both Feishu API and HTTP downloads
- Update tests for new function signature
* fix(feishu): use HTTP client with timeout for external image downloads
Replaced http.DefaultClient with a client that has a 30-second timeout
to prevent hanging on unresponsive external URLs.
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): resolve lint errors for shadow and formatting
- Rename err variables to avoid shadowing in downloadExternalImage
- Fix struct field alignment in TestExtractCardImageKeys
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* refactor(feishu): pass external image URLs to LLM instead of downloading
Instead of downloading external images from interactive cards, pass
the URLs directly to LLM. This reduces network overhead and lets
vision-capable models fetch images as needed.
- Remove downloadExternalImage function
- Append external URLs to card content for LLM processing
- Only download Feishu-hosted images via API
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): add blank line between functions for gci formatting
* fix(feishu): keep interactive card content as valid JSON
2026-03-20 04:59:43 +00:00
|
|
|
case larkim.MsgTypeInteractive:
|
|
|
|
|
// Pass raw JSON to LLM — structured card is more informative than flattened text
|
|
|
|
|
return rawContent
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
case larkim.MsgTypeImage:
|
|
|
|
|
// Image messages don't have text content
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
case larkim.MsgTypeFile, larkim.MsgTypeAudio, larkim.MsgTypeMedia:
|
|
|
|
|
// File/audio/video messages may have a filename
|
|
|
|
|
name := extractFileName(rawContent)
|
|
|
|
|
if name != "" {
|
|
|
|
|
return name
|
|
|
|
|
}
|
|
|
|
|
return ""
|
|
|
|
|
|
|
|
|
|
default:
|
|
|
|
|
return rawContent
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// downloadInboundMedia downloads media from inbound messages and stores in MediaStore.
|
|
|
|
|
func (c *FeishuChannel) downloadInboundMedia(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
chatID, messageID, messageType, rawContent string,
|
|
|
|
|
store media.MediaStore,
|
|
|
|
|
) []string {
|
|
|
|
|
var refs []string
|
|
|
|
|
scope := channels.BuildMediaScope("feishu", chatID, messageID)
|
|
|
|
|
|
|
|
|
|
switch messageType {
|
|
|
|
|
case larkim.MsgTypeImage:
|
|
|
|
|
imageKey := extractImageKey(rawContent)
|
|
|
|
|
if imageKey == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
ref := c.downloadResource(ctx, messageID, imageKey, "image", ".jpg", store, scope)
|
|
|
|
|
if ref != "" {
|
|
|
|
|
refs = append(refs, ref)
|
|
|
|
|
}
|
|
|
|
|
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
case larkim.MsgTypePost:
|
|
|
|
|
for _, imageKey := range extractPostImageKeys(rawContent) {
|
|
|
|
|
ref := c.downloadResource(ctx, messageID, imageKey, "image", ".jpg", store, scope)
|
|
|
|
|
if ref != "" {
|
|
|
|
|
refs = append(refs, ref)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
Feat/feishu card parsing (#1534)
* feat(feishu): add interactive card message parsing
Add support for parsing inbound Feishu interactive card messages.
When a user sends a card message, the text content is now extracted
and passed to the LLM for processing.
- Add extractCardText() to recursively extract text from card JSON
- Support both JSON 1.0 (legacy) and JSON 2.0 schema formats
- Handle nested elements: header, body, actions, columns
- Extract text from markdown, lark_md, and plain_text elements
- Add comprehensive unit tests for card parsing
Fixes #<issue_number>
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* feat(feishu): extract and download images from interactive cards
When receiving interactive card messages, extract embedded images
(img_key, src, icon_key) and download them for LLM processing.
- Add extractCardImageKeys() to recursively extract image keys from card JSON
- Support img elements (img_key, src) and icon elements (icon_key)
- Update downloadInboundMedia() to handle MsgTypeInteractive
- Add comprehensive unit tests for image extraction
Images are downloaded and stored via MediaStore, then appended to
the message content as [image: photo] tags for LLM visibility.
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): simplify card parsing - pass raw JSON, only extract images
Address review feedback: text extraction cannot exhaustively handle all
card formats (i18n_elements, div.fields, etc.). Pass raw JSON to LLM
instead - same approach as MsgTypePost. Only image extraction remains
as images must be downloaded for LLM to process.
- Remove extractCardText() and helper functions
- extractContent() now returns raw JSON for MsgTypeInteractive
- Keep extractCardImageKeys() for downloading embedded images
- Update tests to expect raw JSON for interactive cards
* fix(feishu): don't append media tags to interactive card JSON
Appending media tags like "[attachment]" to raw JSON content produces
invalid JSON format. For interactive cards, the JSON already contains
image information and media refs are downloaded separately.
- Skip appendMediaTags for MsgTypeInteractive to preserve valid JSON
- Add test case for interactive card with images
* fix(feishu): filter out external URLs from card image extraction
Only Feishu-hosted image keys (img_xxx, icon_xxx) can be downloaded via
the Feishu API. External URLs in src field (https://...) should be
filtered out to avoid download failures.
- Add isFeishuImageKey() to detect Feishu-hosted keys vs external URLs
- Update extractImageKeysRecursive to skip external URLs in src field
- Add tests for external URL filtering and mixed scenarios
* feat(feishu): support downloading external images from interactive cards
Previously only Feishu-hosted images (img_key, icon_key) could be
downloaded. Now external URLs in src field are also downloaded via
HTTP and made available to the LLM.
- extractCardImageKeys now returns two slices: Feishu keys and external URLs
- Add downloadExternalImage to download images from HTTP URLs
- Update downloadInboundMedia to handle both Feishu API and HTTP downloads
- Update tests for new function signature
* fix(feishu): use HTTP client with timeout for external image downloads
Replaced http.DefaultClient with a client that has a 30-second timeout
to prevent hanging on unresponsive external URLs.
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): resolve lint errors for shadow and formatting
- Rename err variables to avoid shadowing in downloadExternalImage
- Fix struct field alignment in TestExtractCardImageKeys
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* refactor(feishu): pass external image URLs to LLM instead of downloading
Instead of downloading external images from interactive cards, pass
the URLs directly to LLM. This reduces network overhead and lets
vision-capable models fetch images as needed.
- Remove downloadExternalImage function
- Append external URLs to card content for LLM processing
- Only download Feishu-hosted images via API
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): add blank line between functions for gci formatting
* fix(feishu): keep interactive card content as valid JSON
2026-03-20 04:59:43 +00:00
|
|
|
case larkim.MsgTypeInteractive:
|
|
|
|
|
// Extract and download images embedded in interactive cards
|
|
|
|
|
feishuKeys, _ := extractCardImageKeys(rawContent)
|
|
|
|
|
// Download Feishu-hosted images via API
|
|
|
|
|
for _, imageKey := range feishuKeys {
|
|
|
|
|
ref := c.downloadResource(ctx, messageID, imageKey, "image", ".jpg", store, scope)
|
|
|
|
|
if ref != "" {
|
|
|
|
|
refs = append(refs, ref)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
// External URLs are passed directly to LLM, not downloaded
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
case larkim.MsgTypeFile, larkim.MsgTypeAudio, larkim.MsgTypeMedia:
|
|
|
|
|
fileKey := extractFileKey(rawContent)
|
|
|
|
|
if fileKey == "" {
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
// Derive a fallback extension from the message type.
|
|
|
|
|
var ext string
|
|
|
|
|
switch messageType {
|
|
|
|
|
case larkim.MsgTypeAudio:
|
|
|
|
|
ext = ".ogg"
|
|
|
|
|
case larkim.MsgTypeMedia:
|
|
|
|
|
ext = ".mp4"
|
|
|
|
|
default:
|
|
|
|
|
ext = "" // generic file — rely on resp.FileName
|
|
|
|
|
}
|
|
|
|
|
ref := c.downloadResource(ctx, messageID, fileKey, "file", ext, store, scope)
|
|
|
|
|
if ref != "" {
|
|
|
|
|
refs = append(refs, ref)
|
|
|
|
|
}
|
2026-02-22 22:56:48 +00:00
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return refs
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// downloadResource downloads a message resource (image/file) from Feishu,
|
|
|
|
|
// writes it to the project media directory, and stores the reference in MediaStore.
|
|
|
|
|
// fallbackExt (e.g. ".jpg") is appended when the resolved filename has no extension.
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
//
|
|
|
|
|
// For image resources, if the primary MessageResource.Get API fails (which
|
|
|
|
|
// requires im:message or im:message:readonly scope), a fallback to the
|
|
|
|
|
// Image.Get API (which requires im:resource scope) is attempted. This ensures
|
|
|
|
|
// image downloads succeed regardless of which permission the user has granted.
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
func (c *FeishuChannel) downloadResource(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
messageID, fileKey, resourceType, fallbackExt string,
|
|
|
|
|
store media.MediaStore,
|
|
|
|
|
scope string,
|
|
|
|
|
) string {
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
file, filename := c.fetchResourceData(ctx, messageID, fileKey, resourceType)
|
|
|
|
|
if file == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
if closer, ok := file.(io.Closer); ok {
|
|
|
|
|
defer closer.Close()
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if filename == "" {
|
|
|
|
|
filename = fileKey
|
|
|
|
|
}
|
|
|
|
|
if filepath.Ext(filename) == "" && fallbackExt != "" {
|
|
|
|
|
filename += fallbackExt
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return c.storeResourceFile(ctx, messageID, fileKey, filename, file, store, scope)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fetchResourceData tries to download a resource from Feishu, first via
|
|
|
|
|
// MessageResource.Get, then falling back to Image.Get for image resources.
|
|
|
|
|
func (c *FeishuChannel) fetchResourceData(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
messageID, fileKey, resourceType string,
|
|
|
|
|
) (io.Reader, string) {
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
req := larkim.NewGetMessageResourceReqBuilder().
|
|
|
|
|
MessageId(messageID).
|
|
|
|
|
FileKey(fileKey).
|
|
|
|
|
Type(resourceType).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.MessageResource.Get(ctx, req)
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
if err == nil && resp.Success() && resp.File != nil {
|
|
|
|
|
return resp.File, resp.FileName
|
|
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
if err != nil {
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
logger.WarnCF("feishu", "MessageResource.Get failed", map[string]any{
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
"message_id": messageID,
|
|
|
|
|
"file_key": fileKey,
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
} else if !resp.Success() {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(resp.Code)
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
logger.WarnCF("feishu", "MessageResource.Get api error", map[string]any{
|
|
|
|
|
"message_id": messageID,
|
|
|
|
|
"file_key": fileKey,
|
|
|
|
|
"code": resp.Code,
|
|
|
|
|
"msg": resp.Msg,
|
|
|
|
|
})
|
|
|
|
|
} else {
|
|
|
|
|
logger.WarnCF("feishu", "MessageResource.Get returned empty file body", map[string]any{
|
|
|
|
|
"message_id": messageID,
|
|
|
|
|
"file_key": fileKey,
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
if resourceType != "image" {
|
|
|
|
|
return nil, ""
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
return c.fetchImageDirect(ctx, fileKey)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// fetchImageDirect downloads an image using the Image.Get API
|
|
|
|
|
// (/open-apis/im/v1/images/:image_key), which requires the im:resource scope.
|
|
|
|
|
func (c *FeishuChannel) fetchImageDirect(ctx context.Context, imageKey string) (io.Reader, string) {
|
|
|
|
|
req := larkim.NewGetImageReqBuilder().
|
|
|
|
|
ImageKey(imageKey).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.Image.Get(ctx, req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.ErrorCF("feishu", "Image.Get fallback failed", map[string]any{
|
|
|
|
|
"image_key": imageKey,
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
return nil, ""
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
if !resp.Success() {
|
|
|
|
|
c.invalidateTokenOnAuthError(resp.Code)
|
|
|
|
|
logger.ErrorCF("feishu", "Image.Get fallback api error", map[string]any{
|
|
|
|
|
"image_key": imageKey,
|
|
|
|
|
"code": resp.Code,
|
|
|
|
|
"msg": resp.Msg,
|
|
|
|
|
})
|
|
|
|
|
return nil, ""
|
|
|
|
|
}
|
|
|
|
|
if resp.File == nil {
|
|
|
|
|
return nil, ""
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
logger.DebugCF("feishu", "Image downloaded via Image.Get fallback", map[string]any{
|
|
|
|
|
"image_key": imageKey,
|
|
|
|
|
})
|
|
|
|
|
return resp.File, resp.FileName
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// storeResourceFile writes downloaded resource data to disk and registers it in the MediaStore.
|
|
|
|
|
func (c *FeishuChannel) storeResourceFile(
|
|
|
|
|
ctx context.Context,
|
|
|
|
|
messageID, fileKey, filename string,
|
|
|
|
|
file io.Reader,
|
|
|
|
|
store media.MediaStore,
|
|
|
|
|
scope string,
|
|
|
|
|
) string {
|
2026-03-14 04:01:47 +00:00
|
|
|
mediaDir := media.TempDir()
|
2026-03-02 17:04:06 +00:00
|
|
|
if mkdirErr := os.MkdirAll(mediaDir, 0o700); mkdirErr != nil {
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
logger.ErrorCF("feishu", "Failed to create media directory", map[string]any{
|
2026-03-02 17:04:06 +00:00
|
|
|
"error": mkdirErr.Error(),
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
})
|
|
|
|
|
return ""
|
|
|
|
|
}
|
2026-03-02 17:27:39 +00:00
|
|
|
ext := filepath.Ext(filename)
|
|
|
|
|
localPath := filepath.Join(mediaDir, utils.SanitizeFilename(messageID+"-"+fileKey+ext))
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
|
|
|
|
|
out, err := os.Create(localPath)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.ErrorCF("feishu", "Failed to create local file for resource", map[string]any{
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
if _, copyErr := io.Copy(out, file); copyErr != nil {
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
out.Close()
|
|
|
|
|
os.Remove(localPath)
|
|
|
|
|
logger.ErrorCF("feishu", "Failed to write resource to file", map[string]any{
|
2026-03-02 17:04:06 +00:00
|
|
|
"error": copyErr.Error(),
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
})
|
|
|
|
|
return ""
|
|
|
|
|
}
|
2026-06-07 03:56:09 +00:00
|
|
|
if err := out.Close(); err != nil {
|
|
|
|
|
logger.ErrorCF("feishu", "Failed to close downloaded resource file", map[string]any{
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
os.Remove(localPath)
|
|
|
|
|
return ""
|
|
|
|
|
}
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
|
|
|
|
|
ref, err := store.Store(localPath, media.MediaMeta{
|
2026-03-23 04:13:59 +00:00
|
|
|
Filename: filename,
|
|
|
|
|
Source: "feishu",
|
|
|
|
|
CleanupPolicy: media.CleanupPolicyDeleteOnCleanup,
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}, scope)
|
|
|
|
|
if err != nil {
|
|
|
|
|
logger.ErrorCF("feishu", "Failed to store downloaded resource", map[string]any{
|
|
|
|
|
"file_key": fileKey,
|
|
|
|
|
"error": err.Error(),
|
|
|
|
|
})
|
|
|
|
|
os.Remove(localPath)
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ref
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// appendMediaTags appends media type tags to content (like Telegram's "[image: photo]").
|
Feat/feishu card parsing (#1534)
* feat(feishu): add interactive card message parsing
Add support for parsing inbound Feishu interactive card messages.
When a user sends a card message, the text content is now extracted
and passed to the LLM for processing.
- Add extractCardText() to recursively extract text from card JSON
- Support both JSON 1.0 (legacy) and JSON 2.0 schema formats
- Handle nested elements: header, body, actions, columns
- Extract text from markdown, lark_md, and plain_text elements
- Add comprehensive unit tests for card parsing
Fixes #<issue_number>
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* feat(feishu): extract and download images from interactive cards
When receiving interactive card messages, extract embedded images
(img_key, src, icon_key) and download them for LLM processing.
- Add extractCardImageKeys() to recursively extract image keys from card JSON
- Support img elements (img_key, src) and icon elements (icon_key)
- Update downloadInboundMedia() to handle MsgTypeInteractive
- Add comprehensive unit tests for image extraction
Images are downloaded and stored via MediaStore, then appended to
the message content as [image: photo] tags for LLM visibility.
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): simplify card parsing - pass raw JSON, only extract images
Address review feedback: text extraction cannot exhaustively handle all
card formats (i18n_elements, div.fields, etc.). Pass raw JSON to LLM
instead - same approach as MsgTypePost. Only image extraction remains
as images must be downloaded for LLM to process.
- Remove extractCardText() and helper functions
- extractContent() now returns raw JSON for MsgTypeInteractive
- Keep extractCardImageKeys() for downloading embedded images
- Update tests to expect raw JSON for interactive cards
* fix(feishu): don't append media tags to interactive card JSON
Appending media tags like "[attachment]" to raw JSON content produces
invalid JSON format. For interactive cards, the JSON already contains
image information and media refs are downloaded separately.
- Skip appendMediaTags for MsgTypeInteractive to preserve valid JSON
- Add test case for interactive card with images
* fix(feishu): filter out external URLs from card image extraction
Only Feishu-hosted image keys (img_xxx, icon_xxx) can be downloaded via
the Feishu API. External URLs in src field (https://...) should be
filtered out to avoid download failures.
- Add isFeishuImageKey() to detect Feishu-hosted keys vs external URLs
- Update extractImageKeysRecursive to skip external URLs in src field
- Add tests for external URL filtering and mixed scenarios
* feat(feishu): support downloading external images from interactive cards
Previously only Feishu-hosted images (img_key, icon_key) could be
downloaded. Now external URLs in src field are also downloaded via
HTTP and made available to the LLM.
- extractCardImageKeys now returns two slices: Feishu keys and external URLs
- Add downloadExternalImage to download images from HTTP URLs
- Update downloadInboundMedia to handle both Feishu API and HTTP downloads
- Update tests for new function signature
* fix(feishu): use HTTP client with timeout for external image downloads
Replaced http.DefaultClient with a client that has a 30-second timeout
to prevent hanging on unresponsive external URLs.
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): resolve lint errors for shadow and formatting
- Rename err variables to avoid shadowing in downloadExternalImage
- Fix struct field alignment in TestExtractCardImageKeys
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* refactor(feishu): pass external image URLs to LLM instead of downloading
Instead of downloading external images from interactive cards, pass
the URLs directly to LLM. This reduces network overhead and lets
vision-capable models fetch images as needed.
- Remove downloadExternalImage function
- Append external URLs to card content for LLM processing
- Only download Feishu-hosted images via API
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): add blank line between functions for gci formatting
* fix(feishu): keep interactive card content as valid JSON
2026-03-20 04:59:43 +00:00
|
|
|
// For interactive cards, media tags are not appended because content is raw JSON
|
|
|
|
|
// and appending would produce invalid JSON format.
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
func appendMediaTags(content, messageType string, mediaRefs []string) string {
|
|
|
|
|
if len(mediaRefs) == 0 {
|
|
|
|
|
return content
|
|
|
|
|
}
|
|
|
|
|
|
fix(feishu): fix image download with API fallback and post image support (#2708)
* fix(feishu): fix image download with API fallback and post image support
- Add Image.Get API fallback when MessageResource.Get fails (different
permission scope: im:resource vs im:message:readonly)
- Extract and download images from post (rich text) messages
- Extract images from interactive card messages
- Deduplicate post image keys across locales
- Add comprehensive tests for new helpers
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* feat(media): add image path tags alongside base64 for LLM file access
Images are still base64-encoded into msg.Media for multimodal LLMs,
but now also get [image:path] tags injected into message content so
the LLM knows the local file path for save/forward operations.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* refactor(media): only auto-inject images for tool results, not user messages
Channel-received images (role=user) now get path tags only, letting
the LLM decide whether to view via load_image or just operate on
the file. Tool result images (role=tool, e.g. load_image) are
base64-encoded into a synthetic user message appended after the tool
message, since many LLM APIs don't support image_url in tool messages.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): preserve tool-message ordering for multi-tool-call scenarios
Move synthetic user message (carrying base64 tool images) to after the
entire contiguous tool-message block instead of immediately after each
tool message. This preserves the assistant→tool→tool ordering required
by OpenAI-compatible APIs.
Also fix load_image to use generic [image: photo] placeholder so
injectPathTags can properly replace it with the actual path.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(test): update load_image test for [image: photo] placeholder
The test was checking ForLLM for the media:// ref, but load_image now
emits the generic [image: photo] placeholder instead.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): match all channel image placeholders in injectPathTags
Different channels emit different placeholder formats — Telegram/Feishu
use [image: photo], WeCom/WeChat/Line use bare [image], QQ/Discord use
[image: <filename>]. The previous string-match code only handled
[image: photo], so for the other channels the path tag was appended as
a duplicate, producing content like "[image] [image:/path]".
Switch to per-type regex that matches all generic placeholder shapes
while leaving path tags ([image:/path]) untouched. Also fixes the same
issue for [audio], [video], [file] tags. Added test coverage for the
various placeholder shapes.
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
* fix(media): skip path tag append for JSON content (Feishu cards/posts)
When content is structured JSON (interactive cards, post messages),
injectPathTags now skips the fallback append — only placeholder
replacement is attempted. This prevents corrupting JSON payloads
like {"schema":"2.0",...} with appended [image:/path] tags.
Adds looksLikeJSON() helper and three test cases covering JSON
objects, arrays, and an end-to-end resolveMediaRefs scenario with
Feishu card content.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
* fix(media): prepend path tags for JSON content, narrow looksLikeJSON
Two fixes from code review:
1. looksLikeJSON now only checks for '{' prefix (not '['), avoiding
false positives on regular text like "[update] see attached".
2. For JSON content (Feishu cards/posts), path tags are prepended
before the JSON instead of being silently dropped. This ensures
the LLM can discover attached images via the path tag while the
JSON payload stays valid for downstream parsing.
Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
---------
Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-30 03:08:00 +00:00
|
|
|
// Don't append tags to JSON content - would produce invalid JSON
|
|
|
|
|
if messageType == larkim.MsgTypeInteractive || messageType == larkim.MsgTypePost {
|
Feat/feishu card parsing (#1534)
* feat(feishu): add interactive card message parsing
Add support for parsing inbound Feishu interactive card messages.
When a user sends a card message, the text content is now extracted
and passed to the LLM for processing.
- Add extractCardText() to recursively extract text from card JSON
- Support both JSON 1.0 (legacy) and JSON 2.0 schema formats
- Handle nested elements: header, body, actions, columns
- Extract text from markdown, lark_md, and plain_text elements
- Add comprehensive unit tests for card parsing
Fixes #<issue_number>
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* feat(feishu): extract and download images from interactive cards
When receiving interactive card messages, extract embedded images
(img_key, src, icon_key) and download them for LLM processing.
- Add extractCardImageKeys() to recursively extract image keys from card JSON
- Support img elements (img_key, src) and icon elements (icon_key)
- Update downloadInboundMedia() to handle MsgTypeInteractive
- Add comprehensive unit tests for image extraction
Images are downloaded and stored via MediaStore, then appended to
the message content as [image: photo] tags for LLM visibility.
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): simplify card parsing - pass raw JSON, only extract images
Address review feedback: text extraction cannot exhaustively handle all
card formats (i18n_elements, div.fields, etc.). Pass raw JSON to LLM
instead - same approach as MsgTypePost. Only image extraction remains
as images must be downloaded for LLM to process.
- Remove extractCardText() and helper functions
- extractContent() now returns raw JSON for MsgTypeInteractive
- Keep extractCardImageKeys() for downloading embedded images
- Update tests to expect raw JSON for interactive cards
* fix(feishu): don't append media tags to interactive card JSON
Appending media tags like "[attachment]" to raw JSON content produces
invalid JSON format. For interactive cards, the JSON already contains
image information and media refs are downloaded separately.
- Skip appendMediaTags for MsgTypeInteractive to preserve valid JSON
- Add test case for interactive card with images
* fix(feishu): filter out external URLs from card image extraction
Only Feishu-hosted image keys (img_xxx, icon_xxx) can be downloaded via
the Feishu API. External URLs in src field (https://...) should be
filtered out to avoid download failures.
- Add isFeishuImageKey() to detect Feishu-hosted keys vs external URLs
- Update extractImageKeysRecursive to skip external URLs in src field
- Add tests for external URL filtering and mixed scenarios
* feat(feishu): support downloading external images from interactive cards
Previously only Feishu-hosted images (img_key, icon_key) could be
downloaded. Now external URLs in src field are also downloaded via
HTTP and made available to the LLM.
- extractCardImageKeys now returns two slices: Feishu keys and external URLs
- Add downloadExternalImage to download images from HTTP URLs
- Update downloadInboundMedia to handle both Feishu API and HTTP downloads
- Update tests for new function signature
* fix(feishu): use HTTP client with timeout for external image downloads
Replaced http.DefaultClient with a client that has a 30-second timeout
to prevent hanging on unresponsive external URLs.
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): resolve lint errors for shadow and formatting
- Rename err variables to avoid shadowing in downloadExternalImage
- Fix struct field alignment in TestExtractCardImageKeys
Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* refactor(feishu): pass external image URLs to LLM instead of downloading
Instead of downloading external images from interactive cards, pass
the URLs directly to LLM. This reduces network overhead and lets
vision-capable models fetch images as needed.
- Remove downloadExternalImage function
- Append external URLs to card content for LLM processing
- Only download Feishu-hosted images via API
💘 Generated with Crush
Assisted-by: GLM-5 via Crush <crush@charm.land>
* fix(feishu): add blank line between functions for gci formatting
* fix(feishu): keep interactive card content as valid JSON
2026-03-20 04:59:43 +00:00
|
|
|
return content
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var tag string
|
|
|
|
|
switch messageType {
|
|
|
|
|
case larkim.MsgTypeImage:
|
|
|
|
|
tag = "[image: photo]"
|
|
|
|
|
case larkim.MsgTypeAudio:
|
|
|
|
|
tag = "[audio]"
|
|
|
|
|
case larkim.MsgTypeMedia:
|
|
|
|
|
tag = "[video]"
|
|
|
|
|
case larkim.MsgTypeFile:
|
|
|
|
|
tag = "[file]"
|
|
|
|
|
default:
|
|
|
|
|
tag = "[attachment]"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if content == "" {
|
|
|
|
|
return tag
|
|
|
|
|
}
|
|
|
|
|
return content + " " + tag
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sendCard sends an interactive card message to a chat.
|
2026-04-23 02:35:50 +00:00
|
|
|
func (c *FeishuChannel) sendCard(ctx context.Context, chatID, cardContent string) (string, error) {
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
req := larkim.NewCreateMessageReqBuilder().
|
2026-06-04 21:19:04 +00:00
|
|
|
ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
Body(larkim.NewCreateMessageReqBodyBuilder().
|
|
|
|
|
ReceiveId(chatID).
|
|
|
|
|
MsgType(larkim.MsgTypeInteractive).
|
|
|
|
|
Content(cardContent).
|
|
|
|
|
Build()).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.Message.Create(ctx, req)
|
|
|
|
|
if err != nil {
|
2026-04-23 02:35:50 +00:00
|
|
|
return "", fmt.Errorf("feishu send card: %w", channels.ErrTemporary)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !resp.Success() {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(resp.Code)
|
2026-04-23 02:35:50 +00:00
|
|
|
return "", fmt.Errorf("feishu api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.DebugCF("feishu", "Feishu card message sent", map[string]any{
|
|
|
|
|
"chat_id": chatID,
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-23 02:35:50 +00:00
|
|
|
if resp.Data != nil && resp.Data.MessageId != nil {
|
|
|
|
|
return *resp.Data.MessageId, nil
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
}
|
|
|
|
|
|
2026-03-19 15:46:17 +00:00
|
|
|
// sendText sends a plain text message to a chat (fallback when card fails).
|
2026-04-23 02:35:50 +00:00
|
|
|
func (c *FeishuChannel) sendText(ctx context.Context, chatID, text string) (string, error) {
|
2026-03-19 15:46:17 +00:00
|
|
|
content, _ := json.Marshal(map[string]string{"text": text})
|
|
|
|
|
|
|
|
|
|
req := larkim.NewCreateMessageReqBuilder().
|
2026-06-04 21:19:04 +00:00
|
|
|
ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).
|
2026-03-19 15:46:17 +00:00
|
|
|
Body(larkim.NewCreateMessageReqBodyBuilder().
|
|
|
|
|
ReceiveId(chatID).
|
|
|
|
|
MsgType(larkim.MsgTypeText).
|
|
|
|
|
Content(string(content)).
|
|
|
|
|
Build()).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.Message.Create(ctx, req)
|
|
|
|
|
if err != nil {
|
2026-04-23 02:35:50 +00:00
|
|
|
return "", fmt.Errorf("feishu send text: %w", channels.ErrTemporary)
|
2026-03-19 15:46:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if !resp.Success() {
|
2026-04-23 02:35:50 +00:00
|
|
|
return "", fmt.Errorf("feishu text api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary)
|
2026-03-19 15:46:17 +00:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
logger.DebugCF("feishu", "Feishu text message sent (fallback)", map[string]any{
|
|
|
|
|
"chat_id": chatID,
|
|
|
|
|
})
|
|
|
|
|
|
2026-04-23 02:35:50 +00:00
|
|
|
if resp.Data != nil && resp.Data.MessageId != nil {
|
|
|
|
|
return *resp.Data.MessageId, nil
|
|
|
|
|
}
|
|
|
|
|
return "", nil
|
2026-03-19 15:46:17 +00:00
|
|
|
}
|
|
|
|
|
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
// sendImage uploads an image and sends it as a message.
|
|
|
|
|
func (c *FeishuChannel) sendImage(ctx context.Context, chatID string, file *os.File) error {
|
|
|
|
|
// Upload image to get image_key
|
|
|
|
|
uploadReq := larkim.NewCreateImageReqBuilder().
|
|
|
|
|
Body(larkim.NewCreateImageReqBodyBuilder().
|
|
|
|
|
ImageType("message").
|
|
|
|
|
Image(file).
|
|
|
|
|
Build()).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
uploadResp, err := c.client.Im.V1.Image.Create(ctx, uploadReq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("feishu image upload: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !uploadResp.Success() {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(uploadResp.Code)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return fmt.Errorf("feishu image upload api error (code=%d msg=%s)", uploadResp.Code, uploadResp.Msg)
|
|
|
|
|
}
|
|
|
|
|
if uploadResp.Data == nil || uploadResp.Data.ImageKey == nil {
|
|
|
|
|
return fmt.Errorf("feishu image upload: no image_key returned")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
imageKey := *uploadResp.Data.ImageKey
|
|
|
|
|
|
|
|
|
|
// Send image message
|
|
|
|
|
content, _ := json.Marshal(map[string]string{"image_key": imageKey})
|
|
|
|
|
req := larkim.NewCreateMessageReqBuilder().
|
2026-06-04 21:19:04 +00:00
|
|
|
ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
Body(larkim.NewCreateMessageReqBodyBuilder().
|
|
|
|
|
ReceiveId(chatID).
|
|
|
|
|
MsgType(larkim.MsgTypeImage).
|
|
|
|
|
Content(string(content)).
|
|
|
|
|
Build()).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.Message.Create(ctx, req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("feishu image send: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !resp.Success() {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(resp.Code)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return fmt.Errorf("feishu image send api error (code=%d msg=%s)", resp.Code, resp.Msg)
|
|
|
|
|
}
|
|
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// sendFile uploads a file and sends it as a message.
|
|
|
|
|
func (c *FeishuChannel) sendFile(ctx context.Context, chatID string, file *os.File, filename, fileType string) error {
|
|
|
|
|
// Map part type to Feishu file type
|
|
|
|
|
feishuFileType := "stream"
|
|
|
|
|
switch fileType {
|
|
|
|
|
case "audio":
|
|
|
|
|
feishuFileType = "opus"
|
|
|
|
|
case "video":
|
|
|
|
|
feishuFileType = "mp4"
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Upload file to get file_key
|
|
|
|
|
uploadReq := larkim.NewCreateFileReqBuilder().
|
|
|
|
|
Body(larkim.NewCreateFileReqBodyBuilder().
|
|
|
|
|
FileType(feishuFileType).
|
|
|
|
|
FileName(filename).
|
|
|
|
|
File(file).
|
|
|
|
|
Build()).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
uploadResp, err := c.client.Im.V1.File.Create(ctx, uploadReq)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("feishu file upload: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !uploadResp.Success() {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(uploadResp.Code)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return fmt.Errorf("feishu file upload api error (code=%d msg=%s)", uploadResp.Code, uploadResp.Msg)
|
|
|
|
|
}
|
|
|
|
|
if uploadResp.Data == nil || uploadResp.Data.FileKey == nil {
|
|
|
|
|
return fmt.Errorf("feishu file upload: no file_key returned")
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
fileKey := *uploadResp.Data.FileKey
|
|
|
|
|
|
|
|
|
|
// Send file message
|
|
|
|
|
content, _ := json.Marshal(map[string]string{"file_key": fileKey})
|
|
|
|
|
req := larkim.NewCreateMessageReqBuilder().
|
2026-06-04 21:19:04 +00:00
|
|
|
ReceiveIdType(larkim.CreateMessageV1ReceiveIDTypeChatId).
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
Body(larkim.NewCreateMessageReqBodyBuilder().
|
|
|
|
|
ReceiveId(chatID).
|
|
|
|
|
MsgType(larkim.MsgTypeFile).
|
|
|
|
|
Content(string(content)).
|
|
|
|
|
Build()).
|
|
|
|
|
Build()
|
|
|
|
|
|
|
|
|
|
resp, err := c.client.Im.V1.Message.Create(ctx, req)
|
|
|
|
|
if err != nil {
|
|
|
|
|
return fmt.Errorf("feishu file send: %w", err)
|
|
|
|
|
}
|
|
|
|
|
if !resp.Success() {
|
2026-03-18 11:07:49 +00:00
|
|
|
c.invalidateTokenOnAuthError(resp.Code)
|
feat(feishu): enhance channel with markdown cards, media, mentions, and editing
Upgrade the Feishu channel from basic text-only to full feature parity with
Telegram/Discord: interactive card messages with markdown rendering, message
editing (MessageEditor), placeholder messages (PlaceholderCapable), emoji
reactions (ReactionCapable), and inbound/outbound media support (MediaSender).
Also add @mention detection with lazy bot open_id discovery, group trigger
filtering with mention awareness, and multi-type inbound message parsing
(text, post, image, file, audio, video).
2026-03-02 16:49:11 +00:00
|
|
|
return fmt.Errorf("feishu file send api error (code=%d msg=%s)", resp.Code, resp.Msg)
|
|
|
|
|
}
|
2026-02-20 15:25:44 +00:00
|
|
|
return nil
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func extractFeishuSenderID(sender *larkim.EventSender) string {
|
|
|
|
|
if sender == nil || sender.SenderId == nil {
|
|
|
|
|
return ""
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
if sender.SenderId.UserId != nil && *sender.SenderId.UserId != "" {
|
|
|
|
|
return *sender.SenderId.UserId
|
|
|
|
|
}
|
|
|
|
|
if sender.SenderId.OpenId != nil && *sender.SenderId.OpenId != "" {
|
|
|
|
|
return *sender.SenderId.OpenId
|
|
|
|
|
}
|
|
|
|
|
if sender.SenderId.UnionId != nil && *sender.SenderId.UnionId != "" {
|
|
|
|
|
return *sender.SenderId.UnionId
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
return ""
|
|
|
|
|
}
|
2026-03-18 11:07:49 +00:00
|
|
|
|
|
|
|
|
// invalidateTokenOnAuthError clears the cached tenant_access_token when the
|
|
|
|
|
// Feishu API reports it as invalid (99991663), so the next request fetches a
|
|
|
|
|
// fresh one. The Lark SDK's built-in retry does not clear the cache, causing
|
|
|
|
|
// all API calls to fail until the token naturally expires (~2 hours).
|
|
|
|
|
func (c *FeishuChannel) invalidateTokenOnAuthError(code int) {
|
|
|
|
|
if code == errCodeTenantTokenInvalid {
|
|
|
|
|
c.tokenCache.InvalidateAll()
|
|
|
|
|
logger.WarnCF("feishu", "Invalidated cached token due to auth error", nil)
|
|
|
|
|
}
|
|
|
|
|
}
|