故障档案

日志里没有重连记录——它根本没在重连

客户端重连逻辑中三处独立的"静默终止"缺陷导致 WS 永久断开,任何一条路径命中就不再调度重连计时器。

从 2026-05-11 开始,多台便携包客户反馈 WebSocket 断线后面板卡死。网关进程(openclaw_gateway.exe)明确在运行,/health 返回 200,但面板显示"已停止重连,请手动刷新"——用户必须手动重启才能恢复。

关键线索在控制台:看不到任何 [ws] 计划重连 的日志。

通常断线后客户端会频繁尝试重连,日志里应该是满屏的重连记录。没有日志意味着问题不在重连失败,而在压根没启动重连机制。说明客户端进入了某个永久休眠状态。

三条独立的永久断开路径

代码审查发现了三条完全不同的、独立的永久断开路径。任意一条命中,就会停止调度任何重连计时器。

路径 A:凭据刷新异常

_scheduleReconnect 方法末尾,每次普通重连都会调用:

this._reconnectTimer = setTimeout(() => {
  if (!this._intentionalClose) {
    this._refreshCredentialsAndReconnect(0)
  }
}, delay)

_refreshCredentialsAndReconnect 是异步方法,catch 分支没有任何后续处理:

} catch (e) {
  console.error('[ws] 刷新凭据失败:', e)
  this._setConnected(false, 'error', `凭据刷新失败: ${e}`)
}

如果 api.readOpenclawConfig()api.autoPairDevice() 抛错——磁盘 IO 抖、Rust 端处理器忙、Tauri IPC 队列阻塞——这次重连就永久终止。便携磁盘的 IO 抖动 + 配置文件读写争抢时最容易发生。

路径 B:认证失败后的强制关闭

当 WebSocket 收到 1008 unauthorized 响应时的处理逻辑:

if (this._authRetryCount < 2) {
  this._authRetryCount++
  this._refreshCredentialsAndReconnect()
  return
}
this._setConnected(false, 'auth_failed', `认证失败: ${e.reason}。请检查 Gateway Token 配置。`)
this._intentionalClose = true   // ← 永久关闭标记
this._flushPending()
return

设置 _intentionalClose = true 之后,所有重连逻辑都被 if (!this._intentionalClose) 短路。即使只是 Gateway 重启窗口碰好出现 token 短暂不匹配,也会一次性把客户端打死,必须刷新页面才能恢复。

路径 C:快速重连配额耗尽

定义了常数 MAX_RECONNECT_ATTEMPTS = 60

if (this._reconnectAttempts >= MAX_RECONNECT_ATTEMPTS) {
  this._setConnected(false, 'error', `连接失败,已停止重连。请手动刷新页面重试。`)
  return
}

客户机晚上挂机,Gateway 因为便携磁盘 GC 或 Windows 休眠争抢资源短暂掉线,客户端尝试 60 次仍未成功重连,就永久停摆。早上用户回来面板已成死链。

为什么三条缺陷同时存在

这是逐步累加的历史债:

  • 路径 B 是在修复"避免无限自动配对循环"时添加的保护,但用错了对象——_intentionalClose=true 本来是用户主动断开的语义,不该用在被动失败上
  • 路径 C 是早期"避免无穷重试"的防护,但 60 次后完全放弃而不留任何复活路径是绝对错误
  • 路径 A 是在"凭据 reload"改动时把所有重连都改走 refreshCredentials,没留意 catch 分支已经成了终态

三条各自独立、都能单独打死客户端,组合在一起命中率非常高。

修复方案

核心思想是永不彻底放弃。任何"快速重连配额耗尽"的分支都转入慢轮询,留出窗口让用户改配置或等待 Gateway 恢复后能自动复连。

新增辅助方法 _schedulePoll(delayMs, kind) 作为慢轮询的触发器:

const AUTH_RETRY_LIMIT = 2
const SLOW_POLL_DELAY_AUTH = 60_000      // 认证持续失败:1 分钟探一次
const SLOW_POLL_DELAY_GENERAL = 300_000  // 一般持续失败:5 分钟探一次

_schedulePoll(delayMs, kind) {
  this._clearReconnectTimer()
  this._reconnectAttempts = 0
  if (kind === 'auth') this._authRetryCount = 0
  this._reconnectState = 'scheduled'
  this._pendingReconnect = true
  this._reconnectTimer = setTimeout(() => {
    this._reconnectTimer = null
    if (this._intentionalClose) return
    this._reconnectState = 'attempting'
    if (kind === 'auth') {
      this._refreshCredentialsAndReconnect(0)
    } else {
      this._doConnect()
    }
  }, delayMs)
}

慢轮询命中后失败会重新进入 _scheduleReconnect 快速重连周期(因为 _reconnectAttempts 重置为 0),相当于"快速 60 次 → 慢一次 → 快速 60 次 → 慢一次"的循环,永不放弃。

三条修复并行:

Fix A:普通重连直接走 _doConnect(),凭据刷新隔离到认证失败分支,避免配置读取错误牵连普通重连。

Fix B:认证失败耗尽后转入 _schedulePoll('auth'),不再设置 _intentionalClose=true,给认证恢复留出 60 秒的探测窗口。

Fix C:重连次数超过 MAX_RECONNECT_ATTEMPTS 后转入 _schedulePoll('general'),设置 UI 状态为"连接持续失败,300 秒后重试"而不是终态。

慢轮询失败后重新进入快速重连周期,形成"快速 60 次 → 慢一次 → 快速 60 次"的循环,永不放弃。

防回归验证

新增 wakou-full/src/lib/ws-client.slow-poll.test.js,覆盖 7 个 case:

  • Fix A:普通重连命中 _doConnect,不调用 _refreshCredentialsAndReconnect
  • Fix C:MAX_RECONNECT_ATTEMPTS 后状态是 reconnecting(非终态 error),5 分钟后触发 _doConnect
  • Fix C:第二轮慢轮询失败后还能再调度快速重连
  • Fix B:_schedulePoll('auth') 等待 60 秒调用 _refreshCredentialsAndReconnect(0)
  • Fix B:_schedulePoll('general') 等待后调用 _doConnect
  • _intentionalClose=true 时慢轮询应跳过
  • _refreshCredentialsAndReconnect 抛错路径调度慢轮询

测试全部通过(7/7)。

外部因素的叠加

这个时期同步发生了另一个外部问题:CDN 对 WebSocket 空闲连接会在 4-9 分钟后强制切断。客户端收到连接中断后根据指数退避策略重连,如果恰好命中上述三条路径之一,就进入永久断开状态。这解释了为什么故障特别在长时间挂机后高发——空闲足够长,CDN 必定切断,而后续重连很容易落入某条缺陷路径。两个问题各自独立,但组合效果是"挂机过夜必死"。

排查验证

故障修复后,可以用以下方式验证重连状态:

__clawpanelWsClient.getConnectionInfo()
// {
//   connected: false,
//   reconnectState: 'scheduled' | 'attempting',
//   reconnectAttempts: 0~60,
//   ...
// }

reconnectState='scheduled'reconnectAttempts=0 表示在慢轮询窗口正常工作;reconnectState='idle'connected=false 是老的永久断开路径。

重连机制里避免静默终态是最基本的要求。任何异常路径都必须有可见的状态提示和后续操作入口,否则用户端看到的就是死机。这次故障的根本教训是,不能让客户端在任何情况下进入"不可恢复"的状态而没有任何提示。慢轮询的引入给了所有失败情景一个"最后的机会",即使前面的快速重连机制彻底耗尽了,用户等待足够长的时间后系统仍有自动恢复的可能。

星野的头像

星野 XINGYE

一个人维护 AI 平台的工程师。这里记录 63 篇复盘:18 份故障档案、OTA、架构演进与工作流。