П/ВИН

Чат бот воронка: аудит и пересборка на v2

·9 мин чтения

Есть особый жанр технического долга: код, который вроде бы работает, но по факту давно умер. Вебхуки прилетают, процесс висит в systemd, дашборд открывается, метрики зелёные - а внутри тишина: ни одного отправленного сообщения, ни одного проведённого через воронку клиента. Именно в такое состояние тихо сползла моя чат бот воронка, и когда я это наконец раскопал, стало ясно - чинить там уже нечего, надо строить заново.

Всё началось с простой просьбы: «У нас есть блок автоматизации, давай сделаем его аудит — что работает, а что застряло на каком-то затыке». Классическая ситуация: никто не помнит, когда в последний раз оттуда что-то реально уходило клиенту, но выключать страшно — а вдруг там что-то крутится. Так что первым делом надо было честно посмотреть не на код, а на живое состояние системы.

Что вообще за блок автоматизации

Контекст: это часть SaaS-продукта для автоматизации соцсетей (внутреннее имя — content-farm). Блок автоматизации отвечал за сценарии вроде Comment→DM (человек пишет ключевое слово в комментариях — бот отвечает в личку), воронки прогрева, массовую публикацию, управление подключёнными аккаунтами и приём вебхуков от Instagram. Всё это жило поверх Postgres-схемы content_farm плюс отдельный воркер-сервис проверки подписки (follower-check) и набор крон-роутов.

Звучит внушительно. На бумаге — полноценный движок. Проблема в том, что бумага и продакшн разошлись.

Аудит: смотрим на живое состояние, а не на отчёты

Главный принцип аудита, которого я держался: не верить коду и не верить дашборду. Код описывает намерение, а не факт. Дашборд показывает то, что ему велели показывать. Правду знают только три источника — таблицы в БД с реальными timestamp'ами, логи воркеров и статусы кронов.

Я прошёлся по всем слоям параллельно: код эндпоинтов, содержимое ключевых таблиц (webhook_logs, аккаунты, воронки, interactions), состояние systemd-сервиса follower-check и планировщик задач. Картина сложилась быстро и неприятно:

Что формально живо:

  • Приём вебхуков Instagram — работает. Эндпоинт отвечает, события пишутся в webhook_logs, последняя запись — буквально вчера. Но если присмотреться к содержимому, это входящие DM: люди шарят рилсы в директ. Ни одно из событий не совпадает с ключевым словом, на которое должна реагировать автоматизация. То есть канал открыт, но по нему не приходит ничего осмысленного для движка.
  • Сервис follower-check — процесс жив, systemd показывает active (running), аптайм в неделях. Вот только пул задач на проверку пустой. Сервис исправно просыпается, смотрит на пустую очередь и засыпает обратно. Идеальный образ мёртвой автоматизации: максимальный аптайм при нулевой полезной работе.

Что мёртво:

  • Ответы на вебхуки упираются в протухшие OAuth-токены. Токены Instagram/Facebook живут ограниченное время и требуют рефреша. Механизма авто-рефреша не было, токены истекли ещё в начале апреля — и с тех пор даже если бы пришло правильное ключевое слово, ответить система бы не смогла.
  • Кроны, которые должны двигать клиентов по воронкам, просто не заведены. Не «сломались» — их physically нет в планировщике.

Вывод аудита звучал так: блок автоматизации фактически заморожен с начала апреля. Вебхуки приходят, но всё, что должно на них отвечать, упирается в протухшие токены, пустой пул проверки подписки и незаведённые кроны. Это не баг, который чинится одной строчкой. Это подсистема, которая тихо остановилась на нескольких независимых затыках сразу.

Ключевой урок аудита: аптайм ≠ работа

Самый важный вывод здесь методологический. systemd status = active (running) не значит ничего про полезную работу. Процесс может годами крутиться в холостом цикле «проснулся → очередь пуста → спать». Зелёная галочка мониторинга усыпляет бдительность сильнее, чем красная тревога, — потому что красную ты идёшь чинить, а зелёную игнорируешь.

Поэтому правильный аудит меряет не «жив ли процесс», а «когда система в последний раз произвела бизнес-результат». Для автоматизации это: когда ушло последнее сообщение клиенту, когда кто-то в последний раз прошёл воронку до конца, когда токен последний раз успешно рефрешнулся. Если ответ на любой из этих вопросов — «в апреле», значит система мертва независимо от цвета индикаторов.

Развилка: чинить или строить заново

Когда стало ясно, что затыков несколько и они архитектурные (нет авто-рефреша токенов, нет кронов, старые таблицы — это вообще мартовские тесты), встал вопрос: латать или пересобирать?

Пользователь сформулировал новую реальность так: «Надо проектировать автоматизацию последовательно заново под реалии июля 2026. Самое важное — воронка. Причём на уровне соцсетей (TikTok, Instagram, Facebook, YouTube) и мессенджеров (Telegram, MAX, там где боты), но БД клиентов — одна. И круто бы иметь кнопку перенести БД клиентов из другого агрегатора ботов — там люди уже писали боту, значит бот может продолжить с ними общение».

Это уже не «почини старое». Это запрос на новую архитектуру. Так и родился Automation v2.

Архитектура Automation v2

Мы разложили модуль на четыре слоя поверх единой БД клиентов:

Каналы (adapters)      Движок              UI
──────────────────     ────────────        ──────────────
IG DM  ─ webhook ─┐     inbound_events      Воронки
Telegram ─────────┼──▶  funnel_runs    ──▶  Клиенты (CRM)
MAX ──────────────┤     outbox             Инбокс
Facebook / YouTube┘     scheduler           Рассылки · Каналы · Импорт

Центральная сущность — единая запись клиента (contacts): один человек, один UUID, а к нему прицеплены все его identity на разных платформах, кастомные поля (fields jsonb), теги и источник (какая воронка или импорт его привёл). Один клиент может писать вам и в Telegram, и в Instagram — для системы это одна карточка, а не два разных лида.

Старые таблицы (social_accounts, funnels, interactions) не мигрируем — там только мартовские тесты, дропнем после запуска v2. Это принципиальное решение: тащить мёртвые данные в новую схему — значит тащить и причины, по которым старое встало.

Что взяли из open source, а что нет

Отдельно проверили, что можно переиспользовать. Вывод: готовую платформу целиком не берём (все они навязывают свой дизайн и свою модель данных), но набираем проверенные MIT/Apache-кирпичи.

Главный герой здесь — grammY, современный фреймворк для Telegram-ботов на TypeScript. Он закрывает вебхуки, inline-кнопки, throttling под лимит 30 сообщений/сек — то есть весь низкоуровневый транспорт мессенджера бесплатно и надёжно. Telegram-токен, в отличие от Instagram, вечный, что снимает главную боль старой системы.

А вот от чего отказались — показательно. Изначально в дизайн заложили React Flow (@xyflow/react) для канвас-конструктора воронок: ноды, стрелки, drag-n-drop, зум, миникарта. Мощный движок графа из коробки. Но пользователь остановил: «Не уверен, что хочу канвас — мне нравится, как воронки выглядят визуально сейчас».

И это оказалось не упрощением ради лени, а правильным архитектурным разделением. Решение: в UI воронка остаётся структурным списком шагов (карточки сообщений с кнопками и задержками, в нашем монохромном стиле), но в данных мы всё равно храним графnodes + edges. Кнопка шага ведёт на другой шаг, условие — на ветку. В интерфейсе это селект «куда ведёт → Шаг N / другая воронка / стоп». Если когда-нибудь захочется канвас — данные уже готовы, надо только дорисовать рендер.

Вот этот принцип — храни данные как граф, показывай как удобно пользователю — один из главных выводов проектирования. Модель данных не должна быть заложницей текущего UI.

Каналы как тонкие адаптеры

Каждый канал — модуль с единым интерфейсом: sendMessage, parseWebhook, healthCheck, refreshToken?. Именно refreshToken — прямой ответ на главный затык аудита. Теперь авто-рефреш токенов не забытая деталь, а часть контракта канала.

  • Telegram и MAX — полные воронки. Подключение = вставить токен, вебхук ставим сами через setWebhook. Токен вечный, deeplink t.me/<bot>?start=<token> — точка склейки и триггер входа bot_start.
  • Instagram и Facebook — воронки с учётом 24-часового окна (Messenger Platform policy) и авто-рефрешем токенов через Instagram Graph API.

Импорт базы: та самая «кнопка перенести»

Отдельный киллер-сценарий — визард импорта из другого агрегатора ботов (Salebot, BotHelp). Ключевое техническое ограничение, которое честно объясняем в UI: писать импортированным людям может только тот же бот (тот же токен), которому они писали в агрегаторе. Инструкция прямо в интерфейсе: «отключи бота в старом сервисе → вставь токен сюда» — наш setWebhook перетирает их вебхук, и общение продолжается с теми же людьми без повторного согласия.

Синк с CRM клиента

Ещё одно решение — двусторонняя синхронизация с внешней CRM (в первом кейсе — Twenty). Причём именно синк, а не замена нашей БД: операционные данные мессенджинга (identity платформ, 24h-окна, funnel_runs, outbox) — это не CRM-сущности, их некуда класть в Twenty. Зато обратная синхронизация через вебхуки Twenty даёт killer-фичу сразу: стадия сделки, менеджер и сумма зеркалятся в fields.crm.* контакта и становятся доступны в условиях воронок и сегментах рассылок.

Как реализуем: субагенты и двойное ревью

Сборку ведём не одним заходом, а через subagent-driven development: свежий субагент на каждую задачу, после него — два независимых ревью (сначала соответствие спеке, потом качество кода), и только потом следующая задача.

Первая задача — миграция схемы (10 таблиц, гранты). Показательно, что ревью качества, хоть и одобрило миграцию, тут же дало две дешёвые правки, которые важно закрыть пока таблицы пустые: уникальный индекс против дублей активных funnel_runs и явный контракт soft-delete для каналов. На пустых таблицах это одна строчка DDL; на заполненных — уже миграция с чисткой данных и рисками. Это ровно тот класс решений, который старая система провалила: не заложили инвариант заранее — получили дубли и мусор потом.

Выводы

Первый урок — про сам факт аудита. Мёртвая автоматизация не сообщает о своей смерти. Она не падает, не шлёт алертов, не краснеет в мониторинге. Она тихо переходит в режим «процесс жив, работы ноль» и может провести так месяцы. Единственная защита — периодически мерить не аптайм, а последний бизнес-результат: когда ушло последнее сообщение, когда кто-то прошёл воронку. Если ответ измеряется в месяцах — у вас труп с зелёной галочкой.

Второй урок — про множественность затыков. Наша система встала не по одной причине, а по трём независимым сразу: протухшие токены без авто-рефреша, пустой пул задач и незаведённые кроны. Это типично для сложных пайплайнов: они умирают не от одного удара, а от накопления мелких недоделок, каждая из которых поодиночке кажется несрочной. Поэтому чинить точечно бессмысленно — надо смотреть на систему целиком и решать, не пора ли пересобрать.

Третий урок — про разделение данных и представления. Отказавшись от канваса, но сохранив граф в данных, мы получили лучшее из двух миров: простой знакомый UI сегодня и возможность нарастить сложность завтра без миграции. Модель данных должна отражать суть домена (воронка — это граф), а не текущую прихоть интерфейса. Это решение стоило одной реплики пользователя, но определило всю схему.

Четвёртый урок — про инварианты, которые дёшевы только сейчас. Уникальный индекс от дублей, контракт soft-delete, механизм рефреша токенов как часть контракта канала — всё это копейки на этапе пустой схемы и дорогая боль на заполненной. Дисциплина «заложи инвариант, пока таблица пустая» — прямое следствие вскрытия старой системы, где ровно эти вещи и не были заложены. Аудит одной системы стал спецификацией для следующей — и это, пожалуй, самый ценный результат всей работы.

Паша Вин
Паша Вин

AI-инженер, предприниматель, маркетолог. Основатель feberra.com и x10seo.ru. 13 лет в перфоманс-маркетинге, 3 года в системной интеграции AI в бизнес.