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:
+110
-18
@@ -172,7 +172,7 @@ func getEnv(key, fallback string) string {
|
||||
return fallback
|
||||
}
|
||||
|
||||
// handleChat 处理对话请求
|
||||
// handleChat 处理对话请求(SSE 流式响应)
|
||||
func handleChat(
|
||||
w http.ResponseWriter,
|
||||
r *http.Request,
|
||||
@@ -237,36 +237,94 @@ func handleChat(
|
||||
return
|
||||
}
|
||||
|
||||
// 4. 调用LLM
|
||||
llmResp, err := llmAdapter.Chat(ctx, llmMessages)
|
||||
if err != nil {
|
||||
http.Error(w, fmt.Sprintf("LLM调用失败: %v", err), http.StatusInternalServerError)
|
||||
// 4. 设置 SSE 响应头
|
||||
w.Header().Set("Content-Type", "text/event-stream")
|
||||
w.Header().Set("Cache-Control", "no-cache")
|
||||
w.Header().Set("Connection", "keep-alive")
|
||||
w.Header().Set("X-Accel-Buffering", "no")
|
||||
|
||||
flusher, ok := w.(http.Flusher)
|
||||
if !ok {
|
||||
http.Error(w, "Streaming not supported", http.StatusInternalServerError)
|
||||
return
|
||||
}
|
||||
|
||||
// 5. 异步提取记忆
|
||||
if memExtractor != nil {
|
||||
go memExtractor.ExtractAndStore(context.Background(), req.UserID, req.SessionID, req.Message, llmResp.Content)
|
||||
// 5. 调用LLM流式接口
|
||||
chunkCh, err := llmAdapter.ChatStream(ctx, llmMessages)
|
||||
if err != nil {
|
||||
// 流式初始化失败,返回 SSE 格式错误
|
||||
errData, _ := json.Marshal(map[string]string{"delta": "", "error": fmt.Sprintf("LLM调用失败: %v", err)})
|
||||
fmt.Fprintf(w, "data: %s\n\n", errData)
|
||||
flusher.Flush()
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
// 6. 构建响应
|
||||
resp := map[string]interface{}{
|
||||
"text": llmResp.Content,
|
||||
messageID := fmt.Sprintf("msg-%d", time.Now().UnixNano())
|
||||
|
||||
// 6. 逐 token 推送 SSE
|
||||
var fullContent string
|
||||
var segments []llm.Segment
|
||||
segmenter := llm.NewSegmenter()
|
||||
|
||||
for chunk := range chunkCh {
|
||||
if chunk.Error != nil {
|
||||
log.Printf("[chat] 流式错误: %v", chunk.Error)
|
||||
errData, _ := json.Marshal(map[string]string{"delta": "", "error": chunk.Error.Error()})
|
||||
fmt.Fprintf(w, "data: %s\n\n", errData)
|
||||
flusher.Flush()
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
flusher.Flush()
|
||||
return
|
||||
}
|
||||
|
||||
if chunk.Done {
|
||||
// 流结束,flush 剩余片段
|
||||
if remaining := segmenter.Flush(); remaining != nil {
|
||||
segments = append(segments, *remaining)
|
||||
}
|
||||
break
|
||||
}
|
||||
|
||||
if chunk.Content != "" {
|
||||
fullContent += chunk.Content
|
||||
|
||||
// 实时断句
|
||||
newSegs := segmenter.Feed(chunk.Content)
|
||||
segments = append(segments, newSegs...)
|
||||
|
||||
deltaData, _ := json.Marshal(map[string]string{
|
||||
"delta": chunk.Content,
|
||||
"message_id": messageID,
|
||||
})
|
||||
fmt.Fprintf(w, "data: %s\n\n", deltaData)
|
||||
flusher.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
// 7. 发送结束标记(附带元数据)
|
||||
endData, _ := json.Marshal(map[string]interface{}{
|
||||
"message_id": messageID,
|
||||
"mode": req.Mode,
|
||||
"message_id": fmt.Sprintf("msg-%d", time.Now().UnixNano()),
|
||||
}
|
||||
"segments": segments,
|
||||
"done": true,
|
||||
})
|
||||
fmt.Fprintf(w, "data: %s\n\n", endData)
|
||||
flusher.Flush()
|
||||
fmt.Fprintf(w, "data: [DONE]\n\n")
|
||||
flusher.Flush()
|
||||
|
||||
// 语音助手模式断句
|
||||
if req.Mode == "voice_assistant" {
|
||||
resp["segments"] = llm.SplitIntoSegments(llmResp.Content)
|
||||
// 8. 异步提取记忆
|
||||
if memExtractor != nil && fullContent != "" {
|
||||
go memExtractor.ExtractAndStore(context.Background(), req.UserID, req.SessionID, req.Message, fullContent)
|
||||
}
|
||||
|
||||
// Ensure unused variables don't cause compile errors
|
||||
_ = personaLoader
|
||||
_ = memRetriever
|
||||
_ = memExtractor
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(resp)
|
||||
_ = messageID
|
||||
}
|
||||
|
||||
// handleMemorySearch 处理记忆搜索请求
|
||||
@@ -383,6 +441,40 @@ case http.MethodGet:
|
||||
"total": len(memories),
|
||||
})
|
||||
|
||||
case http.MethodDelete:
|
||||
// 删除单条记忆: DELETE /api/v1/memory?id=xxx
|
||||
memoryID := r.URL.Query().Get("id")
|
||||
if memoryID == "" {
|
||||
http.Error(w, "缺少 id 参数", http.StatusBadRequest)
|
||||
return
|
||||
}
|
||||
|
||||
if memStore == nil {
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusServiceUnavailable)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"error": "记忆系统未就绪",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
ctx := r.Context()
|
||||
if err := memStore.Delete(ctx, memoryID); err != nil {
|
||||
log.Printf("[memory] 删除失败: %v", err)
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
w.WriteHeader(http.StatusInternalServerError)
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"error": "删除失败",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "application/json")
|
||||
json.NewEncoder(w).Encode(map[string]interface{}{
|
||||
"status": "deleted",
|
||||
"memory_id": memoryID,
|
||||
})
|
||||
|
||||
case http.MethodPost:
|
||||
// 手动添加记忆
|
||||
var req struct {
|
||||
|
||||
Reference in New Issue
Block a user