Дата анализа: 2026-06-20
Анализ проведён по кодовой базе smarty-backend-stable.
Архитектура Smarty строится на нескольких зрелых паттернах: жёсткая workspace-изоляция через _wsId на уровне каждой модели, plugin-ориентированное расширение Mongoose-схем, event-driven микросервисный слой поверх RabbitMQ с единым HandlersMap, и продуманная агентная петля для AI-ботов с planner/executor/guards. Статическая типизация отсутствует, но компенсируется обширной runtime-валидацией, audit-логированием и многослойной авторизацией. Кодовая база демонстрирует последовательное применение принципа "изоляция через конвенцию" — _wsId пронизывает каждый слой от БД до tool-call, что делает cross-tenant утечки маловероятными при штатном использовании.
Что реализовано: Каждая доменная модель подключает плагин WsId (smarty-db/models/plugins/WsId.js), который добавляет поле _wsId (ObjectId, required) и автоматически инжектит его из профиля при create/update/read через modifyIncomeData. Плагин WsObject (smarty-db/models/plugins/WsObject.js) композитно объединяет EmployeeObject + WsId и эмиттит фоновые события custom_workspace_events с привязкой к _wsId. Модуль getAccountFromRequest.js (lib/Restify/getAccountFromRequest.js) разрешает три канала аутентификации (cookie, Bearer, X-API-Key) и детектирует коллизии — если несколько токенов присутствуют одновременно, запрос отвергается. Бот-сервисные токены (sck_-префикс) разрешаются прямым lookup в Employee.botSettings.customConnectionToken.
Почему ценно: Изоляция tenant реализована не через отдельные БД или схемы, а через обязательное поле _wsId в каждом запросе. Это дешевле в эксплуатации (одна MongoDB), масштабируется линейно, и не требует миграции при добавлении нового workspace. Автоматическая инъекция через modifyIncomeData исключает забывчивость разработчика — поле проставляется на уровне ORM, а не в каждом endpoint.
Ключевые файлы: smarty-db/models/plugins/WsId.js, smarty-db/models/plugins/WsObject.js, lib/Restify/getAccountFromRequest.js
Что реализовано: Единый синглтон MQHandler (smarty-db/db/mq.js) наследует EventEmitter, управляет пулом каналов с durable: true очередями и автоматическим reconnect через amqp-connection-manager (reconnectTimeInSeconds: 2). Сообщения буферизируются до восстановления соединения. Каждый сервис регистрирует обработчики через HandlersMap (lib/microservice/HandlersMap.js) — Map с методом handle(msg, acker), который ack'ает сообщение и поддерживает chained dispatch (последовательная публикация в следующую очередь). Примеры: smarty-auth/events/index.js маппит 7 типов событий, smarty-dialog/worker.js подписывается на dialog-events и agent-task-events.
Почему ценно: Декаплинг сервисов через RabbitMQ позволяет масштабировать воркеры независимо (CPU-intensive обработка медиа, AI-боты, нотификации). Chained dispatch реализует saga-подобные workflows без оркестратора. Буферизация при disconnect защищает от потери сообщений при кратковременных сетевых сбоях. Prefetch count даёт контроль над параллелизмом обработки.
Ключевые файлы: smarty-db/db/mq.js, lib/microservice/HandlersMap.js, smarty-auth/events/index.js, smarty-dialog/worker.js
Что реализовано: SmartySchema (smarty-db/models/SmartySchema.js) расширяет mongoose.Schema и добавляет: реестр плагинов с отслеживанием firstPlugin, систему модельных событий (onModelEvent / emitModelEvent), incomeDataModifiers для декларативного преобразования входных данных, и MetaData с describe() для автогенерации API-спецификаций. Каталог из 80+ плагинов (smarty-db/models/plugins/) покрывает все доменные паттерны: SmartyObject (аудит + DBCA-события), RightObject (RBAC), ProfilePlugin (контактные данные), Markable, Stageable, Groupable, LinkableObject, ObjectWithCustomFields, Deletable, Archivable и др. Каждый плагин — чистая функция (schema, options) => void, композируемая через schema.plugin().
Почему ценно: Plugin-архитектура реализует DRY на уровне схем: 80+ моделей CRM переиспользуют одни и те же базовые поведения (создание, удаление, мягкое архивирование, audit-поля, права доступа) без дублирования кода. Система is(pluginName) позволяет runtime-проверять, какие capabilities подключены к модели. describe() автоматически генерирует документацию полей для API. Событийная модель onModelEvent расширяет Mongoose хуки без хрупкого monkey-patching.
Ключевые файлы: smarty-db/models/SmartySchema.js, smarty-db/models/plugins/SmartyObject.js, smarty-db/models/plugins/RightObject.js, smarty-db/models/plugins/ProfilePlugin.js
Что реализовано: Провайдер smarty_custom (smarty-dialog/lib/employeeBots/providers/smarty_custom.js) поддерживает два режима через botSettings.openaiCompatMode: (a) OpenAI-совместимый — запрос/ответ парсится по OpenAI /v1/chat/completions схеме, tools/tool_choice проходят транзитом; (b) Raw template — customBodyTemplate с {{placeholders}} (model, messages, message, system_prompt, crm_context, ws_id, bot_id, user_id, session_id и др.), ответ извлекается по customResponseJsonPath. SSRF-защита через validateURL() (lib/utils/ssrfGuard.js) — блокирует приватные IP, localhost, DNS-rebinding. Два независимых канала аутентификации: Authorization: Bearer <apiToken> для внешнего LLM и X-API-Key: <customConnectionToken> для обратных вызовов в CRM.
Почему ценно: Один интерфейс покрывает облачные провайдеры (через Cloudflare AI Gateway) и on-prem развёртки (vLLM, Ollama) без изменения кода бота. Raw-режим позволяет интегрировать произвольные API, не говорящие на OpenAI-диалекте. SSRF-guard закрывает атаку R10 из риск-реестра. Двойной канал аутентификации分离ает идентификацию LLM-провайдера и идентификацию бота в CRM.
Ключевые файлы: smarty-dialog/lib/employeeBots/providers/smarty_custom.js, lib/utils/ssrfGuard.js
Что реализовано: Agent loop (smarty-dialog/lib/employeeBots/agentLoop.js) реализует трёхфазную петлю: (1) Planner — LLM генерирует JSON-план из tool-call'ов с хард-капом 30 шагов и sanity-капом 25 итераций; (2) Executor — каждый tool-call проходит через executor.js с многослойной защитой: permission check через permissionChecker, chat-context gate (C6 — dialog-only tools не запускаются в extchat), high-impact gate для деструктивных операций, placeholder detection, embedded-ref unwrapper; (3) Re-planner — динамическое расширение плана на основе результатов выполненных шагов (до 3 расширений). Fabrication guard (fabricationGuard.js) ведёт реестр легитимных ObjectId из контекста и результатов — 24-hex в аргументах, отсутствующий в реестре, блокируется в enforce-режиме. Anti-hallucination guard детектирует "бот заявил действие без tool-call". Cost guard (costGuard.js) реализует kill-switch, rate-limit и daily token budget per _wsId. Каждое действие записывается в BotActionAudit (R1 Слой 7).
Почему ценно: Архитектура defense-in-depth для AI-агента: fabrication guard блокирует выдуманные ID, plan enforcement не даёт боту выйти с незавершённым планом, anti-hallucination fallback ловит "создал но не создал", cost guard предотвращает runaway расход. Планирование на флагманской модели (gpt-4.1 / claude-opus) с исполнением на более дешёвой — оптимизация цена/качество. Динамический re-planning решает проблему "планировщик не видит данных до выполнения шагов".
Ключевые файлы: smarty-dialog/lib/employeeBots/agentLoop.js, smarty-dialog/lib/employeeBots/tools/executor.js, smarty-dialog/lib/employeeBots/tools/highImpactGate.js, smarty-dialog/lib/employeeBots/fabricationGuard.js
Что реализовано: Три коллекции: custom_field (api-rest/models/custom_field.js) — определения полей (title, fieldType: text/textarea/number/date/datetime/checkbox/select/multiselect/checklist), custom_field_template — привязка fieldIds к группе объектов, custom_field_relation — значения (collectionName + _objectId + _fieldId + value). Плагин ObjectWithCustomFields (api-rest/models/plugins/ObjectWithCustomFields.js) добавляет calculated field customFields через GlobalCalculatedFields, который подтягивает значения из custom_field_relation по шаблону группы. При сохранении проверяется feature flag manageCustomFields на workspace. Бот-инструменты (executor.js) поддерживают find-or-create custom field + attach to template в одном вызове.
Почему ценно: EAV-паттерн даёт пользователям гибкость создавать произвольные поля без миграции схемы. Разделение на три коллекции (определение / шаблон / значение) позволяет переиспользовать поля между объектами одной группы и управлять видимостью через шаблоны. Индексация по (_objectId, _fieldId) обеспечивает приемлемую производительность для типовых CRM-сценариев.
Ключевые файлы: api-rest/models/custom_field.js, api-rest/models/custom_field_template.js, api-rest/models/plugins/ObjectWithCustomFields.js
Что реализовано: Двухуровневая система: AdminRight (smarty-db/models/workspace/rights/admin_right.js) — булевы флаги административных прав (manage_meta, manage_admins, remove_ws, manage_billing и др.) с предотвращением удаления последнего manage_admins=true; AccessRight (smarty-db/models/workspace/rights/access_right.js) — гранулярные права на объекты с полями marks (привязка к меткам), marksFlag (any_mark / no_marks_allowed / no_marks_denied), commissioners / non_commissioners (уровень доступа: none / read / read_update / read_update_archive_delete), export (commiss / non_commiss / any / none). Каждый RightObject (smarty-db/models/plugins/RightObject.js) — сущность с методами addRightToObjects / removeRightFromObjects / updateRightInObjects, автоматической нотификацией затронутых сотрудников и audit-логированием.
Почему ценно: Двухуровневая модель (admin + access)分离ает "что сотрудник может делать в системе" от "к каким объектам имеет доступ". Система commissioners позволяет назначать ответственных за конкретные объекты с расширенными правами. Marks-фильтрация даёт тонкую настройку доступа по тегам без создания отдельных ролей. Предотвращение удаления последнего admin защищает от lockout.
Ключевые файлы: smarty-db/models/workspace/rights/access_right.js, smarty-db/models/workspace/rights/admin_right.js, smarty-db/models/plugins/RightObject.js
Что реализовано: Иерархия ключей: (1) Master key — SECURE_MASTER_KEY (32-byte hex, env var, обязательный в production — lib/crypto.js бросает FATAL при отсутствии); (2) Workspace key — crypto.randomBytes(32), зашифрован master key через encryptWithMasterKey, хранится в Workspace.encryptionKey (api-rest/events/createInitialWorkspace.js); (3) User key — HMAC-SHA256(workspaceKey, userId). Шифрование — AES-256-GCM с random 12-byte IV + auth tag, результат base64. Функция getUserEncryptionKey(workspaceKey, userId) детерминированна — один и тот же user в одном workspace всегда получает тот же ключ.
Почему ценно: Envelope encryption изолирует компрометацию: утечка одного workspace key не раскрывает данные других workspace (каждый зашифрован отдельным мастер-ключом, а user key привязан к конкретному сотруднику). AES-256-GCM обеспечивает authenticated encryption — tamper detection встроен. Детерминированная генерация user key позволяет искать зашифрованные данные без расшифровки всего хранилища.
Ключевые файлы: lib/crypto.js, api-rest/events/createInitialWorkspace.js
Что реализовано: Три слоя: (1) OpenTelemetry (lib/tracing.js) — auto-instrumentation для Express, Mongoose, amqplib, ioredis; sampling ratio конфигурируется через OTEL_TRACES_SAMPLER_ARG; (2) Sentry (lib/sentry.js) — error tracking с mongoose integration, traces + profiles sampling; (3) Prometheus metrics (lib/metrics.js) — HTTP duration/total/errors, MQ messages processing/consumed, bot lock active, agent loop iterations, business events; (4) Audit log (smarty-db/models/workspace/audit_log.js) — immutable append-only записи с compound индексами по (_wsId, timestamp), (actor.id, action, timestamp), TTL 90 дней (конфигурируется). Каждое действие бота записывается в BotActionAudit и BotTrace (agentLoop.js), включая fabricated-id блоки.
Почему ценно: Три слоя observability покрывают разные аудитории: OTEL — для distributed tracing (сквозная трассировка HTTP -> MQ -> worker), Sentry — для production error alerting, Prometheus — для dashboards и алертинга на метриках. Audit log с immutability-guards (pre-save/updateOne/deleteOne hooks блокируют модификацию) обеспечивает compliance. TTL автоматически чистит старые записи. Метрики уровня бизнес-событий (smarty_business_events_total) заменяют InfluxDB.
Ключевые файлы: lib/tracing.js, lib/sentry.js, lib/metrics.js, smarty-db/models/workspace/audit_log.js
Что реализовано: Режим local (scripts/local.js) запускает все сервисы в одном процессе: HTTP-сервер на порту 27123, все REST-роуты (auth, dialog, notification, ext-messenger, media, reminder, sms, sip, webinar, statistic, crm, doc), socket.io, и 18+ воркеров (api-rest, automation, auth, cud, dialog, ext-messenger flow/sender/worker/preprocess, media resize/worker, notification, reminder, sip, webinar, statistic, invitation, timer). Bot-triggers и custom_workspace_events регистрируются как отдельные queue подписки. Telegram DNS-pinning для обхода локальных блокировок. PM2-конфиг находится в smarty-code/scripts/ecosystem.config.cjs (уровень моно-репо, не в smarty-backend-stable/). docker-compose.local.yml в корне smarty-code/ покрывает инфраструктурные сервисы для dev/тестирования (Mongo, Redis, RabbitMQ); production-compose не обнаружен.
Почему ценно: Единый entry point для разработки — один node scripts/local.js поднимает весь стек, включая socket.io и все воркеры. Это радикально упрощает локальную разработку и отладку: нет необходимости поднимать 18+ процессов. Архитектура сервисов через ms.addQueue / ms.addHTTP позволяет при production-развёртывании выделять отдельные сервисы в отдельные процессы/контейнеры без изменения кода.
Ключевые файлы: scripts/local.js
Риск: _wsId инжектится автоматически, но нет единого middleware, который бы валидировал _wsId на каждом запросе к БД. Если разработчик напрямую вызывает Model.find() без profile-контекста (например, в скриптах миграции), cross-tenant чтение возможно. strict:false в botPermissions (employee.js) намеренно, но создаёт surface для неожиданных полей.
Риск: HandlersMap.handle() ack'ает сообщение даже при ошибке обработчика (строка 38 — acker.ack() в catch). Это значит, что сбойное сообщение не попадёт в DLQ и будет потеряно. Комментарий TODO: начать по-нормальному обрабатывать подтверждает, что это известный техдолг.
Риск: 80+ плагинов без TypeScript означает, что порядок подключения критичен (WsObject зависит от EmployeeObject, RightObject зависит от PublicObject + WsObject). Нет runtime-валидации совместимости — неправильный порядок manifestится как mysterious undefined. firstPlugin флаг передаётся, но не используется повсеместно.
Риск: Raw-режим выполняет JSON.parse(rendered) на шаблоне, отрендеренном из пользовательского ввода. Хотя renderTemplate экранирует строки через JSON.stringify, сложные шаблоны с вложенными объектами могут产生 невалидный JSON. Нет лимита на размер customBodyTemplate — потенциальный DoS.
Риск: Fabrication guard проверяет только ObjectId-подобные hex-строки. Строковые ID (UUID, slug) не детектируются. PLANNER_HARD_CAP = 30 — искусственный потолок; сложные workflows могут_legitimately требовать больше шагов, и обрезка молча теряет данные. Anti-hallucination guard использует regex для русских/английских глаголов — ложно-срабатывания на честные ответы "не смог создать" возможны (хотя __HONEST_FAILURE_RE снижает этот риск).
Риск: EAV-запросы требуют join между тремя коллекциями. Для объектов с большим количеством custom fields производительность деградирует линейно. Нет composite индекса на custom_field_relation по (collectionName, _fieldId, value) — фильтрация по значению custom field будет full scan.
Риск: AccessRight.marks хранит массив ObjectId меток. При большом количестве меток (сотни) запрос marks: { $in: [...] } становится тяжёлым. Нет кэширования прав — каждый tool-call заново читает rights из БД. commissioners — boolean, не поддерживает multiple commissioners per object (только один набор прав).
Риск: Master key — single point of compromise. Нет key rotation механизма — если SECURE_MASTER_KEY скомпрометирован, все workspace keys нужно перезашифровать вручную. getUserEncryptionKey использует HMAC-SHA256, что детерминированно — один и тот же plaintext всегда даёт один и тот же ciphertext (нет semantic security для поиска, но это trade-off, не баг).
Риск: Audit log TTL 90 дней может быть insufficient для compliance (GDPR right to erasure vs. audit retention — конфликт). OTEL sampling 10% по умолчанию пропускает 90% трассировок — production debugging требует увеличения. Sentry beforeSend фильтрует test-окружение, но не PII — чувствительные данные могут попасть в Sentry.
Риск: Local mode запускает 18+ воркеров в одном процессе — один unhandled rejection крашит всё. Нет graceful shutdown для отдельных воркеров. PM2-конфиг versioned в моно-репо (smarty-code/scripts/), но не co-located с сервисом; production docker-compose не обнаружен в репо.
| Измерение | Оценка | Главный риск |
|---|---|---|
| Multi-tenancy / workspace isolation | Сильно | Нет единого middleware-валидатора _wsId на уровне драйвера |
| Event-driven / RabbitMQ | Хорошо | Ack при ошибке — потеря сбойных сообщений |
| Plugin архитектура Mongoose | Сильно | Порядок подключения не валидируется runtime |
| Dual-mode AI провайдер | Хорошо | Raw-режим: нет лимита на размер шаблона, JSON injection через template |
| Tool-calling архитектура | Сильно | Fabrication guard не покрывает не-ObjectId идентификаторы |
| EAV кастомные поля | Хорошо | Нет composite индекса для фильтрации по значению |
| RBAC система прав | Сильно | Нет кэширования прав, каждый tool-call — round-trip в БД |
| Envelope encryption | Хорошо | Нет key rotation, single master key |
| Observability stack | Сильно | PII может попасть в Sentry, TTL может конфликтовать с compliance |
| Deployment flexibility | Частично | Production docker-compose не обнаружен; PM2-конфиг в моно-репо, не co-located |