mirror of
https://github.com/tiennm99/goclaw.git
synced 2026-10-14 02:15:57 +00:00
* feat(telegram): add yield mention mode for multi-bot group support - Add "yield" mention_mode: bot responds to all messages unless another bot is explicitly @mentioned, enabling shared groups where bots coexist - Skip messages from other bots (user.IsBot) in yield mode to prevent infinite cross-bot loops - Enter mention gate for yield mode even when require_mention is false - Add display_name to channel instance update allowlist - Change default dev postgres port to 5444 to avoid conflicts * fix(telegram): address yield mode review — pairing guard, mention check, naming - Add pairing guard to bot-skip path in yield mode (prevent history recording in unpaired groups — matches existing security pattern) - Check detectMention before skipping other bots' messages (allow cross-bot @commands like "@my_bot help" from another bot) - Rename hasOtherBotMention → hasOtherMention (function yields on any @mention, not just bots — name now matches behavior) - Remove dead MentionMode field from SlackConfig (no Slack handler uses it) - Add mention_mode validation with warning log for unknown values * fix(ui): swap mention_mode and require_mention field order * fix(i18n): add translations for require_mention disabled hint * fix(ui): apply mention_mode disabled hint to group override tab * fix(i18n): improve mention_mode UI labels for clarity Replace developer jargon (Strict/Yield) with user-friendly labels: - "Mention Mode" → "Group Response Behavior" - "Strict" → "Default (follow @mention setting)" - "Yield" → "Multi-bot (respond unless another bot is @mentioned)" --------- Co-authored-by: viettranx <viettranx@gmail.com>
150 lines
4.6 KiB
Go
150 lines
4.6 KiB
Go
package telegram
|
|
|
|
import (
|
|
"fmt"
|
|
"strings"
|
|
|
|
"github.com/nextlevelbuilder/goclaw/internal/config"
|
|
)
|
|
|
|
// resolvedTopicConfig holds the merged config for a specific group+topic combination.
|
|
// Fields are resolved in order: global TelegramConfig → wildcard group ("*") → specific group → specific topic.
|
|
// TS ref: resolveTelegramGroupConfig() in src/telegram/bot.ts + resolveTelegramGroupPromptSettings() in group-config-helpers.ts.
|
|
type resolvedTopicConfig struct {
|
|
groupPolicy string
|
|
requireMention *bool
|
|
mentionMode string // "strict" (default) or "yield"
|
|
allowFrom []string
|
|
enabled *bool
|
|
skills []string // nil = inherit, non-nil = override (empty = no skills)
|
|
tools []string // nil = inherit (all tools), non-nil = override (supports "group:xxx")
|
|
systemPrompt string // concatenated group + topic prompts
|
|
}
|
|
|
|
// resolveTopicConfig resolves the effective config for a chat/topic by merging layers.
|
|
// chatIDStr is the raw Telegram chat ID (e.g., "-100123456").
|
|
// topicID is the forum topic thread ID (0 = not a forum topic).
|
|
func resolveTopicConfig(cfg config.TelegramConfig, chatIDStr string, topicID int) resolvedTopicConfig {
|
|
result := resolvedTopicConfig{
|
|
groupPolicy: cfg.GroupPolicy,
|
|
requireMention: cfg.RequireMention,
|
|
mentionMode: cfg.MentionMode,
|
|
allowFrom: cfg.AllowFrom,
|
|
}
|
|
|
|
if cfg.Groups == nil {
|
|
return result
|
|
}
|
|
|
|
// Layer 1: wildcard group config ("*") — applies to all groups unless overridden.
|
|
if wildcard, ok := cfg.Groups["*"]; ok && wildcard != nil {
|
|
mergeGroupInto(&result, wildcard)
|
|
}
|
|
|
|
// Layer 2: specific group config — overrides wildcard.
|
|
groupCfg, ok := cfg.Groups[chatIDStr]
|
|
if !ok || groupCfg == nil {
|
|
return result
|
|
}
|
|
mergeGroupInto(&result, groupCfg)
|
|
|
|
// Layer 3: specific topic config — overrides group.
|
|
if topicID > 0 && groupCfg.Topics != nil {
|
|
topicIDStr := fmt.Sprintf("%d", topicID)
|
|
if topicCfg, ok := groupCfg.Topics[topicIDStr]; ok && topicCfg != nil {
|
|
mergeTopicInto(&result, topicCfg, groupCfg)
|
|
}
|
|
}
|
|
|
|
return result
|
|
}
|
|
|
|
// mergeGroupInto applies non-zero group config values over the current result.
|
|
func mergeGroupInto(dst *resolvedTopicConfig, src *config.TelegramGroupConfig) {
|
|
if src.GroupPolicy != "" {
|
|
dst.groupPolicy = src.GroupPolicy
|
|
}
|
|
if src.RequireMention != nil {
|
|
dst.requireMention = src.RequireMention
|
|
}
|
|
if src.MentionMode != "" {
|
|
dst.mentionMode = src.MentionMode
|
|
}
|
|
if len(src.AllowFrom) > 0 {
|
|
dst.allowFrom = src.AllowFrom
|
|
}
|
|
if src.Enabled != nil {
|
|
dst.enabled = src.Enabled
|
|
}
|
|
if src.Skills != nil {
|
|
dst.skills = src.Skills
|
|
}
|
|
if src.Tools != nil {
|
|
dst.tools = src.Tools
|
|
}
|
|
if src.SystemPrompt != "" {
|
|
dst.systemPrompt = src.SystemPrompt
|
|
}
|
|
}
|
|
|
|
// mergeTopicInto applies topic config values, with special handling for systemPrompt
|
|
// (concatenated: group + topic, matching TS resolveTelegramGroupPromptSettings).
|
|
func mergeTopicInto(dst *resolvedTopicConfig, src *config.TelegramTopicConfig, groupCfg *config.TelegramGroupConfig) {
|
|
if src.GroupPolicy != "" {
|
|
dst.groupPolicy = src.GroupPolicy
|
|
}
|
|
if src.RequireMention != nil {
|
|
dst.requireMention = src.RequireMention
|
|
}
|
|
if src.MentionMode != "" {
|
|
dst.mentionMode = src.MentionMode
|
|
}
|
|
if len(src.AllowFrom) > 0 {
|
|
dst.allowFrom = src.AllowFrom
|
|
}
|
|
if src.Enabled != nil {
|
|
dst.enabled = src.Enabled
|
|
}
|
|
// Skills: topic overrides group (firstDefined pattern, matching TS).
|
|
if src.Skills != nil {
|
|
dst.skills = src.Skills
|
|
}
|
|
// Tools: topic overrides group (same firstDefined pattern as skills).
|
|
if src.Tools != nil {
|
|
dst.tools = src.Tools
|
|
}
|
|
// SystemPrompt: concatenate group + topic (both may exist, matching TS).
|
|
if src.SystemPrompt != "" {
|
|
parts := []string{}
|
|
if groupCfg.SystemPrompt != "" {
|
|
parts = append(parts, strings.TrimSpace(groupCfg.SystemPrompt))
|
|
}
|
|
parts = append(parts, strings.TrimSpace(src.SystemPrompt))
|
|
dst.systemPrompt = strings.Join(parts, "\n\n")
|
|
}
|
|
}
|
|
|
|
// isEnabled returns whether the resolved config allows the bot to operate.
|
|
// nil = enabled (default), false = disabled.
|
|
func (r *resolvedTopicConfig) isEnabled() bool {
|
|
return r.enabled == nil || *r.enabled
|
|
}
|
|
|
|
// effectiveMentionMode returns the resolved mention_mode value.
|
|
// Falls back to the channel default if not overridden at group/topic level.
|
|
func (r *resolvedTopicConfig) effectiveMentionMode(defaultVal string) string {
|
|
if r.mentionMode != "" {
|
|
return r.mentionMode
|
|
}
|
|
return defaultVal
|
|
}
|
|
|
|
// effectiveRequireMention returns the resolved require_mention value.
|
|
// Falls back to the provided default if not overridden.
|
|
func (r *resolvedTopicConfig) effectiveRequireMention(defaultVal bool) bool {
|
|
if r.requireMention != nil {
|
|
return *r.requireMention
|
|
}
|
|
return defaultVal
|
|
}
|