Опубликовано
Как я научил Hermes Agent превращать сайт в HTTP-клиент
Автор: Гусев Николай [портфолио]
Как я научил Hermes Agent превращать сайт в HTTP-клиент
Так я подключал к личному ассистенту сайт аптеки, историю чеков ФНС и источники домашней бухгалтерии. Обычно на новый коннектор уходит пара часов. Агент не начинает исследование с пустого места: удачные приёмы, команды и известные подводные камни уже записаны в скилле har-derived-api-client.
Кто что делает
Захватываем и разбираем HAR
Сначала ставлю скилл и Playwright. Затем перехожу в каталог scripts установленного скилла. В примере селектор и адрес заменены, их нужно взять со своего сайта.
hermes skills install official/web-development/har-derived-api-client
pip install playwright requests
playwright install chromium
cd ~/.hermes/skills/har-derived-api-client/scripts
python3 har_capture.py "https://<site>/" out.har --headed \
--action "fill:input[name=search]:test query" --action "sleep:3"
python3 har_to_client.py out.har --host <site> --max-body 400
sleep:3 здесь не украшение. Поиск часто ждёт, пока человек перестанет печатать, и только потом отправляет запрос. Если действие завершится с ошибкой, Playwright не успеет закрыть контекст и записать HAR. В таком случае я исправляю селектор и запускаю захват снова. Официальная документация Playwright отдельно напоминает, что контекст надо закрыть, иначе HAR не сбросится на диск: параметры browser.new_context.
Команда разбора группирует одинаковые пути, заменяет числовые и UUID-сегменты на {id}, показывает образец тела и печатает блок Replay hints. В нём особенно важен браузерный User-Agent. Стандартный python-requests некоторые сайты встречают ответом 403.
Если Hermes подключён к уже открытому Chrome или облачному браузеру по CDP, нужен другой захватчик. Команда выглядит так: python3 har_capture_cdp.py "$BROWSER_CDP_URL" out.har --goto "https://<site>/" --action "fill:input[name=search]:test query" --action "sleep:3" --wait 2. Скрипт подключается к существующей вкладке и не закрывает чужой браузер. Локальный har_capture.py, напротив, запускает собственный Chromium.
Переносим запрос из браузера в Python
Из отчёта har_to_client.py агент берёт URL, параметры и браузерный идентификатор. Минимальный клиент для открытого JSON-эндпоинта получается таким:
import requests
r = requests.get(
"https://<site>/api/search",
params={"query": "test query", "limit": 10},
headers={"User-Agent": "Mozilla/5.0 ... Chrome/... Safari/537.36"},
timeout=15,
)
r.raise_for_status()
print(r.json())
Строку User-Agent нельзя копировать из статьи. Её выводит анализатор из вашего HAR. Если Replay hints сообщает о cookie или заголовке с токеном, клиенту понадобится и он. Справочник по передаче заголовков есть в Requests Quickstart.
Сначала агент сравнивает ответ клиента с тем, что видел браузер. Только после этого запрос становится функцией плагина или CLI-командой. Если сервер вернул 401 или 403, причина обычно не в Python: протухла сессия, потерян служебный заголовок или библиотека отправила другой User-Agent.
Почему у сайта аптеки понадобились и HTML, и JSON-RPC
Первый практический сюрприз встретился на сайте аптеки. Поиск, цены и наличие приходили внутри серверного HTML. В HAR не было отдельного JSON со списком товаров, поэтому автоматическое выделение XHR не могло его найти. Агент оставил поиск обычным GET, а карточки разобрал из HTML.
Корзина работала иначе. Браузер отправлял JSON-RPC-запрос cart.AddV2, причём сервер связывал корзину с cartId из тела, а не с cookie. Ниже форма реального запроса, но домен, товар и регион заменены, чтобы не публиковать пользовательские данные.
{
"method": "cart.AddV2",
"params": {"deliveryType": 11, "force": false,
"cartId": "<cart_id>", "cityId": "<city_id>",
"options": {"isPartial": true, "withDisabled": true,
"withUnchecked": true, "isDeliveryOnly": true},
"product": {"count": 1, "shopId": null, "productId": "<product_id>"}},
"jsonrpc": "2.0", "id": 1
}
Клиент отправляет это тело через POST https://<домен-сайта-аптеки>/rpc/?method=cart.AddV2 с Content-Type: application/json. Для автономной корзины cookie не нужны. Но если результат должен появиться в той же корзине, которую я вижу на сайте, агент один раз читает её cartId из моей открытой сессии и хранит идентификатор вне репозитория.
Здесь проверка HTML оказалась важнее красивой гипотезы об API. Если har_to_client.py пишет No API-looking entries found, данные могли прийти с первой HTML-страницей. Тогда надо разбирать HTML, а не выдумывать отсутствующий эндпоинт.
Чеки ФНС и обновляемая сессия
Для ФНС я сам вошёл в браузере по номеру телефона и коду из SMS. Агент прочитал из localStorage токен доступа, refresh-токен и срок действия, сохранил их в файл вне Git и написал обновление сессии. После этого клиент получает страницы списка чеков и отдельно запрашивает состав каждого чека.
Такой клиент использует только мою авторизованную сессию. HAR, токены и выгрузки содержат персональные данные. Я добавляю out.har, secrets/, файлы токенов и JSONL в .gitignore, не прикладываю их к задачам и удаляю HAR после того, как контракт запроса перенесён в код.
Несколько сервисов для домашней бухгалтерии
Для домашней бухгалтерии тот же приём нужен сразу для нескольких вкладок. Я вхожу в каждый сервис в Chrome. Агент подключается к браузеру по CDP, перебирает открытые вкладки и пишет отдельный файл recon/<service>.har. Затем его extract_api.py оставляет уникальные JSON-пути, методы, статусы и короткие образцы тел и собирает общую сводку recon/api_index.json.
По этой сводке видны настоящие контракты всех сервисов без гигабайтов статики. Агент пишет для каждого источника коннектор с одинаковым циклом: получить сырые данные, привести их к общей модели покупок и сохранить. Куки лежат в secrets/<service>_cookies.json, нужные заголовки в secrets/<service>_headers.json. Это важная деталь: часть личных кабинетов проверяет собственный заголовок с Bearer-токеном и не принимает одну лишь cookie.
Что остаётся после пары часов
Повторить процесс можно по исходникам самого скилла: в репозитории лежат SKILL.md и три рабочих скрипта. Как Hermes устанавливает и загружает такие пакеты, описано в документации системы скиллов. Частные API меняются без предупреждения, поэтому сломанный клиент я не чиню подбором случайных URL. Я снова записываю одно действие, сравниваю новый HAR со старым и обновляю скилл. Следующий похожий сайт после этого разбирается быстрее.