Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
Show all changes
20 commits
Select commit Hold shift + click to select a range
89b4565
Корректное разрешение CALL_ID в batch-запросах finish/attachRecord/sh…
Apr 30, 2026
8027984
Исправление маршрутизации звонков на voicemail и пользователей без UF…
Apr 30, 2026
0bae398
Защита от OOM в HTTP-воркере и настраиваемый уровень логирования
May 4, 2026
c13a034
Регистрация входящих, не дошедших до оператора, на ответственного за …
May 5, 2026
e99ccb5
Не применять orphan-логику к voicemail-звонкам
May 5, 2026
b536b66
Корректный ответственный звонка после перевода (регрессия с 1.272)
May 6, 2026
6a746ed
Хотфикс: исчезающие звонки в B24 после 1.273
May 7, 2026
d2f5164
Импорт истории звонков из ModuleMtsPbx через batch register+finish
May 12, 2026
09b0111
Фикс: orphan-finish от IVR/queue-leg перетирал статус отвеченного звонка
May 12, 2026
1cb207c
AMI-воркер: устранение цикла рестартов + фикс MtsImporter Settings un…
May 12, 2026
96d6696
MtsImporter: pending-guard срабатывает безусловно
May 12, 2026
3763a07
Сохранение настроек: убраны ложный «Fail save externalLines» и опасно…
May 12, 2026
0a31dec
UI-сохранение: import_mts_calls и mts_import_last_id обрабатываются к…
May 12, 2026
d45de80
inner_numbers: устойчивость к разовому сбою user.get + дифф/контекст …
May 13, 2026
4d0520e
Перевод: возврат per-leg CALL_ID для отдельной карточки звонка на плечо
May 13, 2026
dc895a1
per-leg finish: защита от пустого ARGS_REGISTER (дедуп register)
May 13, 2026
a36b82a
finish-фильтр в B24: только ответившие плечи + дедуп пропущенного по …
May 13, 2026
35977fa
queue/transfer UI: hide остальным при ответе + register только при пе…
May 13, 2026
25a99cc
register/show: пропуск плеч без B24-пользователя
May 13, 2026
0fd46dc
MTS import: прикрепление записи при пустом mts_rec_status
May 14, 2026
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
27 changes: 21 additions & 6 deletions App/Controllers/ModuleBitrix24IntegrationController.php
Original file line number Diff line number Diff line change
Expand Up @@ -202,6 +202,9 @@ public function indexAction(): void
}

$this->view->form = new ModuleBitrix24IntegrationForm($settings, $options);
// Опция импорта МТС доступна только если установлен соседний модуль ModuleMtsPbx.
// Используем FQCN-строку: при `use ...` PHP попытается зарезолвить класс ещё до class_exists.
$this->view->isMtsModuleInstalled = class_exists('\\Modules\\ModuleMtsPbx\\Models\\CallHistory');
$this->view->pick("{$this->moduleDir}/App/Views/index");
}

Expand Down Expand Up @@ -323,6 +326,12 @@ public function saveAction(): void
case 'lastCompanyId':
case 'lastLeadId':
case 'lastDealId':
case 'mts_import_last_id':
// Служебные/системные поля, не отображаются в форме.
// НЕ трогаем — без break-ветки default обнулил бы их
// на каждом сохранении настроек ('' → INTEGER → 0).
// Для mts_import_last_id это бы сбрасывало прогресс
// MTS-импорта в 0 при каждом сохранении через UI.
break;
case 'callbackQueue':
$record->$key = trim($data[$key]);
Expand All @@ -338,6 +347,11 @@ public function saveAction(): void
case 'crmCreateLead':
case 'backgroundUpload':
case 'export_records':
case 'import_mts_calls':
// Checkbox-поля: HTML отправляет 'on' если установлен,
// не отправляет вовсе если снят. Раньше import_mts_calls
// попадал в default → $record->key = 'on' → SQLite-affinity
// INTEGER приводил к 0, и галка никогда не сохранялась как 1.
if (array_key_exists($key, $data)) {
$record->$key = ($data[$key] === 'on') ? '1' : '0';
} else {
Expand Down Expand Up @@ -366,12 +380,13 @@ public function saveAction(): void
return;
}
$externalLinesPost = json_decode($data['externalLines'],true);
$resultSaveLines = ConnectorDb::invokePriority(ConnectorDb::FUNC_SAVE_EXTERNAL_LINES, [$externalLinesPost]);
if(!$resultSaveLines){
$this->view->error = 'Fail save externalLines...';
$this->view->success = false;
return;
}
// Результат saveExternalLinesData(bool) теряется при RPC-сериализации:
// unpackResult всегда возвращает array, json_decode(true|false) → bool
// → is_array(bool) === false → возврат `[]`. Старая проверка
// `if(!$resultSaveLines)` ложно срабатывала на каждом сохранении,
// показывая «Fail save externalLines...» даже при успешной записи.
// Полагаемся на лог ConnectorDb для диагностики реальных ошибок.
ConnectorDb::invokePriority(ConnectorDb::FUNC_SAVE_EXTERNAL_LINES, [$externalLinesPost]);
$this->flash->success($this->translation->_('ms_SuccessfulSaved'));
$this->view->success = true;
}
Expand Down
12 changes: 12 additions & 0 deletions App/Forms/ModuleBitrix24IntegrationForm.php
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,7 @@ public function initialize($entity = null, $options = null): void
$this->addCheckBox('backgroundUpload', intval($entity->backgroundUpload) === 1);
$this->addCheckBox('export_records', intval($entity->export_records) === 1);
$this->addCheckBox('use_interception', intval($entity->use_interception) === 1);
$this->addCheckBox('import_mts_calls', intval($entity->import_mts_calls) === 1);

// Numeric
$this->add(new Numeric('interception_call_duration'));
Expand Down Expand Up @@ -99,6 +100,17 @@ public function initialize($entity = null, $options = null): void
'useEmpty' => false,
'class' => 'ui selection dropdown b24_regions-select',
]));

$logLevels = [];
foreach (ModuleBitrix24Integration::getAvailableLogLevels() as $level) {
$logLevels[$level] = $this->translation->_('mod_b24_i_logLevel_' . $level);
}
$this->add(new Select('logLevel', $logLevels, [
'using' => ['id', 'name'],
'value' => empty($entity->logLevel) ? ModuleBitrix24Integration::LOG_LEVEL_INFO : $entity->logLevel,
'useEmpty' => false,
'class' => 'ui selection dropdown',
]));
}
/**
* Adds a checkbox to the form field with the given name.
Expand Down
13 changes: 13 additions & 0 deletions App/Views/index.volt
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,19 @@
<label>{{ t._('mod_b24_i_backgroundUpload') }}</label>
</div>
</div>
{% if isMtsModuleInstalled %}
<div class="field">
<div class="ui toggle checkbox">
{{ form.render('import_mts_calls') }}
<label>{{ t._('mod_b24_i_ImportMtsCalls') }}</label>
</div>
</div>
{% endif %}
<div class="ten wide field">
<label>{{ t._('mod_b24_i_logLevel') }}</label>
{{ form.render('logLevel') }}
<div class="ui small text">{{ t._('mod_b24_i_logLevelHint') }}</div>
</div>
<div class="ui divider"></div>
<div class="field">
<button class="ui basic button disability" id="sync-links-button" type="button">
Expand Down
51 changes: 50 additions & 1 deletion CLAUDE.md
Original file line number Diff line number Diff line change
Expand Up @@ -105,7 +105,56 @@ GET /pbxcore/api/bitrix-integration/workers/state (без авторизации
- `telephony.externalcall.show` — отдельный вызов для открытия карточки остальным участникам очереди
- `open_card_mode`: `DIRECTLY` (сразу), `ANSWERED` (при ответе), `NONE` (никогда) — настройка per-user в `ModuleBitrix24Users`

### Register policy: one register per call

`addDataToQueue` (HTTP-воркер): первый `telephonyExternalCallRegister` для `linkedid` → реальный `telephony.externalcall.register` в B24 (создаёт CALL_ID). Последующие события того же linkedid:
- очередь (нет маркера `b24-answered-<linkedid>`) → `telephony.externalcall.show` на оригинальном CALL_ID;
- перевод (маркер стоит) → новый register, кладётся в `ARGS_REGISTER_<UNIQUEID>` и уходит только при finish'е этого плеча (per-leg CALL_ID).

При отладке: «много register» в `IntegrationAMI.log` ≠ много REST-запросов в B24. Считать REST по REQUEST'ам в `HttpConnection.log`.

### Two sources of "internal number → user" mapping

- **`inner_numbers`** — карта B24 user.get (`UF_PHONE_INNER` → `{ID, NAME, EMAIL, ...}`). Источник USER_ID для register/finish/show/hide. Обновляется `b24GetPhones` раз в 5 мин на ping AMI. Защита от полного обнуления есть, от частичного пропуска сотрудника — нет (см. `inner_numbers_diff` в `Bitrix24Integration.log`).
- **`usersSettingsB24`** — таблица модуля (`ModuleBitrix24Users`): `inner_phone` → `{user_id: MikoPBX-id, open_card_mode}`. Источник режима открытия карточки.
- Расхождение возможно: модуль знает номер (есть в `usersSettingsB24`), а в B24 этому номеру не присвоен `UF_PHONE_INNER`/`PERSONAL_MOBILE`/`WORK_PHONE` → `inner_numbers[$num]` пуст → USER_ID="" в register. AMI-воркер `actionCreateCdr` фильтрует такие плечи: `Skip register` (`WorkerBitrix24IntegrationAMI.php:518`). В `addDataToQueue` есть зеркальная страховка: `show` не отправляется с пустым USER_ID.

### Orphan-leg фильтр в HTTP-воркере

Для входящих звонков AMI-воркер шлёт `telephonyExternalCallFinish` на каждое CDR-плечо. Плечи, не дошедшие до оператора (IVR/queue), помечаются `USER_ID=responsibleMissedCalls`, `GLOBAL_STATUS=NOANSWER`. Эти эвенты могут опередить ANSWERED-finish реального оператора и переписать статус звонка на «пропущен».

HTTP-воркер фильтрует их: при `action_dial_answer` ставит маркер `b24-answered-<linkedid>` в Redis (TTL 3 ч, через `CacheManager`/`b24->saveCache` — переживает рестарт воркера). При обработке `telephonyExternalCallFinish` с `GLOBAL_STATUS!=ANSWERED`, если маркер стоит — эвент пропускается. Полностью пропущенные звонки (никто не ответил) идут штатно — маркера нет.

## MTS Historical Import

Опциональный импорт CDR из соседнего модуля `ModuleMtsPbx` (таблица `mts_cdr`). Cron `bin/MtsImporter.php` каждые 5 минут читает `Modules\ModuleMtsPbx\Models\CallHistory` через ORM (без HTTP к соседу), шлёт пачки по 10 звонков через `Bitrix24InvokeRest::invoke('importHistoricalCalls', ...)`. HTTP-воркер кладёт `register+finish` в общую `q_req` — звонки уходят обычным batch'ем как AMI-эвенты.

Почему mts_cdr, а не общая `cdr_general` (cdr.db): cdr_general — большая Asterisk-таблица без индекса на `from_account`, на крупных инсталляциях первичный скан медленный. `mts_cdr` — узкая таблица соседнего модуля, заполняемая `ModuleMtsPbx/bin/synchCdr.php` через MTS API, содержит только fs-mts звонки и MTS-API метаданные (включая `mts_rec_status` для отслеживания скачивания MP3).

- Опция: `import_mts_calls` в `ModuleBitrix24Integration` (галка в админке, видна только при установленном ModuleMtsPbx).
- Курсор: `mts_import_last_id` (в той же таблице, добавлен в `$syncKeys` в `Bitrix24IntegrationConf::modelsEventChangeData` — иначе апдейты курсора триггерили бы `onAfterModuleEnable`).
- Pending-логика: `mts_rec_status='pending'` означает, что MP3 ещё скачивается ModuleMtsPbx (`downloadRecords.php`). Cron не двигает курсор через такую запись, ждёт следующего тика.
- ACK-протокол invoke: `IMPORT_ACK_OK` / `IMPORT_ACK_NOT_READY` (если карты сотрудников `b24->inner_numbers`/`mobile_numbers` пусты после рестарта). Cron при `not_ready` не двигает курсор.
- Защита от повторного запуска: `flock(LOCK_EX|LOCK_NB)` на `tmp/.../mts_importer.lock`.
- Dedup: `ConnectorDb::FUNC_GET_EXPORTED_CALL_ID` — строгий поиск по `linkedid` в `b24_cdr_data` с непустым `call_id` (без leg-семантики `getCdrDataByLinkedId`).

Модель `Modules\ModuleMtsPbx\Models\CallHistory` доступна через PSR-4 (соседний модуль регистрирует свой namespace). НО при отсутствии модуля любое `use` приведёт к fatal — используем только FQCN-строки + `class_exists($fqcn)` перед обращением.

## Worker initialization patterns

**AMI-воркер: listener регистрируется ДО тяжёлого init.** `WorkerSafeScriptsCore::checkWorkerAMI` (core MikoPBX) шлёт ping через AMI UserEvent. После `MAX_PING_FAILURES` промахов подряд — SIGUSR1 → воркер крашится → safe.php стартует заново. Окно типично 15 секунд. `new Bitrix24Integration('_ami')` делает синхронный REST к B24 и может занимать дольше, поэтому `setFilter()` и `addEventHandler('userevent', [$this, 'callback'])` нужно вызвать **сразу после `createAstManager()`**, до тяжёлого init. `replyOnPingRequest` (WorkerBase) зависит только от `$this->am`, поэтому отвечать на ping можно без `$b24`. `callback()` использует `$this->processState === 'init'` как маркер «не готов» — пока state='init', события (кроме pong) игнорируются.

## ConnectorDb::invoke contract

`ConnectorDb::invoke(FUNC_GET_GENERAL_SETTINGS)` возвращает гетерогенное значение:
- cache-hit → `stdClass` (через `(object)$cached`)
- cache-miss + RPC success → `stdClass`
- cache-miss + RPC fail/timeout → `[]` (пустой массив)
- DB-empty (модель отсутствует) → может быть `false` (Phalcon 3/4)

Проверять: `is_object($settings) && !empty($settings->portal)` — это и есть инвариант «настройки готовы». Обращаться к полям через `->`, **не** через `[...]`.

## Logs

Логи воркеров: `ConnectorDb.log`, `HttpConnection.log`, `HttpConnection_SYNC.log`, `IntegrationAMI.log`.
Логи воркеров: `ConnectorDb.log`, `HttpConnection.log`, `HttpConnection_SYNC.log`, `IntegrationAMI.log`, `Bitrix24Integration.log` (сюда пишется `inner_numbers_diff` — added/removed между снимками b24GetPhones).
Формат: `[ISO8601][level] message(pid): JSON`. Для анализа звонка — искать по `linkedid` во всех логах.
Loading
Loading