feat(phase2e-2): WS 稳定性改进——重连后自动重绑定房间 + 多端登录抖动检测

背景
- Task 9 联调发现同账号多 tab 登录会触发后端单设备踢下线机制,WS 每秒被关闭一次,
  所有 meeting.* 的 sendWithAck 都因 "连接已断开" 而失败,表现为"开启音视频提示失败"
- 同时 WS 断线重连后后端对该连接的 roomCode 绑定丢失,会议广播不再送达本端

改动
- store/meeting.js:_registerListeners 增加 WS 生命周期监听
  - _disconnected:从 CONNECTED/CONNECTING 切到 RECONNECTING,UI 可展示"重连中…"
  - _connected:在 RECONNECTING 状态自动重发 meeting.room.join 重绑定房间,并刷新参与者列表
- services/websocket.js:_recordCloseForFlapping 做 10s 滑窗 flapping 检测
  - 窗口内累计 >=3 次非主动断连 → 触发 _flapping 事件(含 count/windowMs)
  - 15s 冷却避免抖动期间重复告警
- debug.vue:新增 WS 状态指示灯(已连接/重连中/已断)+ flapping 警告横幅
  - 订阅 _connected / _disconnected / _flapping 三个 WS 内置事件,onBeforeUnmount 注销

验证(Playwright)
- 单次断连:testuser1 创建会议后手动 close WS → 指示灯 reconnecting,1s 后重连
  - 日志链 "WS 已断开 → 进入 reconnecting → 连接成功 → 重绑定房间 307-857-909 → 恢复 connected" 全通
- 4 次连续断连:第 3 次触发 flapping,UI 横幅 "⚠ 检测到异常重连(3 次/10s),可能同账号在其他端登录"
  - 每次断连后房号、主持人状态、localState 均正确恢复为 connected

Made-with: Cursor
This commit is contained in:
bujinyuan
2026-04-22 14:08:07 +08:00
parent 807e111cb9
commit 467135e0d5
3 changed files with 143 additions and 3 deletions

View File

@@ -19,6 +19,11 @@ const RECONNECT_BASE_DELAY = 1000
const RECONNECT_MAX_DELAY = 30000
const MAX_RECONNECT_ATTEMPTS = Infinity
const ACK_DEFAULT_TIMEOUT_MS = 10000
/** 异常重连检测10s 窗口内 >=3 次断连即认为异常抖动(常见于同账号多 tab 互踢) */
const FLAPPING_WINDOW_MS = 10000
const FLAPPING_THRESHOLD = 3
/** 两次 flapping 警告之间最小间隔,避免持续抖动时每秒触发 */
const FLAPPING_NOTIFY_COOLDOWN_MS = 15000
class WebSocketService {
constructor() {
@@ -34,6 +39,10 @@ class WebSocketService {
* Task 9 引入,为 meeting.* 事件提供 Promise 化 ACK 等待
*/
this.pendingAcks = new Map()
/** 近期断连时间戳数组,用于 flapping 检测 */
this.recentCloseAt = []
/** 上次 flapping 警告时间,用于冷却 */
this.lastFlappingNotifyAt = 0
}
/**
@@ -213,13 +222,31 @@ class WebSocketService {
console.log('[WS] 连接关闭', e)
this._clearTimers()
this._rejectAllPendingAcks('连接已断开')
this._emit('_disconnected')
this._emit('_disconnected', e)
if (!this.isManualClose) {
this._recordCloseForFlapping()
this._scheduleReconnect()
}
}
/**
* 记录一次非主动断连,并在短窗口内多次断连时触发 _flapping 事件
* 典型场景:同账号多端登录互踢导致 WS 每秒被后端关闭
*/
_recordCloseForFlapping() {
const now = Date.now()
this.recentCloseAt.push(now)
// 丢掉窗口外的历史记录
this.recentCloseAt = this.recentCloseAt.filter((t) => now - t <= FLAPPING_WINDOW_MS)
if (this.recentCloseAt.length < FLAPPING_THRESHOLD) return
if (now - this.lastFlappingNotifyAt < FLAPPING_NOTIFY_COOLDOWN_MS) return
this.lastFlappingNotifyAt = now
const count = this.recentCloseAt.length
console.warn(`[WS] 检测到异常重连抖动:${FLAPPING_WINDOW_MS / 1000}s 内断开 ${count} 次,可能同账号在其他 tab/设备登录`)
this._emit('_flapping', { count, windowMs: FLAPPING_WINDOW_MS })
}
/**
* 连接断开时统一拒绝所有未完成的 ACK 等待Task 9
* 避免业务层 await sendWithAck() 永久挂起