From 3dc9632c1bb43fd35f6fb242b5b9c550d2bfd5ae Mon Sep 17 00:00:00 2001 From: sun <3371392206@qq.com> Date: Tue, 30 Jun 2026 20:55:56 +0800 Subject: [PATCH] =?UTF-8?q?refactor(setting):=20=E9=9B=86=E4=B8=AD?= =?UTF-8?q?=E7=AE=A1=E7=90=86=E7=8E=AF=E5=A2=83=E9=85=8D=E7=BD=AE=E5=B9=B6?= =?UTF-8?q?=E6=96=B0=E5=A2=9E=E5=90=AF=E5=8A=A8=E6=97=A5=E5=BF=97=E6=B1=87?= =?UTF-8?q?=E6=80=BB?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 1. 将所有环境变量读取逻辑迁移至setting包统一管理,移除各模块直接os.Getenv调用 2. 新增全局配置结构体统一管理服务端口、鉴权、跨域等配置 3. 添加启动期配置汇总日志,方便运维核对环境配置生效情况 4. 重构鉴权和CORS中间件,改为读取全局配置而非直接读取环境变量 5. 优化health端点的环境变量检查逻辑,使用已加载的全局配置而非重复读取环境 --- README.md | 15 +++++ main.go | 29 ++++----- middleware/auth.go | 25 +++----- middleware/cors.go | 58 +++++------------- setting/config.go | 148 +++++++++++++++++++++++++++++++++++++++++---- 5 files changed, 183 insertions(+), 92 deletions(-) diff --git a/README.md b/README.md index f3f6b88..d3f8b16 100644 --- a/README.md +++ b/README.md @@ -285,6 +285,21 @@ curl.exe -v -X POST "http://localhost:8080/v1/audio/speech" -H "Content-Type: ap ### 7. 查看日志 + +****启动期环境配置汇总**(每次启动打印一次,便于核对环境变量是否生效): + +``` +[TTS-Server] main.go:31: === 环境配置汇总 === +[TTS-Server] main.go:32: 服务端口: 8080 +[TTS-Server] main.go:35: OPENAI_TTS_API_KEY: 已设置 1 个有效密钥 +[TTS-Server] main.go:41: ALLOWED_ORIGINS: 已配置 1 个允许的跨域来源白名单 +[TTS-Server] main.go:46: 火山 TTS 配置: 初始化成功 (model=seed-tts-2.0-standard, format=mp3, sample_rate=24000, timeout=30s) +``` + +TTS 必填项缺失时会改为打印 `火山 TTS 配置: 初始化失败 - 缺少必需的环境变量: [BYTEDANCE_TTS_API_KEY] (TTS 功能不可用)`,服务继续运行但 `/v1/audio/speech` 路由会返回 500。 + +所有环境变量由 `setting` 包集中读取(其它包不允许直接 `os.Getenv`),`/health` 端点的 `ConfigStatus` 字段直接读全局 Config 状态,不存在重复读取。 + 服务启动后输出到 stdout/stderr。常见日志关键字: **中间件层拒绝**(有专门日志): diff --git a/main.go b/main.go index 7608604..e9163df 100644 --- a/main.go +++ b/main.go @@ -4,12 +4,10 @@ import ( "context" "log" "net/http" - "os" "os/signal" "syscall" "time" - "github.com/volcano-tts/tts-api/common" "github.com/volcano-tts/tts-api/controller" "github.com/volcano-tts/tts-api/middleware" "github.com/volcano-tts/tts-api/router" @@ -21,32 +19,27 @@ func main() { log.SetFlags(log.LstdFlags | log.Lshortfile) log.SetPrefix("[TTS-Server] ") - middleware.InitAPIKeys() - middleware.InitCORSConfig() + // 所有环境变量读取在 setting 包内集中完成,业务模块只读全局 Config。 + setting.InitAllConfigs() + + // 兼容旧调用顺序:rate limiter / 静态文件 / stats / controller 的初始化保持独立。 middleware.InitRateLimiter() setting.CheckStaticFiles() service.InitStats() controller.InitController() - setting.TTSConfigErr = setting.InitTTSConfig() + // 启动期一次性打印所有 Config 状态,便于运维核对。 + setting.LogStartupSummary() if setting.TTSConfigErr != nil { - log.Printf("警告:配置初始化失败: %v", setting.TTSConfigErr) - log.Printf("服务将继续运行,但TTS功能不可用,请检查环境变量配置\n") - } else { - log.Printf("配置初始化成功\n") + log.Printf("警告: 服务将继续运行,但 TTS 功能不可用,请检查环境变量配置") } controller.SetStartTime(time.Now()) r := router.Setup() - port := os.Getenv("PORT") - if port == "" { - port = common.DefaultPort - } - server := &http.Server{ - Addr: ":" + port, + Addr: ":" + setting.Server.Port, Handler: middleware.CORS(r), ReadTimeout: 30 * time.Second, WriteTimeout: 120 * time.Second, @@ -58,9 +51,9 @@ func main() { go func() { log.Printf("Starting ByteDance TTS to OpenAI API Adapter Server") - log.Printf("Listening on port: %s", port) - log.Printf("OpenAI TTS endpoint: http://localhost:%s/v1/audio/speech", port) - log.Printf("Health check: http://localhost:%s/health", port) + log.Printf("Listening on port: %s", setting.Server.Port) + log.Printf("OpenAI TTS endpoint: http://localhost:%s/v1/audio/speech", setting.Server.Port) + log.Printf("Health check: http://localhost:%s/health", setting.Server.Port) log.Printf("Using ByteDance v3 API") if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed { diff --git a/middleware/auth.go b/middleware/auth.go index fb86b9b..95f6a37 100644 --- a/middleware/auth.go +++ b/middleware/auth.go @@ -3,30 +3,21 @@ package middleware import ( "crypto/subtle" "encoding/json" - "log" "net/http" - "os" "strings" + + "github.com/volcano-tts/tts-api/setting" ) -var validAPIKeys []string - +// InitAPIKeys 已在 setting.InitAuthConfig 中完成,这里保留为 no-op 以维持现有调用顺序。 +// 实际鉴权逻辑直接读 setting.Auth.APIKeys。 func InitAPIKeys() { - apiKey := os.Getenv("OPENAI_TTS_API_KEY") - if apiKey != "" { - validAPIKeys = strings.Split(apiKey, ",") - for i, k := range validAPIKeys { - validAPIKeys[i] = strings.TrimSpace(k) - } - log.Printf("已配置 %d 个有效的 API 密钥", len(validAPIKeys)) - } else { - log.Println("警告: OPENAI_TTS_API_KEY环境变量未设置,所有请求将无需认证即可访问") - log.Println("如需启用API密钥验证,请设置 OPENAI_TTS_API_KEY 环境变量(多个密钥用逗号分隔)") - } + // 配置由 setting 包统一加载,日志也由 setting.LogStartupSummary 输出。 + _ = setting.Auth } func ValidateAPIKey(r *http.Request) bool { - if len(validAPIKeys) == 0 { + if len(setting.Auth.APIKeys) == 0 { return true } @@ -40,7 +31,7 @@ func ValidateAPIKey(r *http.Request) bool { } token := strings.TrimPrefix(authHeader, "Bearer ") - for _, validKey := range validAPIKeys { + for _, validKey := range setting.Auth.APIKeys { if subtle.ConstantTimeCompare([]byte(token), []byte(validKey)) == 1 { return true } diff --git a/middleware/cors.go b/middleware/cors.go index 3f3850d..746e94e 100644 --- a/middleware/cors.go +++ b/middleware/cors.go @@ -3,50 +3,20 @@ package middleware import ( "log" "net/http" - "os" "strings" + + "github.com/volcano-tts/tts-api/setting" ) var ( - allowedOrigins []string - allowAllOrigins bool corsMaxAgeHeader = "86400" ) -func normalizeOrigin(origin string) string { - origin = strings.TrimSpace(origin) - origin = strings.TrimRight(origin, "/") - return strings.ToLower(origin) -} - +// InitCORSConfig 已在 setting.InitCORSConfig 中完成,这里保留为 no-op 以维持现有调用顺序。 +// 实际 CORS 匹配逻辑直接读 setting.CORS.Origins / setting.CORS.AllowAll。 func InitCORSConfig() { - origins := os.Getenv("ALLOWED_ORIGINS") - if origins == "" { - log.Println("警告: ALLOWED_ORIGINS 环境变量未设置") - log.Println("出于安全考虑,跨域请求将被拒绝。如需开放跨域请配置 ALLOWED_ORIGINS") - log.Println("开发环境可设置 ALLOWED_ORIGINS=* 允许所有来源(不可与凭据共用)") - return - } - - parts := strings.Split(origins, ",") - for _, p := range parts { - o := strings.TrimSpace(p) - if o == "" { - continue - } - if o == "*" { - allowAllOrigins = true - continue - } - allowedOrigins = append(allowedOrigins, normalizeOrigin(o)) - } - - if allowAllOrigins { - log.Println("警告: ALLOWED_ORIGINS=*,将允许所有来源跨域请求(不携带凭据)") - } - if len(allowedOrigins) > 0 { - log.Printf("已配置 %d 个允许的跨域来源白名单", len(allowedOrigins)) - } + // 配置由 setting 包统一加载,日志也由 setting.LogStartupSummary 输出。 + _ = setting.CORS } func isValidOrigin(origin string) bool { @@ -64,11 +34,11 @@ func matchOrigin(origin string) (string, bool) { if !isValidOrigin(origin) { return "", false } - if allowAllOrigins { + if setting.CORS.AllowAll { return "*", true } - normalized := normalizeOrigin(origin) - for _, allowed := range allowedOrigins { + normalized := strings.ToLower(strings.TrimRight(strings.TrimSpace(origin), "/")) + for _, allowed := range setting.CORS.Origins { if allowed == normalized { return origin, true } @@ -80,13 +50,13 @@ func CORS(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { origin := r.Header.Get("Origin") - // 无 Origin 头:非跨域请求,跳过 CORS 处理 + // 无 Origin 头:非跨域请求,跳过 CORS 处理 if origin == "" { next.ServeHTTP(w, r) return } - // 有 Origin 头时,响应必须携带 Vary: Origin 防止 CDN 缓存污染 + // 有 Origin 头时,响应必须携带 Vary: Origin 防止 CDN 缓存污染 vary := w.Header().Get("Vary") if vary == "" { w.Header().Set("Vary", "Origin") @@ -98,7 +68,7 @@ func CORS(next http.Handler) http.Handler { allowOrigin, matched := matchOrigin(origin) if !matched { - // Origin 不在白名单:拒绝请求(预检和非预检均拒绝), + // Origin 不在白名单:拒绝请求(预检和非预检均拒绝), // 防止不匹配的请求穿透到后端浪费 TTS 资源 log.Printf("CORS拦截: 来源=%q 路径=%s 方法=%s 客户端=%s", origin, r.URL.Path, r.Method, GetClientIP(r)) @@ -106,7 +76,7 @@ func CORS(next http.Handler) http.Handler { return } - // Origin 匹配:设置 CORS 响应头 + // Origin 匹配:设置 CORS 响应头 w.Header().Set("Access-Control-Allow-Origin", allowOrigin) w.Header().Set("Access-Control-Allow-Methods", "GET, POST, OPTIONS") w.Header().Set("Access-Control-Allow-Headers", "Content-Type, Authorization") @@ -116,7 +86,7 @@ func CORS(next http.Handler) http.Handler { w.Header().Set("Access-Control-Allow-Credentials", "true") } - // 预检请求:直接返回 204,不进入内层中间件链, + // 预检请求:直接返回 204,不进入内层中间件链, // 避免消耗速率限制配额和并发槽位 if isPreflight { w.WriteHeader(http.StatusNoContent) diff --git a/setting/config.go b/setting/config.go index 9535fa4..189e388 100644 --- a/setting/config.go +++ b/setting/config.go @@ -4,17 +4,111 @@ import ( "fmt" "log" "os" + "strings" "time" "github.com/volcano-tts/tts-api/common" "github.com/volcano-tts/tts-api/dto" ) +// 全部环境变量读取的单一入口:其它包不允许直接 os.Getenv,只读这里的全局 Config。 + +// TTSConfig 上游火山 TTS 配置(由 InitTTSConfig 填充)。 var ( TTSConfig dto.ByteDanceTTSConfig TTSConfigErr error ) +// AuthConfig OpenAI 兼容接口的客户端 API Key 鉴权配置。 +type AuthConfig struct { + APIKeys []string +} + +var Auth AuthConfig + +// CORSConfig 跨域白名单配置。 +type CORSConfig struct { + Origins []string + AllowAll bool +} + +var CORS CORSConfig + +// ServerConfig HTTP 服务监听配置。 +type ServerConfig struct { + Port string +} + +var Server ServerConfig + +// InitAllConfigs 集中初始化所有配置,启动期调用一次。 +// 返回 TTSConfigErr(火山 TTS 必填项缺失时为非 nil);其它 Config 缺失时不返回 error, +// 各自有合理兜底(Auth 放行 / CORS 拒绝跨域 / Server 默认 8080)。 +func InitAllConfigs() { + InitServerConfig() + InitAuthConfig() + InitCORSConfig() + TTSConfigErr = InitTTSConfig() +} + +// InitServerConfig 读取 PORT,缺省 common.DefaultPort。 +func InitServerConfig() { + Server.Port = os.Getenv("PORT") + if Server.Port == "" { + Server.Port = common.DefaultPort + } +} + +// InitAuthConfig 读取 OPENAI_TTS_API_KEY,支持逗号分隔多个 key。 +// 留空时 Auth.APIKeys 为空,ValidateAPIKey 会放行所有请求。 +func InitAuthConfig() { + raw := os.Getenv("OPENAI_TTS_API_KEY") + if raw == "" { + Auth.APIKeys = nil + return + } + parts := strings.Split(raw, ",") + keys := make([]string, 0, len(parts)) + for _, p := range parts { + k := strings.TrimSpace(p) + if k != "" { + keys = append(keys, k) + } + } + Auth.APIKeys = keys +} + +// InitCORSConfig 读取 ALLOWED_ORIGINS,按逗号分隔;支持 * 通配(AllowAll=true)。 +// 留空时 CORS.Origins 为空,跨域请求会被拒绝。 +func InitCORSConfig() { + raw := os.Getenv("ALLOWED_ORIGINS") + CORS.Origins = nil + CORS.AllowAll = false + if raw == "" { + return + } + for _, p := range strings.Split(raw, ",") { + o := strings.TrimSpace(p) + if o == "" { + continue + } + if o == "*" { + CORS.AllowAll = true + continue + } + CORS.Origins = append(CORS.Origins, normalizeOrigin(o)) + } +} + +// normalizeOrigin 复制自原 middleware/cors.go:小写 + 去尾斜杠。 +func normalizeOrigin(origin string) string { + origin = strings.TrimSpace(origin) + origin = strings.TrimRight(origin, "/") + return strings.ToLower(origin) +} + +// InitTTSConfig 读取火山 TTS 必填和可选配置,填充 TTSConfig。 +// 必填项缺失时返回 error,服务可继续运行但 TTS 功能不可用。 func InitTTSConfig() error { apiKey := os.Getenv("BYTEDANCE_TTS_API_KEY") resourceId := os.Getenv("BYTEDANCE_TTS_RESOURCE_ID") @@ -50,13 +144,13 @@ func InitTTSConfig() error { } } - // 音频格式,默认 mp3(文档默认值,流式场景下 wav 会多次返回 header,不推荐) + // 音频格式,默认 mp3(文档默认值,流式场景中 wav 会多次返回 header,不推荐) format := os.Getenv("BYTEDANCE_TTS_FORMAT") if format == "" { format = "mp3" } - // 采样率,默认 24000 + // 采样率,默认 24000 sampleRate := 24000 if srStr := os.Getenv("BYTEDANCE_TTS_SAMPLE_RATE"); srStr != "" { if sr, err := fmt.Sscanf(srStr, "%d", &sampleRate); err != nil || sr != 1 { @@ -83,11 +177,38 @@ func InitTTSConfig() error { return nil } +// LogStartupSummary 在启动期打印所有 Config 的最终状态。 +// 调用时机:InitAllConfigs 之后,ListenAndServe 之前。 +func LogStartupSummary() { + log.Printf("=== 环境配置汇总 ===") + log.Printf("服务端口: %s", Server.Port) + if len(Auth.APIKeys) == 0 { + log.Printf("OPENAI_TTS_API_KEY: 未设置(所有请求无需鉴权)") + } else { + log.Printf("OPENAI_TTS_API_KEY: 已设置 %d 个有效密钥", len(Auth.APIKeys)) + } + if CORS.AllowAll { + log.Printf("ALLOWED_ORIGINS: *(允许所有跨域,不可与凭据共用)") + } else if len(CORS.Origins) == 0 { + log.Printf("ALLOWED_ORIGINS: 未设置(跨域请求将被拒绝)") + } else { + log.Printf("ALLOWED_ORIGINS: 已配置 %d 个允许的跨域来源白名单", len(CORS.Origins)) + } + if TTSConfigErr != nil { + log.Printf("火山 TTS 配置: 初始化失败 - %v (TTS 功能不可用)", TTSConfigErr) + } else { + log.Printf("火山 TTS 配置: 初始化成功 (model=%s, format=%s, sample_rate=%d, timeout=%v)", + TTSConfig.Model, TTSConfig.Format, TTSConfig.SampleRate, TTSConfig.Timeout) + } +} + +// CheckEnvironmentVariables 返回环境变量状态,供 /health 端点使用。 +// 不再直接 os.Getenv,改为读已初始化的全局 Config(单一数据源)。 func CheckEnvironmentVariables() map[string]interface{} { requiredVars := map[string]bool{ - "BYTEDANCE_TTS_API_KEY": os.Getenv("BYTEDANCE_TTS_API_KEY") != "", - "BYTEDANCE_TTS_RESOURCE_ID": os.Getenv("BYTEDANCE_TTS_RESOURCE_ID") != "", - "BYTEDANCE_TTS_SPEAKER": os.Getenv("BYTEDANCE_TTS_SPEAKER") != "", + "BYTEDANCE_TTS_API_KEY": TTSConfig.ApiKey != "", + "BYTEDANCE_TTS_RESOURCE_ID": TTSConfig.ResourceId != "", + "BYTEDANCE_TTS_SPEAKER": TTSConfig.Speaker != "", } missingVars := []string{} @@ -98,13 +219,13 @@ func CheckEnvironmentVariables() map[string]interface{} { } optionalVars := map[string]bool{ - "BYTEDANCE_TTS_TIMEOUT": os.Getenv("BYTEDANCE_TTS_TIMEOUT") != "", - "BYTEDANCE_TTS_MODEL": os.Getenv("BYTEDANCE_TTS_MODEL") != "", - "BYTEDANCE_TTS_FORMAT": os.Getenv("BYTEDANCE_TTS_FORMAT") != "", - "BYTEDANCE_TTS_SAMPLE_RATE": os.Getenv("BYTEDANCE_TTS_SAMPLE_RATE") != "", - "OPENAI_TTS_API_KEY": os.Getenv("OPENAI_TTS_API_KEY") != "", - "ALLOWED_ORIGINS": os.Getenv("ALLOWED_ORIGINS") != "", - "PORT": os.Getenv("PORT") != "", + + "BYTEDANCE_TTS_MODEL": TTSConfig.Model != "" && TTSConfig.Model != "seed-tts-2.0-standard", + "BYTEDANCE_TTS_FORMAT": TTSConfig.Format != "" && TTSConfig.Format != "mp3", + "BYTEDANCE_TTS_SAMPLE_RATE": TTSConfig.SampleRate != 24000, + "OPENAI_TTS_API_KEY": len(Auth.APIKeys) > 0, + "ALLOWED_ORIGINS": CORS.AllowAll || len(CORS.Origins) > 0, + "PORT": Server.Port != common.DefaultPort, } return map[string]interface{}{ @@ -115,8 +236,9 @@ func CheckEnvironmentVariables() map[string]interface{} { } } +// CheckStaticFiles 静态文件存在性检查,/dashboard 路由需要 health.html。 func CheckStaticFiles() { if _, err := os.Stat("health.html"); os.IsNotExist(err) { - log.Println("警告: health.html 不存在,/dashboard 路由将返回 404") + log.Println("警告: health.html 不存在,/dashboard 路由将返回 404") } }