mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
refactor(channels): standardize group chat trigger filtering (Phase 8)
Add unified ShouldRespondInGroup to BaseChannel, replacing scattered per-channel group filtering logic. Introduce GroupTriggerConfig (with mention_only + prefixes), TypingConfig, and PlaceholderConfig types. Migrate Discord MentionOnly, OneBot checkGroupTrigger, and LINE hardcoded mention-only to the shared mechanism. Add group trigger entry points for Slack, Telegram, QQ, Feishu, DingTalk, and WeCom. Legacy config fields are preserved with automatic migration.
This commit is contained in:
@@ -8,6 +8,7 @@ import (
|
||||
"github.com/google/uuid"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/bus"
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
"github.com/sipeed/picoclaw/pkg/media"
|
||||
)
|
||||
|
||||
@@ -30,6 +31,11 @@ func WithMaxMessageLength(n int) BaseChannelOption {
|
||||
return func(c *BaseChannel) { c.maxMessageLength = n }
|
||||
}
|
||||
|
||||
// WithGroupTrigger sets the group trigger configuration for a channel.
|
||||
func WithGroupTrigger(gt config.GroupTriggerConfig) BaseChannelOption {
|
||||
return func(c *BaseChannel) { c.groupTrigger = gt }
|
||||
}
|
||||
|
||||
// MessageLengthProvider is an opt-in interface that channels implement
|
||||
// to advertise their maximum message length. The Manager uses this via
|
||||
// type assertion to decide whether to split outbound messages.
|
||||
@@ -44,6 +50,7 @@ type BaseChannel struct {
|
||||
name string
|
||||
allowList []string
|
||||
maxMessageLength int
|
||||
groupTrigger config.GroupTriggerConfig
|
||||
mediaStore media.MediaStore
|
||||
}
|
||||
|
||||
@@ -72,6 +79,46 @@ func (c *BaseChannel) MaxMessageLength() int {
|
||||
return c.maxMessageLength
|
||||
}
|
||||
|
||||
// ShouldRespondInGroup determines whether the bot should respond in a group chat.
|
||||
// Each channel is responsible for:
|
||||
// 1. Detecting isMentioned (platform-specific)
|
||||
// 2. Stripping bot mention from content (platform-specific)
|
||||
// 3. Calling this method to get the group response decision
|
||||
//
|
||||
// Logic:
|
||||
// - If isMentioned → always respond
|
||||
// - If mention_only configured and not mentioned → ignore
|
||||
// - If prefixes configured → respond if content starts with any prefix (strip it)
|
||||
// - If prefixes configured but no match and not mentioned → ignore
|
||||
// - Otherwise (no group_trigger configured) → respond to all (permissive default)
|
||||
func (c *BaseChannel) ShouldRespondInGroup(isMentioned bool, content string) (bool, string) {
|
||||
gt := c.groupTrigger
|
||||
|
||||
// Mentioned → always respond
|
||||
if isMentioned {
|
||||
return true, strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
// mention_only → require mention
|
||||
if gt.MentionOnly {
|
||||
return false, content
|
||||
}
|
||||
|
||||
// Prefix matching
|
||||
if len(gt.Prefixes) > 0 {
|
||||
for _, prefix := range gt.Prefixes {
|
||||
if prefix != "" && strings.HasPrefix(content, prefix) {
|
||||
return true, strings.TrimSpace(strings.TrimPrefix(content, prefix))
|
||||
}
|
||||
}
|
||||
// Prefixes configured but none matched and not mentioned → ignore
|
||||
return false, content
|
||||
}
|
||||
|
||||
// No group_trigger configured → permissive (respond to all)
|
||||
return true, strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
func (c *BaseChannel) Name() string {
|
||||
return c.name
|
||||
}
|
||||
|
||||
+126
-1
@@ -1,6 +1,10 @@
|
||||
package channels
|
||||
|
||||
import "testing"
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/sipeed/picoclaw/pkg/config"
|
||||
)
|
||||
|
||||
func TestBaseChannelIsAllowed(t *testing.T) {
|
||||
tests := []struct {
|
||||
@@ -50,3 +54,124 @@ func TestBaseChannelIsAllowed(t *testing.T) {
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldRespondInGroup(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
gt config.GroupTriggerConfig
|
||||
isMentioned bool
|
||||
content string
|
||||
wantRespond bool
|
||||
wantContent string
|
||||
}{
|
||||
{
|
||||
name: "no config - permissive default",
|
||||
gt: config.GroupTriggerConfig{},
|
||||
isMentioned: false,
|
||||
content: "hello world",
|
||||
wantRespond: true,
|
||||
wantContent: "hello world",
|
||||
},
|
||||
{
|
||||
name: "no config - mentioned",
|
||||
gt: config.GroupTriggerConfig{},
|
||||
isMentioned: true,
|
||||
content: "hello world",
|
||||
wantRespond: true,
|
||||
wantContent: "hello world",
|
||||
},
|
||||
{
|
||||
name: "mention_only - not mentioned",
|
||||
gt: config.GroupTriggerConfig{MentionOnly: true},
|
||||
isMentioned: false,
|
||||
content: "hello world",
|
||||
wantRespond: false,
|
||||
wantContent: "hello world",
|
||||
},
|
||||
{
|
||||
name: "mention_only - mentioned",
|
||||
gt: config.GroupTriggerConfig{MentionOnly: true},
|
||||
isMentioned: true,
|
||||
content: "hello world",
|
||||
wantRespond: true,
|
||||
wantContent: "hello world",
|
||||
},
|
||||
{
|
||||
name: "prefix match",
|
||||
gt: config.GroupTriggerConfig{Prefixes: []string{"/ask"}},
|
||||
isMentioned: false,
|
||||
content: "/ask hello",
|
||||
wantRespond: true,
|
||||
wantContent: "hello",
|
||||
},
|
||||
{
|
||||
name: "prefix no match - not mentioned",
|
||||
gt: config.GroupTriggerConfig{Prefixes: []string{"/ask"}},
|
||||
isMentioned: false,
|
||||
content: "hello world",
|
||||
wantRespond: false,
|
||||
wantContent: "hello world",
|
||||
},
|
||||
{
|
||||
name: "prefix no match - but mentioned",
|
||||
gt: config.GroupTriggerConfig{Prefixes: []string{"/ask"}},
|
||||
isMentioned: true,
|
||||
content: "hello world",
|
||||
wantRespond: true,
|
||||
wantContent: "hello world",
|
||||
},
|
||||
{
|
||||
name: "multiple prefixes - second matches",
|
||||
gt: config.GroupTriggerConfig{Prefixes: []string{"/ask", "/bot"}},
|
||||
isMentioned: false,
|
||||
content: "/bot help me",
|
||||
wantRespond: true,
|
||||
wantContent: "help me",
|
||||
},
|
||||
{
|
||||
name: "mention_only with prefixes - mentioned overrides",
|
||||
gt: config.GroupTriggerConfig{MentionOnly: true, Prefixes: []string{"/ask"}},
|
||||
isMentioned: true,
|
||||
content: "hello",
|
||||
wantRespond: true,
|
||||
wantContent: "hello",
|
||||
},
|
||||
{
|
||||
name: "mention_only with prefixes - not mentioned, no prefix",
|
||||
gt: config.GroupTriggerConfig{MentionOnly: true, Prefixes: []string{"/ask"}},
|
||||
isMentioned: false,
|
||||
content: "hello",
|
||||
wantRespond: false,
|
||||
wantContent: "hello",
|
||||
},
|
||||
{
|
||||
name: "empty prefix in list is skipped",
|
||||
gt: config.GroupTriggerConfig{Prefixes: []string{"", "/ask"}},
|
||||
isMentioned: false,
|
||||
content: "/ask test",
|
||||
wantRespond: true,
|
||||
wantContent: "test",
|
||||
},
|
||||
{
|
||||
name: "prefix strips leading whitespace after prefix",
|
||||
gt: config.GroupTriggerConfig{Prefixes: []string{"/ask "}},
|
||||
isMentioned: false,
|
||||
content: "/ask hello",
|
||||
wantRespond: true,
|
||||
wantContent: "hello",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
ch := NewBaseChannel("test", nil, nil, nil, WithGroupTrigger(tt.gt))
|
||||
gotRespond, gotContent := ch.ShouldRespondInGroup(tt.isMentioned, tt.content)
|
||||
if gotRespond != tt.wantRespond {
|
||||
t.Errorf("ShouldRespondInGroup() respond = %v, want %v", gotRespond, tt.wantRespond)
|
||||
}
|
||||
if gotContent != tt.wantContent {
|
||||
t.Errorf("ShouldRespondInGroup() content = %q, want %q", gotContent, tt.wantContent)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
@@ -38,7 +38,10 @@ func NewDingTalkChannel(cfg config.DingTalkConfig, messageBus *bus.MessageBus) (
|
||||
return nil, fmt.Errorf("dingtalk client_id and client_secret are required")
|
||||
}
|
||||
|
||||
base := channels.NewBaseChannel("dingtalk", cfg, messageBus, cfg.AllowFrom, channels.WithMaxMessageLength(20000))
|
||||
base := channels.NewBaseChannel("dingtalk", cfg, messageBus, cfg.AllowFrom,
|
||||
channels.WithMaxMessageLength(20000),
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
)
|
||||
|
||||
return &DingTalkChannel{
|
||||
BaseChannel: base,
|
||||
@@ -165,6 +168,12 @@ func (c *DingTalkChannel) onChatBotMessageReceived(
|
||||
peer = bus.Peer{Kind: "direct", ID: senderID}
|
||||
} else {
|
||||
peer = bus.Peer{Kind: "group", ID: data.ConversationId}
|
||||
// In group chats, apply unified group trigger filtering
|
||||
respond, cleaned := c.ShouldRespondInGroup(false, content)
|
||||
if !respond {
|
||||
return nil, nil
|
||||
}
|
||||
content = cleaned
|
||||
}
|
||||
|
||||
logger.DebugCF("dingtalk", "Received message", map[string]any{
|
||||
|
||||
@@ -39,7 +39,10 @@ func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordC
|
||||
return nil, fmt.Errorf("failed to create discord session: %w", err)
|
||||
}
|
||||
|
||||
base := channels.NewBaseChannel("discord", cfg, bus, cfg.AllowFrom, channels.WithMaxMessageLength(2000))
|
||||
base := channels.NewBaseChannel("discord", cfg, bus, cfg.AllowFrom,
|
||||
channels.WithMaxMessageLength(2000),
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
)
|
||||
|
||||
return &DiscordChannel{
|
||||
BaseChannel: base,
|
||||
@@ -265,9 +268,11 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag
|
||||
return
|
||||
}
|
||||
|
||||
// If configured to only respond to mentions, check if bot is mentioned
|
||||
// Skip this check for DMs (GuildID is empty) - DMs should always be responded to
|
||||
if c.config.MentionOnly && m.GuildID != "" {
|
||||
content := m.Content
|
||||
|
||||
// In guild (group) channels, apply unified group trigger filtering
|
||||
// DMs (GuildID is empty) always get a response
|
||||
if m.GuildID != "" {
|
||||
isMentioned := false
|
||||
for _, mention := range m.Mentions {
|
||||
if mention.ID == c.botUserID {
|
||||
@@ -275,12 +280,18 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isMentioned {
|
||||
logger.DebugCF("discord", "Message ignored - bot not mentioned", map[string]any{
|
||||
content = c.stripBotMention(content)
|
||||
respond, cleaned := c.ShouldRespondInGroup(isMentioned, content)
|
||||
if !respond {
|
||||
logger.DebugCF("discord", "Group message ignored by group trigger", map[string]any{
|
||||
"user_id": m.Author.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
content = cleaned
|
||||
} else {
|
||||
// DMs: just strip bot mention without filtering
|
||||
content = c.stripBotMention(content)
|
||||
}
|
||||
|
||||
senderID := m.Author.ID
|
||||
@@ -289,8 +300,6 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag
|
||||
senderName += "#" + m.Author.Discriminator
|
||||
}
|
||||
|
||||
content := m.Content
|
||||
content = c.stripBotMention(content)
|
||||
mediaPaths := make([]string, 0, len(m.Attachments))
|
||||
|
||||
scope := channels.BuildMediaScope("discord", m.ChannelID, m.ID)
|
||||
|
||||
@@ -32,7 +32,9 @@ type FeishuChannel struct {
|
||||
}
|
||||
|
||||
func NewFeishuChannel(cfg config.FeishuConfig, bus *bus.MessageBus) (*FeishuChannel, error) {
|
||||
base := channels.NewBaseChannel("feishu", cfg, bus, cfg.AllowFrom)
|
||||
base := channels.NewBaseChannel("feishu", cfg, bus, cfg.AllowFrom,
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
)
|
||||
|
||||
return &FeishuChannel{
|
||||
BaseChannel: base,
|
||||
@@ -173,6 +175,12 @@ func (c *FeishuChannel) handleMessageReceive(_ context.Context, event *larkim.P2
|
||||
peer = bus.Peer{Kind: "direct", ID: senderID}
|
||||
} else {
|
||||
peer = bus.Peer{Kind: "group", ID: chatID}
|
||||
// In group chats, apply unified group trigger filtering
|
||||
respond, cleaned := c.ShouldRespondInGroup(false, content)
|
||||
if !respond {
|
||||
return nil
|
||||
}
|
||||
content = cleaned
|
||||
}
|
||||
|
||||
logger.InfoCF("feishu", "Feishu message received", map[string]any{
|
||||
|
||||
@@ -59,7 +59,10 @@ func NewLINEChannel(cfg config.LINEConfig, messageBus *bus.MessageBus) (*LINECha
|
||||
return nil, fmt.Errorf("line channel_secret and channel_access_token are required")
|
||||
}
|
||||
|
||||
base := channels.NewBaseChannel("line", cfg, messageBus, cfg.AllowFrom, channels.WithMaxMessageLength(5000))
|
||||
base := channels.NewBaseChannel("line", cfg, messageBus, cfg.AllowFrom,
|
||||
channels.WithMaxMessageLength(5000),
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
)
|
||||
|
||||
return &LINEChannel{
|
||||
BaseChannel: base,
|
||||
@@ -262,14 +265,6 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
// In group chats, only respond when the bot is mentioned
|
||||
if isGroup && !c.isBotMentioned(msg) {
|
||||
logger.DebugCF("line", "Ignoring group message without mention", map[string]any{
|
||||
"chat_id": chatID,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Store reply token for later use
|
||||
if event.ReplyToken != "" {
|
||||
c.replyTokens.Store(chatID, replyTokenEntry{
|
||||
@@ -339,6 +334,19 @@ func (c *LINEChannel) processEvent(event lineEvent) {
|
||||
return
|
||||
}
|
||||
|
||||
// In group chats, apply unified group trigger filtering
|
||||
if isGroup {
|
||||
isMentioned := c.isBotMentioned(msg)
|
||||
respond, cleaned := c.ShouldRespondInGroup(isMentioned, content)
|
||||
if !respond {
|
||||
logger.DebugCF("line", "Ignoring group message by group trigger", map[string]any{
|
||||
"chat_id": chatID,
|
||||
})
|
||||
return
|
||||
}
|
||||
content = cleaned
|
||||
}
|
||||
|
||||
metadata := map[string]string{
|
||||
"platform": "line",
|
||||
"source_type": event.Source.Type,
|
||||
|
||||
@@ -97,7 +97,9 @@ type oneBotMessageSegment struct {
|
||||
}
|
||||
|
||||
func NewOneBotChannel(cfg config.OneBotConfig, messageBus *bus.MessageBus) (*OneBotChannel, error) {
|
||||
base := channels.NewBaseChannel("onebot", cfg, messageBus, cfg.AllowFrom)
|
||||
base := channels.NewBaseChannel("onebot", cfg, messageBus, cfg.AllowFrom,
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
)
|
||||
|
||||
const dedupSize = 1024
|
||||
return &OneBotChannel{
|
||||
@@ -996,8 +998,8 @@ func (c *OneBotChannel) handleMessage(raw *oneBotRawEvent) {
|
||||
metadata["sender_name"] = sender.Nickname
|
||||
}
|
||||
|
||||
triggered, strippedContent := c.checkGroupTrigger(content, isBotMentioned)
|
||||
if !triggered {
|
||||
respond, strippedContent := c.ShouldRespondInGroup(isBotMentioned, content)
|
||||
if !respond {
|
||||
logger.DebugCF("onebot", "Group message ignored (no trigger)", map[string]any{
|
||||
"sender": senderID,
|
||||
"group": groupIDStr,
|
||||
@@ -1069,23 +1071,3 @@ func truncate(s string, n int) string {
|
||||
}
|
||||
return string(runes[:n]) + "..."
|
||||
}
|
||||
|
||||
func (c *OneBotChannel) checkGroupTrigger(
|
||||
content string,
|
||||
isBotMentioned bool,
|
||||
) (triggered bool, strippedContent string) {
|
||||
if isBotMentioned {
|
||||
return true, strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
for _, prefix := range c.config.GroupTriggerPrefix {
|
||||
if prefix == "" {
|
||||
continue
|
||||
}
|
||||
if strings.HasPrefix(content, prefix) {
|
||||
return true, strings.TrimSpace(strings.TrimPrefix(content, prefix))
|
||||
}
|
||||
}
|
||||
|
||||
return false, content
|
||||
}
|
||||
|
||||
+10
-1
@@ -32,7 +32,9 @@ type QQChannel struct {
|
||||
}
|
||||
|
||||
func NewQQChannel(cfg config.QQConfig, messageBus *bus.MessageBus) (*QQChannel, error) {
|
||||
base := channels.NewBaseChannel("qq", cfg, messageBus, cfg.AllowFrom)
|
||||
base := channels.NewBaseChannel("qq", cfg, messageBus, cfg.AllowFrom,
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
)
|
||||
|
||||
return &QQChannel{
|
||||
BaseChannel: base,
|
||||
@@ -204,6 +206,13 @@ func (c *QQChannel) handleGroupATMessage() event.GroupATMessageEventHandler {
|
||||
return nil
|
||||
}
|
||||
|
||||
// GroupAT event means bot is always mentioned; apply group trigger filtering
|
||||
respond, cleaned := c.ShouldRespondInGroup(true, content)
|
||||
if !respond {
|
||||
return nil
|
||||
}
|
||||
content = cleaned
|
||||
|
||||
logger.InfoCF("qq", "Received group AT message", map[string]any{
|
||||
"sender": senderID,
|
||||
"group": data.GroupID,
|
||||
|
||||
@@ -47,7 +47,10 @@ func NewSlackChannel(cfg config.SlackConfig, messageBus *bus.MessageBus) (*Slack
|
||||
|
||||
socketClient := socketmode.New(api)
|
||||
|
||||
base := channels.NewBaseChannel("slack", cfg, messageBus, cfg.AllowFrom, channels.WithMaxMessageLength(40000))
|
||||
base := channels.NewBaseChannel("slack", cfg, messageBus, cfg.AllowFrom,
|
||||
channels.WithMaxMessageLength(40000),
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
)
|
||||
|
||||
return &SlackChannel{
|
||||
BaseChannel: base,
|
||||
@@ -279,6 +282,15 @@ func (c *SlackChannel) handleMessageEvent(ev *slackevents.MessageEvent) {
|
||||
content := ev.Text
|
||||
content = c.stripBotMention(content)
|
||||
|
||||
// In non-DM channels, apply group trigger filtering
|
||||
if !strings.HasPrefix(channelID, "D") {
|
||||
respond, cleaned := c.ShouldRespondInGroup(false, content)
|
||||
if !respond {
|
||||
return
|
||||
}
|
||||
content = cleaned
|
||||
}
|
||||
|
||||
var mediaPaths []string
|
||||
|
||||
scope := channels.BuildMediaScope("slack", chatID, messageTS)
|
||||
|
||||
@@ -81,6 +81,7 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
|
||||
bus,
|
||||
telegramCfg.AllowFrom,
|
||||
channels.WithMaxMessageLength(4096),
|
||||
channels.WithGroupTrigger(telegramCfg.GroupTrigger),
|
||||
)
|
||||
|
||||
return &TelegramChannel{
|
||||
@@ -417,6 +418,19 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
|
||||
content = "[empty message]"
|
||||
}
|
||||
|
||||
// In group chats, apply unified group trigger filtering
|
||||
if message.Chat.Type != "private" {
|
||||
isMentioned := c.isBotMentioned(message)
|
||||
if isMentioned {
|
||||
content = c.stripBotMention(content)
|
||||
}
|
||||
respond, cleaned := c.ShouldRespondInGroup(isMentioned, content)
|
||||
if !respond {
|
||||
return nil
|
||||
}
|
||||
content = cleaned
|
||||
}
|
||||
|
||||
logger.DebugCF("telegram", "Received message", map[string]any{
|
||||
"sender_id": senderID,
|
||||
"chat_id": fmt.Sprintf("%d", chatID),
|
||||
@@ -629,3 +643,52 @@ func escapeHTML(text string) string {
|
||||
text = strings.ReplaceAll(text, ">", ">")
|
||||
return text
|
||||
}
|
||||
|
||||
// isBotMentioned checks if the bot is mentioned in the message via entities.
|
||||
func (c *TelegramChannel) isBotMentioned(message *telego.Message) bool {
|
||||
botUsername := c.bot.Username()
|
||||
if botUsername == "" {
|
||||
return false
|
||||
}
|
||||
|
||||
entities := message.Entities
|
||||
if entities == nil {
|
||||
entities = message.CaptionEntities
|
||||
}
|
||||
|
||||
for _, entity := range entities {
|
||||
if entity.Type == "mention" {
|
||||
// Extract the mention text from the message
|
||||
text := message.Text
|
||||
if text == "" {
|
||||
text = message.Caption
|
||||
}
|
||||
runes := []rune(text)
|
||||
end := entity.Offset + entity.Length
|
||||
if end <= len(runes) {
|
||||
mention := string(runes[entity.Offset:end])
|
||||
if strings.EqualFold(mention, "@"+botUsername) {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
if entity.Type == "text_mention" && entity.User != nil {
|
||||
if entity.User.Username == botUsername {
|
||||
return true
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// stripBotMention removes the @bot mention from the content.
|
||||
func (c *TelegramChannel) stripBotMention(content string) string {
|
||||
botUsername := c.bot.Username()
|
||||
if botUsername == "" {
|
||||
return content
|
||||
}
|
||||
// Case-insensitive replacement
|
||||
re := regexp.MustCompile(`(?i)@` + regexp.QuoteMeta(botUsername))
|
||||
content = re.ReplaceAllString(content, "")
|
||||
return strings.TrimSpace(content)
|
||||
}
|
||||
|
||||
@@ -122,7 +122,10 @@ func NewWeComAppChannel(cfg config.WeComAppConfig, messageBus *bus.MessageBus) (
|
||||
return nil, fmt.Errorf("wecom_app corp_id, corp_secret and agent_id are required")
|
||||
}
|
||||
|
||||
base := channels.NewBaseChannel("wecom_app", cfg, messageBus, cfg.AllowFrom, channels.WithMaxMessageLength(2048))
|
||||
base := channels.NewBaseChannel("wecom_app", cfg, messageBus, cfg.AllowFrom,
|
||||
channels.WithMaxMessageLength(2048),
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
)
|
||||
|
||||
return &WeComAppChannel{
|
||||
BaseChannel: base,
|
||||
|
||||
@@ -86,7 +86,10 @@ func NewWeComBotChannel(cfg config.WeComConfig, messageBus *bus.MessageBus) (*We
|
||||
return nil, fmt.Errorf("wecom token and webhook_url are required")
|
||||
}
|
||||
|
||||
base := channels.NewBaseChannel("wecom", cfg, messageBus, cfg.AllowFrom, channels.WithMaxMessageLength(2048))
|
||||
base := channels.NewBaseChannel("wecom", cfg, messageBus, cfg.AllowFrom,
|
||||
channels.WithMaxMessageLength(2048),
|
||||
channels.WithGroupTrigger(cfg.GroupTrigger),
|
||||
)
|
||||
|
||||
return &WeComBotChannel{
|
||||
BaseChannel: base,
|
||||
@@ -367,6 +370,15 @@ func (c *WeComBotChannel) processMessage(ctx context.Context, msg WeComBotMessag
|
||||
// Build metadata
|
||||
peer := bus.Peer{Kind: peerKind, ID: peerID}
|
||||
|
||||
// In group chats, apply unified group trigger filtering
|
||||
if isGroupChat {
|
||||
respond, cleaned := c.ShouldRespondInGroup(false, content)
|
||||
if !respond {
|
||||
return
|
||||
}
|
||||
content = cleaned
|
||||
}
|
||||
|
||||
metadata := map[string]string{
|
||||
"msg_type": msg.MsgType,
|
||||
"msg_id": msg.MsgID,
|
||||
|
||||
Reference in New Issue
Block a user