feat: Gateway 消息排队机制 — 同会话串行化处理
同一 session 的消息按顺序处理:当前回复未完成时新消息进入队列, 完成后自动消费下一条。避免并发请求导致上下文竞争和响应交错。 客户端收到 type:"queued" 时可显示排队状态。 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"mime/multipart"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
@@ -23,6 +24,14 @@ import (
|
||||
"github.com/yourname/cyrene-ai/pkg/logger"
|
||||
)
|
||||
|
||||
// queuedMsg 队列中的待处理消息
|
||||
type queuedMsg struct {
|
||||
client *ws.Client
|
||||
mode string
|
||||
reqBody []byte
|
||||
content string
|
||||
}
|
||||
|
||||
// ChatHandler 聊天处理器
|
||||
type ChatHandler struct {
|
||||
cfg *config.Config
|
||||
@@ -30,6 +39,8 @@ type ChatHandler struct {
|
||||
sessionStore *store.SessionStore
|
||||
fileStore *store.FileStore
|
||||
upgrader websocket.Upgrader
|
||||
pending map[string][]queuedMsg // per-session message queue
|
||||
pendingMu sync.Mutex
|
||||
}
|
||||
|
||||
// NewChatHandler 创建聊天处理器
|
||||
@@ -39,6 +50,7 @@ func NewChatHandler(cfg *config.Config, hub *ws.Hub, sessionStore *store.Session
|
||||
hub: hub,
|
||||
sessionStore: sessionStore,
|
||||
fileStore: fileStore,
|
||||
pending: make(map[string][]queuedMsg),
|
||||
upgrader: websocket.Upgrader{
|
||||
ReadBufferSize: 1024,
|
||||
WriteBufferSize: 1024,
|
||||
@@ -230,8 +242,58 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
|
||||
},
|
||||
})
|
||||
|
||||
// 在 goroutine 中进行 AI-Core 调用和流式发送,避免阻塞 ReadPump
|
||||
go h.streamResponse(client, mode, reqBody, msg.Content)
|
||||
// 排队处理:同一会话的消息串行化,避免并发请求导致上下文竞争
|
||||
h.enqueueOrProcess(client, mode, reqBody, msg.Content)
|
||||
}
|
||||
|
||||
// enqueueOrProcess 将消息加入 per-session 队列,若会话空闲则立即处理
|
||||
func (h *ChatHandler) enqueueOrProcess(client *ws.Client, mode string, reqBody []byte, content string) {
|
||||
h.pendingMu.Lock()
|
||||
|
||||
if queue, busy := h.pending[client.SessionID]; busy {
|
||||
// 会话正在处理中,加入队列
|
||||
h.pending[client.SessionID] = append(queue, queuedMsg{
|
||||
client: client, mode: mode, reqBody: reqBody, content: content,
|
||||
})
|
||||
queueLen := len(h.pending[client.SessionID])
|
||||
h.pendingMu.Unlock()
|
||||
|
||||
logger.Printf("[chat] 会话 %s 正在处理中,消息已加入队列 (位置 %d)", client.SessionID, queueLen)
|
||||
client.SendMessage(ws.ServerMessage{
|
||||
Type: "queued",
|
||||
SessionID: client.SessionID,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 标记为处理中
|
||||
h.pending[client.SessionID] = nil
|
||||
h.pendingMu.Unlock()
|
||||
|
||||
go h.processQueue(client, mode, reqBody, content)
|
||||
}
|
||||
|
||||
// processQueue 处理当前消息,完成后自动消费队列中的下一条
|
||||
func (h *ChatHandler) processQueue(client *ws.Client, mode string, reqBody []byte, content string) {
|
||||
h.streamResponse(client, mode, reqBody, content)
|
||||
|
||||
// 处理队列中的后续消息
|
||||
for {
|
||||
h.pendingMu.Lock()
|
||||
queue := h.pending[client.SessionID]
|
||||
if len(queue) == 0 {
|
||||
delete(h.pending, client.SessionID)
|
||||
h.pendingMu.Unlock()
|
||||
return
|
||||
}
|
||||
next := queue[0]
|
||||
h.pending[client.SessionID] = queue[1:]
|
||||
h.pendingMu.Unlock()
|
||||
|
||||
logger.Printf("[chat] 会话 %s 从队列取出消息继续处理 (剩余 %d)", client.SessionID, len(queue)-1)
|
||||
h.streamResponse(next.client, next.mode, next.reqBody, next.content)
|
||||
}
|
||||
}
|
||||
|
||||
// streamResponse 调用 AI-Core SSE 流式接口并逐 delta 转发给客户端
|
||||
|
||||
@@ -256,6 +256,7 @@ ws://<gateway>/ws/chat?token=<jwt>&session_id=<optional>&client_id=<optional>&de
|
||||
| `pong` | 响应 ping |
|
||||
| `device_update` | IoT 设备状态更新 |
|
||||
| `background_thinking` | 后台思考状态变更 |
|
||||
| `queued` | 消息已加入处理队列(会话繁忙时) |
|
||||
|
||||
> **广播机制**:服务端推送分为两类:
|
||||
> - **用户消息回显**(`type: "response"`, `role: "user"`):通过 `SendToUserExcept` 广播,排除发送者自身(发送者本地已渲染),仅同步到同用户的其他设备。
|
||||
@@ -301,6 +302,27 @@ Client Gateway AI-Core
|
||||
|<-- {type:"stream_end"} | (含 full text) |
|
||||
```
|
||||
|
||||
### 消息排队机制
|
||||
|
||||
同一会话的消息处理为串行化队列。若上一轮 AI 回复尚未完成时用户发送新消息,新消息会加入等待队列,并在当前处理完成后自动消费。
|
||||
|
||||
```
|
||||
Client Gateway
|
||||
| |
|
||||
|-- msg1 (type:"message") --> |
|
||||
|<-- {type:"stream_start"} | → AI-Core 处理中...
|
||||
| |
|
||||
|-- msg2 (type:"message") --> |
|
||||
|<-- {type:"queued"} | → 加入队列等待
|
||||
| |
|
||||
|<-- {type:"stream_end"} (msg1) | → msg1 完成
|
||||
|<-- {type:"stream_start"} (msg2) | → 自动取出 msg2 处理
|
||||
|<-- ... |
|
||||
|<-- {type:"stream_end"} (msg2) |
|
||||
```
|
||||
|
||||
> `queued` 消息表明用户消息已接收但尚未开始处理,客户端可据此显示"排队中"状态。
|
||||
|
||||
---
|
||||
|
||||
### 语音输入流程
|
||||
|
||||
Reference in New Issue
Block a user