feat(browser): self-heal sessions after camofox restarts (BR-3)

- Any session operation that hits 404/410 (or finds no open tab) reconciles
  tabs; if none survive it re-injects the account cookies (BR-2), opens a new
  tab under the same userId/sessionKey, returns to the recorded URL (site home
  otherwise), marks old tabs lost and retries the operation once. Failure
  returns 503 browser_reconnect_failed with the reason, with a 10s backoff.
- Restart detection: umt plugin GET /umt/instance (fallback /health fields);
  a changed instance id marks every open session needsReconnect.
- POST /browser/sessions/{id}/reconnect (idempotent); session view exposes
  needsReconnect and reconnect {status, trigger, at, reason, humanNotice}.
- Controller and lease are kept; reconnecting during takeover asks the human
  to double-check the reloaded page.
- Web: screenshot and desktop modes show "reconnecting", auto-reconnect once,
  pause polling, offer a reconnect button with the reason on failure; desktop
  mode rebuilds the Selkies connection after a reconnect.
- Mock camofox restart(), tests, smoke step and runbook section 10.

Co-Authored-By: Claude Opus 5.5 <[email protected]>
This commit is contained in:
rootandClaude Opus 5.5 committed 2026-09-29 17:29:11 +08:00
1 parent da24380870
commit 838aea3ebd
19 files changed
+977 -45

No files matched your search

+56 -2
View File
@@ -79,7 +79,7 @@ docker compose build camofox # 再构建桌面镜像(或 dock
- 会话按站点 / 账号隔离:`profileKey = umt-site-<siteId>-acct-<accountId|anon>`,同时作为 camofox `userId`
(上游的登录状态隔离单元);`browser_session.id` 作 camofox `sessionKey`。同一 provider + profile 至多一个未关闭会话。
- 数据库为准:服务重启后按 `browser_session` / `browser_tab` 恢复适配器内存,`needsSnapshot` 置 true,旧元素引用全部作废。
上游回收的标签页(404)对账标 `lost`,返回 409"标签页已被浏览器服务回收"。
上游回收的标签页(404 / 410)对账标 `lost`;整个会话的标签页都没了(camofox 重启、空闲回收)时自动重连,见 §10。
- 控制者:`automation`(Agent/任务)→ `human`(人工接管)→ `automation` 或 `waiting`;`closed` 终态。
| 事件 | 结果 |
@@ -174,7 +174,9 @@ execute 类需要持续授权(`automation_grant`)或审批。元素引用只来
| 409 先接管会话再操作 | 界面点"接管";或租约已被他人持有 / 已过期 |
| "连接中断" | 截图请求失败;看 API 日志 `component: browser` 的方法 / 路径 / 状态(日志不含正文与凭据) |
| 测试连通提示"访问凭据不对" | `/health` 免认证所以能连上,但 `/openapi.json` 401:`BROWSER_ACCESS_KEY` 与 camofox 的 `CAMOFOX_ACCESS_KEY` 不一致 |
| 409"标签页已被浏览器服务回收" | camofox 重启(旧标签页返回 410)或会话空闲回收(404);Agent 再次 `browser.navigate` 会在同一会话开新标签页 |
| 原站视图"浏览器已重启,正在重新连接…" | camofox 重启 / 会话空闲回收后的自动重连(§10),正常几秒内恢复 |
| 503 `browser_reconnect_failed` / "浏览器连接已断开" | 自动重连失败,消息里是原因(camofox 没起来、`POST /tabs` 5xx 等);`docker compose ps camofox`、容器日志;恢复后点"重新连接" |
| 409"标签页已被浏览器服务回收" | 只在显式指定了已被回收的 `tabId` 时出现(会话里还有别的标签页活着);不指定 `tabId` 重试即可 |
| 重启 camofox 后要重新登录 | 容器是被 SIGKILL / OOM 杀掉的(`docker inspect` 看 `OOMKilled`、退出码 137):只有 SIGTERM 正常停止、会话空闲回收、关闭会话时才落盘登录状态;加大 `CAMOFOX_MEM_LIMIT` |
| 设置页显示"未核验" | 探测到的上游版本不在核验表里(不是 1.17.0,或 fork);按 §6 核验后在适配器 `CAMOFOX_VERIFIED_RELEASES` 加一行,或临时写 `config.verified` |
@@ -358,6 +360,7 @@ camofox 1.17.0 只有 `POST /sessions/:userId/cookies`(导入),而且要另一
| --- | --- |
| `POST /umt/sessions/{userId}/cookies` `{cookies}` | `context.addCookies`,并触发持久化插件立即落盘(与上游导入端点相同) |
| `GET /umt/sessions/{userId}/cookies?urls=a,b` | 作用于这些地址的 Cookie:会话活着读 `context.cookies(urls)`,否则读 profile 落盘文件 |
| `GET /umt/instance` | `{instanceId, startedAt}`:插件加载时生成,camofox 进程重启即变(BR-3 重启检测,§10) |
适配器先走插件;插件不在(404)再试上游导入端点(`CAMOFOX_API_KEY` 与访问凭据相同时可用,只能写不能读);都不行 → 会话照常打开,
`credential.status = unsupported` 并说明。profile 落盘在 `/root/.camofox/profiles/<sha256(userId) 前 32 位>/storage-state.json`
@@ -402,3 +405,54 @@ Cookie 账号替换 `secret_ref`,账密账号替换(或新建)会话 Cookie;`sit
没有核验的:桌面镜像重建后的端到端(接管 → 画面里登录 → 交还回写)只在 mock 上测过;其它三站;sessionStorage / IndexedDB 登录的站点
(Cookie 注入不覆盖这类登录态)。站点账号列表里显示登录态来源待前端接 `userInfo.cookieOrigin`。
## 10. 重启恢复(BR-3)
camofox 容器重启(`docker compose up -d` 重建、OOM、手动 restart)后,上游的标签页全部消失:旧 tabId 返回 410 `browser_restarted`
(或 404),`GET /tabs` 为空。数据库里的会话还在,app 自动把它接回来,不需要关闭重开。代码:`modules/browser/service.ts`
(`checkInstance` / `reconnectIfNeeded` / `doReconnect`、`run` 里的自愈)、适配器 `reopenSession` / `instanceId`、
前端 `components/browser/reconnect.tsx`。
### 10.1 怎么发现
| 途径 | 做法 |
| --- | --- |
| 被动(总是生效) | 任一会话操作(截图、快照、导航、点击、输入、提取、下载、视觉动作)收到 404 / 410,或适配器里已没有打开的标签页 → 按 `GET /tabs` 对账;会话一个活标签页都不剩 → 标 `needsReconnect`(`trigger = tab_gone`)并立即重连 |
| 主动(需要 umt 插件) | 会话详情 / 操作前查实例标识(`GET /umt/instance`,插件不在时退回 `/health` 里的 `instanceId` / `bootId` / `startedAt`),每个浏览器服务 5 秒最多一次;和会话打开时记下的 `recovery_state.instanceId` 不同 → 该服务下全部未关闭会话标 `needsReconnect`(`trigger = provider_restarted`),下一次操作或原站视图触发重连 |
纯截图镜像 `camofox-browser:1.17.0` 不带插件,只有被动发现;桌面镜像(缺省)两种都有,桌面模式因此也能在不截图的情况下发现重启。
### 10.2 重连做什么
1. 按会话的站点 / 账号准备 Cookie(与打开会话同一套,§9.1:匿名 / API Key 类不注入;账密账号没有会话 Cookie 先 refresh-login);
2. 写进该 profile(`POST /umt/sessions/{userId}/cookies`),**再**用同一 `userId` / `sessionKey` 开新标签页;
3. 导航到会话记录的 `current_url`(不在站点域名内或是脱敏过的地址 → 站点首页);
4. 旧的打开中标签页标 `lost`,换当前标签页;`needsSnapshot = true`(旧元素引用作废),记 `recovery_state.instanceId` 与 `reconnect`;
5. 重试原操作**一次**。
同一会话并发的操作共用一次重连。控制者与租约不变:Agent 的会话仍归 Agent;人工接管中重连,租约保留,会话详情 `reconnect.humanNotice = true`,
原站视图提示"浏览器重启过,页面已重新加载,请核对"(表单里没提交的输入、滚动位置都没了)。
失败(camofox 还没起来、`POST /tabs` 5xx、浏览器服务停用等):会话保持 `needsReconnect`,`reconnect.status = failed` 记原因,
接口返回 **503 `browser_reconnect_failed`**(消息带原因,不含凭据)。之后 10 秒内的操作直接报上次的原因,不反复打上游;
显式 `POST /browser/sessions/{id}/reconnect` 不受这个限制。
### 10.3 接口与界面
| 端点 | 说明 |
| --- | --- |
| `POST /browser/sessions/{id}/reconnect` | 幂等:强制查一次实例标识;没标需要重连时先对账,当前标签页还活着就什么都不做;否则按 §10.2 重建。不要求接管。返回会话详情 |
| `GET /browser/sessions/{id}` | 多了 `needsReconnect` 与 `reconnect {status, trigger, at, reason, humanNotice}` |
原站视图:截图模式收到 404 / 409(已回收)/ 503,或会话详情 `needsReconnect = true` → 显示"浏览器已重启,正在重新连接…",
自动调一次 `reconnect`,期间停掉截图轮询;成功后继续轮询;失败显示原因与"重新连接"按钮,不再每 1.5 秒刷错误(浏览器控制台只打一次 debug)。
桌面模式:同样按 `needsReconnect` 自动重连一次,成功(或别处触发的重连成功)后重建 Selkies 连接(iframe 重新加载)。
### 10.4 核验
- 自动化:`apps/server/src/modules/browser/__tests__/browser-reconnect.test.ts`(mock camofox `restart()`:标签页清空、旧 tabId 410、
实例标识换新、可选丢 profile)——截图自愈并注入 Cookie、回到原地址;实例标识变化 + `reconnect` 幂等;接管中重连保持租约;
失败 503 与退避;前端冒烟"原站视图 → 浏览器重启后自动重连(mock)"。
- 真机:未对真实 camofox 容器重启核验。桌面镜像要重建后才有 `GET /umt/instance`(`docker compose build camofox && docker compose up -d camofox`);
重建前只有被动发现(截图 / 操作时自愈),桌面模式要等一次截图或 Agent 操作才会重连。