diff --git a/pkg/channels/feishu/feishu_64.go b/pkg/channels/feishu/feishu_64.go index 9c462e41ee..f044e953ba 100644 --- a/pkg/channels/feishu/feishu_64.go +++ b/pkg/channels/feishu/feishu_64.go @@ -11,6 +11,7 @@ import ( "net/http" "os" "path/filepath" + "strings" "sync" "sync/atomic" @@ -112,6 +113,7 @@ func (c *FeishuChannel) Stop(ctx context.Context) error { } // Send sends a message using Interactive Card format for markdown rendering. +// Falls back to plain text message if card sending fails (e.g., table limit exceeded). func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { if !c.IsRunning() { return channels.ErrNotRunning @@ -124,9 +126,38 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error // Build interactive card with markdown content cardContent, err := buildMarkdownCard(msg.Content) if err != nil { - return fmt.Errorf("feishu send: card build failed: %w", err) + // If card build fails, fall back to plain text + return c.sendText(ctx, msg.ChatID, msg.Content) } - return c.sendCard(ctx, msg.ChatID, cardContent) + + // First attempt: try sending as interactive card + err = c.sendCard(ctx, msg.ChatID, cardContent) + if err == nil { + return nil + } + + // 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 + textErr := c.sendText(ctx, msg.ChatID, msg.Content) + if textErr == nil { + return nil + } + // If text also fails, return the text error + return textErr + } + + // For other errors, return the original card error + return err } // EditMessage implements channels.MessageEditor. @@ -715,6 +746,35 @@ func (c *FeishuChannel) sendCard(ctx context.Context, chatID, cardContent string return nil } +// sendText sends a plain text message to a chat (fallback when card fails). +func (c *FeishuChannel) sendText(ctx context.Context, chatID, text string) error { + content, _ := json.Marshal(map[string]string{"text": text}) + + req := larkim.NewCreateMessageReqBuilder(). + ReceiveIdType(larkim.ReceiveIdTypeChatId). + 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 { + return fmt.Errorf("feishu send text: %w", channels.ErrTemporary) + } + + if !resp.Success() { + return fmt.Errorf("feishu text api error (code=%d msg=%s): %w", resp.Code, resp.Msg, channels.ErrTemporary) + } + + logger.DebugCF("feishu", "Feishu text message sent (fallback)", map[string]any{ + "chat_id": chatID, + }) + + return nil +} + // 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