chore: import clean Agent Desk snapshot (1/7)
This commit is contained in:
349
docs/embedding.ru-RU.md
Normal file
349
docs/embedding.ru-RU.md
Normal file
@@ -0,0 +1,349 @@
|
||||
# Встраивание виджета поддержки (Интеграция)
|
||||
|
||||
Виджет онлайн-чата AgentDesk добавляет кнопку вызова поддержки и интерактивный диалог на любую веб-страницу. Он загружается одним скриптом, отображает плавающую кнопку в углу экрана и открывает интерфейс чата внутри изолированного `iframe`. Посетители могут вести несколько обращений параллельно и переключаться между ними.
|
||||
|
||||
[English](./embedding.md) | [简体中文](./embedding.zh-CN.md)
|
||||
|
||||
---
|
||||
|
||||
## Быстрый старт
|
||||
|
||||
Замените `https://support.example.com` на URL вашего развёрнутого сервера AgentDesk, а `YOUR_CHANNEL_ID` — на идентификатор канала из панели управления (**Каналы** -> ID нужного канала).
|
||||
|
||||
```html
|
||||
<script>
|
||||
window.AgentDeskConfig = {
|
||||
channelId: "YOUR_CHANNEL_ID",
|
||||
}
|
||||
</script>
|
||||
<script src="https://support.example.com/sdk/agent-desk-sdk.min.js" async></script>
|
||||
```
|
||||
|
||||
Разместите оба тега перед закрывающим тегом `</body>`. Блок конфигурации `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<string, unknown>) => void
|
||||
destroy: () => void
|
||||
open: () => Promise<void>
|
||||
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
|
||||
<script setup>
|
||||
import { onMounted, onUnmounted } from "vue"
|
||||
|
||||
const props = defineProps({
|
||||
channelId: { type: String, required: true },
|
||||
})
|
||||
|
||||
onMounted(() => {
|
||||
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: props.channelId,
|
||||
language: "ru-RU",
|
||||
getUserToken: async () => {
|
||||
const { data } = await useFetch("/api/support/user-token")
|
||||
return data.value?.token
|
||||
},
|
||||
})
|
||||
}
|
||||
document.body.appendChild(script)
|
||||
})
|
||||
|
||||
onUnmounted(() => {
|
||||
window.AgentDeskWidget?.destroy()
|
||||
})
|
||||
</script>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## Таблица всех параметров конфигурации
|
||||
|
||||
| Параметр | Тип | По умолчанию | Описание |
|
||||
| :--- | :--- | :--- | :--- |
|
||||
| `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<string>` | — | Функция получения JWT-токена пользователя. |
|
||||
|
||||
---
|
||||
|
||||
## JavaScript API виджета
|
||||
|
||||
После загрузки SDK в объекте `window.AgentDeskWidget` доступны методы управления:
|
||||
|
||||
```javascript
|
||||
// Открыть окно чата поддержки
|
||||
await AgentDeskWidget.open();
|
||||
|
||||
// Свернуть окно чата
|
||||
AgentDeskWidget.close();
|
||||
|
||||
// Смонтировать виджет с явной конфигурацией
|
||||
AgentDeskWidget.mount({ channelId: "..." });
|
||||
|
||||
// Полностью удалить кнопку и фрейм чата из DOM
|
||||
AgentDeskWidget.destroy();
|
||||
|
||||
// Получить прямую ссылку на чат (для открытия в новой вкладке или перехода)
|
||||
const chatUrl = await AgentDeskWidget.getChatUrl();
|
||||
```
|
||||
|
||||
Пример создания собственной кнопки вызова поддержки в шапке вашего сайта:
|
||||
```html
|
||||
<button type="button" onclick="AgentDeskWidget.open()">
|
||||
Написать в поддержку
|
||||
</button>
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 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`.<br>2. Проверьте секретный ключ канала `User Token Secret`.<br>3. Убедитесь, что серверное время на вашем сервере не расходится с сервером AgentDesk (срок действия подписи/токена). |
|
||||
| **Кнопка виджета не появляется** | В консоли браузера проверьте ошибку: чаще всего не указан `channelId` или скрипт SDK загрузился раньше, чем объявлен `window.AgentDeskConfig`. |
|
||||
| **Кнопка есть, но внутри окна чата белый экран** | Браузер блокирует загрузку `iframe`. Проверьте заголовки `X-Frame-Options` и `Content-Security-Policy (frame-src)` на вашем прокси-сервере. |
|
||||
| **Язык виджета не совпадает с сайтом** | Задайте параметр `language: "ru-RU"` (или `en-US`) явно в конфигурации виджета. |
|
||||
| **Дублирование виджета в SPA-приложениях** | При смене маршрута или размонтировании вызывайте `AgentDeskWidget.destroy()`. |
|
||||
Reference in New Issue
Block a user