# Bug 巡查 — 远程浏览器

**日期**：2026-06-12  
**范围**：`browser-worker/`、桥接 API、前端 canvas 客户端、生产 `66.154.115.131:8443`  
**方法**：代码走读 + 单测 + 生产探针复验  
**关联**：`docs/audit/2026-06-12-remote-browser-worker.md`、`docs/reports/2026-06-12-remote-browser-patrol.md`

## 发现与处理

| # | 严重度 | 问题 | 根因 | 状态 |
|---|--------|------|------|------|
| 1 | **P0** | 生产无浏览器 UI，用户「连不上」 | 后端 `BROWSER_ENABLED=1`，但 `web/app` **无** `ws/browser` bundle | **已修** — `index-BimRgA6s.js` 已同步，容器 grep `ws/browser` ≥1 |
| 2 | **P1** | Worker `sessions:1` 长期占槽 | `ready=false` 不进 idle 清理 | **已修** — `WORKER_RESERVE_TIMEOUT_SEC=120` + 重建 Worker，`sessions:0` |
| 3 | **P1** | attach 失败留下僵尸 reservation | `RuntimeError` 未 `destroy_session` | **已修** — `main.py` 失败路径销毁 |
| 4 | **P1** | 页内点击/重定向 SSRF | 仅 `NAVIGATE` 校验；`RELOAD`/`GO_BACK`/`页内导航` 无复验 | **未修**（审计 F1） |
| 5 | **P1** | Worker 8443 公网暴露 `/health` | ufw 过宽 | **未修**（审计 F3） |
| 6 | **P2** | 前进/后退与远程历史不同步 | UI `stack` 仅本地维护；`updateActiveFromRemote` **不**更新 stack；canvas 内点击只改 `url` | **未修** |
| 7 | **P2** | `connect()` 失败后无显式 DELETE | 同上 | **已修** — WS/未登录失败时 `DELETE /browser/sessions/{id}` |
| 8 | **P2** | `attach_stream` 并发竞态 | `ready=false` 时锁在 L140 释放，双 WS 可能各建 `BrowserContext` | 低概率（单桥接） |
| 9 | **P2** | `delete_worker_session` 失败静默 | 日志 warning，Worker 会话残留 | **未修**（审计 F4） |
| 10 | P3 | 切标签每次新建 Worker 会话 | `watch(activeTabId)` → `disconnect()` → 新 `POST` | 设计可接受，略耗资源 |
| 11 | P3 | `persistState` catch 空实现 | 离线时本地状态与 MySQL 可能短暂不一致 | 可接受 |

## P0 — 前端未部署（用户可见）

```
生产容器: BROWSER_ENABLED=1
web/app/assets: grep ws/browser → 0 文件
Worker /health: sessions=1, max_sessions=2
```

管理员打开面板**没有任何**地球按钮 / canvas 客户端代码，与「连不上」完全一致。后端与 Worker 已就绪，**阻塞在 SPA 同步**。

## P1 — Worker 僵尸会话（占槽根因）

### 空闲清理漏掉 `ready=false`

```100:103:browser-worker/session_manager.py
            stale = [
                sid for sid, sess in self._sessions.items()
                if sess.ready and now - sess.last_activity > settings.WORKER_IDLE_TIMEOUT_SEC
            ]
```

`POST /v1/sessions` 只 `reserve_session`；Chromium 在 WS `attach_stream` 后才 `ready=True`。  
任一路径「POST 成功但 stream 未建立」都会永久占 `session_count`，直至 `max_sessions` 打满。

典型路径：

- 前端未部署时 API 测试 / 桥接探测只 POST 不挂 WS  
- Nexus 桥接连 Worker WS 失败（`WORKER_UNREACHABLE`）  
- Worker `attach_stream` 抛 `RuntimeError` 后：

```110:112:browser-worker/main.py
    except RuntimeError:
        await websocket.close(code=4503, reason="Browser not ready")
        return
```

此处 **未** `destroy_session`，reservation 泄漏。

**建议**：idle 对 `not ready` 加 `created_at` 超时（如 120s）；`RuntimeError`/`attach` 失败路径 `destroy_session`；运维可 `docker restart nexus-browser-worker` 清槽。

## P1 — SSRF / 暴露面（审计复查）

与 `docs/audit/2026-06-12-remote-browser-worker.md` 一致，代码层**无新增修复**。

## P2 — 前进/后退逻辑 Bug

`GlobalBrowserPanel.onGoBack`：

```350:353:frontend/src/components/GlobalBrowserPanel.vue
function onGoBack() {
  goBack()                    // 改本地 stack + addressDraft
  if (connected.value) remoteGoBack()  // Playwright page.go_back()
}
```

- 用户在 canvas 内点击链接 → `PAGE_INFO` → `updateActiveFromRemote` 只更新 `tab.url`，**不 push stack**  
- `canGoBack` 仍看本地 stack → 按钮状态错误  
- 点后退时本地 stack 与 Chromium 历史**可能指向不同 URL**

**建议**：以远程为准时去掉本地 stack，或 `PAGE_INFO` 时同步 push stack；后退只发 `GO_BACK` 不回写本地 stack 直到 `PAGE_INFO`。

## 本地验证

```text
pytest tests/test_browser_url_safe.py tests/test_browser_ui_state.py → 7 passed
npm run type-check → PASS
curl https://api.synaglobal.vip/health → ok
```

## 建议修复顺序

1. **P0** — `vite build` + `sync_webapp_to_container.sh`（立刻恢复可用性）  
2. **P1** — Worker 重启清槽 + 修 reservation 泄漏（idle 未 ready + RuntimeError 清理）  
3. **P1** — ufw 收口 8443  
4. **P1** — `framenavigated` SSRF  
5. **P2** — 历史栈与 `GO_BACK` 统一  

## 复巡命令

```bash
ssh nexus 'sudo docker exec nexus-prod-nexus-1 grep -rl ws/browser /app/web/app/assets/ | wc -l'
curl -s http://66.154.115.131:8443/health
pytest tests/test_browser_url_safe.py tests/test_browser_ui_state.py -q
```
