08

Реализация FreeSWITCH

Детальный план реализации FreeSWITCH (Слой 4: Media) — 8 фаз от базового звонка до WebRTC. Миграция с MirtaPBX (Asterisk). ~400 concurrent calls в EU, полный call-center.

Что такое FreeSWITCH

FreeSWITCH — программная телефонная станция

FreeSWITCH — это soft-switch (программный коммутатор). Если раньше телефонные станции были огромными шкафами с оборудованием, то FreeSWITCH — это то же самое, но в виде программы на Linux-сервере.

Представьте коммутатор из кинофильма — оператор втыкает провода, соединяя абонентов. FreeSWITCH делает то же самое, только программно и автоматически.

Open Source Написан на C Создан в 2006 Автор: Anthony Minessale (ex-Asterisk)

Ключевая концепция: B2BUA (Back-to-Back User Agent)

FreeSWITCH — это B2BUA. Он не просто пропускает звонок насквозь (как прокси). Он полностью разрывает звонок на две половинки:

  Телефон A              FreeSWITCH              Телефон B
      │                       │                       │
      │──── SIP Leg A ──────│                       │
      │   (отдельный звонок)  │                       │
      │                       │──── SIP Leg B ──────│
      │                       │   (отдельный звонок)  │
      │                       │                       │
      │◄═══ Голос (RTP) ════►│◄═══ Голос (RTP) ════►│

Leg A — звонок между телефоном A и FreeSWITCH.
Leg B — звонок между FreeSWITCH и телефоном B.

Это даёт полный контроль: между двумя ногами FreeSWITCH может делать что угодно — записывать, транскодировать, подмешивать аудио, ставить на удержание, переводить, подключать третьего участника.

Аналогия: Прокси vs B2BUA

Прокси (Kamailio) — это почтальон, который передаёт конверт не открывая. Быстро, массово, но не может изменить содержимое.
B2BUA (FreeSWITCH) — это секретарь, который читает письмо, может переписать, перенаправить, скопировать, добавить приложение.

Из чего состоит FreeSWITCH
  ┌──────────────────────────────────────────────────────────────┐
  │                      FreeSWITCH                              │
  │                                                              │
  │  ┌────────────────────────────────────────────────────────┐  │
  │  │                     ЯДРО (Core)                        │  │
  │  │                                                         │  │
  │  │  ┌──────────────┐ ┌──────────────┐ ┌────────────────┐  │  │
  │  │  │ State Machine│ │ Event System │ │Channel Engine │  │  │
  │  │  │ жизненный    │ │ все события  │ │ управление     │  │  │
  │  │  │ цикл канала  │ │ pub/sub      │ │ звонками       │  │  │
  │  │  └──────────────┘ └──────────────┘ │ Leg A ↔ Leg B  │  │  │
  │  │                                     └────────────────┘  │  │
  │  │  ┌──────────────┐ ┌──────────────┐ ┌────────────────┐  │  │
  │  │  │ Codec Engine │ │ Timer System │ │ Memory Pool   │  │  │
  │  │  │ перекодиров. │ │              │ │ управление     │  │  │
  │  │  │ Opus↔G.711   │ │              │ │ памятью        │  │  │
  │  │  └──────────────┘ └──────────────┘ └────────────────┘  │  │
  │  └───────────────────────┬────────────────────────────────┘  │
  │                          │ Module API                        │
  │  ┌───────────────────────┼────────────────────────────────┐  │
  │  │                МОДУЛИ (~200)                            │  │
  │  │                                                         │  │
  │  │  Endpoints       Applications      Codecs             │  │
  │  │  mod_sofia(SIP)  mod_dptools       mod_opus            │  │
  │  │  mod_verto(WS)   mod_callcenter    mod_g711            │  │
  │  │  mod_skinny       mod_conference    mod_g729            │  │
  │  │                   mod_voicemail     mod_g722            │  │
  │  │                                                         │  │
  │  │  Events          Languages         Formats            │  │
  │  │  mod_event_socket mod_lua           mod_shout(MP3)     │  │
  │  │  mod_json_cdr     mod_python        mod_sndfile(WAV)   │  │
  │  │                   mod_v8(JS)                            │  │
  │  │                                                         │  │
  │  │  Config          TTS / ASR                              │  │
  │  │  mod_xml_curl     mod_flite                             │  │
  │  │  mod_dialplan_xml mod_tts_commandline                   │  │
  │  │  mod_dialplan_lua                                       │  │
  │  └─────────────────────────────────────────────────────────┘  │
  │                                                              │
  │  ┌─────────────────────────────────────────────────────────┐  │
  │  │              КОНФИГУРАЦИЯ                               │  │
  │  │  Directory (кто)  Dialplan (куда)  SIP Profiles (как)  │  │
  │  └─────────────────────────────────────────────────────────┘  │
  └──────────────────────────────────────────────────────────────┘
Ядро (Core) — мозг FreeSWITCH

Написано на C. Это фундамент, который нельзя менять — на него нанизываются модули.

КомпонентЧто делает
Channel Engine Управляет звонками. Каждый звонок = «канал» (channel) с уникальным UUID. Канал проходит состояния: NEW → INIT → ROUTING → EXECUTE → EXCHANGE_MEDIA → HANGUP
Event System Внутренняя шина событий. Всё что происходит — звонок создан, DTMF нажата, запись начата — генерирует событие. ESL подписывается на эти события извне
State Machine Жизненный цикл каждого канала. Чёткие переходы между состояниями. Гарантирует что ресурсы освобождаются при завершении звонка
Codec Engine Перекодирование аудио на лету: Opus ↔ G.711 ↔ G.729. Если два телефона говорят на разных кодеках — ядро транскодирует прозрачно
Memory Pool Управление памятью. Каждый звонок получает свой пул — когда звонок завершается, вся память освобождается одним блоком (нет утечек)
🔌 Модули FreeSWITCH — подробная классификация

Endpoint-модули — как FreeSWITCH общается с внешним миром:

МодульПротоколЗачем
mod_sofiaSIP (UDP/TCP/TLS/WSS)Основной. 95% звонков идут через него
mod_vertoWebSocket JSON-RPCWebRTC из браузера (собственный протокол FS)
mod_skinnyCisco SCCPАппаратные Cisco-телефоны
mod_freetdmTDM / ISDNПодключение к аналоговым линиям (legacy)

Application-модули — что делать со звонком:

МодульЗачем
mod_dptoolsБазовые инструменты: answer, bridge, playback, record, transfer, hold, park, intercept
mod_callcenterОчереди (ACD): стратегии распределения, агенты, тиры, музыка ожидания
mod_conferenceКонференц-звонки: комнаты, PIN, mute/unmute, запись
mod_voicemailГолосовая почта: greeting, хранение, уведомления, MWI
mod_avmdОпределение автоответчика (Answering Machine Detection)
mod_fifoПростая парковка звонков (First In, First Out)

Codec-модули — как кодировать голос:

МодульКодекБитрейтКогда
mod_opusOpus6-510 kbps (адаптивный)WebRTC, внутренние звонки (лучшее качество)
встроенG.711 (PCMU/PCMA)64 kbpsPSTN-транки, базовая совместимость
mod_g729G.7298 kbpsЭкономия трафика (WAN, мобильные)
mod_g722G.72264 kbpsHD Voice (wideband)

Language-модули — скриптовые языки для dialplan:

МодульЯзыкДля чего
mod_luaLuaРекомендуем Быстрый, встроенный, IVR-логика, HTTP-запросы из dialplan
mod_pythonPythonСложная логика, ML/AI интеграции
mod_v8JavaScript (V8)Для тех кто знает JS

Event/CDR-модули — связь с внешним миром:

МодульЗачем
mod_event_socketESL — Go подключается и управляет FS программно (порт 8021)
mod_json_cdrОтправляет CDR (детализацию) по HTTP в Go API после каждого звонка
mod_xml_curlFS запрашивает конфигурацию по HTTP у Go API (динамические пользователи, dialplan, очереди)
Три раздела конфигурации
РазделЧто определяетАналогияИсточник
Directory Кто может звонить. Список SIP-аккаунтов с паролями и настройками Телефонная книга компании XML или mod_xml_curl (из БД)
Dialplan Куда маршрутизировать звонок. Правила: «если набрали 1XXX → bridge к user/1XXX» Правила коммутатора: «этот провод соединить с тем» XML, Lua или mod_xml_curl
SIP Profiles На каких IP/портах слушать SIP, какие кодеки, TLS, NAT Какие «двери» открыты для звонков XML (статический, меняется редко)
Жизненный цикл звонка в FreeSWITCH
  Телефон A набирает 1002
         │
         ▼
  [1] CHANNEL_CREATE   ← FS создаёт канал (Leg A), UUID: abc-123
         │
         ▼
  [2] ROUTING          ← Ищет в Dialplan: что делать с номером 1002?
         │                 Нашёл: bridge user/1002[3] CHANNEL_CREATE  ← Создаёт второй канал (Leg B), UUID: def-456
         │                 Звонит на телефон 1002
         ▼
  [4] RINGING         ← Телефон 1002 звонит (180 Ringing)
         │
         ▼
  [5] CHANNEL_ANSWER  ← Телефон 1002 поднят (200 OK)
         │
         ▼
  [6] EXCHANGE_MEDIA  ← Голос идёт: A → FS → B и B → FS → A
         │                 Здесь: запись, транскодирование, DTMF
         │
         │  ... разговор ...
         │
         ▼
  [7] CHANNEL_HANGUP  ← Кто-то повесил трубку (BYE)
         │
         ▼
  [8] CDR              ← Запись о звонке → Go API → PostgreSQL
         │
         ▼
  [9] DESTROY          ← Каналы уничтожены, память освобождена
FreeSWITCH ДЕЛАЕТ vs НЕ ДЕЛАЕТ
FreeSWITCH ДЕЛАЕТFreeSWITCH НЕ ДЕЛАЕТ (это другие компоненты)
Принимает/делает SIP-звонкиМаршрутизация тысяч SIP-запросов → Kamailio
IVR, очереди, конференцииВеб-интерфейс → React
Запись звонковREST API для пользователей → Go API
Транскодирование кодековХранение данных → PostgreSQL / ClickHouse
DTMF-обработкаБалансировка нагрузки между серверами → Kamailio
Голосовая почта, TTS, ASRNAT traversal для медиа → RTPEngine
Мост между двумя абонентамиAnti-DDoS, topology hiding → SBC (Kamailio)
Автообзвон (по команде из Go)Бизнес-логика, кампании → Go API
Разделение ответственности

FreeSWITCH = медиа-сервер. Он обрабатывает звонки: принимает, играет аудио, записывает, соединяет, ставит в очередь.
Kamailio = SIP-прокси. Он маршрутизирует SIP быстро и массово, но не трогает медиа (голос).

Вместе: Kamailio решает куда направить звонок, FreeSWITCH решает что с ним делать.

Способы конфигурации FreeSWITCH (XML, HTTP API, Lua, ESL)
СпособКак работаетДля чего
Статический XML Файлы на диске. Изменения → reloadxml в CLI SIP-профили, глобальные параметры — то, что меняется редко
mod_xml_curl Рекомендуем FS запрашивает конфигурацию по HTTP у Go API. Go отвечает XML из PostgreSQL Пользователи, очереди, dialplan — всё динамическое, из БД
mod_lua Lua-скрипт в dialplan вместо XML. Может ходить в Redis, HTTP API Сложная IVR-логика, маршрутизация по данным из БД
ESL (Event Socket) Go подключается к FS (порт 8021) и управляет звонками программно originate, transfer, hold, record — всё в реальном времени
mod_callcenter API ESL-команды для управления очередями и агентами без перезагрузки Добавить/удалить агента, сменить статус, создать очередь

Для enterprise-системы: XML остаётся только для статики (SIP-профили, модули, ESL). Всё остальное — динамически через mod_xml_curl + Go API + PostgreSQL.

  ┌──────────────────────────────────────────────┐
  │                 Go API                        │
  │                    │                          │
  │          PostgreSQL (users, queues,           │
  │          dialplan rules, IVR trees)           │
  └─────────────┬──────────────┬─────────────────┘
                │              │
    ┌───────────┼──────────────┼──────────────┐
    │           │              │              │
    ▼           ▼              ▼              │
  mod_xml_curl  mod_lua       ESL inbound    │
  «кто user?    dialplan     originate,      │
   какие        логика на    transfer,       │
   очереди?»    Lua + HTTP   uuid_kill...    │
    │           │              │              │
    └───────────┼──────────────┘              │
                │                             │
          FreeSWITCH                          │
                │                             │
          Статический XML:                    │
          • sip_profiles/internal.xml         │
          • modules.conf.xml                  │
          • event_socket.conf.xml             │
  ┌───────────────────────────────────────────┘
Контекст миграции

Откуда: MirtaPBX (на базе Asterisk) — полный call-center: IVR, очереди, запись, автообзвон, супервизор, WebRTC.
Куда: FreeSWITCH — в составе enterprise-архитектуры (Kamailio SBC → Kamailio Core → FreeSWITCH → Go API).
Почему: масштабирование (43 страны), полный контроль, стоимость лицензий.
Подход: «снизу вверх» — на каждой фазе работающая система, постепенное наращивание функционала.

Обзор: 8 фаз реализации
  ФАЗА 1          ФАЗА 2         ФАЗА 3         ФАЗА 4
  Базовый         ESL              IVR             Очереди
  звонок          управление       меню            ACD
  ┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
  │ mod_sofia│────▶│mod_event│────▶│ mod_lua │────▶│mod_call │
  │ dialplan │     │ _socket │     │ DTMF    │     │ center  │
  │ bridge   │     │ Go ← FS │     │ TTS     │     │ агенты  │
  └─────────┘     └─────────┘     └─────────┘     └─────────┘
       │               │               │               │
       ▼               ▼               ▼               ▼
  2 телефона      Go управляет    Входящие       Распределение
  звонят друг     звонками        попадают       по операторам
  другу           программно      в меню         автоматически

  ФАЗА 5          ФАЗА 6         ФАЗА 7         ФАЗА 8
  Запись           Конференции     Автообзвон      WebRTC
  + CDR            + Voicemail     (Dialer)        + Видео
  ┌─────────┐     ┌─────────┐     ┌─────────┐     ┌─────────┐
  │record_  │────▶│mod_conf │────▶│ Go ESL  │────▶│mod_verto│
  │ session │     │mod_voice│     │originate│     │ SIP.js  │
  │json_cdr │     │ mail    │     │ AMD     │     │ SRTP    │
  └─────────┘     └─────────┘     └─────────┘     └─────────┘
       │               │               │               │
       ▼               ▼               ▼               ▼
  Все звонки      Комнаты с       Обзвон          Браузер =
  записываются    PIN, голосовая  списков          телефон
  CDR → PostgreSQL почта          автоматически    оператора

Фаза 1: Установка + базовый звонок

Цель Два SIP-телефона звонят друг другу через FreeSWITCH

Самый минимум: FreeSWITCH запущен, два устройства зарегистрированы, можно позвонить. Это фундамент, на котором строится всё остальное.

Что делаем

1. Docker-образ FreeSWITCH

Собираем из исходников (Debian-based) — чтобы включить только нужные модули. Ванильный пакет содержит ~200 модулей, нам нужно ~30.

2. SIP-профили (mod_sofia)

internal (порт 5060) — для внутренних устройств (телефоны, веб-клиенты).
external (порт 5080) — для SIP-транков (пока пустой, пригодится в будущем).

3. User Directory (XML)

Регистрация SIP-аккаунтов: 1001, 1002 — с паролями, доменом, настройками кодеков.

4. Dialplan (XML)

Минимальный: набираем 1001 → bridge к устройству 1001. Набираем 1002 → bridge к 1002.

5. Кодеки

G.711 (PCMU/PCMA) — базовый, работает везде. Opus — для будущего WebRTC.

Ключевые модули
МодульЗачем
mod_sofiaSIP-стек FreeSWITCH. Регистрация, приём/отправка SIP
mod_dptoolsИнструменты dialplan: bridge, answer, hangup, playback
mod_dialplan_xmlПарсинг XML-диалплана
mod_commandsCLI-команды (sofia status, show channels)
mod_consoleКонсоль для отладки (fs_cli)
mod_logfileЛогирование в файл
💻 Пример конфигурации: SIP-профиль + User + Dialplan

SIP-профиль internal (sip_profiles/internal.xml):

<!-- Внутренний SIP-профиль для устройств --> <profile name="internal"> <settings> <param name="sip-ip" value="$${local_ip_v4}"/> <param name="sip-port" value="5060"/> <param name="rtp-ip" value="$${local_ip_v4}"/> <param name="codec-prefs" value="OPUS,PCMU,PCMA"/> <param name="inbound-codec-negotiation" value="generous"/> <param name="auth-calls" value="true"/> </settings> </profile>

User Directory (directory/default/1001.xml):

<user id="1001"> <params> <param name="password" value="$${default_password}"/> <param name="vm-password" value="1001"/> </params> <variables> <variable name="toll_allow" value="domestic,international,local"/> <variable name="accountcode" value="1001"/> <variable name="effective_caller_id_name" value="Extension 1001"/> <variable name="effective_caller_id_number" value="1001"/> </variables> </user>

Dialplan (dialplan/default.xml):

<extension name="internal_calls"> <!-- Любой 4-значный номер → bridge к этому устройству --> <condition field="destination_number" expression="^(10\d{2})$"> <action application="bridge" data="user/$1@$${domain}"/> </condition> </extension>
Проверка

Установите MicroSIP (Windows) или Orologio (Mac). Зарегистрируйте 1001 и 1002. С 1001 наберите 1002 — должен пойти звонок. Если слышите голос — Фаза 1 готова.

Фаза 2: ESL — программное управление

Цель Go-сервис подключается к FreeSWITCH и управляет звонками программно

ESL (Event Socket Library) — протокол FreeSWITCH для внешнего управления. Через него Go-backend может создавать звонки, слушать события, переводить, вешать трубку — всё программно, без изменения XML-конфигурации.

Два режима ESL

Inbound (основной) — Go подключается к FreeSWITCH на порт 8021. Может отправлять команды и подписываться на события. Один Go-сервис управляет всем FS.

Outbound — FreeSWITCH сам подключается к Go при входящем звонке. Каждый звонок = отдельное TCP-соединение. Используется для сложной логики dialplan на стороне Go.

Рекомендация Начинаем с Inbound — проще, Go всегда online, одно соединение.

Ключевые ESL-команды
КомандаЧто делает
originateИнициировать звонок программно (позвонить на номер)
uuid_bridgeСоединить два активных канала
uuid_transferПеревести звонок на другой номер/extension
uuid_killПовесить трубку на конкретном канале
uuid_holdПоставить на удержание
uuid_recordНачать/остановить запись
uuid_setvarУстановить переменную на канале
Ключевые события (Events)

Go подписывается на события FreeSWITCH и реагирует в реальном времени:

СобытиеКогдаЧто делаем в Go
CHANNEL_CREATEНовый звонок появилсяСоздаём запись в Redis (active call)
CHANNEL_ANSWERАбонент поднял трубкуОбновляем статус, начинаем таймер
CHANNEL_HANGUPЗвонок завершёнCDR → PostgreSQL, очищаем Redis
DTMFНажата клавишаОбработка IVR, перевод по DTMF
RECORD_START/STOPЗапись началась/кончиласьМетаданные записи → БД
CUSTOM callcenter::infoСобытие очередиОбновляем dashboard супервизора
💻 Пример: Go ESL-клиент (inbound)
// Go: подключение к FreeSWITCH через ESL (inbound) // Библиотека: github.com/perorin/goeslern или cgrates/fsock package main import ( "fmt" "log" esl "github.com/cgrates/fsock" ) func main() { // Подключаемся к FreeSWITCH ESL conn, err := esl.NewFSock( "127.0.0.1:8021", // FS ESL адрес "ClueCon", // пароль (дефолтный) 10, // reconnect attempts eventHandlers, // map[string][]func(string, int) eventFilters, // какие события слушать logger, ) // Инициируем звонок программно // Звоним на 1001, при ответе — bridge к 1002 reply, _ := conn.SendApiCmd( "originate user/1001 &bridge(user/1002)", ) fmt.Println("Originate result:", reply) // Слушаем события conn.ReadEvents() } // Обработчик событий var eventHandlers = map[string][]func(string, int){ "CHANNEL_ANSWER": {func(body string, _ int) { log.Printf("Звонок отвечен: %s", body) }}, "CHANNEL_HANGUP": {func(body string, _ int) { log.Printf("Звонок завершён: %s", body) }}, }
💻 Конфигурация ESL + Lua в FreeSWITCH

Event Socket (autoload_configs/event_socket.conf.xml):

<configuration name="event_socket.conf"> <settings> <param name="listen-ip" value="0.0.0.0"/> <param name="listen-port" value="8021"/> <param name="password" value="your_secure_password"/> <!-- ВАЖНО: в production — только внутренний IP, не 0.0.0.0 --> </settings> </configuration>

Lua-скрипт (scripts/incoming_call.lua) — пример сложной логики:

-- Lua-скрипт для обработки входящего звонка local caller = session:getVariable("caller_id_number") local destination = session:getVariable("destination_number") freeswitch.consoleLog("INFO", "Звонок от " .. caller .. " на " .. destination .. "\n") -- Проверяем в Redis — VIP-клиент? -- Если да → приоритетная очередь -- Если нет → обычная очередь session:answer() session:execute("playback", "ivr/welcome.wav") session:execute("transfer", "5000 XML default") -- → IVR
Проверка

Go-сервис запускается, подключается к FS по ESL, вызывает originate — телефон 1001 звонит. Go получает события CHANNEL_CREATE → CHANNEL_ANSWER → CHANNEL_HANGUP. Если это работает — Фаза 2 готова.

Фаза 3: IVR (Interactive Voice Response)

Цель Входящий звонок попадает в голосовое меню, клиент выбирает опцию по DTMF

IVR — это «голосовой робот», который отвечает: «Нажмите 1 для продаж, 2 для поддержки, 3 для бухгалтерии». В FreeSWITCH реализуется через Lua-скрипты + аудиофайлы + play_and_get_digits.

Что делаем

1. Аудиофайлы

Приветствие, пункты меню, ожидание, "Пожалуйста подождите", "Все операторы заняты". Формат: WAV 16kHz mono (для качества) или 8kHz (для совместимости).

2. IVR-дерево на Lua

Многоуровневое меню: Главное → Подменю → Действие. Вся логика в Lua-скриптах.

3. DTMF-обработка

RFC 2833 (основной), SIP INFO (fallback). play_and_get_digits — играет аудио и ждёт нажатия.

4. Таймауты и fallback

Не нажал ничего за 5 сек → повторяем. 3 неудачных попытки → перевод на оператора.

5. TTS (Text-to-Speech)

Для динамических фраз: "Ваш номер в очереди — пять". mod_flite (английский) или mod_tts_commandline + Piper (русский).

Типичное IVR-дерево
        Входящий звонок
              │
        «Здравствуйте!»
        «Нажмите 1, 2 или 3»
              │
     ┌────────┼────────┐
     │        │        │
   [1]      [2]      [3]
  Продажи  Поддержка Бухгалтерия
     │        │        │
     │     ┌──┼──┐     │
     │    [1]  [2]    │
     │   Тех. Возврат  │
     │   отдел         │
     ▼        ▼        ▼
  Очередь  Очередь  Extension
  sales    support   3001
💻 Пример: IVR на Lua
-- scripts/ivr_main.lua — Главное IVR-меню session:answer() session:sleep(500) local max_retries = 3 local retry = 0 while retry < max_retries do -- play_and_get_digits(min, max, tries, timeout, terminators, -- audio_file, bad_input_file, var_name, regex, digit_timeout) local digit = session:playAndGetDigits( 1, 1, 1, 5000, "#", "ivr/main_menu.wav", -- «Нажмите 1, 2 или 3» "ivr/invalid_choice.wav", -- «Неверный выбор» "ivr_choice", "[123]", -- допустимые цифры 3000 ) if digit == "1" then -- Продажи → очередь sales session:execute("transfer", "5001 XML default") return elseif digit == "2" then -- Поддержка → подменю session:execute("lua", "ivr_support.lua") return elseif digit == "3" then -- Бухгалтерия → extension напрямую session:execute("transfer", "3001 XML default") return else retry = retry + 1 end end -- 3 неудачных попытки → оператор session:execute("playback", "ivr/connecting_operator.wav") session:execute("transfer", "5000 XML default")
Модули Фазы 3
mod_dptools play_and_get_digits, playback, transfer   mod_lua Lua-скрипты для IVR-логики   mod_flite TTS (английский)   mod_tts_commandline TTS через внешнюю команду (Piper для русского)   mod_say_ru Озвучивание чисел и дат на русском
Проверка

Звоним на номер 5000 (IVR) → слышим приветствие → нажимаем 1 → попадаем в очередь продаж. Нажимаем 2 → подменю поддержки. Ничего не нажимаем → через 3 попытки переводят на оператора. Если всё работает — Фаза 3 готова.

Фаза 4: Очереди (ACD — Automatic Call Distribution)

Цель Звонки распределяются по операторам с учётом стратегии, навыков, загрузки

ACD — ядро call-центра. Клиент ждёт в очереди, система выбирает лучшего свободного оператора и соединяет. В FreeSWITCH — mod_callcenter.

Стратегии распределения
СтратегияКак работаетКогда использовать
longest-idle-agentЗвонок идёт оператору, который дольше всех свободенСамая справедливая. Рекомендуем
round-robinПо кругу: 1→2→3→1→2→3Равная загрузка
ring-allЗвонят всем одновременно, кто первый поднялМаленькие команды
top-downСначала первому, если не ответил → второмуПриоритетные агенты
agent-with-least-talk-timeКто меньше всего наговорил за сменуРавная нагрузка по минутам
Что делаем

Очереди: sales, support, billing — каждая со своей стратегией, MOH, таймаутами.

Агенты: статусы — Available, On Break, Logged Out. Тиры (уровни приоритета): tier 1 звонят первыми, если все в tier 1 заняты → tier 2.

Музыка ожидания (MOH): mod_local_stream — разные плейлисты для разных очередей.

Announcements: «Вы 3-й в очереди», «Примерное время ожидания — 2 минуты».

Callback: клиент нажимает * → оставляет номер → система перезвонит когда оператор освободится.

Overflow: очередь переполнена (>20 ждущих) → перевод в другую очередь / voicemail / IVR.

💻 Конфигурация mod_callcenter

Очередь (autoload_configs/callcenter.conf.xml):

<queue name="sales@default"> <param name="strategy" value="longest-idle-agent"/> <param name="moh-sound" value="local_stream://moh_sales"/> <param name="max-wait-time" value="300"/> <!-- 5 мин макс --> <param name="max-wait-time-with-no-agent" value="30"/> <!-- если агентов нет --> <param name="tier-rules-apply" value="true"/> <param name="tier-rule-wait-second" value="30"/> <!-- 30с → следующий tier --> <param name="announce-sound" value="ivr/queue_position.wav"/> <param name="announce-frequency" value="30"/> </queue>

Агенты:

<agent name="agent-1001" type="callback" contact="user/1001@default" status="Available" max-no-answer="3" wrap-up-time="10" reject-delay-time="15"/>

Тиры:

<tier agent="agent-1001" queue="sales@default" level="1" position="1"/> <tier agent="agent-1002" queue="sales@default" level="1" position="2"/> <tier agent="agent-2001" queue="sales@default" level="2" position="1"/> <!-- level 2 подключается если все level 1 заняты 30+ сек -->

ESL-события от очереди (Go получает в реальном времени):

// Go получает события mod_callcenter через ESL // Event: CUSTOM callcenter::info // CC-Action: member-queue-start ← клиент вошёл в очередь // CC-Action: agent-offering ← звоним оператору // CC-Action: bridge-agent-start ← разговор начался // CC-Action: bridge-agent-end ← разговор окончен // CC-Action: member-queue-end ← клиент покинул очередь
Проверка

Звоним на 5001 (очередь sales) → слышим музыку ожидания → «Вы 1-й в очереди» → телефон оператора 1001 звонит → оператор поднимает → разговор. Go получает все события через ESL. Если работает — Фаза 4 готова.

Фаза 5: Запись + CDR

Цель Все звонки записываются, CDR (детализация) сохраняются в PostgreSQL

Запись нужна для контроля качества, обучения, compliance (GDPR требует уведомлять). CDR — для аналитики: сколько звонков, средняя длительность, пропущенные, загрузка агентов.

Запись звонков

record_session — записывает весь звонок от начала до конца, включая переводы.

Двуканальная запись (stereo): оператор в левом канале, клиент в правом. Это критично для speech analytics — позволяет анализировать каждого отдельно.

Формат: WAV (для качества) → конвертация в MP3 (mod_shout) для хранения. Или сразу MP3.

Хранение: локальный диск → Go API загружает в MinIO (S3). Путь в PostgreSQL.

Условная запись: только очереди, только внешние, по расписанию, по согласию клиента.

GDPR: уведомление «Разговор записывается» (playback перед записью), автоудаление через N дней.

CDR (Call Detail Records)

mod_json_cdr — после каждого звонка FreeSWITCH отправляет JSON по HTTP POST в Go API.

Что содержит CDR:

caller_id кто звонил
destination куда звонил
start_time начало звонка
answer_time когда ответили
end_time конец звонка
duration общая длительность
billsec оплачиваемое время (от answer)
hangup_cause причина завершения
recording_path путь к записи
queue_name очередь
agent оператор

Go API → PostgreSQL → таблица cdr.

Модули Фазы 5
mod_dptools record_session   mod_json_cdr CDR в JSON по HTTP   mod_shout запись в MP3   mod_cdr_pg_csv альтернатива: CDR напрямую в PostgreSQL
Архитектура хранения CDR: PostgreSQL + ClickHouse + Elasticsearch

При 400+ concurrent calls CDR быстро растут до миллионов записей. PostgreSQL одна не справится с аналитическими запросами на таких объёмах. Решение — тройной стек: каждая БД делает то, в чём она лучшая.

Поток данных CDR
  FreeSWITCH
       │
       │ JSON CDR (HTTP POST после каждого звонка)
       ▼
  Go API ──────────────────────────────────────────────
       │                    │                        │
       │ sync (мгновенно)async (батчами)async (батчами)
       ▼                    ▼                        ▼
  ┌─────────────┐   ┌──────────────┐   ┌──────────────────┐
  │ PostgreSQL   │   │ ClickHouse    │   │ Elasticsearch     │
  │             │   │              │   │                  │
  │ «Горячие»   │   │ ВСЯ история  │   │ Полнотекстовый   │
  │ последние   │   │ за годы      │   │ поиск            │
  │ 7-30 дней   │   │              │   │                  │
  │             │   │ Аналитика:   │   │ «Найди звонок    │
  │ JOIN с      │   │ агрегации,   │   │  с +4917...»     │
  │ users,      │   │ отчёты,      │   │ «Все звонки      │
  │ queues,     │   │ дашборды     │   │  агента Иванов»  │
  │ agents      │   │              │   │ Fuzzy match      │
  │             │   │ Retention:   │   │                  │
  │ TTL: 30 дней│   │ 3-5 лет      │   │ TTL: 90 дней     │
  │ затем →     │   │ сжатие 5-10x │   │ (горячий индекс) │
  │ удаляется   │   │              │   │                  │
  └─────────────┘   └──────────────┘   └──────────────────┘
       │                    │                        │
       ▼                    ▼                        ▼
  Оперативная          Grafana /               Kibana /
  работа               BI-отчёты               поиск в UI
PostgreSQL — оперативные данные

Роль: основная БД для «горячих» данных последних 7-30 дней. Здесь хранятся текущие смены, активные агенты, незакрытые звонки.

Почему PG: нужны JOIN-ы с таблицами users, queues, agents, tenants. Транзакции, целостность данных. ACID.

Что хранит:

cdr — CDR за последние 30 дней
active_calls — текущие звонки (Redis лучше, но PG как backup)
agent_sessions — рабочие смены агентов
queue_stats — текущая статистика очередей

TTL: cron-задача или pg_partman — автоудаление записей старше 30 дней (они уже в ClickHouse).

Оптимизации:

• Партиционирование по дате (1 партиция = 1 день)
• Индексы: caller_id, callee, start_time, agent_id, queue_name
• BRIN-индексы на start_time (эффективно для диапазонов дат)

ClickHouse — аналитика и история

Роль: вся история CDR за годы. Аналитические запросы, отчёты, дашборды.

Почему ClickHouse: колоночная СУБД — запросы вида «средняя длительность по очередям за Q4 2025» выполняются за 0.1-0.5 сек на 100M+ записей (PostgreSQL: 15-30 сек).

Что хранит: полную копию всех CDR за 3-5 лет. Сжатие 5-10x (100M CDR ≈ 8-15 GB).

Типичные запросы:

• «Все звонки за март в Германии» — 0.2 сек
• «Средняя длительность по очередям за квартал» — 0.1 сек
• «Top-10 агентов по количеству звонков» — 0.05 сек
• «Почасовая нагрузка за последний год» — 0.3 сек
• «Стоимость звонков по странам за месяц» — 0.1 сек

Запись: Go отправляет CDR батчами (каждые 5 сек или 1000 записей) через HTTP-интерфейс ClickHouse.

Elasticsearch — поиск

Роль: мгновенный поиск CDR по любому полю. Супервизор вводит номер телефона — за 50мс видит все звонки.

Почему Elastic:

Full-text search — поиск по фрагменту номера: «+4917» → все звонки с номерами начинающимися на +4917
Fuzzy matching — ошибся на цифру? Всё равно найдёт
Агрегации в реальном времени — «сколько звонков за последний час по каждой очереди»
Kibana — готовые дашборды без написания кода

Что индексируем:

caller_id callee agent_name queue_name hangup_cause direction country tenant_id

TTL: ILM-политика — горячий индекс (7 дней, SSD) → тёплый (30 дней) → удаление (90 дней). Для долгосрочного хранения — ClickHouse.

Кто что делает — сравнение
ЗадачаКтоСкорость
Текущие звонки агентаPostgreSQL<10 мс
CDR с JOIN users/queuesPostgreSQL<50 мс (30 дней)
«Все звонки за Q4 по Германии»ClickHouse0.1-0.5 сек
«Среднее время ожидания за год»ClickHouse0.05-0.2 сек
Ежемесячный отчёт (PDF)ClickHouse1-3 сек
«Найди звонок с +4917612...»Elasticsearch<50 мс
«Все звонки агента Иванов»Elasticsearch<50 мс
Kibana дашборд (live)Elasticsearchreal-time
💻 Go API: запись CDR в три хранилища
// Go API — обработка CDR от FreeSWITCH // FreeSWITCH отправляет JSON POST на /api/v1/cdr func (h *CDRHandler) HandleCDR(w http.ResponseWriter, r *http.Request) { var cdr CDRRecord json.NewDecoder(r.Body).Decode(&cdr) // 1. PostgreSQL — синхронно (основная запись) h.pgRepo.InsertCDR(ctx, &cdr) // 2. ClickHouse — асинхронно (батчами) h.chBuffer.Add(cdr) // буфер, сбрасывается каждые 5 сек или 1000 записей // 3. Elasticsearch — асинхронно (батчами) h.esBuffer.Add(cdr) // bulk index API w.WriteHeader(http.StatusOK) } // ClickHouse buffer — flush каждые 5 сек func (b *CHBuffer) flushLoop() { ticker := time.NewTicker(5 * time.Second) for range ticker.C { if batch := b.Drain(); len(batch) > 0 { // INSERT INTO cdr VALUES (...), (...), (...) b.ch.ExecBatch(ctx, "INSERT INTO cdr", batch) } } }

ClickHouse — схема таблицы CDR:

CREATE TABLE cdr ( call_id String, start_time DateTime, answer_time Nullable(DateTime), end_time DateTime, duration UInt32, billsec UInt32, caller_id String, callee String, direction Enum8('inbound'=1, 'outbound'=2, 'internal'=3), hangup_cause String, queue_name String, agent_id String, tenant_id String, country LowCardinality(String), recording_url String, cost Decimal64(4) ) ENGINE = MergeTree() PARTITION BY toYYYYMM(start_time) -- партиция по месяцу ORDER BY (tenant_id, start_time) -- кластерный индекс TTL start_time + INTERVAL 5 YEAR -- автоудаление через 5 лет SETTINGS index_granularity = 8192;

Elasticsearch — индекс CDR:

// Elasticsearch index mapping { "mappings": { "properties": { "call_id": { "type": "keyword" }, "caller_id": { "type": "keyword", "fields": { "search": { "type": "search_as_you_type" }}}, "callee": { "type": "keyword", "fields": { "search": { "type": "search_as_you_type" }}}, "agent_name": { "type": "text" }, "queue_name": { "type": "keyword" }, "start_time": { "type": "date" }, "duration": { "type": "integer" }, "country": { "type": "keyword" }, "hangup_cause": { "type": "keyword" }, "tenant_id": { "type": "keyword" } } } }
📈 Примеры запросов: PostgreSQL vs ClickHouse vs Elasticsearch

«Текущие звонки агента 1001» → PostgreSQL (JOIN с users)

SELECT c.*, u.name AS agent_name FROM cdr c JOIN users u ON c.agent_id = u.id WHERE c.agent_id = '1001' AND c.start_time > now() - INTERVAL '1 day' ORDER BY c.start_time DESC; -- 5-10 мс на 30 дней данных

«Среднее время ожидания по очередям за Q4 2025» → ClickHouse

SELECT queue_name, avg(answer_time - start_time) AS avg_wait_sec, count() AS total_calls, countIf(hangup_cause = 'ORIGINATOR_CANCEL') AS abandoned FROM cdr WHERE start_time BETWEEN '2025-10-01' AND '2025-12-31' AND direction = 'inbound' GROUP BY queue_name ORDER BY avg_wait_sec DESC; -- 0.1 сек на 50M записей

«Стоимость звонков по странам за январь» → ClickHouse

SELECT country, count() AS calls, sum(billsec) / 60 AS total_minutes, sum(cost) AS total_cost, avg(cost) AS avg_cost_per_call FROM cdr WHERE toYYYYMM(start_time) = 202601 AND direction = 'outbound' GROUP BY country ORDER BY total_cost DESC; -- 0.2 сек на 100M+ записей

«Найди все звонки с номера +4917612...» → Elasticsearch

// Elasticsearch query — мгновенный поиск по фрагменту номера { "query": { "multi_match": { "query": "+4917612", "fields": ["caller_id.search", "callee.search"], "type": "bool_prefix" } }, "sort": [{ "start_time": "desc" }], "size": 50 } // 30-50 мс, результаты по мере ввода (search-as-you-type)
Порядок внедрения CDR-стека

Шаг 1: Начинаем только с PostgreSQL (Фаза 5 базовая). Это работает сразу.
Шаг 2: Когда CDR перевалят за 1-5M записей и запросы начнут тормозить — добавляем ClickHouse. Go пишет в оба хранилища.
Шаг 3: Когда супервизорам понадобится мгновенный поиск и Kibana-дашборды — добавляем Elasticsearch.

Не нужно внедрять всё сразу. Каждый компонент добавляется когда появляется реальная потребность.

Фаза 6: Конференции + Voicemail

Цель Конференц-звонки с PIN-кодом и голосовая почта

Конференции — для совещаний (несколько участников в одном звонке). Voicemail — когда оператор не ответил, клиент оставляет голосовое сообщение.

mod_conference

Создание комнат: набираем 8XXX → попадаем в конференцию 8XXX. Динамическое создание.

PIN-код: при входе вводится PIN для авторизации.

Управление: mute/unmute по DTMF (0 — mute себя), kick через ESL, запись конференции.

ESL: conference 8001 list — список участников. conference 8001 mute {member_id} — замутить.

Лимиты: максимум участников, автоокончание если остался 1.

mod_voicemail

Голосовая почта: если оператор не ответил за 20 секунд → «Оставьте сообщение после сигнала».

Greeting: персональное приветствие (запись своего) или стандартное.

Прослушивание: набираем *97 → вводим PIN → слушаем сообщения, удаляем, сохраняем.

Уведомление: email (SMTP) или webhook в Go API при новом сообщении.

MWI: индикатор на SIP-телефоне — мигающая лампочка «новое сообщение».

Модули Фазы 6
mod_conference конференц-звонки   mod_voicemail голосовая почта   mod_smtp email-уведомления

Фаза 7: Автообзвон (Dialer)

Цель Система автоматически обзванивает список номеров и соединяет с операторами

Вся логика дайлера — в Go, не в FreeSWITCH. FS только делает звонки по команде через ESL. Go управляет кампаниями, списками, стратегиями, пейсингом.

Три режима автообзвона
РежимКак работаетКогдаRatio
Preview Оператор видит карточку клиента на экране → сам нажимает «Позвонить» → Go отправляет ESL originate Сложные продажи, VIP-клиенты. Оператор готовится к звонку 1:1
Progressive Go ждёт свободного агента → originate на следующий номер из списка → при ответе bridge к агенту Стандартный исходящий обзвон. Агент не простаивает, но и не перегружен 1:1
Predictive Go оценивает average handle time + answer rate → originate заранее на N номеров одновременно → когда агент освободится, звонок уже ждёт Массовый обзвон. Максимальная эффективность, но есть риск «брошенных звонков» (клиент поднял, а агент ещё не освободился) 1.2–2:1
Логика Go-дайлера

Campaign: название, список номеров, режим, расписание, Caller ID, trunk.

Pacing: сколько одновременных originate. Predictive подстраивает автоматически.

Retry: не ответил → повторить через 30 мин, максимум 3 попытки.

Blacklist / DNC: проверка номера перед звонком (Do Not Call).

Расписание: звоним только 9:00–18:00 по часовому поясу клиента.

Статистика: answer rate, average handle time, abandonment rate → dashboard.

AMD (Answering Machine Detection)

mod_avmd — определяет, ответил человек или автоответчик.

Если автоответчик → оставляем голосовое сообщение (или вешаем трубку).

Если человек → bridge к оператору.

Точность: ~85-90%. Ложное срабатывание = потерянный клиент, поэтому лучше ошибиться в сторону «человек».

Альтернатива: не использовать AMD, а сразу bridge к агенту — агент сам разберётся. Проще, но агент тратит время на автоответчики.

Модули Фазы 7
mod_avmd Answering Machine Detection   ESL originate Go инициирует звонки   Вся логика в Go кампании, пейсинг, retry, DNC, расписание

Фаза 8: WebRTC + Видео

Цель Операторы работают через веб-телефон в браузере, видеозвонки

Финальная фаза — браузер становится полноценным телефоном. Оператору не нужен софтфон или аппаратный телефон — всё в веб-интерфейсе.

Два варианта WebRTC
Вариант A: mod_vertoВариант B: SIP over WSS (через Kamailio)
Как Собственный протокол FreeSWITCH — JSON-RPC поверх WebSocket. Клиент: verto.js Стандартный SIP через WebSocket (RFC 7118). Kamailio SBC терминирует WSS → UDP к FS. Клиент: SIP.js или JsSIP
Плюсы Проще настроить. Прямое подключение к FS. Больше контроля Стандартный протокол. Не привязан к FS. Kamailio SBC уже в архитектуре
Минусы Привязка к FreeSWITCH. Нестандартный протокол Сложнее: Kamailio WSS + RTPEngine + SRTP. Больше компонентов
Для нас Быстрый старт, proof of concept Production — SBC уже есть, стандартный SIP, масштабируемость

Рекомендация Начать с mod_verto (быстрый proof of concept), потом перейти на SIP over WSS через Kamailio для production.

Что делаем

1. mod_verto — включаем, настраиваем WSS (порт 8082, TLS обязателен для браузеров).

2. Кодеки — Opus (голос, обязателен для WebRTC), VP8/VP9 (видео).

3. SRTP — обязательно для WebRTC (DTLS-SRTP). Браузеры не поддерживают обычный RTP.

4. React-интеграция — вебтелефон встроен в панель оператора: кнопки звонок/hold/transfer/mute, номеронабиратель, статус.

5. Видео — mod_conference с видео (VP8), screen sharing через extended SDP.

Модули Фазы 8
mod_verto WebSocket JSON-RPC   mod_opus Opus-кодек (голос)   mod_vp8 VP8-кодек (видео)   mod_sofia SIP over WSS (Вариант B)   SIP.js клиентская SIP-библиотека (Вариант B)
Проверка

Открываем React-панель → нажимаем «Позвонить» на 1002 → браузер использует микрофон → звонок проходит → голос слышен в обе стороны. Если работает — все 8 фаз FreeSWITCH реализованы.

Сводка: все модули FreeSWITCH по фазам
ФазаМодулиРезультат
1mod_sofia, mod_dptools, mod_dialplan_xml, mod_commandsДва телефона звонят друг другу
2mod_event_socket, mod_lua, mod_json_cdrGo управляет звонками через ESL
3mod_lua, mod_flite, mod_tts_commandline, mod_say_ruВходящие попадают в голосовое меню
4mod_callcenter, mod_local_streamЗвонки распределяются по операторам
5mod_dptools (record_session), mod_json_cdr, mod_shoutЗапись + CDR в PostgreSQL
6mod_conference, mod_voicemailКонференции и голосовая почта
7mod_avmd + Go ESLАвтообзвон списков
8mod_verto / mod_sofia (WSS), mod_opus, mod_vp8Браузер = телефон
Каждая фаза = работающая система

После Фазы 1 — уже можно звонить. После Фазы 4 — уже полноценный call-center. После Фазы 8 — полный паритет с MirtaPBX (и больше). Миграцию пользователей можно начинать уже после Фазы 4-5.