WS /ws — протокол кадрів
Для розробників, у коді: потокова розмова, якою говорить віджет — кожен кадр клієнта й сервера з полями, коди помилок, гарантія порядку і міст дій на сторінці.
WS /ws — те, як говорить віджет. Один сокет на сторінку; ключ віджета їде в кожному кадрі;
Origin рукостискання перевіряється разом із ключем на кожному кадрі; невдала автентифікація надсилає кадр error і закриває
сокет з кодом 1008.
Клієнт → сервер
Кадр повідомлення — без type. Поля POST /message плюс
widget_key:
{
"widget_key": "wk_…",
"agent_code": "default",
"widget_code": "default",
"message": "Покажи тарифи",
"conversation_id": "…",
"page_url": "https://…",
"page_lang": "uk",
"session_id": "…",
"user_token": "…",
"spoken": false,
"visitor": {
"referrer": "…",
"utm_source": "…",
"browser_language": "uk-UA",
"timezone": "Europe/Kyiv"
},
"page": {
"readable": true,
"actionable": true,
"counts": { "input": 4, "button": 2 },
"submit_policy": "confirm"
}
}
page каже, що цей хід може робити на екрані: readable прив’язує інструмент читання екрана,
actionable — do_page_action, submit_policy повторює налаштування власника. Без page жоден
інструмент сторінки не прив’язується і кадри моста не надсилаються.
| Кадр | Поля | Примітки |
|---|---|---|
proactive_tick |
widget_key, type, agent_code, page_url, page_lang, user_token?, shown_ago_ms? |
не починає розмови; завжди отримує рівно один proactive_teaser або proactive_none |
voice_timing |
widget_key, type, message_id, done_ms, first_audio_requested_ms, first_audio_played_ms |
телеметрія почутої відповіді; без відповіді; хибний кадр мовчки відкидається |
page_elements |
type, req_id, elements[] |
відповідь на page_elements_request — маніфест |
page_done |
type, req_id, results[], added, removed, addedTotal, removedTotal, pressed, stoppedAt, plannedSteps, rows |
відповідь на page_do — результати по кроках плюс що пакет додав на екран чи прибрав |
page_scrolled |
type, req_id, ok, reason?, moved?, at_top?, at_bottom?, section?, next?, handled_by_host? |
відповідь на page_scroll / page_scroll_by |
Сервер → клієнт
| Кадр | Поля | Коли |
|---|---|---|
start |
conversation_id |
хід почався; id, який треба зберегти |
lang |
language |
перед першим токеном; надсилається знову, якщо мова відповіді виявляється іншою |
token |
text |
шматок відповіді, у міру потоку |
reasoning |
text |
міркування моделі, лише коли власник увімкнув розширене мислення |
speakable |
— | дорадчий: наступні токени можна озвучувати в міру надходження |
step |
role: thinking · answer |
thinking означає, що щойно переданий текст — преамбула інструмента, яку треба прибрати, а не відповідь |
done |
message_id, answer, sources[], actions[], nav_suggestions[], confirm_strings?, metadata |
хід закінчився; confirm_strings (confirm.accept/decline/press/dontPress, мовою відповіді) лише коли дію притримано з confirm: true |
limit_reached |
conversation_id, message |
місячна стеля; надсилається після start замість відповіді |
error |
code, message |
див. нижче; сокет лишається відкритим, крім кодів автентифікації |
proactive_teaser |
text (≤ 300), action: { kind: "navigate", url, route } · { kind: "ask", text } · null, codes[] |
знахідка для показу |
proactive_none |
— | сказати нічого |
page_elements_request |
req_id, reason?: "prefetch" |
просканувати екран і відповісти page_elements |
page_do |
req_id, steps[{ op, target, value?, checked? }] |
застосувати кроки і відповісти page_done; target — id з маніфесту, $new[.n] або list[n].field; value може бути мапою { мова: текст } |
page_scroll |
req_id, target |
прокрутити до названого місця |
page_scroll_by |
req_id, direction: down · up · top · bottom |
прокрутити сторінку |
Коди помилок
code |
Коли | Сокет |
|---|---|---|
bad_request |
не JSON-об’єкт або повідомлення не пройшло валідацію | відкритий |
unauthorized · forbidden |
ключ або origin | закритий 1008 |
no_agent · not_found |
у віджета немає помічника; невідомий помічник чи розмова | відкритий |
rate_limited |
хвилинний ліміт акаунта, або хвилинний чи добовий ліміт цього відвідувача; кредит не витрачено | відкритий |
widget_off |
віджет, який називає кадр, вимкнено, або вимкнено всі віджети сайту | відкритий |
agent_error |
генерація не вдалася | відкритий |
busy |
повідомлення відкладено за межу повтору і не виконано — надішліть знову | відкритий |
declined |
модель відмовилася відповідати на це повідомлення; message порожній, нічого не збережено |
відкритий |
Порядок
Кожне повідомлення і кожен запит отримують рівно одну відповідь, у порядку надходження. Кадр,
набраний, поки йде хід із діями на сторінці, відкладається, а не відкидається, і виконується,
щойно хід закінчується; понад глибину два наступні повідомлення отримують busy. Поле вводу
віджета ніколи не вимикається — саме тому це правило існує.
Таймаути, яких має дотримуватися віджет
Запит моста (page_elements_request, page_do, page_scroll*) очікується 5 с; попереднє
зчитування на початку ходу (reason: "prefetch") — 1,5 с; без відповіді екрана цього ходу
немає, і це ніколи не означає «екран порожній». Щонайбільше 40 обмінів моста за хід і 40
кроків на page_do.
Мінімальний клієнт
const ws = new WebSocket(`${agentBase.replace(/^http/, "ws")}/ws`);
ws.onopen = () => ws.send(JSON.stringify({ widget_key, agent_code: "default", message: "Привіт" }));
ws.onmessage = ({ data }) => {
const f = JSON.parse(data);
if (f.type === "token") append(f.text);
if (f.type === "done") finish(f.answer, f.actions, f.nav_suggestions);
if (f.type === "page_elements_request")
ws.send(JSON.stringify({ type: "page_elements", req_id: f.req_id, elements: [] }));
};
Клієнт, що не реалізує міст, не має надсилати page; клієнт, що отримав кадр моста, на який не
може відповісти, має відповісти порожнім результатом, а не мовчати.