fix(events): eventos CUSTOM do ESL nunca eram entregues
Achado ao iniciar a fase de Realtime Monitoring: nenhum evento CUSTOM (sofia::register, sofia::gateway_state, callcenter::info) jamais chegou em b2bcall-fs-events nesta sessao, apesar do estado real do FreeSWITCH mudar de verdade (confirmado originando uma chamada de teste pra dentro de uma fila real com agente logado). Isso deixava dois gaps documentados como "nao verificado" em docs/TRUNKS.md e docs/AGENTS.md. Causa raiz: `event_json(...SUBSCRIBED_EVENTS)` mandava "CUSTOM" como ultimo token do comando `event json`, sem nenhum subclass depois. O mod_event_socket do FreeSWITCH exige que os subclasses (callcenter::info, sofia::register, ...) venham imediatamente depois do token CUSTOM no mesmo comando — sem isso, zero eventos CUSTOM sao entregues, de qualquer subclass. Corrigido separando PLAIN_EVENTS (nomes normais, cada um vira um listener .on()) de CUSTOM_SUBCLASSES (so compoe o comando de subscricao — o client ESL sempre emite "CUSTOM" como nome de evento, com o subclass real no header Event-Subclass). Corrigido tambem um bug de nome de campo: normalizeCustomEvent lia CC-Agent-Status, que nao existe; o campo real e CC-Agent-State. Com o pipeline corrigido, chegam eventos ricos de callcenter::info nunca antes observados: agent-offering, bridge-agent-fail, members-count (fila em tempo real) e member-queue-end (com CC-Cause/CC-Cancel-Reason e timestamps de entrada/saida — atendida vs. abandonada). Adicionados como novos tipos normalizados: AGENT_OFFERED_CALL, AGENT_BRIDGE_FAILED, QUEUE_MEMBER_COUNT, QUEUE_MEMBER_LEFT. De quebra, achado e corrigido um segundo bug real ao reverificar Trunks com o pipeline de eventos funcionando: `sofia profile external rescan` nunca descarregava um gateway cujo arquivo .xml foi apagado (fica fantasma na memoria do Sofia indefinidamente). trunk-sync.ts agora roda `sofia profile external killgw <nome>` pra cada gateway removido, antes do rescan. Reverificado ponta a ponta pra ambos os bugs: - Trunk com register:true apontando pra host inexistente: Trunk.status no banco passa de UNKNOWN pra FAILED sozinho, via evento, sem polling. - Trunk apagado via API: gateway some imediatamente de `sofia status gateway`, sem esperar reinicio de profile. - Chamada de teste real numa fila com agente logado: members-count, agent-offering, agent-state-change (CC-Agent-State correto: Waiting/ Receiving), bridge-agent-fail, todos chegando certos no canal Redis b2bcall:events. typecheck do workspace inteiro limpo. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
This commit is contained in:
@@ -96,14 +96,43 @@ FreeSWITCH (`agent list`).
|
||||
|
||||
## O que falta
|
||||
|
||||
- Estados derivados de chamada (RINGING, IN_CALL, WRAP_UP, RESERVED) —
|
||||
dependem de `callcenter::info` (CUSTOM event), que **ainda não foi
|
||||
provado funcionando** nesta sessão (mesma lacuna de `sofia::gateway_state`
|
||||
documentada em docs/TRUNKS.md). Não implementado; precisa de uma chamada
|
||||
real passando pela fila pra testar.
|
||||
- Estados derivados de chamada (RINGING, IN_CALL, WRAP_UP, RESERVED) — o
|
||||
mecanismo de entrega do `callcenter::info` foi corrigido e confirmado
|
||||
funcionando (ver seção abaixo), mas gravar esses estados de volta em
|
||||
`Agent.state` ainda não foi implementado — hoje o campo só muda via
|
||||
login/logout/pause/resume. O estado real do mod_callcenter (`CC-Agent-State`:
|
||||
`Waiting`/`Receiving`/...) já está disponível em tempo real via WebSocket
|
||||
(docs/REALTIME.md); persistir isso em `Agent.state` fica pra quando o
|
||||
Predictive Engine/CDR precisarem consultar esse histórico.
|
||||
- Tela do agente (secao 49) — fase Frontend.
|
||||
- Monitoramento de filas/ramais em tempo real (secao 54-55) — depende de
|
||||
WebSocket multi-tenant.
|
||||
- Quota de agentes (`max_agents`) — depende de Plans/Entitlements.
|
||||
- `PauseReason.maxDuration` existe no modelo mas não é aplicado
|
||||
automaticamente ainda (ninguém força o fim da pausa ao expirar).
|
||||
|
||||
## Correção: eventos CUSTOM (`callcenter::info`) nunca chegavam
|
||||
|
||||
Achado ao testar esta fase ponta a ponta com uma chamada real de teste
|
||||
(`originate null/dummy &callcenter(fila@dominio)`): nenhum evento CUSTOM
|
||||
aparecia em `b2bcall-fs-events`, apesar do agente ser ofertado a chamada de
|
||||
verdade (`agent list` mostrava `last_offered_call` mudando). Causa raiz:
|
||||
`event_json(...SUBSCRIBED_EVENTS)` mandava `"CUSTOM"` como último argumento
|
||||
do comando `event json`, sem nenhum subclass depois — o mod_event_socket do
|
||||
FreeSWITCH exige que os subclasses (`callcenter::info`, `sofia::register`,
|
||||
`sofia::gateway_state`, ...) venham logo depois do token `CUSTOM` no mesmo
|
||||
comando, senão zero eventos CUSTOM são entregues (de qualquer subclass).
|
||||
Afetava também `sofia::gateway_state` (ver correção equivalente em
|
||||
docs/TRUNKS.md).
|
||||
|
||||
Corrigido separando `PLAIN_EVENTS` (nomes normais, cada um vira um listener
|
||||
`.on()`) de `CUSTOM_SUBCLASSES` (só compõem o comando `event json`, nunca
|
||||
viram listener — o client ESL sempre emite `"CUSTOM"` como nome de evento,
|
||||
com o subclass real no header `Event-Subclass`).
|
||||
|
||||
De quebra, corrigido também o nome de campo errado em
|
||||
`normalizeCustomEvent`: o código lia `CC-Agent-Status`, que não existe — o
|
||||
campo real é `CC-Agent-State`. Os `CC-Action` reais observados no teste
|
||||
(úteis pro monitoramento de filas, agente.md secao 54): `agent-offering`,
|
||||
`agent-state-change`, `bridge-agent-fail`, `member-queue-end` (com
|
||||
`CC-Cause`/`CC-Cancel-Reason` e timestamps de entrada/saída — abandono vs.
|
||||
atendida), `members-count` (contagem ao vivo de chamadas esperando por
|
||||
fila). Ver `packages/telephony/src/normalize-event.ts`.
|
||||
|
||||
@@ -22,9 +22,9 @@ reagir a `connect`/`reconnecting`/`error` e resubscrever a cada `connect`
|
||||
`mod_callcenter` mas não foram exercitados contra uma fila real ainda —
|
||||
não existe nenhuma (fase Queues).
|
||||
- `normalizeEslEvent()`: traduz eventos ESL crus pro vocabulário interno
|
||||
(agente.md secao 24). Mapeamento de `callcenter::info` → `AGENT_STATUS_CHANGED`
|
||||
é best-effort (nomes de campo inferidos da documentação, não testados —
|
||||
revisar na fase Queues/Agents).
|
||||
(agente.md secao 24). Mapeamento de `callcenter::info`/`sofia::*` com nomes
|
||||
de campo confirmados contra uma fila real na fase Realtime Monitoring —
|
||||
ver "Achados" abaixo e docs/AGENTS.md.
|
||||
|
||||
## Achados durante os testes
|
||||
|
||||
@@ -41,6 +41,19 @@ reagir a `connect`/`reconnecting`/`error` e resubscrever a cada `connect`
|
||||
3. O logger JSON de `packages/shared` quebrava (`TypeError: Do not know how
|
||||
to serialize a BigInt`) porque a lib `esl` usa `bigint` nos campos de
|
||||
estatística de erro. Corrigido com um `replacer` no `JSON.stringify`.
|
||||
4. **Bug real, só descoberto na fase Realtime Monitoring**: nenhum evento
|
||||
CUSTOM (`sofia::gateway_state`, `sofia::register`, `callcenter::info`)
|
||||
nunca chegou nesta sessão até então, apesar da subscrição incluir
|
||||
`"CUSTOM"` na lista de `SUBSCRIBED_EVENTS`. Causa: `event_json(...events)`
|
||||
manda `"CUSTOM"` como último token do comando `event json`, sem nenhum
|
||||
subclass depois — o mod_event_socket do FreeSWITCH exige que os
|
||||
subclasses venham imediatamente depois do token `CUSTOM` no mesmo
|
||||
comando pra serem entregues. Corrigido: `SUBSCRIBED_EVENTS` agora termina
|
||||
em `"CUSTOM", "sofia::register", "sofia::unregister", "sofia::expire",
|
||||
"sofia::gateway_state", "callcenter::info"`, e o loop que registra
|
||||
listeners `.on()` só usa os nomes "planos" (`PLAIN_EVENTS`) mais um único
|
||||
`.on("CUSTOM", ...)` — os nomes de subclass nunca viram listener, só
|
||||
compõem o comando de subscrição.
|
||||
|
||||
## Eventos consumidos e publicados
|
||||
|
||||
|
||||
@@ -29,6 +29,16 @@ módulos). Em vez disso, `b2bcall-fs-config`:
|
||||
2. Roda `sofia profile external rescan` via ESL — relê os gateways sem
|
||||
derrubar chamadas em andamento (diferente de `restart`).
|
||||
|
||||
**Achado real (fase Realtime Monitoring)**: `rescan` só lê arquivos
|
||||
novos/alterados — apagar o arquivo `.xml` de um trunk removido não tira o
|
||||
gateway da memória do Sofia, ele fica "fantasma" indefinidamente (confirmado
|
||||
criando e apagando um trunk de teste via API e checando `sofia status
|
||||
gateway` depois do rescan). Corrigido: `trunk-sync.ts` agora roda `sofia
|
||||
profile external killgw <nome>` pra cada gateway removido, antes do rescan —
|
||||
descarrega um gateway específico na hora, sem precisar reiniciar o profile
|
||||
inteiro. Reverificado ponta a ponta (criar → aparece em `sofia status
|
||||
gateway` → apagar via API → some imediatamente).
|
||||
|
||||
## Gatilho de sincronização
|
||||
|
||||
`apps/api` não roda no Docker (ainda está no host) e `fs-config` não expõe
|
||||
@@ -82,12 +92,23 @@ Criei um trunk de teste apontando pra um host inexistente
|
||||
|
||||
## O que falta
|
||||
|
||||
- Propagação de `GATEWAY_UP`/`DOWN` → `Trunk.status` não confirmada com
|
||||
evento real disparado por uma mudança de estado ao vivo nesta sessão de
|
||||
testes (o gateway ficou em `FAIL_WAIT` por falha de DNS, que pode não
|
||||
disparar o mesmo ciclo de eventos que uma rejeição SIP normal) — vale
|
||||
reverificar com um destino que responda de verdade (outro FreeSWITCH, por
|
||||
exemplo) antes de confiar nisso em produção.
|
||||
- Quota de troncos (`max_trunks`, secao 59) — depende de Plans/Entitlements.
|
||||
- `GET /trunks` não mostra `sofia status gateway` ao vivo, só o último
|
||||
status conhecido no banco — suficiente por enquanto, sem WebSocket ainda.
|
||||
status conhecido no banco — resolvido na fase Realtime Monitoring via
|
||||
WebSocket (ver docs/REALTIME.md).
|
||||
|
||||
## Correção: `GATEWAY_UP`/`DOWN` → `Trunk.status` (fase Realtime Monitoring)
|
||||
|
||||
Documentado antes como "não confirmado com evento real" — na verdade nunca
|
||||
funcionava, e a causa raiz não tinha nada a ver com o tipo de falha do
|
||||
gateway. `b2bcall-fs-events` assinava os eventos CUSTOM do ESL passando
|
||||
`"CUSTOM"` como o **último** elemento da lista pro comando `event json`, sem
|
||||
nenhum subclass depois — e o protocolo do FreeSWITCH exige que os nomes de
|
||||
subclass (`sofia::gateway_state`, `sofia::register`, `callcenter::info`,
|
||||
...) venham imediatamente depois do token `CUSTOM` no mesmo comando `event
|
||||
json`, senão zero eventos CUSTOM chegam, de qualquer subclass. Achado e
|
||||
corrigido durante a fase Agentes/Realtime Monitoring (mesmo bug afetava
|
||||
`callcenter::info`, ver docs/AGENTS.md). Reverificado ponta a ponta: criar
|
||||
um trunk com `register: true` apontando pra um host inexistente →
|
||||
`Trunk.status` no banco passa de `UNKNOWN` pra `FAILED` sozinho, sem
|
||||
polling, assim que o Sofia tenta e falha o registro.
|
||||
|
||||
Reference in New Issue
Block a user