package config import ( "fmt" "log/slog" "os" "path/filepath" "strconv" "github.com/fsnotify/fsnotify" "gopkg.in/yaml.v3" ) // Load reads config from file (optional), applies env overrides, validates, and stores globally. // If configPath is empty, it tries "config.yaml" in the working directory. func Load(configPath string) (Config, error) { cfg := defaults() // Try loading YAML file if configPath == "" { configPath = "config.yaml" } if data, err := os.ReadFile(configPath); err == nil { if err := yaml.Unmarshal(data, &cfg); err != nil { return cfg, fmt.Errorf("parse config %s: %w", configPath, err) } slog.Info("loaded config file", "path", configPath) } // Env overrides applyEnv(&cfg) // Validate if err := validate(cfg); err != nil { return cfg, err } store(cfg) return cfg, nil } // applyEnv overrides config fields with environment variables. func applyEnv(cfg *Config) { envStringMap := map[string]*string{ "DOUBAO_APP_ID": &cfg.Doubao.AppID, "DOUBAO_ACCESS_TOKEN": &cfg.Doubao.AccessToken, "DOUBAO_RESOURCE_ID": &cfg.Doubao.ResourceID, } for key, target := range envStringMap { if v := os.Getenv(key); v != "" { *target = v } } if v := os.Getenv("PORT"); v != "" { if port, err := strconv.Atoi(v); err == nil { cfg.Server.Port = port } } if v := os.Getenv("TLS_AUTO"); v != "" { cfg.Server.TLSAuto = v == "true" || v == "1" } } // validate checks required fields. func validate(cfg Config) error { if cfg.Doubao.AppID == "" { return fmt.Errorf("doubao.app_id is required (set DOUBAO_APP_ID or config.yaml)") } if cfg.Doubao.AccessToken == "" { return fmt.Errorf("doubao.access_token is required (set DOUBAO_ACCESS_TOKEN or config.yaml)") } return nil } // WatchAndReload watches the config file for changes and hot-reloads. func WatchAndReload(configPath string) { if configPath == "" { configPath = "config.yaml" } absPath, err := filepath.Abs(configPath) if err != nil { slog.Warn("cannot resolve config path for watching", "error", err) return } watcher, err := fsnotify.NewWatcher() if err != nil { slog.Warn("cannot create file watcher", "error", err) return } dir := filepath.Dir(absPath) if err := watcher.Add(dir); err != nil { slog.Warn("cannot watch config directory", "error", err) watcher.Close() return } slog.Info("watching config for changes", "path", absPath) go func() { defer watcher.Close() for { select { case event, ok := <-watcher.Events: if !ok { return } if filepath.Clean(event.Name) == absPath && (event.Has(fsnotify.Write) || event.Has(fsnotify.Create)) { slog.Info("config file changed, reloading") if _, err := Load(configPath); err != nil { slog.Error("failed to reload config", "error", err) } else { slog.Info("config reloaded successfully") } } case err, ok := <-watcher.Errors: if !ok { return } slog.Error("config watcher error", "error", err) } } }() }