Чек-лист приёмки¶
Пошаговая проверка интеграции. Каждый пункт: как воспроизвести (wscat или код), что должно
быть в ответе, как выглядит типичная ошибка. Токен и device.id — подставьте свои; в
примерах реальных токенов нет.
Инструменты: npm i -g wscat, адрес пилота — wss://hermes-go.com/mobile-ws.
Для локальной разработки — ws://127.0.0.1:8787/ws. Токен удобнее передавать в самом кадре
hello (не в URL), чтобы он не осел в логах прокси.
1. Подключение и ready с capabilities¶
Как получить
wscat -c "wss://hermes-go.com/mobile-ws"
# затем отправьте:
{"t":"hello","token":"<ТОКЕН>","device":{"id":"qa-1","name":"QA","platform":"cli","app_version":"0.0.1"},"user":{"id":"u-qa","name":"QA"}}
Ожидаемый результат
{"t":"ready","v":1,"session":"mobile:qa-1","device":{"id":"qa-1","name":"QA"},
"avatar_url":"…","capabilities":{"text":true,"voice":true,"typing":true,
"avatar_states":["idle","thinking","speaking"],"media_in":["image","video","document","audio"],
"media_out":["image","voice","video","document","animation"],
"prompts":["exec_approval","slash_confirm","clarify"],"edit":true,
"max_media_bytes":8388608,"max_text_chars":12000}}
Проверьте: v == 1, session == "mobile:<ваш device.id>", capabilities.edit == true.
Типичная ошибка: {"t":"error","code":"auth_required",…} — кадр hello не первый (или
не отправлен). Первым кадром обязан идти только hello.
2. Отправка текста и получение ответа¶
Как получить
Ожидаемый результат: серия message с одним id и растущим chunk.index, последний —
final:true; перед этим может прийти avatar/typing. Агенту уходит event.message_id == "q-1".
Типичная ошибка: текст ответа не приходит вовсе — устройство ещё не одобрено владельцем
агента. На машине агента: hermes pairing list → approve mobile qa-1. Либо отправлен
пустой текст — тогда прилетит {"t":"error","code":"bad_frame","message":"Пустое сообщение"}.
3. Повтор с тем же id → нет дубля¶
Как получить
{"t":"text","id":"dup-1","text":"запомни число 42"}
{"t":"text","id":"dup-1","text":"запомни число 42"}
{"t":"text","id":"dup-1","text":"запомни число 42"}
Ожидаемый результат: агенту доходит одно сообщение (адаптер игнорирует повторы с тем же
id). Спросите агента «сколько раз я просил запомнить 42?» — ответ «один раз».
Типичная ошибка: используете новый id при каждой переотправке — агент видит три сообщения.
id обязателен для идемпотентности и должен повторяться при retry после обрыва.
4. Карточка одобрения и ответ на неё¶
Как получить: нужен агент, который запрашивает одобрение опасной команды (в Hermes это
exec_approval). Попросите выполнить команду, требующую подтверждения — придёт кадр:
{"t":"prompt","id":"<prompt_id>","kind":"exec_approval","text":"…rm -rf…",
"command":"rm -rf /tmp/build","description":"Удаляет временный каталог сборки",
"options":[{"id":"once","label":"✅ Одобрить один раз","style":"primary"},
{"id":"deny","label":"❌ Отклонить","style":"danger"}],
"session":"mobile:qa-1","timeout_seconds":300}
Проверьте поля command/description: приложение обязано показать точную команду рядом с кнопками.
Ответьте кнопкой:
Ожидаемый результат: {"t":"prompt_done","id":"<prompt_id>","option_id":"once"} и выполнение
команды. Ответ на карточку не уходит в чат как обычное сообщение.
Типичная ошибка: отправляете {"t":"text","text":"once"} вместо кадра prompt — выбор не
применяется; либо неверный prompt_id — придёт prompt_done{expired:true}. Проверка
безопасности: отправьте option_id:"разрешить_всё" — сервер заменит на deny и вернёт его в
prompt_done.
5. Медиа туда и обратно¶
Туда (клиент → агент). Сначала подготовьте base64 маленькой картинки и отправьте:
{"t":"media","id":"mq-1","kind":"image","mime":"image/png","filename":"dot.png","caption":"точка","data":"<base64>","part":0,"parts":1}
Ожидаемый результат: агент получает изображение как вложение (в логе — MessageType.PHOTO).
Попросите «что на картинке?» — агент опишет.
Обратно (агент → клиент). Попросите агента прислать картинку/файл. Придёт кадр media:
{"t":"media","id":"a1","kind":"image","mime":"image/png","filename":"chart.png","caption":"График",
"part":0,"parts":1,"final":true,"size":20480,"data":"<base64>"}
Если файл крупный — несколько кадров с part 0..parts-1, final:true на последнем.
Типичная ошибка: {"t":"error","code":"too_big",…} — файл > 8 МБ отправлен одним кадром.
Режьте по ~3 МБ (part/parts), досылая все куски. Либо unsupported_frame — kind
вне image|video|document|audio.
6. Стриминг: приходят правки с edit=true¶
Как получить: включите стриминг у агента и попросите длинный ответ. Наблюдайте кадры:
{"t":"message","id":"r1","text":"Идёт","edit":true,"chunk":{"index":0,"total":1},"final":false}
{"t":"message","id":"r1","text":"Идёт ответ…","edit":true,"chunk":{"index":0,"total":1},"final":true}
Ожидаемый результат: приходят кадры message с тем же id и edit:true; ваш клиент
перерисовывает один пузырь, а не добавляет новые.
Типичная ошибка: клиент показывает каждый edit-кадр как новое сообщение — видны дубли
растущего текста. Перерисовывайте по id. Помните: если правка не удалась, шлюз дошлёт хвост
обычным message (без edit) — не считайте это ошибкой.
7. Состояния аватара: thinking / speaking / idle¶
Как получить: отправьте {"t":"text","id":"a2","text":"поздоровайся"} и наблюдайте avatar.
Ожидаемый результат (типичный порядок):
{"t":"avatar","state":"thinking"} // + возможно {"t":"typing"}
…message…
{"t":"avatar","state":"speaking"}
{"t":"avatar","state":"idle"} // через паузу после ответа
Типичная ошибка: жёстко ожидаете строго три кадра — speaking может отсутствовать (аватар
озвучивает клиент), а thinking/typing приходят в разном порядке. Обрабатывайте каждое
состояние независимо, значения строго idle|thinking|speaking.
8. typing.stop гасит индикатор¶
Как получить: во время ответа агента дождитесь кадра {"t":"typing","stop":true}.
Ожидаемый результат: индикатор «агент печатает» гасится немедленно, не дожидаясь
таймаута аватара; затем приходит {"t":"avatar","state":"idle"}.
Типичная ошибка: клиент гасит индикатор только по таймауту и игнорирует stop:true —
индикатор «зависает» на пару секунд дольше, чем нужно. stop — явный сигнал, обрабатывайте его
как финал.
9. Отказ с кодом 1008 на неверном токене¶
Как получить
wscat -c "wss://hermes-go.com/mobile-ws"
{"t":"hello","token":"неверный-токен","device":{"id":"qa-bad"}}
Ожидаемый результат: кадр error, затем закрытие соединения кодом 1008:
wscat напечатает Disconnected (code: 1008).
Типичная ошибка: приложение запускает цикл переподключений на 1008 — сервер долбится
впустую. Повторять бессмысленно: покажите «нет доступа» и не переподключайтесь. То же
касается device.id, отсутствующего в MOBILE_ALLOWED_DEVICES.
10. Незнакомый кадр не ломает соединение¶
Как получить: отправьте серверу кадр с неизвестным типом, затем нормальный текст:
Ожидаемый результат: сервер игнорирует незнакомый кадр и продолжает работать — вы получаете ответ на текст. Соединение не рвётся.
Типичная ошибка: клиент сам рвёт соединение или падает с исключением на неизвестном кадре от сервера. Правило протокола: неизвестные типы и необязательные поля игнорируются. Так же обрабатывайте новые кадры, которые пришлёт обновлённый шлюз.
Дополнительно¶
| Проверка | Как | Ожидание |
|---|---|---|
ping/pong |
{"t":"ping","ts":42} |
{"t":"pong","ts":42} |
| пустой текст | {"t":"text","id":"z","text":" "} |
error{code:"bad_frame"} |
| текст > 12 000 символов | один text длинной строкой |
error{code:"too_big"} |
| аудио не wav/ogg | {"t":"audio","format":"mp3",…} |
error{code:"unsupported_frame"} |
медиа-kind неверный |
{"t":"media","kind":"sticker",…} |
error{code:"unsupported_frame"} |
ответ на чужой prompt_id |
{"t":"prompt","prompt_id":"нет-такого","option_id":"once"} |
prompt_done{expired:true}, без ошибки |
| ответ на карточку чужого устройства | подключиться под своим device.id и ответить на карточку, показанную другому устройству |
error{code:"forbidden"} |
занятый device.id другим токеном |
MOBILE_EXTRA_TOKENS=qa-1=<токен>; подключиться с другим токеном под id:"qa-1" |
error{code:"forbidden"} + закрытие 1008 |
deliver не от cron |
обычное (не loopback) соединение шлёт {"t":"deliver","id":"x","target":"qa-1","text":"hi"} |
error{code:"forbidden"} |
| медиа-загрузка сверх лимита памяти | заявить parts > 2048 либо держать открытыми > 8 серий / > 8 МиБ суммарно |
error{code:"bad_frame"} (parts) / too_big (объём) |
| cron-доставка офлайн | hermes send --to <device.id> "текст" при закрытом приложении |
результат device_offline на машине агента |
Как читать отказы: fatal:true в кадре error — соединение будет закрыто, повторять не
нужно (auth_failed). fatal:false — можно продолжать (например, bad_frame на конкретном
кадре). Код закрытия 1008 — всегда отказ в доступе.