Repository navigation
Добавить возможность принимать входящие VoIP звонки в чатботе на Python - #51
kennyromanov wants to merge 4 commits into
Conversation
| api_token_instance=api_token_instance, | ||
| openai_api_key=openai_api_key, | ||
| realtime_model=realtime_model, | ||
| realtime_voice=realtime_voice, |
There was a problem hiding this comment.
OS здесь не нужно - ломает внешние проекты (если задан системный ENV)
| return value / 1_000 | ||
|
|
||
| if 1_000_000 * MIN_SECONDS <= value < 1_000_000 * MAX_SECONDS: | ||
| return value / 1_000_000 |
There was a problem hiding this comment.
Приводим время в мc., нc. --> в UNIX (сек.)
|
|
||
| await execution.voice.start() | ||
|
|
||
| execution.api = GreenAPI(self._id_instance, self._api_token_instance, host=self._api_url) |
There was a problem hiding this comment.
эта requests.Session нигде не закрывается
_close_connections закрывает calls, voice и bridge_task, но не execution.api.session (а reject_incoming свой api.session.close() делает)
каждый звонок оставляет keep-alive-сокеты до GC - у долгоживущего бота это накопление открытых fd
закрывать в _close_connections / _finalize_call
| self._active_runtime = None | ||
| self._active_loop = None | ||
| self._active_execution = None | ||
| path = execution.recorder.finish() |
There was a problem hiding this comment.
finish() = join потока записи + синхронное кодирование всего звонка в MP3 через PyAV, а вызывается это из async _finalize_call - прямо в событийном цикле
на звонке в talk_timeout (120 с) кодирование занимает секунды CPU, и на это время замирают ВСЕ корутины: координатор очереди, notify_incoming_ended, следующий оффер
дёшево чинится: await asyncio.to_thread(execution.recorder.finish)
|
|
||
| return | ||
|
|
||
| await execution.calls.openAsync(timeout=INITIAL_STATE_TIMEOUT_SECONDS) |
There was a problem hiding this comment.
у входящего пути выше есть проверка state == RINGING, у исходящего зеркальной проверки DIALING нет
если приложение забыло fsm.apply(DEQUEUED) и сессия осталась в QUEUED: dialAsync всё равно выполняется (телефон абонента реально звонит), DIAL_ACCEPTED молча игнорируется FSM, ring_deadline проверяется только в RINGING → _next_timeout даёт None, _process_eventsждёт очередь вечно, executor никогда не возвращается
| if execution.command_started: | ||
| WhatsAppCallService._apply_observed_incoming_answer(execution) | ||
|
|
||
| if state.state == "idle" and not execution.command_completed: |
There was a problem hiding this comment.
путь _ANSWER_CONFIRMED никогда не выставляет command_completed
после такого ответа штатный "idle" (абонент положил трубку) попадает в эту ветку: завершение идёт через CALLER_LEFT вместо REMOTE_IDLE, session.remote_reason ("hangup") теряется, плюс лишний getState в _safe_finish_matching_incoming
| self._record_cancellation(execution, error) | ||
| raise | ||
| except Exception: | ||
| if session.direction == CallDirection.INCOMING: |
There was a problem hiding this comment.
в терминальное состояние переводятся только INCOMING-сессии
для OUTGOING упавший _prepare_call пробрасывает ошибку, а session навсегда остаётся в DIALING с end_reason=None
для входящих тот же отказ корректно даёт FAILED/setup_failed - контракт несимметричен
| calls.on("disconnect", partial(self._on_disconnect, execution)) | ||
|
|
||
| calls.on("error", lambda detail: runtime.events.put_nowait( | ||
| RuntimeEvent.bridge_failed(RuntimeError("callsRtc reported an error")), |
There was a problem hiding this comment.
detail выбрасывается, все ошибки схлопываются в одинаковый RuntimeError, а _handle_call_failure логирует только type(error).__name__
в проде это лог «error=RuntimeError» и error_code="RuntimeError" - что именно сообщил сервер, узнать невозможно
вложить detail в сообщение исключения
|
|
||
| if accepted_statuses is not None: | ||
| if isinstance(accepted_statuses, str): | ||
| accepted_statuses = [accepted_statuses] |
There was a problem hiding this comment.
observer.py:132-146 уже разбирает str/list/tuple при регистрации - здесь та же нормализация повторяется на каждом событии
два места истины: при расширении форм фильтра (enum-статусы) их легко развести
достаточно один раз сохранить готовый список в filters
Требование:
Алгоритм решения:
Дополнительная информация: