feat: 多功能升级 — 流式逐字渲染、对话缓存、会话组织优化、记忆管理修复、性能仪表盘
- 前端消息流式逐字渲染 (AI-Core ChatStream → SSE → Gateway → WebSocket stream_chunk → fadeInUp + cursorBlink) - 后端对话缓存 (conversationCache sync.Map, GET /sessions/:id/messages) - 前端侧边栏历史多轮对话显示 - DevTools 性能监控图标移至首页仪表盘 - DevTools 用户记忆查询/删减功能修复 (补全 DELETE 数据链路) - 后端和 DevTools 按用户分类组织实时活动会话 (map[userID]map[sessionID]*Client) - 新增 docs/api-reference/ 路由参考文档 - 新增 docs/message-flow-architecture.md 消息链路架构文档
This commit is contained in:
@@ -1,19 +1,20 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"log"
|
||||
"net/http"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"github.com/yourname/cyrene-ai/gateway/internal/config"
|
||||
"github.com/yourname/cyrene-ai/gateway/internal/middleware"
|
||||
"github.com/yourname/cyrene-ai/gateway/internal/ws"
|
||||
)
|
||||
|
||||
@@ -96,12 +97,14 @@ func (h *ChatHandler) handleMessage(client *ws.Client, msg ws.ClientMessage) {
|
||||
h.handleChatMessage(client, msg)
|
||||
case "voice_input":
|
||||
h.handleVoiceInput(client, msg)
|
||||
case "history":
|
||||
h.handleHistoryRequest(client, msg)
|
||||
default:
|
||||
log.Printf("[WS] 未知消息类型: %s from user=%s", msg.Type, client.UserID)
|
||||
}
|
||||
}
|
||||
|
||||
// handleChatMessage 处理文字聊天消息 - 转发到 AI-Core
|
||||
// handleChatMessage 处理文字聊天消息 - 转发到 AI-Core(流式发送)
|
||||
func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage) {
|
||||
mode := msg.Mode
|
||||
if mode == "" {
|
||||
@@ -134,7 +137,19 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
|
||||
return
|
||||
}
|
||||
|
||||
// 调用 AI-Core
|
||||
// 缓存用户消息(在 goroutine 前完成,避免竞态)
|
||||
h.hub.CacheMessage(client.UserID, client.SessionID, ws.Message{
|
||||
Role: "user",
|
||||
Content: msg.Content,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
|
||||
// 在 goroutine 中进行 AI-Core 调用和流式发送,避免阻塞 ReadPump
|
||||
go h.streamResponse(client, mode, reqBody, msg.Content)
|
||||
}
|
||||
|
||||
// streamResponse 调用 AI-Core SSE 流式接口并逐 delta 转发给客户端
|
||||
func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []byte, userMsg string) {
|
||||
aiCoreURL := h.cfg.AICoreURL + "/api/v1/chat"
|
||||
httpReq, err := http.NewRequest("POST", aiCoreURL, bytes.NewReader(reqBody))
|
||||
if err != nil {
|
||||
@@ -149,6 +164,7 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
|
||||
return
|
||||
}
|
||||
httpReq.Header.Set("Content-Type", "application/json")
|
||||
httpReq.Header.Set("Accept", "text/event-stream")
|
||||
|
||||
httpClient := &http.Client{Timeout: 120 * time.Second}
|
||||
resp, err := httpClient.Do(httpReq)
|
||||
@@ -165,20 +181,8 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
log.Printf("[chat] 读取 AI-Core 响应失败: %v", err)
|
||||
h.hub.UpdateSessionState(client.SessionID, "error")
|
||||
client.SendMessage(ws.ServerMessage{
|
||||
Type: "error",
|
||||
MessageID: "msg_" + generateID(),
|
||||
Error: "读取响应失败",
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
log.Printf("[chat] AI-Core 返回错误 [%d]: %s", resp.StatusCode, string(body))
|
||||
h.hub.UpdateSessionState(client.SessionID, "error")
|
||||
client.SendMessage(ws.ServerMessage{
|
||||
@@ -190,42 +194,115 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
|
||||
return
|
||||
}
|
||||
|
||||
// 解析 AI-Core 响应
|
||||
var aiResp struct {
|
||||
Text string `json:"text"`
|
||||
Mode string `json:"mode"`
|
||||
MessageID string `json:"message_id"`
|
||||
// 使用 bufio.Scanner 逐行读取 SSE 响应
|
||||
scanner := bufio.NewScanner(resp.Body)
|
||||
// 增大 scanner buffer 以处理大块 SSE 数据
|
||||
scanner.Buffer(make([]byte, 0, 64*1024), 1024*1024)
|
||||
|
||||
var fullText string
|
||||
var msgID string
|
||||
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
// 跳过非 data 行
|
||||
if !strings.HasPrefix(line, "data: ") {
|
||||
continue
|
||||
}
|
||||
|
||||
data := strings.TrimPrefix(line, "data: ")
|
||||
|
||||
// SSE 流结束标记
|
||||
if data == "[DONE]" {
|
||||
break
|
||||
}
|
||||
|
||||
// 解析 delta 数据
|
||||
var chunk struct {
|
||||
Delta string `json:"delta"`
|
||||
Error string `json:"error,omitempty"`
|
||||
MessageID string `json:"message_id,omitempty"`
|
||||
Done bool `json:"done,omitempty"`
|
||||
}
|
||||
if err := json.Unmarshal([]byte(data), &chunk); err != nil {
|
||||
log.Printf("[chat] 解析 SSE delta 失败: %v, raw=%s", err, data)
|
||||
continue
|
||||
}
|
||||
|
||||
// 错误处理
|
||||
if chunk.Error != "" {
|
||||
log.Printf("[chat] AI-Core 流式错误: %s", chunk.Error)
|
||||
h.hub.UpdateSessionState(client.SessionID, "error")
|
||||
client.SendMessage(ws.ServerMessage{
|
||||
Type: "error",
|
||||
MessageID: "msg_" + generateID(),
|
||||
Error: chunk.Error,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 记录 message_id
|
||||
if chunk.MessageID != "" {
|
||||
msgID = chunk.MessageID
|
||||
}
|
||||
|
||||
// 如果是结束标记(含 done: true),跳出
|
||||
if chunk.Done {
|
||||
break
|
||||
}
|
||||
|
||||
// 逐 delta 转发
|
||||
if chunk.Delta != "" {
|
||||
fullText += chunk.Delta
|
||||
|
||||
client.SendMessage(ws.ServerMessage{
|
||||
Type: "stream_chunk",
|
||||
MessageID: msgID,
|
||||
Content: chunk.Delta,
|
||||
Role: "assistant",
|
||||
SessionID: client.SessionID,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
}
|
||||
}
|
||||
if err := json.Unmarshal(body, &aiResp); err != nil {
|
||||
log.Printf("[chat] 解析 AI-Core 响应失败: %v", err)
|
||||
|
||||
if err := scanner.Err(); err != nil {
|
||||
log.Printf("[chat] SSE 读取错误: %v", err)
|
||||
h.hub.UpdateSessionState(client.SessionID, "error")
|
||||
client.SendMessage(ws.ServerMessage{
|
||||
Type: "error",
|
||||
MessageID: "msg_" + generateID(),
|
||||
Error: "解析响应失败",
|
||||
Error: fmt.Sprintf("流读取错误: %v", err),
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 记录助手响应
|
||||
h.hub.RecordMessage(client.SessionID, "assistant", aiResp.Text)
|
||||
if msgID == "" {
|
||||
msgID = "msg_" + generateID()
|
||||
}
|
||||
|
||||
// 发送 stream_end
|
||||
client.SendMessage(ws.ServerMessage{
|
||||
Type: "stream_end",
|
||||
MessageID: msgID,
|
||||
SessionID: client.SessionID,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
|
||||
// 缓存完整响应
|
||||
if fullText != "" {
|
||||
h.hub.CacheMessage(client.UserID, client.SessionID, ws.Message{
|
||||
Role: "assistant",
|
||||
Content: fullText,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
})
|
||||
}
|
||||
h.hub.RecordMessage(client.SessionID, "assistant", fullText)
|
||||
|
||||
// 设置会话状态为 idle
|
||||
h.hub.UpdateSessionState(client.SessionID, "idle")
|
||||
|
||||
// 发送响应给客户端
|
||||
response := ws.ServerMessage{
|
||||
Type: "response",
|
||||
MessageID: aiResp.MessageID,
|
||||
Text: aiResp.Text,
|
||||
ResponseMode: mode,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
if err := client.SendMessage(response); err != nil {
|
||||
log.Printf("[WS] 发送响应失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// handleVoiceInput 处理语音输入
|
||||
@@ -241,6 +318,31 @@ func (h *ChatHandler) handleVoiceInput(client *ws.Client, msg ws.ClientMessage)
|
||||
}
|
||||
|
||||
|
||||
// handleHistoryRequest 处理历史消息请求
|
||||
func (h *ChatHandler) handleHistoryRequest(client *ws.Client, msg ws.ClientMessage) {
|
||||
// 优先使用请求中的 session_id,否则使用客户端的 session_id
|
||||
sessionID := msg.SessionID
|
||||
if sessionID == "" {
|
||||
sessionID = client.SessionID
|
||||
}
|
||||
|
||||
messages := h.hub.GetConversation(client.UserID, sessionID)
|
||||
if messages == nil {
|
||||
messages = []ws.Message{}
|
||||
}
|
||||
|
||||
response := ws.ServerMessage{
|
||||
Type: "history_response",
|
||||
MessageID: "hist_" + generateID(),
|
||||
Messages: messages,
|
||||
Timestamp: time.Now().UnixMilli(),
|
||||
}
|
||||
|
||||
if err := client.SendMessage(response); err != nil {
|
||||
log.Printf("[WS] 发送历史消息失败: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
// SendSystemMessage 向用户发送系统消息(用于主动通知)
|
||||
func (h *ChatHandler) SendSystemMessage(userID, sessionID, text string) error {
|
||||
msg := ws.ServerMessage{
|
||||
@@ -272,5 +374,3 @@ func randomStr(n int) string {
|
||||
return string(b)
|
||||
}
|
||||
|
||||
// 确保未使用变量不报错
|
||||
var _ = middleware.GetUserID
|
||||
|
||||
Reference in New Issue
Block a user