fix: 第二轮修复 — 数据库启动检查、会话持久化、URL路由、设备排序等

1. DevTools 启动前检查数据库状态,失败时自动尝试启动
2. ai-core 添加数据库断线重连机制 (30秒间隔)
3. Dashboard 添加数据库状态卡片 (启动/停止/重启)
4. Gateway 会话空闲超时管理 (30分钟标记空闲)
5. 会话/消息 PostgreSQL 持久化 (SessionStore + REST API)
6. 前端服务端会话持久化 + URL hash 路由 + 侧边栏管理
7. 管理员回到主对话按钮
8. IoT 设备卡片固定排序
9. 更新相关文档
This commit is contained in:
2026-05-17 17:18:02 +08:00
parent 745b1c6aad
commit e7b7eff0d8
21 changed files with 1735 additions and 284 deletions
+93 -2
View File
@@ -8,6 +8,8 @@ import (
"os"
"sync"
"time"
"github.com/yourname/cyrene-ai/gateway/internal/store"
)
// SessionState 会话状态
@@ -60,6 +62,22 @@ type Hub struct {
iotServiceURL string
iotStopCh chan struct{}
iotPollRunning bool
// 持久化存储 (可选,数据库连接失败时为 nil)
store *store.SessionStore
// 闲置超时时间
idleTimeout time.Duration
}
// SetStore 设置持久化存储 (可选)
func (h *Hub) SetStore(s *store.SessionStore) {
h.store = s
}
// SetIdleTimeout 设置闲置超时时间
func (h *Hub) SetIdleTimeout(minutes int) {
h.idleTimeout = time.Duration(minutes) * time.Minute
}
// NewHub 创建WebSocket Hub
@@ -72,9 +90,79 @@ func NewHub() *Hub {
userClients: make(map[string]map[*Client]bool),
sessions: make(map[string]*SessionState),
iotStopCh: make(chan struct{}),
idleTimeout: 30 * time.Minute, // 默认30分钟
}
}
// StartIdleCleanup 启动闲置会话清理 goroutine
// 每5分钟检查一次,将超过 idleTimeout 无活动的会话标记为 idle
func (h *Hub) StartIdleCleanup() {
go func() {
ticker := time.NewTicker(5 * time.Minute)
defer ticker.Stop()
for range ticker.C {
h.cleanupIdleSessions()
}
}()
log.Printf("[WS] 闲置会话清理已启动 (超时: %v)", h.idleTimeout)
}
// cleanupIdleSessions 标记超时会话为 idle(不删除状态)
func (h *Hub) cleanupIdleSessions() {
h.mu.Lock()
defer h.mu.Unlock()
now := time.Now()
idleCount := 0
for sessionID, s := range h.sessions {
// 检查该 session 是否还有活跃连接
hasActiveConn := false
for _, clients := range h.userClients {
for c := range clients {
if c.SessionID == sessionID {
hasActiveConn = true
break
}
}
if hasActiveConn {
break
}
}
// 如果没有活跃连接且超过闲置超时,标记为 idle
if !hasActiveConn && now.Sub(s.LastActivity) > h.idleTimeout {
if s.State != "idle" {
s.State = "idle"
idleCount++
}
}
}
if idleCount > 0 {
log.Printf("[WS] 闲置清理: %d 个会话标记为 idle", idleCount)
}
}
// GetAllActiveSessions 返回所有会话状态(包括 idle),供 DevTools 监看使用
func (h *Hub) GetAllActiveSessions() []*SessionState {
h.mu.RLock()
defer h.mu.RUnlock()
if h.sessions == nil || len(h.sessions) == 0 {
return []*SessionState{}
}
result := make([]*SessionState, 0, len(h.sessions))
for _, s := range h.sessions {
cp := *s
cp.RecentMessages = nil
result = append(result, &cp)
}
return result
}
// Run 启动Hub主循环
func (h *Hub) Run() {
for {
@@ -119,7 +207,7 @@ func (h *Hub) Run() {
}
}
// 检查该session是否还有其他连接,没有则移除会话状态
// 检查该session是否还有其他连接,没有则标记为 idle 而非删除
hasOtherConn := false
if clients, ok := h.userClients[client.UserID]; ok {
for c := range clients {
@@ -130,7 +218,10 @@ func (h *Hub) Run() {
}
}
if !hasOtherConn {
delete(h.sessions, client.SessionID)
// 不再删除 session 状态,而是标记为 idle 保留在内存中
if s, ok := h.sessions[client.SessionID]; ok {
s.State = "idle"
}
}
}
h.mu.Unlock()