feat: add configurable ack reaction support for Feishu
Add global messages configuration to control acknowledgment reactions across all channels. Implement Feishu message reaction (emoji) support based on configuration scope. Features: - Add MessagesConfig with ack_reaction, ack_reaction_scope, remove_ack_after_reply - Add ShouldAckReaction() for cross-channel ack reaction logic - Implement Feishu message reaction via Lark API - Update all channels to receive MessagesConfig via BaseChannel Configuration options: - ack_reaction: emoji to use (e.g., "OK"), empty to disable - ack_reaction_scope: "all", "direct", "group-all", "group-mentions", "off" - remove_ack_after_reply: reserved for future use Refs: inspired by openclaw implementation
This commit is contained in:
parent
57dac394c5
commit
093d24f9ab
18 changed files with 264 additions and 50 deletions
|
|
@ -129,6 +129,11 @@
|
|||
"enabled": false,
|
||||
"monitor_usb": true
|
||||
},
|
||||
"messages": {
|
||||
"ack_reaction": "OK",
|
||||
"ack_reaction_scope": "group-mentions",
|
||||
"remove_ack_after_reply": false
|
||||
},
|
||||
"gateway": {
|
||||
"host": "0.0.0.0",
|
||||
"port": 18790
|
||||
|
|
|
|||
118
pkg/channels/ack_reactions.go
Normal file
118
pkg/channels/ack_reactions.go
Normal file
|
|
@ -0,0 +1,118 @@
|
|||
package channels
|
||||
|
||||
// AckReactionScope defines when to send acknowledgment reactions
|
||||
type AckReactionScope string
|
||||
|
||||
const (
|
||||
// AckReactionScopeAll enables ack reactions for all messages
|
||||
AckReactionScopeAll AckReactionScope = "all"
|
||||
// AckReactionScopeDirect enables ack reactions only for direct messages
|
||||
AckReactionScopeDirect AckReactionScope = "direct"
|
||||
// AckReactionScopeGroupAll enables ack reactions for all group messages
|
||||
AckReactionScopeGroupAll AckReactionScope = "group-all"
|
||||
// AckReactionScopeGroupMentions enables ack reactions only when mentioned in groups
|
||||
AckReactionScopeGroupMentions AckReactionScope = "group-mentions"
|
||||
// AckReactionScopeOff disables ack reactions
|
||||
AckReactionScopeOff AckReactionScope = "off"
|
||||
// AckReactionScopeNone disables ack reactions (alias)
|
||||
AckReactionScopeNone AckReactionScope = "none"
|
||||
)
|
||||
|
||||
// AckReactionParams contains parameters for determining whether to send an ack reaction
|
||||
type AckReactionParams struct {
|
||||
// Scope is the configured ack reaction scope
|
||||
Scope AckReactionScope
|
||||
// IsDirect indicates if the message is a direct/private message
|
||||
IsDirect bool
|
||||
// IsGroup indicates if the message is from a group
|
||||
IsGroup bool
|
||||
// IsMentionableGroup indicates if the group supports mentions
|
||||
IsMentionableGroup bool
|
||||
// RequireMention indicates if the group requires mentioning the bot to respond
|
||||
RequireMention bool
|
||||
// CanDetectMention indicates if the platform can detect mentions
|
||||
CanDetectMention bool
|
||||
// WasMentioned indicates if the bot was mentioned in the message
|
||||
WasMentioned bool
|
||||
// ShouldBypassMention indicates if mention requirements should be bypassed
|
||||
ShouldBypassMention bool
|
||||
}
|
||||
|
||||
// ShouldAckReaction determines whether an ack reaction should be sent based on parameters
|
||||
// Reference: openclaw implementation
|
||||
func ShouldAckReaction(params AckReactionParams) bool {
|
||||
// Default to group-mentions if not specified
|
||||
scope := params.Scope
|
||||
if scope == "" {
|
||||
scope = AckReactionScopeGroupMentions
|
||||
}
|
||||
|
||||
// Disabled cases
|
||||
if scope == AckReactionScopeOff || scope == AckReactionScopeNone {
|
||||
return false
|
||||
}
|
||||
|
||||
// All messages
|
||||
if scope == AckReactionScopeAll {
|
||||
return true
|
||||
}
|
||||
|
||||
// Direct messages only
|
||||
if scope == AckReactionScopeDirect {
|
||||
return params.IsDirect
|
||||
}
|
||||
|
||||
// All group messages
|
||||
if scope == AckReactionScopeGroupAll {
|
||||
return params.IsGroup
|
||||
}
|
||||
|
||||
// Group mentions only
|
||||
if scope == AckReactionScopeGroupMentions {
|
||||
// Not a mentionable group, don't ack
|
||||
if !params.IsMentionableGroup {
|
||||
return false
|
||||
}
|
||||
// No mention required, don't ack (avoid over-acknowledging)
|
||||
if !params.RequireMention {
|
||||
return false
|
||||
}
|
||||
// Can't detect mentions, don't ack
|
||||
if !params.CanDetectMention {
|
||||
return false
|
||||
}
|
||||
// Mentioned or bypass required, ack
|
||||
return params.WasMentioned || params.ShouldBypassMention
|
||||
}
|
||||
|
||||
return false
|
||||
}
|
||||
|
||||
// AckReactionManager manages the lifecycle of acknowledgment reactions
|
||||
type AckReactionManager struct {
|
||||
// RemoveAfterReply indicates whether to remove the ack after reply
|
||||
RemoveAfterReply bool
|
||||
// ReactionValue is the current reaction value (emoji)
|
||||
ReactionValue string
|
||||
// Added indicates if the ack reaction has been added
|
||||
Added bool
|
||||
}
|
||||
|
||||
// NewAckReactionManager creates a new ack reaction manager
|
||||
func NewAckReactionManager(removeAfterReply bool, reaction string) *AckReactionManager {
|
||||
return &AckReactionManager{
|
||||
RemoveAfterReply: removeAfterReply,
|
||||
ReactionValue: reaction,
|
||||
Added: false,
|
||||
}
|
||||
}
|
||||
|
||||
// MarkAdded marks the ack reaction as added
|
||||
func (m *AckReactionManager) MarkAdded() {
|
||||
m.Added = true
|
||||
}
|
||||
|
||||
// ShouldRemoveAfterReply determines whether to remove the ack after reply
|
||||
func (m *AckReactionManager) ShouldRemoveAfterReply() bool {
|
||||
return m.RemoveAfterReply && m.Added && m.ReactionValue != ""
|
||||
}
|
||||
|
|
@ -6,6 +6,7 @@ import (
|
|||
"strings"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
type Channel interface {
|
||||
|
|
@ -19,15 +20,17 @@ type Channel interface {
|
|||
|
||||
type BaseChannel struct {
|
||||
config interface{}
|
||||
messagesConfig config.MessagesConfig
|
||||
bus *bus.MessageBus
|
||||
running bool
|
||||
name string
|
||||
allowList []string
|
||||
}
|
||||
|
||||
func NewBaseChannel(name string, config interface{}, bus *bus.MessageBus, allowList []string) *BaseChannel {
|
||||
func NewBaseChannel(name string, config interface{}, messagesCfg config.MessagesConfig, bus *bus.MessageBus, allowList []string) *BaseChannel {
|
||||
return &BaseChannel{
|
||||
config: config,
|
||||
messagesConfig: messagesCfg,
|
||||
bus: bus,
|
||||
name: name,
|
||||
allowList: allowList,
|
||||
|
|
@ -35,6 +38,11 @@ func NewBaseChannel(name string, config interface{}, bus *bus.MessageBus, allowL
|
|||
}
|
||||
}
|
||||
|
||||
// MessagesConfig returns the global messages configuration
|
||||
func (c *BaseChannel) MessagesConfig() config.MessagesConfig {
|
||||
return c.messagesConfig
|
||||
}
|
||||
|
||||
func (c *BaseChannel) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
|
|
|||
|
|
@ -1,6 +1,10 @@
|
|||
package channels
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func TestBaseChannelIsAllowed(t *testing.T) {
|
||||
tests := []struct {
|
||||
|
|
@ -43,7 +47,7 @@ func TestBaseChannelIsAllowed(t *testing.T) {
|
|||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ch := NewBaseChannel("test", nil, nil, tt.allowList)
|
||||
ch := NewBaseChannel("test", nil, config.MessagesConfig{}, nil, tt.allowList)
|
||||
if got := ch.IsAllowed(tt.senderID); got != tt.want {
|
||||
t.Fatalf("IsAllowed(%q) = %v, want %v", tt.senderID, got, tt.want)
|
||||
}
|
||||
|
|
|
|||
|
|
@ -31,12 +31,12 @@ type DingTalkChannel struct {
|
|||
}
|
||||
|
||||
// NewDingTalkChannel creates a new DingTalk channel instance
|
||||
func NewDingTalkChannel(cfg config.DingTalkConfig, messageBus *bus.MessageBus) (*DingTalkChannel, error) {
|
||||
func NewDingTalkChannel(cfg config.DingTalkConfig, messagesCfg config.MessagesConfig, messageBus *bus.MessageBus) (*DingTalkChannel, error) {
|
||||
if cfg.ClientID == "" || cfg.ClientSecret == "" {
|
||||
return nil, fmt.Errorf("dingtalk client_id and client_secret are required")
|
||||
}
|
||||
|
||||
base := NewBaseChannel("dingtalk", cfg, messageBus, cfg.AllowFrom)
|
||||
base := NewBaseChannel("dingtalk", cfg, messagesCfg, messageBus, cfg.AllowFrom)
|
||||
|
||||
return &DingTalkChannel{
|
||||
BaseChannel: base,
|
||||
|
|
|
|||
|
|
@ -28,13 +28,13 @@ type DiscordChannel struct {
|
|||
ctx context.Context
|
||||
}
|
||||
|
||||
func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) {
|
||||
func NewDiscordChannel(cfg config.DiscordConfig, messagesCfg config.MessagesConfig, bus *bus.MessageBus) (*DiscordChannel, error) {
|
||||
session, err := discordgo.New("Bot " + cfg.Token)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to create discord session: %w", err)
|
||||
}
|
||||
|
||||
base := NewBaseChannel("discord", cfg, bus, cfg.AllowFrom)
|
||||
base := NewBaseChannel("discord", cfg, messagesCfg, bus, cfg.AllowFrom)
|
||||
|
||||
return &DiscordChannel{
|
||||
BaseChannel: base,
|
||||
|
|
|
|||
|
|
@ -16,7 +16,7 @@ type FeishuChannel struct {
|
|||
}
|
||||
|
||||
// NewFeishuChannel returns an error on 32-bit architectures where the Feishu SDK is not supported
|
||||
func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
|
||||
func NewFeishuChannel(cfg config.FeishuConfig, messagesCfg config.MessagesConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
|
||||
return nil, errors.New("feishu channel is not supported on 32-bit architectures (armv7l, 386, etc.). Please use a 64-bit system or disable feishu in your config")
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ type FeishuChannel struct {
|
|||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
|
||||
base := NewBaseChannel("feishu", cfg, bus, cfg.AllowFrom)
|
||||
func NewFeishuChannel(cfg config.FeishuConfig, messagesCfg config.MessagesConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
|
||||
base := NewBaseChannel("feishu", cfg, messagesCfg, bus, cfg.AllowFrom)
|
||||
|
||||
return &FeishuChannel{
|
||||
BaseChannel: base,
|
||||
|
|
@ -128,7 +128,7 @@ func (c *FeishuChannel) Send(ctx context.Context, msg bus.OutboundMessage) error
|
|||
return nil
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) handleMessageReceive(_ context.Context, event *larkim.P2MessageReceiveV1) error {
|
||||
func (c *FeishuChannel) handleMessageReceive(ctx context.Context, event *larkim.P2MessageReceiveV1) error {
|
||||
if event == nil || event.Event == nil || event.Event.Message == nil {
|
||||
return nil
|
||||
}
|
||||
|
|
@ -151,14 +151,26 @@ func (c *FeishuChannel) handleMessageReceive(_ context.Context, event *larkim.P2
|
|||
content = "[empty message]"
|
||||
}
|
||||
|
||||
// Determine chat type: p2p = direct, group = group chat
|
||||
chatType := stringValue(message.ChatType)
|
||||
isGroup := chatType == "group"
|
||||
isDirect := chatType == "p2p"
|
||||
|
||||
// Check if bot was mentioned
|
||||
wasMentioned := false
|
||||
if message.Mentions != nil && len(message.Mentions) > 0 {
|
||||
wasMentioned = true
|
||||
}
|
||||
|
||||
metadata := map[string]string{}
|
||||
if messageID := stringValue(message.MessageId); messageID != "" {
|
||||
messageID := stringValue(message.MessageId)
|
||||
if messageID != "" {
|
||||
metadata["message_id"] = messageID
|
||||
}
|
||||
if messageType := stringValue(message.MessageType); messageType != "" {
|
||||
metadata["message_type"] = messageType
|
||||
}
|
||||
if chatType := stringValue(message.ChatType); chatType != "" {
|
||||
if chatType != "" {
|
||||
metadata["chat_type"] = chatType
|
||||
}
|
||||
if sender != nil && sender.TenantKey != nil {
|
||||
|
|
@ -171,6 +183,30 @@ func (c *FeishuChannel) handleMessageReceive(_ context.Context, event *larkim.P2
|
|||
"preview": utils.Truncate(content, 80),
|
||||
})
|
||||
|
||||
// Add emoji ack reaction based on configuration
|
||||
ackReaction := c.MessagesConfig().AckReaction
|
||||
if ackReaction != "" && messageID != "" {
|
||||
shouldAck := ShouldAckReaction(AckReactionParams{
|
||||
Scope: AckReactionScope(c.MessagesConfig().AckReactionScope),
|
||||
IsDirect: isDirect,
|
||||
IsGroup: isGroup,
|
||||
IsMentionableGroup: true, // Feishu groups support mentions
|
||||
RequireMention: true,
|
||||
CanDetectMention: true,
|
||||
WasMentioned: wasMentioned,
|
||||
ShouldBypassMention: false,
|
||||
})
|
||||
|
||||
if shouldAck {
|
||||
if err := c.addMessageReaction(ctx, messageID, ackReaction); err != nil {
|
||||
logger.ErrorCF("feishu", "Failed to add emoji reaction", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
"message_id": messageID,
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
c.HandleMessage(senderID, chatID, content, nil, metadata)
|
||||
return nil
|
||||
}
|
||||
|
|
@ -216,3 +252,30 @@ func stringValue(v *string) string {
|
|||
}
|
||||
return *v
|
||||
}
|
||||
|
||||
func (c *FeishuChannel) addMessageReaction(ctx context.Context, messageID, emojiType 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("failed to create message reaction: %w", err)
|
||||
}
|
||||
|
||||
if !resp.Success() {
|
||||
return fmt.Errorf("feishu reaction api error: code=%d msg=%s", resp.Code, resp.Msg)
|
||||
}
|
||||
|
||||
logger.DebugCF("feishu", "Emoji reaction added", map[string]interface{}{
|
||||
"message_id": messageID,
|
||||
"emoji_type": emojiType,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
|
|
@ -54,12 +54,12 @@ type LINEChannel struct {
|
|||
}
|
||||
|
||||
// NewLINEChannel creates a new LINE channel instance.
|
||||
func NewLINEChannel(cfg config.LINEConfig, messageBus *bus.MessageBus) (*LINEChannel, error) {
|
||||
func NewLINEChannel(cfg config.LINEConfig, messagesCfg config.MessagesConfig, messageBus *bus.MessageBus) (*LINEChannel, error) {
|
||||
if cfg.ChannelSecret == "" || cfg.ChannelAccessToken == "" {
|
||||
return nil, fmt.Errorf("line channel_secret and channel_access_token are required")
|
||||
}
|
||||
|
||||
base := NewBaseChannel("line", cfg, messageBus, cfg.AllowFrom)
|
||||
base := NewBaseChannel("line", cfg, messagesCfg, messageBus, cfg.AllowFrom)
|
||||
|
||||
return &LINEChannel{
|
||||
BaseChannel: base,
|
||||
|
|
|
|||
|
|
@ -28,8 +28,8 @@ type MaixCamMessage struct {
|
|||
Data map[string]interface{} `json:"data"`
|
||||
}
|
||||
|
||||
func NewMaixCamChannel(cfg config.MaixCamConfig, bus *bus.MessageBus) (*MaixCamChannel, error) {
|
||||
base := NewBaseChannel("maixcam", cfg, bus, cfg.AllowFrom)
|
||||
func NewMaixCamChannel(cfg config.MaixCamConfig, messagesCfg config.MessagesConfig, bus *bus.MessageBus) (*MaixCamChannel, error) {
|
||||
base := NewBaseChannel("maixcam", cfg, messagesCfg, bus, cfg.AllowFrom)
|
||||
|
||||
return &MaixCamChannel{
|
||||
BaseChannel: base,
|
||||
|
|
|
|||
|
|
@ -61,7 +61,7 @@ func (m *Manager) initChannels() error {
|
|||
|
||||
if m.config.Channels.WhatsApp.Enabled && m.config.Channels.WhatsApp.BridgeURL != "" {
|
||||
logger.DebugC("channels", "Attempting to initialize WhatsApp channel")
|
||||
whatsapp, err := NewWhatsAppChannel(m.config.Channels.WhatsApp, m.bus)
|
||||
whatsapp, err := NewWhatsAppChannel(m.config.Channels.WhatsApp, m.config.Messages, m.bus)
|
||||
if err != nil {
|
||||
logger.ErrorCF("channels", "Failed to initialize WhatsApp channel", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
@ -74,7 +74,7 @@ func (m *Manager) initChannels() error {
|
|||
|
||||
if m.config.Channels.Feishu.Enabled {
|
||||
logger.DebugC("channels", "Attempting to initialize Feishu channel")
|
||||
feishu, err := NewFeishuChannel(m.config.Channels.Feishu, m.bus)
|
||||
feishu, err := NewFeishuChannel(m.config.Channels.Feishu, m.config.Messages, m.bus)
|
||||
if err != nil {
|
||||
logger.ErrorCF("channels", "Failed to initialize Feishu channel", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
@ -87,7 +87,7 @@ func (m *Manager) initChannels() error {
|
|||
|
||||
if m.config.Channels.Discord.Enabled && m.config.Channels.Discord.Token != "" {
|
||||
logger.DebugC("channels", "Attempting to initialize Discord channel")
|
||||
discord, err := NewDiscordChannel(m.config.Channels.Discord, m.bus)
|
||||
discord, err := NewDiscordChannel(m.config.Channels.Discord, m.config.Messages, m.bus)
|
||||
if err != nil {
|
||||
logger.ErrorCF("channels", "Failed to initialize Discord channel", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
@ -100,7 +100,7 @@ func (m *Manager) initChannels() error {
|
|||
|
||||
if m.config.Channels.MaixCam.Enabled {
|
||||
logger.DebugC("channels", "Attempting to initialize MaixCam channel")
|
||||
maixcam, err := NewMaixCamChannel(m.config.Channels.MaixCam, m.bus)
|
||||
maixcam, err := NewMaixCamChannel(m.config.Channels.MaixCam, m.config.Messages, m.bus)
|
||||
if err != nil {
|
||||
logger.ErrorCF("channels", "Failed to initialize MaixCam channel", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
@ -113,7 +113,7 @@ func (m *Manager) initChannels() error {
|
|||
|
||||
if m.config.Channels.QQ.Enabled {
|
||||
logger.DebugC("channels", "Attempting to initialize QQ channel")
|
||||
qq, err := NewQQChannel(m.config.Channels.QQ, m.bus)
|
||||
qq, err := NewQQChannel(m.config.Channels.QQ, m.config.Messages, m.bus)
|
||||
if err != nil {
|
||||
logger.ErrorCF("channels", "Failed to initialize QQ channel", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
@ -126,7 +126,7 @@ func (m *Manager) initChannels() error {
|
|||
|
||||
if m.config.Channels.DingTalk.Enabled && m.config.Channels.DingTalk.ClientID != "" {
|
||||
logger.DebugC("channels", "Attempting to initialize DingTalk channel")
|
||||
dingtalk, err := NewDingTalkChannel(m.config.Channels.DingTalk, m.bus)
|
||||
dingtalk, err := NewDingTalkChannel(m.config.Channels.DingTalk, m.config.Messages, m.bus)
|
||||
if err != nil {
|
||||
logger.ErrorCF("channels", "Failed to initialize DingTalk channel", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
@ -139,7 +139,7 @@ func (m *Manager) initChannels() error {
|
|||
|
||||
if m.config.Channels.Slack.Enabled && m.config.Channels.Slack.BotToken != "" {
|
||||
logger.DebugC("channels", "Attempting to initialize Slack channel")
|
||||
slackCh, err := NewSlackChannel(m.config.Channels.Slack, m.bus)
|
||||
slackCh, err := NewSlackChannel(m.config.Channels.Slack, m.config.Messages, m.bus)
|
||||
if err != nil {
|
||||
logger.ErrorCF("channels", "Failed to initialize Slack channel", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
@ -152,7 +152,7 @@ func (m *Manager) initChannels() error {
|
|||
|
||||
if m.config.Channels.LINE.Enabled && m.config.Channels.LINE.ChannelAccessToken != "" {
|
||||
logger.DebugC("channels", "Attempting to initialize LINE channel")
|
||||
line, err := NewLINEChannel(m.config.Channels.LINE, m.bus)
|
||||
line, err := NewLINEChannel(m.config.Channels.LINE, m.config.Messages, m.bus)
|
||||
if err != nil {
|
||||
logger.ErrorCF("channels", "Failed to initialize LINE channel", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
@ -165,7 +165,7 @@ func (m *Manager) initChannels() error {
|
|||
|
||||
if m.config.Channels.OneBot.Enabled && m.config.Channels.OneBot.WSUrl != "" {
|
||||
logger.DebugC("channels", "Attempting to initialize OneBot channel")
|
||||
onebot, err := NewOneBotChannel(m.config.Channels.OneBot, m.bus)
|
||||
onebot, err := NewOneBotChannel(m.config.Channels.OneBot, m.config.Messages, m.bus)
|
||||
if err != nil {
|
||||
logger.ErrorCF("channels", "Failed to initialize OneBot channel", map[string]interface{}{
|
||||
"error": err.Error(),
|
||||
|
|
|
|||
|
|
@ -91,8 +91,8 @@ type oneBotSendGroupMsgParams struct {
|
|||
Message string `json:"message"`
|
||||
}
|
||||
|
||||
func NewOneBotChannel(cfg config.OneBotConfig, messageBus *bus.MessageBus) (*OneBotChannel, error) {
|
||||
base := NewBaseChannel("onebot", cfg, messageBus, cfg.AllowFrom)
|
||||
func NewOneBotChannel(cfg config.OneBotConfig, messagesCfg config.MessagesConfig, messageBus *bus.MessageBus) (*OneBotChannel, error) {
|
||||
base := NewBaseChannel("onebot", cfg, messagesCfg, messageBus, cfg.AllowFrom)
|
||||
|
||||
const dedupSize = 1024
|
||||
return &OneBotChannel{
|
||||
|
|
|
|||
|
|
@ -30,8 +30,8 @@ type QQChannel struct {
|
|||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
func NewQQChannel(cfg config.QQConfig, messageBus *bus.MessageBus) (*QQChannel, error) {
|
||||
base := NewBaseChannel("qq", cfg, messageBus, cfg.AllowFrom)
|
||||
func NewQQChannel(cfg config.QQConfig, messagesCfg config.MessagesConfig, messageBus *bus.MessageBus) (*QQChannel, error) {
|
||||
base := NewBaseChannel("qq", cfg, messagesCfg, messageBus, cfg.AllowFrom)
|
||||
|
||||
return &QQChannel{
|
||||
BaseChannel: base,
|
||||
|
|
|
|||
|
|
@ -36,7 +36,7 @@ type slackMessageRef struct {
|
|||
Timestamp string
|
||||
}
|
||||
|
||||
func NewSlackChannel(cfg config.SlackConfig, messageBus *bus.MessageBus) (*SlackChannel, error) {
|
||||
func NewSlackChannel(cfg config.SlackConfig, messagesCfg config.MessagesConfig, messageBus *bus.MessageBus) (*SlackChannel, error) {
|
||||
if cfg.BotToken == "" || cfg.AppToken == "" {
|
||||
return nil, fmt.Errorf("slack bot_token and app_token are required")
|
||||
}
|
||||
|
|
@ -48,7 +48,7 @@ func NewSlackChannel(cfg config.SlackConfig, messageBus *bus.MessageBus) (*Slack
|
|||
|
||||
socketClient := socketmode.New(api)
|
||||
|
||||
base := NewBaseChannel("slack", cfg, messageBus, cfg.AllowFrom)
|
||||
base := NewBaseChannel("slack", cfg, messagesCfg, messageBus, cfg.AllowFrom)
|
||||
|
||||
return &SlackChannel{
|
||||
BaseChannel: base,
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@ func TestNewSlackChannel(t *testing.T) {
|
|||
BotToken: "",
|
||||
AppToken: "xapp-test",
|
||||
}
|
||||
_, err := NewSlackChannel(cfg, msgBus)
|
||||
_, err := NewSlackChannel(cfg, config.MessagesConfig{}, msgBus)
|
||||
if err == nil {
|
||||
t.Error("expected error for missing bot_token, got nil")
|
||||
}
|
||||
|
|
@ -117,7 +117,7 @@ func TestNewSlackChannel(t *testing.T) {
|
|||
BotToken: "xoxb-test",
|
||||
AppToken: "",
|
||||
}
|
||||
_, err := NewSlackChannel(cfg, msgBus)
|
||||
_, err := NewSlackChannel(cfg, config.MessagesConfig{}, msgBus)
|
||||
if err == nil {
|
||||
t.Error("expected error for missing app_token, got nil")
|
||||
}
|
||||
|
|
@ -129,7 +129,7 @@ func TestNewSlackChannel(t *testing.T) {
|
|||
AppToken: "xapp-test",
|
||||
AllowFrom: []string{"U123"},
|
||||
}
|
||||
ch, err := NewSlackChannel(cfg, msgBus)
|
||||
ch, err := NewSlackChannel(cfg, config.MessagesConfig{}, msgBus)
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
|
@ -151,7 +151,7 @@ func TestSlackChannelIsAllowed(t *testing.T) {
|
|||
AppToken: "xapp-test",
|
||||
AllowFrom: []string{},
|
||||
}
|
||||
ch, _ := NewSlackChannel(cfg, msgBus)
|
||||
ch, _ := NewSlackChannel(cfg, config.MessagesConfig{}, msgBus)
|
||||
if !ch.IsAllowed("U_ANYONE") {
|
||||
t.Error("empty allowlist should allow all users")
|
||||
}
|
||||
|
|
@ -163,7 +163,7 @@ func TestSlackChannelIsAllowed(t *testing.T) {
|
|||
AppToken: "xapp-test",
|
||||
AllowFrom: []string{"U_ALLOWED"},
|
||||
}
|
||||
ch, _ := NewSlackChannel(cfg, msgBus)
|
||||
ch, _ := NewSlackChannel(cfg, config.MessagesConfig{}, msgBus)
|
||||
if !ch.IsAllowed("U_ALLOWED") {
|
||||
t.Error("allowed user should pass allowlist check")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -66,7 +66,7 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
|
|||
return nil, fmt.Errorf("failed to create telegram bot: %w", err)
|
||||
}
|
||||
|
||||
base := NewBaseChannel("telegram", telegramCfg, bus, telegramCfg.AllowFrom)
|
||||
base := NewBaseChannel("telegram", telegramCfg, cfg.Messages, bus, telegramCfg.AllowFrom)
|
||||
|
||||
return &TelegramChannel{
|
||||
BaseChannel: base,
|
||||
|
|
|
|||
|
|
@ -24,8 +24,8 @@ type WhatsAppChannel struct {
|
|||
connected bool
|
||||
}
|
||||
|
||||
func NewWhatsAppChannel(cfg config.WhatsAppConfig, bus *bus.MessageBus) (*WhatsAppChannel, error) {
|
||||
base := NewBaseChannel("whatsapp", cfg, bus, cfg.AllowFrom)
|
||||
func NewWhatsAppChannel(cfg config.WhatsAppConfig, messagesCfg config.MessagesConfig, bus *bus.MessageBus) (*WhatsAppChannel, error) {
|
||||
base := NewBaseChannel("whatsapp", cfg, messagesCfg, bus, cfg.AllowFrom)
|
||||
|
||||
return &WhatsAppChannel{
|
||||
BaseChannel: base,
|
||||
|
|
|
|||
|
|
@ -51,6 +51,7 @@ type Config struct {
|
|||
Tools ToolsConfig `json:"tools"`
|
||||
Heartbeat HeartbeatConfig `json:"heartbeat"`
|
||||
Devices DevicesConfig `json:"devices"`
|
||||
Messages MessagesConfig `json:"messages"`
|
||||
mu sync.RWMutex
|
||||
}
|
||||
|
||||
|
|
@ -166,6 +167,16 @@ type DevicesConfig struct {
|
|||
MonitorUSB bool `json:"monitor_usb" env:"PICOCLAW_DEVICES_MONITOR_USB"`
|
||||
}
|
||||
|
||||
// MessagesConfig controls global message behavior for all channels
|
||||
type MessagesConfig struct {
|
||||
// AckReaction is the emoji used to acknowledge inbound messages (empty to disable)
|
||||
AckReaction string `json:"ack_reaction" env:"PICOCLAW_MESSAGES_ACK_REACTION"`
|
||||
// AckReactionScope controls when to send ack reactions: "all", "direct", "group-all", "group-mentions", "off"
|
||||
AckReactionScope string `json:"ack_reaction_scope" env:"PICOCLAW_MESSAGES_ACK_REACTION_SCOPE"`
|
||||
// RemoveAckAfterReply removes the ack reaction after reply is sent
|
||||
RemoveAckAfterReply bool `json:"remove_ack_after_reply" env:"PICOCLAW_MESSAGES_REMOVE_ACK_AFTER_REPLY"`
|
||||
}
|
||||
|
||||
type ProvidersConfig struct {
|
||||
Anthropic ProviderConfig `json:"anthropic"`
|
||||
OpenAI ProviderConfig `json:"openai"`
|
||||
|
|
@ -331,6 +342,11 @@ func DefaultConfig() *Config {
|
|||
Enabled: false,
|
||||
MonitorUSB: true,
|
||||
},
|
||||
Messages: MessagesConfig{
|
||||
AckReaction: "OK",
|
||||
AckReactionScope: "group-mentions",
|
||||
RemoveAckAfterReply: false,
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue