From a655f8ae0e75f6415b7e3409d3a3fa89187cb1cb Mon Sep 17 00:00:00 2001 From: "3371392206@qq.com" <3371392206@qq.com> Date: Sun, 30 Aug 2026 19:16:04 +0800 Subject: [PATCH] =?UTF-8?q?fix(tts):=20voice.enabled=3D0=20=E7=9C=9F?= =?UTF-8?q?=E7=94=9F=E6=95=88,=E8=BF=94=E5=9B=9E=20403=20voice=5Fdisabled?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bug: admin UI 切换"启用/禁用"toggle → DB 字段更新 ✓, 但 controller/tts.go 路由时不查 enabled, 关掉的 voice 仍能正常调通。 admin 关闭形同虚设。 修法: 在 voice 查库命中后, 立即检查 v.Enabled: - enabled=1 → 走原流程 - enabled=0 → 立即 403, 错误码 voice_disabled Body 格式(对齐 OpenAI error 规范): { "error": { "code": "voice_disabled", "message": "voice 'chun' is disabled", "type": "invalid_request_error" } } 为什么 403 (vs 400 / 410): - 400: 客户端发错 — 不准, voice 存在, 只是被关 - 410: 资源 gone — 不准, 不是永久弃用, 可能再开 - 403: 资源存在但无权用 — 准确, admin 关 = 拒绝调用 (OpenAI 其他端点对禁用资源用 403, 这是惯例) e2e (本机): - TEST 1: voice=chun, enabled=1 → 500 (volcano 假 key 401, 路由通了) - TEST 2: voice=chun, enabled=0 → 403 voice_disabled ✓ - TEST 3: voice=ghost, 不存在 → 400 unknown_voice (原有行为不变) 副作用: - 没有任何 admin 路径失败 (admin 改 enabled 字段直接生效, 无需 reload) - metrics 没加新 label (后续可加 voice_disabled 计数, 留给 M4) 未 push (待用户) --- controller/tts.go | 11 +++++++++-- 1 file changed, 9 insertions(+), 2 deletions(-) diff --git a/controller/tts.go b/controller/tts.go index 727a249..ec5fa09 100644 --- a/controller/tts.go +++ b/controller/tts.go @@ -172,9 +172,9 @@ func OpenaiTTSHandler(w http.ResponseWriter, r *http.Request) { v, err := s.VoiceGetByName(req.Voice) if err != nil { if err == store.ErrNotFound { - log.Printf("警告: 未知 voice=%s - 路径=%s 客户端=%s", req.Voice, r.URL.Path, middleware.GetClientIP(r)) + log.Printf("警告: 未知 voice=%q - 路径=%s 客户端=%s", req.Voice, r.URL.Path, middleware.GetClientIP(r)) middleware.SendJSONError(w, http.StatusBadRequest, - fmt.Sprintf("unknown voice: %s", req.Voice), + fmt.Sprintf("unknown voice: '%s'", req.Voice), "invalid_request_error", "unknown_voice") return } @@ -184,6 +184,13 @@ func OpenaiTTSHandler(w http.ResponseWriter, r *http.Request) { return } // 覆盖 opts(API key / UID 保留自 setting.TTSOptions) + if !v.Enabled { + log.Printf("警告: voice=%q 已禁用 - 客户端=%s", req.Voice, middleware.GetClientIP(r)) + middleware.SendJSONError(w, http.StatusForbidden, + fmt.Sprintf("voice '%s' is disabled", req.Voice), + "invalid_request_error", "voice_disabled") + return + } opts.Speaker = v.Speaker opts.ResourceID = v.ResourceID if v.Model != "" {