CW Sync: Двусторонняя интеграция Chatwoot ↔ Telegram — Техническая документация
Статус: ✅ Работает в продакшене
Дата: 05.03.2026
Версия: v9 (финальная, стабильная)
n8n Version: 2.0.3
Инструкция оператора: chatwoot-operator-guide.md
Оглавление
- Общее описание
- Полная архитектура
- Инфраструктура
- Маршрут 1: NextBot → Chatwoot
- Маршрут 2: Chatwoot → Telegram + NextBot
- Анти-петля (Anti-Loop)
- Перехват команд оператора
- Обогащение контактов
- Supabase таблицы
- Chatwoot API
- Решённые проблемы и gotchas
- Тестирование
- Мониторинг
Общее описание
Система обеспечивает полностью двустороннюю синхронизацию между Telegram-ботом (через NextBot AI-агент) и CRM Chatwoot. Операторы видят все диалоги в Chatwoot и могут вмешиваться в любой момент.
Ключевые возможности
- ✅ Все сообщения клиента из Telegram → отображаются в Chatwoot
- ✅ Все ответы AI-агента → отображаются в Chatwoot
- ✅ Оператор пишет в Chatwoot → сообщение приходит клиенту в Telegram
- ✅ AI автоматически засыпает при вмешательстве оператора
- ✅ Команды оператора (
бот продолжай,стоп) не видны клиенту - ✅ Контакты автоматически обогащаются из Telegram-профиля
- ✅ Нет дубликатов и петель обратной связи
Полная архитектура
┌──────────────────────────────────────────────────────────────────────────┐
│ │
│ МАРШРУТ 1: Telegram → Chatwoot (Workflow bCKmQFvILtXzI3Sa) │
│ │
│ Клиент (Telegram) │
│ ↓ │
│ NextBot (AI Agent) │
│ ↓ webhook: forwarded_output │
│ n8n: NextBot Webhook │
│ ↓ │
│ n8n: Process Message (Code) │
│ ├── Supabase: channel_mapping (lookup/create) │
│ ├── Telegram Bot API: getChat (обогащение контакта) │
│ └── Chatwoot API: создание контакта + разговора (если новый) │
│ ↓ │
│ n8n: IF: Should Send │
│ ↓ (skip=false) │
│ n8n: CW Action → Chatwoot API: отправка сообщения │
│ ↓ ⚡ content_attributes: { source: "n8n_sync" } │
│ n8n: Log to Supabase → sync_message_log │
│ │
│ ──────────────────────────────────────────────────────────────────────── │
│ │
│ МАРШРУТ 2: Chatwoot → Telegram (Workflow EN7OWW2XHHt07oDU) │
│ │
│ Оператор (Chatwoot UI) │
│ ↓ callback webhook │
│ n8n: Chatwoot Callback │
│ ↓ │
│ n8n: Filter & Lookup (Code) │
│ ├── SKIP если event ≠ message_created │
│ ├── SKIP если message_type ≠ outgoing │
│ ├── SKIP если sender.type = contact │
│ ├── ⚡ SKIP если content_attributes.source = "n8n_sync" (ANTI-LOOP)│
│ └── Детект команд (is_command) │
│ ↓ │
│ n8n: Is Command? (IF) │
│ ├── TRUE → NextBot Only (команда) → Log Result │
│ │ клиент НЕ видит! │
│ └── FALSE → Send to Telegram → Notify NextBot → Log Result │
│ клиент ВИДИТ, AI засыпает │
│ │
└──────────────────────────────────────────────────────────────────────────┘
Как это работает пошагово (полный цикл)
Клиент пишет → AI отвечает
- Клиент отправляет сообщение боту в Telegram
- NextBot принимает сообщение, AI-агент формирует ответ
- NextBot отвечает клиенту в Telegram (напрямую)
- NextBot отправляет webhook
forwarded_outputна n8n с событиями:client_message— сообщение клиентаagent_message— ответ AI-агента
- n8n Process Message:
- Ищет маппинг в Supabase (dialog_id → conversation_id)
- Если маппинга нет — создаёт контакт + разговор в Chatwoot
- Определяет тип сообщения (incoming/outgoing)
- n8n CW Action отправляет сообщение в Chatwoot API с маркером
content_attributes: { source: "n8n_sync" } - Chatwoot callback вызывает Workflow #2
- ANTI-LOOP: Filter & Lookup видит
source: "n8n_sync"→return []→ workflow останавливается - ✅ Результат: сообщение в Chatwoot, без дубликатов, AI продолжает работать
Оператор отвечает из Chatwoot
- Оператор набирает сообщение в Chatwoot UI и нажимает "Отправить"
- Chatwoot callback вызывает Workflow #2
- Filter & Lookup проверяет:
event = message_created✅message_type = outgoing✅sender.type ≠ contact✅content_attributes.source ≠ n8n_sync✅ (оператор, не наш бот)
- Is Command? → FALSE (обычное сообщение)
- Send to Telegram → отправляет через Telegram Bot API
sendMessage - Notify NextBot → отправляет
message_type: "output"→ AI засыпает - ✅ Результат: клиент видит ответ оператора, AI на паузе
Оператор отправляет команду
- Оператор набирает "бот продолжай" в Chatwoot UI
- Filter & Lookup определяет
is_command: true - Is Command? → TRUE
- NextBot Only → отправляет команду в NextBot webhook
- ✅ Результат: AI просыпается, клиент НЕ видит команду
Инфраструктура
| Компонент | URL / ID | Описание |
|---|---|---|
| Chatwoot | https://chatcrm.eastpay.online |
CRM для операторов |
| n8n | https://n8n.flowzzy.com |
Автоматизация workflow |
| Supabase | https://cvzsgjksswowqgfxvrsb.supabase.co |
База данных (маппинг + логи) |
| NextBot | https://app.nextbot.kz |
AI-агент платформа |
| Telegram Bot | 8385176510:AAE... |
Бот EastPay ChatAgent |
| Chatwoot Account | 3 |
EastPay |
| Chatwoot Inbox | 3 |
EastPay Telegram AI (тип: API) |
n8n Workflows
| Workflow | ID | Направление | Нод | Webhook URL |
|---|---|---|---|---|
| CW Sync: NextBot → Chatwoot | bCKmQFvILtXzI3Sa |
Telegram → Chatwoot | 5 | /webhook/nextbot-message-sync |
| CW Sync: Chatwoot → NextBot (callback) | EN7OWW2XHHt07oDU |
Chatwoot → Telegram | 7 | /webhook/chatwoot-callback |
Маршрут 1: NextBot → Chatwoot (Workflow bCKmQFvILtXzI3Sa)
Структура нод
NextBot Webhook → Process Message → IF: Should Send → CW Action → Log to Supabase
1. NextBot Webhook
| Параметр | Значение |
|---|---|
| Node ID | 4ef9415f-53bc-4dcd-9abc-d64de5194cc2 |
| Type | n8n-nodes-base.webhook |
| Method | POST |
| Path | nextbot-message-sync |
2. Process Message (Code Node)
| Параметр | Значение |
|---|---|
| Node ID | 3c887340-ae54-481e-9960-20904fad3077 |
| Type | n8n-nodes-base.code |
| typeVersion | 2 |
| Language | JavaScript |
| Version | v9 (axios + contact enrichment + anti-loop marker) |
Ключевые константы в коде:
const SB_URL = 'https://cvzsgjksswowqgfxvrsb.supabase.co';
const SB_KEY = 'eyJhbGci...'; // service_role key
const CW = 'https://chatcrm.eastpay.online';
const CW_TOKEN = 'FksZYfbqjb9RXmiEgAQqQ6Ev';
const CW_ACC = 3; // Chatwoot Account ID
const CW_INBOX = 3; // Chatwoot Inbox ID
const TG_BOT_TOKEN = '8385176510:AAE...'; // Telegram Bot Token
Обрабатываемые события NextBot:
| Событие | Chatwoot message_type | private? | Описание |
|---|---|---|---|
client_message |
incoming |
false | Сообщение клиента |
agent_message |
outgoing |
false | Ответ AI-агента |
manager_message |
outgoing |
true | Комментарий менеджера (из NextBot) |
dialog_start |
incoming |
false | Начало диалога |
send_error |
outgoing |
true | Ошибка отправки |
Логика создания контакта:
- Поиск маппинга в Supabase (
channel_mapping) - Если найден → используем существующий
conversation_id - Если НЕ найден:
- Вызов Telegram Bot API
getChatдля обогащения - Создание контакта в Chatwoot (с custom_attributes)
- Fallback: поиск существующего через
/contacts/search - Обновление контакта через
PUT /contacts/{id} - Создание разговора
- Сохранение маппинга в Supabase
- Вызов Telegram Bot API
3. IF: Should Send
Пропускает только сообщения с skip: false (отфильтровывает невалидные payload).
4. CW Action (HTTP Request)
| Параметр | Значение |
|---|---|
| Method | POST |
| URL | https://chatcrm.eastpay.online/api/v1/accounts/3/conversations/{id}/messages |
| Body | JSON (JSON.stringify) |
| Retry | 3 попытки, 2 сек интервал |
Тело запроса:
JSON.stringify({
content: $json.text,
message_type: $json.message_type,
private: $json.is_private,
content_attributes: { source: "n8n_sync" } // ⚡ ANTI-LOOP MARKER
})
⚠️ Критически важно:
content_attributes: { source: "n8n_sync" }— маркер, предотвращающий петлю обратной связи. Без него Workflow #2 воспримет ответ AI как сообщение оператора!
5. Log to Supabase
Логирует каждое отправленное сообщение в sync_message_log.
Маршрут 2: Chatwoot → Telegram + NextBot (Workflow EN7OWW2XHHt07oDU)
Структура нод
Chatwoot Callback → Filter & Lookup → Is Command?
├── TRUE → NextBot Only (command) → Log Result
└── FALSE → Send to Telegram → Notify NextBot → Log Result
1. Chatwoot Callback (Webhook)
| Параметр | Значение |
|---|---|
| Node ID | webhook-cw-callback |
| Path | chatwoot-callback |
| Webhook URL (настроен в Chatwoot) | https://n8n.flowzzy.com/webhook/chatwoot-callback |
2. Filter & Lookup (Code)
Критическая нода — фильтрует входящие webhook-и Chatwoot.
Порядок проверок:
// 1. Только event = message_created
if (body.event !== 'message_created') return [];
// 2. Только outgoing (ответы агентов/операторов)
if (msgType !== 'outgoing' && msgType !== 1) return [];
// 3. Пропускаем private-сообщения (заметки)
if (body.private === true) return [];
// 4. Пропускаем сообщения от контактов
if (body.sender?.type === 'contact') return [];
// 5. ⚡ ANTI-LOOP: Пропускаем сообщения нашего же workflow
if (contentAttrs.source === 'n8n_sync') return [];
// 6. Проверяем наличие telegram_chat_id в custom_attributes
// 7. Детектируем команды
Извлечение ID из Chatwoot payload:
const meta = body.conversation?.meta?.sender?.custom_attributes || {};
const chatId = meta.telegram_chat_id; // для Telegram sendMessage
const dialogId = meta.nextbot_dialog_id; // для NextBot webhook
3. Is Command? (IF)
Проверяет is_command === true (boolean).
4. Send to Telegram (HTTP Request)
// URL: https://api.telegram.org/bot{TOKEN}/sendMessage
JSON.stringify({
chat_id: $json.telegram_chat_id,
text: $json.content
})
5. Notify NextBot (HTTP Request)
// URL: https://app.nextbot.kz/api/webhooks/v1/{project_id}/{token}
JSON.stringify({
dialog_id: dialogId,
text: content,
message_type: "output" // ⚡ Ставит AI на паузу
})
6. NextBot Only — command (HTTP Request)
Аналогично Notify NextBot, но вызывается только для команд (AI не засыпает от /start, а просыпается).
7. Log Result
Логирует каждую операцию в sync_message_log. continueOnFail: true — не блокирует workflow при ошибке логирования.
Анти-петля (Anti-Loop)
Проблема
Без защиты возникает feedback loop (петля обратной связи):
❌ ПЕТЛЯ:
1. AI отвечает → Workflow #1 отправляет ответ в Chatwoot (outgoing)
2. Chatwoot callback → Workflow #2 ДУМАЕТ что это оператор
3. Workflow #2 → отправляет в Telegram (ДУБЛЬ!) + NextBot (AI ЗАСЫПАЕТ!)
4. Клиент видит 2 одинаковых ответа, AI на паузе без причины
Решение: Маркер content_attributes
✅ РЕШЕНИЕ:
1. Workflow #1 отправляет ответ с content_attributes: { source: "n8n_sync" }
2. Chatwoot callback → Workflow #2 видит source="n8n_sync"
3. Filter & Lookup → return [] (ПРОПУСК)
4. Workflow останавливается — никаких дубликатов и паузы!
Workflow #1 (CW Action):
{
"content": "ответ AI",
"message_type": "outgoing",
"content_attributes": { "source": "n8n_sync" }
}
Workflow #2 (Filter & Lookup):
const contentAttrs = body.content_attributes || {};
if (contentAttrs.source === 'n8n_sync') return []; // ← SKIP!
[!CAUTION] Если убрать маркер из Workflow #1, система сломается — появятся дубликаты и AI будет засыпать при каждом своём ответе. НИКОГДА не удаляйте
content_attributes: { source: "n8n_sync" }из CW Action!
Перехват команд оператора
Проблема
Когда оператор пишет "бот продолжай", это управляющая команда для NextBot, а не сообщение клиенту. Клиент не должен его видеть.
Решение: IF-ветка в callback workflow
| Команда | Действие | Клиент видит? |
|---|---|---|
продолжай |
Возобновить AI | ❌ Нет |
продолжи |
Возобновить AI | ❌ Нет |
бот продолжай |
Возобновить AI | ❌ Нет |
/start |
Возобновить AI | ❌ Нет |
стоп |
Остановить AI | ❌ Нет |
stop |
Остановить AI | ❌ Нет |
/stop |
Остановить AI | ❌ Нет |
менеджер ответит |
Остановить AI | ❌ Нет |
передаю коллеге |
Остановить AI | ❌ Нет |
Добавление новых команд
В ноде Filter & Lookup workflow EN7OWW2XHHt07oDU найдите массив:
const commandPhrases = [
'/start', '/stop', 'стоп', 'stop',
'продолжай', 'продолжи', 'бот продолжай',
'менеджер ответит', 'передаю коллеге'
];
Добавьте новую команду (lowercase) и опубликуйте workflow.
Обогащение контактов
Источник: Telegram Bot API getChat
При первом появлении нового клиента, Process Message вызывает:
GET https://api.telegram.org/bot{TOKEN}/getChat?chat_id={telegram_chat_id}
Данные для обогащения
| Поле Telegram | Поле Chatwoot | Пример |
|---|---|---|
first_name + last_name |
name |
Sergei Modiazhenov |
username |
custom_attributes.telegram_username |
@modyazhenov |
| — (вычисляемое) | custom_attributes.telegram_link |
https://t.me/modyazhenov |
bio |
custom_attributes.telegram_bio |
StaffAI — AI для бизнеса |
chat_id |
custom_attributes.telegram_chat_id |
69691085 |
| dialog_id (NextBot) | custom_attributes.nextbot_dialog_id |
11523024 |
Custom Attribute Definitions
[!IMPORTANT] Custom attributes НЕ отображаются в Chatwoot UI без предварительного определения. Определения создаются ОДИН РАЗ через API:
POST /api/v1/accounts/3/custom_attribute_definitions
Созданные определения:
| Key | Display Name | Тип |
|---|---|---|
telegram_chat_id |
Telegram Chat ID | number |
nextbot_dialog_id |
NextBot Dialog ID | number |
telegram_username |
Telegram Username | text |
telegram_link |
Telegram Link | link |
telegram_bio |
Telegram Bio | text |
source |
Source | text |
messenger |
Messenger | text |
Supabase таблицы
channel_mapping
Связь NextBot dialog_id ↔ Chatwoot conversation_id.
| Колонка | Тип | Описание |
|---|---|---|
id |
uuid | PK |
telegram_chat_id |
bigint | Telegram chat ID |
nextbot_dialog_id |
integer | NextBot dialog ID |
nextbot_project_id |
text | 276a96dd-9dea-41f6-8828-3666a84d85c1 |
chatwoot_contact_id |
integer | Chatwoot Contact ID |
chatwoot_conversation_id |
integer | Chatwoot Conversation ID |
chatwoot_source_id |
text | tg_{chat_id}_{dialog_id} |
user_name |
text | Имя пользователя |
telegram_username |
text | @username |
is_active |
boolean | Активен ли маппинг |
created_at |
timestamp | Дата создания |
sync_message_log
Лог всех синхронизированных сообщений.
| Колонка | Тип | Описание |
|---|---|---|
id |
uuid | PK |
message_id |
text | Уникальный ID сообщения |
source |
text | nextbot или chatwoot_agent |
direction |
text | nb_to_cw или chatwoot_to_telegram |
content_preview |
text | Первые 100-200 символов текста |
message_type |
text | incoming / outgoing / command |
status |
text | sent / success |
created_at |
timestamp | Дата создания |
Chatwoot API
Базовые параметры
| Параметр | Значение |
|---|---|
| Base URL | https://chatcrm.eastpay.online |
| Account ID | 3 |
| API Token | FksZYfbqjb9RXmiEgAQqQ6Ev |
| Inbox ID | 3 (тип: API) |
Используемые endpoints
| Endpoint | Метод | Назначение |
|---|---|---|
/api/v1/accounts/3/contacts |
POST | Создание контакта |
/api/v1/accounts/3/contacts/{id} |
PUT | Обновление контакта |
/api/v1/accounts/3/contacts/search?q= |
GET | Поиск контакта |
/api/v1/accounts/3/conversations |
POST | Создание разговора |
/api/v1/accounts/3/conversations/{id}/messages |
POST | Отправка сообщения |
/api/v1/accounts/3/custom_attribute_definitions |
POST | Определение custom attribute |
NextBot Webhook API
| Параметр | Значение |
|---|---|
| URL | https://app.nextbot.kz/api/webhooks/v1/{project_id}/{token} |
| Project ID | 276a96dd-9dea-41f6-8828-3666a84d85c1 |
| Token | 41a58518d5a44d66bb656df141ad0963 |
Формат запроса к NextBot:
{
"dialog_id": 11436784,
"text": "текст сообщения",
"message_type": "output"
}
message_type: "output"— ключевой параметр, который ставит AI на паузу и сохраняет контекст диалога.
Telegram Bot API
| Параметр | Значение |
|---|---|
| Bot Token | 8385176510:AAEF6IjfCVH1e75pVkcy751Kbs2RgdDpxwk |
| sendMessage | POST https://api.telegram.org/bot{TOKEN}/sendMessage |
| getChat | GET https://api.telegram.org/bot{TOKEN}/getChat?chat_id={id} |
Решённые проблемы и gotchas
🔴 Проблема 1: $helpers is not defined
Причина: n8n task runner sandbox не предоставляет $helpers в Code node.
Решение: Используем const axios = require('axios') (с NODE_FUNCTION_ALLOW_EXTERNAL=axios в docker-compose).
🔴 Проблема 2: Task Runner memory crash (fetch/axios)
Причина: В callback workflow код с axios в Code node вызывал crash task runner.
Решение: Весь HTTP взаимодействие в callback workflow перенесено в нативные HTTP Request ноды.
🔴 Проблема 3: message_type — строка или число?
Причина: Chatwoot callback иногда присылает message_type: "outgoing" (строка), иногда 1 (число).
Решение: Проверяем оба: if (msgType !== 'outgoing' && msgType !== 1) return [];
🔴 Проблема 4: Неверные столбцы sync_message_log
Причина: JSON body содержал chatwoot_conversation_id, которого нет в таблице.
Решение: Исправлен payload на существующие столбцы.
🔴 Проблема 5: Команды видны клиенту
Причина: Все outgoing-сообщения отправлялись в Telegram, включая "бот продолжай".
Решение: IF-нода Is Command? с массивом commandPhrases + отдельная ветка NextBot Only.
🔴 Проблема 6: Контакты без обогащения
Причина: Контакт создавался только с user_name из NextBot (часто неполное имя).
Решение: Telegram Bot API getChat → first_name, last_name, username, bio.
🔴 Проблема 7: Custom attributes не видны в Chatwoot UI
Причина: Chatwoot требует предварительного определения custom attribute через API настроек. Решение: Созданы 7 custom_attribute_definitions через POST API.
🔴 Проблема 8: Feedback Loop (КРИТИЧЕСКАЯ)
Причина: AI отвечает → Workflow #1 → Chatwoot (outgoing) → Callback → Workflow #2 думает оператор → дубль в Telegram + AI на паузе!
Решение: Маркер content_attributes: { source: "n8n_sync" } в Workflow #1, проверка if (contentAttrs.source === 'n8n_sync') return [] в Workflow #2.
🟡 Gotcha: Порядок publicации
При обновлении workflow через API, они сохраняются, но НЕ публикуются автоматически. Нужно:
- Открыть workflow в n8n UI
- Нажать "Publish" / "Опубликовать" (или активировать toggle)
🟡 Gotcha: JSON.stringify для body
Используем JSON.stringify() для body в HTTP Request нодах (не keypair). Это решает проблему с:
- Newlines в тексте (экранирование
\n) - Кириллицей
- Вложенными объектами (
content_attributes)
🟡 Gotcha: continueOnFail
Ноды логирования (Log to Supabase, Log Result) имеют continueOnFail: true. Если Supabase логирование упадёт — основной workflow не сломается.
Тестирование
Тест 1: Новый клиент из Telegram
- Написать боту с нового Telegram-аккаунта
- Проверить в Chatwoot: появился контакт с полным именем и custom attributes
- Проверить: сообщение отображается как incoming
- Проверить: ответ AI отображается как outgoing
- Проверить: НЕТ дубликатов сообщений AI в Telegram
Тест 2: Ответ оператора
- Написать сообщение клиенту из Chatwoot
- Проверить: клиент получил в Telegram
- Проверить: AI поставился на паузу (в NextBot → "Dialog paused")
Тест 3: Команда оператора
- Написать "бот продолжай" из Chatwoot
- Проверить: клиент НЕ получил сообщение в Telegram
- Проверить: AI возобновился (в NextBot → "Dialog resumed")
Тест 4: Отсутствие петли
- Дождаться ответа AI
- Проверить в n8n исполнения callback workflow:
Filter & Lookup: output=0(заблокировано маркером)- Duration < 50ms (workflow мгновенно остановился)
Быстрая проверка через curl
# Отправить сообщение от оператора
curl -X POST \
"https://chatcrm.eastpay.online/api/v1/accounts/3/conversations/3/messages" \
-H "api_access_token: FksZYfbqjb9RXmiEgAQqQ6Ev" \
-H "Content-Type: application/json" \
-d '{"content": "тест", "message_type": "outgoing"}'
Мониторинг
n8n Executions
- Workflow
bCKmQFvILtXzI3Sa— в среднем 200-500ms на execution - Workflow
EN7OWW2XHHt07oDU— 15-50ms для заблокированных (anti-loop), 2-3s для реальных
Ключевые метрики
| Метрика | Нормально | Проблема |
|---|---|---|
| Callback duration для AI-ответов | < 50ms | > 1s (anti-loop не работает!) |
| Filter & Lookup output для AI-ответов | 0 | 1 (петля!) |
| Дубликаты в Telegram | 0 | > 0 (петля!) |
| Паузы AI без вмешательства | 0 | > 0 (петля!) |
Supabase запросы для мониторинга
-- Последние 20 синхронизированных сообщений
SELECT * FROM sync_message_log ORDER BY created_at DESC LIMIT 20;
-- Активные маппинги
SELECT * FROM channel_mapping WHERE is_active = true;
-- Кол-во сообщений за час
SELECT COUNT(*) FROM sync_message_log
WHERE created_at > NOW() - INTERVAL '1 hour';
Связанные документы
| Документ | Описание |
|---|---|
| chatwoot-operator-guide.md | Инструкция для операторов |
| chatwoot-integration-plan.md | Исходный план интеграции |
| nextbot-chatwoot-scenarios.md | Сценарии взаимодействия |
Последнее обновление: 05.03.2026, 19:57 UTC+7 — v9, финальная стабильная версия