feat(channelbridge): 出站语音结论落 voice 列并挂出站 Media(M58)

- voice.Ref 增 SourceTTS 与 FallbackReason,Validate/IsZero/validSource 同步
- 出站成功:INSERT -> 补写 voice 列 -> notify(首帧即带 voice)
- 出站降级:source=tts + status=failed + fallbackReason,正文原样
- 首条气泡挂出站 Media(新开条目,不复制 target.Media)
- gateway historyVoice 投影 fallbackReason
- 新增 outbound_voice.go 与整链 e2e 测试(含变异打红验证时序)
This commit is contained in:
AskaEth committed 2026-10-27 22:08:38 +08:00
1 parent 9f6243a729
commit 28afa11aeb
13 files changed
+755 -44

No files matched your search

+3 -3
View File
@@ -83,7 +83,7 @@ func (b *Bridge) SendProactive(ctx context.Context, msg adapter.UnifiedMessage)
// 把主动消息镜像进主对话上下文(design/16
// §4.4 + §4.6.1)。否则模型看不到自己主动说了什么,
// 主人后续回复就会显得不连贯(该消息「丢失」了历史)。
// 它用的是普通回复那条写入口径(persistMain,相同的列、相同的时间戳语义),
// 它用的是普通回复那条写入口径(persistMainWith,相同的列、相同的时间戳语义),
// 作用域恒为 MainSessionID,落库的同样是**规整后**的那一份(Web 与 IM 看到的必须是
// 同一份),因此落库行为与本切片之前逐字相同;变的只有通知的第二步(见下)。
// turn_id 传 ""(零值):主动推送是**她单方面开口**,没有对应的入站回合,也就没有
@@ -97,13 +97,13 @@ func (b *Bridge) SendProactive(ctx context.Context, msg adapter.UnifiedMessage)
// id),前端按 id 去重 ⇒ 仍是一个气泡、仍只提醒一次。
//
// ② 只在**确实落了库**时发:`message_persisted` 的载荷要求权威 id,客户端拿不到 id
// 会整帧丢弃 —— 发一条没有 id 的帧等于假装提醒过。落库失败本身已由 persistMain
// 会整帧丢弃 —— 发一条没有 id 的帧等于假装提醒过。落库失败本身已由 persistMainWith
// 记 WARN(可见,不静默)。
row := history.Message{
SessionID: history.MainSessionID, Role: history.RoleAssistant, Content: out.Content,
Platform: target.platform, DeviceID: proactiveDeviceID(target), AdapterID: target.platform, Mode: modeChat,
}
if full := b.persistMain(ctx, row, b.notifyPersisted); full.ID != "" {
if full := b.persistMainWith(ctx, row, b.notifyPersisted); full.ID != "" {
b.notifyProactive(full)
}
return nil