Запрошено владельцем: форма-диалог с ИИ-ботом на сайте delritm.ru — клиент описывает потребность своими словами, ИИ объясняет что можно предложить, попутно собирает контакт как лид.

Архитектура

Клиент (браузер, delritm.ru)
  │  плавающая кнопка-баббл в углу, все страницы
  ▼
Виджет (vanilla JS/CSS, один файл, встроен в Tilda через блок «Вставка кода»)
  │  POST {session_id, message, history}
  ▼
n8n webhook  /webhook/delritm-chat   (новый отдельный workflow "delritm-chat-widget",
                                       НЕ завязан на существующую agent-harness — та сейчас
                                       в процессе редизайна, 3 конфликтующих MAX-роутера)
  │
  ├─► OmniRoute (omniroute.skud24.ru/v1) — LLM, статичный system-промпт
  │     (каталог услуг delritm.ru + инструкция естественно запросить имя+контакт)
  │     ответ — structured JSON: {reply: string, lead: {name, contact} | null}
  │
  ├─► reply → назад клиенту в виджет
  │
  └─ если lead != null:
        ├─► Twenty CRM: create_one_person (name, phone/email) [company tag: delritm.ru]
        │              + create_one_note (полный транскрипт диалога, привязан к person)
        └─► MAX Sender API (/webhook/max-sender-api) → push в рабочую группу
              «Деловой Ритм рабочий чат» (chat_id -75540425318449):
              "Новый лид с сайта: <имя>, <контакт>, суть запроса: <кратко>"

Компоненты

1. Frontend-виджет. Плавающая кнопка в углу экрана → раскрывает окно чата. Без localStorage/sessionStorage — история диалога только в памяти вкладки (session_id генерируется на клиенте при открытии). CORS webhook ограничен доменом delritm.ru.

2. Backend n8n workflow delritm-chat-widget. Webhook trigger → HTTP-запрос к OmniRoute с system-промптом + историей диалога → парсинг structured-ответа → ветвление по наличию lead.

3. System-промпт (статичный, каталог услуг из wiki delritm-ru.md): - ИП Шувалов И.А., IT-консалтинг, СПб. Услуги: IT-автоматизация (CRM, «умные таблицы», интеграции с мессенджерами/ИИ-агентами), оптимизация закупок, внешняя модерация совещаний, комплексный пакет. AI-трансформация: обработка заявок 24/7, техподдержка, ИИ-агент в рабочих чатах (Telegram/МАКС/Slack/Discord), мониторинг склада, анализ звонков, контроль рабочего времени. - Инструкция: отвечать по существу вопроса клиента, предлагать релевантную услугу, не выдумывать цены/сроки (их нет в открытых данных — предлагать «обсудим на бесплатной диагностике»), естественно спросить имя и телефон/email после того как понятен запрос клиента, не раньше и не навязчиво. - Формат ответа — JSON с полями reply/lead, чтобы backend мог детерминированно вытащить лид без regex-угадывания.

4. Twenty CRM. create_one_person + create_one_note с транскриптом. Явно помечается принадлежность к delritm.ru (в отличие от остальных данных workspace — они принадлежат проекту skud24).

5. MAX-уведомление. Переиспользует уже работающий канал (MAX Sender API, тот же, что рекламные отчёты) — push в ту же рабочую группу.

Обработка ошибок

Безопасность/приватность

Тестирование

1. Ручной прогон 3-4 тестовых диалогов с разными запросами → проверка, что лид корректно долетает до Twenty CRM (person+note) и до MAX-группы. 2. Проверка виджета на живой Tilda-странице — desktop и mobile, все текущие разделы одностраничника. 3. curl-проверка webhook напрямую на edge-кейсы: пустое сообщение, очень длинная история, отсутствие session_id. 4. Проверка, что диалог без переданного контакта не создаёт мусорных записей в CRM (lead=null не триггерит запись).

Вне рамок текущей итерации

Реализация — готово и проверено на проде (2026-09-04)

Собрано и опубликовано по дизайну выше, с отличиями по ходу сборки:

n8n workflow delritm-chat-widget (id geYWviL4b9TYTxwN, https://n8n.skud24.ru/workflow/geYWviL4b9TYTxwN, опубликован/активен). Webhook trigger → format input → AI Agent (OmniRoute, structured output через outputParserStructured) → Respond to Webhook ({reply}); параллельно AI Agent → IF lead != null → Twenty CRM (create person → create note → link note to person) → push в MAX; onError на AI Agent → fallback-ответ клиенту + пуш в MAX о сбое. 15 нод.

Две особенности SDK, не очевидные из документации, ушли на отладку: .onError(handler) вызывается на самой ноде отдельной строкой ДО композиции workflow(...), не чейнится в цепочке .add(); outputParserStructured с schemaType: 'manual' требует inputSchema строго JSON-строкой (не объектом) — иначе падение "[object Object]" is not valid JSON ещё до вызова модели. Заодно добавлены autoFix: true + отдельная нода-модель для парсера (устойчивость к чуть кривому JSON от LLM).

Фронтенд. Один файл (баббл + панель чата + fetch на вебхук, без localStorage, session_id через crypto.randomUUID() при открытии) встроен в Tilda через «Настройки сайта → Вставка кода → HTML-код для вставки внутрь HEAD» — единственная доступная зона инъекции на сайте (варианта «перед </body>» в Tilda нет), поэтому вся сборка DOM отложена до DOMContentLoaded/наличия document.body. Опубликовано на живом сайте, кнопка и диалог подтверждены визуально.

Инцидент при первом живом тесте — CORS блокировал delritm.ru, разобрано и исправлено. Прод-вебхук сначала не был опубликован (тестировался только /webhook-test/...) — исправлено publish_workflow. После этого реальный запрос с delritm.ru всё равно падал в браузере (TypeError: Failed to fetch, preflight OPTIONS проходил 204, сам POST — нет) — причина оказалась не в самом воркфлоу, а в реверс-прокси перед n8n: /opt/skud24-traefik/nginx-n8n.conf на 198.51.100.1 (см. traefik-ha) жёстко прописывал Access-Control-Allow-Origin: https://n8n-agent-admin.skud24.ru на две строки (add_header в preflight-блоке и в основном ответе /webhook/), полностью игнорируя allowedOrigins самого webhook-триггера. Исправлено через SSH-доступ n8n-агента «Senior DevOps инженер» (HGEAZyo3aNFEuy7i, credential root/*** на 198.51.100.1, тот же путь, что раньше чинил pgadmin-proxy, см. traefik-ha): в файл добавлен map $http_origin $cors_origin { default ""; "~https://n8n-agent-admin.skud24.ru" $http_origin; "~https://delritm.ru" $http_origin; } перед первым server{}, оба add_header Access-Control-Allow-Origin заменены на $cors_origin. Бэкап файла сделан до правки. nginx -t + nginx -s reload в контейнере n8n-proxy (не трогая Traefik и остальные ~25 контейнеров) — прошли успешно. Проверено: OPTIONS и реальный POST с Origin: https://delritm.ru → 200 + корректный access-control-allow-origin, calls.skud24.ru (второй домен в том же файле) не затронут (302, жив).

Twenty CRM — credential не подключён (осталось владельцу). 3 HTTP Request-ноды (Create Person In Twenty, Create Note In Twenty, Link Note To Person) настроены на httpBearerAuth/generic credential, но сам credential с токеном Twenty не создан — я не завожу секреты сам. Владельцу нужно вручную создать Bearer Auth credential в n8n UI (https://n8n.skud24.ru/workflow/geYWviL4b9TYTxwN) и привязать к этим трём нодам — до этого лиды в CRM не улетают (реплай клиенту и MAX-пуш работают уже сейчас).

Открытая заметка на будущее (не блокер, TODO). Вебхук публичный по конструкции (URL в JS фронтенда — это нормально, CORS не защита от прямых curl-вызовов, он ограничивает только браузер). Сейчас нет rate-limit и серверной проверки длины сообщения (только клиентские 2000 символов). Стоит добавить при появлении спама/аномального трафика: rate limit по IP/session на вебхуке, серверный cap длины сообщения, опционально — Header Auth как доп. слой (не как основную защиту, токен в JS всё равно виден).

Связано с (доп. к списку выше)

Доработка — активнее собирать контакты (2026-09-06)

Запрос владельца: ИИ слишком мягко относился к сбору контактов, часть диалогов уходила без лида. Изменён system-промпт ноды Delritm Chat Agent (workflow geYWviL4b9TYTxwN, activeVersionId aa34e9d3-0710-45ee-ae81-bef09c468e2d):

Логика различения строится на истории диалога, которую агент видит целиком (historyText из Format Chat Input) — отдельного state/counter не заводили, агент сам определяет по прошлым репликам, спрашивал ли он уже контакт.

Проверено прямыми curl-запросами на прод-вебхук (/webhook/delritm-chat) с 3 сценариями: (1) клиент мягко отказывается после первого запроса контакта → бот принимает отказ, не давит; (2) клиент уходит от темы (задаёт другой вопрос, игнорируя запрос контакта) → бот переспрашивает контакт во втором ответе; (3) первое сообщение сразу по делу → контакт не запрашивается преждевременно (как и раньше). Публикация (publish_workflow) выполнена сразу после правки — прод подхватил новый промпт.

Инцидент — OmniRoute 502 из-за мёртвого шлюза на хосте 198.51.100.2 (2026-09-06)

Симптом: виджет постоянно отвечал фоллбеком («не могу ответить развёрнуто, оставьте контакт») — клиент словил это 3 раза подряд в реальном диалоге, лид (номер телефона) при этом улетел в MAX сырым текстом через fallback-пуш, но не создался структурированно в Twenty (fallback-ветка лид не формирует).

Диагностика через n8n (execution-логи workflow delritm-chat-widget + временный прогон существующего workflow OmniRoute Configure Auto-Failover 198.51.100.2, id UWzmFGMfndSDuuBz, через который уже были известны SSH-креды для 198.51.100.2):

Фикс: ip route replace default via 198.51.100.4 dev eth0 на 198.51.100.2 (живой роутер, был доступен и по ARP, и по пингу). После правки: пинг 198.51.100.5 — 100% success (~65ms), curl localhost:8787/v1/models внутри контейнера — 200, живой POST на /webhook/delritm-chat — нормальный содержательный ответ.

Не решено — персистентность. Проверка /etc/network, /etc/systemd/network, /etc/netplan, /etc/dhcpcd.conf на хосте не нашла файла со старым шлюзом 198.51.100.3 — правка ip route replace применена только в текущей runtime-таблице маршрутизации и переживёт до следующей перезагрузки/переподнятия сетевого интерфейса хоста (LXC на Proxmox, судя по resolv.conf с меткой PVE), после чего маршрут, скорее всего, вернётся к .15 (источник конфигурации не найден — возможно, задаётся Proxmox-хостом контейнера снаружи, не изнутри гостя). Требуется отдельно найти, что такое 198.51.100.3 (упавший шлюз/VPN-gateway VM?) и либо поднять его обратно, либо закрепить статический маршрут на 198.51.100.4 на уровне конфигурации LXC/Proxmox.