package config import ( "log/slog" "strings" "github.com/fsnotify/fsnotify" "github.com/spf13/viper" ) // DoubaoConfig holds 火山引擎豆包 ASR credentials. type DoubaoConfig struct { AppID string `mapstructure:"app_id"` AccessToken string `mapstructure:"access_token"` ResourceID string `mapstructure:"resource_id"` Hotwords []string `mapstructure:"hotwords"` // 本地热词列表 } // SecurityConfig holds authentication settings. type SecurityConfig struct { Token string `mapstructure:"token"` } // ServerConfig holds server settings. type ServerConfig struct { Port int `mapstructure:"port"` TLSAuto bool `mapstructure:"tls_auto"` } // Config is the top-level configuration. type Config struct { Doubao DoubaoConfig `mapstructure:"doubao"` Server ServerConfig `mapstructure:"server"` Security SecurityConfig `mapstructure:"security"` } // defaults returns a Config with default values. func defaults() Config { return Config{ Doubao: DoubaoConfig{ ResourceID: "volc.seedasr.sauc.duration", }, Server: ServerConfig{ Port: 8443, TLSAuto: true, }, } } // Load reads config from file (or uses defaults if file doesn't exist). // Empty path defaults to "config.yaml". func Load(path string) (Config, error) { if path == "" { path = "config.yaml" } v := viper.New() v.SetConfigFile(path) v.SetConfigType("yaml") // Set defaults def := defaults() v.SetDefault("doubao.resource_id", def.Doubao.ResourceID) v.SetDefault("server.port", def.Server.Port) v.SetDefault("server.tls_auto", def.Server.TLSAuto) // Allow env var overrides (e.g., VOICEPASTE_DOUBAO_APP_ID) v.SetEnvPrefix("voicepaste") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() // Read config file (ignore error if file doesn't exist) if err := v.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { return Config{}, err } slog.Warn("config file not found, using defaults", "path", path) } var cfg Config if err := v.Unmarshal(&cfg); err != nil { return Config{}, err } return cfg, nil } // WatchAndReload starts watching config file for changes and reloads automatically. // Empty path defaults to "config.yaml". func WatchAndReload(path string) { if path == "" { path = "config.yaml" } v := viper.New() v.SetConfigFile(path) v.SetConfigType("yaml") // Set defaults (same as Load) def := defaults() v.SetDefault("doubao.resource_id", def.Doubao.ResourceID) v.SetDefault("server.port", def.Server.Port) v.SetDefault("server.tls_auto", def.Server.TLSAuto) v.SetEnvPrefix("voicepaste") v.SetEnvKeyReplacer(strings.NewReplacer(".", "_")) v.AutomaticEnv() // Initial read (ignore error if file doesn't exist) if err := v.ReadInConfig(); err != nil { if _, ok := err.(viper.ConfigFileNotFoundError); !ok { slog.Warn("config watch: initial read failed", "err", err) } } // Watch for changes v.WatchConfig() v.OnConfigChange(func(e fsnotify.Event) { slog.Info("config file changed, reloading", "file", e.Name) var cfg Config if err := v.Unmarshal(&cfg); err != nil { slog.Error("config reload failed", "err", err) return } slog.Info("config reloaded successfully") }) } // Get returns the current config snapshot. // Note: After switching to Viper, this is deprecated. // Use viper.Get* methods or Load() directly instead. func Get() Config { return defaults() }