Session 模型

面向内部工具的 UX 优先 session 模型 —— 仅做簿记,不做 consent。首 action 自动创建、tabId 作主键、多 session 语义、 以及各种终止路径。

为什么需要 Session 模型

BUA 是内部工具。Session 的角色是调试器挂载生命周期 + 审计的簿记,不是 consent token。没有 TTL、 没有 approval 步骤、没有 “Trust this site” 流程。Agent 在某个 tab 首次发起 action 时 session 随即被自动创建, 在生命周期事件触发(tab 关 / 用户 idle / 用户停止 / 被加入 blocklist)时结束。

扩展仍然强制一条硬边界:domain blocklist,每次 action 前检查。其它一切记录(审计日志)但不拦截

Session 结构

interface ActiveSession {
  tabId: number;        // 主键
  domain: string;       // 当前域名(导航时静默更新)
  startedAt: number;    // 创建时间戳(ms)
  lastActionAt: number; // 最近一次 action 派发时间戳
  actionCount: number;
}

持久化在 chrome.storage.localactiveSessions 键下。写操作通过内存 promise chain (withSessionWriteLock) 串行化 —— chrome.storage.local 无 CAS,并发改会互相覆盖。

Session 身份

Session 的主键是 tabId,不是 domain。同一个域名在多个 tab 上打开时可以同时存在多个 session。这是有意设计 —— 让 agent 在自己专属 tab 上工作(BUA window 里),用户继续在主浏览器的同域 tab 上正常浏览,互不干扰。

CDP 命令按 tabId 下发,一边的操作永远不会影响另一边。Popup 列出每个活跃 session(连同 tabId),用户可以精确停掉 其中一个,不牵连同域的其它 session。

Agent 省略 tabId 时的消岐义

大多数 action (clicktypeextract 等) 都支持可选 tabId。不传时扩展按以下顺序挑一个:

  1. 任意 BUA 拥有的 session(agent 自己开的 tab 永远是安全目标)。
  2. 用户拥有的 session,仅当它是唯一候选
  3. 否则报 session_not_found,并把候选清单带在 message 里,agent 应当改用显式 tabId 重试。

Prompt 里明确要求 agent 追踪 open_tab 的返回值 tabId,后续调用都带上。不传 tabId 只是对单 session 情况的便利, 不是让扩展猜。

Session 如何创建

两条路径,都不需要用户授权

路径触发方式落地 tab
自动 —— 首次 actionAgent 对尚无 session 的 tab 发任意 action那个 tab(无论在哪)
显式 —— open_tab浏览器 subagent 调 open_tab(url)默认落 BUA window 里的新 tab

首次 action 自动创建

当 agent 对 tabId=Tdomain=D 派发 action 时,action-dispatcher 的流程:

  1. 解析 tabId + domain(从 action 参数 + 实时 tab 状态)
  2. 检查 isDomainBlocked(domain) —— 命中即返回 domain_blocked(终止),发 domain_blocked WS 事件,不创建 session
  3. ensureSession(tabId, domain)
    • 尚无 session → startSession + SessionEvent { type: "start" } → 触发 session_started WS 事件
    • 存在同域 session → 复用
    • 存在不同域 session(tab 中途导航)→ 静默更新 session.domain 为新值,不发事件、不发 WS —— 审计日志会记录每次 action 的当前 URL,变更可追溯
  4. 获取 per-tab 锁
  5. 挂载 chrome.debugger(如未挂)
  6. 执行 CDP action
  7. recordAction(tabId) —— 累加 actionCount + 刷新 lastActionAt

v3 里的整个”consent”步骤消失了。授权是隐式的;accountability 在审计日志里。

open_tab(agent 发起)

Agent 调 browser({ action: { type: "open_tab", url: "https://…" } })。Dispatcher:

  1. 检查 blocklist(规则同自动创建)
  2. BUA window 里开新 tab(默认最小化 + 非聚焦 + 对用户不可见)。focus: true 会改成开在用户当前聚焦窗口 —— 仅在用户明确要求”给我看看”时使用
  3. startSession(domain, newTabId) → 触发 session_started WS 事件
  4. 返回 { tabId, windowId, domain } —— subagent 记下 tabId 用于后续所有 action

BUA 窗口

BUA 窗口是扩展为 agent 主动浏览专门创建的独立 Chrome 窗口,目的是让 agent 可以在已登录页面上操作却不打扰用户的主浏览器

不变量:

  • 默认隐藏 —— 用 chrome.windows.create({ focused: false, state: "minimized" }) 创建。永不抢焦点;用户通过 popup 的 Show 按钮按需查看
  • 懒创建 —— 只有 agent 第一次 open_tab 时才创建;agent 若永不自开 tab,则永远不存在 BUA 窗口
  • 自动收窗 —— 当 BUA 窗口里最后一个 tab 关闭时窗口也随之关闭,避免遗留的最小化窗口堆积
  • 窗口 id 持久化 —— 存在 chrome.storage.localbuaWindowId 键里以扛 SW 重启;监听 chrome.windows.onRemoved 在用户手动关窗时清除记录

路由规则:

  • Agent 的 open_tab → BUA 窗口,tab 以 active: false 打开,自动建 session
  • Agent 的 open_tab({ focus: true }) → 用户聚焦的窗口 + active: true。仅在用户明确要求”展示结果”时用

Session 如何结束

六种终止原因,一条清理路径。所有路径都汇聚到 endSessionByTab(tabId, reason)endAllSessions(reason),触发 SessionEvent { type: "end" } → 自动转发为 session_ended WS 事件。

原因触发
tab_closedchrome.tabs.onRemoved —— 一次性任务的正常路径
user_stoppedPopup 单 session “Stop now”,或 Chrome 黄条 “Cancel” → onDetach
system_idlechrome.idle 报告全机 30 min 无输入
screen_lockedchrome.idle 报告屏幕被显式锁定
domain_blocked用户把该域加入 blocklist → endAllSessionsForDomain fan out
global_stopPopup 红色 “Stop all” → endAllSessions fan out
extension_reloadSW 被卸载 / 扩展更新;一般不显式上报 —— session 在下次 SW 唤醒时 reconcile

六者共享同一套清理:detach chrome.debugger(幂等)、关闭 BUA-owned 宿主 tab 防止最小化 tab 堆积、BUA 窗口若空则收窗、 拒绝该 tab 任何在途 action 为 session_not_found

两个”用户在场性”原因 —— system_idlescreen_locked —— 是有意拆开的,而不是合并成一个:它们代表不同的安全姿态, 审计日志值得分开:

  • screen_locked —— 用户主动锁屏。锁屏后继续 agent 控制等同于在不同的信任上下文里运行,扩展在 chrome.idle.onStateChanged("locked") 触发时立刻撤销所有会话
  • system_idle —— 整机在检测间隔内(默认 30 分钟)没观察到输入。软信号 —— 用户可能走开,也可能在看内容不敲键盘。 这条兜的是”被遗忘会话”的风险,不会对短暂停顿过度敏感

两者都走同一个 idle-session-guard 模块,都走和其他终止原因一样的清理路径;审计日志保留具体命中的那一个, 以便 UI / 值班据此给出不同的修复提示。

debugger-detach 那类 bug,现已封闭

chrome.debugger.onDetach 可能因扩展控制不到的原因触发(用户点黄条 Cancel、Chrome 撤销挂载、DevTools 抢占)。v3 里的 handleDebuggerDetached 调了 endSessionByTab清扩展内存的 attached: Set<number>。下一次 attach() 会看到 残留集合、按”已挂载”早退,后续所有 CDP 命令静默挂住。v4 的实现在这条路径上debuggerController.detach(tabId) —— detach() 是幂等的,双调也安全。

用户可见表面

两个表面保证用户随时在控制:Chrome 原生调试条(系统级、不可屏蔽)和扩展 popup(活动监视器 + 全局急停)。两者在后台 走同一条清理路径。

Popup 的红色 “Stop all” 按钮会一次性结束所有 tab 上的所有活跃 session,并发 global_stop WS 事件,让后端在 per-session session_ended 到达前就短路所有在途请求。

Popup 的 session 列表里仍保留单 session 的 Stop 按钮用于精准撤销。两条路径都走 sessionManager.endSessionByTab / endAllSessions → 同一条 SessionEvent 总线 → 同一条 WS 转发。

不弹确认 —— 用户按下去就是已决定了。

Chrome 调试条

只要扩展把 chrome.debugger 挂到一个 tab,Chrome 就会注入一条黄色横幅写 “Zapvol Browser Bridge is debugging this browser” + 一个 Cancel 按钮。这个横幅无法屏蔽 —— Chrome 有意做得醒目。

点 Cancel 触发 chrome.debugger.onDetach,扩展先清内存 attached 集合,再按 user_stopped 结束 session —— 走和 popup Stop 相同的清理路径。

业内所有本地浏览器自动化方案(Anthropic computer-use demo、Browser Use 本地模式、各种扩展化自动化)都要面对这条 横幅。这是在用户自己的 profile 上做真实输入自动化的必要代价。内部部署时 IT 可以通过 Chrome 启动参数 --silent-debugger-extension-api 在 OS 层隐藏黄条 —— 这是部署侧的问题,不是扩展侧的。

错误处理契约(agent 侧)

Agent 应当把以下错误视为当前 action 的终止错误——不做静默重试:

错误码含义Agent 应该
domain_blocked目标域在 blocklist停止本 plan;不要换 tab / 换 action 重试
session_not_found目标没有活跃 session,或多 session 歧义显式传 tabId,扩展不会猜
tab_not_foundTab 已关闭停止,或先 get_tabs 换一个 tab
element_not_foundselector 或活跃 uid 在当前 DOM 上匹配不到重新 extract 页面;挑一个不同的目标;不要重试同一个
element_stale给的 uid 已不在缓存(页面导航 / 缓存被清 / 本次还没 extract)再调一次 extract 重建缓存,用新 uid 重试
timeoutAction 超时瞬态问题可配合 wait_for 重试一次,否则停止
debugger_attach_failedDevTools / 另一个调试器占用请用户关 DevTools;不要重试
invalid_actionSchema / 未知 action,或 evaluate 表达式抛异常message 改请求;不要盲重试
internal_error意外失败停止;把错误消息报给用户

这份契约写进了 browser 工具的 prompt(见 packages/backend/src/agent/tools/browser.tool.ts),每次工具加载时模型 都会看到。

审计日志 (Audit log)

每次 session 的 startend 都追加到 chrome.storage.localsessionHistory 键,上限保留最近 1000 条。 Options 页以倒序表格形式渲染:

  • When —— 时间戳
  • Event —— STARTEND(按颜色区分)
  • Domain + tabId
  • Detail —— END 行额外显示结束原因、持续时长、action 数

用户可一键清空(带确认对话框)。日志主要本地存储,后端同时收到 session_started / session_ended / domain_blocked / global_stop / tab_closed 这几个 WS 事件用于运营观测。

设计说明:审计模块 (session-history.ts) 订阅 session-manager 的事件总线 (onSessionEvent),而不是被各个 端点显式调用。这样 session-manager.ts 不掺杂审计关注点 —— 如果日志格式要改,只碰一个模块。所有终止路径都走同一条 总线,审计日志自然就捕获了所有路径;background.ts 里另有一个 WS 转发订阅,服务端的视图也一并保持一致。

这页有帮助吗?