fix: platform_silent记忆提取 + 群聊上下文整合 + 多QQ实例支持
- platform_silent模式接入Orchestrator记忆提取:被动观察群聊时提取值得记住的信息到对应命名空间 - post_chat后台思考注入平台观察:对话后思考也能看到群聊摘要 - QQ适配器:OneBot v11 self_id动态捕获、CQ图片URL提取、视觉+OCR并行处理 - Router解耦:ConfigName/PlatformName分离,支持多QQ实例独立连接 - 黑白名单功能:后端API + Ethend代理 + UI面板 - \n\n双换行断句:AI回复按双换行分割为多条消息按间隔发送 - @提及修复:bot自感知UID进行@检测 - 群聊上下文共享:channel-based userID避免记忆碎片化 - 消息日志显示处理后内容而非原始SSE数据 - platform-bridge Dockerfile + docker-compose.yml更新 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,150 @@
|
||||
package config
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
"sync"
|
||||
)
|
||||
|
||||
// BlocklistMode is either "blacklist" or "whitelist".
|
||||
type BlocklistSettings struct {
|
||||
Mode string `json:"mode"` // "blacklist" (default) or "whitelist"
|
||||
GroupIDs []string `json:"group_ids"` // group IDs to block/allow
|
||||
UserIDs []string `json:"user_ids"` // private chat user IDs to block/allow
|
||||
}
|
||||
|
||||
// BlocklistStore manages persistence of blocklist settings.
|
||||
type BlocklistStore struct {
|
||||
mu sync.RWMutex
|
||||
path string
|
||||
settings BlocklistSettings
|
||||
}
|
||||
|
||||
// NewBlocklistStore loads or creates blocklist settings file.
|
||||
func NewBlocklistStore(path string) (*BlocklistStore, error) {
|
||||
s := &BlocklistStore{
|
||||
path: path,
|
||||
settings: BlocklistSettings{
|
||||
Mode: "blacklist",
|
||||
GroupIDs: []string{},
|
||||
UserIDs: []string{},
|
||||
},
|
||||
}
|
||||
if err := s.load(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func (s *BlocklistStore) load() error {
|
||||
data, err := os.ReadFile(s.path)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
return s.save() // write defaults
|
||||
}
|
||||
return fmt.Errorf("read blocklist file: %w", err)
|
||||
}
|
||||
if len(data) == 0 {
|
||||
return nil
|
||||
}
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
if err := json.Unmarshal(data, &s.settings); err != nil {
|
||||
return fmt.Errorf("parse blocklist file: %w", err)
|
||||
}
|
||||
if s.settings.Mode == "" {
|
||||
s.settings.Mode = "blacklist"
|
||||
}
|
||||
if s.settings.GroupIDs == nil {
|
||||
s.settings.GroupIDs = []string{}
|
||||
}
|
||||
if s.settings.UserIDs == nil {
|
||||
s.settings.UserIDs = []string{}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *BlocklistStore) save() error {
|
||||
s.mu.RLock()
|
||||
data, err := json.MarshalIndent(s.settings, "", " ")
|
||||
s.mu.RUnlock()
|
||||
if err != nil {
|
||||
return fmt.Errorf("marshal blocklist: %w", err)
|
||||
}
|
||||
tmpPath := s.path + ".tmp"
|
||||
if err := os.WriteFile(tmpPath, data, 0640); err != nil {
|
||||
return fmt.Errorf("write blocklist: %w", err)
|
||||
}
|
||||
return os.Rename(tmpPath, s.path)
|
||||
}
|
||||
|
||||
// Get returns current blocklist settings.
|
||||
func (s *BlocklistStore) Get() BlocklistSettings {
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
cp := BlocklistSettings{
|
||||
Mode: s.settings.Mode,
|
||||
GroupIDs: make([]string, len(s.settings.GroupIDs)),
|
||||
UserIDs: make([]string, len(s.settings.UserIDs)),
|
||||
}
|
||||
copy(cp.GroupIDs, s.settings.GroupIDs)
|
||||
copy(cp.UserIDs, s.settings.UserIDs)
|
||||
return cp
|
||||
}
|
||||
|
||||
// Set updates and persists blocklist settings.
|
||||
func (s *BlocklistStore) Set(bs BlocklistSettings) error {
|
||||
if bs.Mode != "blacklist" && bs.Mode != "whitelist" {
|
||||
return fmt.Errorf("invalid mode: %s (must be blacklist or whitelist)", bs.Mode)
|
||||
}
|
||||
if bs.GroupIDs == nil {
|
||||
bs.GroupIDs = []string{}
|
||||
}
|
||||
if bs.UserIDs == nil {
|
||||
bs.UserIDs = []string{}
|
||||
}
|
||||
s.mu.Lock()
|
||||
s.settings = bs
|
||||
s.mu.Unlock()
|
||||
return s.save()
|
||||
}
|
||||
|
||||
// IsBlocked checks whether a message should be blocked based on channel type and ID.
|
||||
// In blacklist mode: returns true if the id is IN the list.
|
||||
// In whitelist mode: returns true if the id is NOT in the list.
|
||||
// Admin users should call this with isAdmin=true to always bypass.
|
||||
func (s *BlocklistStore) IsBlocked(channelType, channelID, senderID string, isAdmin bool) bool {
|
||||
if isAdmin {
|
||||
return false
|
||||
}
|
||||
s.mu.RLock()
|
||||
defer s.mu.RUnlock()
|
||||
|
||||
switch s.settings.Mode {
|
||||
case "whitelist":
|
||||
// Block if NOT in the whitelist.
|
||||
if channelType == "group" {
|
||||
return !contains(s.settings.GroupIDs, channelID)
|
||||
}
|
||||
return !contains(s.settings.UserIDs, senderID)
|
||||
|
||||
case "blacklist":
|
||||
fallthrough
|
||||
default:
|
||||
// Block if IN the blacklist.
|
||||
if channelType == "group" {
|
||||
return contains(s.settings.GroupIDs, channelID)
|
||||
}
|
||||
return contains(s.settings.UserIDs, senderID)
|
||||
}
|
||||
}
|
||||
|
||||
func contains(list []string, val string) bool {
|
||||
for _, v := range list {
|
||||
if v == val {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
@@ -1,6 +1,9 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
import (
|
||||
"os"
|
||||
"strings"
|
||||
)
|
||||
|
||||
// Config holds Platform Bridge configuration.
|
||||
type Config struct {
|
||||
@@ -11,9 +14,17 @@ type Config struct {
|
||||
InternalToken string
|
||||
|
||||
// Platform-specific.
|
||||
QQBotPort string // port for QQ OBv11 reverse WebSocket
|
||||
TelegramToken string // Telegram Bot API token
|
||||
TelegramWebhookURL string // public webhook URL for Telegram
|
||||
QQBotPort string // port for QQ OBv11 reverse WebSocket
|
||||
TelegramToken string // Telegram Bot API token
|
||||
TelegramWebhookURL string // public webhook URL for Telegram
|
||||
|
||||
// Silent observation mode.
|
||||
PlatformSilentEnabled bool // PLATFORM_SILENT_ENABLED, default true
|
||||
AdminNicknames []string // ADMIN_NICKNAMES, default ["开拓者"]
|
||||
AdminMentionKeywords []string // ADMIN_MENTION_KEYWORDS, default ["昔涟","Cyrene","管理员"]
|
||||
|
||||
// Message sending.
|
||||
MessageSendIntervalMs int // MSG_SEND_INTERVAL_MS, minimum interval between platform messages (default 2000)
|
||||
}
|
||||
|
||||
func Load() *Config {
|
||||
@@ -48,5 +59,54 @@ func Load() *Config {
|
||||
if v := os.Getenv("TELEGRAM_WEBHOOK_URL"); v != "" {
|
||||
cfg.TelegramWebhookURL = v
|
||||
}
|
||||
// Silent observation defaults.
|
||||
cfg.PlatformSilentEnabled = getEnvBool("PLATFORM_SILENT_ENABLED", true)
|
||||
cfg.AdminNicknames = getEnvList("ADMIN_NICKNAMES", []string{"开拓者"})
|
||||
cfg.AdminMentionKeywords = getEnvList("ADMIN_MENTION_KEYWORDS", []string{"昔涟", "Cyrene", "管理员"})
|
||||
cfg.MessageSendIntervalMs = getEnvInt("MSG_SEND_INTERVAL_MS", 2000)
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func getEnvBool(key string, defaultVal bool) bool {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return defaultVal
|
||||
}
|
||||
return v == "true" || v == "1" || v == "yes"
|
||||
}
|
||||
|
||||
func getEnvInt(key string, defaultVal int) int {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return defaultVal
|
||||
}
|
||||
n := 0
|
||||
for _, c := range v {
|
||||
if c >= '0' && c <= '9' {
|
||||
n = n*10 + int(c-'0')
|
||||
} else {
|
||||
return defaultVal
|
||||
}
|
||||
}
|
||||
return n
|
||||
}
|
||||
|
||||
func getEnvList(key string, defaultVal []string) []string {
|
||||
v := os.Getenv(key)
|
||||
if v == "" {
|
||||
return defaultVal
|
||||
}
|
||||
parts := strings.Split(v, ",")
|
||||
result := make([]string, 0, len(parts))
|
||||
for _, p := range parts {
|
||||
p = strings.TrimSpace(p)
|
||||
if p != "" {
|
||||
result = append(result, p)
|
||||
}
|
||||
}
|
||||
if len(result) == 0 {
|
||||
return defaultVal
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
@@ -11,6 +11,7 @@ import (
|
||||
// PlatformConfig holds persistent configuration for one platform adapter.
|
||||
type PlatformConfig struct {
|
||||
Name string `json:"name"`
|
||||
Platform string `json:"platform"` // base platform type: "qq", "telegram", etc.
|
||||
Enabled bool `json:"enabled"`
|
||||
Label string `json:"label"`
|
||||
Fields map[string]string `json:"fields"`
|
||||
@@ -51,6 +52,12 @@ func (s *Store) load() error {
|
||||
if err := json.Unmarshal(data, &s.configs); err != nil {
|
||||
return fmt.Errorf("parse config file: %w", err)
|
||||
}
|
||||
// Backward compat: old configs without platform field default to Name.
|
||||
for _, c := range s.configs {
|
||||
if c.Platform == "" {
|
||||
c.Platform = c.Name
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user