На этой странице

Чтение страницы

Открывает страницу по ссылке и возвращает её текст в чистом markdown: без бокового меню, рекламных врезок, иконок и HTML. Нужен, когда агенту надо прочитать конкретную статью, страницу документации или карточку товара - а не искать информацию по теме.

POSThttps://srezai.ru/api/v1/read

Когда использовать

  • Прочитать статью или страницу документации, ссылку на которую вы уже знаете (в том числе полученную из поиска).
  • Собрать контекст для RAG: чистый markdown ложится в индекс без собственного парсинга HTML и вычистки бойлерплейта.
  • Проверить факт на первоисточнике, когда сниппета поисковой выдачи недостаточно.

Если нужно найти информацию по теме, а не прочитать известную страницу - это веб-поиск: с excerpts: true он сразу вернёт текст топ-страниц под запрос одним вызовом вместо десяти отдельных чтений.

Пример запроса

bash
curl -X POST https://srezai.ru/api/v1/read \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer srz_live_ВАШ_КЛЮЧ" \
  -d '{ "url": "https://exa.ai/blog", "maxChars": 12000 }'

Параметры тела

Тело запроса - JSON. Обязателен адрес страницы: либо url для одной, либо urls для батча.

urlstringобязательный

Полный адрес страницы, схема http или https, порт 80 или 443. Адреса, ведущие во внутреннюю сеть (приватные IP, локальные имена), отклоняются с ошибкой 400 - см. ниже. Не нужен, если передан urls.

urlsstring[]необязательный

До 5 адресов вместо url: страницы читаются параллельно, ответ приходит массивом pages. См. Несколько страниц за раз.

maxCharsnumberнеобязательный

Сколько символов текста вернуть: от 200 до 50000 (4000 по умолчанию). Для длинной документации и RAG ставьте с запасом. Значения вне диапазона подгоняются к границе.

offsetnumberнеобязательный

С какого символа текста начать - для чтения длинной страницы окнами. Значение берите из meta.nextOffset предыдущего ответа. См. Длинные страницы по частям.

enginestringнеобязательный

Как забирать страницу: auto (по умолчанию), fast, dynamic, stealth - см. раздел ниже. Неизвестное значение молча трактуется как auto.

freshbooleanнеобязательный

true - прочитать страницу заново, минуя кеш. По умолчанию свежий ответ из кеша переиспользуется; на цену это не влияет ни в ту, ни в другую сторону. См. Кеш страниц.

Движки и авто-эскалация

Страницы устроены по-разному: статике и документации хватает обычного запроса, а SPA на React или Vue отдаёт пустой каркас, пока не выполнится JavaScript. Поэтому по умолчанию (engine: "auto") мы начинаем с самого быстрого способа и поднимаемся на ступень выше только если текста не оказалось.

  • auto - fast → dynamic → stealth, до первого осмысленного результата. Обычной странице хватает одной попытки, сложной - двух-трёх. Цена от числа попыток не зависит.
  • fast (~1-3 с) - без ожидания JavaScript: статические сайты, документация, блоги, новости.
  • dynamic (~3-8 с) - ждём затишья сети и отрисовки: для SPA и ленивой подгрузки контента.
  • stealth (~8-20 с) - максимально браузерное поведение: полная прокрутка страницы (чтобы сработала ленивая подгрузка) и закрытие куки-баннеров и модальных окон, перекрывающих текст. На страницах, которые и так отдают заглушку, auto делает последнюю попытку через второй, независимый браузерный движок.

В ответе приходит engine - ступень, на которой контент удалось взять, и meta.attempts - сколько ступеней было пройдено. Если у конкретного сайта attempts стабильно равен 2 или 3, передавайте нужный движок явно: запрос перестанет тратить время на заведомо неудачные попытки.

Длинные страницы по частям

Документация на 60 000 символов не влезет ни в один разумный maxChars, а модели с окном 32k - и вовсе никуда. Поэтому страницу можно читать окнами: offset задаёт, с какого символа начать, а ответ сам говорит, откуда продолжать.

javascript
// Первая страница
{ "url": "https://exa.ai/blog", "maxChars": 12000 }
// → meta.nextOffset: 11980, meta.hasMore: true

// Продолжение - с того места, где текст оборвался
{ "url": "https://exa.ai/blog", "maxChars": 12000, "offset": 11980 }
// → meta.nextOffset: 23960, meta.hasMore: true

// Последнее окно
{ "url": "https://exa.ai/blog", "maxChars": 12000, "offset": 23960 }
// → meta.nextOffset: null, meta.hasMore: false

Смещение считается по чистому тексту, а не по сырому HTML: сырой объём вы не видите, и от прогона к прогону он меняется (сайт отдал баннер или не отдал). Чистый текст при одном снимке страницы детерминирован, поэтому nextOffset указывает ровно туда, где текст оборвался.

  • Границы окна двигаются по пробелам, чтобы слово не разрезалось. nextOffset при этом считается по фактически отданному тексту - между окнами ничего не теряется и не дублируется.
  • hasMore - тот же признак, что nextOffset !== null, булевым: моделям проще ветвиться по флагу, чем сравнивать с null.
  • offset за пределами текста вернёт пустое окно и nextOffset: null, а не ошибку. Отрицательный трактуется как 0.
  • Второе и последующие окна почти всегда попадают в кеш: страница читается один раз, окна нарезаются из одного и того же текста, поэтому смещения между ответами согласованы.
  • Каждое окно - отдельный вызов и отдельный кредит. Читать страницу целиком одним запросом с большим maxChars дешевле, если она влезает в контекст.

Кеш страниц

Прочитанная страница держится в кеше 15 минут. Повторный запрос той же ссылки за это время возвращается за десятки миллисекунд вместо секунд - краулер не поднимается вообще. В ответе это видно по meta.cached: true и meta.cacheAgeMs - насколько давно страницу читали на самом деле.

Кеш ускоряет, но не влияет на цену: 1 кредит списывается и за попадание в кеш тоже. Так проще считать бюджет - стоимость чтения не зависит от того, читал ли кто-то эту ссылку минуту назад.

  • Кеш ключуется по ссылке и движку: engine: "fast" и engine: "dynamic" - разные записи, потому что и текст у них разный.
  • Хранится полный чистый текст страницы, поэтому окна с разными offset нарезаются из одного снимка - см. выше.
  • Нужен именно свежий снимок (лента новостей, курс, статус заказа) - fresh: true. Ответ придёт с cached: false, а кеш обновится.
  • Проверка адреса на принадлежность внутренней сети идёт до обращения к кешу: запрещённая ссылка получит ssrf_blocked и в том случае, когда её текст уже лежит в кеше.

Несколько страниц за раз

Вместо url можно передать urls - до 5 ссылок на бесплатном тарифе и до 10 на подписке PRO. Страницы читаются параллельно, поэтому пять адресов занимают примерно столько же, сколько самый медленный из них, а не сумму. Пачка больше лимита не отклоняется целиком: лишние адреса отбрасываются, а их число приходит в meta.skipped. Остальные параметры (maxChars, engine) применяются ко всем ссылкам батча.

json
{
  "urls": [
    "https://exa.ai/blog",
    "https://habr.com/ru/articles/1/",
    "https://example.com/404",
    "https://exa.ai/blog"
  ],
  "maxChars": 6000
}

Ответ - массив pages в порядке переданных ссылок. Успешная страница содержит те же поля, что и одиночный ответ; неудачная - только url, error и code. Одна упавшая ссылка не отменяет остальные: HTTP-статус остаётся 200.

json
{
  "pages": [
    {
      "url": "https://exa.ai/blog",
      "title": "Exa | Blog",
      "markdown": "# Exa Blog\n\nThe latest from the team…",
      "truncated": false,
      "engine": "fast",
      "meta": {
        "tookMs": 1840, "totalChars": 5980,
        "offset": 0, "nextOffset": null, "hasMore": false,
        "tokensApprox": 1495, "cached": false, "attempts": 1
      }
    },
    {
      "url": "https://habr.com/ru/articles/1/",
      "title": "…",
      "markdown": "…",
      "truncated": true,
      "engine": "dynamic",
      "meta": {
        "tookMs": 4210, "totalChars": 19740,
        "offset": 0, "nextOffset": 5990, "hasMore": true,
        "tokensApprox": 1498, "cached": false, "attempts": 2
      }
    },
    {
      "url": "https://example.com/404",
      "error": "Не удалось прочитать страницу. / Failed to read the page.",
      "code": "read_failed"
    }
  ],
  "meta": { "tookMs": 4380, "requested": 3, "succeeded": 2, "skipped": 1 }
}

Общий meta батча описывает, что вообще произошло со списком: requested - сколько ссылок пошло в работу, succeeded - сколько прочитано, skipped - сколько отброшено ещё до чтения как дубликаты. В примере выше skipped: 1 - второй раз переданный exa.ai/blog. Так по ответу видно судьбу каждой ссылки, без сверки своего списка с полученным.

  • Дубликаты в списке отбрасываются до запроса - за одну и ту же ссылку дважды платить не нужно; они попадают в meta.skipped.
  • Каждая страница тарифицируется отдельно (1 кредит) и отдельной строкой попадает в историю запросов.
  • maxChars здесь - лимит на каждую страницу. Пять страниц по 12000 символов - это 60000 символов в ответе; для батча обычно разумнее оставить значение по умолчанию.
  • offset и fresh в батче тоже общие на все ссылки. Общий offset осмыслен редко, поэтому длинную страницу из батча дочитывайте отдельным вызовом с url.
  • Больше 5 ссылок за запрос вернут 400. Нужен объём - шлите несколько батчей с оглядкой на burst-лимит.

Ответ

json
{
  "url": "https://exa.ai/blog",
  "title": "Exa | Blog",
  "markdown": "# Exa Blog\n\nThe latest from the team…",
  "truncated": true,
  "engine": "fast",
  "meta": {
    "tookMs": 1840,
    "totalChars": 28407,
    "offset": 0,
    "nextOffset": 11980,
    "hasMore": true,
    "tokensApprox": 2995,
    "cached": false,
    "cacheAgeMs": 0,
    "attempts": 1
  }
}

Поля ответа

urlstringнеобязательный

Нормализованный адрес, который был прочитан.

titlestringнеобязательный

Заголовок страницы (из <title>/метаданных), может быть пустым.

markdownstringнеобязательный

Текст страницы, очищенный от навигации, иконок и служебной разметки, и обрезанный до maxChars.

truncatedbooleanнеобязательный

true, если текст не поместился в maxChars - у страницы есть продолжение. Сколько всего дала страница, видно в meta.totalChars; откуда продолжать - в meta.nextOffset.

enginestringнеобязательный

Ступень движка, которая дала результат: fast, dynamic или stealth.

meta.tookMsnumberнеобязательный

Общее время запроса, мс.

meta.totalCharsnumberнеобязательный

Объём чистого текста страницы целиком, до нарезки окна. По нему видно, с каким maxChars имеет смысл повторить запрос.

meta.offset / nextOffset / hasMorenumber | null | booleanнеобязательный

Какое окно текста вы получили и откуда продолжать. nextOffset: null и hasMore: false - страница дочитана. См. Длинные страницы по частям.

meta.tokensApproxnumberнеобязательный

Грубая оценка объёма ответа в токенах (≈4 символа на токен). Нужна, чтобы агент решал, влезет ли текст в остаток контекста, не угадывая по числу символов.

meta.cached / cacheAgeMsboolean | numberнеобязательный

Ответ пришёл из кеша и насколько давно страницу читали. На цену не влияет - см. Кеш страниц.

meta.attemptsnumberнеобязательный

Сколько ступеней движка было пройдено.

Остаток лимита приходит в заголовках X-RateLimit-* - см. Лимиты и квоты. Обращение к дневной квоте - одно, наравне с поиском, даже если читается батч: в журнале и в списании каждая ссылка батча считается отдельно.

Через MCP: read_url и read_urls

Тот же функционал доступен ИИ-агенту как встроенные инструменты MCP-сервера: read_url для одной страницы и read_urls для батча - с теми же параметрами maxChars, offset, engine, fresh. Ответ - текстовый блок: заголовок страницы и её markdown; при обрезке добавляется явная пометка с полным объёмом текста и готовое значение offset для продолжения - арифметику модели не поручаем, ошибка на пару символов режет слово или дублирует абзац. В батче первая строка - итог («Обработано 2 из 3»), с отдельными строками про неудачи и про отброшенные дубликаты; дальше каждая страница идёт своим блоком с номером и адресом, а непрочитанные ссылки - блоком с кодом ошибки вида [read_failed]. У read_urls параметра offset нет: он был бы общим на весь батч, а дочитать нужно обычно одну ссылку из пяти - поэтому продолжение адресуется на read_url.