fix(voice): 修 r1 复审四条(正文进日志的隐私泄漏 / format 假开关 / Stats 无消费者 / redactURL 兜底)

上一版把 r1 的三条 finding 静默漏了(我在交付前没读 inbox),本提交逐条补齐:

1. 🔴 隐私:upstreamStatus 不再读也不记响应体
   FastAPI 的 422 会把请求体里的 input 整段回显,原来「错误体前 512 字节进 WARN」
   等于把她的原话写进日志。现在只记 status / content_type / response_bytes
   (字节数优先取 Content-Length,缺失时有界排空取上界,读到即丢)。
   新用例 TestSpeechRejectLogOmitsRequestText 用**回显正文**的假上游抓它;
   变异(把 body 塞回 WARN)实测打红。

2. format 键彻底删除(假开关)
   上游只拿 response_format 改自报 media_type,产物恒 .wav 无转码,我方只按字节
   嗅探裁决 ⇒ wav/mp3/ogg 交付字节与响应头完全相同。删 tts.Config.Format 与
   VoiceTTSTuning.Format,encodeSpeech 硬编码 requestFormat="wav"(不可配置)。
   用例钉住请求体恒为 wav;变异(改成 mp3)打红。旧文件里残留的 format 键被忽略。

3. Stats 接进 degrade 的 WARN(attempts/failures)
   否则统计在生产日志里没有消费者。TestServiceDegradeLogsCounters 钉住;
   变异(去掉两个字段)打红。

4. redactURL 兜底含 "@" 整体打码
   解析失败/无 host 时原来原样返回,user:pass@ 会漏凭据。TestRedactURLCredentials
   覆盖合法 URL 去 userinfo、非法含 @ 打码;变异(退回原样返回)打红。

另:关闭态零行为变化的**当前 tip 重跑**——
- handleSpeech 的 persona 解析改为只在语音服务实现 personaVoice 且 PrimaryActive
  时才查库;未接 emotionTTS(既有链/stub)时零额外 I/O、ctx 不改、音频字节相同。
  用例 TestSpeechEndpointDisabledVoiceDoesNotReadPersona(含对照组证明闸不是恒关);
  变异(去掉闸)打红。
- voice.go 在 dev 基线里本就有 env.PersonaID 解析(无人格解析新增 I/O),本次未改。

Config.Normalized 去掉未使用的 logger 参数。go build/vet/test(含 -race)全绿;
真机 live 复测:cyrene 264104 B、furina 221194 B。
This commit is contained in:
AskaEth committed 2026-10-26 13:06:39 +08:00
1 parent efc25c6a7f
commit 8820b0b316
12 files changed
+333 -69

No files matched your search

+7 -9
View File
@@ -91,8 +91,9 @@ func TestSpeechSendsUTF8ChineseText(t *testing.T) {
if gotPayload.Voice != DefaultCharacter {
t.Errorf("voice = %q, want %q", gotPayload.Voice, DefaultCharacter)
}
if gotPayload.ResponseFormat != DefaultFormat {
t.Errorf("response_format = %q, want %q", gotPayload.ResponseFormat, DefaultFormat)
// response_format 恒为 wav 且**不可配置**(上游产物无转码,见 requestFormat)。
if gotPayload.ResponseFormat != "wav" {
t.Errorf("response_format = %q, want 硬编码的 wav", gotPayload.ResponseFormat)
}
// ③ 回发的格式由**字节嗅探**裁决,不是请求里那个字段。
if audio.Format != "wav" || len(audio.Data) == 0 {
@@ -216,24 +217,21 @@ func TestSpeechGuards(t *testing.T) {
// 🔴 它同时钉住一条**反向**口径:归一化**不得**给任何角色兜底 —— 代码里写死一个
// 默认发音人,正是主人点名的病害(换人格仍用老声音)。
func TestConfigNormalized(t *testing.T) {
got := Config{}.Normalized(discardLogger())
got := Config{}.Normalized()
if got.Enabled {
t.Errorf("零值配置不该被悄悄启用")
}
if got.BaseURL != DefaultBaseURL || got.Format != DefaultFormat || got.Timeout != DefaultTimeout {
if got.BaseURL != DefaultBaseURL || got.Timeout != DefaultTimeout {
t.Fatalf("默认值不对: %+v", got)
}
if got.Default != "" || len(got.Personas) != 0 {
t.Fatalf("归一化给发音人兜底了(红线:不许有代码默认角色): %+v", got)
}
if custom := (Config{BaseURL: " http://127.0.0.1:9880/ ", Format: "MP3"}).Normalized(discardLogger()); custom.BaseURL != DefaultBaseURL || custom.Format != "mp3" {
if custom := (Config{BaseURL: " http://127.0.0.1:9880/ "}).Normalized(); custom.BaseURL != DefaultBaseURL {
t.Errorf("归一化不对: %+v", custom)
}
if bad := (Config{Format: "exe"}).Normalized(discardLogger()); bad.Format != DefaultFormat {
t.Errorf("非法格式应回落默认: %+v", bad)
}
// persona/character 两端的空白被清掉;空 personaId 的键被丢弃。
trimmed := (Config{Personas: map[string]string{" a ": " char_1 ", "": "char_2"}, Default: " char_3 "}).Normalized(discardLogger())
trimmed := (Config{Personas: map[string]string{" a ": " char_1 ", "": "char_2"}, Default: " char_3 "}).Normalized()
if trimmed.Personas["a"] != "char_1" || len(trimmed.Personas) != 1 || trimmed.Default != "char_3" {
t.Fatalf("映射/默认值没有被归一化: %+v", trimmed)
}