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,
|
||||
|
||||
+111
-59
@@ -204,6 +204,23 @@ type ChannelsConfig struct {
|
||||
WeComApp WeComAppConfig `json:"wecom_app"`
|
||||
}
|
||||
|
||||
// GroupTriggerConfig controls when the bot responds in group chats.
|
||||
type GroupTriggerConfig struct {
|
||||
MentionOnly bool `json:"mention_only,omitempty"`
|
||||
Prefixes []string `json:"prefixes,omitempty"`
|
||||
}
|
||||
|
||||
// TypingConfig controls typing indicator behavior (Phase 10).
|
||||
type TypingConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
}
|
||||
|
||||
// PlaceholderConfig controls placeholder message behavior (Phase 10).
|
||||
type PlaceholderConfig struct {
|
||||
Enabled bool `json:"enabled,omitempty"`
|
||||
Text string `json:"text,omitempty"`
|
||||
}
|
||||
|
||||
type WhatsAppConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WHATSAPP_ENABLED"`
|
||||
BridgeURL string `json:"bridge_url" env:"PICOCLAW_CHANNELS_WHATSAPP_BRIDGE_URL"`
|
||||
@@ -211,26 +228,33 @@ type WhatsAppConfig struct {
|
||||
}
|
||||
|
||||
type TelegramConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
|
||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
||||
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_TELEGRAM_ENABLED"`
|
||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_TELEGRAM_TOKEN"`
|
||||
Proxy string `json:"proxy" env:"PICOCLAW_CHANNELS_TELEGRAM_PROXY"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_TELEGRAM_ALLOW_FROM"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||
Typing TypingConfig `json:"typing,omitempty"`
|
||||
Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type DiscordConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
|
||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
|
||||
MentionOnly bool `json:"mention_only" env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"`
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DISCORD_ENABLED"`
|
||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_DISCORD_TOKEN"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DISCORD_ALLOW_FROM"`
|
||||
MentionOnly bool `json:"mention_only" env:"PICOCLAW_CHANNELS_DISCORD_MENTION_ONLY"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||
Typing TypingConfig `json:"typing,omitempty"`
|
||||
Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
|
||||
}
|
||||
|
||||
type MaixCamConfig struct {
|
||||
@@ -241,69 +265,82 @@ type MaixCamConfig struct {
|
||||
}
|
||||
|
||||
type QQConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
|
||||
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
|
||||
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_QQ_ENABLED"`
|
||||
AppID string `json:"app_id" env:"PICOCLAW_CHANNELS_QQ_APP_ID"`
|
||||
AppSecret string `json:"app_secret" env:"PICOCLAW_CHANNELS_QQ_APP_SECRET"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_QQ_ALLOW_FROM"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||
}
|
||||
|
||||
type DingTalkConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
|
||||
ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
|
||||
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_DINGTALK_ENABLED"`
|
||||
ClientID string `json:"client_id" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_ID"`
|
||||
ClientSecret string `json:"client_secret" env:"PICOCLAW_CHANNELS_DINGTALK_CLIENT_SECRET"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_DINGTALK_ALLOW_FROM"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||
}
|
||||
|
||||
type SlackConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
|
||||
BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
|
||||
AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_SLACK_ENABLED"`
|
||||
BotToken string `json:"bot_token" env:"PICOCLAW_CHANNELS_SLACK_BOT_TOKEN"`
|
||||
AppToken string `json:"app_token" env:"PICOCLAW_CHANNELS_SLACK_APP_TOKEN"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_SLACK_ALLOW_FROM"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||
Typing TypingConfig `json:"typing,omitempty"`
|
||||
Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
|
||||
}
|
||||
|
||||
type LINEConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
|
||||
ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
|
||||
ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"`
|
||||
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
|
||||
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
|
||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_LINE_ENABLED"`
|
||||
ChannelSecret string `json:"channel_secret" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_SECRET"`
|
||||
ChannelAccessToken string `json:"channel_access_token" env:"PICOCLAW_CHANNELS_LINE_CHANNEL_ACCESS_TOKEN"`
|
||||
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_HOST"`
|
||||
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PORT"`
|
||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_LINE_WEBHOOK_PATH"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_LINE_ALLOW_FROM"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||
Typing TypingConfig `json:"typing,omitempty"`
|
||||
Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
|
||||
}
|
||||
|
||||
type OneBotConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
|
||||
WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
|
||||
AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
|
||||
ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
|
||||
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_ONEBOT_ENABLED"`
|
||||
WSUrl string `json:"ws_url" env:"PICOCLAW_CHANNELS_ONEBOT_WS_URL"`
|
||||
AccessToken string `json:"access_token" env:"PICOCLAW_CHANNELS_ONEBOT_ACCESS_TOKEN"`
|
||||
ReconnectInterval int `json:"reconnect_interval" env:"PICOCLAW_CHANNELS_ONEBOT_RECONNECT_INTERVAL"`
|
||||
GroupTriggerPrefix []string `json:"group_trigger_prefix" env:"PICOCLAW_CHANNELS_ONEBOT_GROUP_TRIGGER_PREFIX"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_ONEBOT_ALLOW_FROM"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||
Typing TypingConfig `json:"typing,omitempty"`
|
||||
Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
|
||||
}
|
||||
|
||||
type WeComConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_ENABLED"`
|
||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_TOKEN"`
|
||||
EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_ENCODING_AES_KEY"`
|
||||
WebhookURL string `json:"webhook_url" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_URL"`
|
||||
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_HOST"`
|
||||
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PORT"`
|
||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"`
|
||||
ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"`
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_ENABLED"`
|
||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_TOKEN"`
|
||||
EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_ENCODING_AES_KEY"`
|
||||
WebhookURL string `json:"webhook_url" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_URL"`
|
||||
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_HOST"`
|
||||
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PORT"`
|
||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_WEBHOOK_PATH"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_ALLOW_FROM"`
|
||||
ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_REPLY_TIMEOUT"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||
}
|
||||
|
||||
type WeComAppConfig struct {
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_APP_ENABLED"`
|
||||
CorpID string `json:"corp_id" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_ID"`
|
||||
CorpSecret string `json:"corp_secret" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_SECRET"`
|
||||
AgentID int64 `json:"agent_id" env:"PICOCLAW_CHANNELS_WECOM_APP_AGENT_ID"`
|
||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_APP_TOKEN"`
|
||||
EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_APP_ENCODING_AES_KEY"`
|
||||
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_HOST"`
|
||||
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PORT"`
|
||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"`
|
||||
ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"`
|
||||
Enabled bool `json:"enabled" env:"PICOCLAW_CHANNELS_WECOM_APP_ENABLED"`
|
||||
CorpID string `json:"corp_id" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_ID"`
|
||||
CorpSecret string `json:"corp_secret" env:"PICOCLAW_CHANNELS_WECOM_APP_CORP_SECRET"`
|
||||
AgentID int64 `json:"agent_id" env:"PICOCLAW_CHANNELS_WECOM_APP_AGENT_ID"`
|
||||
Token string `json:"token" env:"PICOCLAW_CHANNELS_WECOM_APP_TOKEN"`
|
||||
EncodingAESKey string `json:"encoding_aes_key" env:"PICOCLAW_CHANNELS_WECOM_APP_ENCODING_AES_KEY"`
|
||||
WebhookHost string `json:"webhook_host" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_HOST"`
|
||||
WebhookPort int `json:"webhook_port" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PORT"`
|
||||
WebhookPath string `json:"webhook_path" env:"PICOCLAW_CHANNELS_WECOM_APP_WEBHOOK_PATH"`
|
||||
AllowFrom FlexibleStringSlice `json:"allow_from" env:"PICOCLAW_CHANNELS_WECOM_APP_ALLOW_FROM"`
|
||||
ReplyTimeout int `json:"reply_timeout" env:"PICOCLAW_CHANNELS_WECOM_APP_REPLY_TIMEOUT"`
|
||||
GroupTrigger GroupTriggerConfig `json:"group_trigger,omitempty"`
|
||||
}
|
||||
|
||||
type HeartbeatConfig struct {
|
||||
@@ -536,6 +573,9 @@ func LoadConfig(path string) (*Config, error) {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// Migrate legacy channel config fields to new unified structures
|
||||
cfg.migrateChannelConfigs()
|
||||
|
||||
// Auto-migrate: if only legacy providers config exists, convert to model_list
|
||||
if len(cfg.ModelList) == 0 && cfg.HasProvidersConfig() {
|
||||
cfg.ModelList = ConvertProvidersToModelList(cfg)
|
||||
@@ -549,6 +589,18 @@ func LoadConfig(path string) (*Config, error) {
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func (c *Config) migrateChannelConfigs() {
|
||||
// Discord: mention_only -> group_trigger.mention_only
|
||||
if c.Channels.Discord.MentionOnly && !c.Channels.Discord.GroupTrigger.MentionOnly {
|
||||
c.Channels.Discord.GroupTrigger.MentionOnly = true
|
||||
}
|
||||
|
||||
// OneBot: group_trigger_prefix -> group_trigger.prefixes
|
||||
if len(c.Channels.OneBot.GroupTriggerPrefix) > 0 && len(c.Channels.OneBot.GroupTrigger.Prefixes) == 0 {
|
||||
c.Channels.OneBot.GroupTrigger.Prefixes = c.Channels.OneBot.GroupTriggerPrefix
|
||||
}
|
||||
}
|
||||
|
||||
func SaveConfig(path string, cfg *Config) error {
|
||||
data, err := json.MarshalIndent(cfg, "", " ")
|
||||
if err != nil {
|
||||
|
||||
@@ -80,6 +80,7 @@ func DefaultConfig() *Config {
|
||||
WebhookPort: 18791,
|
||||
WebhookPath: "/webhook/line",
|
||||
AllowFrom: FlexibleStringSlice{},
|
||||
GroupTrigger: GroupTriggerConfig{MentionOnly: true},
|
||||
},
|
||||
OneBot: OneBotConfig{
|
||||
Enabled: false,
|
||||
|
||||
Reference in New Issue
Block a user