-
Notifications
You must be signed in to change notification settings - Fork 5
Description
Контекст
План поэтапной миграции от текущей монолитной архитектуры к модульной декомпозиции, описанной в #481.
Стратегия: Strangler Fig — новая структура создаётся рядом со старой, импорты перенаправляются постепенно, старые файлы становятся тонкими фасадами-реэкспортами, удаляются только когда все потребители мигрированы.
Основано на аудите: #480, целевая архитектура: #481.
Текущее состояние (три монолита)
| Монолит | Размер | Проблема |
|---|---|---|
orchestrator.py |
4140 строк | Startup (365 строк), 8 глобальных сервисов, ~100 legacy endpoints, 5 background tasks, регистрация 28 роутеров |
db/models.py |
3667 строк, 54 модели | Все домены в одном файле |
db/integration.py |
2688 строк, 29 менеджеров | Все менеджеры — синглтоны на уровне модуля |
Что уже хорошо (не нужно трогать)
db/repositories/— 45 файлов, чистая изоляция, не импортируют ничего из orchestrator/routers/services- Бот-подпроцессы (telegram_bot/, whatsapp_bot/) — уже общаются через HTTP API, не импортируют
db/ - Роутеры — уже 28 отдельных файлов в
app/routers/
Ключевые ограничения и риски
1. Alembic-миграции
Существующие миграции делают from db.models import User, ChatSession, .... Перенос моделей без фасада-реэкспорта ломает alembic upgrade head.
Решение: db/models.py остаётся как фасад — импортирует из доменных модулей и реэкспортирует всё. Старые миграции работают без изменений.
2. SQLAlchemy Base.metadata
Все модели должны быть зарегистрированы в одном Base для:
Base.metadata.create_all()(автосоздание таблиц)- Relationship-ы между моделями разных доменов
- Alembic autogenerate
Решение: Base остаётся в db/database.py. Доменные models.py импортируют оттуда. При старте все модели "видны" через явные импорты в точке входа.
3. Cross-domain FK
14 таблиц имеют workspace_id FK, owner_id → User.id повсеместно. Убирание FK ради "чистоты модулей" = потеря referential integrity без выигрыша.
Решение: Принять, что доменные модели знают о core-моделях (User, Workspace) — это нормальная зависимость "все зависят от core". Не убирать FK из существующих таблиц.
4. Параллельная разработка (local + server)
Массовый рефакторинг файловой структуры = merge-ад. Переносы файлов + правки в старых путях = конфликты на каждом PR.
Решение: Каждую фазу делает одна машина. Мелкие PR с чёткими границами. Вторая машина в это время не трогает мигрируемые файлы.
Фазы миграции
✅ Фаза 0: Инфраструктура core (нулевой риск)
Чисто аддитивно — создаём новые модули, не трогая существующий код
Создать modules/core/:
events.py— EventBus (in-process pub/sub)health.py— HealthRegistry (модули регистрируют свои health checks)tasks.py— TaskRegistry (named background tasks, cancel_all)base.py— BaseService (если нужен общий интерфейс)
Критерий готовности: Новые модули импортируемы и покрыты тестами. Существующий код не изменён.
Зависимости: Нет.
Фаза 1: Разделение db/models.py (низкий риск)
Модели переезжают в доменные файлы,
db/models.pyстановится фасадом
Целевая структура:
modules/
├── core/models.py ← User, Role, RolePermission, UserSession,
│ Workspace, WorkspaceMember, WorkspaceInvite,
│ UserIdentity, SystemConfig
├── chat/models.py ← ChatSession, ChatMessage, ChatSessionShare, ResourceShare
├── channels/
│ ├── telegram/models.py ← BotInstance, TelegramSession + Bot* (12 sales-моделей)
│ ├── whatsapp/models.py ← WhatsAppInstance
│ └── widget/models.py ← WidgetInstance
├── llm/models.py ← CloudLLMProvider, LLMPreset
├── knowledge/models.py ← KnowledgeCollection, KnowledgeDocument, FAQEntry
├── speech/models.py ← TTSPreset
├── crm/models.py ← AmoCRMConfig, AmoCRMSyncLog
├── ecommerce/models.py ← WooCommerceConfig
├── kanban/models.py ← KanbanProject, KanbanTask, KanbanTaskDependency,
│ KanbanChecklistItem, KanbanTaskStatus
├── claude_code/models.py ← ClaudeCodeSession, ClaudeCodeProject
├── monitoring/models.py ← AuditLog, UsageLog, UsageLimits
├── sales/models.py ← PaymentLog
├── admin/models.py ← UserConsent
└── telephony/models.py ← GSMCallLog, GSMSMSLog, GitHubRepoProject
db/models.py остаётся — реэкспорт:
# db/models.py — фасад (backward compat для Alembic и старого кода)
from modules.core.models import User, Role, RolePermission, ...
from modules.chat.models import ChatSession, ChatMessage, ...
from modules.kanban.models import KanbanProject, KanbanTask, ...
# ... все 54 моделиПорядок: По одному домену за коммит. Начать с самых изолированных (kanban, claude_code, monitoring), заканчивать core (от которого все зависят).
Критерий готовности: alembic upgrade head работает. alembic revision --autogenerate видит все модели. Все тесты проходят. Все from db.models import X продолжают работать.
Зависимости: Нет (можно параллельно с Фазой 0).
Фаза 2: Разделение db/integration.py (низкий-средний риск)
Менеджеры переезжают в доменные service.py,
db/integration.pyстановится фасадом
Паттерн:
# modules/chat/service.py
class ChatService: # бывший AsyncChatManager
...
# db/integration.py — фасад
from modules.chat.service import ChatService as AsyncChatManager
async_chat_manager = AsyncChatManager()29 менеджеров → ~14 доменных service.py (некоторые домены объединяют несколько менеджеров):
| Домен | Менеджеры → service.py |
|---|---|
| core | AsyncUserManager, AsyncUserSessionManager, AsyncRoleManager, AsyncWorkspaceManager, AsyncUserIdentityManager, AsyncConfigManager |
| chat | AsyncChatManager, AsyncChatShareManager |
| channels/telegram | AsyncBotInstanceManager, AsyncTelegramSessionManager |
| channels/whatsapp | AsyncWhatsAppInstanceManager |
| channels/widget | AsyncWidgetInstanceManager |
| llm | AsyncCloudProviderManager |
| knowledge | AsyncKnowledgeDocManager, AsyncKnowledgeCollectionManager, AsyncFAQManager |
| speech | AsyncPresetManager |
| crm | AsyncAmoCRMManager |
| ecommerce | AsyncWooCommerceManager |
| kanban | AsyncKanbanManager, AsyncKanbanProjectManager |
| claude_code | AsyncClaudeCodeManager, AsyncClaudeCodeProjectManager |
| monitoring | AsyncAuditLogger, AsyncPaymentManager, DatabaseManager |
| admin | AsyncResourceShareManager |
Порядок: Начать с листовых доменов (ecommerce, claude_code, kanban), заканчивать core.
Критерий готовности: Все роутеры работают без изменений (импорт из db.integration по-прежнему валиден). Тесты проходят.
Зависимости: Фаза 1 (модели должны быть в доменах, чтобы service.py импортировал из своего домена).
Фаза 3: Перенос роутеров (средний риск)
app/routers/*.py→modules/{domain}/router.py, фасад вapp/routers/__init__.py
Паттерн:
# modules/chat/router.py — реальный код
router = APIRouter(prefix="/admin/chat", tags=["chat"])
...
# app/routers/chat.py — фасад
from modules.chat.router import router # noqa: F40128 роутеров → ~14 доменов:
| Домен | Роутеры |
|---|---|
| core | auth.py, roles.py, workspace.py |
| chat | chat.py |
| channels/telegram | telegram.py |
| channels/whatsapp | whatsapp.py |
| channels/widget | widget.py |
| llm | llm.py |
| knowledge | wiki_rag.py |
| speech | tts.py, stt.py, services.py |
| crm | amocrm.py |
| ecommerce | woocommerce.py |
| kanban | kanban.py, github_repos.py |
| claude_code | claude_code.py |
| monitoring | audit.py, usage.py, monitor.py |
| sales | bot_sales.py, yoomoney_webhook.py |
| admin | backup.py, legal.py, faq.py, github_webhook.py |
| telephony | gsm.py |
На этом этапе каждый домен имеет: models.py, service.py, router.py — базовая структура из #481.
Параллельно: Обновить импорты в роутерах — вместо from db.integration import X → from modules.{domain}.service import X.
Критерий готовности: orchestrator.py регистрирует роутеры через ту же точку входа (app/routers/__init__.py). Все endpoints работают.
Зависимости: Фаза 2.
Фаза 4: Разборка orchestrator.py (высокий риск)
Самая сложная фаза — разбить 4140 строк на управляемые части
Подэтапы:
4a. Вынести legacy endpoints
~100 legacy endpoints (OpenAI-compatible /v1/*, widget endpoints, helper functions) → отдельные роутеры:
/v1/*→modules/compat/router.py- Widget endpoints (из orchestrator.py) → уже должны быть в
modules/channels/widget/router.py(после Фазы 3) StreamingTTSManager→modules/speech/streaming.py
4b. Модульный startup
Заменить 365-строчный startup_event() на:
for module in enabled_modules:
svc = module.create_service(db, **deps)
app.include_router(module.router)Каждый модуль получает create_service() → возвращает инициализированный сервис.
4c. Background tasks → TaskRegistry
5 create_task() без сохранения references → TaskRegistry из Фазы 0:
tasks.register("session_cleanup", chat_svc.periodic_cleanup, interval=3600)
tasks.register("vacuum", db.periodic_vacuum, interval=7*24*3600)
tasks.register("kanban_sync", kanban_svc.periodic_sync, interval=900)4d. Graceful shutdown
shutdown_event() → tasks.cancel_all() → channels.stop_all_bots() → bridge.stop() → db.close()
4e. Deployment modes через модульную загрузку
MODULES = {
"full": [..., speech, telephony],
"cloud": [...], # speech и telephony не загружаются
}Целевой orchestrator.py (или app.py): ~100-150 строк.
Критерий готовности: Health check работает. Все endpoints доступны. Боты стартуют. Graceful shutdown корректен.
Зависимости: Фазы 0-3.
Фаза 5: EventBus для cross-module коммуникации (низкий-средний риск)
Заменить прямые импорты между доменами на события
Приоритетные события:
| Событие | Публикует | Подписчик | Что заменяет |
|---|---|---|---|
KnowledgeUpdated |
knowledge | llm (reload FAQ cache) | Прямой вызов из wiki_rag роутера |
WidgetSessionCreated |
channels/widget | crm (create lead) | create_task(_widget_create_amocrm_lead) в orchestrator.py |
WidgetMessageSent |
channels/widget | crm (append note) | Прямой вызов в widget endpoint |
UserRoleChanged |
core/auth | core/cache (invalidation) | Ручной cache.invalidate() |
DatasetSynced |
crm, ecommerce | knowledge (reindex) | Прямой вызов reindex в amocrm/woocommerce роутерах |
ConfigChanged |
core/config | affected modules | Нет (сейчас требует рестарт) |
По одному событию за PR. Каждое — отдельная дочерняя issue.
Критерий готовности: Прямые импорты между несвязанными доменами заменены на подписки. Граф зависимостей соответствует #481.
Зависимости: Фаза 4 (EventBus должен быть интегрирован в startup).
Фаза 6: Protocol interfaces (низкий риск, можно делать в любой момент после Фазы 2)
Типизация контрактов между модулями
Определить Protocol-классы для:
KnowledgeService(search, search_multi, get_collections)LLMService(generate, stream, resolve_backend)ChatService(create_session, send_message, stream_message)
Это не ломает код — просто добавляет типизацию для mypy и документации.
Граф зависимостей между фазами
Фаза 0 (core infra) ─────────────────────────────────┐
│ │
Фаза 1 (split models) ─── можно параллельно ──────────┤
│ │
Фаза 2 (split integration) ───────────────────────────┤
│ │
Фаза 3 (move routers) ────────────────────────────────┤
│ │
Фаза 4 (разборка orchestrator) ───────────────────────┤
│ │
Фаза 5 (EventBus) ────────────────────────────────────┘
│
Фаза 6 (Protocols) ── можно в любой момент после Ф2 ──┘
Оценка объёма
| Фаза | Риск | Оценка | Кол-во PR |
|---|---|---|---|
| 0: Core infra | Нулевой | S | 1 |
| 1: Split models | Низкий | M | 3-5 (по группам доменов) |
| 2: Split integration | Низкий-средний | L | 5-7 (по доменам) |
| 3: Move routers | Средний | M | 3-5 |
| 4: Разборка orchestrator | Высокий | XL | 5-8 (по подэтапам) |
| 5: EventBus | Низкий-средний | M | 5-6 (по событиям) |
| 6: Protocols | Низкий | S | 1-2 |
Итого: ~25-35 PR, каждый — небольшой и ревьюабельный.
Правила миграции
- Никогда не ломать
from db.models import X— фасад-реэкспорт обязателен - Никогда не ломать
from db.integration import X— фасад-реэкспорт обязателен - Один домен за PR — не мигрировать несколько доменов в одном PR
- Тесты + lint на каждом PR — CI должен быть зелёным
alembic upgrade head— обязательная проверка на каждом PR с моделями- Новые миграции импортируют из
modules/— старые остаются сdb.models - Фасады удаляются только когда ВСЕ потребители мигрированы (отдельный PR, в конце)
Дочерние issues
Для каждой фазы и подэтапа будут созданы отдельные issues с конкретными задачами, чеклистами и acceptance criteria. Они будут привязаны к этому issue как parent.