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:
2026-05-31 09:37:18 +08:00
parent 71f0a1abdb
commit 47dce276a4
22 changed files with 2375 additions and 313 deletions
@@ -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
}