Widget
Чат-виджет — один script-тег на странице. Он показывает кнопку в углу, открывает чат с поддержкой, ленту сообщений и историю обращений. Посетителю ничего устанавливать и регистрироваться не нужно.
Минимальная установка
<script>
(function(w, d){
function loadWidget(token){
var s = d.createElement('script');
s.async = 1;
s.src = 'https://support.forestsnet.com/widget-bundle?ws=WORKSPACE_UUID';
if (token) s.setAttribute('data-visitor-token', token);
d.head.appendChild(s);
}
// Необязательно: подписанный токен посетителя с вашего сервера
// (один посетитель на разных устройствах, см. /docs/widget/hmac).
// Без него достаточно вызвать loadWidget().
fetch('/api/supporthub/token', { credentials: 'include' })
.then(function(r){ return r.ok ? r.json() : null; })
.then(function(data){ loadWidget(data && data.token); })
.catch(function(){ loadWidget(); });
})(window, document);
</script>?ws= — UUID проекта. Готовый сниппет с ним есть в Настройки → Каналы → Web Widget и в Конструктор виджета → Код для вставки. Ответ /widget-bundle уже содержит адрес API, а если сервер успел получить настройки виджета — и их, так что отдельный запрос за настройками не нужен. Иначе виджет запросит их сам.Атрибуты script-тега
Бандл читает data-* со своего script-тега. Их можно писать прямо в тег или, в сниппете выше, ставить на создаваемый элемент через s.setAttribute(…) — так уже передаётся data-visitor-token.
| Атрибут | Что делает |
|---|---|
| ?ws= (query) | UUID проекта. Обязателен для автозапуска. |
| data-email | Email посетителя. При загрузке уходит в identify и перезаписывает email контакта. |
| data-name | Имя посетителя. Записывается, если у контакта ещё нет имени (или стоит заглушка вроде «Гость»). |
| data-phone | Телефон, как есть. Сохраняется в extra_data.phone контакта, если там пусто. |
| data-telegram-id | Telegram ID посетителя (число). Записывается в контакт, если у него ещё нет Telegram ID. |
| data-visitor-token | Подписанный токен посетителя с вашего сервера: связывает посетителя на разных устройствах с одним контактом. Формат — в HMAC visitor token, секрет для подписи отдаёт GET /api/v1/widget/signing-secret. |
| data-locale | Язык виджета: ru или en (en-GB → en). Порядок выбора языка — ниже. |
| data-cookie-domain | Домен cookie посетителя, например .example.com, чтобы одна беседа сохранялась между поддоменами. Важнее значения из конструктора. |
| data-hide-launcher="true" | Скрыть плавающую кнопку. Открывайте виджет своей кнопкой через SupportHub.open(). |
| data-api-base | Адрес API. Ответ /widget-bundle сам задаёт адрес из настроек сервера, и тот важнее, поэтому со стандартным сниппетом атрибут ни на что не влияет. |
Передача данных пользователя
Если сайт уже знает посетителя, передайте данные в тег. При загрузке виджет отправит их в POST /api/webhooks/widget/{ws}/identify и создаст или обновит контакт, даже если посетитель ещё не открывал чат. Поля, которые уже известны, форма перед чатом больше не спрашивает.
<script
async
src="https://support.forestsnet.com/widget-bundle?ws=WORKSPACE_UUID"
data-email="{{ user.email }}"
data-name="{{ user.full_name }}"
data-phone="{{ user.phone }}"
data-telegram-id="{{ user.telegram_id }}"
></script>Идентификация из JavaScript
Если пользователь входит в аккаунт уже после загрузки страницы (SPA), вызовите window.SupportHub.identify(). Он доступен сразу после выполнения бандла; вызовы, сделанные до окончания запуска виджета, отправятся, как только он запустится. До загрузки бандла window.SupportHub не существует.
// После входа пользователя:
await window.SupportHub.identify({
email: user.email,
name: user.fullName,
phone: user.phone,
telegram_id: user.telegramId,
// visitor_token: tokenFromYourServer,
});Поля — те же, что у data-*, плюс visitor_token; вместо name можно передать full_name. Передавайте только то, что знаете: пустые поля ничего не стирают. Email перезаписывается при каждом вызове; имя, телефон и Telegram ID записываются, только если у контакта их ещё нет.
Язык виджета
Виджет говорит на двух языках: ru и en. Если в конструкторе («Брендинг → Язык виджета») выбран «Всегда русский» или «Всегда английский» (brand.language — "ru" или "en"), виджет всегда на этом языке и цепочка ниже не действует. При «Авто» ("auto", по умолчанию) язык выбирается по цепочке сверху вниз, первое подходящее значение выигрывает:
| 1 | init({ locale }) | язык, переданный в SupportHub.init() |
| 2 | window.__SH_LOCALE | глобальная переменная, которую страница задала до загрузки бандла |
| 3 | data-locale | атрибут script-тега |
| 4 | <html lang> | язык страницы (ru, en, en-GB), если тексты проекта есть на этом языке |
| 5 | navigator.language | язык браузера посетителя, если тексты проекта есть на этом языке |
| 6 | computed.locales.default | основной язык проекта, если ничего выше не подошло |
Шаги 1–3 действуют всегда. Шаги 4 и 5 — только для языков из computed.locales.supported в ответе /config: язык попадает туда, если на нём есть каждый текст, который проект менял в конструкторе (правило — на странице «Бренд и шапка»). Другие языки (fr, xx) пропускаются. Берутся первые две буквы: en-GB → en, ru-RU → ru. Чаще всего ничего делать не нужно: при <html lang="en"> виджет сам будет на английском, если тексты проекта есть на английском.
<script
src="https://support.forestsnet.com/widget-bundle?ws=WORKSPACE_UUID"
data-locale="en"
async
></script><script>window.__SH_LOCALE = "en";</script>
<script src="https://support.forestsnet.com/widget-bundle?ws=WORKSPACE_UUID" async></script><!-- Бандл без ?ws=: сам он не запустится -->
<script>window.__SH_LOCALE = "en";</script>
<script src="https://support.forestsnet.com/widget-bundle" async
onload="SupportHub.init({ workspace_id: 'WORKSPACE_UUID' })"></script>
<script>
// Сменить язык на лету: убрать виджет и запустить заново
function setWidgetLocale(lang) {
window.__SH_LOCALE = lang; // язык виджета; тексты из конструктора переключатся вместе с ним
window.SupportHub.destroy();
window.SupportHub.init({ workspace_id: "WORKSPACE_UUID", locale: lang });
}
</script>init(), если тег уже подключён с ?ws=: виджет запущен, и появится второй. applyConfig() для смены языка на живом сайте не подходит: он заменяет настройки виджета значениями по умолчанию плюс переданными полями (метод нужен для превью в конструкторе).Тексты на двух языках
Кнопки и служебные надписи виджета всегда на языке виджета. Тексты, которые вы пишете в конструкторе виджета, можно задать для каждого языка: переключатель языка (RU / EN) стоит над разделом. Такие поля: Брендинг (заголовок, подзаголовок, название проекта), Приветствие (заголовок, подзаголовок, сообщение, кнопка, время ответа), Формы (сообщение перед чатом), Чат (подпись истории, просьба об оценке), Часы работы (офлайн, вне рабочих часов, кнопка закрытия), Системные сообщения и Email-мост (подпись и подсказка). Если текст на нужном языке пустой, показывается вариант на другом языке.
init({ locale })). Настройки приходят сразу со всеми языками (/config?locale=all): и встроенные в бандл при стандартном сниппете (?ws=), и запрошенные виджетом при запуске через init(). Так же по языкам работают ваши правки из «Тексты интерфейса»: виджет берёт правки своего языка, а у языка без правок остаются стандартные тексты.Где хранится сессия посетителя
ID посетителя (vs_<random>) связывает анонимную сессию с контактом. Чтобы он переживал перезагрузки, закрытие вкладок и чистку отдельных хранилищ, виджет держит его в пяти местах:
| # | Хранилище | Особенности |
|---|---|---|
| 1 | cookie sh_visitor_<ws> | 1 год, Path=/, SameSite=Lax, Secure на https. Единственное хранилище, которое работает между поддоменами (через домен cookie). Safari (ITP) держит cookie, поставленные скриптом, не больше 7 дней. |
| 2 | localStorage sh-visitor-<ws> | Для одного origin, пока не очищены данные сайта. |
| 3 | sessionStorage sh-visitor-<ws> | До закрытия вкладки. Выручает, когда cookie и localStorage недоступны. |
| 4 | IndexedDB sh-widget | Пишется в фоне; читается при запуске, только если хранилища 1–3 пусты. |
| 5 | localStorage sh-visitor-session | Старый ключ без привязки к проекту: читается для переноса старых посетителей, записывается, если его нет. |
Порядок чтения: 1 → 2 → 3 → 5. Первое непустое значение выигрывает и копируется в остальные хранилища: если cookie пропал, при следующей загрузке ID поднимется из localStorage и снова запишется в cookie.
Вкладки одного браузера синхронизируются через BroadcastChannel('sh-widget-sync'), а где его нет — через событие storage, чтобы две одновременно открытые вкладки не завели два разных ID.
Один посетитель на нескольких поддоменах
По умолчанию cookie ставится на текущий хост: посетитель на example.com и на app.example.com — два контакта и две беседы. Хранилища 2–5 всегда привязаны к origin, поэтому общая сессия возможна только через cookie с доменом:
- В конструкторе: «Конструктор виджета → Расширенные → Cookie-домен» →
.example.com. Сниппет на всех поддоменах остаётся тем же. - Атрибутом:
data-cookie-domain=".example.com". Атрибут важнее значения из конструктора — удобно, если стейдж и прод на разных доменах.
.co.uk — такой cookie браузер отклонит. Точка в начале не обязательна: cookie с атрибутом Domain и так действует на все поддомены.Перенос старых посетителей автоматический: при первом заходе ID из ключа sh-visitor-session записывается в cookie с заданным доменом, и на следующем поддомене браузер найдёт ту же сессию.
JS API
init, identify, logout, on и off есть на window.SupportHub сразу после выполнения бандла, остальные методы появляются, когда виджет запустился.
| Метод | Назначение |
|---|---|
| init(opts) | Ручной запуск. Опции: workspace_id, locale, hideLauncher, apiBase, config, mode, onTokenExpired. С ?ws= в теге виджет запускается сам. |
| open() | Открыть панель (например, по клику на свою кнопку). |
| close() | Свернуть панель. |
| isOpen() | true, если панель открыта. |
| setTab(tab) | Переключить вкладку: "home" | "chat" | "help" | "news" | "miniapp". |
| identify(payload) | Передать данные посетителя: email, name, phone, telegram_id, visitor_token. Возвращает Promise. |
| logout() | Пользователь вышел с вашего сайта: забыть токен и данные identify, завести новый анонимный session ID, закрыть WebSocket и вернуть виджет на главную. Возвращает Promise. См. Identify посетителей. |
| on(event, cb) | Подписаться на событие виджета (см. «События на странице»). Возвращает функцию отписки. |
| off(event, cb) | Снять обработчик, добавленный через on. |
| applyConfig(cfg) | Для превью в конструкторе: заменяет настройки значениями по умолчанию плюс переданными. На сайте не нужен. |
| showState(s, sub?) | Демо-состояния для превью; на сайте ничего не делает. |
| destroy() | Убрать виджет: закрывает WebSocket и удаляет его элементы со страницы. После этого можно снова вызвать init(). |
Что происходит при загрузке
- Бандл определяет адрес API (
window.__SH_API_BASE): его задаёт ответ/widget-bundle; без него —data-api-base, origin скрипта или облако SupportHub. Читаетdata-*и, если в адресе скрипта есть?ws=, запускает виджет. - Берёт настройки из
window.__SH_WIDGET_CONFIGили запрашиваетGET /api/webhooks/widget/{ws}/config. - Находит ID посетителя в хранилищах или создаёт новый.
- Отправляет данные посетителя, если они есть:
POST /api/webhooks/widget/{ws}/identify. - Спрашивает открытую беседу:
GET /api/webhooks/widget/{ws}/recent. Если она есть — открывает её и подключает WebSocket; если нет — WebSocket подключится после первого сообщения посетителя.
Глобальные переменные
window.SupportHub— методы вышеwindow.__SH_API_BASE— адрес APIwindow.__SH_WIDGET_CONFIG— настройки виджета, если сервер вложил их в бандлwindow.__SH_LOCALE— язык изdata-localeили заданный страницейwindow.__SH_WIDGET_V2_MOUNTED— защита от повторного автозапуска- Переменные с префиксом
__sh_— внутренние
WebSocket
Когда у посетителя есть беседа, виджет подключается к wss://api.support.forestsnet.com/api/ws/widget/{visitor_id}?workspace_id={ws} (с токеном посетителя — ещё &signed_token=…). Если контакта для этого посетителя нет, сервер закрывает соединение с кодом 4401. Кадры приходят в виде {"event": "…", "data": {…}}; посетитель получает события своих тикетов:
message.created— новое сообщение (внутренние заметки сюда не попадают)message.edited— сообщение изменено (content,edited_at)message.deleted— сообщение удалено (message_id,ticket_id)typing— оператор печатает (ticket_id,sender), пока в конструкторе включены индикаторы печатиmessage.seen_by_operator— оператор открыл переписку (message_id,ticket_id)ticket.assigned— оператор взял тикет (operator_name,operator_avatar_url)ticket.closed,ticket.reopened,ticket.transferred,ticket.updatedи другие события тикета
Сервер закрывает соединение, если от клиента 90 секунд ничего не приходило, и отмечает посетителя офлайн. Чтобы держать его открытым, шлите {"type": "ping"} — сервер ответит {"type": "pong"}; {"type": "typing", "ticket_id": "…"} показывает операторам, что посетитель печатает. Отклонённый signed_token сервер закрывает кодом 4440 (истёк) или 4401 (неверная подпись): с тем же токеном подключаться бесполезно. Виджет после разрыва переподключается сам и догружает сообщения, пришедшие за время разрыва; после такого кода он ждёт новый токен.
События на странице
SupportHub.on(event, cb) сообщает странице о двух вещах. token_expired — сервер отклонил visitor_token; в cb приходит { reason: "expired" | "invalid" }, один раз на токен: получите свежий и передайте в identify (см. Identify посетителей). unread — изменилось число непрочитанных ответов, { count }: пригодится, если кнопка виджета скрыта (hideLauncher) и вы рисуете свою. Чтобы реагировать на то, что происходит в чатах, на своём сервере используйте webhooks.

