refactor chat delivery to graphql + hatchet services

This commit is contained in:
Ruslan Bakiev
2026-03-08 18:55:58 +07:00
parent fe4bd59248
commit 7d1bed0d67
61 changed files with 5007 additions and 5004 deletions

View File

@@ -1,104 +1,93 @@
# ADR-0001: Разделение Chat Platform на 3 сервиса
# ADR-0001: Chat Platform Boundaries (GraphQL + Hatchet)
Дата: 2026-02-21
Дата: 2026-03-08
Статус: accepted
## Контекст
Сейчас delivery уже вынесен отдельно, но часть omni-интеграции остается в приложении `frontend`.
Нужна архитектура, где входящие вебхуки, доменная логика чатов и исходящая доставка развиваются независимо и не ломают друг друга.
Нужна минимальная и предсказуемая схема из 5 сервисов:
Критичные требования:
- `frontend`
- `backend`
- `telegram_backend`
- `telegram_worker`
- `hatchet`
- входящие webhook-события не теряются при рестартах;
- delivery управляет retry/rate-limit централизованно;
- omni_chat остается единственным местом доменной логики и хранения состояния диалогов;
- сервисы можно обновлять независимо.
Ключевые ограничения:
- основная Prisma/доменная БД только в `backend`;
- `telegram_backend` и `telegram_worker` не содержат CRM-домен и не пишут в основную БД;
- взаимодействие между сервисами только через GraphQL;
- асинхронность и ретраи централизованы в Hatchet.
## Решение
Принимаем разделение на 3 сервиса:
Принимаем архитектуру:
1. `omni_inbound`
- Принимает вебхуки провайдеров.
- Валидирует подпись/секрет.
- Нормализует событие в универсальный envelope.
- Пишет событие в durable queue (`receiver.flow`) с идемпотентным `jobId`.
- Возвращает `200` только после успешной durable enqueue.
- Не содержит бизнес-логики CRM.
1. `backend`
- владеет доменной моделью чатов и единственной основной Prisma-базой;
- принимает inbound события от `telegram_worker` через GraphQL (`ingestTelegramInbound`);
- создает outbound задачи в `telegram_backend` через GraphQL (`requestTelegramOutbound`);
- принимает delivery-отчеты от `telegram_worker` через GraphQL (`reportTelegramOutbound`).
2. `omni_chat`
- Потребляет входящие события из `receiver.flow`.
- Разрешает идентичности и треды.
- Создает/обновляет `OmniMessage`, `OmniThread`, статусы и доменные эффекты.
- Формирует исходящие команды и кладет их в `sender.flow`.
2. `telegram_backend`
- принимает webhook Telegram;
- нормализует payload в `OmniInboundEnvelopeV1`;
- ставит задачи в Hatchet (`process-telegram-inbound`, `process-telegram-outbound`);
- предоставляет GraphQL API для enqueue и отправки в Telegram API.
3. `omni_outbound`
- Потребляет `sender.flow`.
- Выполняет отправку в провайдеров (Telegram Business и др.).
- Управляет retry/backoff/failover, DLQ и статусами доставки.
- Не содержит UI и доменной логики чатов.
3. `telegram_worker`
- исполняет задачи Hatchet;
- для inbound вызывает `backend /graphql`;
- для outbound вызывает `telegram_backend /graphql` (`sendTelegramMessage`), затем `backend /graphql` (`reportTelegramOutbound`);
- не имеет собственной Prisma-базы.
## Почему webhook и delivery разделены
4. `hatchet`
- единый оркестратор задач, ретраев и backoff-политик.
- Входящий контур должен отвечать быстро и предсказуемо.
- Исходящий контур живет с долгими retry и ограничениями провайдера.
- Сбой внешнего API не должен блокировать прием входящих сообщений.
## Потоки
### Inbound (Telegram -> CRM)
1. Telegram webhook приходит в `telegram_backend`.
2. `telegram_backend` нормализует событие и enqueue в Hatchet `process-telegram-inbound`.
3. `telegram_worker` исполняет задачу и вызывает `backend.ingestTelegramInbound`.
4. `backend` сохраняет доменные изменения в своей БД.
### Outbound (CRM -> Telegram)
1. `backend` инициирует отправку (`requestTelegramOutbound`) в `telegram_backend`.
2. `telegram_backend` enqueue в Hatchet `process-telegram-outbound`.
3. `telegram_worker` вызывает `telegram_backend.sendTelegramMessage`.
4. `telegram_worker` репортит итог в `backend.reportTelegramOutbound`.
## Границы ответственности
`omni_inbound`:
`backend`:
- можно: вся бизнес-логика и состояние;
- нельзя: прямой вызов Telegram API.
- можно: auth, валидация, нормализация, дедуп, enqueue;
- нельзя: запись доменных сущностей CRM, принятие продуктовых решений.
`telegram_backend`:
- можно: webhook ingress, нормализация, enqueue, адаптер Telegram API;
- нельзя: доменные записи CRM.
`omni_chat`:
`telegram_worker`:
- можно: исполнение задач, ретраи, orchestration шагов;
- нельзя: хранение CRM-состояния и прямой доступ к основной БД.
- можно: вся доменная модель чатов, orchestration, бизнес-правила;
- нельзя: прямые вызовы провайдеров из sync API-контекста.
## Надежность
`omni_outbound`:
- можно: провайдерные адаптеры, retry, rate limits;
- нельзя: резолвинг бизнес-правил и маршрутизации диалога.
## Универсальный протокол событий
Внутренний контракт входящих событий: `docs/contracts/omni-inbound-envelope.v1.json`.
Обязательные поля:
- `version`
- `idempotencyKey`
- `provider`, `channel`, `direction`
- `providerEventId`, `providerMessageId`
- `eventType`, `occurredAt`, `receivedAt`
- `payloadRaw`, `payloadNormalized`
## Идемпотентность и надежность
- `jobId` в очереди строится из `idempotencyKey`.
- Дубликаты входящих webhook событий безопасны и возвращают `200`.
- `200` от `omni_inbound` отдается только после успешного добавления в Redis/BullMQ.
- При ошибке durable enqueue `omni_inbound` возвращает `5xx`, провайдер выполняет повторную доставку.
- Базовые рабочие очереди: `receiver.flow` и `sender.flow`; технические очереди для эскалации: `receiver.retry`, `sender.retry`, `receiver.dlq`, `sender.dlq`.
- webhook отвечает `200` только после успешной постановки задачи в Hatchet;
- при недоступности сервисов задача ретраится Hatchet;
- inbound обработка идемпотентна через `idempotencyKey` и provider identifiers в `backend`.
## Последствия
Плюсы:
- независимые релизы и масштабирование по ролям;
- меньше blast radius при инцидентах;
- проще подключать новые каналы поверх общего контракта.
- меньше сервисов и меньше скрытых связей;
- изоляция доменной БД в `backend`;
- единая точка ретраев/оркестрации (Hatchet).
Минусы:
- больше инфраструктурных компонентов (очереди, мониторинг, трассировка);
- требуется дисциплина по контрактам между сервисами.
## План внедрения
1. Вводим `omni_inbound` как отдельный сервис для Telegram Business.
2. Потребление `receiver.flow` реализуем в `omni_chat`.
3. Текущее исходящее API оставляем за `omni_outbound`.
4. После стабилизации выносим оставшиеся omni endpoint'ы из `frontend` в `omni_chat`/`omni_inbound`.
- выше требования к стабильности GraphQL-контрактов между сервисами;
- нужна наблюдаемость по цепочке `telegram_backend -> hatchet -> telegram_worker -> backend`.