diff --git a/.env.example b/.env.example index e899d2adc9..e0a07236e0 100644 --- a/.env.example +++ b/.env.example @@ -9,6 +9,10 @@ # ── Chat Channel ────────────────────────── # TELEGRAM_BOT_TOKEN=123456:ABC... # DISCORD_BOT_TOKEN=xxx +# Feishu (飞书) +# PICOCLAW_CHANNELS_FEISHU_APP_ID=cli_xxx +# PICOCLAW_CHANNELS_FEISHU_APP_SECRET=xxx +# PICOCLAW_CHANNELS_FEISHU_RANDOM_REACTION_EMOJI=Typing,OneSecond # ── Web Search (optional) ──────────────── # BRAVE_SEARCH_API_KEY=BSA... diff --git a/config/config.example.json b/config/config.example.json index 8e58e32521..c429261206 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, diff --git a/docs/channels/feishu/README.zh.md b/docs/channels/feishu/README.zh.md index 3108277236..3fafffb7d4 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" | ## 设置流程 @@ -35,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 2bd5ddef97..a8e08deb89 100644 --- a/go.mod +++ b/go.mod @@ -8,6 +8,7 @@ require ( github.com/bwmarrin/discordgo v0.29.0 github.com/caarlos0/env/v11 v11.3.1 github.com/chzyer/readline v1.5.1 + github.com/ergochat/irc-go v0.5.0 github.com/gdamore/tcell/v2 v2.13.8 github.com/google/uuid v1.6.0 github.com/gorilla/websocket v1.5.3 @@ -37,7 +38,6 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/dustin/go-humanize v1.0.1 // indirect github.com/elliotchance/orderedmap/v3 v3.1.0 // indirect - github.com/ergochat/irc-go v0.5.0 // indirect github.com/gdamore/encoding v1.0.1 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/lucasb-eyer/go-colorful v1.3.0 // indirect diff --git a/pkg/channels/feishu/feishu_64.go b/pkg/channels/feishu/feishu_64.go index 00f73064d0..5217dd4e92 100644 --- a/pkg/channels/feishu/feishu_64.go +++ b/pkg/channels/feishu/feishu_64.go @@ -4,9 +4,11 @@ package feishu import ( "context" + "crypto/rand" "encoding/json" "fmt" "io" + "math/big" "net/http" "os" "path/filepath" @@ -195,18 +197,35 @@ 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 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). 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 +233,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 fcbfc8e787..f17481e1ec 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -273,15 +273,16 @@ 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"` } type DiscordConfig struct {