105 lines
5.3 KiB
Markdown
105 lines
5.3 KiB
Markdown
# ADR-0001: Разделение Chat Platform на 3 сервиса
|
||
|
||
Дата: 2026-02-21
|
||
Статус: accepted
|
||
|
||
## Контекст
|
||
|
||
Сейчас delivery уже вынесен отдельно, но часть omni-интеграции остается в приложении `frontend`.
|
||
Нужна архитектура, где входящие вебхуки, доменная логика чатов и исходящая доставка развиваются независимо и не ломают друг друга.
|
||
|
||
Критичные требования:
|
||
|
||
- входящие webhook-события не теряются при рестартах;
|
||
- delivery управляет retry/rate-limit централизованно;
|
||
- omni_chat остается единственным местом доменной логики и хранения состояния диалогов;
|
||
- сервисы можно обновлять независимо.
|
||
|
||
## Решение
|
||
|
||
Принимаем разделение на 3 сервиса:
|
||
|
||
1. `omni_inbound`
|
||
- Принимает вебхуки провайдеров.
|
||
- Валидирует подпись/секрет.
|
||
- Нормализует событие в универсальный envelope.
|
||
- Пишет событие в durable queue (`receiver.flow`) с идемпотентным `jobId`.
|
||
- Возвращает `200` только после успешной durable enqueue.
|
||
- Не содержит бизнес-логики CRM.
|
||
|
||
2. `omni_chat`
|
||
- Потребляет входящие события из `receiver.flow`.
|
||
- Разрешает идентичности и треды.
|
||
- Создает/обновляет `OmniMessage`, `OmniThread`, статусы и доменные эффекты.
|
||
- Формирует исходящие команды и кладет их в `sender.flow`.
|
||
|
||
3. `omni_outbound`
|
||
- Потребляет `sender.flow`.
|
||
- Выполняет отправку в провайдеров (Telegram Business и др.).
|
||
- Управляет retry/backoff/failover, DLQ и статусами доставки.
|
||
- Не содержит UI и доменной логики чатов.
|
||
|
||
## Почему webhook и delivery разделены
|
||
|
||
- Входящий контур должен отвечать быстро и предсказуемо.
|
||
- Исходящий контур живет с долгими retry и ограничениями провайдера.
|
||
- Сбой внешнего API не должен блокировать прием входящих сообщений.
|
||
|
||
## Границы ответственности
|
||
|
||
`omni_inbound`:
|
||
|
||
- можно: auth, валидация, нормализация, дедуп, enqueue;
|
||
- нельзя: запись доменных сущностей CRM, принятие продуктовых решений.
|
||
|
||
`omni_chat`:
|
||
|
||
- можно: вся доменная модель чатов, 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`.
|
||
|
||
## Последствия
|
||
|
||
Плюсы:
|
||
|
||
- независимые релизы и масштабирование по ролям;
|
||
- меньше blast radius при инцидентах;
|
||
- проще подключать новые каналы поверх общего контракта.
|
||
|
||
Минусы:
|
||
|
||
- больше инфраструктурных компонентов (очереди, мониторинг, трассировка);
|
||
- требуется дисциплина по контрактам между сервисами.
|
||
|
||
## План внедрения
|
||
|
||
1. Вводим `omni_inbound` как отдельный сервис для Telegram Business.
|
||
2. Потребление `receiver.flow` реализуем в `omni_chat`.
|
||
3. Текущее исходящее API оставляем за `omni_outbound`.
|
||
4. После стабилизации выносим оставшиеся omni endpoint'ы из `frontend` в `omni_chat`/`omni_inbound`.
|