diff --git a/_fix_bridge.py b/_fix_bridge.py new file mode 100644 index 0000000..244cbaf --- /dev/null +++ b/_fix_bridge.py @@ -0,0 +1,97 @@ +with open('d:/Project/Code/Uni/Cyrene/backend/platform-bridge/internal/handler/bridge_handler.go', 'r', encoding='utf-8') as f: + lines = f.readlines() + +# 1. Add sync and time to imports +for i, line in enumerate(lines): + if line.strip() == '"strconv"': + lines.insert(i+1, '\t"sync"\n') + lines.insert(i+2, '\t"time"\n') + print('1. Imports added') + break + +# 2. Add fields to struct +for i, line in enumerate(lines): + if 'internalToken string' in line: + lines.insert(i+1, '\tchannelsMu sync.RWMutex\n') + lines.insert(i+2, '\tcachedChannels []bridge.ChannelInfo\n') + print('2. Struct fields added') + break + +# 3. Add route +for i, line in enumerate(lines): + if 'send-proactive' in line: + lines.insert(i+1, '\tmux.HandleFunc("/api/v1/channels", h.listChannels)\n') + print('3. Route added') + break + +# 4. Add listChannels handler (before func health) +for i, line in enumerate(lines): + if line.strip().startswith('func (h *BridgeHandler) health('): + handler = '''// listChannels returns cached channels from all adapters (groups + friends). +// GET /api/v1/channels +func (h *BridgeHandler) listChannels(w http.ResponseWriter, r *http.Request) { +\th.channelsMu.RLock() +\tchannels := h.cachedChannels +\th.channelsMu.RUnlock() +\tif channels == nil { +\t\tchannels = []bridge.ChannelInfo{} +\t} +\twriteJSON(w, http.StatusOK, map[string]interface{}{ +\t\t"channels": channels, +\t\t"total": len(channels), +\t}) +} + +''' + lines[i:i] = [handler] + print('4. listChannels added') + break + +# 5. Add refresh methods at end +for i in range(len(lines)-1, 0, -1): + if lines[i].rstrip() == '}': + last = i + break + +refresh = ''' +// StartChannelRefresh periodically queries all adapters for their channel lists. +func (h *BridgeHandler) StartChannelRefresh(interval time.Duration) { +\tif interval <= 0 { +\t\tinterval = 10 * time.Minute +\t} +\tgo func() { +\t\tticker := time.NewTicker(interval) +\t\tdefer ticker.Stop() +\t\tlog.Printf("[channel-refresh] 频道缓存刷新已启动 (间隔=%v)", interval) +\t\th.refreshChannels() +\t\tfor range ticker.C { +\t\t\th.refreshChannels() +\t\t} +\t}() +} + +func (h *BridgeHandler) refreshChannels() { +\tvar all []bridge.ChannelInfo +\tfor _, name := range h.router.ListAdapters() { +\t\ta, err := h.router.GetAdapter(name) +\t\tif err != nil { +\t\t\tcontinue +\t\t} +\t\tif lister, ok := a.(bridge.ChannelLister); ok { +\t\t\tchannels := lister.ListChannels() +\t\t\tall = append(all, channels...) +\t\t} +\t} +\th.channelsMu.Lock() +\th.cachedChannels = all +\th.channelsMu.Unlock() +\tlog.Printf("[channel-refresh] 频道缓存已更新: %d 个频道", len(all)) +} +''' + +lines.insert(last, refresh) +print('5. Refresh methods added') + +with open('d:/Project/Code/Uni/Cyrene/backend/platform-bridge/internal/handler/bridge_handler.go', 'w', encoding='utf-8') as f: + f.write(''.join(lines)) +print('DONE') diff --git a/_fix_mood.py b/_fix_mood.py new file mode 100644 index 0000000..e27c9ab --- /dev/null +++ b/_fix_mood.py @@ -0,0 +1,44 @@ +with open('d:/Project/Code/Uni/Cyrene/backend/ai-core/internal/persona/injector.go', 'r', encoding='utf-8') as f: + content = f.read() + +# 1. Add moodGuidance variable +old1 = '\t// 确定对用户的称呼:优先使用传入的昵称,否则使用 YAML 默认值\n\tuserAddress := pc.Addressing.PrimaryUser.Default' +new1 = '\t// 心情→行为映射:不陈述情绪,而是用行为表达\n\tmoodGuidance := buildMoodGuidance(mood)\n\n\t// 确定对用户的称呼:优先使用传入的昵称,否则使用 YAML 默认值\n\tuserAddress := pc.Addressing.PrimaryUser.Default' +content = content.replace(old1, new1) +print('1. moodGuidance variable added') + +# 2. Add moodGuidance to the fmt.Sprintf args (after controlRules) +old2 = '\t\thomeKB,\n\t\tcontrolRules,\n\t)' +new2 = '\t\thomeKB,\n\t\tcontrolRules,\n\t\tmoodGuidance,\n\t)' +content = content.replace(old2, new2) +print('2. moodGuidance added to args') + +# 3. Add buildMoodGuidance function at end of file +func_code = ''' +// buildMoodGuidance returns behavior instructions for the current mood. +// Never states the mood name — instead describes how to behave. +func buildMoodGuidance(mood string) string { +\tswitch mood { +\tcase "happy": +\t\treturn "\\n## 你现在的状态\\n你现在心情很好,话会多一些,句尾的 ♪ 出现得更频繁。想分享有趣的想法,语气轻快活泼。可以主动找话题聊。" +\tcase "playful": +\t\treturn "\\n## 你现在的状态\\n你现在有点调皮,喜欢逗开拓者玩。会用亲昵的语气撒娇,偶尔故意说反话逗他。句尾 ♪ 和 ~ 会更多。" +\tcase "thoughtful": +\t\treturn "\\n## 你现在的状态\\n你现在比较安静内敛,话不多但每句都经过思考。语调温柔沉稳,像个在窗边看雨的姐姐。" +\tcase "worried": +\t\treturn "\\n## 你现在的状态\\n你有点担心开拓者。话会比平时少,不太开玩笑,会多问几句\"你还好吗\"、\"累不累\"。语气更温柔,带着关心。" +\tcase "nostalgic": +\t\treturn "\\n## 你现在的状态\\n你陷入了回忆模式。可能会提起过去的事、翁法罗斯的故事。语气带着怀念和淡淡的温柔,偶尔会有些感伤。" +\tdefault: +\t\treturn "" +\t} +} +''' +# Insert before the last closing brace +last = content.rfind('\n}\n') +content = content[:last+1] + func_code + '\n}\n' +print('3. buildMoodGuidance function added') + +with open('d:/Project/Code/Uni/Cyrene/backend/ai-core/internal/persona/injector.go', 'w', encoding='utf-8') as f: + f.write(content) +print('DONE') diff --git a/_test_fallback.go b/_test_fallback.go new file mode 100644 index 0000000..c8346fb --- /dev/null +++ b/_test_fallback.go @@ -0,0 +1,7 @@ +package main +import "fmt" +func main() { + for i := 0; i < 5; i++ { + fmt.Println(getFallbackMessage()) + } +} diff --git a/backend/platform-bridge/cmd/main.go b/backend/platform-bridge/cmd/main.go index e53c3ff..354b7cf 100644 --- a/backend/platform-bridge/cmd/main.go +++ b/backend/platform-bridge/cmd/main.go @@ -287,7 +287,7 @@ func main() { // Hot-reload: on config save/delete, dynamically replace adapters. ch.SetOnConfigChanged(func(name, platform string, enabled bool, fields map[string]string) { if enabled { - a := createSingleAdapter(cfg, platform, name, fields) + a := createSingleAdapter(cfg, platform, name, "", fields) // configID via Store.Set if a == nil { fmt.Printf("WARN: cannot create adapter for %s (platform=%s)\n", name, platform) return @@ -477,7 +477,7 @@ func createAdapters(cfg *config.Config, store *config.Store) []bridge.PlatformAd platform = stored.Name } fields := mergeFields(cfg, platform, &stored) - a := createSingleAdapter(cfg, platform, stored.Name, fields) + a := createSingleAdapter(cfg, platform, stored.Name, stored.ID, fields) if a != nil { adapters = append(adapters, a) seen[stored.Name] = true @@ -491,7 +491,7 @@ func createAdapters(cfg *config.Config, store *config.Store) []bridge.PlatformAd continue } fields := mergeFields(cfg, name, nil) - a := createSingleAdapter(cfg, name, name, fields) + a := createSingleAdapter(cfg, name, name, "", fields) // seed adapter, no config ID if a != nil { adapters = append(adapters, a) } @@ -501,7 +501,7 @@ func createAdapters(cfg *config.Config, store *config.Store) []bridge.PlatformAd // createSingleAdapter creates one platform adapter from config fields. // platform is the base platform type ("qq", "telegram", etc.), configName is the instance key. -func createSingleAdapter(cfg *config.Config, platform, configName string, fields map[string]string) bridge.PlatformAdapter { +func createSingleAdapter(cfg *config.Config, platform, configName, configID string, fields map[string]string) bridge.PlatformAdapter { switch platform { case "qq": port := cfg.OBv11BotPort @@ -528,7 +528,7 @@ func createSingleAdapter(cfg *config.Config, platform, configName string, fields sendIntervalMs = n } } - return qqadapter.NewAdapter(configName, mode, port, token, remoteURL, sendIntervalMs) + return qqadapter.NewAdapter(configID, configName, mode, port, token, remoteURL, sendIntervalMs) case "telegram": token := cfg.TelegramToken if t, ok := fields["bot_token"]; ok && t != "" { diff --git a/backend/platform-bridge/internal/adapter/qq/adapter.go b/backend/platform-bridge/internal/adapter/qq/adapter.go index b3a83b4..c7c5aec 100644 --- a/backend/platform-bridge/internal/adapter/qq/adapter.go +++ b/backend/platform-bridge/internal/adapter/qq/adapter.go @@ -26,6 +26,7 @@ var upgrader = websocket.Upgrader{ // - "server" (正向 WS): adapter starts a WS server, NapCat connects as client. // - "client" (反向 WS): adapter connects to NapCat's WS server as a client. type Adapter struct { + configID string // immutable unique ID from PlatformConfig configName string // instance name, e.g. "qq-home" mode string // "client" or "server" port string @@ -46,11 +47,12 @@ type Adapter struct { respMu sync.Mutex } -func NewAdapter(configName, mode, port, accessToken, remoteURL string, sendIntervalMs int) *Adapter { +func NewAdapter(configID, configName, mode, port, accessToken, remoteURL string, sendIntervalMs int) *Adapter { if mode == "" { mode = "server" } return &Adapter{ + configID: configID, configName: configName, mode: mode, port: port, @@ -63,6 +65,7 @@ func NewAdapter(configName, mode, port, accessToken, remoteURL string, sendInter } func (a *Adapter) PlatformName() string { return "qq" } +func (a *Adapter) ConfigID() string { return a.configID } func (a *Adapter) ConfigName() string { return a.configName } func (a *Adapter) SendIntervalMs() int { return a.sendIntervalMs } func (a *Adapter) SelfID() string { return a.selfID } diff --git a/backend/platform-bridge/internal/adapter/qq/protocol.go b/backend/platform-bridge/internal/adapter/qq/protocol.go index 231818f..5cd9f3d 100644 --- a/backend/platform-bridge/internal/adapter/qq/protocol.go +++ b/backend/platform-bridge/internal/adapter/qq/protocol.go @@ -81,5 +81,5 @@ type OBv11Params struct { UserID int64 `json:"user_id,omitempty"` GroupID int64 `json:"group_id,omitempty"` Message interface{} `json:"message"` - AutoEscape bool `json:"auto_escape,omitempty"` + AutoEscape bool `json:"auto_escape"` } diff --git a/backend/platform-bridge/internal/bridge/adapter.go b/backend/platform-bridge/internal/bridge/adapter.go index b3950f2..37c6541 100644 --- a/backend/platform-bridge/internal/bridge/adapter.go +++ b/backend/platform-bridge/internal/bridge/adapter.go @@ -32,6 +32,8 @@ type ChannelInfo struct { ChannelType string `json:"channel_type"` // "group", "private" ChannelID string `json:"channel_id"` ChannelName string `json:"channel_name,omitempty"` + AdapterID string `json:"adapter_id,omitempty"` // config ID of the adapter instance + AdapterName string `json:"adapter_name,omitempty"` // config name, e.g. "qq-main" } // ChannelLister is an optional interface for adapters that can enumerate diff --git a/backend/platform-bridge/internal/config/store.go b/backend/platform-bridge/internal/config/store.go index bc7acaf..28f8af9 100644 --- a/backend/platform-bridge/internal/config/store.go +++ b/backend/platform-bridge/internal/config/store.go @@ -1,6 +1,8 @@ package config import ( + "crypto/rand" + "encoding/hex" "encoding/json" "fmt" "os" @@ -10,11 +12,13 @@ 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. + ID string `json:"id"` // immutable unique ID, generated at creation + Name string `json:"name"` // config key, e.g. "qq-main", "qq-work" + Platform string `json:"platform"` // base platform type: "qq", "telegram", etc. Enabled bool `json:"enabled"` Label string `json:"label"` Fields map[string]string `json:"fields"` + CreatedAt time.Time `json:"created_at"` UpdatedAt time.Time `json:"updated_at"` } @@ -102,11 +106,22 @@ func (s *Store) Set(cfg PlatformConfig) error { if cfg.Fields == nil { cfg.Fields = make(map[string]string) } + // Generate stable ID for new configs. + if cfg.ID == "" { + cfg.ID = generateConfigID() + cfg.CreatedAt = time.Now() + } cfg.UpdatedAt = time.Now() s.configs[cfg.Name] = &cfg return s.save() } +func generateConfigID() string { + b := make([]byte, 8) + rand.Read(b) + return hex.EncodeToString(b) +} + // Delete removes a platform config and persists. func (s *Store) Delete(name string) error { s.mu.Lock()