feat(gateway): 语音回合进编排+落库、G5 队列满带回身份、voice_output 归属(安卓 G5/C1/C3)
- G5:pending_full 增 clientMsgId/messageId(仅在可给出时出现、绝不新铸 id —— 队列满的拒绝发生在幂等键认领之前,代铸会让客户端重试被判重复而永远进不来);另定向回发送者 {type:error,data:{code:chat_busy,...}}(与 REST 409 chat_busy 同码),广播帧照旧带同样的键供其它端对齐。
- C1:语音回合改走与文本同一条路径(编排器 Submit → 落库 → 流式 → 跨端同步),deviceId 走既有 withDeviceID、platform 走存储默认 web;未接编排器时保留旧直连路径。音频在回合结算处(persistAssistantReply → persistDocumentStatus → dispatchVoiceReply)合成并定向回原连接,回调各自 goroutine(TTS 延迟不拖住 pending_drain/session_idle);续跑 FIFO 与转向注入两条路径都带走回调 ⇒ 忙时排队/被注入的语音也不丢音频。
- C3:voice_output 增 sessionId/deviceId;三处主动加码(按最优方向):① voice_input 增可选 clientMsgId(语音也有幂等)② 不再拿 stopReason 顶替空正文当语音内容(空正文走 provider 降级信号)③ TTS 失败照旧回帧 + provider:silent(不静默)。
- 拆 chat_documents_test.go(510 行越界)→ 373 + 160,只搬不改(拆前后同 -run 过滤 PASS 集合逐条一致)。
- 主会话独立验收:隔离 worktree(HEAD + 本批次 11 文件)build/vet 绿、gateway -race 绿、全树 59 包(唯一红是既有负载 flake orchestration/TestSpawnTimeoutFromToolDefault)、linux+darwin 交叉编译绿;变异独立复现(pending_full 摘掉 clientMsgId ⇒ 3 条红)。
- 待办(已登记):Docs/api-client.md 与 api-reference.md 的帧/字段登记随 v0.59 一并落笔(本批次刻意未动 Docs,避免与契约批次打架)。
This commit is contained in:
1 parent
72b95df22b
commit
3a01b4e236
11 files changed
+1123
-170
No files matched your search
@@ -146,6 +146,9 @@ type sessionState struct {
|
||||
// 结算时把抽取结论回写进那一列(design/24 §6.1)。它按代次配对,因此被中断/
|
||||
// 取代的陈旧回合读不到本回合的条目。见 session_document_status.go。
|
||||
turnDocs turnDocuments
|
||||
// voiceTurn 是本回合的语音收尾回调(协同 C1),按代次配对;纯文本回合记为
|
||||
// 零值。见 session_voice.go。
|
||||
voiceTurn voiceTurn
|
||||
}
|
||||
|
||||
// pendingMsg 是一条已排队用户回合。它携带自己的 env,使续跑
|
||||
@@ -169,6 +172,9 @@ type pendingMsg struct {
|
||||
// 文档抽取结论(design/24 §6.1):它在入队那一刻就落了库,而那时抽取还没
|
||||
// 发生。见 session_document_status.go。
|
||||
persisted []history.Message
|
||||
// voice 是这条待处理回合的语音收尾回调(协同 C1):它源自 voice_input 时
|
||||
// 非 nil,使续跑回合结算后仍能把音频定向回发起它的那根连接。
|
||||
voice VoiceReplyFunc
|
||||
}
|
||||
|
||||
// Orchestrator 拥有每会话的消息状态机。它对并发安全
|
||||
@@ -257,8 +263,9 @@ func (o *Orchestrator) submitInternal(ctx context.Context, sessionID string, mes
|
||||
// 状态事件在这里解锁之后再发。
|
||||
a := o.admitLocked(ctx, st, sessionID)
|
||||
if a.full {
|
||||
rejected := o.rejectedIdentityLocked(ctx, st, sessionID)
|
||||
o.mu.Unlock()
|
||||
o.emitState(sessionID, EventPendingFull, map[string]any{"sessionId": sessionID, "mode": a.mode, "max": o.maxPending})
|
||||
o.emitPendingFull(ctx, sessionID, a.mode, rejected)
|
||||
return SubmitResult{PendingFull: true}, nil
|
||||
}
|
||||
if a.dup {
|
||||
@@ -272,7 +279,7 @@ func (o *Orchestrator) submitInternal(ctx context.Context, sessionID string, mes
|
||||
if st.mode != ModeIdle {
|
||||
st.pendingSeq++
|
||||
seq := st.pendingSeq
|
||||
st.pending = append(st.pending, pendingMsg{messages: messages, env: env, deviceID: deviceID, mode: msgMode, at: time.Now(), seq: seq})
|
||||
st.pending = append(st.pending, pendingMsg{messages: messages, env: env, deviceID: deviceID, mode: msgMode, at: time.Now(), seq: seq, voice: voiceReplySinkFrom(ctx)})
|
||||
depth := len(st.pending)
|
||||
mode := st.mode
|
||||
o.mu.Unlock()
|
||||
@@ -305,6 +312,8 @@ func (o *Orchestrator) submitInternal(ctx context.Context, sessionID string, mes
|
||||
// 且落库只写**引用** —— 抽取还发生在后面,结论由回合结算时补进同一列
|
||||
// (两段式见 session_document_status.go)。
|
||||
o.rememberTurnDocuments(sessionID, gen, o.persistUserMessages(ctx, sessionID, messages, deviceID, msgMode, env.PersonaID))
|
||||
// 语音收尾回调同样按代次配对(协同 C1,见 session_voice.go)。
|
||||
o.rememberVoiceTurn(sessionID, gen, voiceReplySinkFrom(ctx))
|
||||
|
||||
if sync {
|
||||
return o.runImmediateSync(runCtx, sessionID, gen, turn, env, fence, deviceID, msgMode, a.hint.MessageID)
|
||||
@@ -357,10 +366,12 @@ func (o *Orchestrator) goTurn(sessionID string, gen uint64, messages []model.Mes
|
||||
//
|
||||
// 它同时是文档抽取结论回写的落点(design/24 §6.1):本回合用户消息的
|
||||
// document 列里此前只有引用,装配期解析出的 status/chars/truncated 在这里补上
|
||||
// —— 见 session_document_status.go 的两段式说明。
|
||||
// —— 见 session_document_status.go;语音回合的音频回复同样在此收尾(协同 C1,
|
||||
// 见 session_voice.go):只有这里知道这个回合真正产出了什么。
|
||||
func (o *Orchestrator) completeTurn(sessionID string, gen uint64, res *loop.Result, runErr error, deviceID, mode, personaID string) error {
|
||||
o.persistAssistantReply(sessionID, res, deviceID, mode, personaID)
|
||||
o.persistDocumentStatus(sessionID, gen, res)
|
||||
o.dispatchVoiceReply(sessionID, gen, res)
|
||||
o.mu.Lock()
|
||||
st := o.states[sessionID]
|
||||
if st == nil || st.gen != gen {
|
||||
@@ -389,6 +400,9 @@ func (o *Orchestrator) completeTurn(sessionID string, gen uint64, res *loop.Resu
|
||||
// 续跑的这一轮同样有「回合前已落库的用户消息」,因此把它们的文档状态
|
||||
// 记到新代次上(design/24 §6.1)。
|
||||
st.turnDocs = turnDocuments{gen: gen, msgs: next.persisted}
|
||||
// 续跑的这条若源自语音请求,它的音频回复照旧定向回发起它的那根连接
|
||||
// (协同 C1:忙时排队不许把音频丢掉)。
|
||||
st.voiceTurn = voiceTurn{gen: gen, sinks: voiceSinksOf(next.voice)}
|
||||
depth := len(st.pending)
|
||||
resume := append([]model.Message(nil), st.tail...)
|
||||
env := next.env
|
||||
|
||||
Reference in new issue
Block a user