package config import ( "fmt" "path/filepath" "github.com/joho/godotenv" "github.com/spf13/viper" ) // Config 应用配置。 type Config struct { App AppConfig `mapstructure:"app"` Server ServerConfig `mapstructure:"server"` Session SessionConfig `mapstructure:"session"` Redis RedisConfig `mapstructure:"redis"` AI AIConfig `mapstructure:"ai"` Storage StorageConfig `mapstructure:"storage"` Log LogConfig `mapstructure:"log"` Auth AuthConfig `mapstructure:"auth"` } // SessionConfig 会话管理配置。 type SessionConfig struct { TTL int `mapstructure:"ttl"` // 会话过期时间(分钟) MaxHistory int `mapstructure:"max_history"` // 对话历史上限(条) } type AppConfig struct { Env string `mapstructure:"env"` Version string `mapstructure:"version"` } type ServerConfig struct { Host string `mapstructure:"host"` Port int `mapstructure:"port"` ReadTimeout int `mapstructure:"read_timeout"` WriteTimeout int `mapstructure:"write_timeout"` HeartbeatInterval int `mapstructure:"heartbeat_interval"` // 心跳检查间隔(秒) HeartbeatTimeout int `mapstructure:"heartbeat_timeout"` // 心跳超时(秒) ShutdownTimeout int `mapstructure:"shutdown_timeout"` // 优雅关闭超时(秒) AllowedOrigins []string `mapstructure:"allowed_origins"` // CORS 允许的来源,空表示允许所有 } // Addr 返回 host:port 地址。 func (s ServerConfig) Addr() string { return fmt.Sprintf("%s:%d", s.Host, s.Port) } type RedisConfig struct { Addr string `mapstructure:"addr"` Password string `mapstructure:"password"` DB int `mapstructure:"db"` } type AIConfig struct { STT STTConfig `mapstructure:"stt"` LLM LLMConfig `mapstructure:"llm"` TTS TTSConfig `mapstructure:"tts"` } type STTConfig struct { Provider string `mapstructure:"provider"` APIKey string `mapstructure:"api_key"` Model string `mapstructure:"model"` Endpoint string `mapstructure:"endpoint"` Timeout int `mapstructure:"timeout"` // STT 超时(秒) HTTPClientTimeout int `mapstructure:"http_client_timeout"` // HTTP 客户端超时(秒) } type LLMConfig struct { Provider string `mapstructure:"provider"` APIKey string `mapstructure:"api_key"` Model string `mapstructure:"model"` Endpoint string `mapstructure:"endpoint"` Timeout int `mapstructure:"timeout"` HTTPClientTimeout int `mapstructure:"http_client_timeout"` // HTTP 客户端超时(秒) } type TTSConfig struct { Provider string `mapstructure:"provider"` APIKey string `mapstructure:"api_key"` Model string `mapstructure:"model"` Voice string `mapstructure:"voice"` Speed float64 `mapstructure:"speed"` Endpoint string `mapstructure:"endpoint"` Timeout int `mapstructure:"timeout"` HTTPClientTimeout int `mapstructure:"http_client_timeout"` // HTTP 客户端超时(秒) OutputFormat string `mapstructure:"output_format"` // 输出格式:mp3/wav SampleRate int `mapstructure:"sample_rate"` // 输出采样率 } type StorageConfig struct { Redis RedisStorageConfig `mapstructure:"redis"` Persistence PersistenceConfig `mapstructure:"persistence"` // Deprecated: 使用 Redis 和 Persistence 替代 Driver string `mapstructure:"driver"` DSN string `mapstructure:"dsn"` } type RedisStorageConfig struct { Enabled bool `mapstructure:"enabled"` } type PersistenceConfig struct { Enabled bool `mapstructure:"enabled"` Driver string `mapstructure:"driver"` DSN string `mapstructure:"dsn"` } type LogConfig struct { Level string `mapstructure:"level"` Format string `mapstructure:"format"` } // AuthConfig 认证配置。 type AuthConfig struct { JWTSecret string `mapstructure:"jwt_secret"` // JWT 签名密钥,必须通过环境变量 CAMTALK_AUTH_JWT_SECRET 设置 AccessTTL int `mapstructure:"access_ttl"` // Access Token 过期时间(分钟),默认 15 RefreshTTL int `mapstructure:"refresh_ttl"` // Refresh Token 过期时间(分钟),默认 10080(7天) } // Load 加载配置。优先级:环境变量 > config.{env}.yaml > config.yaml > 默认值。 // workDir 为项目根目录或 backend 目录,用于定位 .env 和 config.yaml。 func Load(workDir string) (*Config, error) { // 1. 加载 .env 文件(敏感信息) envFile := filepath.Join(workDir, ".env") _ = godotenv.Load(envFile) // 文件不存在也不报错 v := viper.New() v.SetConfigName("config") v.SetConfigType("yaml") v.AddConfigPath(workDir) // 2. 设置默认值(与 config.yaml 保持一致,仅作为兜底) setDefaults(v) // 3. 读取 config.yaml if err := v.ReadInConfig(); err != nil { return nil, fmt.Errorf("config: read config.yaml: %w", err) } // 4. 合并环境专属配置 config.{env}.yaml(可选) env := v.GetString("app.env") if env != "" { v.SetConfigName("config." + env) _ = v.MergeInConfig() // 文件不存在也不报错 } // 5. 显式绑定敏感信息环境变量(不用 AutomaticEnv,避免隐式映射) bindEnvVars(v) var cfg Config if err := v.Unmarshal(&cfg); err != nil { return nil, fmt.Errorf("config: unmarshal: %w", err) } return &cfg, nil } // setDefaults 设置兜底默认值,与 config.yaml 保持一致。 func setDefaults(v *viper.Viper) { // app v.SetDefault("app.env", "dev") v.SetDefault("app.version", "dev") // server v.SetDefault("server.host", "0.0.0.0") v.SetDefault("server.port", 8080) v.SetDefault("server.read_timeout", 30) v.SetDefault("server.write_timeout", 30) v.SetDefault("server.shutdown_timeout", 10) v.SetDefault("server.heartbeat_interval", 30) v.SetDefault("server.heartbeat_timeout", 60) // session v.SetDefault("session.ttl", 30) v.SetDefault("session.max_history", 20) // ai — 默认值与 config.yaml 一致(mimo/dashscope) v.SetDefault("ai.stt.provider", "mimo") v.SetDefault("ai.stt.model", "mimo-v2.5-asr") v.SetDefault("ai.stt.endpoint", "https://api.xiaomimimo.com/v1") v.SetDefault("ai.stt.timeout", 5) v.SetDefault("ai.stt.http_client_timeout", 30) v.SetDefault("ai.llm.provider", "dashscope") v.SetDefault("ai.llm.model", "qwen3-vl-plus") v.SetDefault("ai.llm.endpoint", "https://dashscope.aliyuncs.com/compatible-mode/v1") v.SetDefault("ai.llm.timeout", 30) v.SetDefault("ai.llm.http_client_timeout", 60) v.SetDefault("ai.tts.provider", "mimo") v.SetDefault("ai.tts.model", "mimo-v2.5-tts") v.SetDefault("ai.tts.voice", "mimo_default") v.SetDefault("ai.tts.speed", 1.0) v.SetDefault("ai.tts.endpoint", "https://token-plan-cn.xiaomimimo.com/v1") v.SetDefault("ai.tts.timeout", 5) v.SetDefault("ai.tts.http_client_timeout", 30) v.SetDefault("ai.tts.output_format", "mp3") v.SetDefault("ai.tts.sample_rate", 24000) // storage v.SetDefault("storage.driver", "memory") v.SetDefault("storage.redis.enabled", false) v.SetDefault("storage.persistence.enabled", false) v.SetDefault("storage.persistence.driver", "postgres") // redis v.SetDefault("redis.addr", "localhost:6379") v.SetDefault("redis.password", "") v.SetDefault("redis.db", 0) // auth v.SetDefault("auth.access_ttl", 15) v.SetDefault("auth.refresh_ttl", 10080) // log v.SetDefault("log.level", "info") v.SetDefault("log.format", "console") } // bindEnvVars 显式绑定敏感信息环境变量。 // 只绑定不应出现在 config.yaml 中的敏感字段,非敏感配置通过 config.yaml 管理。 func bindEnvVars(v *viper.Viper) { // app.env 特殊处理:环境变量 APP_ENV 覆盖 config.yaml 中的 app.env v.BindEnv("app.env", "APP_ENV") // AI API Key v.BindEnv("ai.stt.api_key", "CAMTALK_AI_STT_API_KEY") v.BindEnv("ai.llm.api_key", "CAMTALK_AI_LLM_API_KEY") v.BindEnv("ai.tts.api_key", "CAMTALK_AI_TTS_API_KEY") // JWT v.BindEnv("auth.jwt_secret", "CAMTALK_AUTH_JWT_SECRET") // 数据库 v.BindEnv("storage.dsn", "CAMTALK_STORAGE_DSN") v.BindEnv("storage.persistence.dsn", "CAMTALK_STORAGE_DSN") v.BindEnv("storage.redis.enabled", "CAMTALK_STORAGE_REDIS_ENABLED") v.BindEnv("storage.persistence.enabled", "CAMTALK_STORAGE_PERSISTENCE_ENABLED") v.BindEnv("storage.persistence.driver", "CAMTALK_STORAGE_PERSISTENCE_DRIVER") // Redis(密码可能包含特殊字符,通过环境变量设置更安全) v.BindEnv("redis.addr", "CAMTALK_REDIS_ADDR") v.BindEnv("redis.password", "CAMTALK_REDIS_PASSWORD") }