refactor: 统一配置文件系统
- 补全 backend/config.yaml 所有非敏感配置项并添加中文注释
- 重写 config.go:Load(workDir) 显式传参,BindEnv 绑定敏感字段,删除 AutomaticEnv
- setDefaults 默认值与 config.yaml 保持一致(mimo/dashscope)
- backend/.env.example 重写为纯敏感信息模板
- .env 固定在 /opt/camtalk/.env,docker-compose 通过绝对路径加载
- deploy.sh 统一使用 --env-file,移除硬编码 IP
- deploy.yml 删除 CI 写入 .env 的步骤
- Dockerfile 移除 COPY config.yaml
- 修复 deploy.yml 中 POSTGRES_PASSWORD 的 &{{ 拼写错误
This commit is contained in:
@@ -2,9 +2,9 @@ package config
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"strings"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/joho/godotenv"
|
||||
"github.com/spf13/viper"
|
||||
)
|
||||
|
||||
@@ -107,90 +107,120 @@ type AuthConfig struct {
|
||||
RefreshTTL int `mapstructure:"refresh_ttl"` // Refresh Token 过期时间(分钟),默认 10080(7天)
|
||||
}
|
||||
|
||||
// Load 加载配置。优先级:环境变量 > config.{env}.yaml > config.yaml。
|
||||
func Load() (*Config, error) {
|
||||
// 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(".")
|
||||
v.AddConfigPath("./config")
|
||||
v.AddConfigPath("./backend")
|
||||
v.AddConfigPath("..") // 兼容从 backend/cmd/ 启动
|
||||
v.AddConfigPath("../..") // 兼容从 backend/cmd/server/ 启动
|
||||
v.AddConfigPath(workDir)
|
||||
|
||||
// 默认值
|
||||
v.SetDefault("app.env", "dev")
|
||||
v.SetDefault("app.version", "dev")
|
||||
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.heartbeat_interval", 30)
|
||||
v.SetDefault("server.heartbeat_timeout", 60)
|
||||
v.SetDefault("server.shutdown_timeout", 10)
|
||||
v.SetDefault("session.ttl", 30)
|
||||
v.SetDefault("session.max_history", 20)
|
||||
v.SetDefault("redis.addr", "localhost:6379")
|
||||
v.SetDefault("redis.db", 0)
|
||||
v.SetDefault("ai.stt.provider", "deepgram")
|
||||
v.SetDefault("ai.stt.model", "nova-2")
|
||||
v.SetDefault("ai.stt.endpoint", "wss://api.deepgram.com/v1/listen")
|
||||
v.SetDefault("ai.stt.timeout", 5)
|
||||
v.SetDefault("ai.stt.http_client_timeout", 30)
|
||||
v.SetDefault("ai.llm.provider", "openai")
|
||||
v.SetDefault("ai.llm.model", "gpt-4o")
|
||||
v.SetDefault("ai.llm.endpoint", "https://api.openai.com/v1")
|
||||
v.SetDefault("ai.llm.timeout", 10)
|
||||
v.SetDefault("ai.llm.http_client_timeout", 60)
|
||||
v.SetDefault("ai.tts.provider", "openai")
|
||||
v.SetDefault("ai.tts.model", "tts-1")
|
||||
v.SetDefault("ai.tts.voice", "mimo_default")
|
||||
v.SetDefault("ai.tts.speed", 1.0)
|
||||
v.SetDefault("ai.tts.endpoint", "https://api.openai.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)
|
||||
v.SetDefault("storage.driver", "memory")
|
||||
v.SetDefault("storage.dsn", "")
|
||||
v.SetDefault("log.level", "info")
|
||||
v.SetDefault("log.format", "console")
|
||||
v.SetDefault("auth.access_ttl", 15)
|
||||
v.SetDefault("auth.refresh_ttl", 10080)
|
||||
// 2. 设置默认值(与 config.yaml 保持一致,仅作为兜底)
|
||||
setDefaults(v)
|
||||
|
||||
// 读取基础配置文件
|
||||
_ = v.ReadInConfig() // 文件不存在不报错
|
||||
|
||||
// 根据 APP_ENV 覆盖
|
||||
env := os.Getenv("APP_ENV")
|
||||
if env == "" {
|
||||
env = v.GetString("app.env")
|
||||
// 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()
|
||||
_ = v.MergeInConfig() // 文件不存在也不报错
|
||||
}
|
||||
|
||||
// 环境变量覆盖
|
||||
v.SetEnvPrefix("CAMTALK")
|
||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
||||
v.AutomaticEnv()
|
||||
// 5. 显式绑定敏感信息环境变量(不用 AutomaticEnv,避免隐式映射)
|
||||
bindEnvVars(v)
|
||||
|
||||
var cfg Config
|
||||
if err := v.Unmarshal(&cfg); err != nil {
|
||||
return nil, fmt.Errorf("config unmarshal: %w", err)
|
||||
}
|
||||
|
||||
// 填充默认值
|
||||
if cfg.Server.Host == "" {
|
||||
cfg.Server.Host = "0.0.0.0"
|
||||
}
|
||||
if cfg.Server.Port == 0 {
|
||||
cfg.Server.Port = 8080
|
||||
}
|
||||
if cfg.App.Env == "" {
|
||||
cfg.App.Env = "dev"
|
||||
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")
|
||||
|
||||
// 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")
|
||||
|
||||
// Redis(密码可能包含特殊字符,通过环境变量设置更安全)
|
||||
v.BindEnv("redis.password", "CAMTALK_REDIS_PASSWORD")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user