feat(feishu): 添加飞书消息反应功能

- 在配置中新增 react_emoji 字段用于设置反应表情类型
- 实现 ReactToMessage 方法为入站消息添加表情反应
- 添加 addReaction 和 removeReaction 方法管理反应操作
- 提供撤消函数支持移除已添加的表情反应
- 默认使用 OK 表情作为反应类型
This commit is contained in:
ario 2026-03-01 22:01:22 +08:00
parent 302cd05be4
commit 99a50eb3b2
2 changed files with 102 additions and 0 deletions

View file

@ -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

View file

@ -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 {