ego-browser
面向 AI Agent 的浏览器自动化运行时,连接 ego lite 的真实 Chromium 会话。
ego-browser 是 ego lite 为 AI Agent 提供的浏览器自动化运行时。它通过 Chrome DevTools Protocol 连接到 ego lite 的真实 Chromium 会话,以 Node.js heredoc 脚本为入口:Agent 在一次 stdin 投递中写完整段 JS 流程,所有 helper 都已预注入到脚本作用域,浏览器状态在 Space 中持续保留。
ego-browser 不是给人手动操作浏览器用的,也不是 Playwright / Puppeteer 的替代品,目标读者是 LLM Agent。
适合谁用
- 需要自动操作浏览器的 AI 编程 Agent:Claude Code、Codex、Cursor、自定义 SDK Agent。
- 构建垂直 Agent 的团队:自动操作飞书、Google Docs、Salesforce 等后台系统。
- 反复执行固定网页流程:登录、填表、导出、检索、读取表格。
- 曾经把完整 DOM 或 HTML 塞进 LLM、被 token 限制卡住的人。
安装
随 ego lite 一起安装,参见 快速开始。安装后即可在任意目录用 ego-browser 命令调用。
也可以单独安装 Skill:
npx skills add github:CitroLabs/ego-lite/skills/ego-browser
核心循环
Agent 操作网页的典型节奏,所有命令都在一次 heredoc 内完成:
ego-browser nodejs <<'EOF'
const task = await useOrCreateTaskSpace('search github issues')
await openOrReuseTab('https://github.com/issues', { wait: true, timeout: 20 })
cliLog(await snapshotText())
EOF
- 复用或创建 Task Space(每次 heredoc 都要声明,详见 Space)。
- 打开目标页面。
- 读取快照(
snapshotText())拿到带[ref=N, loc=..., url=...]的语义树。 - 基于
@Nref 或 CSS selector 执行动作。 - 用
cliLog(...)输出最终结果。
heredoc 体内是 Node.js 进程;
js(...)内才是浏览器页面上下文。两者不要混用。
Helper 参考
所有 helper 在脚本作用域内以 camelCase 直接可用,无需 import。
Task Space
await listTaskSpaces()
const task = await useOrCreateTaskSpace('describe task') // 复用或创建
await completeTaskSpace(task.name) // 任务完成、保留 tab
await closeTaskSpace(task.name) // 不再需要展示,关闭空间
name 用 3-6 词自然语言描述当前任务,不要用占位符。
导航与状态
await listTabs()
await openOrReuseTab(url, { wait: true, timeout: 20 })
await gotoAndWait(url, { timeout: 20, settle: 1 })
await newTab(url)
await switchTab(tabId)
await currentTab()
await pageInfo()
await ensureRealTab() // 任务空间刚创建时可能无 tab
观察
await snapshotText() // 整页语义快照(默认)
await snapshotText({ scope: 'only_within_viewport' })
await captureScreenshot('result.png')
await drainEvents() // 消费导航 / 网络事件队列
鼠标与滚动
click / doubleClick / hover / dragMouse 接受统一的 target 格式(CSS 像素):
'string':CSS selector 或@ref,点击元素中心。[x, y]或{x, y}:viewport 坐标。{selector, x, y}:以元素左上角为基准,叠加偏移。options.label:3-6 词描述,传入后触发视觉高亮动画。
await click('@21', { label: '查看登录态' })
await click('button.primary', { label: '点击提交按钮' })
await click([420, 260])
await hover('@5', { label: '悬停查看菜单' })
await dragMouse([from, to], { label: '拖拽卡片' })
await scrollBy(900)
await scroll({ dy: 900 })
await scrollToBottomUntil(
async () => await js(String.raw`document.querySelectorAll('article').length`) >= 20,
{ step: 900, wait: 1, maxSteps: 20 },
)
键盘与输入
await typeText('hello world')
await fillInput('@2', 'user@test.com')
await pressKey('Enter')
await dispatchKey({ ... })
文件与网络
await uploadFile('input[type="file"]', '/absolute/path/to/file.pdf')
await httpGet('https://api.example.com/data') // 浏览器上下文发起的 GET
等待
await wait(1) // 秒
await waitForLoad()
await waitForElement('@1')
await waitForNetworkIdle()
wait()和timeout单位是秒;只有以Ms结尾的参数才是毫秒。
浏览器执行
js(source) 本质是 Runtime.evaluate,接受字符串。不要像 Puppeteer 那样传函数加参数,会触发 warning 并被 .toString() 包裹,闭包变量与参数通道都会丢失。
多步逻辑封装成一个 IIFE 一次返回:
const data = await js(String.raw`(() => {
const items = [...document.querySelectorAll('article')]
return items.map(el => ({
text: el.innerText,
links: [...el.querySelectorAll('a')].map(a => a.href),
}))
})()`)
await elementEval('@1', el => el.getBoundingClientRect())
await cdp('Page.captureScreenshot', { format: 'png' })
输出与自查
cliLog(value) // heredoc 中唯一的输出方式
cliLog(help('click')) // 查看 helper 用法
推荐工作流
通常先从 snapshotText + ref / loc 入手,因为它保留语义结构,并显著降低坐标脆弱性:
- 复用或创建 Task Space。
- 打开或切换页面(
openOrReuseTab/gotoAndWait)。 snapshotText()获取[ref=N, loc=..., url=...]语义树,ref 自动注册到 refMap。- 用
@N配合click/fillInput/elementEval执行动作,或在js(...)内一次性完成 DOM 抽取。 cliLog(...)输出最终结果。
可组合的其他路径:
captureScreenshot+click([x, y]):视觉布局、canvas 类界面、虚拟列表、accessibility 不完整的页面。js/elementEval/cdp:直接抽取 DOM、查看浏览器状态,或常规 helper 不够直接的场景。
优先把导航、观察、滚动、抽取、过滤、聚合、输出写在一段
ego-browser nodejsheredoc 里完成,不要再用第二个本地node脚本处理同一批数据。
ref 的有效范围
@N 仅对最近一次 snapshotText 的 refMap 有效。每次 snapshotText() 都会重建 refMap。ref 编号来自元素的 CDP backendNodeId,同一元素在多次 snapshot 中编号通常一致;但要操作 @N,N 必须出现在最近一次 snapshot 输出中。
触发 Unknown ref 的常见原因:
- 元素被滚出 viewport。
- DOM 重渲染。
- 上一轮
scope: 'only_within_viewport'而下一轮未覆盖该元素。
需要跨多轮稳定引用同一个元素时,使用 snapshot 输出里的 loc=... 作为稳定 selector,或直接写 CSS selector。这也是 Experience 沉淀的基础(见 Skills)。
Skill Workspace
ego-browser 不自带可变 agent experience。默认从仓库 skill 包加载 helper 增强与已学站点经验:
../../skills/ego-browser
通过环境变量覆盖:
EGO_BROWSER_AGENT_WORKSPACE=/path/to/ego-browser ego-browser nodejs <<'EOF'
cliLog(await siteSkills())
EOF
learnings/ 目录下的站点经验始终生效,每次 helper 调用都会读取一次。Experience 写入与发现机制见 Skills。
校验已学经验:
npm run validate:learnings
目录结构
package/ego-browser/
├── src/ # browser-runtime / helpers / run.js
│ ├── browser-runtime.js # 浏览器侧 ego runtime 桥接
│ ├── helpers.js # 暴露给 Agent 脚本的 helper
│ ├── run.js # CLI 入口(执行 stdin)
│ └── learning/ # 经验索引、域名校验、格式校验
├── artifacts/ego-browser/ # 构建产物,npm bin 指向这里
└── test/ # 单元测试
skills/ego-browser/
├── SKILL.md / SKILL.zh.md # Agent 调用入口
└── learnings/ # 站点经验目录
注意事项
snapshotText()默认scope: 'full_page',覆盖整页。仅在只需可见区时才传'only_within_viewport'。js()返回的是表达式求值结果,不要再JSON.parse(...)。js()模板字符串里写正则时,反斜杠要写两次(\\d、\\s),或改用String.raw。- 顶层
return会被自动包成 IIFE;嵌套回调中的return也可能误触发,复杂表达式优先写成(() => { ... })()。 - 用户明确要求使用 ego-browser 时,默认运行时已就绪,不要预先
which ego-browser/node -v/ 查看 help,除非首次运行报错。