Skip to content

Разработка скриптов RoxyBrowser

Подсказка

Автоматизационные скрипты RoxyBrowser — это переиспользуемые TypeScript-скрипты на Playwright. Они выполняются в контролируемой sandbox-среде RoxyBrowser и подключаются к назначенному браузерному профилю, чтобы выполнять действия на странице, собирать данные, публиковать контент, обслуживать аккаунты и решать другие задачи автоматизации.

Что такое скрипт

Скрипт RoxyBrowser — это не обычная локальная Node.js-программа. Каждый скрипт является самоописывающимся TypeScript-файлом. Перед запуском RoxyBrowser извлекает из него метаданные, схему параметров и точку входа, а затем выполняет код внутри контролируемой VM sandbox.

Стандартный скрипт состоит из следующих частей:

ЧастьНазначение
defineMetadata({...})Объявляет имя, описание, версию, имя рабочей директории и другие метаданные скрипта
defineParams({...})Объявляет JSON Schema для параметров запуска
export async function main(params)Точка входа скрипта, которую вызывает sandbox
Логика подключения PlaywrightПодключается к браузеру RoxyBrowser через process.env.BROWSER_URL
Необязательный файл разрешенийРазрешает чтение внешних файлов или добавляет нестандартные хосты для fetch

Базовый шаблон скрипта

ts
import { chromium, firefox } from 'playwright'
import { defineMetadata, defineParams } from '@roxybrowser/sandbox'
import { sleep } from '@roxybrowser/sandbox/utils'

interface ScriptParams {
  keyword: string
}

defineMetadata({
  name: 'Поиск ключевого слова',
  description: 'Открывает текущий браузерный профиль и ищет указанное ключевое слово',
  version: '1.0.0',
  slug: 'search-keyword',
})

defineParams({
  type: 'object',
  properties: {
    keyword: {
      type: 'string',
      description: 'Ключевое слово для поиска',
    },
  },
  required: ['keyword'],
})

export async function main(params: ScriptParams): Promise<void> {
  const browserUrl = process.env.BROWSER_URL ?? ''
  const browserType = process.env.BROWSER_TYPE ?? 'chromium'
  const browser = browserType === 'firefox'
    ? await firefox.connect(browserUrl)
    : await chromium.connect(browserUrl)

  const context = browser.contexts()[0] ?? await browser.newContext()
  const page = context.pages()[0] ?? await context.newPage()

  await page.goto('https://www.google.com/search?q=' + encodeURIComponent(params.keyword))
  await sleep(1000, 2000)

  await browser.close()
}

При написании скриптов учитывайте следующие правила:

  • defineMetadata и defineParams должны быть статическими вызовами на верхнем уровне файла. Нельзя использовать внутри них переменные, spread-синтаксис, вызовы вспомогательных функций или вычисляемые значения.
  • Модуль должен экспортировать main.
  • Не вызывайте main вручную. Runner вызывает его автоматически.
  • Все значения, которые меняются от запуска к запуску, передавайте через main(params): ключевые слова, URL, количества, вероятности, диапазоны и т. д.
  • Даже если скрипту не нужны параметры, нужно объявить defineParams({ type: 'object', properties: {} }).

Sandbox-среда выполнения

Скрипты RoxyBrowser выполняются внутри vm.SourceTextModule sandbox. Это контролируемое подмножество Node.js, предназначенное для надежного управления браузером и ограничения доступа к системе хоста, сети и переменным окружения.

Встроенные глобальные объекты

Следующие возможности доступны без import:

Глобальный объектОписание
consoleЛоги передаются в журнал запуска; console.debug выводится только в debug-режиме
processКонтролируемое подмножество process с безопасными методами и разрешенными переменными окружения
fetchДоступен; распространенные платформенные домены разрешены по умолчанию, остальные хосты требуют файл разрешений
ТаймерыsetTimeout, clearTimeout, setInterval, clearInterval
Стандартные встроенные объектыBuffer, URL, URLSearchParams, TextEncoder, TextDecoder, Promise, JSON, Date, Map, Set и другие

XMLHttpRequest в sandbox недоступен.

Подмножество process

process.cwd() возвращает реальную рабочую директорию текущего скрипта:

text
workdir/{slug}/

Эта директория не является корнем проекта или системным корнем. Файлы, созданные скриптом, а также данные Table и KV по возможности следует хранить здесь.

Доступны только следующие переменные окружения:

Переменная окруженияОписание
BROWSER_URLWebSocket-адрес браузера для chromium.connect() или firefox.connect()
BROWSER_TYPEТип браузера, обычно chromium, иногда firefox
SANDBOX_DEBUG1 в debug-режиме, 0 в release-режиме

Другие переменные окружения хоста не передаются и возвращают undefined. Не перебирайте и не используйте переменные окружения хоста.

Разрешенные import

Sandbox применяет allowlist для import. Разрешены следующие модули:

МодульНазначение
playwrightПодключение к браузеру RoxyBrowser и управление им
fs, node:fs, fs/promises, node:fs/promisesРабота с файлами с учетом модели разрешений
path, node:pathРабота с путями
url, node:urlРабота с URL
crypto, node:cryptoХеширование, случайные значения и базовые crypto-утилиты
buffer, node:bufferРабота с Buffer
stream, node:streamРабота с потоками
@roxybrowser/sandboxМакросы самоописания скрипта
@roxybrowser/sandbox/*Встроенные пакеты возможностей sandbox RoxyBrowser

Import за пределами allowlist завершится ошибкой во время выполнения:

text
Module not allowed

Часто заблокированные модули: child_process, worker_threads, net, http, https, os, vm, dgram. Сетевой трафик страницы должен выполняться через Playwright. Если самому скрипту нужен HTTP-запрос, используйте fetch, который контролируется разрешениями.

Встроенные sandbox-пакеты

RoxyBrowser предоставляет набор пакетов @roxybrowser/sandbox/*. Предпочитайте их повторной реализации типовых возможностей внутри скрипта.

@roxybrowser/sandbox

Корневой пакет предоставляет макросы самоописания скрипта:

ts
import { defineMetadata, defineParams } from '@roxybrowser/sandbox'
APIОписание
defineMetadata(metadata)Объявляет имя, описание, версию, slug, браузерный профиль по умолчанию и другие метаданные
defineParams(schema)Объявляет JSON Schema для main(params)

name, description и description у параметров поддерживают локализованные объекты:

ts
defineMetadata({
  name: {
    zh: '采集商品价格',
    us: 'Scrape product prices',
    ru: 'Сбор цен товаров',
  },
  description: {
    zh: '从商品列表页采集标题和价格',
    us: 'Scrape titles and prices from a product listing page',
    ru: 'Собирает названия и цены со страницы списка товаров',
  },
})

@roxybrowser/sandbox/utils

Пакет utilities предоставляет чистые вспомогательные функции. Он не обращается к сети, не использует секреты и сам по себе не сохраняет данные.

APIОписание
sleep(min, max)Ждет случайное время в диапазоне [min, max] миллисекунд, удобно для естественного темпа
randomBetween(min, max)Возвращает случайное целое число в указанном диапазоне
pick(arr)Случайно выбирает один элемент массива
maybe(probability)Возвращает true с заданной вероятностью, например maybe(0.7) означает 70%
chunk(arr, size)Делит массив на пакеты фиксированного размера
retry(fn, opts)Повторяет нестабильные операции с backoff

Пример:

ts
import { maybe, retry, sleep } from '@roxybrowser/sandbox/utils'

if (maybe(0.7)) {
  await page.getByRole('button', { name: 'Like' }).click()
}

await retry(
  () => page.locator('[data-testid="result"]').waitFor({ state: 'visible' }),
  { attempts: 3, baseDelay: 500 },
)

await sleep(1000, 3000)

@roxybrowser/sandbox/human

Пакет human предоставляет явные человекоподобные действия мыши, клавиатуры и прокрутки.

ts
import { createHuman } from '@roxybrowser/sandbox/human'

const human = createHuman(page)
await human.click({
  target: page.getByRole('button', { name: 'Submit' }),
  motion: {
    duration: 600,
    curve: 'ease-in-out',
    path: { type: 'bezier', curvature: 40 },
  },
  settleDuration: 300,
  holdDuration: 80,
})

createHuman(page) отслеживает собственную позицию указателя. После создания human-актора по возможности используйте в одном сценарии human.move, human.hover, human.click, human.type и human.scroll. Смешивание с page.mouse или Locator click/hover может сделать отслеживаемую позицию неточной.

@roxybrowser/sandbox/2fa

Пакет 2fa работает с TOTP-кодами двухфакторной аутентификации.

ts
import { parse2FA, totp } from '@roxybrowser/sandbox/2fa'

const code = totp(params.twoFactorSecret)
const detail = parse2FA(params.twoFactorSecret)
console.log(`2FA code expires in ${detail.secondsRemaining}s`)
APIОписание
totp(input, options?)Возвращает текущий код подтверждения
parse2FA(input, options?)Возвращает код, оставшиеся секунды, время истечения, количество цифр, алгоритм и другие детали

@roxybrowser/sandbox/llm

Пакет llm вызывает модель через хост. Он полезен для генерации текста, классификации, извлечения данных и структурированных решений внутри скрипта. Вызов может быть медленнее и может иметь стоимость, поэтому используйте его только когда это действительно требуется бизнес-логике.

ts
import { json } from '@roxybrowser/sandbox/llm'

const result = await json<{ price: number }>(
  `Extract the product price from this text: ${text}`,
  {
    type: 'object',
    properties: {
      price: { type: 'number' },
    },
    required: ['price'],
  },
  { temperature: 0, maxTokens: 200 },
)
APIОписание
ask(prompt, opts?)Возвращает обычный текст, сгенерированный моделью
json(prompt, schema, opts?)Возвращает структурированный объект, соответствующий JSON Schema

@roxybrowser/sandbox/table

Table — это CSV-хранилище строк. Оно подходит для результатов парсинга, записей выполнения и экспортируемых данных.

ts
import { Table } from '@roxybrowser/sandbox/table'

const records = new Table('product-records')
await records.append({
  title,
  price,
  url: page.url(),
  capturedAt: Date.now(),
})

const rows = await records.all()

Один экземпляр new Table(name) соответствует одному CSV-файлу в рабочей директории скрипта:

text
<name>.csv

Все методы возвращают Promise и требуют await:

APIОписание
append(record | record[])Добавляет одну или несколько строк
all()Читает все строки
find(predicate)Находит первую подходящую строку
clear()Очищает таблицу

@roxybrowser/sandbox/kv

KV — это JSON key-value хранилище. Оно подходит для состояния между запусками: обработанные ID, время последнего запуска, курсоры пагинации.

ts
import { KV } from '@roxybrowser/sandbox/kv'

const state = new KV('crawler-state')
if (await state.has(productId)) {
  console.log('skip processed product')
  return
}

await state.set(productId, {
  processedAt: Date.now(),
  url: page.url(),
})

Один экземпляр new KV(name) соответствует одному файлу в рабочей директории скрипта:

text
<name>.kv.json
APIОписание
get(key)Читает ключ; если ключа нет, возвращает undefined
set(key, value)Записывает JSON-сериализуемое значение
has(key)Проверяет наличие ключа
delete(key)Удаляет ключ
keys()Читает все ключи
all()Читает полный снимок
clear()Очищает namespace

Модель разрешений

Модель разрешений sandbox следует принципу минимальных привилегий:

  • Рабочая директория workdir/{slug}/ доступна для чтения и записи по умолчанию.
  • Внешние файлы недоступны для чтения по умолчанию и должны быть объявлены в fs.read.
  • Внешние пути недоступны для записи, даже если они указаны в файле разрешений.
  • fetch по умолчанию разрешает распространенные платформенные домены. Хосты вне этого списка нужно объявлять в fetch.allow.
  • Import, переменные окружения и низкоуровневые сетевые модули ограничены allowlist.

Разрешения файловой системы

Скрипты могут импортировать нативные fs / node:fs / fs/promises / node:fs/promises. Методы этих модулей не переписываются RoxyBrowser. Фактические границы доступа задаются Node permission flags.

Рекомендуемый вариант — записывать результаты скрипта в текущую рабочую директорию:

ts
import { writeFile } from 'node:fs/promises'
import path from 'node:path'

const outputPath = path.join(process.cwd(), 'result.json')
await writeFile(outputPath, JSON.stringify({ ok: true }, null, 2), 'utf8')

Если скрипту нужно читать внешний входной файл, например /Users/me/input/accounts.csv, объявите путь для чтения в файле разрешений:

json
{
  "version": 1,
  "fs": {
    "read": [
      "/Users/me/input/"
    ]
  }
}

Важно:

  • Каждое значение в fs.read передается Node как отдельный аргумент --allow-fs-read=<value>.
  • Обычно лучше объявлять директорию, а не один временный файл.
  • Запись во внешние пути не поддерживается. Записывайте файлы в process.cwd() или используйте Table и KV.

Разрешения fetch

Runtime предоставляет fetch и по умолчанию разрешает набор распространенных платформенных доменов. Стандартный список покрывает популярные платформы трансграничной электронной коммерции, соцсетей и контента, например Alibaba, AliExpress, Amazon, eBay, Etsy, Lazada, Mercado Libre, Rakuten, Shopee, Shopify, Walmart, Temu, TikTok Shop, Meta, TikTok, YouTube, X/Twitter, Reddit, LinkedIn, Pinterest, Telegram и Discord.

Домены из стандартного списка совпадают как с корневым доменом, так и с поддоменами. Например, если youtube.com разрешен по умолчанию, доступны и youtube.com, и www.youtube.com.

Если скрипту нужен хост вне стандартного списка, добавьте его через файл разрешений. Файл разрешений расширяет стандартный список, а не заменяет его:

json
{
  "version": 1,
  "fetch": {
    "allow": [
      "api.example.com",
      "*.googleapis.com"
    ]
  }
}

Правила сопоставления:

  • api.example.com разрешает только этот точный хост.
  • *.googleapis.com разрешает поддомены вроде sheets.googleapis.com и drive.googleapis.com.
  • Wildcard совпадает только с поддоменами, но не с apex-доменом. *.example.com не включает example.com.
  • Поддерживаются только HTTP(S)-запросы.
  • Автоматические редиректы отключены. Если endpoint возвращает 3xx, скрипт должен проверить location, убедиться, что целевой хост также разрешен, и затем запросить его явно.

Если данные уже проходят через страницу, лучше слушать ответы Playwright, а не делать отдельный fetch:

ts
page.on('response', async (response) => {
  if (response.url().includes('/api/products')) {
    const data = await response.json()
    console.log('products', data.length)
  }
})

Трафик страницы инициируется браузером и не требует fetch.allow.

Сигнал отказа в разрешении

Когда скрипт пересекает границу разрешений, типичная ошибка выглядит так:

text
Access to this API has been restricted.

При такой ошибке сначала определите, какая операция не прошла:

Неудачная операцияЧто проверить
Не удалось прочитать файлПроверьте, находится ли путь внутри process.cwd() или покрыт fs.read
Не удалось записать файлПроверьте, не идет ли запись во внешний каталог; внешнюю запись нельзя разрешить
Ошибка fetchПроверьте, есть ли хост запроса и каждый хост редиректа в стандартном allowlist или fetch.allow

Не считайте отказ в разрешении ошибкой логики страницы. Сначала исправьте путь или файл разрешений, затем запустите скрипт снова.

Вывод, сохранение и отладка

Разные типы вывода лучше направлять в разные места:

ТипРекомендуемый способ
Прогресс выполненияconsole.log / console.info
Отладочные деталиconsole.debug
Причина сбояconsole.error с достаточным контекстом для диагностики
Результаты сбора данныхTable
Состояние между запускамиKV
Произвольные файловые результатыЗапись в process.cwd()
Скриншоты страницыpage.screenshot({ path: 'name.png' }), путь должен быть относительным

Путь в page.screenshot({ path: 'result.png' }) обрабатывается Playwright через хост и сохраняется в browser artifact зоне рабочей директории скрипта. Оставляйте пути скриншотов относительными. Не собирайте host-only директории Playwright вручную.

Распространенные ошибки

ОшибкаПричинаКак исправить
Module not allowedИмпортирован модуль вне allowlistИспользуйте разрешенный модуль или sandbox-пакет
Access to this API has been restricted.Не хватает разрешений для файла или fetchПроверьте fs.read, стандартный fetch allowlist, fetch.allow или путь записи
defineMetadata не разбираетсяМетаданные не являются статическим объектом на верхнем уровнеУберите динамические переменные, spread-синтаксис и helper-вызовы из аргументов макроса
defineParams не разбираетсяСхема параметров не является статическим объектом на верхнем уровнеИспользуйте полную статическую JSON Schema
main не найденСкрипт не экспортирует точку входаИспользуйте export async function main(...)
Клик по неправильному элементуLocator слишком широкий или .first() выбрал не тот экземплярСначала сузьте поиск до уникального контейнера, затем найдите control внутри него
Скрипт периодически уходит в timeoutСтраница еще не отрендерилась или lazy loading не завершилсяЖдите конкретный элемент или сигнал стабильной загрузки