接入 Playwright Worker、会话 WebSocket 与全局浏览器面板(固定于 App Bar 下); 含验证码 stealth、设置项默认音与 URL 安全校验;附带 worker 部署与设计文档。 Co-authored-by: Cursor <cursoragent@cursor.com>
5.0 KiB
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
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后:
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:
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。
本地验证
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
建议修复顺序
- P0 —
vite build+sync_webapp_to_container.sh(立刻恢复可用性) - P1 — Worker 重启清槽 + 修 reservation 泄漏(idle 未 ready + RuntimeError 清理)
- P1 — ufw 收口 8443
- P1 —
framenavigatedSSRF - P2 — 历史栈与
GO_BACK统一
复巡命令
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