From 99a50eb3b2646fdbccd370c4e428bd872b9a7e68 Mon Sep 17 00:00:00 2001 From: ario Date: Sun, 1 Mar 2026 22:01:22 +0800 Subject: [PATCH] =?UTF-8?q?feat(feishu):=20=E6=B7=BB=E5=8A=A0=E9=A3=9E?= =?UTF-8?q?=E4=B9=A6=E6=B6=88=E6=81=AF=E5=8F=8D=E5=BA=94=E5=8A=9F=E8=83=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 在配置中新增 react_emoji 字段用于设置反应表情类型 - 实现 ReactToMessage 方法为入站消息添加表情反应 - 添加 addReaction 和 removeReaction 方法管理反应操作 - 提供撤消函数支持移除已添加的表情反应 - 默认使用 OK 表情作为反应类型 --- pkg/channels/feishu/feishu_64.go | 101 +++++++++++++++++++++++++++++++ pkg/config/config.go | 1 + 2 files changed, 102 insertions(+) diff --git a/pkg/channels/feishu/feishu_64.go b/pkg/channels/feishu/feishu_64.go index df2c172b6..6815866a0 100644 --- a/pkg/channels/feishu/feishu_64.go +++ b/pkg/channels/feishu/feishu_64.go @@ -93,6 +93,107 @@ func (c *FeishuChannel) Stop(ctx context.Context) error { return nil } +// ReactToMessage implements channels.ReactionCapable. +// It adds an emoji reaction to the inbound message and returns an undo function. +// Common emoji types: THUMBSUP, OK, EYES, DONE, HEART, ROCKET +func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) { + if messageID == "" { + return func() {}, nil + } + + emojiType := c.config.ReactEmoji + if emojiType == "" { + emojiType = "OK" // Default emoji + } + + // Add reaction and get reaction_id + reactionID, err := c.addReaction(ctx, messageID, emojiType) + if err != nil { + logger.DebugCF("feishu", "Failed to add reaction", map[string]any{ + "message_id": messageID, + "emoji": emojiType, + "error": err.Error(), + }) + // Return no-op on error + return func() {}, nil + } + + logger.DebugCF("feishu", "Added reaction to message", map[string]any{ + "message_id": messageID, + "emoji": emojiType, + "reaction_id": reactionID, + }) + + // Return undo function + return func() { + if reactionID != "" { + c.removeReaction(context.Background(), messageID, reactionID) + } + }, nil +} + +// addReaction adds an emoji reaction to a message and returns the reaction_id. +func (c *FeishuChannel) addReaction(ctx context.Context, messageID, emojiType string) (string, error) { + req := larkim.NewCreateMessageReactionReqBuilder(). + MessageId(messageID). + Body(larkim.NewCreateMessageReactionReqBodyBuilder(). + ReactionType(larkim.NewEmojiBuilder(). + EmojiType(emojiType). + Build()). + Build()). + Build() + + resp, err := c.client.Im.V1.MessageReaction.Create(ctx, req) + if err != nil { + return "", fmt.Errorf("feishu add reaction: %w", err) + } + + if !resp.Success() { + return "", fmt.Errorf("feishu add reaction failed: code=%d msg=%s", resp.Code, resp.Msg) + } + + if resp.Data != nil && resp.Data.ReactionId != nil { + return *resp.Data.ReactionId, nil + } + + return "", nil +} + +// removeReaction removes an emoji reaction from a message using reaction_id. +func (c *FeishuChannel) removeReaction(ctx context.Context, messageID, reactionID string) error { + req := larkim.NewDeleteMessageReactionReqBuilder(). + MessageId(messageID). + ReactionId(reactionID). + Build() + + resp, err := c.client.Im.V1.MessageReaction.Delete(ctx, req) + if err != nil { + logger.DebugCF("feishu", "Failed to remove reaction", map[string]any{ + "message_id": messageID, + "reaction_id": reactionID, + "error": err.Error(), + }) + return err + } + + if !resp.Success() { + logger.DebugCF("feishu", "Failed to remove reaction", map[string]any{ + "message_id": messageID, + "reaction_id": reactionID, + "code": resp.Code, + "msg": resp.Msg, + }) + return fmt.Errorf("feishu remove reaction failed: code=%d msg=%s", resp.Code, resp.Msg) + } + + logger.DebugCF("feishu", "Removed reaction from message", map[string]any{ + "message_id": messageID, + "reaction_id": reactionID, + }) + + return nil +} + func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error { if !c.IsRunning() { return channels.ErrNotRunning diff --git a/pkg/config/config.go b/pkg/config/config.go index d84772d2b..8576b25a7 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -252,6 +252,7 @@ type FeishuConfig struct { AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"` GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_FEISHU_REASONING_CHANNEL_ID"` + ReactEmoji string `json:"react_emoji" env:"PICOCLAW_CHANNELS_FEISHU_REACT_EMOJI"` // Emoji type for reaction, e.g. "THUMBSUP", "EYES", "OK" } type DiscordConfig struct {