From 9f017d077e9bbec678ba64ff39e8e67762a846c3 Mon Sep 17 00:00:00 2001 From: mutezebra Date: Fri, 6 Mar 2026 12:53:47 +0800 Subject: [PATCH 1/5] feat(feishu): add random reaction emoji config - Add random_reaction_emoji config for Feishu channel - Update .env.example with new env vars - Update documentation (README.zh.md) --- .env.example | 4 ++++ docs/channels/feishu/README.zh.md | 3 ++- pkg/channels/feishu/feishu_64.go | 17 +++++++++++++++-- pkg/config/config.go | 1 + 4 files changed, 22 insertions(+), 3 deletions(-) diff --git a/.env.example b/.env.example index bc68456d6..98fa7f868 100644 --- a/.env.example +++ b/.env.example @@ -12,6 +12,10 @@ # DISCORD_BOT_TOKEN=xxx # LINE_CHANNEL_SECRET=xxx # LINE_CHANNEL_ACCESS_TOKEN=xxx +# Feishu (飞书) +# PICOCLAW_CHANNELS_FEISHU_APP_ID=cli_xxx +# PICOCLAW_CHANNELS_FEISHU_APP_SECRET=xxx +# PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI=Typing,Onit # ── Web Search (optional) ──────────────── # BRAVE_SEARCH_API_KEY=BSA... diff --git a/docs/channels/feishu/README.zh.md b/docs/channels/feishu/README.zh.md index 310827723..8c1c267cb 100644 --- a/docs/channels/feishu/README.zh.md +++ b/docs/channels/feishu/README.zh.md @@ -26,7 +26,8 @@ | app_secret | string | 是 | 飞书应用的 App Secret | | encrypt_key | string | 否 | 事件回调加密密钥 | | verification_token | string | 否 | 用于Webhook事件验证的Token | -| allow_from | array | 否 | 用户ID白名单,空表示允许所有用户 | +| allow_from | array | 否 | 用户ID白名单,空表示所有用户 | +| random_reaction_emoji | array | 否 | 随机添加的表情列表,空则使用默认 "Pin" | ## 设置流程 diff --git a/pkg/channels/feishu/feishu_64.go b/pkg/channels/feishu/feishu_64.go index 00f73064d..7827962f4 100644 --- a/pkg/channels/feishu/feishu_64.go +++ b/pkg/channels/feishu/feishu_64.go @@ -7,6 +7,7 @@ import ( "encoding/json" "fmt" "io" + "math/rand" "net/http" "os" "path/filepath" @@ -195,18 +196,29 @@ func (c *FeishuChannel) SendPlaceholder(ctx context.Context, chatID string) (str } // ReactToMessage implements channels.ReactionCapable. -// Adds an "Pin" reaction and returns an undo function to remove it. +// Adds a reaction (randomly chosen from config) and returns an undo function to remove it. func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, messageID string) (func(), error) { + // Get emoji list from config + emojiList := c.config.RandomReactionEmoji + if len(emojiList) == 0 { + // Default to "Pin" if no config + emojiList = []string{"Pin"} + } + + // Randomly choose one from the list + chosenEmoji := emojiList[rand.Intn(len(emojiList))] + req := larkim.NewCreateMessageReactionReqBuilder(). MessageId(messageID). Body(larkim.NewCreateMessageReactionReqBodyBuilder(). - ReactionType(larkim.NewEmojiBuilder().EmojiType("Pin").Build()). + ReactionType(larkim.NewEmojiBuilder().EmojiType(chosenEmoji).Build()). 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{ + "emoji": chosenEmoji, "message_id": messageID, "error": err.Error(), }) @@ -214,6 +226,7 @@ func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, messageID st } if !resp.Success() { logger.ErrorCF("feishu", "Reaction API error", map[string]any{ + "emoji": chosenEmoji, "message_id": messageID, "code": resp.Code, "msg": resp.Msg, diff --git a/pkg/config/config.go b/pkg/config/config.go index 7a0ec323c..b517d8c70 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -268,6 +268,7 @@ type FeishuConfig struct { GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` Placeholder PlaceholderConfig `json:"placeholder,omitempty"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_FEISHU_REASONING_CHANNEL_ID"` + RandomReactionEmoji FlexibleStringSlice `json:"random_reaction_emoji" env:"PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI"` } type DiscordConfig struct { From 92a0db4993d2e44a677421386b8ca524d55cfc67 Mon Sep 17 00:00:00 2001 From: mutezebra Date: Fri, 6 Mar 2026 12:53:48 +0800 Subject: [PATCH 2/5] chore(feishu): document reaction emoji option and promote filetype dep Add an info log for `RandomReactionEmoji` configuration. --- docs/channels/feishu/README.zh.md | 1 + go.mod | 2 +- pkg/channels/feishu/feishu_64.go | 3 ++- 3 files changed, 4 insertions(+), 2 deletions(-) diff --git a/docs/channels/feishu/README.zh.md b/docs/channels/feishu/README.zh.md index 8c1c267cb..3fafffb7d 100644 --- a/docs/channels/feishu/README.zh.md +++ b/docs/channels/feishu/README.zh.md @@ -36,3 +36,4 @@ 3. 配置事件订阅和Webhook URL 4. 设置加密(可选,生产环境建议启用) 5. 将 App ID、App Secret、Encrypt Key 和 Verification Token(如果启用加密) 填入配置文件中 +6. 自定义你希望 PicoClaw react 你消息时的表情(可选, Reference URL: [Feishu Emoji List](https://open.larkoffice.com/document/server-docs/im-v1/message-reaction/emojis-introduce)) diff --git a/go.mod b/go.mod index 238bd405c..6fa3a900c 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/gdamore/tcell/v2 v2.13.8 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 + github.com/h2non/filetype v1.1.3 github.com/larksuite/oapi-sdk-go/v3 v3.5.3 github.com/mdp/qrterminal/v3 v3.2.1 github.com/modelcontextprotocol/go-sdk v1.3.0 @@ -37,7 +38,6 @@ require ( github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect - github.com/h2non/filetype v1.1.3 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect github.com/mattn/go-colorable v0.1.14 // indirect diff --git a/pkg/channels/feishu/feishu_64.go b/pkg/channels/feishu/feishu_64.go index 7827962f4..fdb3b8e22 100644 --- a/pkg/channels/feishu/feishu_64.go +++ b/pkg/channels/feishu/feishu_64.go @@ -204,6 +204,7 @@ func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, messageID st // Default to "Pin" if no config emojiList = []string{"Pin"} } + logger.Info(fmt.Sprintf("[MABEN] c.config.RandomReactionEmoji, %v", c.config.RandomReactionEmoji)) // Randomly choose one from the list chosenEmoji := emojiList[rand.Intn(len(emojiList))] @@ -218,7 +219,7 @@ func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, messageID st resp, err := c.client.Im.V1.MessageReaction.Create(ctx, req) if err != nil { logger.ErrorCF("feishu", "Failed to add reaction", map[string]any{ - "emoji": chosenEmoji, + "emoji": chosenEmoji, "message_id": messageID, "error": err.Error(), }) From 109a382507dd894282983721ec2416fb61a0678c Mon Sep 17 00:00:00 2001 From: mutezebra Date: Fri, 6 Mar 2026 12:53:48 +0800 Subject: [PATCH 3/5] chore(feishu): add random_reaction_emoji to example config --- config/config.example.json | 3 ++- 1 file changed, 2 insertions(+), 1 deletion(-) diff --git a/config/config.example.json b/config/config.example.json index 2f643d41b..f0e6e0c4b 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -98,7 +98,8 @@ "encrypt_key": "", "verification_token": "", "allow_from": [], - "reasoning_channel_id": "" + "reasoning_channel_id": "", + "random_reaction_emoji": [] }, "dingtalk": { "enabled": false, From 6aa1d02fff77912490e0d9a03508f6476571ee42 Mon Sep 17 00:00:00 2001 From: mutezebra Date: Sun, 8 Mar 2026 17:30:50 +0800 Subject: [PATCH 4/5] =?UTF-8?q?fix(feishu):=20=E7=94=A8=20crypto/rand=20?= =?UTF-8?q?=E9=80=89=E6=8B=A9=E9=9A=8F=E6=9C=BA=E8=A1=A8=E6=83=85=E5=B9=B6?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3=E7=A4=BA=E4=BE=8B=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .env.example | 2 +- pkg/channels/feishu/feishu_64.go | 14 ++++++++++---- 2 files changed, 11 insertions(+), 5 deletions(-) diff --git a/.env.example b/.env.example index 798815a7a..e0a07236e 100644 --- a/.env.example +++ b/.env.example @@ -12,7 +12,7 @@ # Feishu (飞书) # PICOCLAW_CHANNELS_FEISHU_APP_ID=cli_xxx # PICOCLAW_CHANNELS_FEISHU_APP_SECRET=xxx -# PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI=Typing,Onit +# PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI=Typing,OneSecond # ── Web Search (optional) ──────────────── # BRAVE_SEARCH_API_KEY=BSA... diff --git a/pkg/channels/feishu/feishu_64.go b/pkg/channels/feishu/feishu_64.go index fdb3b8e22..5217dd4e9 100644 --- a/pkg/channels/feishu/feishu_64.go +++ b/pkg/channels/feishu/feishu_64.go @@ -4,10 +4,11 @@ package feishu import ( "context" + "crypto/rand" "encoding/json" "fmt" "io" - "math/rand" + "math/big" "net/http" "os" "path/filepath" @@ -204,10 +205,15 @@ func (c *FeishuChannel) ReactToMessage(ctx context.Context, chatID, messageID st // Default to "Pin" if no config emojiList = []string{"Pin"} } - logger.Info(fmt.Sprintf("[MABEN] c.config.RandomReactionEmoji, %v", c.config.RandomReactionEmoji)) - // Randomly choose one from the list - chosenEmoji := emojiList[rand.Intn(len(emojiList))] + // Randomly choose one from the list using crypto/rand for better distribution + idx, err := rand.Int(rand.Reader, big.NewInt(int64(len(emojiList)))) + var chosenEmoji string + if err != nil { + chosenEmoji = emojiList[0] + } else { + chosenEmoji = emojiList[idx.Int64()] + } req := larkim.NewCreateMessageReactionReqBuilder(). MessageId(messageID). From 08d668c1657e3ddb03d173ecaf0f565585dfa401 Mon Sep 17 00:00:00 2001 From: mutezebra Date: Sun, 8 Mar 2026 17:32:24 +0800 Subject: [PATCH 5/5] =?UTF-8?q?chore(config):=20gofmt=20=E6=A0=BC=E5=BC=8F?= =?UTF-8?q?=E5=8C=96=20FeishuConfig=20=E5=AD=97=E6=AE=B5=E5=AF=B9=E9=BD=90?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- pkg/config/config.go | 18 +++++++++--------- 1 file changed, 9 insertions(+), 9 deletions(-) diff --git a/pkg/config/config.go b/pkg/config/config.go index 73733be64..f17481e1e 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -273,15 +273,15 @@ type TelegramConfig struct { } type FeishuConfig struct { - Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"` - AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"` - AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"` - EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"` - VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"` - AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"` - GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` - Placeholder PlaceholderConfig `json:"placeholder,omitempty"` - ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_FEISHU_REASONING_CHANNEL_ID"` + Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_FEISHU_ENABLED"` + AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_FEISHU_APP_ID"` + AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_FEISHU_APP_SECRET"` + EncryptKey string `json:"encrypt_key" env:"PICOCLAW_CHANNELS_FEISHU_ENCRYPT_KEY"` + VerificationToken string `json:"verification_token" env:"PICOCLAW_CHANNELS_FEISHU_VERIFICATION_TOKEN"` + AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_FEISHU_ALLOW_FROM"` + GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"` + Placeholder PlaceholderConfig `json:"placeholder,omitempty"` + ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_FEISHU_REASONING_CHANNEL_ID"` RandomReactionEmoji FlexibleStringSlice `json:"random_reaction_emoji" env:"PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI"` }