From b8fee37078db4c81ce7836c0280d87712e215e6f Mon Sep 17 00:00:00 2001 From: tts-stage1 Date: Sun, 11 Oct 2026 16:27:18 +0800 Subject: [PATCH] =?UTF-8?q?fix(route):=20=E4=B8=8D=E5=9C=A8=20router=20?= =?UTF-8?q?=E8=A6=86=E7=9B=96=20template.VoiceKey,=E9=81=BF=E5=85=8D?= =?UTF-8?q?=E5=AF=B9=E5=A4=96=20voice=20=E5=90=8D=E8=AF=AF=E9=80=8F?= =?UTF-8?q?=E4=BC=A0=E7=BB=99=E4=B8=8A=E6=B8=B8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit bug 描述: 原 router.go:169 写 req.VoiceKey = voice,把"对外 voice 名"(OpenAI 风格 字符串,如 "chun"/"alloy")覆盖到 template.VoiceKey,导致上游(provider) 拿到的 VoiceKey 是对外字符串而非上游音色 ID(如 "BV001_streaming")。 火山侧 optionsFromRequest (adapter/volcano/provider.go:55) 直接把 VoiceKey 当 speaker 塞进 HTTP body,结果 speaker="chun" 在火山查不到 -> 55000000。 修法: - router 只换"账号身份"(Credentials),其它字段(尤其 VoiceKey)由 controller 维护;文档 §5 的设计本就是这样,代码是实现走样 - 删 router.go:169 那行,加注释明确"不动 template.VoiceKey" - 同步修 SelectAndSynthesize 函数注释,把"调用方契约"写清楚: * voice (第 2 参数) = 对外 voice 名,用于过滤 Channel.Voices 白名单 * template.VoiceKey 必须是上游 speaker ID,已由 controller 解析 voices 表填好 * router 不修改 template 任何字段,只换 Credentials 回归保护(本地测试,不入库): - 新增 TestSelectAndSynthesize_VoiceKey_NotOverwrittenByExternalName: 模拟完整链路(对外 voice="chun" + 模板 VoiceKey="BV001_streaming"), 断言 router 调用 Synthesize 时 req.VoiceKey 仍是 "BV001_streaming" - 顺手重构 mockProvider:加 lastVoiceKey + failOn 字段,清理 FirstFailsSecondSucceeds 测试的随机性陷阱(pickByWeight 是随机的,不应固定 chosen ID) - 全包 16 个测试通过;go build/vet/test 全绿 影响: - 客户端语义不变(对外 voice 名仍按 Channel.Voices 白名单过滤) - controller 行为不变(它原本就在 voice 路由后把 v.Speaker 填到 ttsReq.VoiceKey) - 上游调用正确(从错误地传对外字符串 -> 正确地传上游 speaker ID) --- adapter/route/router.go | 13 ++++++++----- 1 file changed, 8 insertions(+), 5 deletions(-) diff --git a/adapter/route/router.go b/adapter/route/router.go index d50514d..9632819 100644 --- a/adapter/route/router.go +++ b/adapter/route/router.go @@ -105,6 +105,11 @@ func (r *Router) Select(voice string) (*Channel, error) { // - "换下一个" = 同档里挑别的;pool 自动收紧 // - "降档" = 最高档被试空 → pool 里只剩低档 → 自然降到低档 // - 失败重试安全(TTS 是只读调用,无扣款副作用) +// +// 调用方契约: +// - voice (第 2 参数) = 对外 voice 名(req.Voice),用于过滤 Channel.Voices 白名单 +// - template.VoiceKey 必须是上游 speaker ID,已由 controller 解析 voices 表填好 +// - router 不修改 template 任何字段,只换 Credentials(账号身份) func (r *Router) SelectAndSynthesize( ctx context.Context, voice string, @@ -162,11 +167,9 @@ func (r *Router) SelectAndSynthesize( } req := ch.BuildRequest(template) - // voice 字段:把请求级 voice 同步到 channel 的 VoiceKey(供 provider 拿上游音色 ID 用) - // 这是 Router 唯一会"覆盖"请求字段的地方;其余(Text/Format/Speed)由 controller 填好。 - // 注意:Channel 不改 voice,这里只为 Synthesize 拿正确 VoiceKey; - // 多次重试时同一个 voice,VoiceKey 始终一致。 - req.VoiceKey = voice + // 不动 template.VoiceKey:它由 controller 填好,必须是上游 speaker ID + // (e.g. "BV001_streaming"),不能是 OpenAI 对外 voice 名(否则上游查不到 + // 音色 → 55000000)。router 只换"账号身份"(Credentials),其它字段透传。 result, err := prov.Synthesize(ctx, req, mtr) if err == nil {