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

@@ -177,7 +177,10 @@ export const useMeetingStore = defineStore('meeting', () => {
[MEETING_WS_MEMBER_KICKED]: _onMemberKicked,
[MEETING_WS_MEMBER_PRODUCER_NEW]: _onProducerNew,
[MEETING_WS_HOST_CHANGED]: _onHostChanged,
[MEETING_WS_CHAT_MESSAGE]: _onChatMessage
[MEETING_WS_CHAT_MESSAGE]: _onChatMessage,
// WS 连接生命周期:用于断线/重连期间维持会议会话,避免"连接断开 → 后端失去 roomCode 绑定 → 广播丢失"
_connected: _onWsReconnected,
_disconnected: _onWsDisconnected
}
Object.entries(handlers).forEach(([event, handler]) => {
wsService.on(event, handler)
@@ -321,6 +324,51 @@ export const useMeetingStore = defineStore('meeting', () => {
chatMessages.value.push(data)
}
/**
* WS 连接断开仅当当前已进入会议CONNECTED/CONNECTING时切换到 RECONNECTING
* 供 UI 展示"重连中…",避免用户误以为自己已离会。
*/
const _onWsDisconnected = () => {
if (!currentRoom.value) return
if (
localState.value === MEETING_LOCAL_STATE_CONNECTED ||
localState.value === MEETING_LOCAL_STATE_CONNECTING
) {
_log('warn', '[Meeting] WS 已断开,进入 reconnecting')
localState.value = MEETING_LOCAL_STATE_RECONNECTING
}
}
/**
* WS 重连成功:若当前仍在某个房间内,自动重发 meeting.room.join 以恢复后端 WS↔roomCode 绑定。
* 后端 WS Handler 对同一用户重连后,需要再次收到 meeting.room.join 才会将该连接加入房间广播组,
* 否则即使 mediasoup 层仍在线,所有 meeting.* 广播都不会再送达本端。
*/
const _onWsReconnected = async () => {
if (!currentRoom.value) return
// 仅在 RECONNECTING 状态触发(避免首次进房 _onOpen 与 _afterJoined 的首次 join 并发冲突)
if (localState.value !== MEETING_LOCAL_STATE_RECONNECTING) return
const roomCode = currentRoom.value.room_code
try {
_log('info', '[Meeting] WS 重连后重绑定房间', roomCode)
await wsService.sendWithAck(MEETING_WS_ROOM_JOIN, { room_code: roomCode })
// 重连期间可能错过若干广播,重新拉一次详情对齐参与者列表
try {
const detail = await meetingApi.getRoom(roomCode)
if (detail && Array.isArray(detail.participants)) {
participants.value = detail.participants
}
} catch (e) {
_log('warn', '[Meeting] 重绑定后拉取房间详情失败', e)
}
localState.value = MEETING_LOCAL_STATE_CONNECTED
_log('info', '[Meeting] WS 重绑定房间成功,恢复 connected')
} catch (err) {
_log('warn', '[Meeting] WS 重绑定房间失败,保持 reconnecting 等待下次重连', err)
// 保留 RECONNECTING 状态,下一次 WS 重连会再次触发本回调
}
}
// ==================== 远端媒体清理 ====================
const _cleanupRemoteProducer = (userId, producerId) => {