fix(ws): 发送 WebSocket 协议级 Ping 控制帧防止 60s 后连接断开

writePump 只发 ActionCable 文本 JSON ping,从不发 websocket.PingMessage,
导致 readPump 的 PongHandler 永远不触发,ReadDeadline(60s) 到期后连接
被强制关闭。前端每 60 秒弹出"离线""重连"通知。

修复:在 ticker 中先发 PingMessage 控制帧(浏览器自动回 Pong 重置
ReadDeadline),再发 ActionCable 文本 ping(保活 ConnectionMonitor)。

同时包含此前会话中的其他变更:
- 移除安全设置(SAML)页面及路由
- 新增超级管理员控制台前端页面
- platform agent_bots 路由使用 Platform* handler
- 超级管理员入口改为 SPA 内路由跳转
This commit is contained in:
Rogee
2026-07-30 16:55:44 +08:00
parent 1fb95e4151
commit cd82d079e3
26 changed files with 2421 additions and 199 deletions
+14 -2
View File
@@ -194,7 +194,19 @@ func (h *Handler) writePump(client *Client) {
}
case <-ticker.C:
// Send ActionCable-level ping message (JSON text frame).
// Send WebSocket protocol-level ping control frame.
// The browser automatically responds with a Pong, which triggers
// the PongHandler in readPump and resets the ReadDeadline.
// Without this, the ReadDeadline (PongWait=60s) expires and the
// connection is forcibly closed, causing the client to show
// "offline" / "reconnecting" notifications every ~60 seconds.
client.Conn.SetWriteDeadline(time.Now().Add(WriteWait))
if err := client.Conn.WriteMessage(websocket.PingMessage, nil); err != nil {
logger.L().Errorf("ws: ws-ping control frame failed for user=%d: %v", client.UserID, err)
return
}
// Also send ActionCable-level ping message (JSON text frame).
// The JS ConnectionMonitor expects periodic ping messages to
// keep the connection alive (staleThreshold = 6s by default).
pingMsg, _ := json.Marshal(PingFrame{
@@ -203,7 +215,7 @@ func (h *Handler) writePump(client *Client) {
})
client.Conn.SetWriteDeadline(time.Now().Add(WriteWait))
if err := client.Conn.WriteMessage(websocket.TextMessage, pingMsg); err != nil {
logger.L().Errorf("ws: ping write failed for user=%d: %v", client.UserID, err)
logger.L().Errorf("ws: actioncable ping write failed for user=%d: %v", client.UserID, err)
return
}
}