环境
- ClawPanel: 0.16.5
- OpenClaw Gateway: 2026.5.27
- 部署方式: Docker/headless web server
- 容器系统: Ubuntu 24.04
- ClawPanel 监听: 0.0.0.0:1420
- OpenClaw Gateway 监听: 127.0.0.1:18789
- 外部访问: HTTPS + 反向代理,域名形如 https://example.com
- WebSocket 地址: wss://example.com/ws?token=...
现象
页面刷新后,Dashboard/聊天页显示“WebSocket 握手中 / WebSocket 重连中 #0”。浏览器调试日志显示 WebSocket 实际已经连通并完成握手:
[ws:open] wss://example.com/ws?token=...
[ws:msg] {"type":"event","event":"connect.challenge",...}
[api] 200 /__api/create_connect_frame
[ws:msg] {"type":"res","ok":true,"payload":{"type":"hello-ok","protocol":4,"server":{"version":"2026.5.27"}}}
[ws:msg] {"type":"event","event":"health","payload":{"ok":true,...}}
随后前端会请求:
[api] 500 /__api/reload_gateway
容器日志中能看到 Gateway 被 SIGTERM,导致刚建立的 WebSocket 被关闭,然后页面进入重连循环:
[gateway] signal SIGTERM received
[gateway] received SIGTERM; shutting down
[shutdown] websocket server close exceeded 1000ms; forcing shutdown continuation with 1 tracked client(s)
exited: openclaw (exit status 0; expected)
spawned: 'openclaw' with pid ...
根因分析
这不是单纯“反代没有开启 WebSocket”的问题。因为浏览器已经收到了 connect.challenge、hello-ok 和 health,说明 /ws 已经通过反代连到了 Gateway。
现场排查发现主要有几条自动重载路径在 Docker/Web/headless + 反代环境下会互相打架:
-
src/main.js -> autoConnectWebSocket() 在自动连接前会执行 api.autoPairDevice() 和 api.patchModelVision(),只要认为配置有变化,就调用 api.reloadGateway()。在 Web/Docker 模式中,这会把刚准备建立或刚建立的 Gateway WS 会话重启掉。
-
src/lib/ws-client.js -> _autoPairAndReconnect() 在自动配对后也会直接调用 api.reloadGateway(),同样会造成 Gateway 重启和 WS 断开。
-
src/lib/tauri-api.js -> _debouncedReloadGateway() 会在 writeOpenclawConfig() / calibrateOpenclawConfig() 后 3 秒自动调用 reload_gateway。这在桌面端也许合理,但在 Web/Docker 模式中容易变成“后台配置写入 -> 自动重载 Gateway -> WS 被踢掉”的循环。
另外还有一个 UI 层问题:src/pages/dashboard.js -> renderWsStatus() 会根据 wsClient.reconnectState 渲染“WebSocket 重连中 #0”。Dashboard 初次渲染时如果刚好处于 attempting/scheduled,后续 wsClient 进入 ready 后该状态块没有及时订阅 ready/status 事件刷新,导致页面看起来仍然是“重连中 #0”,即使实际 WS 已经 hello-ok。
临时验证/本地补丁
在现场容器里做了以下临时补丁后恢复正常:
- Web/Docker 模式下,禁止自动连接路径调用
reloadGateway。
- Web/Docker 模式下,禁止
_autoPairAndReconnect() 自动 reload Gateway。
- Web/Docker 模式下,
_debouncedReloadGateway() 直接 return,只保留显式按钮/服务页的手动 restart/reload。
- Headless server 里把
/__api/reload_gateway 暂时改成 no-op,避免旧前端缓存继续触发 Gateway 重启。
- Dashboard 增加
wsClient.onStatusChange / wsClient.onReady,只刷新 WebSocket 状态块。
- 静态 assets 响应头从
public, max-age=31536000, immutable 改为 no-cache, no-store, must-revalidate,避免 Docker 中临时修复后浏览器仍长期使用旧 JS。
补丁后浏览器显示:
WebSocket 已连接
Gateway 2026.5.27
Gateway 不再出现新的 SIGTERM。
建议修复
建议区分 Tauri 桌面端和 Web/headless Docker 模式:
- 自动连接、自动配对、配置校准等“隐式流程”不要在 Web/headless 模式自动重启 Gateway。
- 如果确实检测到配置变化,可以提示用户“需要手动重载 Gateway”,或只在明确点击保存/重载按钮时执行。
writeOpenclawConfig(config, opts) 建议默认不隐式 reload,或至少在 Web/headless 下默认 noReload: true。
reload_gateway API 在 headless 模式中最好有更明确的调用来源和日志,便于排查是谁触发重启。
- Dashboard 的 WebSocket 状态块应订阅
wsClient.onStatusChange / onReady,避免停留在首次渲染时的 reconnecting #0。
- Docker/headless 版如果需要热修或升级,assets 使用 immutable 长缓存时要确保 HTML hash 变化能可靠让用户拉到新 bundle。
预期行为
在 Docker/Web + HTTPS 反代环境中,只要 /ws 已成功完成 hello-ok,ClawPanel 不应自动重启 Gateway 导致 WS 断开;Dashboard 应及时显示“WebSocket 已连接 / Gateway 版本”。
环境
现象
页面刷新后,Dashboard/聊天页显示“WebSocket 握手中 / WebSocket 重连中 #0”。浏览器调试日志显示 WebSocket 实际已经连通并完成握手:
随后前端会请求:
容器日志中能看到 Gateway 被 SIGTERM,导致刚建立的 WebSocket 被关闭,然后页面进入重连循环:
根因分析
这不是单纯“反代没有开启 WebSocket”的问题。因为浏览器已经收到了
connect.challenge、hello-ok和health,说明/ws已经通过反代连到了 Gateway。现场排查发现主要有几条自动重载路径在 Docker/Web/headless + 反代环境下会互相打架:
src/main.js -> autoConnectWebSocket()在自动连接前会执行api.autoPairDevice()和api.patchModelVision(),只要认为配置有变化,就调用api.reloadGateway()。在 Web/Docker 模式中,这会把刚准备建立或刚建立的 Gateway WS 会话重启掉。src/lib/ws-client.js -> _autoPairAndReconnect()在自动配对后也会直接调用api.reloadGateway(),同样会造成 Gateway 重启和 WS 断开。src/lib/tauri-api.js -> _debouncedReloadGateway()会在writeOpenclawConfig()/calibrateOpenclawConfig()后 3 秒自动调用reload_gateway。这在桌面端也许合理,但在 Web/Docker 模式中容易变成“后台配置写入 -> 自动重载 Gateway -> WS 被踢掉”的循环。另外还有一个 UI 层问题:
src/pages/dashboard.js -> renderWsStatus()会根据wsClient.reconnectState渲染“WebSocket 重连中 #0”。Dashboard 初次渲染时如果刚好处于attempting/scheduled,后续wsClient进入ready后该状态块没有及时订阅 ready/status 事件刷新,导致页面看起来仍然是“重连中 #0”,即使实际 WS 已经hello-ok。临时验证/本地补丁
在现场容器里做了以下临时补丁后恢复正常:
reloadGateway。_autoPairAndReconnect()自动 reload Gateway。_debouncedReloadGateway()直接 return,只保留显式按钮/服务页的手动 restart/reload。/__api/reload_gateway暂时改成 no-op,避免旧前端缓存继续触发 Gateway 重启。wsClient.onStatusChange/wsClient.onReady,只刷新 WebSocket 状态块。public, max-age=31536000, immutable改为no-cache, no-store, must-revalidate,避免 Docker 中临时修复后浏览器仍长期使用旧 JS。补丁后浏览器显示:
Gateway 不再出现新的 SIGTERM。
建议修复
建议区分 Tauri 桌面端和 Web/headless Docker 模式:
writeOpenclawConfig(config, opts)建议默认不隐式 reload,或至少在 Web/headless 下默认noReload: true。reload_gatewayAPI 在 headless 模式中最好有更明确的调用来源和日志,便于排查是谁触发重启。wsClient.onStatusChange/onReady,避免停留在首次渲染时的reconnecting #0。预期行为
在 Docker/Web + HTTPS 反代环境中,只要
/ws已成功完成hello-ok,ClawPanel 不应自动重启 Gateway 导致 WS 断开;Dashboard 应及时显示“WebSocket 已连接 / Gateway 版本”。