# Встраивание виджета поддержки (Интеграция) Виджет онлайн-чата AgentDesk добавляет кнопку вызова поддержки и интерактивный диалог на любую веб-страницу. Он загружается одним скриптом, отображает плавающую кнопку в углу экрана и открывает интерфейс чата внутри изолированного `iframe`. Посетители могут вести несколько обращений параллельно и переключаться между ними. [English](./embedding.md) | [简体中文](./embedding.zh-CN.md) --- ## Быстрый старт Замените `https://support.example.com` на URL вашего развёрнутого сервера AgentDesk, а `YOUR_CHANNEL_ID` — на идентификатор канала из панели управления (**Каналы** -> ID нужного канала). ```html ``` Разместите оба тега перед закрывающим тегом ``. Блок конфигурации `window.AgentDeskConfig` обязательно должен выполняться **до** загрузки скрипта SDK. > [!TIP] > Заголовок, подзаголовок, основной цвет темы, позиция (слева/справа) и ширина виджета по умолчанию подтягиваются из настроек канала в панели управления, поэтому их необязательно прописывать в коде. --- ## Идентификация пользователей (Авторизация клиентов) По умолчанию любой посетитель считается **анонимным гостем** с браузерным идентификатором вида `guest_xxxxxxxx`. Чтобы связать диалог с реальным пользователем вашего сайта, передавать его имя и историю обращений, платформа требует криптографического подтверждения личности. Это защищает от спуфинга: посетитель не может просто указать чужой `userId` в браузере и прочитать чужие конфиденциальные сообщения. Поддерживаются два способа авторизации: **JWT-токен (`userToken`)** и **HMAC-подпись (`externalId`)**. --- ### Вариант 1: JWT-токен пользователя (`userToken`) — Рекомендуемый В этом варианте ваш сервер выпускает подписанный JWT-токен при авторизации пользователя на вашем сайте. #### 1. Настройка секрета в панели управления 1. Откройте панель администратора: **Каналы** -> выберите нужный канал (или создайте новый Web/Chat канал). 2. Найдите поле **User Token Secret** (секретный ключ токена пользователя) и сохраните надёжную случайную строку (например, 32+ символа). #### 2. Полезная нагрузка токена (JWT Claims) JWT подписывается алгоритмом `HS256` (также поддерживаются `HS384` и `HS512`) и должен содержать следующие поля: ```json { "userId": "12345", // Уникальный ID пользователя в вашей системе (string) "name": "Иван Иванов", // Отображаемое имя пользователя (string) "exp": 1792100000 // Время истечения срока действия (Unix timestamp в секундах) } ``` #### 3. Генерация токена на вашем бэкенде **Node.js / Express**: ```javascript import jwt from "jsonwebtoken"; app.get("/api/support/user-token", requireAuth, (req, res) => { const token = jwt.sign( { userId: String(req.user.id), name: req.user.fullName || req.user.email, }, process.env.AGENT_DESK_CHANNEL_SECRET, // User Token Secret из настроек канала { algorithm: "HS256", expiresIn: "24h", } ); res.json({ token }); }); ``` **Python (FastAPI / Flask + PyJWT)**: ```python import time import jwt @app.get("/api/support/user-token") def get_support_user_token(current_user: User = Depends(get_current_user)): payload = { "userId": str(current_user.id), "name": current_user.name, "exp": int(time.time()) + 86400, # 24 часа } token = jwt.encode(payload, os.environ["AGENT_DESK_CHANNEL_SECRET"], algorithm="HS256") return {"token": token} ``` **PHP**: ```php use Firebase\JWT\JWT; $payload = [ 'userId' => (string)$user->id, 'name' => $user->name, 'exp' => time() + 86400, ]; $token = JWT::encode($payload, getenv('AGENT_DESK_CHANNEL_SECRET'), 'HS256'); echo json_encode(['token' => $token]); ``` **Go**: ```go claims := jwt.MapClaims{ "userId": fmt.Sprintf("%d", user.ID), "name": user.Name, "exp": time.Now().Add(24 * time.Hour).Unix(), } token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims) signedToken, err := token.SignedString([]byte(channelSecret)) ``` #### 4. Передача в виджет на фронтенде Передайте функцию `getUserToken` в конфигурацию виджета. SDK будет автоматически запрашивать свежий токен при открытии окна чата: ```javascript window.AgentDeskConfig = { channelId: "YOUR_CHANNEL_ID", getUserToken: async () => { const res = await fetch("/api/support/user-token"); const data = await res.json(); return data.token; }, }; ``` --- ### Вариант 2: HMAC-SHA256 подпись (`externalId` + `signExternalId`) Если вы предпочитаете не выпускать полный JWT, можно передавать ID пользователя и подписывать его через HMAC: $$\text{signature} = \text{hex}\Big(\text{HMAC-SHA256}\big(\text{secret},\, \text{channelId} + \text{"\textbackslash n"} + \text{externalId} + \text{"\textbackslash n"} + \text{issuedAt}\big)\Big)$$ - `secret`: общий ключ `identity.secret` из файла `config.yaml` (переменная окружения `IDENTITY_HMAC_SECRET`). - `channelId`: идентификатор канала в AgentDesk. - `externalId`: ID пользователя на вашем сайте. - `issuedAt`: текущее время Unix в секундах. > [!IMPORTANT] > Подпись должна вычисляться **исключительно на вашем бэкенде**. Никогда не передавайте `identity.secret` в браузер. **Пример эндпоинта подписи (Node.js)**: ```javascript import crypto from "node:crypto"; app.get("/api/support/sign", requireAuth, (req, res) => { const issuedAt = Math.floor(Date.now() / 1000); const channelId = process.env.AGENT_DESK_CHANNEL_ID; const externalId = String(req.user.id); const payload = `${channelId}\n${externalId}\n${issuedAt}`; const signature = crypto .createHmac("sha256", process.env.AGENT_DESK_IDENTITY_SECRET) .update(payload) .digest("hex"); res.json({ signature, issuedAt }); }); ``` **Подключение в виджете**: ```javascript window.AgentDeskConfig = { channelId: "YOUR_CHANNEL_ID", externalId: currentUserId, externalName: currentUserName, signExternalId: async (id) => { const res = await fetch(`/api/support/sign?id=${encodeURIComponent(id)}`); return await res.json(); // { signature: "...", issuedAt: 171... } }, }; ``` --- ## Примеры для популярных фронтенд-фреймворков ### React / Next.js (App Router) Поскольку функции `getUserToken` или `signExternalId` нельзя передать в JSON-строке скрипта, для них используется программное монтирование через `AgentDeskWidget.mount`: ```tsx "use client" import { useEffect } from "react" declare global { interface Window { AgentDeskWidget?: { mount: (config: Record) => void destroy: () => void open: () => Promise close: () => void } } } export function SupportChatWidget({ channelId, user, }: { channelId: string user?: { id: string; name: string } | null }) { useEffect(() => { const script = document.createElement("script") script.src = "https://support.example.com/sdk/agent-desk-sdk.min.js" script.async = true script.onload = () => { window.AgentDeskWidget?.mount({ channelId, language: "ru-RU", getUserToken: user ? async () => { const res = await fetch("/api/support/user-token") const data = await res.json() return data.token } : undefined, }) } document.body.appendChild(script) return () => { window.AgentDeskWidget?.destroy() script.remove() } }, [channelId, user?.id]) return null } ``` ### Vue 3 / Nuxt 3 ```vue ``` --- ## Таблица всех параметров конфигурации | Параметр | Тип | По умолчанию | Описание | | :--- | :--- | :--- | :--- | | `channelId` | `string` | — | **Обязательный.** Идентификатор канала из панели управления. | | `baseUrl` | `string` | домен скрипта | Базовый URL сервера AgentDesk. | | `apiBaseUrl` | `string` | `baseUrl` | URL для запросов API (если API вынесен на отдельный домен). | | `widgetBaseUrl` | `string` | домен скрипта | URL, с которого загружается страница фрейма чата. | | `language` | `"ru-RU" \| "en-US" \| "zh-CN"` | язык сервера | Язык интерфейса виджета чата. | | `position` | `"left" \| "right"` | из настроек канала | Сторона экрана, где находится кнопка и всплывающее окно. | | `themeColor` | `string` | из настроек канала | Основной цвет темы (HEX, например `#2563eb`). | | `width` | `string` | `380px` | Ширина окна чата на экранах десктопа (например `400px`). | | `title` | `string` | из настроек канала | Заголовок окна поддержки в шапке. | | `subtitle` | `string` | из настроек канала | Подзаголовок окна поддержки. | | `externalId` | `string` | браузерный ID | Уникальный ID вашего пользователя. | | `externalName` | `string` | — | Имя пользователя (используется при создании профиля). | | `signExternalId` | `(id) => Promise<{signature, issuedAt}>` | — | Функция вычисления HMAC-подписи для `externalId`. | | `getUserToken` | `() => Promise` | — | Функция получения JWT-токена пользователя. | --- ## JavaScript API виджета После загрузки SDK в объекте `window.AgentDeskWidget` доступны методы управления: ```javascript // Открыть окно чата поддержки await AgentDeskWidget.open(); // Свернуть окно чата AgentDeskWidget.close(); // Смонтировать виджет с явной конфигурацией AgentDeskWidget.mount({ channelId: "..." }); // Полностью удалить кнопку и фрейм чата из DOM AgentDeskWidget.destroy(); // Получить прямую ссылку на чат (для открытия в новой вкладке или перехода) const chatUrl = await AgentDeskWidget.getChatUrl(); ``` Пример создания собственной кнопки вызова поддержки в шапке вашего сайта: ```html ``` --- ## Content Security Policy (CSP) и CORS Если на вашем сайте настроена строгая политика безопасности Content Security Policy (CSP), добавьте домен AgentDesk в разрешённые источники: ```http script-src https://support.example.com; frame-src https://support.example.com; connect-src https://support.example.com; ``` --- ## Диагностика частых проблем | Проблема | Причина и способ устранения | | :--- | :--- | | **Пользователь отображается как «Гость...»** | 1. Проверьте, что передаётся либо валидный `userToken`, либо `externalId` + `signExternalId`.
2. Проверьте секретный ключ канала `User Token Secret`.
3. Убедитесь, что серверное время на вашем сервере не расходится с сервером AgentDesk (срок действия подписи/токена). | | **Кнопка виджета не появляется** | В консоли браузера проверьте ошибку: чаще всего не указан `channelId` или скрипт SDK загрузился раньше, чем объявлен `window.AgentDeskConfig`. | | **Кнопка есть, но внутри окна чата белый экран** | Браузер блокирует загрузку `iframe`. Проверьте заголовки `X-Frame-Options` и `Content-Security-Policy (frame-src)` на вашем прокси-сервере. | | **Язык виджета не совпадает с сайтом** | Задайте параметр `language: "ru-RU"` (или `en-US`) явно в конфигурации виджета. | | **Дублирование виджета в SPA-приложениях** | При смене маршрута или размонтировании вызывайте `AgentDeskWidget.destroy()`. |