feat(voice): 语音消息落库 + 转码接入(切片 0020)

- 迁移 0020_chat_message_voice:按 design/24 §10 与契约 §7.2 已登记的 voice 形状建列/索引
- 语音回合进编排并落库;voice 引用与统计如实回填(转码/转写没成功就如实标未成功,不假装)
- 复用既有 internal/capability/transcode:非 wav(微信 AMR 等)先转码再送 ASR;失败可分类且不丢消息

⚠️ 部分完成(如实登记):生产端到端未成立(本片只到落库与转码接入),且**未经验真机**。
This commit is contained in:
AskaEth committed 2026-10-06 09:04:06 +08:00
1 parent 120d926cf8
commit ede00d9928
17 files changed
+2125 -31

No files matched your search

@@ -386,7 +386,13 @@ func TestInboundVoiceTimeoutIsHonest(t *testing.T) {
}
// TestInboundVoiceWithoutBytesOrFormatIsHonest 覆盖两条「拿不到音频」的路径:没有字节
// (渠道只给平台内部引用 / 代取失败)与格式不支持 —— 都落诚实提示,且**不调上游**。
// (渠道只给平台内部引用 / 代取失败)与格式不在**可转写白名单**内 —— 都落诚实提示,
// 且**不调上游**。
//
// ⚠️ 文案留痕(2026-10-06,切片 CODE-VOICE):本用例原先钉的是「格式不支持(需要先转码,
// 当前未做)」——那句在转码能力(design/25)落地后**就是一句假话**(design/25 §5.4 明令
// 同步改)。因此这里改钉新口径:格式**不在可转写白名单内**。这不是「改测试迁就代码」,
// 而是被断言保护的行为**已按设计变更**(与 QQ `[语音]` 占位标记那条同一性质)。
func TestInboundVoiceWithoutBytesOrFormatIsHonest(t *testing.T) {
t.Parallel()
t.Run("没有字节", func(t *testing.T) {
@@ -404,7 +410,7 @@ func TestInboundVoiceWithoutBytesOrFormatIsHonest(t *testing.T) {
t.Fatalf("落库正文 = %q, want 字节缺失的诚实提示", got)
}
})
t.Run("格式不支持", func(t *testing.T) {
t.Run("格式不在可转写白名单内", func(t *testing.T) {
t.Parallel()
stt := &fakeVoiceSTT{}
env := newVoiceEnv(t, stt, 0)
@@ -415,8 +421,12 @@ func TestInboundVoiceWithoutBytesOrFormatIsHonest(t *testing.T) {
if stt.callCount() != 0 {
t.Fatalf("STT 调用次数 = %d, want 0(格式不在白名单,不调上游)", stt.callCount())
}
if got := lastUserContent(env); !strings.Contains(got, "格式不支持") {
t.Fatalf("落库正文 = %q, want 格式不支持的诚实提示", got)
got := lastUserContent(env)
if !strings.Contains(got, "不在服务端可转写的白名单内") {
t.Fatalf("落库正文 = %q, want 白名单外的诚实提示", got)
}
if strings.Contains(got, "当前未做") {
t.Fatalf("落库正文 = %q:转码能力已落地(design/25),这句「当前未做」是假话", got)
}
})
}