@casecore/chat-widget
v0.7.0
Published
Embeddable CaseCore chat widget for marketing and support conversations on a website
Readme
@casecore/chat-widget
Встраиваемый чат для сайта, обращения из которого попадают в CaseCore: маркетинговые вопросы как лид, технические — как обращение поддержки.
Пакет без зависимостей и без фреймворка — обычный ES-модуль, который монтирует себя сам. Так один и тот же виджет работает и в статической сборке (Astro, Hugo, plain HTML), и в приложении на React или Vue, где иначе понадобился бы клиентский boundary и сборщик.
Граница доверия
Виджет никогда не держит credential CaseCore и не обращается к CaseCore напрямую. Он говорит
только с backend своего сайта, а тот уже добавляет x-casecore-key и вызывает Intake API.
Абсолютный URL в endpoint отклоняется на этапе монтирования: путь должен быть
same-origin, иначе credential пришлось бы отдать браузеру, а на стороне CaseCore включать CORS.
Серверную часть удобно собрать на @casecore/intake-client — там есть startChat,
appendChatMessage и fetchChatTimeline.
Установка
pnpm add @casecore/chat-widgetИспользование
import { mountSupportChat } from '@casecore/chat-widget';
const chat = mountSupportChat({
endpoint: '/api/chat',
campaign: 'pricing-page',
turnstileSiteKey: '0x4AAAA-public-site-key',
});В статическом HTML:
<script type="module">
import { mountSupportChat } from '/vendor/support-chat-widget.js';
mountSupportChat({ endpoint: '/api/chat' });
</script>Возвращается { open, close, destroy } — например, чтобы открыть панель по кнопке на странице
контактов.
Что делает бот
Здоровается, спрашивает тему, собирает имя, e-mail и вопрос — и уходит с дороги: дальше отвечает оператор. Своих ответов по существу он не даёт сознательно. Сценарный ответ, похожий на настоящий, хуже честного ожидания, а KB-ассистент CaseCore требует опознанного контакта, которым посетитель сайта не является.
Согласие на обработку данных запрашивается только у темы продаж: у обращения в поддержку законное основание есть и без него, а чекбокс, который ничего не значит, хуже его отсутствия.
Ожидаемый контракт backend сайта
Три пути относительно endpoint:
| Запрос | Тело | Ответ |
| ------------------------ | ------------------------------------------------------------------------------------------------------ | ------------------------------------------------------------------------ |
| POST /start | { topic, name, email, message, consent, pageUrl, campaign, turnstileToken, website, idempotencyKey } | { data: { caseId, threadToken, caseNumber } } |
| POST /messages | { caseId, threadToken, body } | { data: { id } } |
| GET /timeline?caseId=… | заголовок x-support-thread-token | { data: { messages: [{ id, body, direction, createdAt }], closedAt } } |
threadToken — credential посетителя: он привязан к одному обращению и одному контакту,
хранится в CaseCore только хэшем и отзывается при закрытии обращения. Виджет держит его в
localStorage, чтобы перезагрузка страницы не начинала второе обращение. При каждом открытии
он заново загружает полный timeline с сервера. В шапке доступны «История» для переключения
между сохранёнными обращениями и «Новый диалог» для явного возврата к выбору «Продажи и
демонстрация» / «Получить поддержку». В истории есть явное действие «Назад к диалогу».
«Очистить на этом устройстве» после подтверждения удаляет только локальные credentials и список
из браузера — кейсы и сообщения в CaseCore остаются. Новый диалог не удаляет доступ к предыдущим
обращениям. Панель закрывается видимой кнопкой и клавишей Escape с возвратом фокуса на launcher.
По 401/403/410 недействующая сессия не оставляет пользователя в мёртвой форме ответа: виджет
предлагает начать новый диалог, а сохранённые обращения остаются в истории.
website — скрытое поле-ловушка: заполненное значение backend передаёт как есть, а CaseCore
отвечает обычным успехом, ничего не сохраняя.
Параметры
| Параметр | По умолчанию | Смысл |
| ---------------------- | ------------------- | ----------------------------------------------- |
| endpoint | /api/chat | Same-origin префикс путей backend сайта |
| container | document.body | Куда монтировать |
| topics | продажи + поддержка | Темы и их маршрутизация |
| copy | русский текст | Переопределение любых строк |
| pollIntervalMs | 8000 | Частота опроса ответов, пока панель открыта |
| injectStyles | true | Встроенные стили; false, если оформление своё |
| pageUrl, campaign | — | Записываются на обращение |
| turnstileSiteKey | — | Публичный site key Cloudflare Turnstile |
| fetchImpl, storage | глобальные | Подмена для тестов |
Если передан turnstileSiteKey, виджет сам загружает официальный browser script, блокирует
отправку до успешной проверки и добавляет одноразовый turnstileToken только в первый запрос.
Секрет Turnstile остаётся на CaseCore API; в браузер попадает только публичный site key.
Опрос идёт только при открытой панели: фоновая вкладка не должна расходовать лимиты запросов на разговор, который никто не смотрит.
Оформление
Встроенные стили — одиночные классы support-chat__* и CSS-переменные, без reset. Дефолты
объявлены внутри :where(:root), у которого нулевая специфичность, поэтому переопределение
хоста выигрывает без !important и независимо от того, подключён ли CSS сайта раньше
инжектируемой таблицы:
:root {
--support-accent: #7c3aed;
--support-radius-lg: 2px;
}Полный список — экспорт supportChatThemeTokens; это тот же словарь --support-*, который
использует @casecore/support-widget, поэтому сайт с формой и чатом настраивает тему один
раз. Тёмная палитра применяется по prefers-color-scheme, а на ширине до 30rem панель
разворачивается на весь экран.
Сайт, который владеет внешним видом целиком, передаёт injectStyles: false и пишет свои
правила на те же классы.
Время сообщений форматируется по языку страницы (<html lang>); проп locale переопределяет
это явно.
Несовместимые изменения в 0.7.0
Имена, которые виджет оставлял в чужой странице, больше не содержат названия системы поддержки — оно описывает CaseCore, а не функцию, которую встраивает сайт:
| Было | Стало |
| -------------------------- | ------------------------ |
| mountCaseCoreChat | mountSupportChat |
| CaseCoreChat* (типы) | SupportChat* |
| .casecore-chat* (классы) | .support-chat* |
| --casecore-chat-* | --support-* |
| casecore.chat.* (ключи) | support.chat.* |
| x-casecore-thread-token | x-support-thread-token |
Совместимости со старыми именами нет намеренно. Сохранённые в браузере разговоры под старым
ключом не подхватятся: посетитель начнёт новый диалог, кейсы в CaseCore при этом остаются.
BFF должен принять новый заголовок x-support-thread-token.
