fix(channelbridge): M44 入站语音先写 voice 列再广播 ⇒ 首帧就带 voice
安卓 2026-10-26 回执关心的场景:新来一条语音消息时,**第一条**帧里有没有
`voice`。此前没有 —— `persistBatchUsers` 先 `persistToMain`/`persistIsolated`
(内部同步 notify 把帧发出去),之后才 `persistMessageVoice` 写列,帧发出那
一刻列还是空串。网关侧的帧投影(上一 commit)读的就是这一列,因此首帧必然
缺 voice,客户端要等重连或下一次 REST 拉取。
修法(A 路的唯一可行形态,理由见下):
- `persistBatchUsers` 的通知改成 `persistVoiceFirstNotify(ref)`:
**INSERT → 写 voice 列 → notify**。语句数不变,只把 notify 从 INSERT 内部
挪到最后;非语音消息(ref 零值)时该包装与 `notifyPersisted` 逐字等价。
- `persist.go` 抽出 `mainRow` / `isolatedRow`(字段清单只写一次)与
`persistIsolatedWith`(可注入通知的变体),`persistToMain`/
`persistIsolated` 的对外行为逐字不变。
为什么不走 B(给 `history.Message` 加 voice 随 INSERT 写):`persistMessageVoice`
是按 id 的 UPDATE,行还不存在时必然 `ErrMessageNotFound` ⇒ 写列**不可能**真正
挪到 INSERT 之前;可行的最小形态就是上面那次换序,无需动 domain 类型、列清单
与 7 条读语句。B 若要做,属于 design/24 §10.5 的独立切片(那时还应删掉这里的
两段式,避免同一事实两处写)。
整链验收(`inbound_voice_gateway_e2e_test.go`,真桥接器 + 真存储 + 真网关 WS):
- 主人私聊语音:第一帧就是 `message_persisted`,带 `voice`,且与
`GET /sessions/{id}/messages` 同一行**逐字相等**;随后的非语音消息帧整键缺席。
- 群聊语音(被点名 ⇒ 走完整回合):第一帧 `channel_persisted` 带 `voice`,
且与渠道日志读面同一行逐字相等。
- 变异实测(把顺序还原成「先广播、后写列」):两条用例都 FAIL,输出即报障症状
(帧里 content 是转写文本,却没有 voice 键)。
顺带把网关侧两处描述旧时序的注释改成新事实。
This commit is contained in:
1 parent
5717735da9
commit
b955a27479
6 files changed
+416
-19
No files matched your search
@@ -141,21 +141,32 @@ func dropLeadingSystem(msgs []model.Message) []model.Message {
|
||||
|
||||
// persistBatchUsers 按**到达顺序**逐条落 user 行(共享 turnID),并把语音事实与用户
|
||||
// 消息同源落库(design/24 §10.2 第 5 步)。返回刚落库的行,供回合结束后补抽取结论。
|
||||
//
|
||||
// 🔴 顺序(M44,主人 2026-10-26 指令):通知 = **先写 `voice` 列、再广播**。此前是
|
||||
// 「先广播、后写列」,于是这条语音消息的**第一帧**(`message_persisted` /
|
||||
// `channel_persisted`)里读不到 voice,客户端得靠重连或下一次 REST 拉取才升级 ——
|
||||
// 而「实时帧一到就出播放器」正是本切片的诉求。换序后帧发出时列已写好。
|
||||
//
|
||||
// 之所以不能把写列真正挪到落库**之前**:`persistMessageVoice` 是按 id 的 UPDATE,
|
||||
// 行还不存在时它必然 ErrMessageNotFound(`voice` 列也没有随 INSERT 写入的通道,
|
||||
// 见 design/24 §10.5 的两种口径)。因此可行的最小形态就是「INSERT → UPDATE 列 →
|
||||
// notify」——语句数不变,只把 notify 从 INSERT 内部挪到最后。
|
||||
func (b *Bridge) persistBatchUsers(ctx context.Context, parts []batchMessage, target inboundTarget, turnID string) []history.Message {
|
||||
rows := make([]history.Message, 0, len(parts))
|
||||
for i := range parts {
|
||||
p := &parts[i]
|
||||
msg := p.prepared.item.msg
|
||||
a := p.prepared.item.a
|
||||
// 非语音消息(p.voiceRef 零值)时这个包装与 notifyPersisted 逐字等价。
|
||||
notify := b.persistVoiceFirstNotify(p.voiceRef)
|
||||
var row history.Message
|
||||
if target.private {
|
||||
row = b.persistToMain(ctx, history.RoleUser, p.prepared.text, "", p.document, p.image,
|
||||
target.platform, deviceIDFor(msg), adapterName(a), turnID)
|
||||
row = b.persistMain(ctx, mainRow(history.RoleUser, p.prepared.text, "", p.document, p.image,
|
||||
target.platform, deviceIDFor(msg), adapterName(a), turnID), notify)
|
||||
} else {
|
||||
row = b.persistIsolated(ctx, history.RoleUser, p.prepared.text, p.document, p.image,
|
||||
deviceIDFor(msg), p.prepared.sess, msg.Platform, p.senderName, adapterName(a), turnID)
|
||||
row = b.persistIsolatedWith(ctx, isolatedRow(history.RoleUser, p.prepared.text, p.document, p.image,
|
||||
deviceIDFor(msg), p.prepared.sess, msg.Platform, p.senderName, adapterName(a), turnID), notify)
|
||||
}
|
||||
b.persistMessageVoice(row, p.voiceRef)
|
||||
rows = append(rows, row)
|
||||
}
|
||||
return rows
|
||||
|
||||
Reference in new issue
Block a user