feat(agents): agentes, tiers e pausas — call center completo
Fecha agente.md secao 45-49/52. Depois desta fase, um usuario autenticado consegue logar como agente, entrar numa fila real, se pausar e voltar, tudo refletido de verdade no FreeSWITCH. Schema (migration 20260828124245_agents): - agents (tenant-scoped, RLS): User -> Extension -> identidade de agente, state (enum AgentState de 8 valores) espelhando o estado real, so alterado via login/logout/pause/resume, nunca escrito direto pela API. - tiers: Queue<->Agent (level/position 1:1 com mod_callcenter). - agent_sessions: um ciclo login->logout por linha. - agent_state_events: historico de transicoes de estado. - pause_reasons / agent_pause_events (secao 48). Dois bugs reais corrigidos em FreeSwitchTelephonyProvider, presentes desde a fase de Event Socket original: - "queue add/del member" nao existe no mod_callcenter — membership de fila usa tier add/tier del. So foi pego agora ao confirmar de novo a sintaxe via `help callcenter_config` antes de codar esta fase. - addAgent/removeAgent nao existiam ainda (agent add/del). Mecanismo de sync: agentes e tiers nao tem representacao em XML, so comando ESL direto — diferente do padrao "regenera todos os arquivos" usado em Trunks/Queues. apps/api publica uma mensagem por acao com payload (b2bcall:agents:sync, b2bcall:tiers:sync); b2bcall-fs-config aplica o comando correspondente (agent-sync.ts). Achados confirmados manualmente contra o FreeSWITCH real antes de codar: - `agent add`/`tier add` nao sao idempotentes (erro em duplicata) — sync ignora esse erro (.catch), condicao esperada em resync. - `agent del`/`tier del` em algo inexistente nao da erro — seguro chamar sem checar existencia antes. - `agent set status` so aceita 3 valores exatos (Available/On Break/ Logged Out) — testado deliberadamente com valor invalido. - Corrida real: atribuir tier antes do primeiro login do agente falha silenciosamente do lado do FreeSWITCH (agente so existe la a partir do `agent add` no login). Login sempre re-sincroniza todos os tiers do agente depois de garantir que ele existe — auto-correcao confirmada no teste ponta a ponta. apps/api: AgentsController (CRUD), AgentsMeController (login/logout/ pause/resume — sempre resolve o agente via JWT, nunca um agentId arbitrario do client), PauseReasonsController (CRUD), QueueAgentsController (POST/DELETE de tier em /queues/:id/agents). Verificado ponta a ponta via curl + fs_cli contra o FreeSWITCH real: login -> Available, pause -> On Break, resume -> Available, logout -> Logged Out, todos batendo entre Agent.state (banco) e `agent list` (FreeSWITCH). typecheck do workspace inteiro limpo. ~350MB de memoria total (docker stats). Documentado em docs/AGENTS.md, incluindo lacuna conhecida: estados derivados de chamada (RINGING/IN_CALL/WRAP_UP/RESERVED) dependem do evento CUSTOM callcenter::info, ainda nao comprovado chegando em fs-events nesta sessao (mesma lacuna de sofia::gateway_state ja documentada em docs/TRUNKS.md) — precisa de uma chamada real passando pela fila pra investigar. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01X1HxY46WGU4G1zmVDNKcWw
This commit is contained in:
109
docs/AGENTS.md
Normal file
109
docs/AGENTS.md
Normal file
@@ -0,0 +1,109 @@
|
||||
# Agentes, Tiers e Pausas
|
||||
|
||||
Agente.md secao 45-49 e 52. Completa o Call Center: depois desta fase, um
|
||||
usuário autenticado consegue logar como agente, entrar numa fila real, se
|
||||
pausar e voltar — tudo refletido de verdade no FreeSWITCH.
|
||||
|
||||
## Modelo
|
||||
|
||||
- `agents` (tenant-scoped, RLS): liga `User` (login) → `Extension` (ramal
|
||||
SIP) → identidade de agente (`User`/`Agent`/`Extension` separados,
|
||||
agente.md secao 45). `state` espelha o estado real (secao 46), nunca
|
||||
escrito direto pela API — só via login/logout/pause/resume.
|
||||
- `tiers`: Queue↔Agent (secao 52) — level/position espelham 1:1 o
|
||||
mod_callcenter.
|
||||
- `agent_sessions`: uma linha por ciclo login→logout.
|
||||
- `agent_state_events`: histórico de transições de estado (auditoria).
|
||||
- `pause_reasons`/`agent_pause_events` (secao 48).
|
||||
|
||||
## Mecanismo: 100% dinâmico, sem arquivo
|
||||
|
||||
Confirmado na fase anterior (Queues) via `help callcenter_config`: agentes e
|
||||
tiers não têm representação em XML — só comandos ESL diretos
|
||||
(`agent add/del/set status/set contact`, `tier add/del`). Isso muda o padrão
|
||||
de sincronização: em vez de "regenerar todos os arquivos" (Trunks/Queues),
|
||||
`apps/api` publica uma mensagem **por ação, com payload** (`{tenantId,
|
||||
agentId, action}` em `b2bcall:agents:sync`; `{tenantId, queueId, agentId,
|
||||
level, position, action}` em `b2bcall:tiers:sync`) — `b2bcall-fs-config`
|
||||
recebe e aplica o comando correspondente.
|
||||
|
||||
## Achados reais confirmados manualmente antes de codar
|
||||
|
||||
Rodei cada comando contra o FreeSWITCH real antes de escrever qualquer
|
||||
lógica de sync (mesmo método usado nas fases anteriores):
|
||||
|
||||
- `agent add` em cima de um agente que já existe: **erro**
|
||||
("Agent already exist!") — não é idempotente. `tier add` duplicado: mesmo
|
||||
erro ("Tier already exist!"). Ambos capturados e ignorados no sync
|
||||
(`.catch(() => undefined)`), já que essa condição é esperada em qualquer
|
||||
resync.
|
||||
- `tier del`/`agent del` em algo que não existe: **não dá erro** (+OK) —
|
||||
seguro chamar sem checar existência antes.
|
||||
- `agent set status` só aceita 3 valores exatos: `Available`, `On Break`,
|
||||
`Logged Out` — qualquer outro string dá `-ERR Invalid Agent Status!`
|
||||
(testado deliberadamente). Os estados derivados de chamada do nosso enum
|
||||
(RESERVED/RINGING/IN_CALL/WRAP_UP) não têm status próprio no
|
||||
mod_callcenter — mapeiam pra "Available" (só o `state`, campo separado,
|
||||
muda sozinho conforme a chamada progride — não escrevemos isso).
|
||||
- **Achado de corrida real**, visto ao testar o fluxo completo: como um
|
||||
agente só passa a existir no FreeSWITCH no login (`agent add` roda ali,
|
||||
não na criação do registro `Agent`), atribuir um tier (`POST
|
||||
/queues/:id/agents`) **antes** do primeiro login do agente falha
|
||||
silenciosamente do lado do FreeSWITCH (`-ERR Agent not found!`, logado
|
||||
mas não propagado como erro HTTP). Por isso o login **sempre
|
||||
re-sincroniza todos os tiers do agente** depois de garantir que ele
|
||||
existe — a auto-correção aconteceu exatamente assim no teste real.
|
||||
|
||||
## Fluxo de login (agente.md secao 47)
|
||||
|
||||
```
|
||||
POST /agents/me/login
|
||||
→ valida usuário (JWT) e ramal (Agent.extensionId precisa existir)
|
||||
→ cria agent_session
|
||||
→ agent add (idempotente via catch) + set contact (dial-string do ramal)
|
||||
→ set status Available
|
||||
→ re-sincroniza todos os tiers do agente
|
||||
→ Agent.state = AVAILABLE
|
||||
```
|
||||
|
||||
`POST /agents/me/logout|pause|resume` seguem o mesmo padrão — sempre sobre
|
||||
o agente do **próprio usuário autenticado** (nunca um `agentId` arbitrário
|
||||
do client, mesmo princípio de nunca confiar em tenant_id/ids sensíveis vindo
|
||||
do frontend sem checar contra o JWT).
|
||||
|
||||
## Verificado ponta a ponta
|
||||
|
||||
```
|
||||
POST /extensions {"number":"2000",...}
|
||||
POST /agents {userId, extensionId, name}
|
||||
POST /queues {"name":"Suporte"}
|
||||
POST /queues/:queueId/agents {agentId} → tier add falha (agente ainda nao existe)
|
||||
POST /agents/me/login → agent add + contact + status Available
|
||||
→ tier re-sync (dessa vez funciona)
|
||||
|
||||
callcenter_config agent list
|
||||
→ status=Available, contact={ignore_early_media=true}user/2000@b2bcall.local ✓
|
||||
callcenter_config tier list
|
||||
→ queue|agent com state=Ready ✓
|
||||
|
||||
POST /agents/me/pause {pauseReasonId} → status=On Break ✓
|
||||
POST /agents/me/resume → status=Available ✓
|
||||
POST /agents/me/logout → status=Logged Out ✓
|
||||
```
|
||||
|
||||
Todos os 4 estados confirmados batendo entre o banco (`Agent.state`) e o
|
||||
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.
|
||||
- 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).
|
||||
Reference in New Issue
Block a user