Files
call-center/longread.md
T
2026-08-21 07:11:50 +00:00

226 lines
37 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# call-center — лонгрид по архитектуре и флоу
> Документ собран по результатам сквозного ревью репозитория (25+ Python-сервисов, 6 вэб-UI, Asterisk-телефония, "AI-стек"). Задача документа — дать целостную картину: что здесь на самом деле есть, как это работает end-to-end, и где основной технический долг. Короткие архитектурные факты уже были описаны в `docs/architecture/*.md` — этот файл идёт глубже и местами поправляет/уточняет то, что там написано (см. раздел 8 "Расхождения с существующей документацией").
---
## 1. Что это за проект — TL;DR
**call-center** — омниканальная контакт-центр платформа собственной разработки: телефония (Asterisk), Telegram, WhatsApp, Webchat, Email, плюс встроенный "AI-оператор" первой линии и полноценный CRM/sales-модуль поверх того же ядра. Технически это не микросервисы в строгом смысле — это **модульный монолит, физически нарезанный на ~25 FastAPI-процессов, но использующий одну общую SQLAlchemy-схему и одну БД**. Сервисы регулярно напрямую читают/пишут таблицы, которыми формально владеют другие сервисы (пример: `customer_service` джойнит 6 чужих таблиц, `sales_service` пишет прямо в `Customer`, `supervisor_service` читает `Interaction`).
Есть ли здесь фронт, бэк и ML?
- **Фронт** — да, но без фреймворка: 6 отдельных vanilla-JS SPA (`ui/operator`, `ui/supervisor`, `ui/admin`, `ui/analyst`, `ui/sales`, `ui/login`), без сборщика, без TypeScript, ~28 000 строк JS суммарно. Самые крупные — `ui/analyst/app.js` (7 927 строк) и `ui/operator/app.js` (7 456 строк, включает встроенный SIP.js WebRTC-софтфон).
- **Бэк** — да, это основной объём кода: 25 FastAPI-сервисов на Python, SQLite по умолчанию / PostgreSQL для прод, событийная шина поверх RabbitMQ (transactional outbox/inbox), но **шина выключена по умолчанию** (`EVENT_BUS_ENABLED=False`).
- **ML** — здесь стоит быть точным: **это почти не ML, а оркестрация сторонних AI-API** (OpenAI-совместимый чат-эндпоинт, Yandex SpeechKit, ElevenLabs) плюс много ручных rule-based эвристик (regex/keyword-словари на русском и казахском). Единственный по-настоящему локальный ML-компонент — `faster-whisper` (модель Whisper) в отдельном сервисе `streaming_asr_sidecar_service`, и то это вспомогательный "partial transcript" ускоритель, а не основной ASR-путь. Поиск по базе знаний ("RAG") — это keyword-скоринг без эмбеддингов и без векторного индекса.
---
## 2. Состав системы (карта сервисов)
### 2.1 Инфраструктурные
| Сервис | Роль |
|---|---|
| `gateway` (`gateway/app.py`) | Единая точка входа: реверс-прокси `/proxy/{service}/{path}``SERVICE_URLS`, раздаёт статику всех 6 UI, шлёт алерты об ошибках в Telegram-бот |
| `auth_service` | Логин/пароль (в открытом виде!), OIDC/Keycloak PKCE-флоу, кастомный HMAC-токен (не JWT-библиотека, а самописный HS256-подобный формат) |
| `audit_service` | Лог аудита; пишется либо напрямую (`POST /audit/events`), либо через подписку на шину событий (которая по умолчанию выключена — см. §5) |
| `event_bus_service` | RabbitMQ dispatcher поверх transactional outbox/inbox |
### 2.2 Клиентские/роутинг
| Сервис | Роль |
|---|---|
| `customer_service` | Профиль клиента + кросс-канальные внешние идентичности (`CustomerExternalIdentity`) |
| `interaction_service` | Канонический "Interaction" (обращение/тикет) + таймлайн |
| `routing_service` | Распределение по очередям — **на деле не skill-based**, см. §7.3 |
### 2.3 Телефония и медиа-каналы
| Сервис | Роль |
|---|---|
| `asterisk_bridge_service` | Мост к Asterisk: AMI-события, FastAGI/IVR, управление звонком, хендофф в AI, запись |
| `ivr_service` | Граф IVR-флоу (DTMF state machine), не знает про Asterisk напрямую |
| `recording_service` | Хранилище записей звонков (SHA-256, локальный диск) |
| `voice_adapter_service` | Пассивный append-only лог voice-событий для идемпотентности bridge'а (не эквивалент interaction/state) |
| `telegram_adapter_service`, `whatsapp_adapter_service`, `webchat_adapter_service`, `email_adapter_service` | Входящие/исходящие интеграции по каналам |
### 2.4 AI и база знаний
| Сервис | Роль |
|---|---|
| `kb_service` | Хранилище статей + keyword-поиск (`services/shared/kb_search.py`) |
| `ai_orchestrator_service` | "Мозг": решает reply/handoff для Telegram, WhatsApp И голоса (все три канала — в одном сервисе) |
| `ai_voice_runtime_service` | Real-time аудио-слой: ASR/TTS, barge-in, AudioSocket-протокол |
| `streaming_asr_sidecar_service` | Локальный faster-whisper для partial-транскриптов (опциональный ускоритель) |
| `real_time_voice` | **Не отдельный сервис** — 15-строчный alias-реэкспорт `ai_voice_runtime_service.app:app` |
### 2.5 Аналитика/CRM/надзор
| Сервис | Роль |
|---|---|
| `reporting_service` | KPI-агрегация (SLA/ASA/AHT/Abandon/FCR) |
| `supervisor_service` | Live-мониторинг очередей и статусов агентов |
| `sales_service` | **Отдельный полноценный CRM** (лиды/сделки/оплаты/эскалации), 7 140 строк в одном `app.py`, 94 REST-эндпоинта — самый большой файл в репозитории, см. §7.4 |
---
## 3. Данные и схема
- По умолчанию — **SQLite** (`.data/mvp_cc.db`), для прод/K8s — **PostgreSQL**.
- Схема управляется **тремя параллельными механизмами одновременно**, которые надо вручную синхронизировать при любом изменении:
1. SQLAlchemy ORM-модели (`services/shared/sql_models.py`, `sales_sql_models.py`) — единый `Base` на все "микросервисы".
2. `migrations/sql/*_{postgres,sqlite}.sql` — 31 миграция × 2 диалекта = **62 файла**, написанные вручную (не codegen), почти механический перевод друг друга.
3. `services/shared/sql_init.py::_apply_runtime_schema_compatibility()` — ~700-строчный императивный шим `ALTER TABLE ... ADD COLUMN IF NOT EXISTS`, который выполняется при **каждом старте каждого сервиса**, чтобы "дотянуть" SQLite-схему до актуального состояния ORM.
- Миграции запускаются `scripts/migrate_core_db.py`; отслеживаются только по имени файла (без checksum), без down-миграций.
- Найдена коллизия номеров: `0013_asterisk_bridge_idempotency` и `0013_telegram_ai` — оба претендуют на номер 13.
- **Контракты (`contracts/openapi/*.yaml`, `contracts/events/*.json`) существуют только как документация — нигде не валидируются.** Нет `jsonschema`/`openapi-spec-validator` в зависимостях, event-конверты никем не сверяются со схемой перед публикацией.
---
## 4. Как это работает: два сквозных флоу
### 4.1 Входящий телефонный звонок (ring → AI или человек)
1. Оператор связи → Asterisk (`telecom-kz` trunk) → dialplan `[from-telecom]` жёстко маршрутизирует всё на extension `7100` (AI-first — это зашито в dialplan, а не конфигурируется).
2. `[mvpcc-call-init]`: генерируется `CALL_ID`/`LINKED_ID`, стартует запись (`MixMonitor`), летит AMI `UserEvent(MVPCCCallStarted)`.
3. `asterisk_bridge_service.ami.ami_loop()` (фоновый поток, персистентное AMI-соединение) ловит событие → пишет в `AsteriskEventLogRow` (inbox/outbox-паттерн с at-least-once доставкой) → `process_call_started`: создаёт `Interaction(channel="voice")` через `interaction_service`, создаёт/обновляет `AsteriskCallLinkRow` (телефонный state machine: `ringing → claimed → connected → ended`, плюс отдельный `ai_state`: `queued → greeting → active/handoff_required → human_owned/closed`).
4. Так как очередь сконфигурирована как `ai_first`, бридж стартует Voice AI сессию (`POST .../internal/voice-ai/sessions` в `ai_voice_runtime_service`), Asterisk запускает `AudioSocket()` — сырой TCP-канал аудио к рантайму.
5. Внутри `ai_voice_runtime_service`/`ai_orchestrator_service` идёт разговор (ASR → LLM-решение → TTS), подробности в §6.
6. Если AI решает передать на человека — вызывает бридж (`POST /internal/voice-ai/calls/{id}/handoff`), тот резолвит текущий live-канал (нетривиальная эвристика — Asterisk меняет `UniqueID` на каждом хопе `Dial`/`Redirect`), ставит `MVPCC_*` channel vars и делает AMI `Redirect` в `[mvpcc-transfer]`.
7. На пикапе оператором — `MVPCCOperatorConnected``process_operator_connected`: `AsteriskCallLinkRow` переходит в `human_owned`, в таймлайн обращения падает запись "AI запросил перевод на специалиста".
8. Хэнгап → `h`-extension: `StopMixMonitor` + `MVPCCCallEnded`/`MVPCCRecordingReady` → бридж скачивает файл (локально или по SFTP) и загружает в `recording_service`, который публикует `call.recording.ready` в реальную шину событий.
9. Если что-то из AMI-событий потерялось (обрыв AMI-сессии, рестарт бриджа) — фоновый `reconcile.py` каждые N секунд сверяет "зависшие" звонки через `CoreShowChannels` и синтезирует недостающие `call.ended`/`recording.ready`.
**Важное расхождение с документацией**: события `call.connected` и `call.transferred`, хотя и описаны в `contracts/events/`, **реально никогда не долетают до event-bus**`emit_voice_event()` в бридже — это просто синхронный POST в `voice_adapter_service` (который только пишет лог-таблицу и не публикует в шину). `event_bus.ensure_rabbitmq_topology()` даже не биндит эти два типа событий ни к одной очереди. А `call.recording.ready`, наоборот, до шины доходит, но **со схемным дрейфом**: контракт требует поле `recording_path`, а реальный паблишер шлёт `file_name` — такого поля вообще нет.
### 4.2 Входящее сообщение в Telegram → создание обращения
1. `POST /integrations/telegram/bot/webhook` (защита — только статичный shared-secret заголовок, **без HMAC**, в отличие от WhatsApp).
2. Парсинг апдейта → дедуп по `external_message_id` (на случай повторной доставки вебхука).
3. `_upsert_thread_for_inbound`: резолвит/создаёт клиента через `CustomerExternalIdentity` (`telegram:{user_id}`), находит/создаёт `TelegramThreadRow`; если нет открытого треда — создаёт `Interaction(channel="telegram")` + таймлайн.
4. Два побочных эффекта: `_sync_sales_thread()` (синхронизация с CRM — **только у Telegram, у WhatsApp такой синхронизации нет**, необъяснённая асимметрия) и `_maybe_enqueue_ai_for_thread()` (если тред не забрал человек — просит `ai_orchestrator_service` сгенерировать ответ).
5. Исходящая доставка ответа идёт через отдельный фоновый worker с claim/lease/retry-backoff — этот же паттерн **дословно продублирован** в `whatsapp_adapter_service` под другими именами функций.
---
## 5. Событийная шина: что реально работает, а что нет
Документация (`docs/architecture/overview.md`, `event-schemas.md`) описывает event-driven архитектуру поверх RabbitMQ с transactional outbox/inbox — **это правда как код**, но не как поведение по умолчанию:
- `append_outbox_event()` действительно пишет событие в той же транзакции, что и доменные данные — это честный outbox.
- Но `EVENT_BUS_ENABLED` по умолчанию **`False`**. Диспетчер (`event_bus_service`), который открывает `pika`-соединение к RabbitMQ и реально публикует, стартует только если флаг включён.
- `audit_service` подписан на шину — но раз шина выключена по умолчанию, **аудит из доменных событий в демо/дефолтной конфигурации не работает вообще**.
- `interaction_service`/`supervisor_service` оборачивают запись в outbox условием `if event_bus_enabled()`, а вот `sales_service` — нет: пишет outbox-строки всегда, даже когда шина выключена (просто копятся неотправленные записи).
Итог: в дефолтной конфигурации это **набор сервисов, синхронно общающихся HTTP-вызовами и делящих одну БД**, а не событийная архитектура. Событийность — реальная, но опциональная фича, которая явно включается только в проде (и то не факт, что везде).
---
## 6. AI/ML-стек: честная оценка
**Главный вывод: это оркестрация сторонних AI-API + большой объём rule-based логики на regex/keyword-словарях, а не собственные ML-модели.** Единственное исключение — локальный `faster-whisper` в отдельном сервисе.
### 6.1 Кто с кем говорит
- **`ai_orchestrator_service`** — единственный "мозг" для **всех** текстовых/голосовых AI-веток: Telegram AI, WhatsApp AI и голосовой AI живут в одном `app.py` (3 717 строк) + `voice.py` (2 853 строки, специфика голоса). Из них добрая половина `app.py` (~1400 строк) — это аналитика/дэшборды (AI containment/handoff funnel), а не сама логика решений.
- LLM-вызов — не через официальный SDK, а сырой `httpx` POST на `{AI_API_BASE}/chat/completions` (OpenAI-совместимый формат, `AI_PROVIDER=openai_compatible`). По умолчанию `AI_PROVIDER=stub` — работает вообще без LLM, чисто на детерминированных правилах (`_stub_decision`): проверка идентичности → запрос человека → чувствительная тема → лучшее совпадение в KB → generic-уточнение.
- Хэндофф на человека — не отдельный классификатор, а поле `needs_handoff`, которое либо возвращает сама LLM в JSON-ответе, либо жёстко срабатывает раньше LLM по regex-правилам (`_looks_like_human_request`, `_is_sensitive_request`).
- **Голосовая ветка** (`voice.py`) — многослойная rule-based политика (проверка идентичности → "вы ещё здесь?" → счётчик уточнений ≥4 → off-domain-отказ → LLM-вызов только если явно включён `llm_guarded`/`v2_*` режим → фоллбэк на KB → generic-уточнение). Голос может полностью работать без единого обращения к LLM.
### 6.2 Реальный аудио-пайплайн (`ai_voice_runtime_service`)
- **ASR/TTS-провайдеры**, подтверждено по коду: **Yandex SpeechKit** (дефолт), **OpenAI**, **ElevenLabs** — всё через самописные `httpx`/`grpc`-клиенты (нет `openai`/`anthropic` пакетов в `requirements.txt` вообще — ни одной официальной SDK).
- **AudioSocket** — протокол Asterisk для стриминга аудио, реализован вручную (3-байтовый заголовок + payload).
- **VAD (детекция речи) и barge-in** — не ML, а простой **RMS-энергетический порог** (`EnergyVAD`): если энергия кадра выше порога дольше 220 мс во время проигрывания ответа бота — прерываем воспроизведение. Ни Silero-VAD, ни WebRTC-VAD — ничего ML-based здесь нет.
- `ack_bank.py` — файловый кэш коротких "заполняющих" фраз ("секунду, посмотрю...") с ключом по SHA-256, чтобы маскировать задержку LLM/TTS без живого TTS-раунд-трипа на каждую реплику.
### 6.3 "RAG" по базе знаний
`services/shared/kb_search.py`**не векторный поиск**. Загружает все строки в Python и скорит каждую вручную (совпадение токенов + префиксный fuzzy-match + бонус за фразу), без обратного индекса, без учёта частоты термина (IDF), без SQLite FTS5/pgvector. При росте базы знаний это не масштабируется — сейчас это полный скан таблицы на каждый запрос.
### 6.4 Единственный настоящий локальный ML-компонент
`streaming_asr_sidecar_service` — отдельный FastAPI-сервис, реально грузит модель **`faster-whisper`** (`WhisperModel`, CPU, int8-квантование по умолчанию). Используется только как опциональный "partial transcript" ускоритель в режиме `voice v2` (быстрые промежуточные транскрипты для более ранней реакции бота), финальный/авторитетный транскрипт по умолчанию всё равно идёт через облачный Yandex SpeechKit.
### 6.5 Найденные проблемы (детали ниже в §7)
- Мёртвый дублированный код в `voice.py` (закрытая копия `_voice_decision`, полностью неиспользуемая `_voice_decision_legacy`).
- Три почти идентичных модуля "singleton JSON config в БД" (`ai_operator_config.py`, `voice_tts_config.py`, `voice_name_config.py`).
- God-class на 1954 строки (`media_runtime.py`) — протокол, VAD, barge-in, стриминг ASR, воспроизведение и метрики в одном классе.
- Казахстанские гео-специфичные словари (город, алиасы) захардкожены прямо в `services/shared/ai_context_summary.py` — не вынесены в конфиг, что мешает мультирегиональному переиспользованию.
- Три независимых механизма кэширования TTS, решающих одну и ту же задачу по-разному (offline-материализация IVR-промптов, live-синтез, `ack_bank`).
---
## 7. Технический долг и находки (сведено по приоритету)
### 7.1 🔴 Безопасность: подмена ролей через заголовки
`get_actor()` (`services/shared/security.py`) принимает **либо** подписанный Bearer-токен, **либо**, если `ALLOW_LEGACY_HEADER_AUTH=True` (это **дефолт**), сырые несигнированные заголовки `X-User`/`X-Role`. Gateway (`gateway/app.py::_forward`) **слепо форвардит** любые `X-User`/`X-Role`, которые прислал клиент, без сверки с токеном. Итог: **любой клиент, достучавшийся до `/proxy/*`, может выставить себе `X-Role: admin` и пройти любую `require_roles()`-проверку**, пока legacy header auth явно не выключен во всех окружениях. В UI это ровно та лазейка, о которой предупреждает `access-guards.js` (клиентский guard в `ui/operator` — чисто косметический, обходится правкой `localStorage`). Судя по `deployment/helm/values.track9-strict.yaml`/`values.track9-live.yaml` (`allowLegacyHeaderAuth: "0"`, `authMode: bearer`), это уже осознанная проблема, которую в проде отключают через overlay — но дефолт остаётся небезопасным.
### 7.2 🔴 Пароли и секреты
- `auth_service`: пароли пользователей сравниваются и хранятся **в открытом виде** (`user.password != payload.password`), нет ни одного вызова хэш-функции нигде в auth-пути. Демо-пользователи вида `admin123` захардкожены в сидинге.
- `services/shared/security.py`: секрет токена по умолчанию `"dev-secret-change-me"`, если `APP_TOKEN_SECRET` не задан — тихий даунгрейд безопасности при забытой переменной окружения.
- `deployment/docker-compose.asterisk.server.yml`: реальный продовый IP (`92.38.48.166`) захардкожен прямо в compose-файле.
### 7.3 🟠 Routing на деле не skill-based
`routing_service` парсит поле `strategy` (`round_robin|least_loaded|skill_based`) из правил очереди, но **реально использует только round-robin** по захардкоженному в коде словарю `_AGENTS = {"voice": [...], "telegram": [...], ...}` (6 фейковых username). `least_loaded`/`skill_based` — мёртвые значения enum, не влияющие ни на что. Состояние агентов (online/busy) вообще хранится в другом сервисе (`supervisor_service`) и никогда не читается роутингом — это два несвязанных подсистемы, случайно делящие общее событие `agent.state.changed`.
### 7.4 🟠 `sales_service` — параллельный продукт, слабо связанный с ядром
7 140 строк в одном `app.py`, 94 эндпоинта — крупнейший файл в репозитории на порядок. Полноценный CRM (лиды/сделки/33 стадии в явном state machine/платежи с HMAC-верификацией вебхуков/эскалации) с собственным мульти-тенантным слоем (`tenant_id` на каждой таблице — единственное место в системе, где мультитенантность реально используется). Стыкуется с ядром только в одной точке — таблице `Customer`, куда пишет напрямую, **в обход `customer_service`** (та же таблица, но два независимых write-пути без единой каскадной логики — при переименовании клиента через `customer_service` каскадно обновляются 3 связанные таблицы, через `sales_service` — нет). Отдельно стоит отметить: `automation_worker.py` (695 строк, полноценная task-queue с retry/backoff) **нигде не инстанциируется вне тестов** — задачи автоматизации (напоминания по счетам, эскалации) складываются в таблицу и никогда не обрабатываются в проде.
### 7.5 🟠 Дублирование кода в адаптерах каналов
- `webchat_adapter_service` и `email_adapter_service` — структурно идентичные файлы (120 и 110 строк), различаются только именами полей.
- `telegram_adapter_service` (2 145 строк) и `whatsapp_adapter_service` (2 328 строк) — почти построчное зеркало друг друга (одинаковые имена функций, одинаковый порядок): резолв клиента, рендер AI-саммари, worker доставки ответов с claim/lease/retry — всё продублировано. Оценка потенциального сокращения при вынесении общего `chat_adapter.py` в `services/shared/`**~15001800 строк**.
- 6 UI-приложений (`ui/*/app.js`) независимо реализуют одинаковые `persistSession()`/`api()`/`log()`/`escapeHtml()` — без единого модуля, потому что нет сборщика и ES-модулей. При этом обработка истёкшего токена (авто-логаут на 401) есть **только** в `ui/operator` — остальные 5 приложений просто тихо продолжают фейлить запросы после истечения токена.
### 7.6 🟡 Мёртвый/дублированный код (конкретные находки)
| Файл | Проблема |
|---|---|
| `services/ai_orchestrator_service/voice.py:1785-1900` | Закрытая (shadowed) первая версия `_voice_decision`, вторая версия на строке 1903 её перекрывает — редактировать первую бессмысленно |
| `services/ai_orchestrator_service/voice.py:1697-1783` | `_voice_decision_legacy` — нигде не вызывается |
| `ui/analyst/app.js` | Минимум 6 функций продублированы дважды в файле (`loadSavedAnalyticsViews`, `saveAnalyticsView`, `deleteAnalyticsView`, `loadAnalyticsTrend` и др.) — похоже на не до конца слитые две версии модуля аналитики; вторая версия по факту рабочая, первая — мёртвый вес (сотни строк) |
| `ui/operator/sip-0.21.2.min.js` vs `ui/operator/vendor/sip-0.21.2.min.js` | Побайтово идентичный дубль, `vendor/`-копия сиротская, ничем не используется |
| `deployment/kubernetes/mvp-cc-platform.yaml` | Устаревший ручной K8s-манифест, не содержит 5 из текущих сервисов (`sales`, `ai-orchestrator`, `ai-voice-runtime`, `whatsapp-adapter`, `streaming-asr-sidecar`) — судя по всему, вытеснен Helm-чартом, но не удалён |
| `services/shared/ai_operator_config.py`, `voice_tts_config.py`, `ai_orchestrator_service/voice_name_config.py` | Три почти идентичных модуля "singleton JSON row в БД" — кандидат на общий `SingletonJSONConfigStore[T]` |
### 7.7 🟡 Инфраструктура/CI: следы миграций, которые не довели до конца
- **3 параллельных CI-системы** (GitHub Actions, GitLab CI, Gitea) — реально используется только GitLab CI (полный test→build→deploy, включая 5 deploy-джобов и live-патч прод-контейнера в обход образа — `hotfix-analyst-assets`). GitHub Actions не имеет деплоя вообще, Gitea вызывает непрозрачный внешний скрипт. Похоже на артефакт смены CI-провайдера.
- **"Track 9"** — конкретное историческое событие (переход на реальный Asterisk PBX на "scale500" в K8s + переключение auth с legacy-заголовков на bearer-токены). Скрипты `track9_*.py/.ps1` и Helm-оверлеи `values.track9-*.yaml` — это тулинг под один конкретный cutover, не переиспользуемые операционные скрипты; кандидаты на архивацию, если cutover завершён.
- Параллельно живёт "теневой" Postgres-стек (`docker-compose.parallel.server.yml`, отдельный env-темплейт) — явно инфраструктура идущей миграции SQLite → PostgreSQL, а не постоянная часть архитектуры.
- 5 файлов `.env*.template` с реальным дрейфом контента между `.env.example` и `.env.production.template` (не просто "прод — подмножество", а разные наборы переменных в обе стороны).
### 7.8 🟢 `asterisk_bridge_service` — архитектурный узел
Самый сложный сервис (5 790 строк суммарно по всем файлам). Каждый вынесенный в отдельный файл модуль (`ami.py`, `bridge_processing.py`, `voice_ai.py`, `call_control.py`, `ivr_fastagi.py`, `reconcile.py`, `recording_io.py`) на деле не независим — каждый определяет `_bridge_app()`, который лениво импортирует `app.py` и лезет обратно за конфигом/функциями/даже реэкспортами stdlib (`bridge.datetime`). Это не модульность, а один файл на 5 790 строк, искусственно нарезанный, с `app.py` в роли service-locator/mediator и реальным циклическим импортом, который обходят через ленивый импорт внутри функций. Резолв "какой live-канал сейчас соответствует этому звонку" реализован независимо 3 раза (`call_control.py` дважды, `voice_ai.py` один раз) вместо общей функции.
---
## 8. Расхождения с существующей документацией (`docs/architecture/*.md`)
- **overview.md** говорит "шина RabbitMQ — канонический механизм обмена" — верно как код, но не как поведение по умолчанию (шина выключена, см. §5).
- **voice-ai.md** описывает флоу верно на уровне намерения, но не упоминает, что голосовая ветка может полностью работать без обращения к LLM (rule-based fallback), и что часть "AI Summary" для оператора рендерится прямо в `asterisk_bridge_service/voice_ai.py` с захардкоженными русскими keyword-словарями, а не в `ai_orchestrator_service`, как можно было бы предположить по разделению ответственности из документа.
- **real-time-voice-service.md** — подтверждено ревью: `services/real_time_voice/app.py` действительно 15-строчный alias, без расхождений. Единственное уточнение: как самостоятельная директория сервиса это создаёт путаницу при первом знакомстве с деревом каталогов (выглядит как полноценный сервис).
- **event-schemas.md** перечисляет `call.connected.json`/`call.transferred.json` как часть контракта шины — по факту эти два события до шины не доходят вообще (см. §4.1). `call.recording.ready.json` доходит, но с несовпадающей схемой (`recording_path` vs `file_name`).
---
## 9. Резюме: что стоит делать в первую очередь
Если приоритизировать по риску/выгоде (без учёта уже сделанных в этой сессии небольших правок, см. отдельное сообщение с их списком):
1. **Безопасность** — выключить legacy header auth по умолчанию (или хотя бы явно предупредить/логировать при старте, что он включён), захэшировать пароли в `auth_service`.
2. **Контракты событий** — либо чинить `call.connected`/`call.transferred`/`call.recording.ready`, либо выпилить их из `contracts/events/`, чтобы документация не врала.
3. **Дублирование адаптеров каналов** — вынести общий `chat_adapter.py` для telegram/whatsapp (наибольший ROI по сокращению кода — полторы-две тысячи строк).
4. **`ui/analyst/app.js`** — вычистить задублированные функции (быстрая, низкорисковая правка, ощутимо уменьшает файл).
5. **`asterisk_bridge_service`** — если сервис ещё будет активно дорабатываться, стоит разорвать `_bridge_app()`-индирекцию через явный контекст/DI, иначе долг будет только расти вместе с файлом.
6. Архивировать/переименовать "track9"-скрипты и устаревший `deployment/kubernetes/mvp-cc-platform.yaml`, если Helm — единственный актуальный путь деплоя в K8s.