feat(recording): gravacao de chamadas + object storage abstraction
Fecha agente.md secao 90-94. A especificacao lista "Recording" e "Object
Storage" como dois passos separados na ordem de implementacao (secao
232), mas ficaram numa unica fase — sao acoplados o suficiente (Recording
precisa de um lugar pra guardar bytes) pra fazer sentido construir juntos.
## packages/storage — ObjectStorageProvider (secao 92)
Abstracao pequena: putObject/getObjectStream/deleteObject. Dois backends:
LocalObjectStorageProvider (filesystem, com checagem de path traversal
mesmo a key sendo sempre montada no servidor) e S3ObjectStorageProvider
(@aws-sdk/client-s3, preparado pra AWS S3 e MinIO via endpoint/
forcePathStyle customizaveis — nunca exercitado nesta sessao, sem
servidor S3 disponivel neste laboratorio). Escolhido por STORAGE_PROVIDER
env.
buildRecordingObjectKey (secao 93):
tenants/{tenant_id}/recordings/YYYY/MM/DD/{call_id}.wav, sempre montada
no servidor a partir de dados confiaveis.
## Bind mounts, nao volumes nomeados
/recordings e /data/object-storage usam bind mount pra um diretorio real
do host — apps/api roda no host, nao em Docker, e precisa enxergar os
mesmos arquivos que fs-events escreve. LOCAL_STORAGE_ROOT tem valores
diferentes por ambiente (mesmo padrao ja usado pra REDIS_URL).
## Quem grava: apps/predictive-dialer
So' chamadas originadas pelo discador com Campaign.recordingEnabled sao
gravadas nesta fase (unico caminho de originate que o sistema controla
hoje). RECORD_STEREO=true + execute_on_answer='record_session ...'
adicionados ao originate; origination_uuid pre-gerado (em vez de deixar o
provider sortear) porque o path de gravacao precisa dele antes do
comando de originate ser montado — o mesmo uuid vira Call.id no CDR.
## Quem sobe: apps/freeswitch-events/src/recording.ts
Em CALL_ENDED, encadeado depois do persistCallEvent terminar (nao em
paralelo) — uploadRecordingIfPresent le Call.talkTime/durationSeconds,
que e' exatamente o que persistCallEvent acabou de calcular no mesmo
evento (mesma classe de corrida ja corrigida uma vez na fase CDR, aqui
evitada por ordenacao). Sobe pro storage, cria Recording (retentionUntil
a partir de Plan.recordingRetentionDays), apaga o spool local.
## API + retencao
GET /recordings, GET /recordings/:id, GET /recordings/:id/audio (stream
autenticado, nunca URL direta pro storage). runRetentionSweep (secao 94)
no boot do apps/api + a cada hora — apaga o objeto, marca status=DELETED
(linha nunca apagada, fica como auditoria).
## Bug real achado testando esta fase
Recording.sizeBytes (BigInt) quebrava GET /recordings com 500 — Fastify
nao serializa BigInt nativamente (mesma classe de bug ja corrigida uma
vez no logger, fase Event Socket). Corrigido convertendo pra number na
resposta.
Verificado ponta a ponta: gravacao real criada (RIFF WAVE, PCM 16-bit,
ESTEREO 8000Hz — RECORD_STEREO confirmado), upload com path exato da
secao 93, download via API com md5 identico ao objeto original, varredura
de retencao apagando objeto + status DELETED + list/download bloqueados
depois. 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:
44
TODO.md
44
TODO.md
@@ -426,8 +426,48 @@
|
||||
- [ ] Cross-tenant reports pra platform admin (secao 157) — sem console
|
||||
de plataforma ainda
|
||||
|
||||
## PHASE 18+ — ver `agente.md` seções 90 em diante (Recordings, AI,
|
||||
Billing, Frontend, Security, Tests)
|
||||
## PHASE 18 — Recording / Object Storage (agente.md secao 90-94)
|
||||
- [x] `packages/storage`: `ObjectStorageProvider` (secao 92) — Local
|
||||
(filesystem, com checagem de path traversal) e S3-compatible
|
||||
(`@aws-sdk/client-s3`, preparado pra MinIO — nunca exercitado nesta
|
||||
sessão, sem servidor S3 disponível). Escolhido por
|
||||
`STORAGE_PROVIDER` env, cada processo monta o seu
|
||||
- [x] `buildRecordingObjectKey` (secao 93):
|
||||
`tenants/{tenant_id}/recordings/YYYY/MM/DD/{call_id}.wav`, sempre
|
||||
montada no servidor
|
||||
- [x] Bind mounts (não volumes nomeados) pro spool de gravação e pro
|
||||
storage local — `apps/api` roda no host, precisa enxergar os mesmos
|
||||
arquivos que `fs-events` escreve (mesmo padrão de `REDIS_URL`)
|
||||
- [x] `apps/predictive-dialer`: `RECORD_STEREO=true` +
|
||||
`execute_on_answer='record_session ...'` quando
|
||||
`Campaign.recordingEnabled` — `origination_uuid` pré-gerado pra
|
||||
poder montar o path de gravação antes do originate
|
||||
- [x] `apps/freeswitch-events/src/recording.ts`: em CALL_ENDED (encadeado
|
||||
depois do CDR terminar, não em paralelo — evita a mesma corrida já
|
||||
corrigida na fase CDR), sobe a gravação, cria `Recording`
|
||||
(`retentionUntil` a partir de `Plan.recordingRetentionDays`), apaga
|
||||
o spool local
|
||||
- [x] `GET /recordings`, `GET /recordings/:id`, `GET /recordings/:id/audio`
|
||||
(stream autenticado, nunca URL direta pro storage)
|
||||
- [x] Retenção (secao 94): `runRetentionSweep` no boot do `apps/api` + a
|
||||
cada hora — apaga o objeto, marca `status=DELETED` (linha nunca
|
||||
apagada, fica como auditoria)
|
||||
- [x] **Bug real, achado no teste desta fase**: `Recording.sizeBytes`
|
||||
(BigInt) quebrava `GET /recordings` com 500 — Fastify não serializa
|
||||
BigInt nativamente (mesma classe de bug já corrigida uma vez no
|
||||
logger). Corrigido convertendo pra `number` na resposta.
|
||||
- [x] Testado ponta a ponta: gravação real criada (RIFF WAVE, PCM 16-bit,
|
||||
ESTÉREO 8000Hz — RECORD_STEREO confirmado), upload com path exato da
|
||||
secao 93, download via API com md5 idêntico ao objeto original,
|
||||
varredura de retenção apagando objeto + status DELETED + list/
|
||||
download bloqueados depois
|
||||
- [ ] Chamadas manuais/internas não são gravadas — só o caminho do
|
||||
discador tem originate próprio
|
||||
- [ ] `max_recording_storage_gb` (Plan) existe mas não é aplicado
|
||||
- [ ] Transcrição (secao 95+, fase IA)
|
||||
|
||||
## PHASE 19+ — ver `agente.md` seções 95 em diante (AI, Billing, Frontend,
|
||||
Security, Tests)
|
||||
|
||||
---
|
||||
|
||||
|
||||
Reference in New Issue
Block a user