Источник: smarty-code/smarty-backend-stable/smarty-automation/docs/ARCHITECTURE.md
Дата актуальности: 2026-06-13. Read-only документация, не меняет код.
smarty-automation — микросервис (воркер) очереди RabbitMQ, отвечающий за реактивную автоматизацию в Smarty CRM. Он потребляет события из очереди automation и выполняет побочные действия: создаёт объекты CRM, делает исходящие HTTP-запросы к внешним сервисам (webhook-и, OAuth-integration), добавляет участников в чаты, архивирует заявки по таймеру, обрабатывает hook-события диалогов.
Почему отдельный воркер, а не часть монолита:
- Автоматизации выполняют I/O-тяжёлые операции (внешние HTTP, скачивание файлов) — изоляция защищает основной API-сервер от таймаутов и retry-штормов.
- Retry-логика (reSend) и delayed-события через
queueTimer требуют долгоживущего процесса с очередью — это не подходит для stateless HTTP-обработчиков.
- Масштабирование: воркер можно масштабировать независимо от API-подов через количество консьюмеров RabbitMQ.
Ограничения:
- Нет своего HTTP-интерфейса — только консьюмер RabbitMQ.
- Зависит от
smarty-db (модели), smarty-media (файлы), smarty-system-events (dialogHook), lib/microservice (фреймворк воркера).
accessTokenCache в authorizedRequestUrl — in-memory, не переживает рестарт воркера.
| Точка |
Описание |
worker.js |
Точка запуска. Регистрирует очередь automation с картой обработчиков, вызывает ms.run(). |
events/index.js |
HandlersMap — реестр: имя события → обработчик. 6 событий. |
lib/* |
Переиспользуемые утилиты для всех обработчиков. |
models/workflow.js |
Mongoose-модель workflow (коллекция automation_workflow) — сущность «автоматизация». |
| Файл |
Имя события в очереди |
Роль |
requestUrl.js |
requestUrl |
Исходящий HTTP-запрос к внешнему сервису (webhook, API). Поддерживает JSON, form-data, multipart, скачивание файла-ответа. Retry при сетевых ошибках. |
authorizedRequestUrl.js |
authorizedRequestUrl |
HTTP-запрос с OAuth 2.0 JWT-bearer авторизацией (Google Dialogflow). Кэширует access_token in-memory. |
autoCreateObject.js |
createObject |
Создание объекта CRM (лид, сделка, задача и т.д.) с вложениями, связями, гео, markdown-полями. |
addParticipant.js |
addParticipant |
Добавление сотрудников в чат объекта CRM. |
checkRequisitionsToArchive.js |
checkRequisitionsToArchive |
Периодическая проверка заявок: архивация или перенос в спец. группу после 30 дней на финальной стадии. Сам себя переотправляет через delayed event. |
dialogHook.js (из smarty-system-events) |
dialogHook |
Выполнение hook-логики объекта CRM по токену WsObjectToken. |
| Файл |
Роль |
uploadFileFromRequest.js |
Центральный модуль исходящих запросов. Экспортирует: buildRequestOptions() — сборка опций HTTP (URL, метод, headers, body, multipart); makeExternalRequest() — исполнение (либо HTTP, либо скачивание файла); getFileFormData() — подготовка файла из БД для multipart. |
checkAttachesAvailable.js |
Проверка готовности вложений перед обработкой. Если файлы ещё скачиваются — retry до 31 попытки × 10 сек (~5 мин). Если ошибка загрузки — ServerError. |
startCheckingRequisitionsToArchive.js |
Инициализация периодической проверки заявок (закомментирован в worker.js). Хардкод wsId — техдолг. |
| Файл |
Роль |
workflow.js |
Mongoose-схема workflow (коллекция automation_workflow). Плагин AutomationObject + ObjectWithAvatar. Поле algorithm — заглушка (пустой объект). |
plugins/AutomationObject.js |
Композитный плагин: PublicInterface + CheckAccessRights + WsObject + WsMiscParams. Контроль доступа через право can_manage_workflow / админ-право manage_automation. |
Продюсер (бот/триггер)
│
├─ mq.publish('automation', 'requestUrl', { url, method, params, headers, ... })
│
▼
HandlersMap.handle(msg, acker)
│
├─ 1. Находит fn = eventsMap.get('requestUrl')
├─ 2. Вызывает fn(msg)
│ │
│ ├─ 2a. Валидация URL (validator.isURL, protocol allowlist)
│ ├─ 2b. Разбор параметров: файлы → getFileFormData(), paramsRaw → params
│ ├─ 2c. checkAttachesAvailable() — ждём готовности вложений (retry)
│ ├─ 2d. buildRequestOptions() — сборка HTTP-опций
│ ├─ 2e. makeExternalRequest() — исполнение
│ │ ├─ isFileResponse=true → downloadFile() → Attaches.create()
│ │ └─ isFileResponse=false → request(options)
│ ├─ 2f. handleResponse() → emitBgEvent.sendEvent(resultHandler.event, ...)
│ └─ 2g. catch → retryErrors? → reSend (5 попыток × 3 сек)
│ → иначе → handleResponse(error, isError=true)
│
├─ 3. acker.ack() — всегда (ошибки не nack-аются, см. TODO в HandlersMap)
└─ 4. chained? → mq.publish(next)
startCheckingRequisitionsToArchive() [инициализация]
│
└─ emitBgEvent.sendDelayedEvent(wsId, 'checkRequisitionsToArchive', ..., +10 сек, 'automation')
│
▼ (через queueTimer → RabbitMQ delayed)
checkRequisitionsToArchive(msg)
│
├─ find requisitions (не архив, _wsId)
├─ для каждой: последняя стадия? → waitTime >= 30 дней?
│ ├─ isImportant → перенос в группу 672a02c...
│ └─ иначе → inArchive=true
└─ emitBgEvent.sendDelayedEvent(..., +1 час, 'automation') ← самосcheduling
smarty-automation/
├── worker.js # Точка входа воркера
├── events/
│ ├── index.js # Реестр событий (HandlersMap)
│ ├── requestUrl.js # Исходящий HTTP
│ ├── authorizedRequestUrl.js # HTTP + OAuth JWT
│ ├── autoCreateObject.js # Создание объекта CRM
│ ├── addParticipant.js # Участники чата
│ └── checkRequisitionsToArchive.js # Архивация заявок
├── lib/
│ ├── uploadFileFromRequest.js # HTTP-клиент + multipart + скачивание файлов
│ ├── checkAttachesAvailable.js # Готовность вложений (retry)
│ └── startCheckingRequisitionsToArchive.js # Инициализация таймера
├── models/
│ ├── workflow.js # Модель автоматизации
│ └── plugins/
│ └── AutomationObject.js # Плагин доступа
└── docs/
├── ARCHITECTURE.md # Этот файл
└── API.md # Контракты событий и HTTP
- Очередь
automation — единственная. Все 6 событий идут в одну очередь. Один воркер = один консьюмер.
- Ack всегда.
HandlersMap.handle() вызывает acker.ack() и при успехе, и при ошибке. Nack закомментирован (TODO). Ошибки не возвращаются в очередь — только retry через reSend/delayed events.
- Вложения — async-download. Файлы могут быть ещё не скачаны к моменту обработки события.
checkAttachesAvailable решает эту проблему retry-циклом (до ~5 мин).
- URL-валидация — на границе.
requestUrl проверяет validator.isURL с protocols: ['http', 'https']. В production — require_tld: true (нет localhost).
- Результат → через
emitBgEvent. Обработчики не возвращают данные напрямую — публикуют событие в другую очередь (resultHandler.event/resultHandler.queue).
| # |
Проблема |
Где |
Комментарий |
| 1 |
In-memory кэш токенов |
authorizedRequestUrl.js:8 |
accessTokenCache — обычный объект. При рестарте воркера все токены теряются, первый запрос после рестарта — всегда updateToken(). Нет инвалидации по ошибке 401. |
| 2 |
Хардкод wsId и moveGroupId |
startCheckingRequisitionsToArchive.js:3, checkRequisitionsToArchive.js:8 |
ID воркспейса и группы архивации захардкожены. Не масштабируется на другие воркспейсы. |
| 3 |
startCheckingRequisitionsToArchive закомментирован |
worker.js:7 |
Периодическая проверка не стартует автоматически. Запуск — только вручную/извне. |
| 4 |
algorithm — пустая схема |
models/workflow.js:7-9 |
Поле algorithm не типизировано. Модель workflow по факту — заглушка, бизнес-логика автоматизаций — не в ней. |
| 5 |
Нет nack при ошибках |
HandlersMap.js:37 |
Ошибки проглатываются (ack + лог). Если reSend не сработал — событие потеряно. |
| 6 |
console.error в reSend |
lib/utils/reSend.js:23-27 |
Отладочный console.error в production-коде. |
| 7 |
OAuth scope захардкожен |
authorizedRequestUrl.js:19 |
scope: 'https://www.googleapis.com/auth/dialogflow' — только для Dialogflow, не универсально. |
| 8 |
isFileResponse не валидирует content-type |
uploadFileFromRequest.js:116-119 |
Если isFileResponse=true, ответ скачивается как файл без проверки, что сервер действительно вернул файл. |
| 9 |
dialogHook — из другого модуля |
events/index.js:4 |
Подключён из smarty-system-events, не из smarty-automation. Нарушает границу каталога, но логично — hook-и общие. |
| Модуль |
Использование |
lib/microservice |
Фреймворк воркера: addQueue, run. |
smarty-db |
Модели (SmartySchema, Attaches, Group, WsObjectToken, WorkspaceApi), emitBgEvent, mq. |
smarty-media |
Модель File, downloadFile, fileStreamUploadParam, reformatAttache, getFileId. |
smarty-system-events |
dialogHook (переиспользуется). |
lib/utils/reSend |
Retry-механизм через delayed events. |
lib/utils/retryErrors |
Набор сообщений ошибок для автоматического retry. |
lib/utils/request |
HTTP-клиент (обёртка). |
config |
filestore.file.container — контейнер файлов. |
form-data |
Построение multipart/form-data. |
jsonwebtoken |
JWT для OAuth (RS256). |
validator |
Валидация URL. |