refactor: 统一配置文件系统并重构项目设计文档 #121
21
.env.example
21
.env.example
@@ -1,21 +0,0 @@
|
|||||||
# CamTalk 环境变量模板
|
|
||||||
# 复制为 .env 并填入实际值:cp .env.example .env
|
|
||||||
# .env 已在 .gitignore 中,不会提交到版本控制
|
|
||||||
|
|
||||||
# ---- AI 服务 API Key ----
|
|
||||||
CAMTALK_AI_LLM_API_KEY=sk-xxx
|
|
||||||
CAMTALK_AI_STT_API_KEY=
|
|
||||||
CAMTALK_AI_TTS_API_KEY=
|
|
||||||
|
|
||||||
# ---- 可选覆盖(默认值见 config.yaml)----
|
|
||||||
# CAMTALK_AI_LLM_MODEL=gpt-4o
|
|
||||||
# CAMTALK_AI_LLM_ENDPOINT=https://api.openai.com/v1
|
|
||||||
# CAMTALK_AI_LLM_TIMEOUT=10
|
|
||||||
# CAMTALK_AI_STT_ENDPOINT=https://api.xiaomimimo.com/v1
|
|
||||||
# CAMTALK_AI_TTS_ENDPOINT=https://api.openai.com/v1
|
|
||||||
# CAMTALK_AI_TTS_VOICE=alloy
|
|
||||||
# CAMTALK_AI_TTS_SPEED=1.0
|
|
||||||
# CAMTALK_AI_TTS_TIMEOUT=5
|
|
||||||
|
|
||||||
# ---- 应用 ----
|
|
||||||
# APP_ENV=dev
|
|
||||||
@@ -2,7 +2,7 @@ name: Deploy
|
|||||||
|
|
||||||
on:
|
on:
|
||||||
push:
|
push:
|
||||||
branches: [main]
|
branches: [v2]
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
deploy:
|
deploy:
|
||||||
|
|||||||
23
backend/.env.example
Normal file
23
backend/.env.example
Normal 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
|
||||||
|
|
||||||
|
# PostgreSQL(storage.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
1
backend/.gitignore
vendored
@@ -3,6 +3,7 @@
|
|||||||
bin/
|
bin/
|
||||||
|
|
||||||
# 环境配置
|
# 环境配置
|
||||||
|
.env
|
||||||
config.dev.yaml
|
config.dev.yaml
|
||||||
config.prod.yaml
|
config.prod.yaml
|
||||||
|
|
||||||
|
|||||||
@@ -21,9 +21,8 @@ RUN apk add --no-cache ca-certificates tzdata
|
|||||||
|
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
|
|
||||||
# 复制二进制和配置
|
# 复制二进制(配置通过 docker-compose env_file 注入)
|
||||||
COPY --from=builder /camtalk .
|
COPY --from=builder /camtalk .
|
||||||
COPY config.yaml .
|
|
||||||
|
|
||||||
EXPOSE 8080
|
EXPOSE 8080
|
||||||
|
|
||||||
|
|||||||
@@ -32,8 +32,8 @@ var Version string
|
|||||||
var startTime = time.Now()
|
var startTime = time.Now()
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
// 加载配置
|
// 加载配置(工作目录用于定位 .env 和 config.yaml)
|
||||||
cfg, err := config.Load()
|
cfg, err := config.Load(".")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
panic("failed to load config: " + err.Error())
|
panic("failed to load config: " + err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,42 +1,60 @@
|
|||||||
# config.yaml — 默认配置
|
# CamTalk 后端配置
|
||||||
|
|
||||||
app:
|
app:
|
||||||
env: dev
|
env: dev # dev / prod,可通过 APP_ENV 环境变量覆盖
|
||||||
|
|
||||||
server:
|
server:
|
||||||
host: "0.0.0.0"
|
host: "0.0.0.0"
|
||||||
port: 8080
|
port: 8080
|
||||||
read_timeout: 30
|
read_timeout: 30 # 秒
|
||||||
write_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:
|
redis:
|
||||||
addr: "localhost:6379"
|
addr: "localhost:6379"
|
||||||
password: ""
|
password: ""
|
||||||
db: 0
|
db: 0
|
||||||
|
|
||||||
ai:
|
auth:
|
||||||
stt:
|
# jwt_secret 通过环境变量 CAMTALK_AUTH_JWT_SECRET 设置
|
||||||
provider: mimo
|
access_ttl: 15 # Access Token 过期时间(分钟)
|
||||||
model: mimo-v2.5-asr
|
refresh_ttl: 10080 # Refresh Token 过期时间(分钟),7 天
|
||||||
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
|
|
||||||
|
|
||||||
log:
|
log:
|
||||||
level: info
|
level: info # debug / info / warn / error
|
||||||
format: console
|
format: console # console / json
|
||||||
|
|||||||
@@ -4,13 +4,16 @@ go 1.25.0
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/gin-gonic/gin v1.10.0
|
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/google/uuid v1.6.0
|
||||||
github.com/gorilla/websocket v1.5.3
|
github.com/gorilla/websocket v1.5.3
|
||||||
github.com/jackc/pgx/v5 v5.10.0
|
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/redis/go-redis/v9 v9.20.1
|
||||||
github.com/spf13/viper v1.21.0
|
github.com/spf13/viper v1.21.0
|
||||||
github.com/stretchr/testify v1.11.1
|
github.com/stretchr/testify v1.11.1
|
||||||
go.uber.org/zap v1.28.0
|
go.uber.org/zap v1.28.0
|
||||||
|
golang.org/x/crypto v0.23.0
|
||||||
)
|
)
|
||||||
|
|
||||||
require (
|
require (
|
||||||
@@ -28,7 +31,6 @@ require (
|
|||||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||||
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
github.com/go-viper/mapstructure/v2 v2.4.0 // indirect
|
||||||
github.com/goccy/go-json v0.10.2 // 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/pgpassfile v1.0.0 // indirect
|
||||||
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
github.com/jackc/pgservicefile v0.0.0-20240606120523-5a60cdf6a761 // indirect
|
||||||
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
github.com/jackc/puddle/v2 v2.2.2 // indirect
|
||||||
@@ -53,7 +55,6 @@ require (
|
|||||||
go.uber.org/multierr v1.10.0 // indirect
|
go.uber.org/multierr v1.10.0 // indirect
|
||||||
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
go.yaml.in/yaml/v3 v3.0.4 // indirect
|
||||||
golang.org/x/arch v0.8.0 // 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/net v0.25.0 // indirect
|
||||||
golang.org/x/sync v0.17.0 // indirect
|
golang.org/x/sync v0.17.0 // indirect
|
||||||
golang.org/x/sys v0.30.0 // indirect
|
golang.org/x/sys v0.30.0 // indirect
|
||||||
|
|||||||
@@ -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/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 h1:PR8nw+E/1w0GLuRFSmiioY6UooMp6KJv0/61nB7icHo=
|
||||||
github.com/jackc/puddle/v2 v2.2.2/go.mod h1:vriiEXHvEE654aYKXXjOvZM39qJ0q+azkZFrfEOc3H4=
|
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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
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=
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ package config
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
|
|
||||||
|
"github.com/joho/godotenv"
|
||||||
"github.com/spf13/viper"
|
"github.com/spf13/viper"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -107,90 +107,120 @@ type AuthConfig struct {
|
|||||||
RefreshTTL int `mapstructure:"refresh_ttl"` // Refresh Token 过期时间(分钟),默认 10080(7天)
|
RefreshTTL int `mapstructure:"refresh_ttl"` // Refresh Token 过期时间(分钟),默认 10080(7天)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Load 加载配置。优先级:环境变量 > config.{env}.yaml > config.yaml。
|
// Load 加载配置。优先级:环境变量 > config.{env}.yaml > config.yaml > 默认值。
|
||||||
func Load() (*Config, error) {
|
// 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 := viper.New()
|
||||||
v.SetConfigName("config")
|
v.SetConfigName("config")
|
||||||
v.SetConfigType("yaml")
|
v.SetConfigType("yaml")
|
||||||
v.AddConfigPath(".")
|
v.AddConfigPath(workDir)
|
||||||
v.AddConfigPath("./config")
|
|
||||||
v.AddConfigPath("./backend")
|
|
||||||
v.AddConfigPath("..") // 兼容从 backend/cmd/ 启动
|
|
||||||
v.AddConfigPath("../..") // 兼容从 backend/cmd/server/ 启动
|
|
||||||
|
|
||||||
// 默认值
|
// 2. 设置默认值(与 config.yaml 保持一致,仅作为兜底)
|
||||||
v.SetDefault("app.env", "dev")
|
setDefaults(v)
|
||||||
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)
|
|
||||||
|
|
||||||
// 读取基础配置文件
|
// 3. 读取 config.yaml
|
||||||
_ = v.ReadInConfig() // 文件不存在不报错
|
if err := v.ReadInConfig(); err != nil {
|
||||||
|
return nil, fmt.Errorf("config: read config.yaml: %w", err)
|
||||||
// 根据 APP_ENV 覆盖
|
|
||||||
env := os.Getenv("APP_ENV")
|
|
||||||
if env == "" {
|
|
||||||
env = v.GetString("app.env")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 4. 合并环境专属配置 config.{env}.yaml(可选)
|
||||||
|
env := v.GetString("app.env")
|
||||||
if env != "" {
|
if env != "" {
|
||||||
v.SetConfigName("config." + env)
|
v.SetConfigName("config." + env)
|
||||||
_ = v.MergeInConfig()
|
_ = v.MergeInConfig() // 文件不存在也不报错
|
||||||
}
|
}
|
||||||
|
|
||||||
// 环境变量覆盖
|
// 5. 显式绑定敏感信息环境变量(不用 AutomaticEnv,避免隐式映射)
|
||||||
v.SetEnvPrefix("CAMTALK")
|
bindEnvVars(v)
|
||||||
v.SetEnvKeyReplacer(strings.NewReplacer(".", "_"))
|
|
||||||
v.AutomaticEnv()
|
|
||||||
|
|
||||||
var cfg Config
|
var cfg Config
|
||||||
if err := v.Unmarshal(&cfg); err != nil {
|
if err := v.Unmarshal(&cfg); err != nil {
|
||||||
return nil, fmt.Errorf("config unmarshal: %w", err)
|
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 &cfg, nil
|
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")
|
||||||
|
}
|
||||||
|
|||||||
45
deploy.sh
45
deploy.sh
@@ -4,30 +4,57 @@ set -euo pipefail
|
|||||||
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
PROJECT_DIR="$(cd "$(dirname "$0")" && pwd)"
|
||||||
cd "$PROJECT_DIR"
|
cd "$PROJECT_DIR"
|
||||||
|
|
||||||
|
# .env 固定路径(独立于项目目录,保证持久性)
|
||||||
|
ENV_FILE="/opt/camtalk/.env"
|
||||||
|
|
||||||
# 颜色输出
|
# 颜色输出
|
||||||
GREEN='\033[0;32m'
|
GREEN='\033[0;32m'
|
||||||
NC='\033[0m'
|
NC='\033[0m'
|
||||||
|
|
||||||
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
info() { echo -e "${GREEN}[INFO]${NC} $*"; }
|
||||||
|
|
||||||
|
# .env 检查:首次部署时从 .env.example 复制模板,提示用户填写
|
||||||
|
check_env() {
|
||||||
|
local env_example="$PROJECT_DIR/backend/.env.example"
|
||||||
|
if [ ! -f "$ENV_FILE" ]; then
|
||||||
|
mkdir -p "$(dirname "$ENV_FILE")"
|
||||||
|
if [ -f "$env_example" ]; then
|
||||||
|
cp "$env_example" "$ENV_FILE"
|
||||||
|
info "未找到 $ENV_FILE,已从 .env.example 复制模板"
|
||||||
|
echo " 请编辑 $ENV_FILE 填入实际配置后重新运行本脚本"
|
||||||
|
exit 0
|
||||||
|
else
|
||||||
|
echo "错误: $ENV_FILE 和 .env.example 均不存在,请手动创建"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_env
|
||||||
|
|
||||||
|
# 所有 docker compose 命令统一使用 --env-file,用于解析 ${POSTGRES_USER} 等变量
|
||||||
|
DC="docker compose --env-file $ENV_FILE"
|
||||||
|
|
||||||
cmd_build() {
|
cmd_build() {
|
||||||
info "构建 Docker 镜像..."
|
info "构建 Docker 镜像..."
|
||||||
# 启用 BuildKit 加速构建
|
DOCKER_BUILDKIT=1 $DC build --parallel
|
||||||
DOCKER_BUILDKIT=1 docker compose build --parallel
|
|
||||||
info "构建完成"
|
info "构建完成"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_up() {
|
cmd_up() {
|
||||||
info "启动服务..."
|
info "启动服务..."
|
||||||
docker compose up -d
|
$DC up -d
|
||||||
info "服务已启动"
|
info "服务已启动"
|
||||||
info "前端: http://8.161.227.145:9000"
|
PUBLIC_IP=$(curl -s --connect-timeout 3 https://ifconfig.me 2>/dev/null || \
|
||||||
info "健康检查: http://8.161.227.145:9000/api/health"
|
curl -s --connect-timeout 3 https://api.ipify.org 2>/dev/null || \
|
||||||
|
echo "YOUR_SERVER_IP")
|
||||||
|
info "前端: http://$PUBLIC_IP:9000"
|
||||||
|
info "健康检查: http://$PUBLIC_IP:9000/api/health"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_down() {
|
cmd_down() {
|
||||||
info "停止服务..."
|
info "停止服务..."
|
||||||
docker compose down
|
$DC down
|
||||||
info "服务已停止"
|
info "服务已停止"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -38,11 +65,11 @@ cmd_restart() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cmd_logs() {
|
cmd_logs() {
|
||||||
docker compose logs -f "${@}"
|
$DC logs -f "${@}"
|
||||||
}
|
}
|
||||||
|
|
||||||
cmd_status() {
|
cmd_status() {
|
||||||
docker compose ps
|
$DC ps
|
||||||
}
|
}
|
||||||
|
|
||||||
usage() {
|
usage() {
|
||||||
@@ -58,6 +85,8 @@ CamTalk 部署脚本
|
|||||||
restart 重启服务
|
restart 重启服务
|
||||||
logs 查看日志(可加服务名,如: $0 logs backend)
|
logs 查看日志(可加服务名,如: $0 logs backend)
|
||||||
status 查看服务状态
|
status 查看服务状态
|
||||||
|
|
||||||
|
.env 路径: $ENV_FILE
|
||||||
EOF
|
EOF
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,11 +17,11 @@ services:
|
|||||||
context: ./backend
|
context: ./backend
|
||||||
dockerfile: Dockerfile
|
dockerfile: Dockerfile
|
||||||
container_name: camtalk-backend
|
container_name: camtalk-backend
|
||||||
|
env_file:
|
||||||
|
- /opt/camtalk/.env
|
||||||
environment:
|
environment:
|
||||||
- APP_ENV=production
|
- CAMTALK_STORAGE_DRIVER=${CAMTALK_STORAGE_DRIVER:-postgres}
|
||||||
- CAMTALK_STORAGE_DRIVER=postgres
|
- CAMTALK_STORAGE_DSN=postgres://${POSTGRES_USER}:${POSTGRES_PASSWORD}@postgres:5432/camtalk?sslmode=disable
|
||||||
- CAMTALK_STORAGE_DSN=postgres://camtalk:camtalk123@postgres:5432/camtalk?sslmode=disable
|
|
||||||
- CAMTALK_AUTH_JWT_SECRET=78uWBBAF8XEQEotKDlrnlnd4y8i4WN3E4zXmNmC8BYQ=
|
|
||||||
depends_on:
|
depends_on:
|
||||||
postgres:
|
postgres:
|
||||||
condition: service_healthy
|
condition: service_healthy
|
||||||
@@ -30,12 +30,11 @@ services:
|
|||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
|
|
||||||
postgres:
|
postgres:
|
||||||
# 轩辕镜像加速,避免 Docker Hub 拉取超时
|
|
||||||
image: docker.m.daocloud.io/library/postgres:15-alpine
|
image: docker.m.daocloud.io/library/postgres:15-alpine
|
||||||
container_name: camtalk-postgres
|
container_name: camtalk-postgres
|
||||||
environment:
|
environment:
|
||||||
POSTGRES_USER: camtalk
|
POSTGRES_USER: ${POSTGRES_USER}
|
||||||
POSTGRES_PASSWORD: camtalk123
|
POSTGRES_PASSWORD: ${POSTGRES_PASSWORD}
|
||||||
POSTGRES_DB: camtalk
|
POSTGRES_DB: camtalk
|
||||||
volumes:
|
volumes:
|
||||||
- pgdata:/var/lib/postgresql/data
|
- pgdata:/var/lib/postgresql/data
|
||||||
@@ -43,7 +42,7 @@ services:
|
|||||||
networks:
|
networks:
|
||||||
- camtalk-net
|
- camtalk-net
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: ["CMD-SHELL", "pg_isready -U camtalk -d camtalk"]
|
test: ["CMD-SHELL", "pg_isready -U ${POSTGRES_USER} -d camtalk"]
|
||||||
interval: 5s
|
interval: 5s
|
||||||
timeout: 3s
|
timeout: 3s
|
||||||
retries: 10
|
retries: 10
|
||||||
|
|||||||
Reference in New Issue
Block a user