refactor(setting): 集中管理环境配置并新增启动日志汇总

1.  将所有环境变量读取逻辑迁移至setting包统一管理,移除各模块直接os.Getenv调用
2.  新增全局配置结构体统一管理服务端口、鉴权、跨域等配置
3.  添加启动期配置汇总日志,方便运维核对环境配置生效情况
4.  重构鉴权和CORS中间件,改为读取全局配置而非直接读取环境变量
5.  优化health端点的环境变量检查逻辑,使用已加载的全局配置而非重复读取环境
This commit is contained in:
sun
2026-06-30 20:55:56 +08:00
parent 0aad65ed78
commit 3dc9632c1b
5 changed files with 183 additions and 92 deletions
+15
View File
@@ -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。常见日志关键字:
**中间件层拒绝**(有专门日志):
+11 -18
View File
@@ -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 {
+8 -17
View File
@@ -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
}
+14 -44
View File
@@ -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)
+135 -13
View File
@@ -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")
}
}