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
@@ -1,6 +1,7 @@
|
||||
package gateway
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
|
||||
"git.yeij.top/AskaEth/Cyrene_v2/internal/runtime/history"
|
||||
@@ -36,10 +37,10 @@ func (s *Server) BroadcastPersisted(msg history.Message) {
|
||||
// 帧的 data 另带**可选**的 `unreadCount`(契约 v0.76):该会话此刻的绝对未读数,
|
||||
// 与 GET /sessions 的 `unreadCount` 同一口径。它只在 >0 且真能算出来时出现(见
|
||||
// unreadCountOf),缺席即老行为。Web 路径与 IM 路径共用本函数 ⇒ 两处口径天然一致,
|
||||
// 不存在「Web 帧带、IM 帧不带」的分叉。
|
||||
// 不存在「Web 帧带、IM 帧不带」的分叉。帧内的 `message` 对象另带**可选**的 `voice`
|
||||
// (契约 v0.67 §4.4;见 persistedWire):非语音消息整键缺席,因此老客户端逐字不变。
|
||||
func (s *Server) broadcastPersisted(msg history.Message, proactive bool) {
|
||||
wire := toHistoryMessage(msg)
|
||||
wire.Thinking = ""
|
||||
wire := s.persistedWire(msg)
|
||||
typ := "channel_persisted"
|
||||
if msg.SessionID == history.MainSessionID || strings.HasPrefix(msg.SessionID, history.MainSessionID+":") {
|
||||
typ = "message_persisted"
|
||||
@@ -58,6 +59,25 @@ func (s *Server) broadcastPersisted(msg history.Message, proactive bool) {
|
||||
s.hub.broadcast(wsEvent{Type: typ, SessionID: msg.SessionID, Data: data})
|
||||
}
|
||||
|
||||
// persistedWire 把一条待广播的消息投影成帧内的 `message` 对象:thinking 抹除(与 WS
|
||||
// 历史回放一致,design/16 §6)+ 可选的语音面(契约 v0.67 §4.4)。
|
||||
//
|
||||
// 语音面与 REST 读面共用**同一个**收口点(attachVoiceTo → historyVoiceOf),因此
|
||||
// 「同一行消息在帧里与 GET /sessions/{id}/messages 里」是逐字相同的对象,客户端不必
|
||||
// 再拉一次 REST 才认出语音气泡。
|
||||
//
|
||||
// 代价(实测,见交付报告):每帧**一次**批量读列(`IN (?)` 一条 SQL,页内 id 去重),
|
||||
// 与本路径既有的 unreadCountOf 查询同量级;存储未提供该视图时**零**查询、字段缺席。
|
||||
// 之所以要查库而不是「从 history.Message 直接填」:那个结构体没有 voice 列字段
|
||||
// (`messageColumns` 也不含它),补它要改 `internal/runtime/history` 与 INSERT 写路径
|
||||
// ——那是本切片的范围之外(见交付报告的跨片接缝)。
|
||||
func (s *Server) persistedWire(msg history.Message) historyMessage {
|
||||
one := []historyMessage{toHistoryMessage(msg)}
|
||||
one[0].Thinking = ""
|
||||
attachVoiceTo(context.Background(), s.log, voiceColumnsOf(s.history), one)
|
||||
return one[0]
|
||||
}
|
||||
|
||||
// wsCapacityAwaken 是能力状态迁移的 WS 事件类型(design/14 §5)。
|
||||
// 其 data 字段是前端的 AwakenPayload {fromState,toState,tool}。
|
||||
const wsCapacityAwaken = "capacity_awaken"
|
||||
|
||||
Reference in new issue
Block a user