一、项目定位

BrowserSkill 是腾讯开源的一款本地桥接中间件,核心使命是:

把 AI Agent(Claude Code、Cursor、Codex、Kimi Code 等)连接到用户已经登录的真实 Chrome 浏览器,让 Agent 直接复用你的 Cookie 和 Session 来操作网页,无需任何额外登录。

它不是一个 AI 框架,也不内置任何 LLM 调用逻辑。它只做一件事:提供一个 bsk 命令行工具,让外部的任意 Agent 通过 Shell 调用来驱动浏览器。这个设计使它与任何 Agent Harness 完全解耦。

与同类工具的核心差异

维度BrowserSkillPlaywright MCPbrowser-use
浏览器实例复用用户真实登录态新建独立进程(空 Profile)默认新建进程
用户干扰Agent Window 完全隔离,不影响用户正常浏览通常独占或共享实例框架内部管理
Agent 接入任意 Shell 调用(bsk CLI);skill 文件教 LLM 用法需 MCP 客户端Python 代码调用
AI 模型耦合完全解耦解耦深度集成 langchain
Human-in-loop内置 request-help,页面叠加层交互
语义观察层自研 VOM(AX树+DOM融合,含 surfaceProbe)aria snapshot截图+DOM混合
多浏览器支持同时连接多实例,--browser <id> 指定每个进程独立
Skill 分发bsk install-skill 一键部署到 12 种 harness

二、整体架构

2.1 系统拓扑

在这里插入图片描述

2.2 技术栈

层次语言/框架关键依赖
CLI + DaemonRust 2024 edition(tokio 全特性)clap 4 / serde-json / semver / reqwest / tokio-tungstenite
协议层Rust(bsk-protocol crate)schemars 0.8(生成 JSON Schema 供 TS 同步)
浏览器扩展TypeScript + React 19WXT(MV3)/ Vite / Tailwind CSS v4
VOM 语义层TypeScript 纯逻辑库@browser-skill/vom(无运行时依赖)
构建Cargo Workspace + pnpm WorkspaceBiome(lint/format)/ vitest

2.3 IPC 协议帧格式

CLI 与 Daemon 之间、Daemon 与 Extension 之间均使用相同的 JSON 帧结构(每行一帧):

RequestFrame  → { "id": "rpc-1", "method": "tool.click", "params": {...} }
ResponseFrame → { "id": "rpc-1", "body": { "ok": {...} } }
             或 { "id": "rpc-1", "body": { "err": { "code": "...", "message": "..." } } }
EventFrame    → { "event": "session.user_interrupt", "payload": { "session_id": "ab12" } }

Daemon 监听的 WebSocket 端口只接受 Origin: chrome-extension://[a-p]{32} 的连接,连接建立后第一帧必须是 system.handshake,有 5 秒超时,握手失败立即断开。


三、核心原理:如何复用真实登录态

这是 BrowserSkill 最核心的差异化设计,答案出乎意料地简单,却是其他工具难以轻易复制的护城河。

3.1 根本原因:Chrome Extension 运行在同一 Profile

BrowserSkill 以 Chrome 扩展(MV3) 身份运行。扩展天然与用户的浏览器同属同一个 Chrome Profile,意味着:

Chrome Profile(一个用户身份)
  ├── Cookie Store(全 Profile 共享)
  ├── localStorage / IndexedDB(按域隔离,Profile 内共享)
  ├── 用户窗口 A(已登录 GitHub、企业微信、腾讯云…)
  ├── 用户窗口 B
  └── Agent Window ← 扩展用 chrome.windows.create() 创建,同一 Profile!

当扩展执行 chrome.windows.create({ type: "normal", url: "about:blank" }) 时,Chrome 在同一个进程、同一个 Profile 里新建一个普通窗口。没有新的 Cookie Jar,没有独立的 Storage,一切存储都和用户已有的窗口共享。

对比 Playwright 的做法:playwright.chromium.launch() 会启动一个全新的 Chrome 进程,使用一个临时空白的 userDataDir,所有 Cookie 都不存在,必须重新走登录流程。

3.2 核心代码:Agent Window 的创建

来自 apps/extension/src/session-manager/agent-window.ts

export const chromeAgentWindowApi: AgentWindowApi = {
  async create(url: string, opts: AgentWindowCreateOptions = {}): Promise<number> {
    const win = await chrome.windows.create({
      type: "normal",
      focused: opts.focused ?? true,   // --no-focus 时传 false
      url,                              // 从 "about:blank" 开始
      ...(opts.size ? { width: opts.size.width, height: opts.size.height } : {}),
    });
    if (typeof win?.id !== "number") {
      throw new Error("[bh] chrome.windows.create returned no window id");
    }
    return win.id;
  },
  // ...
};

这就是全部——一个普通的 chrome.windows.create() 调用。Agent Window 的 URL 从 about:blank 开始,而非真实页面,原因在代码注释里写明:chrome:// 页面(包括新标签页)会拒绝 Page.navigate CDP 命令,所以用 about:blank 作为启动 URL。

3.3 核心代码:CDP 接入方式

扩展使用 chrome.debugger 权限,不需要开启远程调试端口,直接按 tabId 在 Profile 内附加。

来自 apps/extension/src/browser-driver/chromium-cdp.ts

async ensureAttached(tabId: number): Promise<void> {
    if (this.attachedTabs.has(tabId)) return;
    // 防止并发 attach 同一 tab
    const existing = this.attachInFlight.get(tabId);
    if (existing) { await existing; return; }

    const attach = (async () => {
      // 直接用 tabId,同 Profile 内的 tab,无需远程调试端口
      await this.api.attach({ tabId }, CDP_PROTOCOL_VERSION);  // "1.3"
      try {
        await this.enablePageDomain(tabId);       // Page.enable
        await this.enableConsoleDomains(tabId);   // Runtime.enable + Log.enable
        await this.enableNetworkDomainBestEffort(tabId); // Network.enable
        this.attachedTabs.add(tabId);
      } catch (err) {
        // 关键:domain enable 失败后必须回滚 attach,否则 tab 永久卡死
        // "Another debugger is already attached" 错误将永远触发
        await this.api.detach({ tabId }).catch(() => {});
        throw err;
      }
    })().finally(() => { this.attachInFlight.delete(tabId); });

    this.attachInFlight.set(tabId, attach);
    await attach;
}

一旦附加成功,所有操作通过 sendCommand 发出:

async send<T = unknown>(tabId: number, method: string, params?: object): Promise<T> {
    if (!this.attachedTabs.has(tabId)) await this.ensureAttached(tabId);
    const result = await this.api.sendCommand({ tabId }, method, params ?? {});
    return result as T;
}

操作已登录的 Tab 时,它的 Cookie 状态完全不受影响,CDP 命令只是在已有页面上执行鼠标/键盘事件或导航,等同于用户本人在操作。

3.4 Tab Borrow:直接挪用用户已登录的 Tab

除了在 Agent Window 自主导航,BrowserSkill 还支持将用户已经打开的、已登录的 Tab 直接借入 Agent Window 使用。

来自 apps/extension/src/tools/tabs.ts,核心逻辑在 moveTabForBorrow

async function moveTabForBorrow(
  tabsApi: TabMutationApi,
  tabId: number,
  agentWindowId: number,
  originalWindowId: number,
  originalIndex: number,
  signal?: AbortSignal,
): Promise<RpcError | null> {
  try {
    // 关键:只是把 Tab 从用户窗口移到 Agent Window
    // Tab 的 DOM / Cookie / JS 运行时 / XHR 连接全部原样保留
    await tabsApi.move(tabId, { windowId: agentWindowId, index: -1 });
  } catch (err) {
    return { code: "cdp_failed", message: `chrome.tabs.move failed: ...` };
  }

  if (aborted(signal, "tab_borrow")) {
    // 被取消:立即回滚,把 Tab 移回原处
    await tabsApi.move(tabId, { windowId: originalWindowId, index: originalIndex });
    return { code: "cancelled", message: "tab_borrow aborted" };
  }
  return null;
}

chrome.tabs.move() 只是改变了 Tab 所属的窗口,Tab 本身的状态(DOM 树、JS 堆、网络连接、Cookie)一个字节都不会变。Agent 拿到这个 Tab 后用 CDP 直接操作,等于在用户已经登录的页面上继续操作。

Borrow 流程完成后,tab_return 把 Tab 移回原始窗口(或当原始窗口已关闭时找到备用窗口):

// 归还时,原始窗口已关闭 → 找到最后聚焦的普通窗口作为备用
// 关键:必须排除所有 Session 的 Agent Window,防止跨会话写权限提升(PR #3 修复)
if (lastId !== null && lastId !== ctx.agentWindowId && !isAgentWindowId(lastId)) {
    return { windowId: lastId, index: -1 };
}

3.5 两种路径的完整流程

路径 1:Agent Window 自主导航(最常用)
─────────────────────────────────────────
bsk session start
  → chrome.windows.create() → Agent Window(同 Profile,天然有 Cookie)

bsk navigate --session ab12 --url https://corp.com
  → CDP Page.navigate
  → 服务端看到用户已登录的 Session → 直接进入主页,无需登录


路径 2:借用用户已打开的 Tab(精确复用已有状态)
─────────────────────────────────────────
bsk tab_list --session ab12 --scope user
  → 列出用户所有普通窗口里的 Tab(含 URL、标题)

bsk tab_borrow --session ab12 --tab-id 42
  → 弹出 OS 通知 + 页面叠加层(5 秒倒计时确认)
  → 用户点 Allow → chrome.tabs.move(42, agentWindowId)
  → Tab 携带完整登录状态、DOM、JS 上下文进入 Agent Window

bsk snapshot / click / fill ...
  → CDP 操作这个已登录的 Tab

bsk tab_return --session ab12 --tab-id 42
  → chrome.tabs.move(42, originalWindowId)
  → Tab 归还,状态完整,用户可继续正常使用

四、VOM —— 专为 LLM 设计的语义观察层

4.1 原始 AX Tree 的问题

大多数浏览器自动化工具直接把 Chrome 的 Accessibility Tree 转给 LLM。AX Tree 是为无障碍辅助技术设计的,不是为 LLM 设计的,存在以下问题:

  • 重复标签无法区分:表格里有 50 个 “查看” 按钮,AX Tree 全部标注为 button "查看",LLM 不知道该点哪个
  • 标签与控件绑定不稳定StaticText "验证码:" 和后面的 input 在 AX Tree 里是兄弟节点,不是父子关系,LLM 要自己推断哪个 input 对应这个 label
  • 自定义控件漏识别:很多用 div + JS 实现的下拉菜单、日期选择器,AX Tree 要么不标记为可操作,要么 role 信息不准确
  • 表单状态混淆:placeholder 文字在 AX Tree 里和真实内容没有区分,LLM 容易以为表单已经填了内容
  • hover-only 元素不可见:很多导航菜单只在 hover 时展开,静态 AX Tree 里根本看不到这些选项

4.2 VOM 的核心设计

VOM(Virtual Object Model)是 BrowserSkill 自研的语义观察层,位于 packages/vom/,核心思想是融合多源信息,产出任务导向的语义视图

输入来源

chrome.accessibility.getTree()  → AX 语义结构(role/name/value)
CDP Accessibility.getFullAXTree → backendNodeId 映射(用于后续 CDP 操作)
CDP DOM.captureSnapshot         → 真实 DOM 结构 + 元素坐标
Runtime.evaluate(formState)     → 表单运行时状态(是否填写、实际值)

关键处理步骤

  1. 上下文消歧:为重复弱标签(如多个 “查看”)补充局部上下文——找到最近的区分性祖先节点(如行首的名称或 ID),附加到标签前
  2. 表单状态分类:区分 [empty](从未填写)/ [filled](用户输入)/ [default](有默认值但可修改),让 LLM 知道哪些字段需要填写
  3. 冗余压缩:移除已被 cell/group name 覆盖的重复文本子节点,减少 Token 消耗
  4. 自定义控件恢复:识别非原生但实际可操作的控件(如 div[role=button] + tabindex),补全可操作性标注
  5. Surface Probe:对可能有 hover-triggered 内容的元素,发起受控的 hover 探测,收集展开后新出现的节点,附加到父节点描述中

@eN 引用系统:每次 snapshotobserve 调用,所有可操作节点按顺序编号为 @e1@e2……LLM 使用这些短引用来指代操作目标(bsk click @e3),比 CSS 选择器短得多,也比 XPath 稳定得多。每次重新 snapshot 后引用重新编号,确保引用总是指向当前页面真实存在的元素。

VOM 输出示例(LLM 实际看到的内容)

nav @e1
  a[href=/dashboard] @e2 "主页"
  a[href=/settings]  @e3 "设置"
  a[href=/logout]    @e4 "退出"
main @e5
  h1 "用户管理"
  table @e6
    thead: 姓名 | 部门 | 操作
    tr[row=1] "张三 | 技术部"
      button @e7 "查看"    ← 上下文消歧:行首名称已附加
      button @e8 "编辑"
    tr[row=2] "李四 | 产品部"
      button @e9 "查看"
      button @e10 "编辑"
  form @e11
    input[text][empty] @e12 placeholder="搜索用户名..."
    button @e13 "搜索"

相比原始 AX Tree,这个视图:压缩了 60%+ 的 Token、消除了重复标签歧义、明确了表单状态、保留了操作所需的所有语义信息。

4.3 snapshotobserve 的区别

命令Effect 分类是否触发 hover 探测适用场景
bsk snapshotPassiveRead(只读,不受 Stop 门控)❌ 不触发快速获取当前页面静态状态
bsk observeTransientInput(受用户中断门控)✅ 受控探测有下拉菜单、tooltip、hover-only 导航的复杂页面

observe 的 hover 探测有边界限制,不会无限递归展开——它对每个候选节点做一次 hover,收集新出现的节点后停止,避免副作用过大。


五、安全设计

5.1 Fail-Closed 原则

BrowserSkill 在所有授权路径上严格遵循 fail-closed(不确定则拒绝)原则,这是项目开源第一周就集中修复的核心问题。

PR #5fix: don't fail-open the borrow gate on a transient tabs.get error)修复了一个典型 fail-open 漏洞:当 tabs.get() 因 Service Worker 被系统回收而抛出 transient 错误时,原代码把异常 catch 后继续执行了借用操作。修复后,任何错误都返回拒绝。

PR #16fix: fail closed on borrow confirmation)修复了确认流程的多个 fail-open 路径,包括:sendMessage 超时、content script 未加载、response 格式异常等情况,全部改为返回 false(拒绝)。

当前代码 borrow-confirmation.ts 中,所有异常路径均如此处理:

const settle = (allowed: boolean) => { /* ... */ resolve(allowed); };
// 超时
setTimeout(() => { dismissPendingOverlay(); settle(false); }, BACKGROUND_TIMEOUT_MS);
// abort
const onAbort = () => { dismissPendingOverlay(); settle(false); };
// sendMessage 失败 → 尝试下一个候选窗口,全部失败 → 等待 OS 通知按钮或超时 fail-closed

5.2 跨会话隔离

每个 Session 有独立的 Agent Window。PR #3 修复了一个严重的跨会话安全漏洞:

漏洞场景:Session A 借用的 Tab,原始窗口被用户关闭后需要归还到备用窗口。chooseFallbackWindow 调用 getLastFocused({ windowTypes: ["normal"] }) 获取最近聚焦的窗口,但 Agent Window 本身也是 type: "normal",若 Session B 的 Agent Window 恰好是最后聚焦的窗口(这种情况很常见,因为 Agent Window 会主动抢占焦点),用户的 Tab 就会被错误移入 Session B 的 Agent Window。

影响:Session B 的所有写操作(click/fill/evaluate)会通过 Agent Window 的归属校验,可以对这个用户从未授权给 Session B 的 Tab 执行操作。Session B 关闭时会销毁这个 Tab,用户的数据静默丢失。

修复:注入 isAgentWindowId 谓词,在所有窗口选择逻辑中排除所有活跃会话的 Agent Window,而非仅当前会话的:

isAgentWindowId: (windowId) => manager.findByWindowId(windowId) !== null

5.3 用户中断门控(编译时类型安全)

每个 RPC 方法在 crates/bsk-protocol/src/method.rs 中被 Rust 枚举 MethodEffect 强制分类:

pub enum MethodEffect {
    BrowserMutation,   // 写操作:click/fill/navigate/press/select/evaluate...
    TransientInput,    // 临时输入:hover/observe(可以被 Stop,但不影响页面状态)
    PassiveRead,       // 只读:snapshot/screenshot/get_html(永远不被 Stop)
    ControlPlane,      // 控制面:session_stop/session_list(永远不被 Stop)
}

effect() 方法使用 match 穷举所有变量,Rust 编译器强制保证每个新增方法都必须分类,遗漏分类会导致编译失败

用户在 Agent Window 点击 Stop 按钮后:

  1. content script 发送 session.user_interrupt 事件 → background → WS → Daemon
  2. Daemon 标记该 Session 的中断信号
  3. 下一个 BrowserMutation 操作在进入队列前命中门控,立即返回 UserAborted
  4. PassiveReadControlPlane 操作不受影响session stop 可以正常执行

5.4 会话生命周期中的 session stop 时序

Chrome Extension Daemon AI Agent Chrome Extension Daemon AI Agent loop [归还所有借用的 Tab] CDP 从 Agent Window 所有 Tab 上 detach 页面顶部 DevTools infobar 消失 Agent Window 关闭 bsk session stop ab12 tool.session_stop {session_id: "ab12"} chrome.tabs.move(tabId, originalWindowId) cdp.detachSession("ab12") chrome.windows.remove(agentWindowId) {session_id: "ab12"} sessions.remove("ab12") tool_queues.remove("ab12") session_interrupts.clear("ab12") 会话已结束

六、Daemon 内部设计

6.1 每会话串行队列

Daemon 对每个 Session 维护一个容量为 64 的串行队列(daemon/queue.rs),同一 Session 的工具调用严格串行执行,不同 Session 可以并行。

为什么需要串行? 扩展侧维护一个 ref-store:每次 snapshot/observe 产生的 @eN → { backendNodeId, tabId } 映射。如果两个调用并发执行,一个 snapshot@e1 可能被另一个 snapshot 的新编号覆盖,导致 click @e1 操作到错误的元素。串行队列从根本上消除了这个竞争条件。

Session ab12 的队列:  [observe] → [click @e3] → [fill @e7 "hello"] → ...
                         ↑ 正在执行    ↑ 等待          ↑ 等待
                         
Session cd34 的队列:  [navigate] → [snapshot] → ...
                         ↑ 并行执行

6.2 自动更新系统

Daemon 每 30 分钟做一次版本检查(update/check.rs)。PR #76 修复了周期任务未被实际调度的 bug,PR #77 在此基础上实现了自动升级:

检查到新版本
  → 下载 release archive(reqwest)
  → SHA256 校验(PR #38 加入)
  → 解压到临时路径
  → 检查是否有活跃 Session
    → 有活跃 Session → 推迟到下一个 tick(30 分钟后)
    → 无活跃 Session
      → Unix:原子替换二进制 → spawn 分离子进程等待旧进程退出 → 旧 daemon 优雅退出
      → Windows:暂存替换包,记录日志(exe 运行中无法原地覆盖,PR #85 修复)

可通过 BSK_AUTO_UPDATE=off 禁用自动升级,退回到仅提示模式。

6.3 MV3 Service Worker 保活

Chrome MV3 扩展的 Service Worker 在无活动时会被系统回收(通常 30 秒内),这是 BrowserSkill 稳定性的最大挑战之一。项目通过两层机制保活:

  • 30 秒 Keepalive Alarmlib/keepalive.ts):每 30 秒触发一个 Chrome Alarm,唤醒 Service Worker,检查 WebSocket 是否还连着,不连就重连
  • 20 秒 Heartbeatlib/heartbeat.ts):握手成功后,Extension 每 20 秒向 Daemon 发一个 system.heartbeat 帧,通过 WS 活动阻止浏览器判定 SW 为空闲

此外,chrome.runtime.onStartupchrome.idle.onStateChanged 事件在系统休眠唤醒后触发重连,不依赖 Alarm 恰好命中。


七、PR 全景分析:项目演进路线

自 2026 年 7 月 6 日开源,项目累计 88 个 PR(约 50 个已合并),主要贡献者为 BB-fat(主力协作者)和社区开发者 NianJiuZst、hobostay、shnpd、Ljy-0827、iuyo5678、hjxccc 等。

7.1 第一阶段:安全底线(7 月 6~18 日,PR #3~#16)

开源第一周,核心工作是修复可能导致跨会话写权限提升的安全漏洞,几乎都是 hobostay 提交:

PR标题解决的问题
#3never return a borrowed tab into another session’s Agent WindowTab 归还时 fallback 窗口选择不排除其他会话的 Agent Window,导致跨会话写权限提升
#4reject cross-browser session events from non-ownersDaemon 未校验 WS 消息的 browser_instance_id 归属,任何扩展实例可发送他人会话的控制事件
#5don’t fail-open the borrow gate on a transient tabs.get errortabs.get() transient 错误被 catch 后静默放行了借用操作
#16fail closed on borrow confirmation确认流程多个错误路径 fail-open

这一阶段的模式非常清晰:在任何授权决策点,错误必须拒绝而非放行。这几个 PR 的补丁都极小,但影响极大——如果不修,任何 Agent 都可以通过时序竞争操作到不属于自己 Session 的用户 Tab。

7.2 第二阶段:能力建设(7 月 7~31 日,PR #7~#53)

修完安全底线后,开始大规模功能扩展:

调试能力

  • PR #7bsk console):读取 Agent Window 的 JS 控制台输出。Agent 在页面上执行操作后如果出错,之前只能靠 snapshot 猜,现在可以直接读 console.error 定位问题
  • PR #8bsk network):读取 XHR/Fetch 请求记录。对于需要监控 API 调用结果的场景(如提交表单后检查后端返回)非常有用

录制能力

  • PR #28bsk record):录制用户操作为语义 trace.json。这不是屏幕录制,而是语义录制——每一步的 role/name/tag 等语义描述都保留,坐标不保留,密码字段自动脱敏。Agent 可以读取这个 trace 然后重放,也可以作为 few-shot 示例告诉 LLM 如何操作某个系统

无人值守模式

  • PR #53BSK_REQUEST_HELP=off):request-help 是阻塞式的人机交互命令,在 CI/CD 流水线或后台服务器上没有人工操作者。设置此环境变量后,命令立即返回 {"outcome": "disabled"},Agent 收到后自主决策是否继续,而不是永久挂起等待

观察层重构

  • PR #45(VOM 语义层):这是这一阶段最重大的变更,引入了整个 VOM 语义层体系,详见第四章

7.3 第三阶段:打磨与扩展(8 月 4~13 日,PR #52~#87)

能力补全

PR新增能力解决的场景
#52Agent Window 尺寸控制响应式布局测试、特定分辨率下的操作
#60移动设备模拟(bsk emulate移动 H5 页面的 Agent 自动化,含 UA/viewport/触控三重模拟,7 种内置设备预设
#68Hover 端到端支持Agent 遇到 hover-only 导航菜单时不再卡住,observehover @refobserveclick 完整路径
#81Popup 隐藏控制提示开关用户觉得 Agent Window 里显示的控制 overlay 视觉干扰大,可在扩展 Popup 里关掉
#87--no-focus(无焦点会话启动)Agent 后台执行任务时不抢占用户当前焦点,社区 Issue #26 反映的实际使用痛点

稳定性修复

PR修复内容
#33CDP domain enable 失败后必须回滚 attach,否则 tab 永久卡在"另一个 debugger 已附加"状态
#34wait_until=commit 在 readyState probe 失败时无法正确解析
#39Console domain enable 失败后被错误标记为"已启用",导致实际上没有抓到任何日志
#64screenshot 时叠加层(Control / Record overlay)会出现在截图里,现在截图前临时隐藏
#82WS 重连生命周期加固,解决 MV3 Service Worker 回收后重连不稳定的问题
#83captureVisibleTab 在某些情况下失败,回退到 CDP Page.captureScreenshot

自动更新系统完善(详见第六章):PR #76 修复周期检查未触发 → PR #77 实现自动升级 → PR #85 修复 Windows staged 替换 → PR #86 修复 Windows Named Pipe 超长路径

生态接入:PR #65 新增 Kimi Code harness 支持,bsk install-skill 现支持 12 种 Agent Harness 自动部署

7.4 未合并但有价值的 PR(社区贡献与方向探索)

PR标题未合并原因价值
#54support unfocused agent windows与 #52(窗口尺寸)的参数位置冲突,被 BB-fat 重构为 #87 后合并体现社区在推动非阻塞会话
#51agent self-recovery when 0 browsers connected浏览器未运行时自动拉起 Chrome无人值守场景的重要能力,尚未合并
#49Make recording overlay draggable录制叠加层可拖动,避免遮挡页面控件实用的 UX 改进
#47添加控制覆盖层可见性切换功能与 #81 重叠,#81 从 Popup 层面解决了同样的需求

八、完整工具命令速查

BrowserSkill CLI 共 28 个子命令,按 Effect 分类:

只读命令(PassiveRead)——不受用户 Stop 门控,永远可以执行:

bsk snapshot  --session <id>                    # 获取当前页面的 VOM 语义快照
bsk observe   --session <id>                    # 语义快照 + hover 探测(发现隐藏菜单)
bsk get-html  --session <id>                    # 获取原始 HTML
bsk screenshot --session <id>                   # 截图(PNG)
bsk console   --session <id> [--since <n>]      # 读取 JS 控制台输出
bsk network   --session <id> [--since <n>]      # 读取网络请求记录
bsk tab list  --session <id> [--scope user|agent|all]
bsk status                                       # 检查 daemon 和扩展连接状态
bsk doctor                                       # 诊断环境(退出码非 0 表示有问题)
bsk browsers                                     # 列出已连接的浏览器实例
bsk logs      [--tail <n>]                       # 查看 daemon 日志

写操作命令(BrowserMutation)——用户按 Stop 后立即拒绝:

bsk navigate       --session <id> --url <url>
bsk navigate-back  --session <id>
bsk navigate-forward --session <id>
bsk reload         --session <id>
bsk click          --session <id> --ref <@eN>
bsk hover          --session <id> --ref <@eN>
bsk fill           --session <id> --ref <@eN> --value <text>
bsk press          --session <id> --ref <@eN> --key <Enter|Tab|...>
bsk select         --session <id> --ref <@eN> --value <option>
bsk evaluate       --session <id> --expression <js>
bsk tab create     --session <id> [--url <url>]
bsk tab close      --session <id> --tab-id <n>
bsk tab select     --session <id> --tab-id <n>
bsk tab borrow     --session <id> --tab-id <n>
bsk tab return     --session <id> --tab-id <n>
bsk window resize  --session <id> --width <n> --height <n>
bsk emulate        --session <id> --device <iphone-14|pixel-7|...> | --off
bsk request-help   --session <id> --prompt <text> [--target <@eN>] [--timeout <5m>]
bsk wait-for-navigation --session <id>
bsk wait-ms        --session <id> --ms <n>

控制面命令(ControlPlane)——不受 Stop 门控,即使用户中断也可执行:

bsk session start  [--browser <id>] [--no-focus] [--width <n>] [--height <n>]
bsk session stop   <session-id>
bsk session list
bsk record start   --session <id>
bsk record stop    --session <id>
bsk install-skill  [--harness <name>] [--all]
bsk update         [--check]
bsk daemon         [start|stop|status]

九、设计哲学总结

9.1 安全优先:Fail-Closed

在任何授权路径上,错误状态必须拒绝而非放行。这不只是代码规范,而是系统设计的第一原则,体现在编译时的 MethodEffect 强制分类、所有 Borrow 确认的超时 fail-closed、Tab 归还目标的多重 Agent Window 排除等每一个细节上。

9.2 LLM-Native 观察层

VOM 不是给人类工程师调试用的,是专门为 LLM 的 Token 效率和理解能力设计的。这意味着:不保留人类可以脑补的冗余信息,明确标注 LLM 无法自行推断的状态(表单是否填写),主动发现 LLM 无法感知的隐藏元素(hover-only 菜单)。这是从第一性原理重新设计「AI 如何感知网页」。

9.3 Shell-First = 生态开放

bsk 是纯 CLI 工具,任何支持 Shell 的 Agent Harness 都能接入,不依赖特定 AI SDK 或 MCP 协议。bsk install-skill 自动检测本机安装的 Harness 并写入 SKILL.md,让 LLM 在下次启动时自动知道如何使用 bsk。目前支持 12 种 harness,这是一个生态锁定策略——接入越多,替换成本越高,网络效应越强。

9.4 产品的核心护城河

BrowserSkill 的差异化不在于技术复杂度(核心机制"Chrome 扩展 + 同一 Profile"并不复杂),而在于它是第一个把这个机制产品化、系统化、安全化的开源项目:

  • 解决了所有别人没解决的边界情况:跨会话隔离、用户中断、Session 清理、CDP 附加回滚
  • 为 LLM 专门重新设计了观察格式(VOM),而不是把现有 API 的输出直接喂给 LLM
  • Human-in-loop 是一等公民,而不是事后补丁
  • 真实浏览器意味着可以访问一切需要登录的内网系统、企业 SaaS、个人账号,这是测试账号永远无法完全替代的

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐