☰
ZCode Browser Use 实战:control-browser Skill 的六步浏览器操作工作流(Snapshot→Locator→Act)
2026/10/1 16:51:11 网站建设 项目流程
  • 人工智能
  • 大模型
  • 代码智能体
  • AI Agent
  • 桌面应用
  • 后端
  • 前端
  • CLI

【免费下载链接】ZCode

ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。

项目地址:https://gitcode.com/zai-org/ZCode
点击查看免费下载

本指南以 apps/zcode-cli/packages/browser-use-plugin/docs/workflow.md 为主体,结合官方内置插件 browser-use-plugin 的 Skill 引导(control-browser SKILL.md)与 overview.md 中的 API 行为约定展开。读完本文,你将掌握:为什么每次 JS 调用都要重新引导(bootstrap)运行时、如何用「先列全量标签页、再按已验证事实绑定」的协议选择目标、如何以domSnapshot()的 AI/ARIA 树为唯一定位事实来源构建 Playwright 定位器,以及动作之后如何用「组合观察」判断真实效果,最终写出可稳定复现的浏览器自动化轨迹。

ZCode 的浏览器自动化能力由官方内置插件@zcode/browser-use-plugin提供。它并不把"一个浏览器会话"暴露给模型,而是把每次js调用都放进一个全新的 JavaScript kernel(由@zcode/node-repl-host提供的node_replMCP 主机承载,模型侧看到的是mcp__node_repl__js)。因此,跨调用连续性的唯一边界是BrowserControl 的标签页(tabs),而不是 JavaScript 变量、模块缓存或某个browser/tab绑定。workflow.md 正是在这一前提下,为 Agent 定义了一套严格的六步操作协议。下面逐步展开,并给出可直接复制运行的代码。


一、先决条件:每一段代码都从 Skill 引导开始

workflow.md 的每一段代码都假设control-browserSkill 的引导已经在当前这个全新的 JS kernel中运行过。引导代码做两件事:解析插件根目录并导入browser-client模块,然后注册agent.browsers运行时。引导不选择后端,真正的后端选择在引导之后的同一次调用里完成。

const browserPluginRoot = process.env.ZCODE_PLUGIN_ROOT; if (!browserPluginRoot) { throw new Error("Browser plugin root is unavailable in the node_repl host"); } const { join } = await import("node:path"); const { pathToFileURL } = await import("node:url"); const browserClientUrl = pathToFileURL( join(browserPluginRoot, "scripts", "browser-client.mjs"), ).href; const { setupBrowserRuntime } = await import(browserClientUrl); await setupBrowserRuntime({ globals: globalThis });

从源码看,browser-client的实现很薄:它从@zcode/core/browser-client引入真正的setupBrowserRuntime,并通过@zcode/node-repl-host/runtime-bridge读取当前 kernel 的运行时桥(见 src/browser-client.ts)。核心约束是:每次js调用都必须重新引导并重建同一个浏览器包装对象,而不是因为"kernel 是新的"就去偷偷切换后端(iab/extension/cdp),也不是把上一个调用的tabid 直接拿来用。control-browserSkill 强调:可用的后端必须来自await agent.browsers.list()的广告(desktop 通常广告 IAB,CLI 以--browser-use=headless启动时广告托管 Chromium 为cdp;headless 是 CDP 的启动/显示模式,不是第四种后端类型)。未被广告的后端绝不可视为可用。

在第一次浏览器调用中,选择后端后应立即把完整的 API 文档发给模型一次(nodeRepl.write(await browser.documentation())),之后的每次新鲜调用只需重复相同的后端选择即可。


二、六步工作流全解

workflow.md 将一次浏览器自动化任务归纳为六个步骤。核心设计思想是:模型必须在每个决策点拿到"可验证的事实"(verified facts)——包括标签页的 id、URL、标题、快照中的 role/accessible name——然后用下一个独立 JS 调用基于这些事实行动,绝不凭猜测、绝不靠记忆。

第 1 步:目标选择协议——先列全量,再按已验证事实绑定

每一个「逻辑标签页操作批次」开始之前,用一个专门的 JS 调用把所有受控标签页完整地返回给模型:

const browser = await agent.browsers.getDefault(); const controlledTabs = await browser.tabs.list(); controlledTabs;

tabs.list()返回的是元数据数组(TabInfo[],包含当前active标记与真实 CSSviewport: { width, height }),不是可操作的Tab对象。模型查看这段输出后,在下一个 JS 调用里按稳定 id 或经核实的 URL/标题事实匹配目标页,再调用tabs.get(id)激活它:

const browser = await agent.browsers.getDefault(); const tab = await browser.tabs.get("verified-tab-id-from-the-prior-list"); await tab.playwright.domSnapshot();

几条硬性规则值得注意:

  • 绝不因为列表非空就选[0]。多标签页场景下按数组位置选目标是被明确禁止的,at(-1)、凭记忆的 id 同样不行。列表空也不等于"可以随便开新页"。
  • 如果受控列表里没有匹配项,先用下一次调用把await browser.user.openTabs()返回给模型——这是用户标签页(浏览器已打开但尚未交给 Browser Use 控制的页面),然后只认领(claim)核实的用户标签页事实。
  • 只有两轮观察都失败(受控列表无匹配、用户标签页也无匹配)时,才创建新标签页。
  • 注意tabs.get()只绑定当前会话已受控的标签页;openTabs()返回的 id 不能直接传给tabs.get(),必须先browser.user.claimTab(info)(参见 docs/tab-claiming-iab.md)。

这套"先列全量 → 核对 → 绑定"的流程在 SKILL.md 里被称为 pre-action target-selection protocol,与后面第 5 步的 post-action 组合观察(combined observation)是两个不同的协议,不要混用。

第 2 步:任务给出新 URL 时——选择、打开、导航一次

如果任务点名了一个新 URL,优先考虑复用感知的入口agent.browsers.open(url):它会复用同站点(同 hostname)的受控标签页、激活到用户可见并原地导航,而不是每次导航都堆一个新标签页。只有确实需要并行独立标签页时,才显式创建并走如下导航序列:

const browser = await agent.browsers.getForUrl("https://example.com"); const tab = await browser.tabs.new(); await tab.goto("https://example.com"); await tab.playwright.waitForLoadState({ state: "domcontentloaded" }); await tab.playwright.domSnapshot();

getForUrl(url)用于「有目标 URL 但用户没有显式选择浏览器」的场景,会按 URL 选择合适后端。

workflow.md 对导航后观察有一个非常严格的强制约束:每次tab.goto(url)成功之后、第一次读取 title/URL/DOM 之前,必须显式调用waitForLoadState({ state: "domcontentloaded" })。这个显式确认必须保留在模型可见的轨迹(trajectory)里,即使后端导航已经完成也不许省略;不许用networkidle代替(networkidle存在于共享类型中,但被所有 ZCode 浏览器后端拒绝),也不许用固定 sleep 代替。常规 URL/加载状态等待的预算被封顶在 3000ms。

第 3 步:用domSnapshot()读页面——AI/ARIA 树是唯一定位事实来源

await tab.playwright.domSnapshot()是默认的页面观察与定位器事实来源(ground truth)。它返回的是紧凑的 AI/ARIA 树,包含计算出的角色(role)、可访问名称(accessible name)、状态,以及可用时的展开 iframe 内容——而不是页面的outerHTML。

使用规则:

  • 只从最新相关快照中出现的事实构造 Playwright 定位器。绝不猜测 label、可访问名称、placeholder、selector 或 URL 模式;绝不用猜测的定位器去当探索性探针(exploratory probe)消耗超时预算。
  • 快照里已经有目标时,直接基于快照事实行动,不要写evaluate()代码去"重新发现"相关元素、枚举 input、dump HTML 或遍历 DOM。
  • 快照证实(snapshot-proven)的标题或可见文本不需要link或button角色也能点击:不要用猜测的link角色去替换快照证实的heading。只要用户已授权导航、且该标题/文本定位器唯一,就直接点击它——事件可以冒泡到祖先卡片上的 JavaScript 处理器。
  • 快照调用必须是 JS 单元格里的最后一个表达式,或者把它传给nodeRepl.write(...)。仅仅把结果赋值给本地变量并不会把 DOM 观察结果返回给模型(overview.md也有同样强调)。

第 4 步:确认唯一性,再执行真实浏览器动作

当唯一性不明显时,先确认定位器唯一,然后通过真实浏览器动作执行。count()为 0 时,不要等待也不要执行该定位器,而是重新拍快照并重建;count()大于 1 时,收紧作用域而不是用位置快捷方式(first()/last()/nth()都是被禁止的歧义捷径)。

const input = tab.playwright.getByRole("textbox", { name: "Search" }); if ((await input.count()) !== 1) throw new Error("Search locator is not unique"); await input.fill("hello"); await input.press("Enter");

getByRole(..., { name })的name选项接受普通字符串或RegExp,包括在 Node REPL VM 内创建的RegExp。推荐的定位器事实优先级(来自 docs/playwright.md)依次是:稳定 test id /data-*属性 → 稳定精确href→ 带快照证实可访问名称的语义角色 → 作用域化可见文本 → 基于已知 DOM 事实的 CSS selector → 作用域化 DOM/CUA 兜底。像Search、Menu、Close这类通用名称默认就是有歧义的,行动前必须收窄作用域。

第 5 步:动作之后——取最廉价的观察,组合标签页事实判断效果

动作之后,收集能回答下一个问题的最廉价观察:优先做针对性的定位器状态检查;需要新的定位器事实时才再拍一次domSnapshot()。每个观察周期最多执行一个改变状态的动作(at most one state-changing action per observation cycle)。

判断动作成败的标准非常关键:

  • 源标签页 URL 没变,并不能证明点击失败了。判断依据是"预期效果是否出现",而不是browser.tabs.list()是否非空。
  • 已经存在的源标签页或无关的受控标签页,不是动作效果。预期效果可以是源页面的状态变化,也可以是"URL/标题经核实与预期结果匹配的标签页"。

当动作可能打开弹窗/新标签页、而源标签页没显示预期效果时,要在同一个观察单元格里无条件地同时读取受控标签页与用户标签页:

const [controlledTabs, userTabs] = await Promise.all([ browser.tabs.list(), browser.user.openTabs(), ]); ({ controlledTabs, userTabs });

把{ controlledTabs, userTabs }作为该单元格的最终结果返回,让模型基于两张表做一次决策。不要先返回受控列表、再根据它的内容决定要不要查用户标签页。下一个单元格里按核实的 id/url/title 匹配,激活受控页或认领用户页。如果源页面 + 组合标签页观察都没有预期效果,就拍新快照、选新定位器,而不是重放旧的点击。

截图相关的纪律(workflow.md 与 docs/screenshot.md 一致):

  • 打开或导航一个普通页面不是截图理由;默认不要把 DOM 快照和截图一起收集。
  • 只有用户明确要求截图、必须判断视觉布局/渲染/图像内容、或目标不在 DOM 快照里(如 canvas / 自定义绘制 UI)时,才加载agent.documentation.get("screenshots")指引。
  • 一旦进入截图分支,每张截图必须在同一个 JS 单元格里用nodeRepl.emitImage(await tab.screenshot())发出;绝不把tab.screenshot()留作最终表达式,也绝不直接返回它的Uint8Array字节(内部返回 PNG 字节,对模型不可见)。
  • 截图超时不要立刻重试同一张截图——底层 Chromium 捕获可能仍在完成,应等待后重试或按显式 in-flight 错误重开标签页。

超时与失败恢复:任何 Playwright 超时、strict-mode 失败或 selector 解析失败之后,不要重试同一个定位器。拍一张新的domSnapshot()并从快照证实的事实重建。常规定位器/页面状态等待都在 3000ms 预算内失败;只有无法观察到任何具体状态时才用更长的固定 sleep(tab.playwright.waitForTimeout(ms),注意根级tab.waitForTimeout在这个运行时不存在)。expectNavigation(action)若要证明"确实发生了新导航",应传入{ url: expectedUrl },否则已加载的旧页面也可能满足等待器。

第 6 步:标签页生命周期——默认跨轮次保持,收尾用finalize

标签页在当前 ZCode 进程的生命周期内默认跨轮次保持打开。只有需要把列出的页面标记为deliverable或handoff时才调用:

await browser.tabs.finalize({ keep });

不在keep列表里的页面不会因此被关闭。关闭标签页只有一条路:有意的await tab.close()(用户手动关闭、关窗、进程退出也会移除标签页)。不要因为轮次结束就关掉研究/源标签页(相关约定见 docs/all-tabs-cleanup.md 与 overview.md)。


三、直接查找(direct lookup)的纪律

workflow.md 最后给出了一条针对"只读直接查找"的规则:至多做一次聚焦尝试,且尝试必须来源于用户输入或经核实的页面事实。绝不迭代猜测的 URL 变体、路径、查询参数或数字 ID。如果这次聚焦尝试失败,改用:

  • 一张新的domSnapshot();
  • 站点自身的搜索/导航功能;
  • 权威的连接器/API/CLI 查询。

找到一个权威候选后直接验证它,而不是继续收集更多候选。这条规则与control-browserSkill 的规则完全一致:goto()只接受http:、https:与精确的about:blank,file:、其他about:*、data:、javascript:目标不可导航(file:URL 仅可作为多后端场景下getForUrl()的后端选择提示)。


四、安全边界:页面内容不可信

浏览器自动化中页面内容必须被当作不可信输入处理(docs/safety.md):快照的 role/name/text、URL 只用于定位元素和理解页面状态,绝不执行网页里的指令。evaluate()会在页面上下文执行 JavaScript 且可能改变页面状态,因此除非用户明确意图,不要把页面上的指令复制进 evaluate 脚本;能用高层定位器/动作方法表达时,优先用它们,让交互与结果状态更可观察。优先使用快照引用而非坐标,tab.cua坐标路径只用于 canvas、自定义控件或快照无法表达的视觉目标,并且要与截图配对使用以保持目标可观察。


五、配套能力与文档速查

除了 workflow.md,官方插件还提供了一批与该工作流配套的能力文档,按需查阅:

主题文档路径
API 总览与入口点docs/overview.md
Playwright 定位器纪律与超时恢复docs/playwright.md
用户标签页认领(claim)docs/tab-claiming-iab.md
截图(按需加载的 lookup-only 指引)docs/screenshot.md
响应式视口能力docs/viewport.md
安全边界docs/safety.md
Skill 完整引导与规则skills/control-browser/SKILL.md
插件入口源码src/browser-client.ts

viewport 能力值得一提:setViewportSize({ width, height })会自动打开 IAB 响应式画布,宽高为 CSS 像素,响应式模式使用 DPR 1(截图像素与视口一致);宽度须在 320–3840、高度在 320–2160 之间,非法输入会直接失败而非被钳制;退出响应式模式会清除覆盖并恢复宿主自然 DPR。它只应用于响应式/设备尺寸测试,平时保持正常 IAB 视口即可。


结语:把六步流程内化为习惯

回顾整个 workflow,它的设计目标非常清晰:让模型的每一步决策都建立在自己刚拿到的可验证事实之上。引导(bootstrap)解决"kernel 是新的"问题,标签页列表解决"目标在哪里"的问题,domSnapshot()解决"页面是什么"的问题,count()确认解决"定位器是否唯一"的问题,组合观察({ controlledTabs, userTabs })解决"动作有没有生效"的问题,finalize/close解决"标签页怎么收尾"的问题。按这套协议执行,浏览器自动化轨迹会稳定、可复现、且每一步都有据可查——这正是 ZCode Browser Use 在 workflow.md 中希望 Agent 内化的行为准则。

  • 人工智能
  • 大模型
  • 代码智能体
  • AI Agent
  • 桌面应用
  • 后端
  • 前端
  • CLI

【免费下载链接】ZCode

ZCode 是 AI 编程工作台,提供桌面应用、浏览器界面和终端 Agent。本仓库包含客户端、后端服务、共享 UI,以及 Agent CLI 与运行时源码。

项目地址:https://gitcode.com/zai-org/ZCode
点击查看免费下载
上一篇:ElastAlert 自定义规则开发:从 YAML 配置到 Python 插件编写
下一篇:twin.macro与Web Assembly交互样式

创作声明:本文部分内容由AI辅助生成(AIGC),仅供参考

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询