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
+93
View File
@@ -1,6 +1,7 @@
package ws
import (
"fmt"
"log"
"sync"
"time"
@@ -24,6 +25,13 @@ type SessionMessage struct {
Timestamp int64 `json:"timestamp"`
}
// Message 完整对话消息(用于缓存)
type Message struct {
Role string `json:"role"`
Content string `json:"content"`
Timestamp int64 `json:"timestamp"`
}
const maxRecentMessages = 20
// Hub WebSocket连接池
@@ -39,6 +47,10 @@ type Hub struct {
// 会话状态追踪 (sessionID -> SessionState)
sessions map[string]*SessionState
// 对话缓存:key = "userID:sessionID", value = []Message
conversationCache sync.Map
convCacheMu sync.Mutex
}
// NewHub 创建WebSocket Hub
@@ -203,6 +215,42 @@ func (h *Hub) GetActiveSessions() []*SessionState {
return result
}
// GetActiveSessionsByUser 返回按用户分组的活跃会话列表
func (h *Hub) GetActiveSessionsByUser() map[string][]*SessionState {
h.mu.RLock()
defer h.mu.RUnlock()
result := make(map[string][]*SessionState)
for _, s := range h.sessions {
cp := *s
cp.RecentMessages = nil
result[s.UserID] = append(result[s.UserID], &cp)
}
return result
}
// GetUserSessions 获取某用户的所有活跃会话
func (h *Hub) GetUserSessions(userID string) []*SessionState {
h.mu.RLock()
defer h.mu.RUnlock()
var result []*SessionState
if clients, ok := h.userClients[userID]; ok {
seen := make(map[string]bool)
for c := range clients {
if !seen[c.SessionID] {
if s, ok := h.sessions[c.SessionID]; ok {
cp := *s
cp.RecentMessages = nil
result = append(result, &cp)
seen[c.SessionID] = true
}
}
}
}
return result
}
// GetSession 返回指定会话的详细信息(含最近消息)
func (h *Hub) GetSession(sessionID string) *SessionState {
h.mu.RLock()
@@ -263,3 +311,48 @@ func (h *Hub) RecordMessage(sessionID, role, content string) {
s.RecentMessages = s.RecentMessages[len(s.RecentMessages)-maxRecentMessages:]
}
}
// ========== 对话缓存方法 ==========
// cacheKey 生成对话缓存 key
func cacheKey(userID, sessionID string) string {
return fmt.Sprintf("%s:%s", userID, sessionID)
}
// CacheMessage 缓存单条消息到对话历史
func (h *Hub) CacheMessage(userID, sessionID string, msg Message) {
key := cacheKey(userID, sessionID)
h.convCacheMu.Lock()
defer h.convCacheMu.Unlock()
existing, _ := h.conversationCache.Load(key)
var messages []Message
if existing != nil {
messages = existing.([]Message)
}
messages = append(messages, msg)
h.conversationCache.Store(key, messages)
}
// GetConversation 获取完整对话历史
func (h *Hub) GetConversation(userID, sessionID string) []Message {
key := cacheKey(userID, sessionID)
val, ok := h.conversationCache.Load(key)
if !ok {
return []Message{}
}
messages, ok := val.([]Message)
if !ok {
return []Message{}
}
return messages
}
// DeleteConversation 删除对话缓存
func (h *Hub) DeleteConversation(userID, sessionID string) {
key := cacheKey(userID, sessionID)
h.conversationCache.Delete(key)
}
+19 -15
View File
@@ -2,25 +2,29 @@ package ws
// 客户端 → 服务端消息
type ClientMessage struct {
Type string `json:"type"` // message | voice_input | ping
SessionID string `json:"session_id"`
Mode string `json:"mode"` // text | voice_msg | voice_assistant
Content string `json:"content"`
AudioData string `json:"audio_data,omitempty"` // base64
Timestamp int64 `json:"timestamp"`
Type string `json:"type"` // message | voice_input | ping | history
SessionID string `json:"session_id"`
Mode string `json:"mode"` // text | voice_msg | voice_assistant
Content string `json:"content"`
AudioData string `json:"audio_data,omitempty"` // base64
Timestamp int64 `json:"timestamp"`
}
// 服务端 → 客户端消息
type ServerMessage struct {
Type string `json:"type"` // response | segment | audio | error | device_update
MessageID string `json:"message_id"`
Text string `json:"text,omitempty"`
Segments []VoiceSegment `json:"segments,omitempty"` // 断句数组
FullAudioURL string `json:"full_audio_url,omitempty"`
ResponseMode string `json:"response_mode"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
Type string `json:"type"` // response | segment | audio | error | device_update | pong | history_response | stream_chunk | stream_end
MessageID string `json:"message_id"`
Text string `json:"text,omitempty"`
Content string `json:"content,omitempty"` // stream_chunk 的增量文本
Role string `json:"role,omitempty"` // stream 消息的角色
SessionID string `json:"session_id,omitempty"` // 会话 ID
Segments []VoiceSegment `json:"segments,omitempty"` // 断句数组
FullAudioURL string `json:"full_audio_url,omitempty"`
ResponseMode string `json:"response_mode"`
ToolCalls []ToolCall `json:"tool_calls,omitempty"`
Error string `json:"error,omitempty"`
Timestamp int64 `json:"timestamp"`
Messages []Message `json:"messages,omitempty"` // 历史消息列表
}
type VoiceSegment struct {