9.8 KiB
Поддержка: webhook n8n, диалог (треды), лимиты вложений
Функционал «Поддержка» реализован как диалог: треды и сообщения хранятся в БД. Таблицы с префиксом clpr_: clpr_support_threads, clpr_support_messages. Исходящие сообщения пользователя проксируются в n8n; входящие ответы оператора приходят в backend через webhook POST /api/v1/support/incoming (из n8n при ответе в CRM).
Подключение к PostgreSQL: креды берутся из .env — POSTGRES_HOST, POSTGRES_PORT, POSTGRES_DB, POSTGRES_USER, POSTGRES_PASSWORD.
Переменные окружения
В .env задаются:
| Переменная | Описание |
|---|---|
N8N_SUPPORT_WEBHOOK |
URL webhook n8n (multipart). Обязателен. |
SUPPORT_ATTACHMENTS_MAX_COUNT |
Макс. количество файлов (0 = без ограничений). |
SUPPORT_ATTACHMENTS_MAX_SIZE_MB |
Макс. размер одного файла в МБ (0 = без ограничений). |
SUPPORT_ATTACHMENTS_ALLOWED_TYPES |
Допустимые типы (пусто = любые). |
SUPPORT_INCOMING_SECRET |
Секрет для POST /api/v1/support/incoming (заголовок X-Support-Incoming-Secret или query secret). Если задан — только n8n с этим секретом может слать ответы в тред. |
Значение 0 или пустая строка для лимитов означает «без ограничений».
Формат запроса от backend к n8n
Backend отправляет на N8N_SUPPORT_WEBHOOK POST multipart/form-data:
- Поля:
message,subject,claim_id,source,unified_id,phone,email,session_id,timestamp,thread_id(UUID треда),ticket_id(если тред уже привязан к тикету в CRM). - Файлы:
attachments[0], … илиattachments.
Ответ n8n может содержать ticket_id — backend сохранит его в clpr_support_threads для последующих сообщений и для входящего webhook.
API backend
- POST /api/v1/support — multipart: message, subject?, claim_id?, source, thread_id?, session_token (или channel+channel_user_id), файлы. Создаёт/находит тред по (unified_id, claim_id), записывает сообщение (user), проксирует в n8n. Ответ:
{ "success": true, "thread_id": "...", "message_id": "..." }. - GET /api/v1/support/threads — список всех тредов пользователя. В каждом элементе есть
unread_count(число непрочитанных сообщений от поддержки). Ответ:{ "threads": [{ "thread_id", "claim_id" | null, "source", "ticket_id", "created_at", "updated_at", "last_body", "last_at", "messages_count", "unread_count" }] }. - GET /api/v1/support/unread-count — суммарное число непрочитанных по всем тредам (для бейджа в баре). Ответ:
{ "unread_count": number }. - POST /api/v1/support/read — отметить тред как прочитанный (пользователь открыл чат). Query или body:
thread_idилиclaim_id. Обновляетclpr_support_reads. - GET /api/v1/support/thread — query:
claim_id?,session_token(илиchannel+channel_user_id). Возвращает один тред и сообщения:{ "thread_id": "...", "messages": [...], "ticket_id": "..." }. Если треда нет —thread_id: null,messages: []. - POST /api/v1/support/incoming — для n8n: добавить сообщение от поддержки в тред. Тело JSON:
{ "thread_id" или "ticket_id", "body", "attachments?": [] }. ЗаголовокX-Support-Incoming-Secretили querysecretдолжен совпадать сSUPPORT_INCOMING_SECRET(если задан). Поticket_idbackend находит thread_id и вставляет сообщение с direction=support. - GET /api/v1/support/limits — лимиты вложений из env.
- GET /api/v1/support/stream — SSE: один поток на пользователя (query
session_tokenилиchannel+channel_user_id). Новые сообщения от поддержки приходят в реальном времени через Postgres NOTIFY (триггер наclpr_support_messages). События:connected,support_message(в теле —thread_id,message: id, direction, body, attachments, created_at).
Доставка в реальном времени (Postgres NOTIFY)
При INSERT в clpr_support_messages срабатывает триггер, который делает NOTIFY support_events с payload (unified_id, thread_id, сообщение). Backend при старте подписывается на канал support_events одним LISTEN-соединением и раскидывает события по реестру стримов (unified_id → очереди SSE).
Прочитано/непрочитано: таблица clpr_support_reads (unified_id, thread_id, last_read_at). Пользователь «прочитал» тред, когда открывает чат — фронт вызывает POST /read. Непрочитанные = сообщения от support с created_at > last_read_at. По этим данным можно в n8n/CRM строить сценарии напоминаний (push, повторная отправка), если пользователь долго не читает.
Миграции (таблицы с префиксом clpr_): 003 — треды и сообщения; 004 — триггер NOTIFY; 005_support_reads.sql — отметки прочтения. Применять к БД вручную. Креды Postgres — из .env:
# из корня aiform_prod, креды из .env
export $(grep -E '^POSTGRES_' .env | xargs)
psql -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f backend/db/migrations/003_support_threads_messages.sql
psql -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f backend/db/migrations/004_support_notify_trigger.sql
psql -h "$POSTGRES_HOST" -p "$POSTGRES_PORT" -U "$POSTGRES_USER" -d "$POSTGRES_DB" -f backend/db/migrations/005_support_reads.sql
Если в БД уже есть таблицы без префикса (support_threads, support_messages), их нужно переименовать в clpr_support_threads и clpr_support_messages перед применением 004, либо пересоздать схему (миграция 003 с префиксом создаёт таблицы с IF NOT EXISTS).
n8n
- Webhook приёма обращений — multipart, при первом сообщении создаёт тикет в CRM, в ответе возвращает
ticket_id. При последующих (есть thread_id/ticket_id) — добавляет комментарий к тикету. - Вызов нашего incoming — когда оператор ответил в CRM, workflow n8n должен вызвать POST https://.../api/v1/support/incoming с заголовком
X-Support-Incoming-Secret: <SUPPORT_INCOMING_SECRET>и телом{ "thread_id": "..." или "ticket_id": "...", "body": "текст ответа" }, чтобы сообщение появилось в чате мини-аппа.
Как тестировать SSE (ответы в реальном времени)
-
В мини-аппе: зайти в поддержку (бар → «Поддержка» или страница /support), авторизоваться, отправить первое сообщение (или открыть уже существующий тред). Оставить чат открытым.
-
Узнать
thread_id: в DevTools → Network найти запросGET .../api/v1/support/threadи в ответе скопироватьthread_id, либо после отправки сообщения — ответPOST .../api/v1/supportсодержитthread_id. -
Имитация ответа поддержки: вызвать incoming (как будет делать n8n):
# Подставить THREAD_ID и секрет из .env (SUPPORT_INCOMING_SECRET). Если секрет пустой — заголовок можно не передавать. curl -s -X POST 'https://miniapp.clientright.ru/api/v1/support/incoming' \ -H 'Content-Type: application/json' \ -H 'X-Support-Incoming-Secret: ВАШ_SUPPORT_INCOMING_SECRET' \ -d '{"thread_id":"THREAD_ID","body":"Тестовый ответ от поддержки"}' -
Ожидание: в открытом чате в мини-аппе в течение 1–2 секунд должно появиться новое сообщение без перезагрузки и без повторного запроса (доставка по SSE). Если сообщение появляется только после обновления страницы — проверить, что фронт пересобран с SSE (
docker compose build frontend && docker compose up -d frontend) и что в Network есть запрос к/api/v1/support/streamсо статусом pending (длинное соединение).