Интеграция виджета
Встраиваемый виджет - это отдельный сервис (packages/widget, порт 3211), который публикует AI-чат на твоём сайте. Архитектура: Express + Preact IIFE в sandboxed iframe, прозрачный SSE-прокси к серверу, анонимные сессии, lead capture, branding для каждого…
Встраиваемый виджет - это отдельный сервис (packages/widget, порт 3211), который публикует AI-чат на твоём сайте. Архитектура: Express + Preact IIFE в sandboxed iframe, прозрачный SSE-прокси к серверу, анонимные сессии, lead capture, branding для каждого сайта.
Виджет - это Enterprise-функция (license widget_enabled); в Community по умолчанию 1 виджет, но для создания требуется активный флаг.Как встроить (quick start)
- Создай виджет (admin UI → Settings → Widgets, или API):
POST /api/widgets
Authorization: Bearer <admin JWT>
{ "name": "Sito Azienda", "allowedOrigins": ["https://www.azienda.it"],
"workspaceIds": ["<uuid>"], "primaryColor": "#4c6ef5", "botName": "Assistente" }Ответ: { widget } с widget.id.
- Whitelist базы знаний -
PUT /api/widgets/:id/workspacesс UUID тех workspace, чей RAG будет доступен виджету (таблицаWidgetWorkspace, составной PK[widgetId, workspaceId]).
- Вставь snippet в host-страницу:
<div id="simmetric-widget" data-widget-id="<widget.id>" data-position="bottom-right"></div>
<script
src="https://widget.tuodominio.it/widget/<widget.id>.js"
integrity="sha384-<HASH>"
crossorigin="anonymous"
async
></script>Безопасность: если отдаёшь loader с стороннего CDN, сгенерируй SHA-384 hash бандла и добавьintegrity+crossorigin="anonymous"(Subresource Integrity). Если loader same-origin с твоим widget service, SRI опционален, но рекомендован для защиты от компрометации CDN/asset.
Loader (packages/widget/src/routes/loader.ts) читает атрибуты data-*, создаёт <iframe sandbox="allow-scripts allow-forms"> (без allow-same-origin, без allow-top-navigation), загружает /widget/<id> (HTML), который монтирует Preact-бандл /widget/app.js.
Конфигурация branding и trigger
Поля модели Widget (валидируются widget.schema.ts в shared):
| Поле | По умолчанию | Описание |
|---|---|---|
primaryColor | #4c6ef5 | Акцентный цвет (hex regex) |
botName | AI Assistant | Имя бота |
logoUrl / avatarUrl | - | URL изображения (http/https) |
welcomeMessage / fallbackMessage | - | Приветственное/fallback-сообщение (per-locale тексты через localizedTexts) |
position | bottom-right | bottom-right/bottom-left |
allowedOrigins | - | Массив URL (JSON, максимум 2000 символов) для динамического CORS |
autoOpenDelay | - | Секунды авто-открытия (null = disabled, 1–300) |
autoOpenUrlPatterns | - | Glob patterns (**,*,?,{a,b}) для авто-открытия по URL |
exitIntentEnabled | false | Открытие по exit-intent |
exitIntentCooldownMs | 1800000 | 60000–86400000 ms |
leadCaptureEnabled | false | Lead capture |
leadCapturePrompt | - | Lead prompt (максимум 500) |
rateLimitPerMinute / sessionLimitPerDay | - | Per-widget override лимитов (null = глобальный) |
responseProviderId / responseModel | - | Response model pin (v0.22, см. ниже) |
suggestedQuestions / credits | - | Per-locale подсказки (максимум 10) / кредиты виджета |
URL query param (?primaryColor=...&position=...&locale=...) переопределяет на лету. Дополнительные поля v0.22: rateLimitPerMinute / sessionLimitPerDay (per-widget override глобальных лимитов; null = глобальный дефолт) и responseProviderId/responseModel.
Response model pin (v0.22): admin-конфигурация каждого виджета может выбрать provider + LLM-модель, обслуживающую ответы этого виджета (responseProviderId, UUID провайдера; responseModel, имя модели). Pin разрешается на стороне сервера из строки виджета в DB при каждом чат-запросе (никогда не предоставляется клиентом - proxy и посетительский UI не затронуты), имеет приоритет над дефолтом workspace, а виджеты без pin сохраняют существующую цепочку разрешения workspace/глобальный. Чаты виджета записывают фактически обслужившую модель.
Trigger (widget/hooks/useTriggers.ts): слушает postMessage от loader - simmetric:urlChange (проверка autoOpenUrlPatterns через matchUrlPattern/globToRegex: **, *, ?, {a,b}), simmetric:exitIntent (открытие с cooldown). Exit intent детектируется на стороне loader через mouseleave с e.clientY <= 10. autoOpenDelay - в секундах (1–300, null = отключено); exitIntentCooldownMs 60000–86400000 ms.
Аутентификация и API key
- Widget ↔ Server: header
X-Api-Key: <WIDGET_API_KEY>к/api/internal/widget/*. Key должен совпадать междуpackages/widget/.envиpackages/server/.env. - Посетитель ↔ Widget service: header
x-session-token(анонимная сессия, не JWT). - Динамический CORS (
widgetCors.ts): per-origin CORS headers на основеallowedOrigins. Fail-closed при ошибке DB (нет header = браузер блокирует).
Анонимные сессии
POST /api/sessions (widget) → сервер создаёт WidgetSession: 256-bit hex token (crypto.randomBytes(32)), expiry 24h, IP логируется. sessionMiddleware валидирует token при каждом чат-запросе и прикрепляет req.widgetSession + req.widgetConfig. Сессия сохраняется в sessionStorage хоста через postMessage-handshake с loader (simmetric:storage-*) и переиспользуется при reload; по истечении → 401 → Preact показывает "Session expired", и при обновлении страницы виджет создаёт сессию заново.
Поток чата (SSE proxy)
Visitatore → POST /api/chat/:widgetId/stream (x-session-token, body {message, chatId?})
→ widgetDailyMessageLimiter (per widget+IP, 5/24h prod, override sessionLimitPerDay)
→ widgetChatLimiter (per widgetId, 30/min prod, override rateLimitPerMinute)
→ sessionMiddleware valida sessione
→ check hourlyRemaining (DB-tracked, 20/h) → 429 se esaurito
→ increment messageCount (fire-and-forget)
→ pre-search RAG: POST /api/internal/widget/search {query, widgetId}
server risolve workspaceIds dalla whitelist WidgetWorkspace (IDOR prevention)
→ proxy a SERVER_URL/api/internal/widget/chat/stream
headers: X-Api-Key, X-Widget-Id, X-Widget-Session-Id, X-Accel-Buffering: no
→ relay byte SSE verbatim (responseType: "stream", filtra event: thinking)
→ Preact useWidgetChat parse: token | citations | done | errorProxy прозрачен: не парсит/фильтрует/трансформирует SSE-события. Если RAG pre-search падает, он emits status: rag-degraded, но чат продолжается без контекста.
Lead capture
POST /api/lead/:widgetId (widget, sessionMiddleware):
- Проверка
config.leadCaptureEnabled(403 если выключено). - Body
widgetLeadSubmitSchema:email(обяз.),name(опц.),transcript[](role/content/timestamp). submitLead(widgetId, email, name, transcript, session.id)- передаётsession.id(UUID FK).- Limiter 3 leads/hour на IP.
- Admin получает:
GET /api/widgets/:id/leads, CSV-экспортGET /api/widgets/:id/leads/export(с фильтром по дате + выбором колонок; с v0.22 всегда доступен - commodity-флагlead_exportудалён).
Rate limit (двойной слой)
| Limiter | Scope | Лимит (prod) | Window |
|---|---|---|---|
| widgetDailyMessageLimiter | per widget+IP | 5/day (override sessionLimitPerDay) | 24h |
| widgetChatLimiter | per widgetId | 30/min (override rateLimitPerMinute) | 1 min |
| widgetSessionLimiter | per IP | 50/day (создания сессий) | 24h |
| widgetLeadLimiter | per IP | 3/hour | 1h |
| server hourlyRemaining | per session | 20/h | 1h |
| server dailyRemaining | per session | 5/24h | 24h |
Безопасность
- iframe sandbox
allow-scripts allow-forms(без same-origin, без top-navigation). helmetослаблен для embedding (без CSP, без frameguard, без COOP/COEP/CORP).- Сообщения рендерятся через
markdown-it+dompurify(anti-XSS). - Серверный DLP: DLP-совпадения каждого прогона путешествуют в SSE
done(dlp_matches, только при активном DLP и без bypass-роли); согласие на PII посетителя управляетсяPIIWarningPrompt(pre-chat gate, текстыpiiConsentпо locale). - Без cookie, без localStorage из sandboxed iframe (storage через
postMessage-handshake с loader, batching на done/error/unmount). - Loader использует
getAttribute()(неinnerHTML) +encodeURIComponent; входящие сообщенияsimmetric:*валидируются guard'омevent.source === iframeEl.contentWindow. - Endpoint cache-bust (
POST /api/config/:widgetId/cache-bust) сравниваетX-Api-Keyв постоянном времени (crypto.timingSafeEqualс length guard, fail-closed при unset).
MCP (косвенно)
Виджет не управляет MCP: установка/unpin происходит в server admin. Связанные workspace могут иметь установленные MCP tool → агент использует их во время чата виджета автоматически (ноль кода виджета). mcpSources в SSE done показывает, какие MCP внесли вклад.
Расширение
- Новый компонент →
packages/widget/src/widget/components/(Preact + Tailwind, не shadcn/ui). - Новый trigger → расширь
useTriggers.ts+ полеWidget+ routeinternalWidget.ts. - Новый rate limit →
src/middleware/rateLimit.ts+ mount вindex.ts. - Темизируемый CSS → namespace
--widget-*вsrc/widget/index.css; статические стили через Tailwind utility.
Ключевые файлы
| Файл | Роль |
|---|---|
packages/widget/src/index.ts | createApp() factory, mount routes |
src/routes/loader.ts | Loader JS + HTML iframe |
src/routes/chat.ts | SSE proxy + pre-search RAG |
src/routes/session.ts | Анонимная сессия |
src/routes/config.ts | Config (cache 5 min) |
src/routes/lead.ts | Lead capture |
src/middleware/session.ts | sessionMiddleware |
src/widget/App.tsx | Preact root, hook coord |
src/widget/hooks/{useWidgetChat,useWidgetConfig,useTriggers}.ts | State |
Cross-link
- Endpoints виджета: 04 - API Reference
- Внутренности RAG/agent: 05 - Feature Guide