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:
2026-05-16 17:44:03 +08:00
parent 63513210b7
commit 186513f381
24 changed files with 1024 additions and 216 deletions
+141 -41
View File
@@ -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