Surface Stoat's voice errors and stop leaving orphan connections

Joining a channel failed silently: revoice's join() runs an async
executor inside `new Promise`, so a rejected join_call never reaches
reject() — the promise hangs forever and the real error escapes as an
unhandled rejection. The client wrapper now latches API failures and
settles the join itself, translating Stoat's error codes (AlreadyConnected,
LiveKitUnavailable, UnknownNode, ...) into messages the chat can show.

A failed join also used to leave the connection object alive, which kept
the bot registered in the channel and made the next attempt fail with
AlreadyConnected; it is now destroyed on any failure. The LiveKit node
name is configurable via VOICE_NODE for instances that renamed it, and
the README documents how to clear a stuck voice state from Redis.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
This commit is contained in:
Leonid Pershin
2026-09-08 23:40:21 +03:00
co-authored by Claude Opus 5
parent d4880e757e
commit 823a9f1565
6 changed files with 584 additions and 464 deletions
+19 -1
View File
@@ -149,7 +149,25 @@ cd /opt/stoat-mbot && docker compose up -d --build && docker compose logs -f
резолвится в хост: `docker compose exec mbot node -e "fetch(process.env.STOAT_API_URL).then(r=>console.log(r.status))"`.
2. **`joining voice channel`, но нет `voice connection established`.** Значит `join_call` отдал
токен, а WebSocket до LiveKit не поднялся — смотрите, доступен ли `/livekit` через внешний домен.
3. **Бот в канале, но звука нет.** Это уже медиа-трафик: LiveKit анонсирует клиентам свой адрес
3. **`Stoat считает, что бот уже в этом голосовом канале` (`AlreadyConnected`).** Зависшее
состояние в Redis после падения бота: `join_call` регистрирует участника, а выйти он не успел.
Ботам API запрещает `force_disconnect`, поэтому чистим вручную — в каталоге инстанса:
```bash
docker compose exec redis valkey-cli --scan --pattern 'vc:*'
```
```bash
docker compose exec redis valkey-cli DEL 'vc:<ID_бота>'
```
```bash
docker compose exec redis valkey-cli SREM 'vc_members:<ID_голосового_канала>' '<ID_бота>'
```
В норме состояние снимает `voice-ingress` по вебхуку от LiveKit — если ситуация повторяется
после каждого перезапуска, смотрите `docker compose logs voice-ingress`.
4. **Бот в канале, но звука нет.** Это уже медиа-трафик: LiveKit анонсирует клиентам свой адрес
из `rtc.node_ip` / `use_external_ip` в `/opt/stoat/livekit.yml` и ждёт UDP на 50000-50100.
Если анонсируется внешний IP, а роутер не умеет NAT loopback, пакеты от контейнера до него не
дойдут. Тогда либо включите hairpin на роутере, либо запустите бота внутри compose-проекта