37 KiB
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. - Схема управляется тремя параллельными механизмами одновременно, которые надо вручную синхронизировать при любом изменении:
- SQLAlchemy ORM-модели (
services/shared/sql_models.py,sales_sql_models.py) — единыйBaseна все "микросервисы". migrations/sql/*_{postgres,sqlite}.sql— 31 миграция × 2 диалекта = 62 файла, написанные вручную (не codegen), почти механический перевод друг друга.services/shared/sql_init.py::_apply_runtime_schema_compatibility()— ~700-строчный императивный шимALTER TABLE ... ADD COLUMN IF NOT EXISTS, который выполняется при каждом старте каждого сервиса, чтобы "дотянуть" SQLite-схему до актуального состояния ORM.
- SQLAlchemy 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 или человек)
- Оператор связи → Asterisk (
telecom-kztrunk) → dialplan[from-telecom]жёстко маршрутизирует всё на extension7100(AI-first — это зашито в dialplan, а не конфигурируется). [mvpcc-call-init]: генерируетсяCALL_ID/LINKED_ID, стартует запись (MixMonitor), летит AMIUserEvent(MVPCCCallStarted).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).- Так как очередь сконфигурирована как
ai_first, бридж стартует Voice AI сессию (POST .../internal/voice-ai/sessionsвai_voice_runtime_service), Asterisk запускаетAudioSocket()— сырой TCP-канал аудио к рантайму. - Внутри
ai_voice_runtime_service/ai_orchestrator_serviceидёт разговор (ASR → LLM-решение → TTS), подробности в §6. - Если AI решает передать на человека — вызывает бридж (
POST /internal/voice-ai/calls/{id}/handoff), тот резолвит текущий live-канал (нетривиальная эвристика — Asterisk меняетUniqueIDна каждом хопеDial/Redirect), ставитMVPCC_*channel vars и делает AMIRedirectв[mvpcc-transfer]. - На пикапе оператором —
MVPCCOperatorConnected→process_operator_connected:AsteriskCallLinkRowпереходит вhuman_owned, в таймлайн обращения падает запись "AI запросил перевод на специалиста". - Хэнгап →
h-extension:StopMixMonitor+MVPCCCallEnded/MVPCCRecordingReady→ бридж скачивает файл (локально или по SFTP) и загружает вrecording_service, который публикуетcall.recording.readyв реальную шину событий. - Если что-то из 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 → создание обращения
POST /integrations/telegram/bot/webhook(защита — только статичный shared-secret заголовок, без HMAC, в отличие от WhatsApp).- Парсинг апдейта → дедуп по
external_message_id(на случай повторной доставки вебхука). _upsert_thread_for_inbound: резолвит/создаёт клиента черезCustomerExternalIdentity(telegram:{user_id}), находит/создаётTelegramThreadRow; если нет открытого треда — создаётInteraction(channel="telegram")+ таймлайн.- Два побочных эффекта:
_sync_sales_thread()(синхронизация с CRM — только у Telegram, у WhatsApp такой синхронизации нет, необъяснённая асимметрия) и_maybe_enqueue_ai_for_thread()(если тред не забрал человек — проситai_orchestrator_serviceсгенерировать ответ). - Исходящая доставка ответа идёт через отдельный фоновый 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, а сырой
httpxPOST на{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/— ~1500–1800 строк.- 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_pathvsfile_name).
9. Резюме: что стоит делать в первую очередь
Если приоритизировать по риску/выгоде (без учёта уже сделанных в этой сессии небольших правок, см. отдельное сообщение с их списком):
- Безопасность — выключить legacy header auth по умолчанию (или хотя бы явно предупредить/логировать при старте, что он включён), захэшировать пароли в
auth_service. - Контракты событий — либо чинить
call.connected/call.transferred/call.recording.ready, либо выпилить их изcontracts/events/, чтобы документация не врала. - Дублирование адаптеров каналов — вынести общий
chat_adapter.pyдля telegram/whatsapp (наибольший ROI по сокращению кода — полторы-две тысячи строк). ui/analyst/app.js— вычистить задублированные функции (быстрая, низкорисковая правка, ощутимо уменьшает файл).asterisk_bridge_service— если сервис ещё будет активно дорабатываться, стоит разорвать_bridge_app()-индирекцию через явный контекст/DI, иначе долг будет только расти вместе с файлом.- Архивировать/переименовать "track9"-скрипты и устаревший
deployment/kubernetes/mvp-cc-platform.yaml, если Helm — единственный актуальный путь деплоя в K8s.