2026-06-13 15:17:37 +08:00
|
|
|
|
package config
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"fmt"
|
2026-06-19 18:41:35 +08:00
|
|
|
|
"path/filepath"
|
2026-06-13 15:17:37 +08:00
|
|
|
|
|
2026-06-19 18:41:35 +08:00
|
|
|
|
"github.com/joho/godotenv"
|
2026-06-13 15:17:37 +08:00
|
|
|
|
"github.com/spf13/viper"
|
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
|
|
// Config 应用配置。
|
|
|
|
|
|
type Config struct {
|
2026-06-14 11:55:06 +08:00
|
|
|
|
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"`
|
2026-06-14 16:40:06 +08:00
|
|
|
|
Auth AuthConfig `mapstructure:"auth"`
|
2026-06-14 11:55:06 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// SessionConfig 会话管理配置。
|
|
|
|
|
|
type SessionConfig struct {
|
|
|
|
|
|
TTL int `mapstructure:"ttl"` // 会话过期时间(分钟)
|
|
|
|
|
|
MaxHistory int `mapstructure:"max_history"` // 对话历史上限(条)
|
2026-06-13 15:17:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type AppConfig struct {
|
|
|
|
|
|
Env string `mapstructure:"env"`
|
|
|
|
|
|
Version string `mapstructure:"version"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type ServerConfig struct {
|
2026-06-14 11:55:06 +08:00
|
|
|
|
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 允许的来源,空表示允许所有
|
2026-06-13 15:17:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 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 {
|
2026-06-14 11:55:06 +08:00
|
|
|
|
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 客户端超时(秒)
|
2026-06-13 15:17:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type LLMConfig struct {
|
2026-06-14 11:55:06 +08:00
|
|
|
|
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 客户端超时(秒)
|
2026-06-13 15:17:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type TTSConfig struct {
|
2026-06-14 11:55:06 +08:00
|
|
|
|
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"` // 输出采样率
|
2026-06-13 15:17:37 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type StorageConfig struct {
|
2026-06-19 22:53:06 +08:00
|
|
|
|
Redis RedisStorageConfig `mapstructure:"redis"`
|
|
|
|
|
|
Persistence PersistenceConfig `mapstructure:"persistence"`
|
|
|
|
|
|
// Deprecated: 使用 Redis 和 Persistence 替代
|
2026-06-13 15:17:37 +08:00
|
|
|
|
Driver string `mapstructure:"driver"`
|
|
|
|
|
|
DSN string `mapstructure:"dsn"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 22:53:06 +08:00
|
|
|
|
type RedisStorageConfig struct {
|
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
type PersistenceConfig struct {
|
|
|
|
|
|
Enabled bool `mapstructure:"enabled"`
|
|
|
|
|
|
Driver string `mapstructure:"driver"`
|
|
|
|
|
|
DSN string `mapstructure:"dsn"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-13 15:17:37 +08:00
|
|
|
|
type LogConfig struct {
|
|
|
|
|
|
Level string `mapstructure:"level"`
|
|
|
|
|
|
Format string `mapstructure:"format"`
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-14 16:40:06 +08:00
|
|
|
|
// 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天)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2026-06-19 18:41:35 +08:00
|
|
|
|
// 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) // 文件不存在也不报错
|
|
|
|
|
|
|
2026-06-13 15:17:37 +08:00
|
|
|
|
v := viper.New()
|
|
|
|
|
|
v.SetConfigName("config")
|
|
|
|
|
|
v.SetConfigType("yaml")
|
2026-06-19 18:41:35 +08:00
|
|
|
|
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)
|
2026-06-13 15:17:37 +08:00
|
|
|
|
|
2026-06-19 18:41:35 +08:00
|
|
|
|
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
|
2026-06-13 15:17:37 +08:00
|
|
|
|
v.SetDefault("app.env", "dev")
|
2026-06-14 11:55:06 +08:00
|
|
|
|
v.SetDefault("app.version", "dev")
|
2026-06-19 18:41:35 +08:00
|
|
|
|
|
|
|
|
|
|
// server
|
2026-06-13 15:17:37 +08:00
|
|
|
|
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)
|
2026-06-19 18:41:35 +08:00
|
|
|
|
v.SetDefault("server.shutdown_timeout", 10)
|
2026-06-14 11:55:06 +08:00
|
|
|
|
v.SetDefault("server.heartbeat_interval", 30)
|
|
|
|
|
|
v.SetDefault("server.heartbeat_timeout", 60)
|
2026-06-19 18:41:35 +08:00
|
|
|
|
|
|
|
|
|
|
// session
|
2026-06-14 11:55:06 +08:00
|
|
|
|
v.SetDefault("session.ttl", 30)
|
|
|
|
|
|
v.SetDefault("session.max_history", 20)
|
2026-06-19 18:41:35 +08:00
|
|
|
|
|
|
|
|
|
|
// 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")
|
2026-06-14 11:55:06 +08:00
|
|
|
|
v.SetDefault("ai.stt.timeout", 5)
|
|
|
|
|
|
v.SetDefault("ai.stt.http_client_timeout", 30)
|
2026-06-19 18:41:35 +08:00
|
|
|
|
|
|
|
|
|
|
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)
|
2026-06-14 11:55:06 +08:00
|
|
|
|
v.SetDefault("ai.llm.http_client_timeout", 60)
|
2026-06-19 18:41:35 +08:00
|
|
|
|
|
|
|
|
|
|
v.SetDefault("ai.tts.provider", "mimo")
|
|
|
|
|
|
v.SetDefault("ai.tts.model", "mimo-v2.5-tts")
|
2026-06-14 11:12:49 +08:00
|
|
|
|
v.SetDefault("ai.tts.voice", "mimo_default")
|
2026-06-13 15:17:37 +08:00
|
|
|
|
v.SetDefault("ai.tts.speed", 1.0)
|
2026-06-19 18:41:35 +08:00
|
|
|
|
v.SetDefault("ai.tts.endpoint", "https://token-plan-cn.xiaomimimo.com/v1")
|
2026-06-13 15:17:37 +08:00
|
|
|
|
v.SetDefault("ai.tts.timeout", 5)
|
2026-06-14 11:55:06 +08:00
|
|
|
|
v.SetDefault("ai.tts.http_client_timeout", 30)
|
|
|
|
|
|
v.SetDefault("ai.tts.output_format", "mp3")
|
|
|
|
|
|
v.SetDefault("ai.tts.sample_rate", 24000)
|
2026-06-19 18:41:35 +08:00
|
|
|
|
|
|
|
|
|
|
// storage
|
2026-06-13 15:17:37 +08:00
|
|
|
|
v.SetDefault("storage.driver", "memory")
|
2026-06-19 22:53:06 +08:00
|
|
|
|
v.SetDefault("storage.redis.enabled", false)
|
|
|
|
|
|
v.SetDefault("storage.persistence.enabled", false)
|
|
|
|
|
|
v.SetDefault("storage.persistence.driver", "postgres")
|
2026-06-19 18:41:35 +08:00
|
|
|
|
|
|
|
|
|
|
// redis
|
|
|
|
|
|
v.SetDefault("redis.addr", "localhost:6379")
|
|
|
|
|
|
v.SetDefault("redis.password", "")
|
|
|
|
|
|
v.SetDefault("redis.db", 0)
|
|
|
|
|
|
|
|
|
|
|
|
// auth
|
2026-06-14 16:40:06 +08:00
|
|
|
|
v.SetDefault("auth.access_ttl", 15)
|
|
|
|
|
|
v.SetDefault("auth.refresh_ttl", 10080)
|
2026-06-13 15:17:37 +08:00
|
|
|
|
|
2026-06-19 18:41:35 +08:00
|
|
|
|
// log
|
|
|
|
|
|
v.SetDefault("log.level", "info")
|
|
|
|
|
|
v.SetDefault("log.format", "console")
|
|
|
|
|
|
}
|
2026-06-13 15:17:37 +08:00
|
|
|
|
|
2026-06-19 18:41:35 +08:00
|
|
|
|
// bindEnvVars 显式绑定敏感信息环境变量。
|
|
|
|
|
|
// 只绑定不应出现在 config.yaml 中的敏感字段,非敏感配置通过 config.yaml 管理。
|
|
|
|
|
|
func bindEnvVars(v *viper.Viper) {
|
|
|
|
|
|
// app.env 特殊处理:环境变量 APP_ENV 覆盖 config.yaml 中的 app.env
|
|
|
|
|
|
v.BindEnv("app.env", "APP_ENV")
|
2026-06-13 15:17:37 +08:00
|
|
|
|
|
2026-06-19 18:41:35 +08:00
|
|
|
|
// 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")
|
2026-06-13 15:17:37 +08:00
|
|
|
|
|
2026-06-19 18:41:35 +08:00
|
|
|
|
// JWT
|
|
|
|
|
|
v.BindEnv("auth.jwt_secret", "CAMTALK_AUTH_JWT_SECRET")
|
2026-06-13 15:17:37 +08:00
|
|
|
|
|
2026-06-19 18:41:35 +08:00
|
|
|
|
// 数据库
|
|
|
|
|
|
v.BindEnv("storage.dsn", "CAMTALK_STORAGE_DSN")
|
2026-06-19 22:53:06 +08:00
|
|
|
|
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")
|
2026-06-13 15:17:37 +08:00
|
|
|
|
|
2026-06-19 18:41:35 +08:00
|
|
|
|
// Redis(密码可能包含特殊字符,通过环境变量设置更安全)
|
2026-06-19 22:53:06 +08:00
|
|
|
|
v.BindEnv("redis.addr", "CAMTALK_REDIS_ADDR")
|
2026-06-19 18:41:35 +08:00
|
|
|
|
v.BindEnv("redis.password", "CAMTALK_REDIS_PASSWORD")
|
2026-06-13 15:17:37 +08:00
|
|
|
|
}
|