Files
Cyrene_v2/backend/services/platform-bridge/config.go
T
Aska 6a4887e52a feat(llm): add LLM call recording with admin panel integration
- llm.Adapter: SetCallLogger hook for recording every API call
- llm.CallLogger: ring-buffer with Recent+Stats for admin API
- main.go: wire DeepSeek adapter to CallLogger
- Fix: single CallLogger instance for both recording and display
- Metrics: model name, duration, success/fail, tokens
- All 16 packages pass
2026-08-12 19:10:51 +08:00

210 lines
5.2 KiB
Go

// Package bridge provides configuration management for platform adapters.
// Configs are stored as JSON on disk and loaded only when created via the admin panel.
// Supports hot-reload: the router watches the config file and reloads adapters on change.
package bridge
import (
"encoding/json"
"fmt"
"git.yeij.top/AskaEth/Cyrene_v2/internal/logger"
"os"
"path/filepath"
"sync"
"time"
)
// AdapterConfig holds configuration for a single platform adapter.
type AdapterConfig struct {
Name string `json:"name"`
Type string `json:"type"` // "qq-obv11" | "telegram" | "discord" | "webhook"
Enabled bool `json:"enabled"`
Settings map[string]string `json:"settings"`
CreatedAt string `json:"created_at"`
UpdatedAt string `json:"updated_at"`
}
// BridgeConfig is the top-level configuration file structure.
type BridgeConfig struct {
Version string `json:"version"`
UpdatedAt string `json:"updated_at"`
Adapters []AdapterConfig `json:"adapters"`
}
// ConfigStore manages bridge configurations with file persistence.
type ConfigStore struct {
mu sync.RWMutex
path string
config *BridgeConfig
onChange func(*BridgeConfig) // Called when config is loaded or reloaded
stopCh chan struct{}
}
// NewConfigStore creates a bridge config store. Path is the config file location.
// If the file doesn't exist, no config is created — it will be created on first save via admin panel.
func NewConfigStore(path string, onChange func(*BridgeConfig)) *ConfigStore {
s := &ConfigStore{
path: path,
config: &BridgeConfig{Version: "1.0", Adapters: []AdapterConfig{}},
onChange: onChange,
stopCh: make(chan struct{}),
}
// Try to load existing file
if _, err := os.Stat(path); err == nil {
s.load()
if s.onChange != nil {
s.onChange(s.config)
}
}
return s
}
// StartWatching begins polling the config file for changes (hot-reload).
func (s *ConfigStore) StartWatching(interval time.Duration) {
var lastMod time.Time
if info, err := os.Stat(s.path); err == nil {
lastMod = info.ModTime()
}
go func() {
ticker := time.NewTicker(interval)
defer ticker.Stop()
for {
select {
case <-s.stopCh:
return
case <-ticker.C:
info, err := os.Stat(s.path)
if err != nil {
continue
}
if info.ModTime().After(lastMod) {
lastMod = info.ModTime()
s.mu.Lock()
if s.load() {
logger.Infof("[bridge] config hot-reloaded (%d adapters)", len(s.config.Adapters))
if s.onChange != nil {
s.onChange(s.config)
}
}
s.mu.Unlock()
}
}
}
}()
}
// Stop stops the file watcher.
func (s *ConfigStore) Stop() { close(s.stopCh) }
// GetAdapters returns all configured adapters.
func (s *ConfigStore) GetAdapters() []AdapterConfig {
s.mu.RLock()
defer s.mu.RUnlock()
result := make([]AdapterConfig, len(s.config.Adapters))
copy(result, s.config.Adapters)
return result
}
// GetAdapter returns a single adapter by name.
func (s *ConfigStore) GetAdapter(name string) (*AdapterConfig, error) {
s.mu.RLock()
defer s.mu.RUnlock()
for _, a := range s.config.Adapters {
if a.Name == name {
ac := a
return &ac, nil
}
}
return nil, fmt.Errorf("adapter %s not found", name)
}
// SaveAdapter creates or updates an adapter config.
func (s *ConfigStore) SaveAdapter(cfg AdapterConfig) error {
s.mu.Lock()
defer s.mu.Unlock()
now := time.Now().Format(time.RFC3339)
cfg.UpdatedAt = now
if cfg.CreatedAt == "" {
cfg.CreatedAt = now
}
found := false
for i, a := range s.config.Adapters {
if a.Name == cfg.Name {
// Preserve creation time
if a.CreatedAt != "" { cfg.CreatedAt = a.CreatedAt }
s.config.Adapters[i] = cfg
found = true
break
}
}
if !found {
s.config.Adapters = append(s.config.Adapters, cfg)
}
s.config.UpdatedAt = now
return s.persist()
}
// DeleteAdapter removes an adapter config.
func (s *ConfigStore) DeleteAdapter(name string) error {
s.mu.Lock()
defer s.mu.Unlock()
filtered := make([]AdapterConfig, 0)
for _, a := range s.config.Adapters {
if a.Name != name {
filtered = append(filtered, a)
}
}
s.config.Adapters = filtered
s.config.UpdatedAt = time.Now().Format(time.RFC3339)
return s.persist()
}
// Count returns the number of enabled adapters.
func (s *ConfigStore) Count() int {
s.mu.RLock()
defer s.mu.RUnlock()
n := 0
for _, a := range s.config.Adapters {
if a.Enabled { n++ }
}
return n
}
// load reads the config file from disk. Returns true if loaded successfully.
func (s *ConfigStore) load() bool {
data, err := os.ReadFile(s.path)
if err != nil {
return false
}
var cfg BridgeConfig
if err := json.Unmarshal(data, &cfg); err != nil {
logger.Infof("[bridge] config parse error: %v", err)
return false
}
if cfg.Adapters == nil {
cfg.Adapters = []AdapterConfig{}
}
s.config = &cfg
return true
}
// persist writes the current config to disk.
func (s *ConfigStore) persist() error {
data, err := json.MarshalIndent(s.config, "", " ")
if err != nil {
return fmt.Errorf("marshal config: %w", err)
}
// Ensure parent dir exists
os.MkdirAll(filepath.Dir(s.path), 0o755)
if err := os.WriteFile(s.path, data, 0o644); err != nil {
return fmt.Errorf("write config: %w", err)
}
return nil
}