Skip to content

Веб-встраивание (Embed Channel) ​

Канал встраивания служит для размещения виджета вопросов по базе знаний на официальном сайте или в справочном центре. После создания канала и привязки агента добавьте сгенерированный скрипт на страницу — посетители смогут общаться без учётной записи aihelpme.ru. Область поиска и модель для посетителя полностью определяются агентом, привязанным к каналу: сервер отбрасывает базы знаний, документы, теги, @упоминания, навыки и переопределения модели из запроса.

Создайте канал в разделе «Настройки → Веб-встраивание», привяжите агента и задайте разрешённые домены, затем скопируйте код подключения. Перед публичным использованием настройте белый список доменов и ограничение частоты, чтобы ограничить источники доступа и объём запросов.

Канал поддерживает подключение со статическим token и в безопасном режиме, а также приём событий диалога через Webhook.

Изображения на стороне посетителя проходят через прокси авторизации уровня канала, а не основного сайта; если изображения не отображаются, см. Доступ к изображениям и файлам извне.

Подключение фронтенд-виджета ​

SDK виджета — это не имеющий зависимостей скрипт-загрузчик frontend/public/weknora-widget.js (после развёртывания раздаётся из корневого пути сервиса), который отрисовывает плавающую кнопку + панель iframe; iframe указывает на SPA страницы встраивания /embed/{channel_id} (точка входа frontend/src/embed-main.ts).

Способ 1: статический режим Token (token виден посетителю) ​

html
<script
  src="https://your-weknora.example.com/weknora-widget.js"
  data-channel="UUID_вашего_канала"
  data-token="em_ваш_publish_token"
  data-position="bottom-right"
  data-primary-color="#07C05F"
  data-title="AI Assistant"
></script>

publish token записывается прямо в HTML страницы и виден любому посетителю; при ротации token нужно синхронно обновить все страницы развёртывания. Подходит для внутренних сайтов или сценариев с низкой чувствительностью.

Способ 2: безопасный режим (Secure Mode, рекомендуется) ​

publish token хранится только в бизнес-бэкенде, а страница через data-token-endpoint указывает на эндпоинт обмена этого бэкенда:

html
<script
  src="https://your-weknora.example.com/weknora-widget.js"
  data-channel="UUID_вашего_канала"
  data-token-endpoint="https://your-backend.example.com/weknora/embed-token"
  data-position="bottom-right"
></script>

Бизнес-бэкенд реализует этот endpoint: сервер хранит token em_, вызывает POST /api/v1/embed/{channel_id}/exchange, чтобы получить короткоживущий token ems_, и возвращает { "token": "ems_...", "expiresIn": 1800 }. Виджет автоматически обновляет token примерно при 80% TTL (не раньше чем через 30 секунд) (см. scheduleRefresh в weknora-widget.js). publish token никогда не попадает в браузер.

Эндпоинт обмена должен сначала проверить действительность бизнес-сессии/JWT и право гостя на доступ; одной лишь проверки наличия Cookie или заголовка Authorization недостаточно для аутентификации. При вызове exchange на сервере нужно вручную отправлять бизнес-хост Origin, совпадающий с белым списком канала, например Origin: https://shop.example.com; не раскрывайте административный Token платформы посетителям.

Прочие необязательные атрибуты: data-base-url (по умолчанию выводится из src скрипта), data-width / data-height (размеры панели, по умолчанию 400×600), data-sandbox (политика sandbox для iframe; при кросс-доменном встраивании автоматически добавляется allow-scripts allow-forms allow-popups allow-modals allow-same-origin).

Способ 3: программный API ​

html
<script src="https://your-weknora.example.com/weknora-widget.js"></script>
<script>
  WeKnora.init({
    channel: 'UUID_канала',
    tokenEndpoint: 'https://your-backend.example.com/weknora/embed-token', // или token: 'em_...'
    position: 'bottom-right',
    primaryColor: '#07C05F',
    title: 'AI Assistant',
    baseUrl: 'https://your-weknora.example.com',
  });
  WeKnora.setContext({ userId: 'u_123', page: location.pathname }); // контекст подставляется при каждом вопросе
  WeKnora.setLocale('en-US');
  WeKnora.openWithQuery('Как сбросить пароль?');   // открыть панель и автоматически отправить вопрос
  WeKnora.on('ready', () => console.log('widget ready'));
  // прочее: WeKnora.open() / close() / toggle() / destroy() / off(event, fn)
</script>

Прямое подключение через iframe ​

Можно обойтись без загрузчика и встроить iframe напрямую (в этом случае token нужно передать через URL/postMessage; обычно рекомендуется использовать загрузчик):

html
<iframe src="https://your-weknora.example.com/embed/UUID_канала"
        width="400" height="600" style="border:none"></iframe>

Протокол postMessage Bridge ​

Хост-страница (загрузчик) и страница встраивания внутри iframe общаются через postMessage, обе стороны строго проверяют Origin (загрузчик отправляет сообщения только в выведенный embedOrigin и никогда не использует *; страница встраивания фиксирует origin по первому доверенному сообщению — см. frontend/src/composables/useEmbedBridge.ts):

  • хост → iframe (source: "weknora-host"): provide_token (передача token), set_context, set_locale, open_with_query;
  • iframe → хост (source: "weknora-embed"): ready, bootstrap_request (запрос token), message_sent, message_received.

Авторизация и анонимные сессии ​

Два вида Token ​

TokenПрефиксСрок жизниНазначение
Publish Tokenem_длительный (до ротации)Токен публикации канала; можно встроить прямо в страницу (статический режим) или хранить только в бизнес-бэкенде (безопасный режим)
Session Tokenems_30 минут (Redis TTL)Короткоживущий токен, получаемый из publish token через /exchange, используется на стороне браузера

Все публичные интерфейсы передают токен через заголовок Authorization: Embed <token> (query string не принимается). Промежуточное ПО EmbedAuth (internal/middleware/embed_auth.go) выполняет по порядку:

  1. Находит канал по channel_id, проверяет совпадение token с publish_token либо находит в Redis (ключ embed:session:{token}) session token, принадлежащий этому каналу;
  2. Проверяет enabled канала;
  3. Разрешает same-origin API-запросы внутри iframe; кросс-доменные API-запросы и Origin серверного exchange в безопасном режиме должны попадать в allowed_origins. Пустой белый список по-прежнему отклоняет всё; ограничения хоста обеспечивает CSP встроенного HTML;
  4. Ограничивает частоту (Lua-скрипт Redis, скользящее окно):
  • один IP не более RateLimitPerMinute в минуту;
  • весь канал не более max(RateLimitPerMinute × 20, 120) в минуту — защита от атакующего, обходящего лимит на один IP сменой IP;
  • суммарно по каналу за день не более RateLimitPerDay.

Источник хоста и развёртывание ​

Когда сайт A встраивает экземпляр с сайта B, в белый список заносится A. Стандартный Nginx получает политику канала через /api/v1/embed-frame-policy (без token, возвращает только CSP, без конфигурации канала) и устанавливает frame-ancestors в HTML-ответе /embed/:channelId; Lite использует ту же политику. Эта страница не кешируется, а при неудачном получении политики встроенный HTML не возвращается.

При обновлении каналы, где раньше указывался только B, нужно перевести на фактический хост A и обновить одновременно фронтенд и бэкенд. Пользовательский обратный прокси должен сохранять CSP, исходный Host (с портом), протокол и Sec-Fetch-Site. Белый список ограничивает встраивание в браузере, но не заменяет аутентификацию посетителя и не блокирует не-браузерных клиентов с token; для такого контроля доступа используйте безопасный режим и ограничение частоты.

Отдельный поддомен (необязательно) ​

По умолчанию достаточно общий домен для страницы embed и панели управления. Когда нужен отдельный вход или изоляция Cookie, можно разместить страницу embed на https://embed.example.com, оставив панель управления на https://app.example.com, а бизнес-хостом сделать https://shop.example.com.

Панель управления задаёт origin embed через window.__RUNTIME_CONFIG__.EMBED_BASE_URL в frontend/public/config.js или через переменную времени сборки VITE_EMBED_BASE_URL; если оставить пустым, используется origin текущей страницы. После изменения убедитесь, что сгенерированный код Widget/iframe указывает на новый адрес.

Отдельный server Nginx раздаёт только /embed/*, /weknora-widget.js, /assets/* и необходимые прокси до бэкенда, не отдавая index.html панели управления. Сохраните в стандартном frontend/nginx.conf для /embed/ директиву auth_request, внутренний /_embed-frame-policy и заголовки ответа CSP: при неудачном получении политики страницу возвращать нельзя, а шлюз/CDN не должен кешировать или отбрасывать эту политику.

В белый список по-прежнему заносится фактический бизнес-хост https://shop.example.com, а exchange в безопасном режиме объявляет тот же Origin; origin embed не нужно дополнительно добавлять для обычных same-origin запросов чата. Widget автоматически добавляет sandbox для iframe, когда хост и embed находятся на разных origin. При проверке открывайте страницу с фактического хоста и смотрите на iframe, API, CSP и сгенерированный код — недостаточно проверить только в режиме предпросмотра панели управления.

Обмен Token (ядро безопасного режима) ​

POST /api/v1/embed/:channel_id/exchange, заголовок Authorization: Embed em_xxx (принимается только publish token, session token отклоняется). Ответ:

json
{ "success": true, "data": { "session_token": "ems_...", "expires_in": 1800 } }

Реализация — IssueSessionToken в internal/application/service/embed_session.go: 32 случайных байта base64 с префиксом ems_, запись в Redis, TTL 30 минут.

Создание анонимной сессии ​

POST /api/v1/embed/:channel_id/sessions создаёт сессию чата и возвращает:

json
{ "success": true, "data": { "id": "<session_uuid>", "sig": "<HMAC-SHA256 base64>" } }
  • Сессия записывается в таблицу sessions, Description помечается как embed_channel:{channel_id}, а UserID использует непрозрачный идентификатор посетителя, сгенерированный EmbedSessionPrincipal(tenantID, channelID, sessionID).StorageID();
  • sig — это подпись сессии: HMAC-SHA256(channel.PublishToken, "{channel_id}|{session_id}"). В дальнейшем каждое обращение к /sessions/:session_id/* должно нести заголовок X-Embed-Session: <sig>, а сервер выполняет сравнение за константное время (internal/handler/embed_channel.go). Это защищает от выдачи чужой сессии только по session_id; после ротации publish token все подписи одновременно становятся недействительными.

Фронтенд также может передавать X-Embed-Visitor: <uuid> для статистики на уровне посетителя. id сессии и sig кешируются по каналу в localStorage, поэтому после обновления страницы сессия сразу восстанавливается (frontend/src/composables/useEmbedBridge.ts).

Обратные вызовы Webhook ​

Каналы с настроенным webhook_url при следующих событиях отправляют JSON методом POST в бизнес-бэкенд (internal/application/service/embed_webhook.go):

СобытиеМомент срабатыванияПоля нагрузки
message_sentПосетитель задал вопросtype, channel_id, session_id, timestamp, query
message_receivedОтвет ассистента завершёнtype, channel_id, session_id, timestamp, content

Семантика безопасности и доставки:

  • при настроенном webhook_secret добавляется заголовок подписи X-WeKnora-Signature: sha256=<hex(HMAC-SHA256(secret, raw_body))>;
  • URL должен быть HTTPS, исходящий запрос идёт через SSRF-безопасный клиент (при каждом редиректе проверка заново, не более 5 переходов), таймаут 5 секунд, User-Agent — WeKnora-Embed-Webhook/1.0;
  • асинхронная доставка best-effort: при сбое только запись в лог, без повторов;
  • фронтенд также может явно пересылать события через POST /api/v1/embed/:channel_id/sessions/:session_id/events.

Справочник по конфигурации и интерфейсам ​

Модель данных ​

EmbedChannel в internal/types/embed_channel.go — полное определение канала (таблица embed_channels, мягкое удаление, частично уникальный индекс по publish_token):

go
type EmbedChannel struct {
    ID                     string         // первичный ключ UUID
    TenantID               uint64         // тенант-владелец
    AgentID                string         // привязанный Agent (по умолчанию builtin-quick-answer)
    Name                   string         // имя канала
    Enabled                bool           // включён ли
    PublishToken           string         // долгоживущий токен публикации, префикс "em_"
    AllowedOrigins         JSON           // список разрешённых Origin (JSONB)
    WelcomeMessage         string         // приветствие
    RateLimitPerMinute     int            // лимит на один IP в минуту (по умолчанию 30)
    RateLimitPerDay        int            // суточный лимит канала (по умолчанию 10000)
    PrimaryColor           string         // цвет темы
    PageTitle              string         // заголовок страницы
    HeaderTitleMode        string         // "channel" | "session"
    ShowSuggestedQuestions bool           // показывать предлагаемые вопросы
    WidgetPosition         string         // положение виджета
    AllowWebSearch         bool           // разрешить веб-поиск
    AllowFileUpload        bool           // разрешить загрузку файлов/изображений
    DefaultLocale          string         // язык по умолчанию
    WebhookURL             string         // исходящий webhook (HTTPS)
    WebhookSecret          string         // ключ подписи HMAC-SHA256
    ...
}

Параметры конфигурации канала ​

При создании/обновлении канала (embedChannelRequest в internal/handler/embed_channel.go) можно настроить:

ПараметрТипЗначение по умолчаниюОписание
namestring—Отображаемое имя канала
enabledbooltrueПереключатель канала; после выключения все публичные интерфейсы отклоняют доступ
agent_idstringbuiltin-quick-answerПривязанный Agent, определяет область базы знаний и возможности диалога
allowed_originsstring[]—Обязательно указать хотя бы один элемент — хост-сайт встраивания A, а не адрес B. Поддерживаются три формы: полный http(s):// Origin (без пути и query-параметров), подстановка поддомена *.example.com (совпадает только с поддоменами, не с самим example.com; при отсутствии порта совпадает с любым портом), полная подстановка * (при GIN_MODE=release отклоняется при сохранении, только для разработки)
welcome_messagestringпустоПриветствие при открытии виджета
rate_limit_per_minuteint30Лимит запросов на один IP в минуту
rate_limit_per_dayint10000Суточный лимит запросов канала
primary_colorstring—Цвет темы виджета (значение цвета CSS, например #0052d9)
page_titlestringпустоЗаголовок браузера страницы встраивания
header_title_modestringchannelРежим заголовка: channel (фиксированное имя канала) / session (генерируется автоматически по сессии)
show_suggested_questionsbooltrueПоказывать ли предлагаемые вопросы
widget_positionstringbottom-rightbottom-right | bottom-left | top-right | top-left
allow_web_searchboolfalseРазрешён ли веб-поиск на стороне посетителя
allow_file_uploadboolfalseРазрешена ли загрузка изображений/файлов на стороне посетителя
default_localestringпусто (следует за браузером)zh-CN | en-US | ko-KR | ja-JP | ru-RU
webhook_urlstringпустоАдрес обратного вызова событий, обязательно HTTPS и проходит проверку SSRF (внутренние/link-local адреса запрещены)
webhook_secretstringпустоКлюч подписи webhook (никогда не возвращается в ответе API)

Когда изображения включены и в канале, и в привязанном Агенте, посетитель может вставить скриншот в поле ввода чата, отправить его после предпросмотра или удалить из предпросмотра. Вставка и выбор файла используют общие ограничения: JPEG, PNG, GIF, WebP, не более 10 МБ на изображение и не более 5 изображений за раз. Вставка обычного текста сохраняет прежнее поведение; если в буфере обмена одновременно есть изображение и текст, добавляется только изображение — как и в основном чате.

API управления (требуется аутентификация) ​

Регистрируется через RegisterEmbedChannelRoutes (internal/router/routes_agent.go), поддерживает возможность API Key ManageChannels:

МетодПутьПраваОписание
POST/api/v1/agents/:id/embed-channelsAdminСоздать канал для Agent
GET/api/v1/agents/:id/embed-channelsViewerСписок каналов некоторого Agent
GET/api/v1/embed-channelsViewerСписок всех каналов тенанта
GET/api/v1/embed-channels/:channel_idViewerДетали канала (включая publish_token)
PUT/api/v1/embed-channels/:channel_idAdminОбновить конфигурацию канала
DELETE/api/v1/embed-channels/:channel_idAdminУдалить канал (мягкое удаление)
POST/api/v1/embed-channels/:channel_id/rotate-tokenAdminРотация publish_token (старый token и все выданные подписи сессий сразу становятся недействительными)
POST/api/v1/embed-channels/:channel_id/preview-sessionViewerВыдать короткоживущий session token для предпросмотра (предпросмотр виджета в консоли)
GET/api/v1/embed-channels/:channel_id/statsViewerСтатистика сессий канала

Публичный API (анонимный доступ, авторизация Embed) ​

Регистрируется через RegisterEmbedPublicRoutes под префиксом /api/v1/embed/:channel_id, все проходят через промежуточное ПО middleware.EmbedAuth (проверка token + проверка Origin + ограничение частоты):

go
embed := r.Group("/api/v1/embed/:channel_id", middleware.EmbedAuth(embedService, tenantService, redisClient))
{
    embed.POST("/exchange", embedHandler.ExchangeEmbedSession)
    embed.GET("/config", embedHandler.GetEmbedConfig)
    embed.GET("/suggested-questions", embedHandler.GetEmbedSuggestedQuestions)
    embed.GET("/chunks/:chunk_id", embedHandler.GetEmbedChunk)
    embed.POST("/sessions", embedHandler.CreateEmbedSession)
    embed.POST("/knowledge-chat/:session_id", embedHandler.EmbedKnowledgeChat)
    embed.POST("/agent-chat/:session_id", embedHandler.EmbedAgentChat)
    embed.GET("/messages/:session_id/load", embedHandler.EmbedLoadMessages)
    embed.POST("/sessions/:session_id/stop", embedHandler.EmbedStopSession)
    embed.POST("/sessions/:session_id/events", embedHandler.EmbedRelayWebhookEvent)
    // маршруты для предлагаемых вопросов к сообщениям, MCP OAuth, подтверждения инструментов, файлового сервиса и т. п. опущены
    embed.GET("/files", newFileServeHandler(...))
}

Выдача публичной конфигурации ​

GET /api/v1/embed/:channel_id/config возвращает EmbedChannelPublicConfig (internal/types/embed_channel.go) — только информацию об отображении и возможностях, нужную для отрисовки виджета:

  • Выдаётся: channel_id, name, display_title (сервер разбирает по порядку PageTitle → Name → AgentName → "AI Assistant"), agent_id/agent_name/agent_avatar, knowledge_base_ids, welcome_message, primary_color, header_title_mode, show_suggested_questions, widget_position, allow_web_search, allow_file_upload, agent_web_search_enabled, agent_image_upload_enabled, default_locale и т. д.;
  • Никогда не выдаётся: publish_token, webhook_url, webhook_secret.

Сквозная последовательность ​

Краткая сводка по безопасности ​

  • Белый список хостов: встроенный HTML задаёт канальный CSP frame-ancestors, ограничивая все родительские страницы; same-origin API внутри iframe пропускается как обычно. Пустой allowed_origins отклоняет все запросы; * доступен только в режиме разработки; поддерживается подстановка поддомена *.example.com. Same-origin предпросмотр в консоли управления разрешён.
  • Двухтокенная схема: в безопасном режиме publish token не покидает сервер, браузер держит только короткоживущий 30-минутный token ems_.
  • Подпись сессии: HMAC-подпись X-Embed-Session привязывает сессию к тройке (канал, сессия, текущий publish token); ротация token позволяет отозвать всё сразу.
  • Трёхуровневое ограничение частоты: на IP/минуту, на канал/минуту (20× на IP, минимум 120), на канал/день, атомарно реализовано на Lua в Redis.
  • Защита Webhook от SSRF: только HTTPS, внутренние адреса отклоняются, редиректы проверяются пошагово, таймаут 5 секунд.

Справочник по реализации ​

Все пути ниже указаны относительно корня репозитория:

СлойФайл
Структуры данныхinternal/types/embed_channel.go
HTTP Handlerinternal/handler/embed_channel.go
Сервис каналовinternal/application/service/embed_channel.go
Анонимные сессии/Tokeninternal/application/service/embed_session.go
Рассылка Webhookinternal/application/service/embed_webhook.go
Промежуточное ПО авторизацииinternal/middleware/embed_auth.go
Регистрация маршрутовinternal/router/routes_agent.go (RegisterEmbedPublicRoutes / RegisterEmbedChannelRoutes / политика CSP страницы встраивания)
Правила источника хостаinternal/embedpolicy/origin.go
Загрузчик виджета (SDK)frontend/public/weknora-widget.js
Точка входа SPA страницы встраиванияfrontend/src/embed-main.ts, frontend/src/composables/useEmbedBridge.ts, useEmbedChatSession.ts
Миграции БДmigrations/versioned/000060_embed_channels.up.sql

Версия v0.8.3 · MIT License