45fac267fe
- PlatformConfig 新增 ID 字段 (8字节hex,创建时自动生成) - QQ adapter 新增 configID + ConfigID() 访问器 - ChannelInfo 新增 AdapterID + AdapterName - createSingleAdapter 接受 configID 参数 - 同一个平台类型的多个配置实例通过 ID 唯一区分 Co-Authored-By: Claude <noreply@anthropic.com>
143 lines
3.4 KiB
Go
143 lines
3.4 KiB
Go
package config
|
|
|
|
import (
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"encoding/json"
|
|
"fmt"
|
|
"os"
|
|
"sync"
|
|
"time"
|
|
)
|
|
|
|
// PlatformConfig holds persistent configuration for one platform adapter.
|
|
type PlatformConfig struct {
|
|
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"`
|
|
}
|
|
|
|
// Store manages persistence of platform configs to a JSON file.
|
|
type Store struct {
|
|
mu sync.RWMutex
|
|
path string
|
|
configs map[string]*PlatformConfig
|
|
}
|
|
|
|
// NewStore creates a Store, creating the config file if it doesn't exist.
|
|
func NewStore(path string) (*Store, error) {
|
|
s := &Store{
|
|
path: path,
|
|
configs: make(map[string]*PlatformConfig),
|
|
}
|
|
if err := s.load(); err != nil {
|
|
return nil, err
|
|
}
|
|
return s, nil
|
|
}
|
|
|
|
func (s *Store) load() error {
|
|
data, err := os.ReadFile(s.path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
// Initialize empty file.
|
|
return s.save()
|
|
}
|
|
return fmt.Errorf("read config file: %w", err)
|
|
}
|
|
if len(data) == 0 {
|
|
return nil
|
|
}
|
|
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
|
|
}
|
|
|
|
func (s *Store) save() error {
|
|
data, err := json.MarshalIndent(s.configs, "", " ")
|
|
if err != nil {
|
|
return fmt.Errorf("marshal configs: %w", err)
|
|
}
|
|
tmpPath := s.path + ".tmp"
|
|
if err := os.WriteFile(tmpPath, data, 0640); err != nil {
|
|
return fmt.Errorf("write config file: %w", err)
|
|
}
|
|
return os.Rename(tmpPath, s.path)
|
|
}
|
|
|
|
// List returns all platform configs.
|
|
func (s *Store) List() []PlatformConfig {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
result := make([]PlatformConfig, 0, len(s.configs))
|
|
for _, c := range s.configs {
|
|
result = append(result, *c)
|
|
}
|
|
return result
|
|
}
|
|
|
|
// Get returns a single platform config.
|
|
func (s *Store) Get(name string) (*PlatformConfig, error) {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
c, ok := s.configs[name]
|
|
if !ok {
|
|
return nil, fmt.Errorf("config not found: %s", name)
|
|
}
|
|
return c, nil
|
|
}
|
|
|
|
// Set upserts a platform config and persists.
|
|
func (s *Store) Set(cfg PlatformConfig) error {
|
|
s.mu.Lock()
|
|
defer s.mu.Unlock()
|
|
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()
|
|
defer s.mu.Unlock()
|
|
if _, ok := s.configs[name]; !ok {
|
|
return fmt.Errorf("config not found: %s", name)
|
|
}
|
|
delete(s.configs, name)
|
|
return s.save()
|
|
}
|
|
|
|
// HasConfig checks if a config exists for the given platform.
|
|
func (s *Store) HasConfig(name string) bool {
|
|
s.mu.RLock()
|
|
defer s.mu.RUnlock()
|
|
_, ok := s.configs[name]
|
|
return ok
|
|
}
|