Files
goclaw/internal/channels/telegram/topic_config.go
T

133 lines
4.1 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
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,
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 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 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
}
// 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
}