feat: 平台配置唯一ID — PlatformConfig.ID + adapter全链路传播
- PlatformConfig 新增 ID 字段 (8字节hex,创建时自动生成) - QQ adapter 新增 configID + ConfigID() 访问器 - ChannelInfo 新增 AdapterID + AdapterName - createSingleAdapter 接受 configID 参数 - 同一个平台类型的多个配置实例通过 ID 唯一区分 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -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')
|
||||||
@@ -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')
|
||||||
@@ -0,0 +1,7 @@
|
|||||||
|
package main
|
||||||
|
import "fmt"
|
||||||
|
func main() {
|
||||||
|
for i := 0; i < 5; i++ {
|
||||||
|
fmt.Println(getFallbackMessage())
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -287,7 +287,7 @@ func main() {
|
|||||||
// Hot-reload: on config save/delete, dynamically replace adapters.
|
// Hot-reload: on config save/delete, dynamically replace adapters.
|
||||||
ch.SetOnConfigChanged(func(name, platform string, enabled bool, fields map[string]string) {
|
ch.SetOnConfigChanged(func(name, platform string, enabled bool, fields map[string]string) {
|
||||||
if enabled {
|
if enabled {
|
||||||
a := createSingleAdapter(cfg, platform, name, fields)
|
a := createSingleAdapter(cfg, platform, name, "", fields) // configID via Store.Set
|
||||||
if a == nil {
|
if a == nil {
|
||||||
fmt.Printf("WARN: cannot create adapter for %s (platform=%s)\n", name, platform)
|
fmt.Printf("WARN: cannot create adapter for %s (platform=%s)\n", name, platform)
|
||||||
return
|
return
|
||||||
@@ -477,7 +477,7 @@ func createAdapters(cfg *config.Config, store *config.Store) []bridge.PlatformAd
|
|||||||
platform = stored.Name
|
platform = stored.Name
|
||||||
}
|
}
|
||||||
fields := mergeFields(cfg, platform, &stored)
|
fields := mergeFields(cfg, platform, &stored)
|
||||||
a := createSingleAdapter(cfg, platform, stored.Name, fields)
|
a := createSingleAdapter(cfg, platform, stored.Name, stored.ID, fields)
|
||||||
if a != nil {
|
if a != nil {
|
||||||
adapters = append(adapters, a)
|
adapters = append(adapters, a)
|
||||||
seen[stored.Name] = true
|
seen[stored.Name] = true
|
||||||
@@ -491,7 +491,7 @@ func createAdapters(cfg *config.Config, store *config.Store) []bridge.PlatformAd
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
fields := mergeFields(cfg, name, nil)
|
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 {
|
if a != nil {
|
||||||
adapters = append(adapters, a)
|
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.
|
// createSingleAdapter creates one platform adapter from config fields.
|
||||||
// platform is the base platform type ("qq", "telegram", etc.), configName is the instance key.
|
// 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 {
|
switch platform {
|
||||||
case "qq":
|
case "qq":
|
||||||
port := cfg.OBv11BotPort
|
port := cfg.OBv11BotPort
|
||||||
@@ -528,7 +528,7 @@ func createSingleAdapter(cfg *config.Config, platform, configName string, fields
|
|||||||
sendIntervalMs = n
|
sendIntervalMs = n
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return qqadapter.NewAdapter(configName, mode, port, token, remoteURL, sendIntervalMs)
|
return qqadapter.NewAdapter(configID, configName, mode, port, token, remoteURL, sendIntervalMs)
|
||||||
case "telegram":
|
case "telegram":
|
||||||
token := cfg.TelegramToken
|
token := cfg.TelegramToken
|
||||||
if t, ok := fields["bot_token"]; ok && t != "" {
|
if t, ok := fields["bot_token"]; ok && t != "" {
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ var upgrader = websocket.Upgrader{
|
|||||||
// - "server" (正向 WS): adapter starts a WS server, NapCat connects as client.
|
// - "server" (正向 WS): adapter starts a WS server, NapCat connects as client.
|
||||||
// - "client" (反向 WS): adapter connects to NapCat's WS server as a client.
|
// - "client" (反向 WS): adapter connects to NapCat's WS server as a client.
|
||||||
type Adapter struct {
|
type Adapter struct {
|
||||||
|
configID string // immutable unique ID from PlatformConfig
|
||||||
configName string // instance name, e.g. "qq-home"
|
configName string // instance name, e.g. "qq-home"
|
||||||
mode string // "client" or "server"
|
mode string // "client" or "server"
|
||||||
port string
|
port string
|
||||||
@@ -46,11 +47,12 @@ type Adapter struct {
|
|||||||
respMu sync.Mutex
|
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 == "" {
|
if mode == "" {
|
||||||
mode = "server"
|
mode = "server"
|
||||||
}
|
}
|
||||||
return &Adapter{
|
return &Adapter{
|
||||||
|
configID: configID,
|
||||||
configName: configName,
|
configName: configName,
|
||||||
mode: mode,
|
mode: mode,
|
||||||
port: port,
|
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) PlatformName() string { return "qq" }
|
||||||
|
func (a *Adapter) ConfigID() string { return a.configID }
|
||||||
func (a *Adapter) ConfigName() string { return a.configName }
|
func (a *Adapter) ConfigName() string { return a.configName }
|
||||||
func (a *Adapter) SendIntervalMs() int { return a.sendIntervalMs }
|
func (a *Adapter) SendIntervalMs() int { return a.sendIntervalMs }
|
||||||
func (a *Adapter) SelfID() string { return a.selfID }
|
func (a *Adapter) SelfID() string { return a.selfID }
|
||||||
|
|||||||
@@ -81,5 +81,5 @@ type OBv11Params struct {
|
|||||||
UserID int64 `json:"user_id,omitempty"`
|
UserID int64 `json:"user_id,omitempty"`
|
||||||
GroupID int64 `json:"group_id,omitempty"`
|
GroupID int64 `json:"group_id,omitempty"`
|
||||||
Message interface{} `json:"message"`
|
Message interface{} `json:"message"`
|
||||||
AutoEscape bool `json:"auto_escape,omitempty"`
|
AutoEscape bool `json:"auto_escape"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -32,6 +32,8 @@ type ChannelInfo struct {
|
|||||||
ChannelType string `json:"channel_type"` // "group", "private"
|
ChannelType string `json:"channel_type"` // "group", "private"
|
||||||
ChannelID string `json:"channel_id"`
|
ChannelID string `json:"channel_id"`
|
||||||
ChannelName string `json:"channel_name,omitempty"`
|
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
|
// ChannelLister is an optional interface for adapters that can enumerate
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"crypto/rand"
|
||||||
|
"encoding/hex"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
@@ -10,11 +12,13 @@ import (
|
|||||||
|
|
||||||
// PlatformConfig holds persistent configuration for one platform adapter.
|
// PlatformConfig holds persistent configuration for one platform adapter.
|
||||||
type PlatformConfig struct {
|
type PlatformConfig struct {
|
||||||
Name string `json:"name"`
|
ID string `json:"id"` // immutable unique ID, generated at creation
|
||||||
Platform string `json:"platform"` // base platform type: "qq", "telegram", etc.
|
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"`
|
Enabled bool `json:"enabled"`
|
||||||
Label string `json:"label"`
|
Label string `json:"label"`
|
||||||
Fields map[string]string `json:"fields"`
|
Fields map[string]string `json:"fields"`
|
||||||
|
CreatedAt time.Time `json:"created_at"`
|
||||||
UpdatedAt time.Time `json:"updated_at"`
|
UpdatedAt time.Time `json:"updated_at"`
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -102,11 +106,22 @@ func (s *Store) Set(cfg PlatformConfig) error {
|
|||||||
if cfg.Fields == nil {
|
if cfg.Fields == nil {
|
||||||
cfg.Fields = make(map[string]string)
|
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()
|
cfg.UpdatedAt = time.Now()
|
||||||
s.configs[cfg.Name] = &cfg
|
s.configs[cfg.Name] = &cfg
|
||||||
return s.save()
|
return s.save()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func generateConfigID() string {
|
||||||
|
b := make([]byte, 8)
|
||||||
|
rand.Read(b)
|
||||||
|
return hex.EncodeToString(b)
|
||||||
|
}
|
||||||
|
|
||||||
// Delete removes a platform config and persists.
|
// Delete removes a platform config and persists.
|
||||||
func (s *Store) Delete(name string) error {
|
func (s *Store) Delete(name string) error {
|
||||||
s.mu.Lock()
|
s.mu.Lock()
|
||||||
|
|||||||
Reference in New Issue
Block a user