feat(gateway): M44 实时帧与握手回放挂上 voice(纯增量)
修实根:`message_persisted` / `channel_persisted` 与握手 `history` 回放
此前只在 `toHistoryMessage` 之后直接扇出,于是消息 DTO 的语音面在这三条
面上恒缺席,客户端只能等下一次 REST 拉取才知道那是一条语音。
- `history_voice.go`:把语音补齐抽成存储无关的唯一收口点 `attachVoiceTo`
(Server 与 hub 共用),并加 `voiceColumnsOf` 把任意历史视图窄化成读面,
未实现该视图时为真 nil 接口(字段缺席,不 panic、不 500)。
- `broadcast.go`:新增 `persistedWire` —— thinking 抹除 + 可选 voice,
`broadcastPersisted` 改用它;两类帧因此天然同口径。
- `ws_history.go`:握手回放同样补一次批量读列(最多 50 个 id 一条 SQL)。
成本实测:一帧 = 1 次 `IN (?)` 批量读列(1 个 id);回放一页 = 1 次
(去重后的全部 id);存储未提供该视图时 0 次。`history.Message` 没有
voice 列字段(`messageColumns` 也不含),故无法「零查询直接填」——补它
要改 internal/runtime/history 与 INSERT 写路径,超出本切片范围。
纯增量:voice 是可选键,非语音消息整键缺席(不是 null、不是 {});
帧壳、帧类型、REST DTO 形状一律未动。
测试(广播_voice_test.go):帧序列化输出上断言 ①②③④,并逐字比对
「帧/回放里的 voice」与「REST 读面同一行」;摘掉任一挂载即打红。
This commit is contained in:
1 parent
af61b8ccfe
commit
5717735da9
4 files changed
+507
-19
No files matched your search
@@ -20,13 +20,21 @@ package gateway
|
||||
// 没有「读不出」这一态,因此缺席是唯一诚实的表达(与文档列回一条 `unreadable`
|
||||
// 不同:文档有那个态,语音没有)。
|
||||
//
|
||||
// 覆盖范围(**如实**):本文件的补齐点挂在会用 DTO 的读侧出口上(会话消息页、渠道
|
||||
// 消息日志两个端点、编辑消息的响应与其广播帧)。**尚未覆盖**:`message_persisted` /
|
||||
// `channel_persisted`(`broadcast.go`)与握手 `history` 回放(`ws_history.go`)——
|
||||
// 前者还额外需要「随 INSERT 写入」的写路径(设计 §10.5 的首选口径),见交付报告。
|
||||
// 覆盖范围(**如实**):本文件的补齐点挂在**所有**会用 DTO 的出口上 —— 会话消息页、
|
||||
// 渠道消息日志两个端点、编辑消息的响应与其 `message_edited` 帧、`message_persisted` /
|
||||
// `channel_persisted` 实时帧(`broadcast.go` 的 persistedWire)与握手 `history` 回放
|
||||
// (`ws_history.go`)。三条收口点共用本文件这一份投影,因此「同一条消息在 REST 读面与
|
||||
// 实时帧 / 回放里的 `voice` 对象」逐字相同,不会两处各算一份。
|
||||
//
|
||||
// ⚠️ 实时帧那条路的**已知边界**(不在本文件能修的范围内):入站语音的 `voice` 列由
|
||||
// 渠道桥在**消息落库之后**回填(channelbridge 的两段式),而 `message_persisted` 那一帧
|
||||
// 在**落库那一刻**就发出去了 ⇒ 这一帧读到的列仍是空串、`voice` 依旧缺席;紧随其后的
|
||||
// 握手回放 / REST 拉取才带上它。要让「刚落库的那一帧」也带上,须把渠道桥的两段式顺序
|
||||
// 倒过来(先写列、再 notify),见交付报告与 Docs §4.4 的同一处如实登记。
|
||||
|
||||
import (
|
||||
"context"
|
||||
"log/slog"
|
||||
"strings"
|
||||
|
||||
voiceref "git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/voice"
|
||||
@@ -65,24 +73,30 @@ type voiceColumnReader interface {
|
||||
VoiceColumns(ctx context.Context, ids []string) (map[string]string, error)
|
||||
}
|
||||
|
||||
// attachVoice 给一页消息补上各自的语音面(就地写进 DTO 切片)。它只查页内**非空**
|
||||
// 的消息 id,且**一次批量**(页内 id 集合交给存储侧一条 SQL 查完)。
|
||||
// attachVoice 给一页消息补上各自的语音面(就地写进 DTO 切片)。
|
||||
func (s *Server) attachVoice(ctx context.Context, msgs []historyMessage) {
|
||||
attachVoiceTo(ctx, s.log, voiceColumnsOf(s.history), msgs)
|
||||
}
|
||||
|
||||
// attachVoiceTo 是语音面的**唯一**收口点:REST 读面、实时帧与握手回放都调它,因此三处
|
||||
// 对同一条消息给出**同一个**投影(`historyVoiceOf`),不存在「REST 带、帧不带」的分叉。
|
||||
//
|
||||
// 它只查页内**非空**的消息 id,且**一次批量**(页内 id 集合交给存储侧一条 `IN (...)`
|
||||
// 查完)—— 成本与「一页 / 一帧」同量级,不是逐条查。
|
||||
//
|
||||
// reader 为 nil(存储未提供这一面:未接线 / 测试替身)或 log 为 nil 时都只让这一面整体
|
||||
// 缺席:这条路径上「问不到」不是错误,帧与响应照常发出。
|
||||
//
|
||||
// 失败处理与工具回看面同一纪律:查不到就只让这一面缺席并记 WARN —— 消息本身已经
|
||||
// 读到了,让一个可选列的故障把整页历史变成 500 是反的。
|
||||
func (s *Server) attachVoice(ctx context.Context, msgs []historyMessage) {
|
||||
func attachVoiceTo(ctx context.Context, log *slog.Logger, reader voiceColumnReader, msgs []historyMessage) {
|
||||
ids := pageMessageIDs(msgs)
|
||||
if len(ids) == 0 {
|
||||
return
|
||||
}
|
||||
reader, ok := s.history.(voiceColumnReader)
|
||||
if !ok {
|
||||
// 存储未提供这一面(未接线 / 测试替身):整体缺席,不是错误。
|
||||
if len(ids) == 0 || reader == nil || log == nil {
|
||||
return
|
||||
}
|
||||
cols, err := reader.VoiceColumns(ctx, ids)
|
||||
if err != nil {
|
||||
s.log.Warn("history voice unavailable", "err", err, "messages", len(ids))
|
||||
log.Warn("history voice unavailable", "err", err, "messages", len(ids))
|
||||
return
|
||||
}
|
||||
for i := range msgs {
|
||||
@@ -94,13 +108,24 @@ func (s *Server) attachVoice(ctx context.Context, msgs []historyMessage) {
|
||||
if err != nil {
|
||||
// 坏列**可见地**缺席:绝不静默当成「不是语音」。日志只带消息 id 与错误,
|
||||
// 绝不带列内容或正文。
|
||||
s.log.Warn("history voice column malformed", "err", err, "message", msgs[i].ID)
|
||||
log.Warn("history voice column malformed", "err", err, "message", msgs[i].ID)
|
||||
continue
|
||||
}
|
||||
msgs[i].Voice = historyVoiceOf(ref)
|
||||
}
|
||||
}
|
||||
|
||||
// voiceColumnsOf 把任意历史存储视图窄化成语音列读面;没实现时返回**真 nil 接口**
|
||||
// (`_, ok` 形式让未实现的存储得到 nil,而不是一个非 nil 的 nil 指针 —— 后者会在
|
||||
// 调用点炸成 panic)。Server 与 hub 持有的 history 字段同源,因此两处同一结果。
|
||||
func voiceColumnsOf(store any) voiceColumnReader {
|
||||
reader, ok := store.(voiceColumnReader)
|
||||
if !ok {
|
||||
return nil
|
||||
}
|
||||
return reader
|
||||
}
|
||||
|
||||
// pageMessageIDs 收集页内非空且**互不相同**的消息 id(保持首次出现顺序)。去重让
|
||||
// 同一页的「提问 + 回答」或编辑场景里的两份 DTO 共用同一次查询。
|
||||
func pageMessageIDs(msgs []historyMessage) []string {
|
||||
|
||||
Reference in new issue
Block a user