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:
hhs
2026-06-19 18:41:35 +08:00
parent e6481e0faa
commit c0b4eeda46
12 changed files with 226 additions and 145 deletions

23
backend/.env.example Normal file
View File

@@ -0,0 +1,23 @@
# 运行环境
# dev / prod决定加载 config.dev.yaml 或 config.prod.yaml可选
APP_ENV=dev
# AI 服务 API Key
CAMTALK_AI_STT_API_KEY=sk-your-stt-key
CAMTALK_AI_LLM_API_KEY=sk-your-llm-key
CAMTALK_AI_TTS_API_KEY=sk-your-tts-key
# JWT 认证
CAMTALK_AUTH_JWT_SECRET=your-jwt-secret-here
# PostgreSQLstorage.driver 为 postgres 时必填)
POSTGRES_USER=camtalk
POSTGRES_PASSWORD=your-postgres-password
CAMTALK_STORAGE_DSN=postgres://camtalk:your-postgres-password@postgres:5432/camtalk?sslmode=disable
# 可选覆盖(默认值见 config.yaml
# CAMTALK_SERVER_PORT=8080
# CAMTALK_LOG_LEVEL=info
# CAMTALK_STORAGE_DRIVER=memory
# CAMTALK_REDIS_ADDR=localhost:6379
# CAMTALK_REDIS_PASSWORD=

1
backend/.gitignore vendored
View File

@@ -3,6 +3,7 @@
bin/
# 环境配置
.env
config.dev.yaml
config.prod.yaml

View File

@@ -21,9 +21,8 @@ RUN apk add --no-cache ca-certificates tzdata
WORKDIR /app
# 复制二进制配置
# 复制二进制配置通过 docker-compose env_file 注入)
COPY --from=builder /camtalk .
COPY config.yaml .
EXPOSE 8080

View File

@@ -32,8 +32,8 @@ var Version string
var startTime = time.Now()
func main() {
// 加载配置
cfg, err := config.Load()
// 加载配置(工作目录用于定位 .env 和 config.yaml
cfg, err := config.Load(".")
if err != nil {
panic("failed to load config: " + err.Error())
}

View File

@@ -1,42 +1,60 @@
# config.yaml — 默认配置
# CamTalk 后端配置
app:
env: dev
env: dev # dev / prod可通过 APP_ENV 环境变量覆盖
server:
host: "0.0.0.0"
port: 8080
read_timeout: 30
write_timeout: 30
read_timeout: 30 # 秒
write_timeout: 30 # 秒
shutdown_timeout: 10 # 优雅关闭超时(秒)
heartbeat_interval: 30 # 心跳检查间隔(秒)
heartbeat_timeout: 60 # 心跳超时断开(秒)
allowed_origins: [] # CORS 白名单,空=允许所有
session:
ttl: 30 # 会话过期时间(分钟)
max_history: 20 # 对话历史上限(条)
ai:
stt:
provider: mimo # mimo / deepgram
model: mimo-v2.5-asr
endpoint: "https://api.xiaomimimo.com/v1"
timeout: 5 # STT 请求超时(秒)
http_client_timeout: 30 # HTTP 客户端超时(秒)
llm:
provider: dashscope # dashscope / openai
model: qwen3-vl-plus
endpoint: "https://dashscope.aliyuncs.com/compatible-mode/v1"
timeout: 30 # LLM 请求超时(秒)
http_client_timeout: 60 # HTTP 客户端超时(秒)
tts:
provider: mimo # mimo / openai
model: mimo-v2.5-tts
voice: mimo_default
speed: 1.0
endpoint: "https://token-plan-cn.xiaomimimo.com/v1"
timeout: 5 # TTS 请求超时(秒)
http_client_timeout: 30 # HTTP 客户端超时(秒)
output_format: mp3 # 输出格式mp3 / wav
sample_rate: 24000 # 输出采样率
storage:
driver: memory # memory / redis / postgres
# dsn 通过环境变量 CAMTALK_STORAGE_DSN 设置
redis:
addr: "localhost:6379"
password: ""
db: 0
ai:
stt:
provider: mimo
model: mimo-v2.5-asr
endpoint: "https://api.xiaomimimo.com/v1"
api_key: "sk-c3jhv58rr5djhxw398w2rrij5tfpnpdgxqq1bojagshzviah"
llm:
provider: dashscope
model: qwen3-vl-plus
endpoint: "https://dashscope.aliyuncs.com/compatible-mode/v1"
api_key: "sk-ws-H.REHELLY.C4s3.MEUCIQCRee37XWEKp2szaxVLFDtR1rxNNsf372zMvCR0Xl6UvQIgZgvhRTvaa1FmhbCQJgaHu4Jny29AQkn01-3hX9CWBOg"
timeout: 30
tts:
provider: mimo
model: mimo-v2.5-tts
voice: mimo_default
speed: 1.0
endpoint: "https://token-plan-cn.xiaomimimo.com/v1"
api_key: "tp-c9e7scwfx94qvqyhpnahnw8uaiya01za2qzvg4xe24rp3xiv"
timeout: 5
storage:
driver: memory
auth:
# jwt_secret 通过环境变量 CAMTALK_AUTH_JWT_SECRET 设置
access_ttl: 15 # Access Token 过期时间(分钟)
refresh_ttl: 10080 # Refresh Token 过期时间分钟7 天
log:
level: info
format: console
level: info # debug / info / warn / error
format: console # console / json

View File

@@ -4,13 +4,16 @@ go 1.25.0
require (
github.com/gin-gonic/gin v1.10.0
github.com/golang-jwt/jwt/v5 v5.3.1
github.com/google/uuid v1.6.0
github.com/gorilla/websocket v1.5.3
github.com/jackc/pgx/v5 v5.10.0
github.com/joho/godotenv v1.5.1
github.com/redis/go-redis/v9 v9.20.1
github.com/spf13/viper v1.21.0
github.com/stretchr/testify v1.11.1
go.uber.org/zap v1.28.0
golang.org/x/crypto v0.23.0
)
require (
@@ -28,7 +31,6 @@ require (
github.com/go-playground/validator/v10 v10.20.0 // indirect
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
github.com/goccy/go-json v0.10.2 // indirect
github.com/golang-jwt/jwt/v5 v5.3.1 // indirect
github.com/jackc/pgpassfile v1.0.0 // indirect
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
github.com/jackc/puddle/v2 v2.2.2 // indirect
@@ -53,7 +55,6 @@ require (
go.uber.org/multierr v1.10.0 // indirect
go.yaml.in/yaml/v3 v3.0.4 // indirect
golang.org/x/arch v0.8.0 // indirect
golang.org/x/crypto v0.23.0 // indirect
golang.org/x/net v0.25.0 // indirect
golang.org/x/sync v0.17.0 // indirect
golang.org/x/sys v0.30.0 // indirect

View File

@@ -54,6 +54,8 @@ github.com/jackc/pgx/v5 v5.10.0 h1:VhSvgU2jSli8o3AqIEOTJr7rZwAEUVo4E4XhR94Zfr0=
github.com/jackc/pgx/v5 v5.10.0/go.mod h1:mal1tBGAFfLHvZzaYh77YS/eC6IX9OWbRV1QIIM0Jn4=
github.com/jackc/puddle/v2 v2.2.2 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
github.com/joho/godotenv v1.5.1 h1:7eLL/+HRGLY0ldzfGMeQkb7vMd0as4CfYvUVzLqw0N0=
github.com/joho/godotenv v1.5.1/go.mod h1:f4LDr5Voq0i2e/R5DDNOoa2zzDfwtkZa6DnEwAbqwq4=
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=

View File

@@ -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 过期时间(分钟),默认 100807天
}
// 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")
}