ego-browser
Рантайм автоматизации браузера, которым AI-агенты управляют реальной сессией Chromium внутри ego lite.
ego-browser — это рантайм автоматизации браузера, который ego lite поставляет для AI-агентов. По Chrome DevTools Protocol он подключается к реальной сессии Chromium внутри ego lite, а в качестве точки входа принимает Node.js heredoc-скрипт: агент пишет весь JS-сценарий за одну подачу на stdin, все helper'ы заранее доступны в области видимости скрипта, а состояние браузера сохраняется в Space.
ego-browser не задуман для ручного управления браузером и не заменяет Playwright или Puppeteer. Целевой читатель — LLM-агент.
Кому подходит
- AI-кодинг-агентам, которым нужно водить браузер: Claude Code, Codex, Cursor, кастомные SDK-агенты.
- Командам, делающим вертикальных агентов: автоматизация Lark, Google Docs, Salesforce и других бэк-офисов.
- Повторяющимся веб-флоу: логин, заполнение формы, экспорт, поиск, чтение таблиц.
- Всем, кто пытался запихнуть полный DOM или страницу HTML в LLM и упирался в лимит токенов.
Установка
Идёт в комплекте с ego lite — см. Быстрый старт. После установки ego-browser доступен из любой директории.
Скилл можно поставить и отдельно:
npx skills add github:CitroLabs/ego-lite/skills/ego-browser
Базовый цикл
Типичный ритм агента, управляющего страницей, — всё внутри одного 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=...]. - Действовать по странице через
@Nили CSS-селектор. - Вывести итог через
cliLog(...).
Внутри heredoc вы в Node.js-процессе; внутри
js(...)— в контексте страницы. Не смешивайте.
Справочник helper'ов
Все helper'ы доступны в области видимости скрипта под camelCase именами, без import.
Task Space
await listTaskSpaces()
const task = await useOrCreateTaskSpace('describe task') // переиспользовать или создать
await completeTaskSpace(task.name) // завершено, вкладку оставляем
await closeTaskSpace(task.name) // больше не нужно, закрываем space
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() // в только что созданном task space вкладок может ещё не быть
Наблюдение
await snapshotText() // семантический снимок всей страницы (по умолчанию)
await snapshotText({ scope: 'only_within_viewport' })
await captureScreenshot('result.png')
await drainEvents() // вычитать очередь событий навигации / сети
Мышь и скролл
click, doubleClick, hover и dragMouse принимают единый формат target (CSS-пиксели):
'string': CSS-селектор или@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 — получите варнинг, всё обернётся в .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или сделать одноразовое DOM-извлечение внутриjs(...). cliLog(...)с итоговым результатом.
Полезные комбинации:
captureScreenshot+click([x, y]): визуальная разметка, canvas-UI, виртуализированные списки, страницы с дырявой доступностью.js/elementEval/cdp: прямое извлечение DOM, просмотр состояния браузера, или когда стандартный helper не дотягивает.
Держите навигацию, наблюдение, скролл, извлечение, фильтрацию, агрегацию и вывод в одном heredoc
ego-browser nodejs. Не перекачивайте те же данные через второй локальныйnode-скрипт.
Область действия ref
@N действителен только для refMap последнего snapshotText. Каждый вызов snapshotText() пересобирает refMap. Номера refs берутся из CDP backendNodeId элемента, поэтому один и тот же элемент часто получает один и тот же номер в нескольких снимках — но чтобы @N сработал, N должен присутствовать в последнем снимке.
Типичные причины Unknown ref:
- Элемент уехал за пределы viewport.
- DOM перерисовался.
- Предыдущий проход был
scope: 'only_within_viewport', а текущий не покрыл этот элемент.
Когда нужна стабильная ссылка на один и тот же элемент через много проходов, используйте loc=... из вывода snapshot или CSS-селектор. На этом же стоит накопление Experience — см. Skills.
Skill workspace
ego-browser не несёт изменяемого опыта агента сам по себе. По умолчанию он подгружает расширения helper'ов и накопленный опыт сайтов из skill-bundle репозитория:
../../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 # helper'ы, выставленные в скрипт агента
│ ├── run.js # CLI-вход (исполняет stdin)
│ └── learning/ # индекс опыта, валидация домена, валидация формата
├── artifacts/ego-browser/ # результат сборки; npm bin указывает сюда
└── test/ # unit-тесты
skills/ego-browser/
├── SKILL.md / SKILL.zh.md # точка входа агента
└── learnings/ # каталог опыта по сайтам
Замечания
- По умолчанию
snapshotText()используетscope: 'full_page'— снимает всю страницу. Передавайте'only_within_viewport', только если правда нужна только видимая область. js()возвращает уже результат вычисления выражения, повторныйJSON.parse(...)не нужен.- Когда пишете regex в шаблонной строке
js(), удваивайте обратные слэши (\\d,\\s) или используйтеString.raw. returnверхнего уровня автоматически оборачивается в IIFE. Вложенныйreturnв коллбэке тоже может это вызвать, поэтому сложные выражения лучше сразу писать как(() => { ... })().- Если пользователь явно попросил ego-browser, рантайм уже готов. Не делайте предварительных проверок
which ego-browser/node -v/ дампа help — только если первый запуск реально упал с ошибкой.