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
@@ -55,8 +55,9 @@ func (c *wsConn) route(in wsInbound) {
|
||||
c.handleInterrupt(in.Data)
|
||||
case "voice_input":
|
||||
// 语音经注入的 STT→chat→TTS 管线处理。没有 voice service 时回一个
|
||||
// 明确的错误,而不是把帧丢掉。音频回复作为 voice_output 事件发在
|
||||
// 发起请求的那根连接上。
|
||||
// 明确的错误,而不是把帧丢掉。转写之后的**回合**与文本同一条路(协同
|
||||
// C1:编排器 → 落库 → 流式 → 跨端同步),音频回复在回合结算时作为
|
||||
// voice_output 事件定向回发起请求的那根连接。
|
||||
c.handleVoiceInput(in)
|
||||
case "subagent_ask":
|
||||
c.handleSubagentAsk(in)
|
||||
@@ -152,6 +153,28 @@ func claimRole(claims *auth.Claims) string {
|
||||
return string(claims.Role)
|
||||
}
|
||||
|
||||
// wsChatBusyCode 是待处理队列满时定向回发送者的机器可读错误码。它与 REST
|
||||
// 同步/流式路径的 409 `chat_busy` **同码**:客户端用同一个 code 处理两条入口的
|
||||
// 「队列满」,不必为 WS 再造一套判定(协同 G5)。
|
||||
const wsChatBusyCode = "chat_busy"
|
||||
|
||||
// notifyPendingFull 定向回一条拒绝帧,告诉发送这条消息的连接「会话正忙、待处理
|
||||
// 消息已满、本条未入队」,并带上能对齐本地乐观气泡的键(协同 G5):clientMsgId
|
||||
// 原样带回(未携带该键时不伪造),messageId 只在该键此前已落库/已认领时出现。
|
||||
//
|
||||
// 它只到发送者、不广播:广播的 pending_full 是状态信号,其余连接按 clientMsgId
|
||||
// 自行忽略即可。
|
||||
func (c *wsConn) notifyPendingFull(sessionID, clientMsgID, messageID string) {
|
||||
data := map[string]any{"code": wsChatBusyCode, "message": "会话正忙,待处理消息已满,本条未入队", "sessionId": sessionID}
|
||||
if clientMsgID != "" {
|
||||
data["clientMsgId"] = clientMsgID
|
||||
}
|
||||
if messageID != "" {
|
||||
data["messageId"] = messageID
|
||||
}
|
||||
c.enqueueJSON(wsEvent{Type: "error", Data: data})
|
||||
}
|
||||
|
||||
// submitChat 把一条 chat 上行帧直接交给会话编排器(design/15 §2.1/§2.2),
|
||||
// 而不是经 bus 转发:会话消息不得依赖可丢弃的通道(见 route 的 chat 分支)。
|
||||
// 解码、角色解析与用户消息落库都在该 goroutine 上完成,因此读泵绝不等待
|
||||
@@ -168,6 +191,8 @@ func (c *wsConn) submitChat(payload any) {
|
||||
return
|
||||
}
|
||||
// 回合自身的取消由编排器持有(Interrupt/InterruptAll 取消它),
|
||||
// 因此这里的基 ctx 只需是存活到回合结束的背景上下文。
|
||||
go c.h.streamChat(context.Background(), payload)
|
||||
// 因此这里的基 ctx 只需是存活到回合结束的背景上下文。队列满的拒绝要定向回
|
||||
// **这条**连接(协同 G5):编排器只认 ctx 上给出的回调(REST 走 409,不接)。
|
||||
ctx := withPendingFullNotifier(context.Background(), c.notifyPendingFull)
|
||||
go c.h.streamChat(ctx, payload)
|
||||
}
|
||||
Reference in new issue
Block a user