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:
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2040,11 +2040,12 @@ func registerPlatformRoutes(g *gin.RouterGroup, h *Handlers) {
|
||||
|
||||
// Agent Bot CRUD (super-admin, global bots without account_id)
|
||||
// Reference: Chatwoot namespace :agent_bots under :platform_app
|
||||
g.GET("/agent_bots", h.AgentBot.List)
|
||||
g.POST("/agent_bots", h.AgentBot.Create)
|
||||
g.GET("/agent_bots/:id", h.AgentBot.Get)
|
||||
g.PUT("/agent_bots/:id", h.AgentBot.Update)
|
||||
g.DELETE("/agent_bots/:id", h.AgentBot.Delete)
|
||||
// Use Platform* handlers — these don't require account_id from URL params.
|
||||
g.GET("/agent_bots", h.AgentBot.PlatformList)
|
||||
g.POST("/agent_bots", h.AgentBot.PlatformCreate)
|
||||
g.GET("/agent_bots/:id", h.AgentBot.PlatformGet)
|
||||
g.PUT("/agent_bots/:id", h.AgentBot.PlatformUpdate)
|
||||
g.DELETE("/agent_bots/:id", h.AgentBot.PlatformDelete)
|
||||
g.POST("/agent_bots/:id/reset_token", h.AgentBot.ResetToken)
|
||||
g.POST("/agent_bots/:id/reset_secret", h.AgentBot.ResetSecret)
|
||||
g.POST("/agent_bots/:id/delete_avatar", h.AgentBot.DeleteAvatar)
|
||||
|
||||
Reference in New Issue
Block a user