feat: DevTools综合升级 — 记忆查询 + 会话监看 + WebUI侧边栏重构

- docs: 17个文件重命名为 YYYY-MM-DD.HH-mm-SS-内容.md 格式
- config: 管理员凭据移至 backend/.env (ADMIN_USERNAME/PASSWORD)
- gateway: 新增 SessionState 会话追踪 + GET /api/v1/admin/sessions
- devtools: 新增7个代理端点 (dashboard/sessions/memory)
- devtools: WebUI重构为侧边栏 + 5面板 (仪表盘/记忆/会话/服务/性能)
This commit is contained in:
2026-05-16 15:02:44 +08:00
parent cd60b01cf3
commit d15acf587c
24 changed files with 1934 additions and 347 deletions
+112 -22
View File
@@ -1,7 +1,10 @@
package handler
import (
"bytes"
"encoding/json"
"fmt"
"io"
"log"
"net/http"
"time"
@@ -98,25 +101,128 @@ func (h *ChatHandler) handleMessage(client *ws.Client, msg ws.ClientMessage) {
}
}
// handleChatMessage 处理文字聊天消息
// handleChatMessage 处理文字聊天消息 - 转发到 AI-Core
func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage) {
mode := msg.Mode
if mode == "" {
mode = "text"
}
// MVP阶段:生成模拟回复(后续对接AI-Core)
// 实际部署时,这里应转发消息到AI-Core并等待响应
// 记录用户消息
h.hub.RecordMessage(client.SessionID, "user", msg.Content)
// 设置会话状态为 thinking
h.hub.UpdateSessionState(client.SessionID, "thinking")
// 构建 AI-Core 请求
aiReq := map[string]string{
"user_id": client.UserID,
"session_id": client.SessionID,
"message": msg.Content,
"mode": mode,
}
reqBody, err := json.Marshal(aiReq)
if err != nil {
log.Printf("[chat] 序列化请求失败: %v", err)
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
MessageID: "msg_" + generateID(),
Error: "内部错误,请稍后重试",
Timestamp: time.Now().UnixMilli(),
})
return
}
// 调用 AI-Core
aiCoreURL := h.cfg.AICoreURL + "/api/v1/chat"
httpReq, err := http.NewRequest("POST", aiCoreURL, bytes.NewReader(reqBody))
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
}
httpReq.Header.Set("Content-Type", "application/json")
httpClient := &http.Client{Timeout: 120 * time.Second}
resp, err := httpClient.Do(httpReq)
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: fmt.Sprintf("AI-Core 调用失败: %v", err),
Timestamp: time.Now().UnixMilli(),
})
return
}
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 {
log.Printf("[chat] AI-Core 返回错误 [%d]: %s", resp.StatusCode, string(body))
h.hub.UpdateSessionState(client.SessionID, "error")
client.SendMessage(ws.ServerMessage{
Type: "error",
MessageID: "msg_" + generateID(),
Error: fmt.Sprintf("AI-Core 错误 (%d)", resp.StatusCode),
Timestamp: time.Now().UnixMilli(),
})
return
}
// 解析 AI-Core 响应
var aiResp struct {
Text string `json:"text"`
Mode string `json:"mode"`
MessageID string `json:"message_id"`
}
if err := json.Unmarshal(body, &aiResp); 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
}
// 记录助手响应
h.hub.RecordMessage(client.SessionID, "assistant", aiResp.Text)
// 设置会话状态为 idle
h.hub.UpdateSessionState(client.SessionID, "idle")
// 发送响应给客户端
response := ws.ServerMessage{
Type: "response",
MessageID: "msg_" + generateID(),
Text: h.generateMockResponse(msg.Content, mode),
MessageID: aiResp.MessageID,
Text: aiResp.Text,
ResponseMode: mode,
Timestamp: time.Now().UnixMilli(),
}
// 发送响应给客户端
if err := client.SendMessage(response); err != nil {
log.Printf("[WS] 发送响应失败: %v", err)
}
@@ -134,22 +240,6 @@ func (h *ChatHandler) handleVoiceInput(client *ws.Client, msg ws.ClientMessage)
client.SendMessage(response)
}
// generateMockResponse 生成模拟回复
func (h *ChatHandler) generateMockResponse(content, mode string) string {
// MVP阶段:没有对接AI-Core时的默认回复
responses := []string{
"嗯嗯,人家听到了哦♪ 开拓者想和昔涟聊些什么呢?",
"嘻嘻,开拓者说的话真有趣呢♪ 让我想想怎么回答……",
"啊,这个问题很有意思呢!虽然人家现在还在学习阶段,但我很乐意倾听开拓者说的每一句话哦♡",
}
// 简单hash选一条
hash := 0
for _, c := range content {
hash += int(c)
}
return responses[hash%len(responses)]
}
// SendSystemMessage 向用户发送系统消息(用于主动通知)
func (h *ChatHandler) SendSystemMessage(userID, sessionID, text string) error {
@@ -1,16 +1,20 @@
package handler
import (
"bytes"
"encoding/json"
"fmt"
"io"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yourname/cyrene-ai/gateway/internal/middleware"
)
// MemoryHandler 记忆查询处理器
// MemoryHandler 记忆查询处理器 — 代理到 AI-Core
type MemoryHandler struct {
// MVP阶段:直接透传到AI-Core,Gateway本身不需要记忆存储
aiCoreURL string
client *http.Client
}
@@ -19,42 +23,59 @@ type MemoryHandler struct {
func NewMemoryHandler(aiCoreURL string) *MemoryHandler {
return &MemoryHandler{
aiCoreURL: aiCoreURL,
client: &http.Client{},
client: &http.Client{
Timeout: 15 * time.Second,
},
}
}
// Query 查询用户记忆
// Query 搜索用户记忆 — 代理 GET /api/v1/memory/search?user_id=...&q=...
func (h *MemoryHandler) Query(c *gin.Context) {
userID := middleware.GetUserID(c)
query := c.Query("q")
if query == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "查询参数q不能为空"})
return
}
// MVP阶段:返回简单的内存数据
// 后续将请求转发到AI-Core的记忆API
c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"query": query,
"memories": []gin.H{},
"message": "记忆查询功能将在后续版本中接入AI-Core",
})
url := fmt.Sprintf("%s/api/v1/memory/search?user_id=%s&q=%s",
h.aiCoreURL, userID, query)
resp, err := h.client.Get(url)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("AI-Core 不可达: %v", err)})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result interface{}
json.Unmarshal(body, &result)
c.JSON(resp.StatusCode, result)
}
// List 列出用户所有记忆
// List 列出用户所有记忆 — 代理 GET /api/v1/memory?user_id=...
func (h *MemoryHandler) List(c *gin.Context) {
userID := middleware.GetUserID(c)
c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"memories": []gin.H{},
"message": "记忆列表功能将在后续版本中接入AI-Core",
})
url := fmt.Sprintf("%s/api/v1/memory?user_id=%s", h.aiCoreURL, userID)
resp, err := h.client.Get(url)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("AI-Core 不可达: %v", err)})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result interface{}
json.Unmarshal(body, &result)
c.JSON(resp.StatusCode, result)
}
// Add 手动添加记忆
// Add 手动添加记忆 — 代理 POST /api/v1/memory
func (h *MemoryHandler) Add(c *gin.Context) {
userID := middleware.GetUserID(c)
@@ -76,13 +97,33 @@ func (h *MemoryHandler) Add(c *gin.Context) {
req.Priority = 1
}
// MVP阶段:返回成功但暂不持久化
c.JSON(http.StatusCreated, gin.H{
"status": "accepted",
// 转发到 AI-Core
aiReq := map[string]interface{}{
"user_id": userID,
"content": req.Content,
"category": req.Category,
"priority": req.Priority,
"message": "记忆手动添加功能将在后续版本中接入AI-Core",
})
}
reqBody, _ := json.Marshal(aiReq)
url := fmt.Sprintf("%s/api/v1/memory", h.aiCoreURL)
httpReq, err := http.NewRequest("POST", url, bytes.NewReader(reqBody))
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "构建请求失败"})
return
}
httpReq.Header.Set("Content-Type", "application/json")
resp, err := h.client.Do(httpReq)
if err != nil {
c.JSON(http.StatusBadGateway, gin.H{"error": fmt.Sprintf("AI-Core 不可达: %v", err)})
return
}
defer resp.Body.Close()
body, _ := io.ReadAll(resp.Body)
var result interface{}
json.Unmarshal(body, &result)
c.JSON(resp.StatusCode, result)
}
@@ -2,16 +2,19 @@ package handler
import (
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/yourname/cyrene-ai/gateway/internal/middleware"
"github.com/yourname/cyrene-ai/gateway/internal/ws"
)
// SessionHandler 会话管理处理器
type SessionHandler struct {
// MVP阶段使用内存存储,后续迁移到PostgreSQL
sessions map[string][]SessionInfo // userID -> sessions
hub *ws.Hub
}
// SessionInfo 会话信息
@@ -24,9 +27,10 @@ type SessionInfo struct {
}
// NewSessionHandler 创建会话处理器
func NewSessionHandler() *SessionHandler {
func NewSessionHandler(hub *ws.Hub) *SessionHandler {
return &SessionHandler{
sessions: make(map[string][]SessionInfo),
hub: hub,
}
}
@@ -49,8 +53,8 @@ func (h *SessionHandler) Create(c *gin.Context) {
ID: "session_" + randomID(12),
UserID: userID,
Title: req.Title,
CreatedAt: nowMillis(),
UpdatedAt: nowMillis(),
CreatedAt: time.Now().UnixMilli(),
UpdatedAt: time.Now().UnixMilli(),
}
h.sessions[userID] = append([]SessionInfo{session}, h.sessions[userID]...)
@@ -104,6 +108,38 @@ func (h *SessionHandler) Get(c *gin.Context) {
c.JSON(http.StatusNotFound, gin.H{"error": "会话不存在"})
}
// ========== Admin 端点 ==========
// ListActiveSessions 获取当前所有活跃 WebSocket 会话列表 (管理员)
func (h *SessionHandler) ListActiveSessions(c *gin.Context) {
sessions := h.hub.GetActiveSessions()
if sessions == nil {
sessions = []*ws.SessionState{}
}
c.JSON(http.StatusOK, gin.H{
"sessions": sessions,
"total": len(sessions),
})
}
// GetSession 获取指定会话的详细信息 (管理员)
func (h *SessionHandler) GetSession(c *gin.Context) {
sessionID := c.Param("id")
if sessionID == "" {
c.JSON(http.StatusBadRequest, gin.H{"error": "缺少会话ID"})
return
}
session := h.hub.GetSession(sessionID)
if session == nil {
c.JSON(http.StatusNotFound, gin.H{"error": "会话不存在"})
return
}
c.JSON(http.StatusOK, session)
}
// 简单的工具函数
func randomID(n int) string {
const letters = "abcdefghijklmnopqrstuvwxyz0123456789"
@@ -114,8 +150,3 @@ func randomID(n int) string {
// 使用纳秒时间戳增加唯一性
return string(b)
}
func nowMillis() int64 {
// 避免引入time包,直接返回一个值
return 0
}