fix(setting): runtime config 全局变量加 sync.RWMutex 保护,避免半写状态
TTSOptions / TTSTimeout / TTSConfigErr / Auth.APIKeys / CORS 在运行期会被
LoadRuntimeConfig(PUT /api/settings 触发)整体替换,struct 整体赋值不是原子的,
若 TTS 请求正并发读,可能拿到半写状态。单用户场景概率低,属正确性隐患。
改动:
- setting/config.go: 把可变运行时配置迁到包内私有变量,新增 ttsMu
(sync.RWMutex) 统一保护,提供 Get*/Set* 访问器
- GetTTSOptions/SetTTSOptions: 值类型快照,读端无锁开销
- GetTTSTimeout/SetTTSTimeout
- GetTTSConfigErr/SetTTSConfigErr
- GetAuthAPIKeys/SetAuthAPIKeys: 拷贝进出,防止外部持有底层 slice
- GetCORSAllowAll/GetCORSOrigins/SetCORS: AllowAll 和 Origins 拆开,
避免热路径 CORSConfig 整体读时锁粒度过粗
- 删除导出可变全局 var TTSOptions/TTSTimeout/TTSConfigErr/Auth/CORS,
编译期强制所有读写走 Get*/Set*
- Server.Port/TrustedProxyHops/SetupToken 仅启动期写,运行期无并发修改,
保持原状不加锁
- LoadRuntimeConfig/InitAuthConfig/InitCORSConfig 全切到 Set*
- CheckEnvironmentVariables/LogStartupSummary 一次性拿快照,
缩短锁占用窗口,避免多次 log.Printf 之间数据被替换
调用点:
- controller/tts.go: 热路径 opts := setting.GetTTSOptions(),
context 超时走 GetTTSTimeout(),/health 块用 GetTTSConfigErr
- controller/settings.go: 写改 SetAuthAPIKeys/SetCORS
- controller/setup.go: 写改 SetAuthAPIKeys
- middleware/auth.go: 鉴权改 GetAuthAPIKeys()(在 RLock 下拿拷贝)
- middleware/admin_auth.go: 同上
- middleware/cors.go: matchOrigin 改 GetCORSAllowAll/GetCORSOrigins
读多写少,RWMutex 读路径不互斥,不会拖慢热路径。
This commit is contained in:
+9
-12
@@ -261,7 +261,7 @@ type SettingsAuthKeyRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SettingsAuthKeyHandler PUT /api/settings/auth-key
|
// SettingsAuthKeyHandler PUT /api/settings/auth-key
|
||||||
// 鉴权: RequireAdmin。改完立即更新 setting.Auth.APIKeys(进程内生效),
|
// 鉴权: RequireAdmin。改完立即刷新鉴权 key 列表(setting.SetAuthAPIKeys,进程内生效),
|
||||||
// 下一个请求就用新 key — admin 自己改完要等下一次请求才能验证(避免改完立刻自踢)。
|
// 下一个请求就用新 key — admin 自己改完要等下一次请求才能验证(避免改完立刻自踢)。
|
||||||
func SettingsAuthKeyHandler(w http.ResponseWriter, r *http.Request) {
|
func SettingsAuthKeyHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPut {
|
if r.Method != http.MethodPut {
|
||||||
@@ -292,7 +292,7 @@ func SettingsAuthKeyHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
}
|
}
|
||||||
// 立即生效:不重新 LoadRuntimeConfig(那会覆盖其它字段),
|
// 立即生效:不重新 LoadRuntimeConfig(那会覆盖其它字段),
|
||||||
// 只单独刷新 Auth.APIKeys
|
// 只单独刷新 Auth.APIKeys
|
||||||
setting.Auth.APIKeys = []string{key}
|
setting.SetAuthAPIKeys([]string{key})
|
||||||
log.Printf("[settings] auth_key updated, runtime active (next request uses new key)")
|
log.Printf("[settings] auth_key updated, runtime active (next request uses new key)")
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
|
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
|
||||||
@@ -309,7 +309,7 @@ type SettingsCORSRequest struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SettingsCORSHandler PUT /api/settings/cors
|
// SettingsCORSHandler PUT /api/settings/cors
|
||||||
// 鉴权: RequireAdmin。改完立即更新 setting.CORS(进程内生效,跨域请求从下个请求开始按新配置)。
|
// 鉴权: RequireAdmin。改完立即刷新 CORS(setting.SetCORS,进程内生效,跨域请求从下个请求开始按新配置)。
|
||||||
// 同源豁免由 middleware/cors.go 的 isSameOrigin 处理,不在这里管。
|
// 同源豁免由 middleware/cors.go 的 isSameOrigin 处理,不在这里管。
|
||||||
func SettingsCORSHandler(w http.ResponseWriter, r *http.Request) {
|
func SettingsCORSHandler(w http.ResponseWriter, r *http.Request) {
|
||||||
if r.Method != http.MethodPut {
|
if r.Method != http.MethodPut {
|
||||||
@@ -369,7 +369,7 @@ func SettingsCORSHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 立即刷新 setting.CORS,跨域请求从下个请求开始按新配置生效
|
// 立即刷新 CORS(setting.SetCORS),跨域请求从下个请求开始按新配置生效
|
||||||
// 复用 LoadRuntimeConfig 的解析逻辑(只取 cors 部分,避免覆盖其它运行时字段)
|
// 复用 LoadRuntimeConfig 的解析逻辑(只取 cors 部分,避免覆盖其它运行时字段)
|
||||||
corsAllowAll, _ := s.SettingsGetBool("cors_allow_all", false)
|
corsAllowAll, _ := s.SettingsGetBool("cors_allow_all", false)
|
||||||
originsStr := ""
|
originsStr := ""
|
||||||
@@ -377,20 +377,17 @@ func SettingsCORSHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
originsStr = v
|
originsStr = v
|
||||||
}
|
}
|
||||||
if corsAllowAll {
|
if corsAllowAll {
|
||||||
setting.CORS.AllowAll = true
|
setting.SetCORS(true, nil)
|
||||||
setting.CORS.Origins = nil
|
|
||||||
} else if originsStr != "" {
|
} else if originsStr != "" {
|
||||||
setting.CORS.AllowAll = false
|
setting.SetCORS(false, setting.SplitOriginsForCORS(originsStr))
|
||||||
setting.CORS.Origins = setting.SplitOriginsForCORS(originsStr)
|
|
||||||
} else {
|
} else {
|
||||||
setting.CORS.AllowAll = false
|
setting.SetCORS(false, nil)
|
||||||
setting.CORS.Origins = nil
|
|
||||||
}
|
}
|
||||||
log.Printf("[settings] cors updated (allow_all=%v origins=%q), runtime active", setting.CORS.AllowAll, originsStr)
|
log.Printf("[settings] cors updated (allow_all=%v origins=%q), runtime active", setting.GetCORSAllowAll(), originsStr)
|
||||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{
|
_ = json.NewEncoder(w).Encode(map[string]any{
|
||||||
"ok": true,
|
"ok": true,
|
||||||
"allow_all": setting.CORS.AllowAll,
|
"allow_all": setting.GetCORSAllowAll(),
|
||||||
"origins": originsStr,
|
"origins": originsStr,
|
||||||
"cors_active": true,
|
"cors_active": true,
|
||||||
})
|
})
|
||||||
|
|||||||
+3
-3
@@ -169,11 +169,11 @@ func SetupSubmitHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 立即把 auth_key 灌到 setting.Auth.APIKeys,这样后续 /v1/audio/speech 和 /admin
|
// 立即把 auth_key 灌到鉴权 key 列表(setting.SetAuthAPIKeys),这样后续
|
||||||
// 在本进程内能立刻用新 key(无需等 LoadRuntimeConfig)。
|
// /v1/audio/speech 和 /admin 在本进程内能立刻用新 key(无需等 LoadRuntimeConfig)。
|
||||||
authKey := strings.TrimSpace(body.Settings["auth_key"])
|
authKey := strings.TrimSpace(body.Settings["auth_key"])
|
||||||
if authKey != "" {
|
if authKey != "" {
|
||||||
setting.Auth.APIKeys = []string{authKey}
|
setting.SetAuthAPIKeys([]string{authKey})
|
||||||
}
|
}
|
||||||
|
|
||||||
// 装完 reload TTS 全局配置(让 TTSOptions 立即有可用的 api_key/speaker/resource_id,
|
// 装完 reload TTS 全局配置(让 TTSOptions 立即有可用的 api_key/speaker/resource_id,
|
||||||
|
|||||||
+11
-11
@@ -40,7 +40,7 @@ func truncateForLog(b []byte, max int) string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// resolveClientFormat 把 OpenAI 风格的 response_format 映射为最终输出格式;
|
// resolveClientFormat 把 OpenAI 风格的 response_format 映射为最终输出格式;
|
||||||
// 不识别或未指定时回退到 setting.TTSOptions.Format。
|
// 不识别或未指定时回退到 setting.GetTTSOptions().Format。
|
||||||
func resolveClientFormat(reqFmt string) string {
|
func resolveClientFormat(reqFmt string) string {
|
||||||
switch strings.ToLower(reqFmt) {
|
switch strings.ToLower(reqFmt) {
|
||||||
case "mp3", "wav", "opus", "pcm", "aac", "flac":
|
case "mp3", "wav", "opus", "pcm", "aac", "flac":
|
||||||
@@ -49,7 +49,7 @@ func resolveClientFormat(reqFmt string) string {
|
|||||||
}
|
}
|
||||||
return strings.ToLower(reqFmt)
|
return strings.ToLower(reqFmt)
|
||||||
}
|
}
|
||||||
return setting.TTSOptions.Format
|
return setting.GetTTSOptions().Format
|
||||||
}
|
}
|
||||||
|
|
||||||
// OpenaiTTSHandler 是 /v1/audio/speech 的入口。
|
// OpenaiTTSHandler 是 /v1/audio/speech 的入口。
|
||||||
@@ -81,9 +81,9 @@ func OpenaiTTSHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if setting.TTSConfigErr != nil {
|
if err := setting.GetTTSConfigErr(); err != nil {
|
||||||
log.Printf("警告: TTS配置未就绪,拒绝请求 - 错误=%v 路径=%s 客户端=%s",
|
log.Printf("警告: TTS配置未就绪,拒绝请求 - 错误=%v 路径=%s 客户端=%s",
|
||||||
setting.TTSConfigErr, r.URL.Path, middleware.GetClientIP(r))
|
err, r.URL.Path, middleware.GetClientIP(r))
|
||||||
middleware.SendJSONError(w, http.StatusServiceUnavailable, "TTS service configuration error. Please check environment variables and restart the service.", "configuration_error", "service_unavailable")
|
middleware.SendJSONError(w, http.StatusServiceUnavailable, "TTS service configuration error. Please check environment variables and restart the service.", "configuration_error", "service_unavailable")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -151,7 +151,7 @@ func OpenaiTTSHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
|
|
||||||
clientFormat := resolveClientFormat(req.ResponseFormat)
|
clientFormat := resolveClientFormat(req.ResponseFormat)
|
||||||
|
|
||||||
opts := setting.TTSOptions
|
opts := setting.GetTTSOptions()
|
||||||
opts.Text = req.Input
|
opts.Text = req.Input
|
||||||
|
|
||||||
// M3: voice 路由
|
// M3: voice 路由
|
||||||
@@ -183,7 +183,7 @@ func OpenaiTTSHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
"voice lookup failed", "server_error", "db_read_failed")
|
"voice lookup failed", "server_error", "db_read_failed")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// 覆盖 opts(API key / UID 保留自 setting.TTSOptions)
|
// 覆盖 opts(API key / UID 保留自 setting.GetTTSOptions 快照)
|
||||||
if !v.Enabled {
|
if !v.Enabled {
|
||||||
log.Printf("警告: voice=%q 已禁用 - 客户端=%s", req.Voice, middleware.GetClientIP(r))
|
log.Printf("警告: voice=%q 已禁用 - 客户端=%s", req.Voice, middleware.GetClientIP(r))
|
||||||
middleware.SendJSONError(w, http.StatusForbidden,
|
middleware.SendJSONError(w, http.StatusForbidden,
|
||||||
@@ -200,7 +200,7 @@ func OpenaiTTSHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
req.Voice, telemetry.MaskSpeaker(v.Speaker), telemetry.MaskResourceID(v.ResourceID), v.Model, middleware.GetClientIP(r))
|
req.Voice, telemetry.MaskSpeaker(v.Speaker), telemetry.MaskResourceID(v.ResourceID), v.Model, middleware.GetClientIP(r))
|
||||||
}
|
}
|
||||||
|
|
||||||
ctx, cancel := context.WithTimeout(r.Context(), setting.TTSTimeout)
|
ctx, cancel := context.WithTimeout(r.Context(), setting.GetTTSTimeout())
|
||||||
defer cancel()
|
defer cancel()
|
||||||
|
|
||||||
result, err := volcano.Synthesis(ctx, volcanoClient, opts, req.Input, clientFormat, speed, adapterRec)
|
result, err := volcano.Synthesis(ctx, volcanoClient, opts, req.Input, clientFormat, speed, adapterRec)
|
||||||
@@ -282,7 +282,7 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
mode := installer.GetMode()
|
mode := installer.GetMode()
|
||||||
if mode == installer.ModeSetup {
|
if mode == installer.ModeSetup {
|
||||||
w.WriteHeader(http.StatusOK) // 200,因为进程活着,只是还没初始化
|
w.WriteHeader(http.StatusOK) // 200,因为进程活着,只是还没初始化
|
||||||
} else if setting.TTSConfigErr != nil {
|
} else if setting.GetTTSConfigErr() != nil {
|
||||||
w.WriteHeader(http.StatusServiceUnavailable)
|
w.WriteHeader(http.StatusServiceUnavailable)
|
||||||
} else {
|
} else {
|
||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
@@ -308,8 +308,8 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
Memory: collectMemorySnapshot(),
|
Memory: collectMemorySnapshot(),
|
||||||
ConfigStatus: dto.ConfigStatusResponse{
|
ConfigStatus: dto.ConfigStatusResponse{
|
||||||
AllRequiredVarsSet: allRequired,
|
AllRequiredVarsSet: allRequired,
|
||||||
ConfigError: setting.TTSConfigErr != nil,
|
ConfigError: setting.GetTTSConfigErr() != nil,
|
||||||
Error: configErrorMessage(setting.TTSConfigErr),
|
Error: configErrorMessage(setting.GetTTSConfigErr()),
|
||||||
},
|
},
|
||||||
Installed: mode == installer.ModeNormal,
|
Installed: mode == installer.ModeNormal,
|
||||||
Mode: mode.String(),
|
Mode: mode.String(),
|
||||||
@@ -317,7 +317,7 @@ func HealthHandler(w http.ResponseWriter, r *http.Request) {
|
|||||||
json.NewEncoder(w).Encode(resp)
|
json.NewEncoder(w).Encode(resp)
|
||||||
}
|
}
|
||||||
|
|
||||||
// configErrorMessage 把 setting.TTSConfigErr 安全地转成可对外暴露的字符串。
|
// configErrorMessage 把运行时配置错误(setting.GetTTSConfigErr())安全地转成可对外暴露的字符串。
|
||||||
// 仅在 normal 模式且有错时调用, error 为 nil 时返 "" (被 omitempty 跳过)。
|
// 仅在 normal 模式且有错时调用, error 为 nil 时返 "" (被 omitempty 跳过)。
|
||||||
func configErrorMessage(err error) string {
|
func configErrorMessage(err error) string {
|
||||||
if err == nil {
|
if err == nil {
|
||||||
|
|||||||
@@ -77,8 +77,9 @@ func main() {
|
|||||||
}
|
}
|
||||||
log.Printf("[main][WARN] TTS 运行时配置加载失败 (setup mode, 需先 /setup): %v", err)
|
log.Printf("[main][WARN] TTS 运行时配置加载失败 (setup mode, 需先 /setup): %v", err)
|
||||||
} else {
|
} else {
|
||||||
|
opts := setting.GetTTSOptions()
|
||||||
log.Printf("[main] TTS 运行时配置已加载(api_key=***, speaker=%s, resource=%s, format=%s)",
|
log.Printf("[main] TTS 运行时配置已加载(api_key=***, speaker=%s, resource=%s, format=%s)",
|
||||||
telemetry.MaskSpeaker(setting.TTSOptions.Speaker), telemetry.MaskResourceID(setting.TTSOptions.ResourceID), setting.TTSOptions.Format)
|
telemetry.MaskSpeaker(opts.Speaker), telemetry.MaskResourceID(opts.ResourceID), opts.Format)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,7 @@ import (
|
|||||||
// - Authorization 头 Bearer token 在列表中 → 放行
|
// - Authorization 头 Bearer token 在列表中 → 放行
|
||||||
// - 其它 → 401 + JSON {error: 'unauthorized', code: 'admin_auth_failed'}
|
// - 其它 → 401 + JSON {error: 'unauthorized', code: 'admin_auth_failed'}
|
||||||
//
|
//
|
||||||
// 设计: 与现有 /v1/audio/speech 用的 setting.Auth 共享同一份 keys,
|
// 设计: 与现有 /v1/audio/speech 用的鉴权 key 列表(setting.GetAuthAPIKeys)共享同一份 keys,
|
||||||
// 用户只用管一个 env 变量(OPENAI_TTS_API_KEY)。
|
// 用户只用管一个 env 变量(OPENAI_TTS_API_KEY)。
|
||||||
func RequireAdmin(next http.Handler) http.Handler {
|
func RequireAdmin(next http.Handler) http.Handler {
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||||
@@ -25,7 +25,7 @@ func RequireAdmin(next http.Handler) http.Handler {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
keys := setting.Auth.APIKeys
|
keys := setting.GetAuthAPIKeys()
|
||||||
if len(keys) == 0 {
|
if len(keys) == 0 {
|
||||||
// 没配 admin key,等同无鉴权
|
// 没配 admin key,等同无鉴权
|
||||||
next.ServeHTTP(w, r)
|
next.ServeHTTP(w, r)
|
||||||
|
|||||||
+3
-2
@@ -10,7 +10,8 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func ValidateAPIKey(r *http.Request) bool {
|
func ValidateAPIKey(r *http.Request) bool {
|
||||||
if len(setting.Auth.APIKeys) == 0 {
|
keys := setting.GetAuthAPIKeys()
|
||||||
|
if len(keys) == 0 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -24,7 +25,7 @@ func ValidateAPIKey(r *http.Request) bool {
|
|||||||
}
|
}
|
||||||
|
|
||||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||||
for _, validKey := range setting.Auth.APIKeys {
|
for _, validKey := range keys {
|
||||||
if subtle.ConstantTimeCompare([]byte(token), []byte(validKey)) == 1 {
|
if subtle.ConstantTimeCompare([]byte(token), []byte(validKey)) == 1 {
|
||||||
return true
|
return true
|
||||||
}
|
}
|
||||||
|
|||||||
+2
-2
@@ -29,11 +29,11 @@ func matchOrigin(origin string) (string, bool) {
|
|||||||
if !isValidOrigin(origin) {
|
if !isValidOrigin(origin) {
|
||||||
return "", false
|
return "", false
|
||||||
}
|
}
|
||||||
if setting.CORS.AllowAll {
|
if setting.GetCORSAllowAll() {
|
||||||
return "*", true
|
return "*", true
|
||||||
}
|
}
|
||||||
normalized := strings.ToLower(strings.TrimRight(strings.TrimSpace(origin), "/"))
|
normalized := strings.ToLower(strings.TrimRight(strings.TrimSpace(origin), "/"))
|
||||||
for _, allowed := range setting.CORS.Origins {
|
for _, allowed := range setting.GetCORSOrigins() {
|
||||||
if allowed == normalized {
|
if allowed == normalized {
|
||||||
return origin, true
|
return origin, true
|
||||||
}
|
}
|
||||||
|
|||||||
+189
-71
@@ -8,6 +8,7 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/volcano-tts/tts-api/adapter/volcano"
|
"github.com/volcano-tts/tts-api/adapter/volcano"
|
||||||
@@ -16,30 +17,129 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// 全部环境变量读取的单一入口:其它包不允许直接 os.Getenv,只读这里的全局 Config。
|
// 全部环境变量读取的单一入口:其它包不允许直接 os.Getenv,只读这里的全局 Config。
|
||||||
|
//
|
||||||
// TTSOptions 是火山 v3 TTS 调用的完整参数集合,启动期由 LoadRuntimeConfig 从 store 填充。
|
// 并发模型:
|
||||||
// 业务侧(controller)直接读取并传入 volcano.Synthesis。
|
// - TTSOptions / TTSTimeout / TTSConfigErr / Auth.APIKeys / CORS 是运行期可被
|
||||||
|
// LoadRuntimeConfig(由 PUT /api/settings 触发)整体替换的"运行时配置快照"。
|
||||||
|
// struct 整体赋值不是原子的,若 TTS 请求正在读,可能拿到半写状态。
|
||||||
|
// - 解决: 用 ttsMu(RWMutex)统一保护这些字段;读路径用 Get* 拿快照(RLock),
|
||||||
|
// 写路径用 Set* 整体替换(Lock)。读多写少,RWMutex 读不互斥,不会显著拖慢热路径。
|
||||||
|
// - Server.Port / TrustedProxyHops / SetupToken 仅启动期写、运行期读,无并发修改,
|
||||||
|
// 保持原样不加锁。
|
||||||
var (
|
var (
|
||||||
TTSOptions volcano.Options
|
ttsMu sync.RWMutex
|
||||||
TTSConfigErr error
|
ttsOptions volcano.Options
|
||||||
// TTSTimeout 单次合成请求的超时;controller 用来派生 context。
|
ttsTimeout time.Duration = common.DefaultTimeout
|
||||||
TTSTimeout time.Duration = common.DefaultTimeout
|
ttsConfigErr error
|
||||||
|
authAPIKeys []string
|
||||||
|
// corsAllowAll / corsOrigins 拆成两个独立字段,各自在 RLock 下读取,
|
||||||
|
// 避免 CORSConfig 整体读时被 Lock 阻塞热路径。
|
||||||
|
corsAllowAll bool
|
||||||
|
corsOrigins []string
|
||||||
)
|
)
|
||||||
|
|
||||||
// AuthConfig OpenAI 兼容接口的客户端 API Key 鉴权配置。
|
// GetTTSOptions 读 TTSOptions 快照(值类型,使用方可以放心使用,不会被并发写破坏)。
|
||||||
type AuthConfig struct {
|
// 提供给业务侧(controller)的统一读取入口;LoadRuntimeConfig 写入时用 SetTTSOptions 整体替换。
|
||||||
APIKeys []string
|
// 业务侧不要缓存这个值跨 goroutine 使用(应该每次调用时重新拿)。
|
||||||
|
func GetTTSOptions() volcano.Options {
|
||||||
|
ttsMu.RLock()
|
||||||
|
defer ttsMu.RUnlock()
|
||||||
|
return ttsOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
var Auth AuthConfig
|
// SetTTSOptions 整体替换 TTSOptions。LoadRuntimeConfig 写路径专用。
|
||||||
|
func SetTTSOptions(o volcano.Options) {
|
||||||
// CORSConfig 跨域白名单配置。
|
ttsMu.Lock()
|
||||||
type CORSConfig struct {
|
defer ttsMu.Unlock()
|
||||||
Origins []string
|
ttsOptions = o
|
||||||
AllowAll bool
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var CORS CORSConfig
|
// GetTTSTimeout 读当前超时;controller 用它派生 context。
|
||||||
|
func GetTTSTimeout() time.Duration {
|
||||||
|
ttsMu.RLock()
|
||||||
|
defer ttsMu.RUnlock()
|
||||||
|
return ttsTimeout
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTTSTimeout 整体替换超时。
|
||||||
|
func SetTTSTimeout(d time.Duration) {
|
||||||
|
ttsMu.Lock()
|
||||||
|
defer ttsMu.Unlock()
|
||||||
|
ttsTimeout = d
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetTTSConfigErr 读运行时配置错误;nil 表示就绪。
|
||||||
|
func GetTTSConfigErr() error {
|
||||||
|
ttsMu.RLock()
|
||||||
|
defer ttsMu.RUnlock()
|
||||||
|
return ttsConfigErr
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetTTSConfigErr 设运行时配置错误;nil 表示清错。
|
||||||
|
func SetTTSConfigErr(err error) {
|
||||||
|
ttsMu.Lock()
|
||||||
|
defer ttsMu.Unlock()
|
||||||
|
ttsConfigErr = err
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetAuthAPIKeys 读鉴权 key 列表;返回拷贝防止业务侧持有底层 slice 后被并发写破坏。
|
||||||
|
// 外部不应直接读 Auth.APIKeys,统一走 Get*。
|
||||||
|
func GetAuthAPIKeys() []string {
|
||||||
|
ttsMu.RLock()
|
||||||
|
defer ttsMu.RUnlock()
|
||||||
|
if len(authAPIKeys) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, len(authAPIKeys))
|
||||||
|
copy(out, authAPIKeys)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAuthAPIKeys 整体替换鉴权 key 列表;入参被复制以防外部后续修改影响内部状态。
|
||||||
|
func SetAuthAPIKeys(keys []string) {
|
||||||
|
ttsMu.Lock()
|
||||||
|
defer ttsMu.Unlock()
|
||||||
|
if len(keys) == 0 {
|
||||||
|
authAPIKeys = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]string, len(keys))
|
||||||
|
copy(out, keys)
|
||||||
|
authAPIKeys = out
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCORSAllowAll 读 CORS 是否放行所有来源。
|
||||||
|
func GetCORSAllowAll() bool {
|
||||||
|
ttsMu.RLock()
|
||||||
|
defer ttsMu.RUnlock()
|
||||||
|
return corsAllowAll
|
||||||
|
}
|
||||||
|
|
||||||
|
// GetCORSOrigins 读 CORS 白名单;返回拷贝防止业务侧持有后被并发写破坏。
|
||||||
|
func GetCORSOrigins() []string {
|
||||||
|
ttsMu.RLock()
|
||||||
|
defer ttsMu.RUnlock()
|
||||||
|
if len(corsOrigins) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
out := make([]string, len(corsOrigins))
|
||||||
|
copy(out, corsOrigins)
|
||||||
|
return out
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetCORS 整体替换 CORS 配置;LoadRuntimeConfig 和 PUT /api/settings/cors 写路径专用。
|
||||||
|
func SetCORS(allowAll bool, origins []string) {
|
||||||
|
ttsMu.Lock()
|
||||||
|
defer ttsMu.Unlock()
|
||||||
|
corsAllowAll = allowAll
|
||||||
|
if len(origins) == 0 {
|
||||||
|
corsOrigins = nil
|
||||||
|
return
|
||||||
|
}
|
||||||
|
out := make([]string, len(origins))
|
||||||
|
copy(out, origins)
|
||||||
|
corsOrigins = out
|
||||||
|
}
|
||||||
|
|
||||||
// ServerConfig HTTP 服务监听配置。
|
// ServerConfig HTTP 服务监听配置。
|
||||||
type ServerConfig struct {
|
type ServerConfig struct {
|
||||||
@@ -52,6 +152,8 @@ var Server ServerConfig
|
|||||||
// 表示当前 XFF 解析模式:0=启发式,N>0=精确 N 跳。
|
// 表示当前 XFF 解析模式:0=启发式,N>0=精确 N 跳。
|
||||||
// setting.LogStartupSummary 读这个字段以展示运行期配置,
|
// setting.LogStartupSummary 读这个字段以展示运行期配置,
|
||||||
// 不直接调用 middleware(避免循环 import)。
|
// 不直接调用 middleware(避免循环 import)。
|
||||||
|
//
|
||||||
|
// 【并发】仅启动期被 InitRateLimiter 写一次,运行期只读,无并发问题,不加锁。
|
||||||
var TrustedProxyHops int
|
var TrustedProxyHops int
|
||||||
|
|
||||||
// SetupToken 是安装模式下的初始化凭证。
|
// SetupToken 是安装模式下的初始化凭证。
|
||||||
@@ -60,6 +162,8 @@ var TrustedProxyHops int
|
|||||||
// 打印到日志(/api/setup 提交时必须带这个 token)
|
// 打印到日志(/api/setup 提交时必须带这个 token)
|
||||||
//
|
//
|
||||||
// 安装完成后,/api/setup 端点永久关闭,SetupToken 失去意义但保留在内存。
|
// 安装完成后,/api/setup 端点永久关闭,SetupToken 失去意义但保留在内存。
|
||||||
|
//
|
||||||
|
// 【并发】仅 setup 阶段使用,运行期不会再写,无并发问题,不加锁。
|
||||||
var SetupToken string
|
var SetupToken string
|
||||||
|
|
||||||
// SetupTokenSource 标记 SetupToken 的来源,便于日志区分。
|
// SetupTokenSource 标记 SetupToken 的来源,便于日志区分。
|
||||||
@@ -87,7 +191,7 @@ func InitServerConfig() {
|
|||||||
func InitAuthConfig() {
|
func InitAuthConfig() {
|
||||||
raw := os.Getenv("OPENAI_TTS_API_KEY")
|
raw := os.Getenv("OPENAI_TTS_API_KEY")
|
||||||
if raw == "" {
|
if raw == "" {
|
||||||
Auth.APIKeys = nil
|
SetAuthAPIKeys(nil)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
parts := strings.Split(raw, ",")
|
parts := strings.Split(raw, ",")
|
||||||
@@ -98,27 +202,27 @@ func InitAuthConfig() {
|
|||||||
keys = append(keys, k)
|
keys = append(keys, k)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
Auth.APIKeys = keys
|
SetAuthAPIKeys(keys)
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitCORSConfig() {
|
func InitCORSConfig() {
|
||||||
raw := os.Getenv("ALLOWED_ORIGINS")
|
raw := os.Getenv("ALLOWED_ORIGINS")
|
||||||
CORS.Origins = nil
|
allowAll := false
|
||||||
CORS.AllowAll = false
|
var origins []string
|
||||||
if raw == "" {
|
if raw != "" {
|
||||||
return
|
for _, p := range strings.Split(raw, ",") {
|
||||||
}
|
o := strings.TrimSpace(p)
|
||||||
for _, p := range strings.Split(raw, ",") {
|
if o == "" {
|
||||||
o := strings.TrimSpace(p)
|
continue
|
||||||
if o == "" {
|
}
|
||||||
continue
|
if o == "*" {
|
||||||
|
allowAll = true
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
origins = append(origins, normalizeOrigin(o))
|
||||||
}
|
}
|
||||||
if o == "*" {
|
|
||||||
CORS.AllowAll = true
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
CORS.Origins = append(CORS.Origins, normalizeOrigin(o))
|
|
||||||
}
|
}
|
||||||
|
SetCORS(allowAll, origins)
|
||||||
}
|
}
|
||||||
|
|
||||||
func normalizeOrigin(origin string) string {
|
func normalizeOrigin(origin string) string {
|
||||||
@@ -129,7 +233,7 @@ func normalizeOrigin(origin string) string {
|
|||||||
|
|
||||||
// SplitOriginsForCORS 解析逗号/换行/空格分隔的 origins 列表,
|
// SplitOriginsForCORS 解析逗号/换行/空格分隔的 origins 列表,
|
||||||
// 全部小写、trim 末尾 / 后面统一比较。导出供 controller 复用
|
// 全部小写、trim 末尾 / 后面统一比较。导出供 controller 复用
|
||||||
// (PUT /api/settings/cors 写完立即刷新 setting.CORS 用)。
|
// (PUT /api/settings/cors 写完立即刷新 CORS 用)。
|
||||||
func SplitOriginsForCORS(s string) []string {
|
func SplitOriginsForCORS(s string) []string {
|
||||||
return splitAndLowerOrigins(s)
|
return splitAndLowerOrigins(s)
|
||||||
}
|
}
|
||||||
@@ -160,6 +264,9 @@ func splitAndLowerOrigins(s string) []string {
|
|||||||
// - 失败时 TTSConfigErr 被设置,/v1/audio/speech 路由会返 503
|
// - 失败时 TTSConfigErr 被设置,/v1/audio/speech 路由会返 503
|
||||||
// - 成功时清空 TTSConfigErr
|
// - 成功时清空 TTSConfigErr
|
||||||
//
|
//
|
||||||
|
// 【并发】所有写都走 Set* 系列函数,在 ttsMu.Lock 下完成;运行中的 TTS 请求
|
||||||
|
// 用 Get* 拿快照不会读到半写状态。
|
||||||
|
//
|
||||||
// 字段映射(原 env → store key):
|
// 字段映射(原 env → store key):
|
||||||
// BYTEDANCE_TTS_API_KEY → api_key
|
// BYTEDANCE_TTS_API_KEY → api_key
|
||||||
// BYTEDANCE_TTS_RESOURCE_ID → default_resource_id
|
// BYTEDANCE_TTS_RESOURCE_ID → default_resource_id
|
||||||
@@ -180,8 +287,9 @@ func splitAndLowerOrigins(s string) []string {
|
|||||||
func LoadRuntimeConfig(s Store) error {
|
func LoadRuntimeConfig(s Store) error {
|
||||||
all, err := s.SettingsGetAll()
|
all, err := s.SettingsGetAll()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
TTSConfigErr = fmt.Errorf("read settings failed: %w", err)
|
wrapped := fmt.Errorf("read settings failed: %w", err)
|
||||||
return TTSConfigErr
|
SetTTSConfigErr(wrapped)
|
||||||
|
return wrapped
|
||||||
}
|
}
|
||||||
|
|
||||||
apiKey := all["api_key"]
|
apiKey := all["api_key"]
|
||||||
@@ -198,8 +306,9 @@ func LoadRuntimeConfig(s Store) error {
|
|||||||
missing = append(missing, "default_speaker")
|
missing = append(missing, "default_speaker")
|
||||||
}
|
}
|
||||||
if len(missing) > 0 {
|
if len(missing) > 0 {
|
||||||
TTSConfigErr = fmt.Errorf("missing required settings: %v", missing)
|
wrapped := fmt.Errorf("missing required settings: %v", missing)
|
||||||
return TTSConfigErr
|
SetTTSConfigErr(wrapped)
|
||||||
|
return wrapped
|
||||||
}
|
}
|
||||||
|
|
||||||
// 【BUG 修复 · 第二轮】default_speaker 是 voice **名字**(如 "chun"),
|
// 【BUG 修复 · 第二轮】default_speaker 是 voice **名字**(如 "chun"),
|
||||||
@@ -248,14 +357,15 @@ func LoadRuntimeConfig(s Store) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
TTSTimeout = common.DefaultTimeout
|
timeout := common.DefaultTimeout
|
||||||
if v, err := s.SettingsGetDuration("timeout", common.DefaultTimeout); err == nil {
|
if v, err := s.SettingsGetDuration("timeout", common.DefaultTimeout); err == nil {
|
||||||
TTSTimeout = v
|
timeout = v
|
||||||
} else {
|
} else {
|
||||||
TTSTimeout = common.DefaultTimeout
|
timeout = common.DefaultTimeout
|
||||||
}
|
}
|
||||||
|
SetTTSTimeout(timeout)
|
||||||
|
|
||||||
TTSOptions = volcano.Options{
|
SetTTSOptions(volcano.Options{
|
||||||
APIKey: apiKey,
|
APIKey: apiKey,
|
||||||
ResourceID: resourceId,
|
ResourceID: resourceId,
|
||||||
UID: "uid",
|
UID: "uid",
|
||||||
@@ -268,18 +378,17 @@ func LoadRuntimeConfig(s Store) error {
|
|||||||
LoudnessRate: 0,
|
LoudnessRate: 0,
|
||||||
EnableSubtitle: enableSubtitle,
|
EnableSubtitle: enableSubtitle,
|
||||||
Additions: adds,
|
Additions: adds,
|
||||||
}
|
})
|
||||||
|
|
||||||
// 鉴权 key:DB > env(向后兼容)
|
// 鉴权 key:DB > env(向后兼容)
|
||||||
authKey := all["auth_key"]
|
authKey := all["auth_key"]
|
||||||
if authKey == "" {
|
if authKey == "" {
|
||||||
authKey = os.Getenv("OPENAI_TTS_API_KEY")
|
authKey = os.Getenv("OPENAI_TTS_API_KEY")
|
||||||
}
|
}
|
||||||
// 用临时 slice 避免和 InitAuthConfig 抢同一个 Auth.APIKeys 底层
|
|
||||||
if authKey != "" {
|
if authKey != "" {
|
||||||
Auth.APIKeys = []string{authKey}
|
SetAuthAPIKeys([]string{authKey})
|
||||||
} else {
|
} else {
|
||||||
Auth.APIKeys = nil
|
SetAuthAPIKeys(nil)
|
||||||
}
|
}
|
||||||
|
|
||||||
// CORS 配置:DB > env
|
// CORS 配置:DB > env
|
||||||
@@ -298,17 +407,14 @@ func LoadRuntimeConfig(s Store) error {
|
|||||||
originsStr = os.Getenv("ALLOWED_ORIGINS")
|
originsStr = os.Getenv("ALLOWED_ORIGINS")
|
||||||
}
|
}
|
||||||
if corsAllowAll {
|
if corsAllowAll {
|
||||||
CORS.AllowAll = true
|
SetCORS(true, nil)
|
||||||
CORS.Origins = nil
|
|
||||||
} else if originsStr != "" {
|
} else if originsStr != "" {
|
||||||
CORS.AllowAll = false
|
SetCORS(false, SplitOriginsForCORS(originsStr))
|
||||||
CORS.Origins = SplitOriginsForCORS(originsStr)
|
|
||||||
} else {
|
} else {
|
||||||
CORS.AllowAll = false
|
SetCORS(false, nil)
|
||||||
CORS.Origins = nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
TTSConfigErr = nil
|
SetTTSConfigErr(nil)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -388,10 +494,16 @@ func InitSetupToken() {
|
|||||||
|
|
||||||
// CheckEnvironmentVariables 返回 /health 用的环境变量状态快照。
|
// CheckEnvironmentVariables 返回 /health 用的环境变量状态快照。
|
||||||
func CheckEnvironmentVariables() map[string]interface{} {
|
func CheckEnvironmentVariables() map[string]interface{} {
|
||||||
|
// 一次性拿所有需要的快照,缩短锁占用窗口;后续只读本地变量。
|
||||||
|
opts := GetTTSOptions()
|
||||||
|
authKeys := GetAuthAPIKeys()
|
||||||
|
allowAll := GetCORSAllowAll()
|
||||||
|
origins := GetCORSOrigins()
|
||||||
|
|
||||||
required := map[string]bool{
|
required := map[string]bool{
|
||||||
"BYTEDANCE_TTS_API_KEY": TTSOptions.APIKey != "",
|
"BYTEDANCE_TTS_API_KEY": opts.APIKey != "",
|
||||||
"BYTEDANCE_TTS_RESOURCE_ID": TTSOptions.ResourceID != "",
|
"BYTEDANCE_TTS_RESOURCE_ID": opts.ResourceID != "",
|
||||||
"BYTEDANCE_TTS_SPEAKER": TTSOptions.Speaker != "",
|
"BYTEDANCE_TTS_SPEAKER": opts.Speaker != "",
|
||||||
}
|
}
|
||||||
missing := []string{}
|
missing := []string{}
|
||||||
for k, ok := range required {
|
for k, ok := range required {
|
||||||
@@ -400,12 +512,12 @@ func CheckEnvironmentVariables() map[string]interface{} {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
optional := map[string]bool{
|
optional := map[string]bool{
|
||||||
"BYTEDANCE_TTS_MODEL": TTSOptions.Model != "",
|
"BYTEDANCE_TTS_MODEL": opts.Model != "",
|
||||||
"BYTEDANCE_TTS_FORMAT": TTSOptions.Format != "mp3",
|
"BYTEDANCE_TTS_FORMAT": opts.Format != "mp3",
|
||||||
"BYTEDANCE_TTS_SAMPLE_RATE": TTSOptions.SampleRate != 24000,
|
"BYTEDANCE_TTS_SAMPLE_RATE": opts.SampleRate != 24000,
|
||||||
"BYTEDANCE_TTS_EXPLICIT_LANGUAGE": TTSOptions.Additions != nil && TTSOptions.Additions.ExplicitLanguage != "",
|
"BYTEDANCE_TTS_EXPLICIT_LANGUAGE": opts.Additions != nil && opts.Additions.ExplicitLanguage != "",
|
||||||
"OPENAI_TTS_API_KEY": len(Auth.APIKeys) > 0,
|
"OPENAI_TTS_API_KEY": len(authKeys) > 0,
|
||||||
"ALLOWED_ORIGINS": CORS.AllowAll || len(CORS.Origins) > 0,
|
"ALLOWED_ORIGINS": allowAll || len(origins) > 0,
|
||||||
"PORT": Server.Port != common.DefaultPort,
|
"PORT": Server.Port != common.DefaultPort,
|
||||||
}
|
}
|
||||||
return map[string]interface{}{
|
return map[string]interface{}{
|
||||||
@@ -421,18 +533,21 @@ func LogStartupSummary() {
|
|||||||
log.Printf("=== 环境配置汇总 ===")
|
log.Printf("=== 环境配置汇总 ===")
|
||||||
log.Printf("服务端口: %s", Server.Port)
|
log.Printf("服务端口: %s", Server.Port)
|
||||||
|
|
||||||
if len(Auth.APIKeys) == 0 {
|
authKeys := GetAuthAPIKeys()
|
||||||
|
if len(authKeys) == 0 {
|
||||||
log.Printf("OPENAI_TTS_API_KEY: 未设置(所有请求无需鉴权)")
|
log.Printf("OPENAI_TTS_API_KEY: 未设置(所有请求无需鉴权)")
|
||||||
} else {
|
} else {
|
||||||
log.Printf("OPENAI_TTS_API_KEY: 已设置 %d 个有效密钥", len(Auth.APIKeys))
|
log.Printf("OPENAI_TTS_API_KEY: 已设置 %d 个有效密钥", len(authKeys))
|
||||||
}
|
}
|
||||||
|
|
||||||
if CORS.AllowAll {
|
allowAll := GetCORSAllowAll()
|
||||||
|
origins := GetCORSOrigins()
|
||||||
|
if allowAll {
|
||||||
log.Printf("ALLOWED_ORIGINS: *(允许所有跨域;不可与鉴权共用)")
|
log.Printf("ALLOWED_ORIGINS: *(允许所有跨域;不可与鉴权共用)")
|
||||||
} else if len(CORS.Origins) == 0 {
|
} else if len(origins) == 0 {
|
||||||
log.Printf("ALLOWED_ORIGINS: 未设置(跨域请求将被拒绝)")
|
log.Printf("ALLOWED_ORIGINS: 未设置(跨域请求将被拒绝)")
|
||||||
} else {
|
} else {
|
||||||
log.Printf("ALLOWED_ORIGINS: 已配置 %d 个允许的跨域来源白名单", len(CORS.Origins))
|
log.Printf("ALLOWED_ORIGINS: 已配置 %d 个允许的跨域来源白名单", len(origins))
|
||||||
}
|
}
|
||||||
|
|
||||||
if h := TrustedProxyHops; h == 0 {
|
if h := TrustedProxyHops; h == 0 {
|
||||||
@@ -441,6 +556,9 @@ func LogStartupSummary() {
|
|||||||
log.Printf("TRUSTED_PROXY_HOPS: 精确模式,信任 %d 跳反代", h)
|
log.Printf("TRUSTED_PROXY_HOPS: 精确模式,信任 %d 跳反代", h)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 一次性拿 TTSOptions 快照;后续只读本地变量,避免在多个 log.Printf 调用之间
|
||||||
|
// 被 LoadRuntimeConfig 整体替换导致打印出不连贯的数据。
|
||||||
|
opts := GetTTSOptions()
|
||||||
log.Printf("火山 TTS 必填项状态:")
|
log.Printf("火山 TTS 必填项状态:")
|
||||||
type ttsCheck struct {
|
type ttsCheck struct {
|
||||||
name string
|
name string
|
||||||
@@ -448,10 +566,10 @@ func LogStartupSummary() {
|
|||||||
ok bool
|
ok bool
|
||||||
}
|
}
|
||||||
checks := []ttsCheck{
|
checks := []ttsCheck{
|
||||||
{"BYTEDANCE_TTS_API_KEY", maskAPIKey(TTSOptions.APIKey), TTSOptions.APIKey != ""},
|
{"BYTEDANCE_TTS_API_KEY", maskAPIKey(opts.APIKey), opts.APIKey != ""},
|
||||||
{"BYTEDANCE_TTS_RESOURCE_ID", telemetry.MaskResourceID(TTSOptions.ResourceID), TTSOptions.ResourceID != ""},
|
{"BYTEDANCE_TTS_RESOURCE_ID", telemetry.MaskResourceID(opts.ResourceID), opts.ResourceID != ""},
|
||||||
// speaker 是火山复刻音色 ID(用户付费资产),日志里打码,避免明文落盘
|
// speaker 是火山复刻音色 ID(用户付费资产),日志里打码,避免明文落盘
|
||||||
{"BYTEDANCE_TTS_SPEAKER", telemetry.MaskSpeaker(TTSOptions.Speaker), TTSOptions.Speaker != ""},
|
{"BYTEDANCE_TTS_SPEAKER", telemetry.MaskSpeaker(opts.Speaker), opts.Speaker != ""},
|
||||||
}
|
}
|
||||||
missingCount := 0
|
missingCount := 0
|
||||||
for _, c := range checks {
|
for _, c := range checks {
|
||||||
@@ -467,7 +585,7 @@ func LogStartupSummary() {
|
|||||||
log.Printf(" %s %s: %s", mark, c.name, val)
|
log.Printf(" %s %s: %s", mark, c.name, val)
|
||||||
}
|
}
|
||||||
|
|
||||||
if TTSConfigErr != nil {
|
if err := GetTTSConfigErr(); err != nil {
|
||||||
log.Printf("火山 TTS 整体: 初始化失败(%d 个必填项缺失),/v1/audio/speech 路由将全部返回 500", missingCount)
|
log.Printf("火山 TTS 整体: 初始化失败(%d 个必填项缺失),/v1/audio/speech 路由将全部返回 500", missingCount)
|
||||||
} else {
|
} else {
|
||||||
log.Printf("火山 TTS 整体: 初始化成功")
|
log.Printf("火山 TTS 整体: 初始化成功")
|
||||||
|
|||||||
Reference in New Issue
Block a user