enВойти в Senler

Public API и события виджета

Когда нужен Public API

Обычное подключение и настройка канала выполняются по инструкции «Виджет на сайте». Public API нужен после установки кода, если сайт должен сам открывать чат, менять контекст, выбирать диалог, отправлять подготовленный вопрос или реагировать на события виджета.

Отдельно включать Public API в кабинете не нужно: методы появляются в window.SenlerWidget после загрузки кода канала. Настройки, для которых нужен интерфейс кабинета, показаны со скриншотами в связанной инструкции; на этой странице приведены только действия, выполняемые из кода сайта.

Инициализация

Создайте канал по инструкции со скриншотами, затем скопируйте скрипт-загрузчик и channel_id из раздела «Код для встраивания». На одной странице один раз вызовите SenlerWidget.init(config):

<script src="URL_ИЗ_ГОТОВОГО_КОДА" crossorigin="anonymous"></script>
<script>
  SenlerWidget.init({
    channel_id: "xxx",
  });
</script>

Замените оба плейсхолдера значениями из кабинета; не подставляйте адрес скрипта-загрузчика из примера другого проекта или окружения. Параметры init, режимы размещения, пользователь и тема описаны в «Параметрах инициализации». Инициализация создаёт один экземпляр. Повторный init сначала удаляет текущий экземпляр, поэтому после запуска управляйте им методами SenlerWidget, не пересоздавая iframe и не теряя состояние диалога.

Методы

МетодВозвращаетРезультат
SenlerWidget.open(config?)undefinedПрименяет runtime-конфиг, если он передан, и показывает виджет.
SenlerWidget.close()undefinedСкрывает виджет, сохраняя iframe и состояние диалога.
SenlerWidget.toggle()undefinedПереключает видимость.
SenlerWidget.isOpen()booleanПоказывает, открыт ли виджет.
SenlerWidget.selectDialog(dialogId)undefinedВыбирает существующий диалог без отправки сообщения и без автоматического открытия скрытого виджета.
SenlerWidget.setPageContext(items)undefinedЗаменяет постоянный контекст страницы.
SenlerWidget.updateRuntime(config)undefinedМеняет runtime-настройки без пересоздания виджета и сохранения в канал.
SenlerWidget.createInlineTextEdit(config)объект-контроллерСоздаёт контроллер для inline-правок.
SenlerWidget.destroy()undefinedПолностью удаляет iframe, кнопку и обработчики. Переданный сайтом embedded-контейнер остаётся на странице.

Для обычного скрытия используйте close(), а не destroy(). После close() достаточно вызвать open(); после destroy() нужен новый SenlerWidget.init(...).

До готовности экземпляра isOpen() возвращает false. В режиме button_only методы открытия и закрытия ничего не показывают; подробности приведены в настройках.

После загрузки скрипта-загрузчика в SenlerWidget.runtimeProtocolVersion доступен номер текущего протокола (number). Это диагностическое значение для проверки совместимости; не связывайте с конкретным номером бизнес-логику сайта.

Runtime-параметры

Runtime-параметры — это временное состояние текущего экземпляра. Они не сохраняются в настройках канала и сбрасываются после destroy() или повторного init. open(config?) и updateRuntime(config) принимают одинаковую конфигурацию:

ПараметрТипЧто меняет
lang"ru" | "en" | "auto"Язык интерфейса.
display_mode"popup" | "embedded"Режим размещения. Контейнер для embedded должен быть передан ещё при init.
theme_mode"light" | "dark" | "auto"Тему текущего экземпляра.
border_radiusnumberСкругление текущего экземпляра от 0 до 50.
shellobjectcollapse_button и mobile_edge_swipe.
customActionsobjectПолный набор доступных действий сайта и их обработчики handler в браузере.
customActionsLanguage"ru" | "en"Язык описаний действий.
autoExecuteCustomActionNamesstring[]Имена действий, которые разрешено автоматически выполнить в текущем сценарии.
dialogIdstringСуществующий диалог, который нужно выбрать.
startNewDialogbooleanПри true создаёт новый пустой диалог.
focusInputbooleanПри true ставит фокус в поле ввода.
pageContextItemsarrayЗаменяет постоянный контекст страницы. Для навигации понятнее setPageContext(items).
contextItemsarrayПередаёт одноразовый контекст следующего сообщения.
messageobjectПодготавливает или автоматически отправляет сообщение.

Если передан dialogId, виджет выбирает этот диалог; ID должен быть непустой строкой длиной до 200 символов. startNewDialog: true создаёт пустой диалог, а message.startNewDialog: true отправляет сообщение без текущего dialog_id. Если вместе передать dialogId и любой вариант startNewDialog: true, приоритет получает новый диалог, поэтому не объединяйте эти намерения в одном вызове. Не считайте autoExecuteCustomActionNames постоянным разрешением: передавайте список заново в каждом сценарии, где нужен автоматический запуск. Подходящие кнопки скрываются; из одного ответа виджет автоматически выполняет только первое совпавшее действие.

В autoExecuteCustomActionNames можно передать не более 20 уникальных имён. Каждое имя должно соответствовать тем же правилам, что и имя custom action: от 1 до 120 символов, первая буква — латинская, далее допустимы латинские буквы, цифры, _, ., : и -. Повторяющееся имя делает конфигурацию недопустимой. Указывайте только объявленные действия: имя без соответствующего customActions не сможет вызвать обработчик сайта.

message принимает { text, requestId?, startNewDialog?, autoSend? }. Текст должен быть непустой строкой до 10 000 символов, requestId — непустой строкой до 200 символов. autoSend: true отправляет сообщение после готовности виджета; false или отсутствие параметра только подставляет текст в поле ввода. Для такого черновика не передавайте requestId: события результата предназначены для автоматической отправки, а скрипт-загрузчик будет ждать начало запроса и сообщит об ошибке, если пользователь не отправит его в течение 10 секунд.

Неизвестный временный ключ, неверный тип или значение вне допустимого диапазона приводят к синхронной ошибке вызова. В рабочем коде передавайте только поля из этой таблицы и перехватывайте ошибки вокруг динамически собранной конфигурации.

В SPA не вызывайте destroy() и повторный init() при каждом переходе. Обновляйте страницу через setPageContext(items), а для точечного сценария используйте open({ contextItems, message }).

Сворачивание embedded-виджета

В popup крестик уже есть в шапке и скрывает окно. Плавающая кнопка открывает тот же экземпляр снова.

В embedded-режиме включите shell.collapse_button: true, если пользователю нужна кнопка сворачивания в шапке. Кнопка только сообщает сайту о намерении свернуть виджет: сайт сам вызывает SenlerWidget.close() или закрывает внешнюю панель.

SenlerWidget.init({
  channel_id: "xxx",
  display_mode: "embedded",
  container: "#senler-widget",
  shell: {
    collapse_button: true,
  },
  onCollapse(detail) {
    console.log("Пользователь запросил сворачивание", detail);
    SenlerWidget.close();
  },
});

Вместо onCollapse можно один раз подписаться на событие:

window.addEventListener("senler-widget:collapse-request", (event) => {
  if (event.detail.display_mode === "embedded") {
    SenlerWidget.close();
  }
});

В detail приходят channel_id и display_mode. Если настроить и callback, и событие, сработают оба обработчика. Обычно выбирайте один способ, чтобы не выполнить сворачивание дважды.

CLOSE_WIDGET и COLLAPSE_WIDGET — внутренние сообщения протокола iframe. Сайт не должен отправлять их через postMessage.

Мобильный жест

shell.mobile_edge_swipe: true включает свайп от левого края внутри виджета. Скрипт-загрузчик не закрывает интерфейс, а отправляет senler-widget:mobile-edge-swipe.

window.addEventListener("senler-widget:mobile-edge-swipe", (event) => {
  if (event.detail.side === "left") {
    closeMobilePanel();
  }
});

В detail приходят channel_id, display_mode и side: "left". Это навигационный сигнал для сайта, а не замена кнопки сворачивания.

Результат runtime-сообщения

Если передать message.requestId, скрипт-загрузчик отправляет событие senler-widget:runtime-message-result. Оно связывает запрос сайта с отправкой и ответом без опроса истории.

const requestId = crypto.randomUUID();

window.addEventListener("senler-widget:runtime-message-result", (event) => {
  if (event.detail.request_id !== requestId) return;

  if (event.detail.status === "message_sent") {
    console.log("Диалог", event.detail.dialog_id);
  }

  if (event.detail.status === "answered") {
    console.log("Ответ готов");
  }

  if (["message_send_failed", "message_answer_failed", "preview_failed"].includes(
    event.detail.status,
  )) {
    console.error(event.detail.error_message);
  }
});

SenlerWidget.open({
  message: {
    text: "Сделай текст понятнее.",
    requestId,
    startNewDialog: true,
    autoSend: true,
  },
});

Возможные статусы:

СтатусЗначение
acceptedКонфигурация автоматически отправляемого сообщения принята виджетом, но отправка ещё не началась.
sendingНачалась отправка.
message_sentСообщение отправлено; в событии доступен dialog_id.
answeredОтвет завершён.
message_send_failedСообщение не удалось отправить.
message_answer_failedФормирование ответа завершилось ошибкой.
preview_failedНе удалось сформировать preview для связанного сценария правки.

В detail всегда есть request_id и status, а dialog_id и error_message появляются там, где применимы. Скрипт-загрузчик контролирует два отдельных этапа: у iframe есть 10 секунд, чтобы стать готовым и принять запрос; после передачи у виджета есть ещё 10 секунд, чтобы сообщить о начале отправки. Тайм-аут на любом из этапов приводит к message_send_failed. Диалог можно открыть через SenlerWidget.open({ dialogId }) или SenlerWidget.selectDialog(dialogId).

Для правок текста используйте inline-controller: он сам отслеживает requestId и возвращает готовый preview.

Запрос на пополнение лимита

Если в настройках канала включено «Предлагать оплатить лимит», кнопка «Пополнить» отправляет iframe служебное сообщение скрипту-загрузчику. Скрипт проверяет origin, источник iframe и канал, а затем создаёт на window событие senler-widget:credit-purchase-requested:

const expectedChannelId = "xxx";

window.addEventListener("senler-widget:credit-purchase-requested", (event) => {
  if (event.detail.channel_id !== expectedChannelId) return;

  openCreditPayment({
    channelId: event.detail.channel_id,
    leadId: event.detail.lead_id,
  });
});

В event.detail находятся проверенные channel_id и lead_id. Событие только сообщает о намерении пользователя: оно не проводит оплату и не меняет кредитный остаток. Сайт всё равно должен убедиться, что channel_id относится к его интеграции, и открыть собственную форму оплаты.

Начисление после оплаты

После подтверждённой оплаты сервер сайта должен начислить купленные кредиты лиду отдельным запросом к API Senler:

POST /api/projects/{projectId}/leads/{leadId}/credits
Authorization: Bearer senler_sk_...
Content-Type: application/json

{
  "credits": 50000,
  "type": "purchase",
  "reason": "Оплата заказа shop-order-123",
  "idempotency_key": "widget-credit-purchase:shop-order-123"
}

В путь запроса подставьте projectId проекта, а leadId возьмите из события. Перед открытием оплаты проверьте, что event.detail.channel_id совпадает с каналом этой интеграции. Проектный API-ключ должен принадлежать тому же проекту и иметь право can_manage_leads. Храните ключ только на сервере: не помещайте его в конфигурацию скрипта-загрузчика, JavaScript страницы или сетевые запросы браузера.

Поле credits принимает целое количество минимальных кредитных единиц: один показываемый пользователю кредит равен 10 000 единиц, поэтому 50 000 начисляет 5 кредитов. Цена и состав заказа остаются данными сайта и в этот запрос не передаются.

Для одной оплаченной позиции всегда повторяйте один стабильный idempotency_key. При сетевой ошибке можно безопасно повторить запрос с тем же телом и ключом; новый ключ для той же позиции приведёт к повторному начислению. Начисление меняет только дополнительный остаток выбранного лида и не пополняет кредитный баланс проекта.

После начисления виджет получает обновление по уже открытому соединению в реальном времени, снимает блокировку и обновляет остаток. Дополнительный запрос из браузера и периодический опрос не нужны.

Функции обратного вызова и события

В SenlerWidget.init доступны:

  • contextProvider() — синхронно возвращает постоянный контекст страницы; Promise не поддерживается;
  • onCollapse(detail) — сообщает о нажатии кнопки shell.collapse_button.

Кнопки customActions обрабатываются через handler каждого действия, а не через общий callback.

Событие windowКогда приходит
senler-widget:collapse-requestПользователь нажал кнопку сворачивания.
senler-widget:mobile-edge-swipeПользователь сделал разрешённый свайп от левого края.
senler-widget:credit-purchase-requestedПользователь запросил покупку дополнительных кредитов; в detail находятся channel_id и lead_id.
senler-widget:runtime-message-resultRuntime-сообщение принято, завершено или завершилось ошибкой.
senler-widget:stageИзменился диагностический этап загрузки, соединения, отправки, истории или файла. Используйте для наблюдаемости, не для бизнес-логики.

У senler-widget:stage в detail всегда есть area, phase и timestamp; дополнительно могут прийти attempt, duration_ms и итог result: "success" | "error" | "timeout".

areaВозможные phase
loaderinstance-created, iframe-created, iframe-loading, iframe-loaded, app-mounted, ready-timeout, error
bootstraploading, success, error, timeout, interactive
dialogsidle, loading, refetching, success, empty, error
historyidle, loading, refetching, loading-more, success, empty, error
realtimedisabled, token-loading, connecting, connected, reconnecting, disconnected, error
messageoptimistic, sending, queued, sent, failed, waiting, typing, streaming, done, error
uploadrequesting-url, uploading, confirming, ready, error

Это диагностический поток, а не конечный автомат бизнес-процесса. Набор этапов может расширяться, а отдельные фазы — повторяться или пропускаться, поэтому бизнес-сценарий не должен зависеть от их конкретной последовательности.

Действия во встроенном приложении

Обычным страницам достаточно разметки элементов. Для отдельного приложения со своим протоколом управления можно передать pageElementActions в init. Это локальный адаптер сайта, не инструмент MCP; через updateRuntime он не меняется.

  • execute(payload) получает event_id, attempt_id, action, target либо target_chain и, для ввода или выбора, value. Цель содержит context_id, необязательную role и пару entity_type/entity_id для конкретной сущности.
  • Верните null, если цель не относится к приложению: её обработает загрузчик. Для своей цели верните результат с теми же event_id, attempt_id, action, временем executed_at и статусом success, not_found, blocked или failed. Для отказа добавьте error_code и понятный error_message.
  • Поддерживается Promise. Адаптер должен завершиться за 2,5 секунды. После исключения, неверного результата или тайм-аута загрузчик сообщает ошибку и не повторяет действие другим способом. Не отбрасывайте ограничения цели: если приложение не умеет выбирать конкретную сущность, верните blocked.
  • clear(scope) очищает подсветку адаптера: tool относится к подсказке агента, selected к выбранному пользователем элементу, all к обеим. Загрузчик вызывает очистку при новой команде, закрытии подсказки и уничтожении экземпляра.
SenlerWidget.init({
  channel_id: "YOUR_CHANNEL_ID",
  pageElementActions: {
    async execute(payload) {
      const target = payload.target ?? payload.target_chain.at(-1);
      if (!target?.context_id.startsWith("my-app.")) return null;
      return appBridge.execute(payload);
    },
    clear(scope) {
      appBridge.clearHighlights(scope);
    },
  },
});

appBridge реализует разработчик приложения; это не метод Senler. Его execute должен сохранить полную цель и вернуть результат описанного выше формата. Не подключайте рядом второй слушатель PAGE_ELEMENT_ACTION: загрузчик уже принимает команду и отправляет результат.

При передаче команды дочерним iframe загрузчик ждёт ответ одного окна, прежде чем обращаться к следующему. Продолжить поиск позволяет только not_found; успех, отказ или ошибка завершают попытку. На поиск в дочерних окнах отводится 2,5 секунды. Если к этому моменту текущий iframe не ответил, возвращается failed с кодом child_frame_action_timeout, без повторного выполнения в другом окне. Тайм-аут не доказывает, что действие не произошло: перед повтором проверьте состояние приложения.

Связанные страницы