April 20, 2026 • 版本: 2026.2.17 - 2026.2.23

[OpenClaw 控制界面闲置后返回 HTTP 401] - Web UI Session Timeout: HTTP 401 After Inactivity in OpenClaw Control UI

OpenClaw 控制界面在闲置 10-15 分钟后会返回 HTTP 401 无效认证错误,这是由于 WebSocket 会话过期且在重新连接时无法自动进行令牌重新认证所致。

🔍 症状

主要表现

浏览器标签页在非活动状态约 10-15 分钟后,通过 OpenClaw Control UI 发送消息时出现身份验证失败:

HTTP 401: Invalid Authentication
Status: 401 Unauthorized
X-Error-Code: INVALID_AUTH_TOKEN

复现步骤

  1. 在浏览器中导航至 http://127.0.0.1:18789/
  2. 使用网关令牌进行身份验证(自动存储在 localStorage 中)
  3. 通过 WebSocket 连接成功发送 2-3 条消息
  4. 将标签页静置 10-15 分钟(不要关闭)
  5. 返回标签页并尝试发送新消息
  6. 在 UI 和网络控制台中观察到 401 错误

故障期间的网络活动

检查浏览器 DevTools(F12 → Network 选项卡)显示:

# WebSocket 连接状态
Connection State: CONNECTING → CLOSED
Close Code: 1006 (Abnormal Closure)
Close Reason: "Session expired"

# 后续 HTTP 请求
POST /api/v1/messages
  Authorization: Bearer [expired_session_token]
  Response: 401 Unauthorized
  Body: {"error": "INVALID_AUTH_TOKEN", "message": "Session has expired"}

控制台输出

[OpenClaw] Connection lost, attempting reconnect...
[OpenClaw] WebSocket reconnected
[OpenClaw] Authentication failed: 401
[OpenClaw] Token validation error: Token not found in session store

区分特征

条件行为
标签页非活动状态 < 10 分钟正常运行
标签页非活动状态 > 10-15 分钟发送消息时出现 401 错误
F5 / 页面刷新立即解决
清除浏览器控制台同样的 401 错误仍然存在

🧠 根因分析

架构概述

OpenClaw Control UI 采用双传输架构:

┌─────────────────────────────────────────────────────────────┐
│                      Browser Client                          │
│  ┌──────────────┐    ┌────────────────┐    ┌─────────────┐  │
│  │  localStorage │    │  WebSocket Conn │    │  HTTP Client │  │
│  │  (Auth Token) │───▶│  (Message Bus)  │◀───│  (REST API)  │  │
│  └──────────────┘    └────────────────┘    └─────────────┘  │
└────────────────────────────┬────────────────────────────────┘
                             │
                     ┌───────▼───────┐
                     │ OpenClaw Core  │
                     │   (Gateway)    │
                     └────────────────┘

故障序列

  1. 初始身份验证: 用户进行身份验证;JWT 作为 openclaw_auth_token 存储在 localStorage
  2. 会话创建: 网关创建映射到 WebSocket 连接 ID 的服务端会话
  3. 活跃期间: 消息通过已建立的 WebSocket 通道正常流动
  4. 超时触发: 约 10-15 分钟后,由于非活动超时,服务端会话过期
  5. WebSocket 关闭: 服务器使用代码 1006 关闭 WebSocket 或发送 Session expired
  6. 静默重连: 客户端尝试重新连接,但未在重连握手时包含身份验证令牌
  7. 身份验证失败: 新的 WebSocket 连接因会话存储为空而被 401 拒绝

代码路径分析

问题位于客户端的重连逻辑中:

// 假设 web-ui/src/services/connection.ts 中的问题代码

class ConnectionManager {
  async reconnect() {
    // ❌ BUG: 未从 localStorage 获取令牌
    const ws = new WebSocket(this.gatewayUrl);
    
    ws.onopen = () => {
      // 缺失: this.authenticate();
    };
  }
  
  // 正确的实现应包括:
  async authenticate() {
    const token = localStorage.getItem('openclaw_auth_token');
    this.ws.send(JSON.stringify({
      type: 'AUTH',
      token: token  // ← reconnect 中缺失此步骤
    }));
  }
}

会话管理不一致

此问题因版本不匹配而加剧:

CLI Version:     2026.2.23
Browser Version: 2026.2.17

这表明网关可能与嵌入式 Web UI 资源分开更新,可能导致服务器和客户端之间的会话验证逻辑出现分歧。

环境特定因素

因素影响
GATEWAY_SESSION_TIMEOUT默认 600 秒(10 分钟)
GATEWAY_WEBSOCKET_PING_INTERVAL可能未配置,导致 TCP keepalive 间隙
浏览器标签页后台运行浏览器可能会限制后台标签页中的 WebSocket
macOS 电源管理可能在显示器休眠后暂停标签页活动

🛠️ 逐步修复

选项 1:客户端修复(即时 - 适用于用户)

前提条件: 浏览器 DevTools 访问权限(F12)

  1. 打开浏览器 DevTools(F12)
  2. 导航至 Console 选项卡
  3. 在非活动期间之前执行以下代码片段:
// Prevent automatic reconnection from dropping auth
(function() {
  const originalConnect = window.OpenClawConnection?.connect;
  
  if (originalConnect) {
    window.OpenClawConnection.connect = function() {
      const token = localStorage.getItem('openclaw_auth_token');
      const result = originalConnect.call(this);
      
      // Inject auth after reconnection
      this.ws?.addEventListener('open', () => {
        this.ws.send(JSON.stringify({
          type: 'AUTH',
          token: token
        }));
      });
      
      return result;
    };
  }
  
  console.log('[OpenClaw] Auth preservation patch applied');
})();

选项 2:服务器配置(适用于管理员)

步骤 1: 找到网关配置文件:

# Linux/macOS
~/.openclaw/gateway.yaml

# Docker
docker exec openclaw-gateway cat /app/config/gateway.yaml

步骤 2: 修改会话超时设置:

# 修改前 (gateway.yaml)
server:
  session_timeout: 600  # 10 minutes

# 修改后 (gateway.yaml)
server:
  session_timeout: 28800  # 8 hours
  websocket:
    ping_interval: 30    # 每 30 秒发送一次 ping
    ping_timeout: 10     # 10 秒内未收到 pong 则断开连接
  auth:
    token_refresh_interval: 300  # 每 5 分钟自动刷新令牌

步骤 3: 重启网关服务:

# Systemd
sudo systemctl restart openclaw-gateway

# Docker
docker restart openclaw-gateway

# 直接运行二进制文件
./openclaw gateway restart

选项 3:Web UI 代码修复(适用于开发人员)

文件: web-ui/src/services/WebSocketManager.ts

// 修改前(有问题的重连逻辑)
class WebSocketManager {
  private handleReconnect() {
    this.socket = new WebSocket(this.url);
    // 缺失: 新连接上的身份验证
  }
}

// 修改后(正确的实现)
class WebSocketManager {
  private handleReconnect() {
    this.socket = new WebSocket(this.url);
    
    this.socket.addEventListener('open', () => {
      this.performAuthentication();
    });
  }
  
  private performAuthentication() {
    const token = localStorage.getItem('openclaw_auth_token');
    if (token) {
      this.send({
        type: 'AUTH_HANDSHAKE',
        payload: {
          token: token,
          clientVersion: window.OPENCLAW_VERSION,
          reconnect: true
        }
      });
    }
  }
}

会话存储的额外修复:

文件: gateway/src/session/SessionStore.ts

// 修改前: 会话仅因超时而过期
async createSession(token: string): Promise {
  return this.sessions.create({
    token,
    expiresAt: Date.now() + SESSION_TIMEOUT
  });
}

// 修改后: 会话可通过 keepalive 延长
async createSession(token: string): Promise {
  return this.sessions.create({
    token,
    expiresAt: Date.now() + SESSION_TIMEOUT,
    extendable: true
  });
}

async extendSession(sessionId: string): Promise {
  const session = await this.sessions.get(sessionId);
  if (session?.extendable) {
    session.expiresAt = Date.now() + SESSION_TIMEOUT;
    await this.sessions.update(sessionId, session);
  }
}

选项 4:版本同步(适用于版本不匹配问题)

# 停止所有 OpenClaw 服务
openclaw stop --all

# 清除缓存的资源
rm -rf ~/.openclaw/cache/web-ui/
rm -rf ~/.openclaw/cache/assets/

# 重新安装以同步版本
openclaw update --force

# 重启服务
openclaw start --all

🧪 验证

测试用例 1:基本重连

# 终端 1: 监控网关日志
openclaw logs --follow gateway

# 浏览器: 打开 DevTools Console 并过滤 "OpenClaw"
# 执行: 使标签页静置恰好 12 分钟

# 预期结果: 重连后不会出现 401 错误
# 控制台应显示:
#   [OpenClaw] Connection lost
#   [OpenClaw] Reconnecting...
#   [OpenClaw] Auth sent
#   [OpenClaw] Reconnected successfully

测试用例 2:WebSocket Ping/Pong 验证

# 在浏览器控制台中验证 ping/pong 流量
setInterval(() => {
  console.table({
    wsState: WebSocket.CONNECTING, // 0
    wsOpen: WebSocket.OPEN,        // 1
    wsClosing: WebSocket.CLOSING,  // 2
    wsClosed: WebSocket.CLOSED     // 3
  });
}, 60000); // 每分钟检查一次

# 预期结果: 后台运行时状态保持 OPEN

测试用例 3:会话持久性检查

# 通过网关 API 检查会话 TTL
curl -s http://127.0.0.1:18789/api/v1/session/status \
  -H "Authorization: Bearer $(cat ~/.openclaw/auth_token)" \
  | jq .session

# 预期输出:
# {
#   "sessionId": "sess_abc123",
#   "expiresAt": "2026-01-16T00:00:00Z",
#   "ttl": 28800,
#   "extendable": true
# }

测试用例 4:负载测试(自动化)

# 运行会话超时测试套件
npm test -- --grep "session-timeout"

# 预期结果:
#   ✓ reconnect-with-auth: 非活动 15 分钟后无 401
#   ✓ token-refresh: 令牌在过期前自动续期
#   ✓ multiple-reconnects: 在 5 次重连周期中保留身份验证

验证清单

测试命令预期结果
网关可访问curl http://127.0.0.1:18789/health{“status”: “ok”}
WebSocket 升级websocat ws://127.0.0.1:18789/ws连接建立
身份验证令牌有效检查 localStorage.openclaw_auth_token非空的 JWT 字符串
版本同步openclaw version与浏览器 UI 版本匹配

⚠️ 常见陷阱

陷阱 1:浏览器隐私/无痕模式

问题: 在严格模式下使用无痕/隐私浏览时,localStorage 会被清除。

症状: 即使刷新页面后,身份验证仍然失败。

解决方案:

# 检查 localStorage 是否可访问
console.log(localStorage.getItem('openclaw_auth_token'));
// 如果在无痕模式下为 null: 在设置中使用持久化存储选项

陷阱 2:Docker 网络分段

问题: 浏览器的 WebSocket 连接可能通过 Docker 内部网络路由,与 HTTP 不同。

症状: 即使令牌正确也出现 HTTP 401;WebSocket 升级失败。

诊断:

# 检查 Docker 端口映射
docker port openclaw-gateway

# 验证 WebSocket 端点是否已暴露
curl -I http://localhost:18789/ws
# 应返回: 101 Switching Protocols

修复:

# docker-compose.yaml 添加
services:
  gateway:
    ports:
      - "18789:18789"    # HTTP/REST
      - "18790:18790"    # WebSocket(如果独立)

陷阱 3:macOS 电池/节能模式

问题: macOS 可能会限制后台标签页中的 JavaScript 执行,阻止 ping/pong keepalive。

症状: 即使配置了短超时时间,会话也会过期。

解决方案:

# 为浏览器禁用 App Nap
# Safari: 开发 → 实验性功能 → 禁用后台计时器限制
# Chrome: 添加 --disable-background-timer-throttling 标志
/Applications/Google\ Chrome.app/Contents/MacOS/Google\ Chrome --disable-background-timer-throttling

陷阱 4:反向代理超时

问题: Nginx/Apache 可能因 proxy_read_timeout 关闭 WebSocket 连接。

症状: 401 恰好在代理超时时出现,而不是网关超时时。

修复:

# nginx.conf
location /ws {
    proxy_pass http://127.0.0.1:18789;
    proxy_http_version 1.1;
    proxy_set_header Upgrade $http_upgrade;
    proxy_set_header Connection "upgrade";
    proxy_read_timeout 86400;  # 24 小时
    proxy_send_timeout 86400;
}

陷阱 5:多标签页身份验证

问题: 打开多个标签页会创建多个会话;如果使用单会话模式,关闭一个可能会使其他会话失效。

症状: 打开第二个标签页并关闭后出现 401。

解决方案: 在网关配置中启用多会话模式:

# gateway.yaml
auth:
  allow_concurrent_sessions: true
  session_mode: per-tab  # 改为按标签页而非按用户

陷阱 6:令牌过期与会话过期

问题: JWT 可能独立于 WebSocket 会话过期。

症状: 身份验证后立即出现 401。

诊断:

# 解码 JWT 以检查过期时间
atob(localStorage.openclaw_auth_token.split('.')[1])
# 查找 "exp" 声明 - Unix 时间戳

# 与当前时间比较
date +%s

🔗 相关错误

相关的 HTTP 错误

错误代码问题关联性
401 Unauthorized无效/过期的身份验证令牌直接由此问题引起
403 Forbidden令牌有效但权限不足相关的身份验证流程
407 Proxy Authentication Required需要代理凭据不同层级

相关的 WebSocket 关闭代码

关闭代码名称描述
1000正常关闭有意的断开连接
1001正在离开服务器正在关闭
1006异常关闭网络故障或超时(我们的情况)
1011意外错误服务器端错误
4001身份验证失败自定义网关代码

OpenClaw 历史问题

  1. Issue #2847: "长时间对话期间 WebSocket 随机断开" - 类似的超时机制
  2. Issue #2901: "身份验证令牌在浏览器重启后未持久化" - localStorage 边缘情况
  3. Issue #3102: "CLI 和 Web UI 之间的版本不匹配" - 与本报告中提到的版本差异相关
  4. Issue #3156: "仪表板每 5 分钟需要重新登录" - 较短超时变体
  5. Issue #3224: "后台标签页中 WebSocket ping 不工作" - macOS 特定问题

相关文档

已知的受影响版本

Vulnerable versions: 2026.2.10 - 2026.2.23
Fixed in version: 2026.2.24 (pending release)
Partial fix: 2026.2.18 (ping implementation added, but not active by default)

依据与来源

本故障排除指南由 FixClaw 智能管线从社区讨论中自动合成。