mirror of
https://github.com/sipeed/picoclaw.git
synced 2026-06-12 18:08:54 +00:00
feat(discord): add mention_only option for @-mention responses (#518)
* feat(discord): add mention_only option for @-mention responses Add MentionOnly config option to Discord channel. When enabled, the bot only responds when explicitly @-mentioned, useful for shared servers. - Add MentionOnly bool field to DiscordConfig - Store botUserID on startup for mention checking - Check m.Mentions before processing messages when MentionOnly is true - Update config example and README documentation * fix(discord): resolve race condition and strip mention from content - Get botUserID before opening session to avoid race condition - Add stripBotMention to remove @mention from message content - Handles both <@USER_ID> and <@!USER_ID> mention formats * fix(discord): skip mention_only check for DMs DMs should always be responded to regardless of mention_only setting. Added check to skip the mention_only logic when GuildID is empty. * Update README.md Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com> --------- Co-authored-by: Hua Audio <161028864+Huaaudio@users.noreply.github.com> Co-authored-by: Copilot <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
committed by
GitHub
parent
d692cc0cc6
commit
2fb2a733d4
@@ -334,7 +334,8 @@ picoclaw gateway
|
||||
"discord": {
|
||||
"enabled": true,
|
||||
"token": "YOUR_BOT_TOKEN",
|
||||
"allow_from": ["YOUR_USER_ID"]
|
||||
"allow_from": ["YOUR_USER_ID"],
|
||||
"mention_only": false
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -347,6 +348,10 @@ picoclaw gateway
|
||||
* Bot Permissions: `Send Messages`, `Read Message History`
|
||||
* Open the generated invite URL and add the bot to your server
|
||||
|
||||
**Optional: Mention-only mode**
|
||||
|
||||
Set `"mention_only": true` to make the bot respond only when @-mentioned. Useful for shared servers where you want the bot to respond only when explicitly called.
|
||||
|
||||
**6. Run**
|
||||
|
||||
```bash
|
||||
|
||||
@@ -57,7 +57,8 @@
|
||||
"discord": {
|
||||
"enabled": false,
|
||||
"token": "YOUR_DISCORD_BOT_TOKEN",
|
||||
"allow_from": []
|
||||
"allow_from": [],
|
||||
"mention_only": false
|
||||
},
|
||||
"qq": {
|
||||
"enabled": false,
|
||||
|
||||
+46
-9
@@ -4,6 +4,7 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
@@ -28,6 +29,7 @@ type DiscordChannel struct {
|
||||
ctx context.Context
|
||||
typingMu sync.Mutex
|
||||
typingStop map[string]chan struct{} // chatID → stop signal
|
||||
botUserID string // stored for mention checking
|
||||
}
|
||||
|
||||
func NewDiscordChannel(cfg config.DiscordConfig, bus *bus.MessageBus) (*DiscordChannel, error) {
|
||||
@@ -63,6 +65,14 @@ func (c *DiscordChannel) Start(ctx context.Context) error {
|
||||
logger.InfoC("discord", "Starting Discord bot")
|
||||
|
||||
c.ctx = ctx
|
||||
|
||||
// Get bot user ID before opening session to avoid race condition
|
||||
botUser, err := c.session.User("@me")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get bot user: %w", err)
|
||||
}
|
||||
c.botUserID = botUser.ID
|
||||
|
||||
c.session.AddHandler(c.handleMessage)
|
||||
|
||||
if err := c.session.Open(); err != nil {
|
||||
@@ -71,10 +81,6 @@ func (c *DiscordChannel) Start(ctx context.Context) error {
|
||||
|
||||
c.setRunning(true)
|
||||
|
||||
botUser, err := c.session.User("@me")
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get bot user: %w", err)
|
||||
}
|
||||
logger.InfoCF("discord", "Discord bot connected", map[string]any{
|
||||
"username": botUser.Username,
|
||||
"user_id": botUser.ID,
|
||||
@@ -131,7 +137,7 @@ func (c *DiscordChannel) Send(ctx context.Context, msg bus.OutboundMessage) erro
|
||||
}
|
||||
|
||||
func (c *DiscordChannel) sendChunk(ctx context.Context, channelID, content string) error {
|
||||
// 使用传入的 ctx 进行超时控制
|
||||
// Use the passed ctx for timeout control
|
||||
sendCtx, cancel := context.WithTimeout(ctx, sendTimeout)
|
||||
defer cancel()
|
||||
|
||||
@@ -152,7 +158,7 @@ func (c *DiscordChannel) sendChunk(ctx context.Context, channelID, content strin
|
||||
}
|
||||
}
|
||||
|
||||
// appendContent 安全地追加内容到现有文本
|
||||
// appendContent safely appends content to existing text
|
||||
func appendContent(content, suffix string) string {
|
||||
if content == "" {
|
||||
return suffix
|
||||
@@ -169,7 +175,7 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag
|
||||
return
|
||||
}
|
||||
|
||||
// 检查白名单,避免为被拒绝的用户下载附件和转录
|
||||
// Check allowlist first to avoid downloading attachments and transcribing for rejected users
|
||||
if !c.IsAllowed(m.Author.ID) {
|
||||
logger.DebugCF("discord", "Message rejected by allowlist", map[string]any{
|
||||
"user_id": m.Author.ID,
|
||||
@@ -177,6 +183,24 @@ 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 != "" {
|
||||
isMentioned := false
|
||||
for _, mention := range m.Mentions {
|
||||
if mention.ID == c.botUserID {
|
||||
isMentioned = true
|
||||
break
|
||||
}
|
||||
}
|
||||
if !isMentioned {
|
||||
logger.DebugCF("discord", "Message ignored - bot not mentioned", map[string]any{
|
||||
"user_id": m.Author.ID,
|
||||
})
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
senderID := m.Author.ID
|
||||
senderName := m.Author.Username
|
||||
if m.Author.Discriminator != "" && m.Author.Discriminator != "0" {
|
||||
@@ -184,10 +208,11 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag
|
||||
}
|
||||
|
||||
content := m.Content
|
||||
content = c.stripBotMention(content)
|
||||
mediaPaths := make([]string, 0, len(m.Attachments))
|
||||
localFiles := make([]string, 0, len(m.Attachments))
|
||||
|
||||
// 确保临时文件在函数返回时被清理
|
||||
// Ensure temp files are cleaned up when function returns
|
||||
defer func() {
|
||||
for _, file := range localFiles {
|
||||
if err := os.Remove(file); err != nil {
|
||||
@@ -211,7 +236,7 @@ func (c *DiscordChannel) handleMessage(s *discordgo.Session, m *discordgo.Messag
|
||||
if c.transcriber != nil && c.transcriber.IsAvailable() {
|
||||
ctx, cancel := context.WithTimeout(c.getContext(), transcriptionTimeout)
|
||||
result, err := c.transcriber.Transcribe(ctx, localPath)
|
||||
cancel() // 立即释放context资源,避免在for循环中泄漏
|
||||
cancel() // Release context resources immediately to avoid leaks in for loop
|
||||
|
||||
if err != nil {
|
||||
logger.ErrorCF("discord", "Voice transcription failed", map[string]any{
|
||||
@@ -333,3 +358,15 @@ func (c *DiscordChannel) downloadAttachment(url, filename string) string {
|
||||
LoggerPrefix: "discord",
|
||||
})
|
||||
}
|
||||
|
||||
// stripBotMention removes the bot mention from the message content.
|
||||
// Discord mentions have the format <@USER_ID> or <@!USER_ID> (with nickname).
|
||||
func (c *DiscordChannel) stripBotMention(text string) string {
|
||||
if c.botUserID == "" {
|
||||
return text
|
||||
}
|
||||
// Remove both regular mention <@USER_ID> and nickname mention <@!USER_ID>
|
||||
text = strings.ReplaceAll(text, fmt.Sprintf("<@%s>", c.botUserID), "")
|
||||
text = strings.ReplaceAll(text, fmt.Sprintf("<@!%s>", c.botUserID), "")
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
|
||||
@@ -215,9 +215,10 @@ type FeishuConfig struct {
|
||||
}
|
||||
|
||||
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"`
|
||||
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"`
|
||||
}
|
||||
|
||||
type MaixCamConfig struct {
|
||||
|
||||
@@ -43,9 +43,10 @@ func DefaultConfig() *Config {
|
||||
AllowFrom: FlexibleStringSlice{},
|
||||
},
|
||||
Discord: DiscordConfig{
|
||||
Enabled: false,
|
||||
Token: "",
|
||||
AllowFrom: FlexibleStringSlice{},
|
||||
Enabled: false,
|
||||
Token: "",
|
||||
AllowFrom: FlexibleStringSlice{},
|
||||
MentionOnly: false,
|
||||
},
|
||||
MaixCam: MaixCamConfig{
|
||||
Enabled: false,
|
||||
|
||||
Reference in New Issue
Block a user