fix: 第一轮修复 - 记忆管理/IoT操控/历史消息持久化/动作消息/链路优化/安全配置

- 修复记忆管理数据库连接不可用 (ai-core重编译+Unicode修复)
- 修复IoT子会话工具调用链路日志缺失
- 新增最终审查子会话(review_provider) 支持消息格式解析拆分
- 实现历史消息持久化(后端存储+前端分页加载)
- 前端新增动作消息(ActionMessage)类型和渲染
- 优化对话链路速度(非阻塞子会话+快速问候通道)
- JWT密钥环境变量化(无默认值启动panic)
- Token自动刷新机制(401拦截器+refresh接口)
- WebSocket指数退避重连(jitter+最大10次)
- localStorage清理一致性(cyrene_前缀+版本检查)
- IoT环境变量统一为IOT_SERVICE_URL
This commit is contained in:
2026-05-21 23:10:07 +08:00
parent 8b7d4ec19a
commit a058b0ab8e
53 changed files with 5535 additions and 241 deletions
@@ -108,11 +108,19 @@ func (h *AuthHandler) Register(c *gin.Context) {
return
}
// 生成 refresh_token (长期有效)
refreshToken, err := h.cfg.GenerateRefreshToken(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成刷新令牌失败"})
return
}
c.JSON(http.StatusCreated, gin.H{
"user_id": userID,
"token": token,
"expires": time.Now().Add(h.cfg.JWTExpiryHours).Unix(),
"nickname": req.Nickname,
"user_id": userID,
"token": token,
"refresh_token": refreshToken,
"expires": time.Now().Add(h.cfg.JWTExpiryHours).Unix(),
"nickname": req.Nickname,
})
}
@@ -186,10 +194,18 @@ func (h *AuthHandler) Login(c *gin.Context) {
return
}
// 生成 refresh_token (长期有效)
refreshToken, err := h.cfg.GenerateRefreshToken(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成刷新令牌失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"user_id": userID,
"token": token,
"expires": time.Now().Add(h.cfg.JWTExpiryHours).Unix(),
"user_id": userID,
"token": token,
"refresh_token": refreshToken,
"expires": time.Now().Add(h.cfg.JWTExpiryHours).Unix(),
})
}
@@ -219,18 +235,38 @@ func (h *AuthHandler) verifyUserPassword(username, password string) (bool, error
}
// RefreshToken 刷新令牌
// 支持两种方式:
// 1. 在 Authorization header 中传入有效的 access_token (可以已过期但 refresh_token 有效)
// 2. 在请求体中传入 refresh_token
func (h *AuthHandler) RefreshToken(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" || len(authHeader) < 8 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
return
}
var userID string
tokenString := authHeader[7:] // 去掉 "Bearer "
userID, err := h.cfg.ValidateToken(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "令牌无效或已过期"})
return
// 优先从请求体获取 refresh_token
var req struct {
RefreshToken string `json:"refresh_token"`
}
if err := c.ShouldBindJSON(&req); err == nil && req.RefreshToken != "" {
uid, err := h.cfg.ValidateRefreshToken(req.RefreshToken)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "刷新令牌无效或已过期"})
return
}
userID = uid
} else {
// 回退:从 Authorization header 获取 access_token 并验证
authHeader := c.GetHeader("Authorization")
if authHeader == "" || len(authHeader) < 8 {
c.JSON(http.StatusUnauthorized, gin.H{"error": "未提供认证令牌"})
return
}
tokenString := authHeader[7:] // 去掉 "Bearer "
uid, err := h.cfg.ValidateToken(tokenString)
if err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "令牌无效或已过期"})
return
}
userID = uid
}
newToken, err := h.cfg.GenerateToken(userID)
@@ -239,8 +275,16 @@ func (h *AuthHandler) RefreshToken(c *gin.Context) {
return
}
// 生成新的 refresh_token
newRefreshToken, err := h.cfg.GenerateRefreshToken(userID)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "生成刷新令牌失败"})
return
}
c.JSON(http.StatusOK, gin.H{
"token": newToken,
"expires": time.Now().Add(h.cfg.JWTExpiryHours).Unix(),
"token": newToken,
"refresh_token": newRefreshToken,
"expires": time.Now().Add(h.cfg.JWTExpiryHours).Unix(),
})
}
@@ -17,21 +17,24 @@ import (
"github.com/gorilla/websocket"
"github.com/yourname/cyrene-ai/gateway/internal/config"
"github.com/yourname/cyrene-ai/gateway/internal/store"
"github.com/yourname/cyrene-ai/gateway/internal/ws"
)
// ChatHandler 聊天处理器
type ChatHandler struct {
cfg *config.Config
hub *ws.Hub
upgrader websocket.Upgrader
cfg *config.Config
hub *ws.Hub
sessionStore *store.SessionStore
upgrader websocket.Upgrader
}
// NewChatHandler 创建聊天处理器
func NewChatHandler(cfg *config.Config, hub *ws.Hub) *ChatHandler {
func NewChatHandler(cfg *config.Config, hub *ws.Hub, sessionStore *store.SessionStore) *ChatHandler {
return &ChatHandler{
cfg: cfg,
hub: hub,
cfg: cfg,
hub: hub,
sessionStore: sessionStore,
upgrader: websocket.Upgrader{
ReadBufferSize: 1024,
WriteBufferSize: 1024,
@@ -123,6 +126,13 @@ func (h *ChatHandler) handleChatMessage(client *ws.Client, msg ws.ClientMessage)
mode = "text"
}
// 持久化用户消息到数据库(在 WebSocket 发送之前)
if h.sessionStore != nil && h.sessionStore.IsAvailable() {
if err := h.sessionStore.AddMessage(client.SessionID, "user", msg.Content); err != nil {
log.Printf("[chat] 持久化用户消息失败: %v", err)
}
}
// 记录用户消息
h.hub.RecordMessage(client.SessionID, "user", msg.Content)
@@ -359,8 +369,14 @@ func (h *ChatHandler) streamResponse(client *ws.Client, mode string, reqBody []b
Timestamp: time.Now().UnixMilli(),
})
// 缓存完整响应
// 持久化 AI 回复到数据库(在 WebSocket 发送之前)
if fullText != "" {
if h.sessionStore != nil && h.sessionStore.IsAvailable() {
if err := h.sessionStore.AddMessage(client.SessionID, "assistant", fullText); err != nil {
log.Printf("[chat] 持久化 AI 回复失败: %v", err)
}
}
h.hub.CacheMessage(client.UserID, client.SessionID, ws.Message{
ID: msgID,
Role: "assistant",
@@ -285,8 +285,22 @@ func (h *SessionHandler) GetMessages(c *gin.Context) {
}
}
offset := 0
if o := c.Query("offset"); o != "" {
parsed := 0
for _, ch := range o {
if ch < '0' || ch > '9' {
break
}
parsed = parsed*10 + int(ch-'0')
}
if parsed > 0 {
offset = parsed
}
}
if h.useDB {
messages, err := h.store.GetMessages(sessionID, limit)
messages, err := h.store.GetMessages(sessionID, limit, offset)
if err != nil {
log.Printf("[SessionHandler] 查询消息失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败", "errorType": "db_error"})
@@ -536,7 +550,7 @@ func (h *SessionHandler) ExportSession(c *gin.Context) {
}
// 获取所有消息 (不限制数量,导出全部)
messages, err := h.store.GetMessages(sessionID, 0)
messages, err := h.store.GetMessages(sessionID, 0, 0)
if err != nil {
log.Printf("[SessionHandler] 查询消息失败: %v", err)
c.JSON(http.StatusInternalServerError, gin.H{"error": "查询消息失败", "errorType": "db_error"})