@mbermeo/wa-blaster 0.1.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/README.md +1060 -0
  2. package/bin/wa-blast.js +124 -0
  3. package/dist/node20/cli/index.js +2 -0
  4. package/dist/node20/cli/index.jsc +0 -0
  5. package/dist/node20/cli/lib.js +2 -0
  6. package/dist/node20/cli/lib.jsc +0 -0
  7. package/dist/node20/mcp/index.js +2 -0
  8. package/dist/node20/mcp/index.jsc +0 -0
  9. package/dist/node20/src/accountManager.js +2 -0
  10. package/dist/node20/src/accountManager.jsc +0 -0
  11. package/dist/node20/src/campaignRunner.js +2 -0
  12. package/dist/node20/src/campaignRunner.jsc +0 -0
  13. package/dist/node20/src/db.js +2 -0
  14. package/dist/node20/src/db.jsc +0 -0
  15. package/dist/node20/src/paths.js +2 -0
  16. package/dist/node20/src/paths.jsc +0 -0
  17. package/dist/node20/src/routes/accounts.js +2 -0
  18. package/dist/node20/src/routes/accounts.jsc +0 -0
  19. package/dist/node20/src/routes/campaigns.js +2 -0
  20. package/dist/node20/src/routes/campaigns.jsc +0 -0
  21. package/dist/node20/src/routes/inbound.js +2 -0
  22. package/dist/node20/src/routes/inbound.jsc +0 -0
  23. package/dist/node20/src/server.js +2 -0
  24. package/dist/node20/src/server.jsc +0 -0
  25. package/dist/node20/src/utils/csvParser.js +2 -0
  26. package/dist/node20/src/utils/csvParser.jsc +0 -0
  27. package/dist/node20/src/utils/templateEngine.js +2 -0
  28. package/dist/node20/src/utils/templateEngine.jsc +0 -0
  29. package/dist/node22/cli/index.js +2 -0
  30. package/dist/node22/cli/index.jsc +0 -0
  31. package/dist/node22/cli/lib.js +2 -0
  32. package/dist/node22/cli/lib.jsc +0 -0
  33. package/dist/node22/mcp/index.js +2 -0
  34. package/dist/node22/mcp/index.jsc +0 -0
  35. package/dist/node22/src/accountManager.js +2 -0
  36. package/dist/node22/src/accountManager.jsc +0 -0
  37. package/dist/node22/src/campaignRunner.js +2 -0
  38. package/dist/node22/src/campaignRunner.jsc +0 -0
  39. package/dist/node22/src/db.js +2 -0
  40. package/dist/node22/src/db.jsc +0 -0
  41. package/dist/node22/src/paths.js +2 -0
  42. package/dist/node22/src/paths.jsc +0 -0
  43. package/dist/node22/src/routes/accounts.js +2 -0
  44. package/dist/node22/src/routes/accounts.jsc +0 -0
  45. package/dist/node22/src/routes/campaigns.js +2 -0
  46. package/dist/node22/src/routes/campaigns.jsc +0 -0
  47. package/dist/node22/src/routes/inbound.js +2 -0
  48. package/dist/node22/src/routes/inbound.jsc +0 -0
  49. package/dist/node22/src/server.js +2 -0
  50. package/dist/node22/src/server.jsc +0 -0
  51. package/dist/node22/src/utils/csvParser.js +2 -0
  52. package/dist/node22/src/utils/csvParser.jsc +0 -0
  53. package/dist/node22/src/utils/templateEngine.js +2 -0
  54. package/dist/node22/src/utils/templateEngine.jsc +0 -0
  55. package/dist/node24/cli/index.js +2 -0
  56. package/dist/node24/cli/index.jsc +0 -0
  57. package/dist/node24/cli/lib.js +2 -0
  58. package/dist/node24/cli/lib.jsc +0 -0
  59. package/dist/node24/mcp/index.js +2 -0
  60. package/dist/node24/mcp/index.jsc +0 -0
  61. package/dist/node24/src/accountManager.js +2 -0
  62. package/dist/node24/src/accountManager.jsc +0 -0
  63. package/dist/node24/src/campaignRunner.js +2 -0
  64. package/dist/node24/src/campaignRunner.jsc +0 -0
  65. package/dist/node24/src/db.js +2 -0
  66. package/dist/node24/src/db.jsc +0 -0
  67. package/dist/node24/src/paths.js +2 -0
  68. package/dist/node24/src/paths.jsc +0 -0
  69. package/dist/node24/src/routes/accounts.js +2 -0
  70. package/dist/node24/src/routes/accounts.jsc +0 -0
  71. package/dist/node24/src/routes/campaigns.js +2 -0
  72. package/dist/node24/src/routes/campaigns.jsc +0 -0
  73. package/dist/node24/src/routes/inbound.js +2 -0
  74. package/dist/node24/src/routes/inbound.jsc +0 -0
  75. package/dist/node24/src/server.js +2 -0
  76. package/dist/node24/src/server.jsc +0 -0
  77. package/dist/node24/src/utils/csvParser.js +2 -0
  78. package/dist/node24/src/utils/csvParser.jsc +0 -0
  79. package/dist/node24/src/utils/templateEngine.js +2 -0
  80. package/dist/node24/src/utils/templateEngine.jsc +0 -0
  81. package/package.json +44 -0
  82. package/public/index.html +2459 -0
package/README.md ADDED
@@ -0,0 +1,1060 @@
1
+ # WA Blast — Sistema de Mensajes Masivos por WhatsApp
2
+
3
+ Sistema para enviar mensajes masivos por WhatsApp con soporte multi-cuenta, plantillas con variables, imágenes con A/B testing, scheduling por hora/día, control de campañas y comportamiento anti-spam. Incluye API HTTP, interfaz web y CLI para uso por agentes de IA.
4
+
5
+ ---
6
+
7
+ ## Tabla de contenidos
8
+
9
+ - [Requisitos](#requisitos)
10
+ - [Instalación](#instalación)
11
+ - [Configuración](#configuración)
12
+ - [Arranque](#arranque)
13
+ - [Despliegue con Docker](#despliegue-con-docker)
14
+ - [CLI global (`wa-blast`)](#cli-global-wa-blast)
15
+ - [Arquitectura](#arquitectura)
16
+ - [Flujo de uso](#flujo-de-uso)
17
+ - [Interfaz web](#interfaz-web)
18
+ - [API HTTP](#api-http)
19
+ - [CLI](#cli)
20
+ - [Comportamiento anti-spam](#comportamiento-anti-spam)
21
+ - [Scheduling y límites de envío](#scheduling-y-límites-de-envío)
22
+ - [Imágenes y A/B testing](#imágenes-y-ab-testing)
23
+ - [Campañas inbound](#campañas-inbound)
24
+ - [Formato de archivos de contactos](#formato-de-archivos-de-contactos)
25
+ - [Sistema de plantillas](#sistema-de-plantillas)
26
+ - [Base de datos](#base-de-datos)
27
+ - [Solución de problemas](#solución-de-problemas)
28
+
29
+ ---
30
+
31
+ ## Requisitos
32
+
33
+ - **Node.js v24+** (recomendado via `nvm`)
34
+ - **npm**
35
+ - Chromium (Puppeteer lo descarga automáticamente al hacer `npm install`)
36
+ - Un teléfono con WhatsApp para escanear el QR
37
+
38
+ ---
39
+
40
+ ## Instalación
41
+
42
+ ```bash
43
+ git clone <repo>
44
+ cd agente_whatsapp
45
+ npm install
46
+ ```
47
+
48
+ ---
49
+
50
+ ## Configuración
51
+
52
+ Crea un archivo `.env` en la raíz del proyecto:
53
+
54
+ ```env
55
+ PORT=3000 # Puerto del servidor HTTP (default: 3000)
56
+ DELAY_MS=3000 # Delay por defecto entre mensajes en ms (default: 3000)
57
+ MAX_ACCOUNTS=3 # Máximo de cuentas WhatsApp activas simultáneas (default: 3)
58
+ ```
59
+
60
+ > `DELAY_MS` es solo el default global. Cada campaña puede sobreescribirlo con su propio `delay_ms`.
61
+
62
+ ---
63
+
64
+ ## Arranque
65
+
66
+ ```bash
67
+ # Modo producción
68
+ npm start
69
+
70
+ # Modo desarrollo (recarga automática ante cambios)
71
+ npm run dev
72
+
73
+ # Ejecutar tests
74
+ npm test
75
+ ```
76
+
77
+ El servidor arranca en `http://localhost:3000`.
78
+
79
+ Al arrancar, el sistema reconecta automáticamente todas las cuentas que tienen sesión guardada en `./sessions/` — no es necesario volver a escanear el QR.
80
+
81
+ ---
82
+
83
+ ## Despliegue con Docker
84
+
85
+ La opción recomendada para producción. El contenedor incluye Chromium del sistema (no descarga el suyo propio), lo que hace la imagen reproducible sin sorpresas de red.
86
+
87
+ ### Requisitos
88
+
89
+ - Docker + Docker Compose
90
+
91
+ ### Levantar el servidor
92
+
93
+ ```bash
94
+ # (Opcional) Copiar y ajustar variables de entorno
95
+ cp .env.example .env
96
+
97
+ # Primera vez — construir imagen y arrancar
98
+ docker compose up -d --build
99
+
100
+ # Ver logs
101
+ docker compose logs -f
102
+ ```
103
+
104
+ El servidor queda disponible en `http://localhost:3000`.
105
+
106
+ ### Verificar que funciona
107
+
108
+ ```bash
109
+ curl http://localhost:3000/health
110
+ # → {"status":"ok","timestamp":"..."}
111
+ ```
112
+
113
+ ### Persistencia de datos
114
+
115
+ Los datos sobreviven reinicios mediante volúmenes montados en el directorio local:
116
+
117
+ | Directorio local | Montura en contenedor | Contenido |
118
+ |---|---|---|
119
+ | `sessions/` | `/app/sessions` | Sesiones de WhatsApp (~500 MB por cuenta) |
120
+ | `data/` | `/app/data` | Base de datos SQLite (`data.db`) |
121
+ | `uploads/` | `/app/uploads` | Archivos CSV temporales |
122
+ | `campaign_images/` | `/app/campaign_images` | Imágenes de campañas (A/B testing) |
123
+
124
+ ### Comandos útiles
125
+
126
+ ```bash
127
+ docker compose restart # Reiniciar sin reconstruir
128
+ docker compose up -d --build # Actualizar tras cambios de código
129
+ docker compose down # Detener y eliminar contenedor
130
+ docker compose logs -f # Seguir logs en tiempo real
131
+ ```
132
+
133
+ ---
134
+
135
+ ## CLI global (`wa-blast`)
136
+
137
+ Tras instalar globalmente el CLI, el comando `wa-blast` queda disponible en el PATH para controlar cualquier servidor (local o remoto) desde scripts o desde otro equipo.
138
+
139
+ ### Instalación
140
+
141
+ ```bash
142
+ # En el directorio del repo clonado
143
+ npm install -g .
144
+
145
+ # Verificar
146
+ wa-blast accounts list
147
+ ```
148
+
149
+ ### Configurar la URL del servidor
150
+
151
+ ```bash
152
+ # Via flag en cada comando
153
+ wa-blast --url http://mi-servidor:3000 accounts list
154
+
155
+ # Via variable de entorno (más cómodo para scripts)
156
+ export WA_BLAST_URL=http://mi-servidor:3000
157
+ wa-blast accounts list
158
+ ```
159
+
160
+ ---
161
+
162
+ ## Arquitectura
163
+
164
+ ```
165
+ agente_whatsapp/
166
+ ├── src/
167
+ │ ├── server.js # Entry point: Express + arranque + auto-reconexión + scheduler
168
+ │ ├── db.js # SQLite (better-sqlite3) + schema + migraciones automáticas
169
+ │ ├── accountManager.js # Gestión de clientes whatsapp-web.js en memoria
170
+ │ ├── campaignRunner.js # Motor de envío: round-robin, anti-spam, scheduling, A/B
171
+ │ └── routes/
172
+ │ ├── accounts.js # Endpoints REST de cuentas
173
+ │ ├── campaigns.js # Endpoints REST de campañas e imágenes
174
+ │ └── inbound.js # Endpoints REST de campañas inbound
175
+ │ └── utils/
176
+ │ ├── csvParser.js # Parseo de CSV y XLSX
177
+ │ └── templateEngine.js # Reemplazo de {{variables}}
178
+ ├── cli/
179
+ │ └── index.js # CLI para uso por agente (salida JSON, exit codes)
180
+ ├── sessions/ # Sesiones de WhatsApp (LocalAuth, generado en runtime)
181
+ ├── uploads/ # Archivos CSV temporales (se limpian tras importar)
182
+ ├── campaign_images/ # Imágenes de campañas (persisten)
183
+ ├── data.db # Base de datos SQLite (generada en runtime)
184
+ ├── public/
185
+ │ └── index.html # Frontend web (SPA estático)
186
+ ├── test/
187
+ │ └── campaigns.test.js # Tests de integración (node:test + supertest)
188
+ ├── package.json
189
+ └── .env
190
+ ```
191
+
192
+ ### Componentes principales
193
+
194
+ | Componente | Responsabilidad |
195
+ |---|---|
196
+ | `server.js` | Arranca Express, monta rutas, reconecta cuentas, ejecuta scheduler de campañas |
197
+ | `db.js` | Conexión singleton a SQLite, schema y migraciones idempotentes |
198
+ | `accountManager.js` | `Map` en memoria de clientes whatsapp-web.js con sus estados y QR |
199
+ | `campaignRunner.js` | Distribuye contactos en round-robin, lanza un loop por cuenta, anti-spam, scheduling, A/B |
200
+ | `csvParser.js` | Lee CSV/XLSX, normaliza columnas, valida columna `phone` |
201
+ | `templateEngine.js` | Reemplaza `{{variable}}` con valores del objeto de datos del contacto |
202
+ | `cli/index.js` | CLI sin dependencias extra, salida JSON limpia, exit codes claros |
203
+
204
+ ---
205
+
206
+ ## Flujo de uso
207
+
208
+ ```
209
+ 1. Agregar cuenta WhatsApp → POST /api/accounts
210
+
211
+ 2. Escanear QR con el teléfono → GET /api/accounts/:id/qr
212
+
213
+ 3. Crear campaña (nombre + cuentas + template + opciones) → POST /api/campaigns
214
+
215
+ 4. (Opcional) Subir imagen(es) para A/B testing → POST /api/campaigns/:id/images
216
+
217
+ 5. Subir CSV/XLSX con contactos → POST /api/campaigns/:id/contacts
218
+
219
+ 6. Iniciar campaña → POST /api/campaigns/:id/start
220
+
221
+ 7. Monitorear / pausar / cancelar → GET /api/campaigns/:id
222
+ POST /api/campaigns/:id/pause
223
+ POST /api/campaigns/:id/cancel
224
+ ```
225
+
226
+ Si la campaña tiene límites de scheduling configurados, pasará automáticamente a estado `scheduled` al alcanzar el tope diario/horario o salir de la ventana de envío, y se reanudará sola cuando las condiciones lo permitan.
227
+
228
+ ---
229
+
230
+ ## Interfaz web
231
+
232
+ Abre `http://localhost:3000` en el navegador.
233
+
234
+ ### Panel Cuentas
235
+
236
+ - **Agregar cuenta**: ingresa un ID (ej. `micuenta`) y haz clic en AGREGAR
237
+ - **Escanear QR**: el botón QR aparece mientras la cuenta espera autenticación
238
+ - **LÍMITE/DÍA**: campo inline para configurar cuántos mensajes puede enviar esa cuenta por día (default 50)
239
+ - El estado cambia automáticamente: `pending` → `authenticated` → `ready`
240
+
241
+ ### Panel Campañas
242
+
243
+ - **Nueva campaña**: abre el modal con todos los parámetros (anti-spam + scheduling)
244
+ - **UPLOAD CSV**: sube contactos (solo en estado `pending`)
245
+ - **IMAGEN / 📷 N IMGS**: sube imágenes para A/B testing (hasta 5 variantes)
246
+ - **START**: inicia el envío (requiere cuentas `ready` y contactos cargados)
247
+ - **PAUSE / RESUME / CANCEL / DEL**: botones según estado
248
+ - **EDIT**: edita cualquier parámetro de campañas `pending`
249
+ - **TRACKING**: modal con funnel ACK en tiempo real, respuestas recibidas y stats por variante A/B
250
+
251
+ ### Estados de campaña en la UI
252
+
253
+ | Badge | Significado |
254
+ |---|---|
255
+ | `PENDING` | Lista para configurar |
256
+ | `RUNNING` | Enviando activamente |
257
+ | `PAUSED` | Pausada manualmente |
258
+ | `⏰ SCHEDULED` | Auto-suspendida por límite/horario; se reanuda sola |
259
+ | `DONE` | Completada |
260
+ | `CANCELLED` | Cancelada definitivamente |
261
+
262
+ ---
263
+
264
+ ## API HTTP
265
+
266
+ Base URL: `http://localhost:3000`
267
+
268
+ Todos los endpoints de recursos devuelven JSON. Los errores incluyen `{ "error": "..." }`.
269
+
270
+ ### Health check
271
+
272
+ ```
273
+ GET /health
274
+ ```
275
+ ```json
276
+ { "status": "ok", "timestamp": "2026-03-31T10:00:00.000Z" }
277
+ ```
278
+
279
+ ---
280
+
281
+ ### Cuentas — `/api/accounts`
282
+
283
+ #### Crear / inicializar cuenta
284
+
285
+ ```
286
+ POST /api/accounts
287
+ Content-Type: application/json
288
+
289
+ { "id": "micuenta" }
290
+ ```
291
+
292
+ Respuesta `202`:
293
+ ```json
294
+ {
295
+ "message": "Cuenta \"micuenta\" inicializando. Use GET /api/accounts/micuenta/qr para obtener el QR.",
296
+ "id": "micuenta",
297
+ "status": "pending"
298
+ }
299
+ ```
300
+
301
+ > La inicialización es asíncrona. El QR estará disponible en ~5-10 segundos.
302
+
303
+ ---
304
+
305
+ #### Listar cuentas
306
+
307
+ ```
308
+ GET /api/accounts
309
+ ```
310
+
311
+ ```json
312
+ [
313
+ {
314
+ "id": "micuenta",
315
+ "phone": "5491112345678",
316
+ "status": "ready",
317
+ "daily_limit": 50,
318
+ "created_at": "2026-03-31T10:00:00",
319
+ "in_memory": true
320
+ }
321
+ ]
322
+ ```
323
+
324
+ **Estados de cuenta:** `pending` → `authenticated` → `ready` | `disconnected`
325
+
326
+ ---
327
+
328
+ #### Obtener QR
329
+
330
+ ```
331
+ GET /api/accounts/:id/qr
332
+ ```
333
+
334
+ ```json
335
+ { "qr": "data:image/png;base64,iVBORw0KGgo..." }
336
+ ```
337
+
338
+ ---
339
+
340
+ #### Estado de una cuenta
341
+
342
+ ```
343
+ GET /api/accounts/:id/status
344
+ ```
345
+
346
+ ---
347
+
348
+ #### Editar cuenta
349
+
350
+ ```
351
+ PATCH /api/accounts/:id
352
+ Content-Type: application/json
353
+
354
+ { "daily_limit": 30 }
355
+ ```
356
+
357
+ Actualiza el límite diario de envío. Valor mínimo: 1.
358
+
359
+ ---
360
+
361
+ #### Reset de cuenta
362
+
363
+ ```
364
+ POST /api/accounts/:id/reset
365
+ ```
366
+
367
+ Borra la sesión guardada y fuerza un QR nuevo. Útil si la sesión quedó corrupta.
368
+
369
+ ---
370
+
371
+ #### Eliminar cuenta
372
+
373
+ ```
374
+ DELETE /api/accounts/:id
375
+ ```
376
+
377
+ Desconecta WhatsApp, destruye el cliente en memoria y elimina el registro de la DB.
378
+
379
+ ---
380
+
381
+ ### Campañas — `/api/campaigns`
382
+
383
+ #### Crear campaña
384
+
385
+ ```
386
+ POST /api/campaigns
387
+ Content-Type: application/json
388
+
389
+ {
390
+ "name": "Promo mayo",
391
+ "account_ids": ["cuenta1"],
392
+ "template": "Hola {{nombre}}, tu código es {{codigo}}",
393
+ "delay_ms": 4000,
394
+ "jitter_ms": 2000,
395
+ "typing_sim": 1,
396
+ "burst_size": 15,
397
+ "burst_pause_ms": 60000,
398
+ "daily_cap": 40,
399
+ "hourly_cap": 10,
400
+ "send_window_start": "09:00",
401
+ "send_window_end": "18:00",
402
+ "active_days": "1,2,3,4,5"
403
+ }
404
+ ```
405
+
406
+ **Campos:**
407
+
408
+ | Campo | Tipo | Req. | Default | Descripción |
409
+ |---|---|---|---|---|
410
+ | `name` | string | ✓ | — | Nombre de la campaña |
411
+ | `account_ids` | string[] | ✓ | — | IDs de cuentas WhatsApp asignadas |
412
+ | `template` | string | ✓ | — | Texto con `{{variables}}` |
413
+ | `delay_ms` | integer | — | env `DELAY_MS` | ms entre mensajes (mín. 1000) |
414
+ | `jitter_ms` | integer | — | 2000 | Variación aleatoria del delay |
415
+ | `typing_sim` | 0\|1 | — | 1 | Simular "escribiendo..." |
416
+ | `burst_size` | integer | — | 0 | Pausa cada N mensajes (0=off) |
417
+ | `burst_pause_ms` | integer | — | 60000 | Duración de la pausa periódica |
418
+ | `daily_cap` | integer | — | 0 | Máx. mensajes por día para esta campaña (0=usa límite de cuenta) |
419
+ | `hourly_cap` | integer | — | 0 | Máx. mensajes por hora (0=desactivado) |
420
+ | `send_window_start` | string | — | null | Inicio de ventana de envío (`HH:MM`) |
421
+ | `send_window_end` | string | — | null | Fin de ventana de envío (`HH:MM`) |
422
+ | `active_days` | string | — | null | Días activos: `"1,2,3,4,5"` (1=Lun … 7=Dom). `null`=todos |
423
+
424
+ > También se acepta `account_id` (string) para compatibilidad legacy con una sola cuenta.
425
+
426
+ Respuesta `201`: campaña creada con todos sus campos.
427
+
428
+ ---
429
+
430
+ #### Editar campaña
431
+
432
+ Solo campañas en estado `pending`. Acepta todos los mismos campos de creación (todos opcionales).
433
+
434
+ ```
435
+ PATCH /api/campaigns/:id
436
+ ```
437
+
438
+ ---
439
+
440
+ #### Subir contactos (CSV/XLSX)
441
+
442
+ Solo campañas en estado `pending`.
443
+
444
+ ```
445
+ POST /api/campaigns/:id/contacts
446
+ Content-Type: multipart/form-data
447
+
448
+ file=<archivo.csv>
449
+ ```
450
+
451
+ ---
452
+
453
+ #### Imágenes para A/B testing
454
+
455
+ Ver sección [Imágenes y A/B testing](#imágenes-y-ab-testing).
456
+
457
+ ```
458
+ GET /api/campaigns/:id/images
459
+ POST /api/campaigns/:id/images # multipart/form-data, campo: image
460
+ DELETE /api/campaigns/:id/images/:imgId
461
+ ```
462
+
463
+ ---
464
+
465
+ #### Iniciar / reanudar campaña
466
+
467
+ ```
468
+ POST /api/campaigns/:id/start
469
+ ```
470
+
471
+ Inicia o reanuda el envío. Funciona desde estados `pending`, `paused` y `scheduled`.
472
+
473
+ Los contactos se distribuyen en round-robin entre las cuentas `ready`. Si hay imágenes, también se asignan en round-robin (A/B).
474
+
475
+ ---
476
+
477
+ #### Pausar campaña
478
+
479
+ ```
480
+ POST /api/campaigns/:id/pause
481
+ ```
482
+
483
+ Funciona desde estados `running` y `scheduled`. Para reanudar, llama a `/start`.
484
+
485
+ ---
486
+
487
+ #### Cancelar campaña
488
+
489
+ ```
490
+ POST /api/campaigns/:id/cancel
491
+ ```
492
+
493
+ ---
494
+
495
+ #### Eliminar campaña
496
+
497
+ Solo en estados `pending`, `done`, `cancelled` y `scheduled`.
498
+
499
+ ```
500
+ DELETE /api/campaigns/:id
501
+ ```
502
+
503
+ Elimina en cascada contactos, imágenes del disco e mensajes entrantes asociados.
504
+
505
+ ---
506
+
507
+ #### Listar campañas
508
+
509
+ ```
510
+ GET /api/campaigns
511
+ ```
512
+
513
+ Incluye `stats`, `accounts` e `images` (variantes A/B) por campaña.
514
+
515
+ ---
516
+
517
+ #### Detalle de campaña
518
+
519
+ ```
520
+ GET /api/campaigns/:id
521
+ ```
522
+
523
+ ---
524
+
525
+ #### Tracking ACK
526
+
527
+ ```
528
+ GET /api/campaigns/:id/tracking
529
+ ```
530
+
531
+ ```json
532
+ {
533
+ "campaign": { "id": 1, "name": "Promo mayo", "status": "scheduled" },
534
+ "total": 500,
535
+ "sent": 80,
536
+ "ack": [
537
+ { "ack_status": "delivered", "count": 65 },
538
+ { "ack_status": "read", "count": 15 }
539
+ ],
540
+ "replies": 3,
541
+ "image_variants": [
542
+ {
543
+ "label": "A", "image_id": 1, "original_name": "promo_verano.jpg",
544
+ "total": 250, "sent": 40, "failed": 0, "delivered": 35, "read": 8
545
+ },
546
+ {
547
+ "label": "B", "image_id": 2, "original_name": "promo_invierno.jpg",
548
+ "total": 250, "sent": 40, "failed": 0, "delivered": 30, "read": 7
549
+ }
550
+ ]
551
+ }
552
+ ```
553
+
554
+ `image_variants` solo aparece si la campaña tiene imágenes configuradas.
555
+
556
+ ---
557
+
558
+ #### Respuestas recibidas
559
+
560
+ ```
561
+ GET /api/campaigns/:id/replies
562
+ ```
563
+
564
+ ---
565
+
566
+ ## CLI
567
+
568
+ ```bash
569
+ # Instalado globalmente
570
+ wa-blast <resource> <action> [args] [flags]
571
+
572
+ # Directo
573
+ node cli/index.js <resource> <action> [args] [flags]
574
+ ```
575
+
576
+ ### CLI: Cuentas
577
+
578
+ ```bash
579
+ wa-blast accounts list
580
+ wa-blast accounts add micuenta
581
+ wa-blast accounts status micuenta
582
+ wa-blast accounts qr micuenta
583
+ wa-blast accounts edit micuenta --daily-limit 30
584
+ wa-blast accounts reset micuenta
585
+ wa-blast accounts delete micuenta
586
+ wa-blast accounts messages micuenta
587
+ ```
588
+
589
+ ---
590
+
591
+ ### CLI: Campañas
592
+
593
+ ```bash
594
+ # Listar y ver detalle
595
+ wa-blast campaigns list
596
+ wa-blast campaigns show 1
597
+
598
+ # Crear campaña básica
599
+ wa-blast campaigns create \
600
+ --name "Promo mayo" \
601
+ --accounts "cuenta1" \
602
+ --template "Hola {{nombre}}, tu oferta: {{oferta}}" \
603
+ --delay 4000
604
+
605
+ # Crear campaña con scheduling completo
606
+ wa-blast campaigns create \
607
+ --name "Campaña semana laboral" \
608
+ --accounts "cuenta1" \
609
+ --template "Hola {{nombre}}" \
610
+ --delay 4000 \
611
+ --jitter 2000 \
612
+ --typing 1 \
613
+ --burst-size 15 \
614
+ --burst-pause 60000 \
615
+ --daily-cap 40 \
616
+ --hourly-cap 10 \
617
+ --window-start 09:00 \
618
+ --window-end 18:00 \
619
+ --active-days "1,2,3,4,5"
620
+
621
+ # Subir contactos
622
+ wa-blast campaigns upload 1 ./contactos.csv
623
+
624
+ # Imágenes A/B testing
625
+ wa-blast campaigns images 1
626
+ wa-blast campaigns image-add 1 ./imagen_verano.jpg
627
+ wa-blast campaigns image-add 1 ./imagen_invierno.jpg
628
+ wa-blast campaigns image-delete 1 2
629
+
630
+ # Ciclo de vida
631
+ wa-blast campaigns start 1
632
+ wa-blast campaigns pause 1
633
+ wa-blast campaigns cancel 1
634
+ wa-blast campaigns delete 1
635
+
636
+ # Editar campaña pending
637
+ wa-blast campaigns edit 1 \
638
+ --daily-cap 30 \
639
+ --window-start 10:00 \
640
+ --window-end 17:00 \
641
+ --active-days "1,2,3,4,5"
642
+
643
+ # Esperar resultado (imprime puntos hasta done/paused/cancelled)
644
+ wa-blast campaigns wait 1
645
+ wa-blast campaigns wait 1 --interval 10
646
+
647
+ # Stats y respuestas
648
+ wa-blast campaigns tracking 1
649
+ wa-blast campaigns replies 1
650
+ ```
651
+
652
+ ---
653
+
654
+ ### Flags de `campaigns create` y `campaigns edit`
655
+
656
+ **Anti-spam:**
657
+
658
+ | Flag | Descripción | Default |
659
+ |---|---|---|
660
+ | `--name <texto>` | Nombre | — |
661
+ | `--accounts <c1,c2>` | Cuentas separadas por coma | — |
662
+ | `--template <texto>` | Plantilla del mensaje | — |
663
+ | `--delay <ms>` | Delay base entre mensajes | 3000 |
664
+ | `--jitter <ms>` | Variación aleatoria del delay | 2000 |
665
+ | `--typing <0\|1>` | Simular "escribiendo..." | 1 |
666
+ | `--burst-size <n>` | Pausa cada N mensajes (0=off) | 0 |
667
+ | `--burst-pause <ms>` | Duración de la pausa periódica | 60000 |
668
+
669
+ **Scheduling:**
670
+
671
+ | Flag | Descripción | Default |
672
+ |---|---|---|
673
+ | `--daily-cap <n>` | Máx. mensajes/día para esta campaña (0=usa cuenta) | 0 |
674
+ | `--hourly-cap <n>` | Máx. mensajes/hora (0=desactivado) | 0 |
675
+ | `--window-start HH:MM` | Inicio de ventana de envío | null |
676
+ | `--window-end HH:MM` | Fin de ventana de envío | null |
677
+ | `--active-days <1,2,3,4,5>` | Días activos (1=Lun … 7=Dom) | null (todos) |
678
+
679
+ ---
680
+
681
+ ### Ejemplo completo con CLI (uso por agente)
682
+
683
+ ```bash
684
+ export WA_BLAST_URL=http://mi-servidor:3000
685
+
686
+ # 1. Verificar cuenta lista
687
+ wa-blast accounts status cuenta1
688
+
689
+ # 2. Crear campaña con límite 50/día, solo días hábiles 9-18h
690
+ CAMPAIGN=$(wa-blast campaigns create \
691
+ --name "Promo 500 contactos" \
692
+ --accounts "cuenta1" \
693
+ --template "Hola {{nombre}}, tu descuento es {{descuento}}%" \
694
+ --delay 4000 --jitter 2000 --typing 1 \
695
+ --daily-cap 50 --window-start 09:00 --window-end 18:00 \
696
+ --active-days "1,2,3,4,5")
697
+
698
+ ID=$(echo $CAMPAIGN | jq '.id')
699
+
700
+ # 3. Agregar imagen (opcional)
701
+ wa-blast campaigns image-add $ID ./promo.jpg
702
+
703
+ # 4. Subir 500 contactos
704
+ wa-blast campaigns upload $ID ./contactos.csv
705
+
706
+ # 5. Iniciar — enviará 50 hoy, quedará en 'scheduled', se reanuda sola mañana
707
+ wa-blast campaigns start $ID
708
+
709
+ # 6. Monitorear
710
+ wa-blast campaigns show $ID | jq '{status: .status, stats: .stats}'
711
+
712
+ # 7. Ver tracking con stats por variante
713
+ wa-blast campaigns tracking $ID
714
+ ```
715
+
716
+ ---
717
+
718
+ ## Comportamiento anti-spam
719
+
720
+ El sistema implementa cuatro capas de humanización para reducir el riesgo de ban por parte de Meta.
721
+
722
+ ### 1. Jitter en el delay
723
+
724
+ ```
725
+ delay_final = delay_ms + random(0, jitter_ms)
726
+ ```
727
+
728
+ Ejemplo: `delay_ms=4000` + `jitter_ms=2000` → espera entre **4s y 6s** por mensaje.
729
+
730
+ ### 2. Simulación de escritura (`typing_sim`)
731
+
732
+ Antes de enviar: activa "escribiendo...", espera proporcional al largo del mensaje (±30%), detiene el estado, envía.
733
+
734
+ ```
735
+ base = clamp(largo × 60ms, 1500ms, 7000ms)
736
+ tiempo = base × (0.7 + random × 0.6)
737
+ ```
738
+
739
+ ### 3. Presencia online
740
+
741
+ Al inicio de cada loop de cuenta se llama `sendPresenceAvailable()`.
742
+
743
+ ### 4. Pausa periódica (burst pause)
744
+
745
+ Cada `burst_size` mensajes, pausa de `burst_pause_ms`. Ejemplo: `burst_size=15` + `burst_pause_ms=60000` → pausa de 1 minuto cada 15 mensajes.
746
+
747
+ ### Valores recomendados por volumen
748
+
749
+ | Volumen | `delay_ms` | `jitter_ms` | `typing_sim` | `burst_size` | `burst_pause_ms` |
750
+ |---|---|---|---|---|---|
751
+ | Bajo (<100/día) | 3000 | 1000 | 1 | 0 | — |
752
+ | Medio (100-500/día) | 4000 | 2000 | 1 | 20 | 60000 |
753
+ | Alto (500+/día) | 5000 | 3000 | 1 | 15 | 90000 |
754
+
755
+ ---
756
+
757
+ ## Scheduling y límites de envío
758
+
759
+ Permite que una campaña se distribuya automáticamente en varios días respetando límites de WhatsApp.
760
+
761
+ ### Límite diario por cuenta
762
+
763
+ Cada cuenta tiene un `daily_limit` (default: **50**). Cuando una cuenta alcanza su límite del día, la campaña pasa automáticamente a estado `scheduled` y el scheduler la reanuda al día siguiente.
764
+
765
+ Configurar via UI (campo "LÍMITE/DÍA" en la tarjeta de cuenta) o CLI:
766
+
767
+ ```bash
768
+ wa-blast accounts edit cuenta1 --daily-limit 40
769
+ ```
770
+
771
+ ### Parámetros de scheduling por campaña
772
+
773
+ | Parámetro | Descripción |
774
+ |---|---|
775
+ | `daily_cap` | Tope diario de la campaña. Si `0`, usa el `daily_limit` de la cuenta. Útil para usar solo una parte del cupo cuando hay varias campañas activas. |
776
+ | `hourly_cap` | Tope por hora. `0` = sin límite horario. |
777
+ | `send_window_start` / `send_window_end` | Solo envía dentro de este rango horario (formato `HH:MM`). |
778
+ | `active_days` | Días de la semana activos: `"1,2,3,4,5"` = lunes a viernes. `1`=Lun, `2`=Mar, … `7`=Dom. `null` o vacío = todos los días. |
779
+
780
+ ### Estado `scheduled`
781
+
782
+ Cuando cualquier límite se alcanza o la campaña está fuera de ventana, el runner cambia automáticamente el estado a `scheduled`:
783
+
784
+ ```
785
+ running → [límite alcanzado o fuera de ventana] → scheduled
786
+ scheduled → [condiciones ok, scheduler cada 60s] → running
787
+ ```
788
+
789
+ Desde la UI/CLI también se puede resumir manualmente una campaña `scheduled`.
790
+
791
+ ### Ejemplo: 500 contactos con cuenta de 50/día
792
+
793
+ Con `daily_cap=50`, ventana `09:00-18:00`, días `1,2,3,4,5`:
794
+
795
+ - **Día 1 (lunes)**: runner envía 50 → `scheduled`
796
+ - **Día 2 (martes)**: scheduler detecta nuevo día, ventana activa → `running` → envía 50 → `scheduled`
797
+ - …10 días hábiles después → `done`
798
+
799
+ > **Nota**: Si la campaña usa múltiples cuentas y una de ellas alcanza su `daily_limit`, la campaña completa pasa a `scheduled`. El scheduler la reanudará cuando al menos una cuenta tenga capacidad.
800
+
801
+ ---
802
+
803
+ ## Imágenes y A/B testing
804
+
805
+ Permite adjuntar hasta **5 imágenes** (variantes A-E) a una campaña. Se distribuyen en round-robin entre los contactos y se trackea la performance de cada variante.
806
+
807
+ ### Subir imágenes
808
+
809
+ ```bash
810
+ # Subir variante A
811
+ wa-blast campaigns image-add 1 ./imagen_a.jpg
812
+ # → { "id": 1, "label": "A", "original_name": "imagen_a.jpg", ... }
813
+
814
+ # Subir variante B
815
+ wa-blast campaigns image-add 1 ./imagen_b.png
816
+ # → { "id": 2, "label": "B", ... }
817
+ ```
818
+
819
+ Formatos aceptados: `.jpg`, `.jpeg`, `.png`, `.gif`, `.webp`. Máximo 5MB por imagen.
820
+
821
+ ### Distribución
822
+
823
+ Al iniciar la campaña, los contactos se asignan en round-robin a las variantes:
824
+
825
+ - 9 contactos + 3 variantes → 3 contactos por variante (A, B, C, A, B, C, ...)
826
+ - Si hay solo 1 imagen → todos los contactos reciben esa imagen (imagen única sin A/B)
827
+
828
+ ### Envío
829
+
830
+ La imagen se envía con el texto del template como **caption**.
831
+
832
+ ### Tracking por variante
833
+
834
+ El endpoint `GET /api/campaigns/:id/tracking` incluye `image_variants` con métricas separadas por variante: total, sent, delivered, read.
835
+
836
+ En la UI, el tab "A/B IMAGES" en el modal de tracking muestra estas métricas.
837
+
838
+ ### Eliminar imagen
839
+
840
+ ```bash
841
+ wa-blast campaigns image-delete 1 2 # elimina imagen con id=2
842
+ ```
843
+
844
+ Al eliminar, los labels se reordenan automáticamente (A, B, C, ... consecutivos).
845
+
846
+ > Las imágenes solo se pueden agregar/eliminar en campañas en estado `pending`.
847
+
848
+ ---
849
+
850
+ ## Campañas inbound
851
+
852
+ Las campañas inbound son auto-respuestas que se envían automáticamente cuando un contacto escribe a una cuenta WhatsApp. Soportan distribución de cupones únicos por contacto.
853
+
854
+ ### Endpoints
855
+
856
+ ```
857
+ GET /api/inbound
858
+ POST /api/inbound
859
+ GET /api/inbound/:id
860
+ POST /api/inbound/:id/activate
861
+ POST /api/inbound/:id/pause
862
+ DELETE /api/inbound/:id
863
+ POST /api/inbound/:id/coupons # multipart, campo: file (.txt/.csv)
864
+ GET /api/inbound/:id/coupons
865
+ GET /api/inbound/:id/contacts
866
+ GET /api/inbound/:id/contacts/export # descarga CSV
867
+ POST /api/inbound/:id/to-campaign # crear campaña outbound con los contactos inbound
868
+ ```
869
+
870
+ ### Parámetros
871
+
872
+ | Campo | Descripción |
873
+ |---|---|
874
+ | `account_id` | Cuenta que recibirá mensajes entrantes |
875
+ | `name` | Nombre descriptivo |
876
+ | `template` | Mensaje de respuesta (soporta `{{nombre}}` y `{{cupon}}`) |
877
+ | `max_uses` | Máximo de usos (0=ilimitado) |
878
+ | `reply_mode` | `once_per_campaign` (default), `always`, `new_contacts_only` |
879
+
880
+ ### CLI inbound
881
+
882
+ ```bash
883
+ wa-blast inbound list
884
+ wa-blast inbound create --account cuenta1 --name "Promo QR" --template "Tu cupón: {{cupon}}"
885
+ wa-blast inbound coupons-upload 1 ./cupones.txt
886
+ wa-blast inbound coupons 1
887
+ wa-blast inbound contacts 1
888
+ wa-blast inbound export 1 > contactos.csv
889
+ wa-blast inbound to-campaign 1 --name "Follow-up" --template "Hola {{nombre}}" --accounts "cuenta1"
890
+ wa-blast inbound activate 1
891
+ wa-blast inbound pause 1
892
+ wa-blast inbound delete 1
893
+ ```
894
+
895
+ ---
896
+
897
+ ## Formato de archivos de contactos
898
+
899
+ Se aceptan **CSV** y **Excel (.xlsx, .xls)**. Tamaño máximo: 10MB.
900
+
901
+ La columna de teléfono puede llamarse: `phone`, `telefono`, `numero`, `número`.
902
+
903
+ El resto de columnas se convierten en variables para el template.
904
+
905
+ ### Ejemplo
906
+
907
+ ```csv
908
+ phone,nombre,empresa,descuento
909
+ 5491112345678,Juan García,Acme SA,20%
910
+ 5491133445566,María López,Tech Corp,15%
911
+ 5491144556677,Carlos Martínez,StartupXYZ,30%
912
+ ```
913
+
914
+ ### Formato de teléfonos
915
+
916
+ - Incluir **código de país** sin el símbolo `+`
917
+ - Se eliminan automáticamente: espacios, guiones, paréntesis, `+`
918
+ - Argentina: `54` + `9` + código de área + número → `5491112345678`
919
+
920
+ ---
921
+
922
+ ## Sistema de plantillas
923
+
924
+ ```
925
+ Hola {{nombre}}, te contactamos de {{empresa}}.
926
+ Tu código es {{codigo}} válido hasta {{vencimiento}}.
927
+ ```
928
+
929
+ Si una variable no existe para ese contacto, el placeholder queda tal cual.
930
+
931
+ Cuando la campaña tiene imagen, el template se envía como **caption** de la imagen.
932
+
933
+ ---
934
+
935
+ ## Base de datos
936
+
937
+ El archivo `data.db` (SQLite) se crea automáticamente. Las migraciones son idempotentes.
938
+
939
+ ### `accounts`
940
+
941
+ | Columna | Tipo | Default | Descripción |
942
+ |---|---|---|---|
943
+ | `id` | TEXT PK | — | Identificador de cuenta |
944
+ | `phone` | TEXT | — | Número de teléfono (disponible tras autenticar) |
945
+ | `status` | TEXT | `pending` | `pending` / `authenticated` / `ready` / `disconnected` |
946
+ | `daily_limit` | INTEGER | 50 | Máx. mensajes que esta cuenta puede enviar por día |
947
+ | `created_at` | TEXT | now | |
948
+
949
+ ### `campaigns`
950
+
951
+ | Columna | Tipo | Default | Descripción |
952
+ |---|---|---|---|
953
+ | `id` | INTEGER PK | autoincrement | |
954
+ | `name` | TEXT | — | Nombre |
955
+ | `template` | TEXT | — | Plantilla con `{{...}}` |
956
+ | `status` | TEXT | `pending` | `pending` / `running` / `paused` / `scheduled` / `done` / `cancelled` |
957
+ | `delay_ms` | INTEGER | 3000 | Delay base entre mensajes |
958
+ | `jitter_ms` | INTEGER | 2000 | Variación aleatoria del delay |
959
+ | `typing_sim` | INTEGER | 1 | Simular escritura (0/1) |
960
+ | `burst_size` | INTEGER | 0 | Pausa cada N mensajes |
961
+ | `burst_pause_ms` | INTEGER | 60000 | Duración de pausa periódica |
962
+ | `daily_cap` | INTEGER | 0 | Tope diario de campaña (0=usa daily_limit de cuenta) |
963
+ | `hourly_cap` | INTEGER | 0 | Tope por hora (0=desactivado) |
964
+ | `send_window_start` | TEXT | null | Inicio de ventana horaria (`HH:MM`) |
965
+ | `send_window_end` | TEXT | null | Fin de ventana horaria (`HH:MM`) |
966
+ | `active_days` | TEXT | null | Días activos como CSV de números 1-7 |
967
+ | `created_at` | TEXT | now | |
968
+
969
+ ### `campaign_accounts`
970
+
971
+ Relación N:N entre campañas y cuentas.
972
+
973
+ ### `campaign_images`
974
+
975
+ | Columna | Tipo | Descripción |
976
+ |---|---|---|
977
+ | `id` | INTEGER PK | |
978
+ | `campaign_id` | INTEGER FK | |
979
+ | `label` | TEXT | `A`, `B`, `C`, `D` o `E` |
980
+ | `file_path` | TEXT | Ruta relativa en `campaign_images/` |
981
+ | `original_name` | TEXT | Nombre original del archivo |
982
+ | `created_at` | TEXT | |
983
+
984
+ ### `contacts`
985
+
986
+ | Columna | Tipo | Descripción |
987
+ |---|---|---|
988
+ | `id` | INTEGER PK | |
989
+ | `campaign_id` | INTEGER FK | |
990
+ | `phone` | TEXT | Número destino |
991
+ | `data` | TEXT | JSON con variables del CSV |
992
+ | `status` | TEXT | `pending` / `sent` / `failed` |
993
+ | `error` | TEXT | Mensaje de error si falló |
994
+ | `sent_at` | TEXT | Timestamp de envío exitoso |
995
+ | `assigned_account_id` | TEXT | Cuenta asignada (round-robin) |
996
+ | `image_id` | INTEGER FK | Variante de imagen asignada (A/B testing) |
997
+ | `message_id` | TEXT | ID interno del mensaje en WhatsApp |
998
+ | `ack_status` | TEXT | `sent` / `delivered` / `read` / `played` |
999
+ | `delivered_at` | TEXT | Timestamp de entrega |
1000
+ | `read_at` | TEXT | Timestamp de lectura |
1001
+
1002
+ ### `incoming_messages`
1003
+
1004
+ Mensajes recibidos de contactos durante campañas activas.
1005
+
1006
+ ### `inbound_campaigns`, `inbound_coupons`, `inbound_replies`
1007
+
1008
+ Tablas para el sistema de auto-respuesta inbound con distribución de cupones.
1009
+
1010
+ ---
1011
+
1012
+ ## Solución de problemas
1013
+
1014
+ ### El QR no aparece
1015
+
1016
+ - Esperar ~10 segundos después de `accounts add`
1017
+ - Verificar que el servidor esté corriendo: `npm start`
1018
+ - Revisar logs: buscar `[clientId] QR generado`
1019
+
1020
+ ### "Ninguna cuenta asignada está lista (ready)"
1021
+
1022
+ - La cuenta debe estar en estado `ready` antes de iniciar
1023
+ - Verificar: `wa-blast accounts status <id>`
1024
+
1025
+ ### La campaña queda en `scheduled` y no reanuda
1026
+
1027
+ - Verificar que la hora actual esté dentro de `send_window_start` y `send_window_end`
1028
+ - Verificar que hoy sea un `active_day`
1029
+ - Verificar que la cuenta no haya alcanzado su `daily_limit`: `wa-blast campaigns tracking <id>`
1030
+ - El scheduler corre cada 60s; esperar o resumir manualmente con `wa-blast campaigns start <id>`
1031
+
1032
+ ### La campaña queda en `running` indefinidamente
1033
+
1034
+ - Si todas las cuentas asignadas se desconectaron, el loop no termina
1035
+ - Pausar o cancelar manualmente, reconectar cuentas y reiniciar
1036
+
1037
+ ### Error de compilación de `better-sqlite3`
1038
+
1039
+ - Requiere `better-sqlite3 v12+` con Node.js v24
1040
+ - Reinstalar: `npm install better-sqlite3@latest`
1041
+
1042
+ ### Sesiones perdidas tras reiniciar
1043
+
1044
+ - Las sesiones se guardan en `./sessions/` via `LocalAuth`
1045
+ - El servidor reconecta automáticamente al arrancar
1046
+ - Si se borra `sessions/`, hay que escanear QR de nuevo
1047
+ - Usar `POST /api/accounts/:id/reset` para forzar un QR nuevo
1048
+
1049
+ ### Cuenta baneada o mensajes no entregados
1050
+
1051
+ - Aumentar `delay_ms` y `jitter_ms`
1052
+ - Activar `typing_sim=1`
1053
+ - Configurar `burst_size` para pausas periódicas
1054
+ - Respetar el `daily_limit` de 50 mensajes/día por cuenta
1055
+ - Una cuenta nueva necesita warm-up gradual (no enviar 100 el primer día)
1056
+
1057
+ ### Límite de cuentas activas superado
1058
+
1059
+ - Default es `MAX_ACCOUNTS=3` en `.env`
1060
+ - Aumentar o eliminar cuentas inactivas: `wa-blast accounts delete <id>`