@gnpdev/rpa-tools 1.3.0 → 1.5.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.
- package/README.md +110 -14
- package/package.json +1 -1
- package/src/capture.js +54 -7
- package/src/gateway.js +293 -0
- package/src/index.d.ts +47 -0
- package/src/index.js +41 -1
package/README.md
CHANGED
|
@@ -33,20 +33,20 @@ const pool = new Pool({
|
|
|
33
33
|
|
|
34
34
|
// 2. Inicializa las herramientas
|
|
35
35
|
export const rpa = await createRpaTools({
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
36
|
+
botId: process.env.RPA_BOT_ID,
|
|
37
|
+
db: pool,
|
|
38
|
+
minio: {
|
|
39
|
+
endPoint: process.env.MINIO_ENDPOINT,
|
|
40
|
+
port: parseInt(process.env.MINIO_PORT || '9000'),
|
|
41
|
+
useSSL: process.env.MINIO_USE_SSL === 'true',
|
|
42
|
+
accessKey: process.env.MINIO_ACCESS_KEY,
|
|
43
|
+
secretKey: process.env.MINIO_SECRET_KEY,
|
|
44
|
+
bucket: 'rpa-screenshots'
|
|
45
|
+
},
|
|
46
|
+
log: {
|
|
47
|
+
level: process.env.NODE_ENV === 'development' ? 'debug' : 'info',
|
|
48
|
+
pretty: process.env.NODE_ENV !== 'production',
|
|
49
|
+
}
|
|
50
50
|
})
|
|
51
51
|
|
|
52
52
|
// 3. Exporta las herramientas
|
|
@@ -184,3 +184,99 @@ MINIO_SECRET_KEY=password
|
|
|
184
184
|
MINIO_BUCKET=rpa-screenshots
|
|
185
185
|
DATABASE_URL=postgres://user:pass@localhost:5432/db
|
|
186
186
|
```
|
|
187
|
+
screenshot: {
|
|
188
|
+
format: 'webp',
|
|
189
|
+
quality: 70,
|
|
190
|
+
captureQuality: 90,
|
|
191
|
+
},
|
|
192
|
+
// Opcional: Redis para live frame y heartbeat
|
|
193
|
+
redis: new Redis({ host: process.env.REDIS_HOST || 'localhost' }),
|
|
194
|
+
// Opcional: Gateway para registro centralizado de agentes
|
|
195
|
+
gateway: {
|
|
196
|
+
url: process.env.GATEWAY_URL,
|
|
197
|
+
apiKey: process.env.GATEWAY_API_KEY,
|
|
198
|
+
capabilities: ['screenshots'],
|
|
199
|
+
},
|
|
200
|
+
})
|
|
201
|
+
|
|
202
|
+
// 3. Exporta las herramientas
|
|
203
|
+
export const { logger, state, step } = rpa;
|
|
204
|
+
#### D. Live Frame (requiere Redis)
|
|
205
|
+
Captura la página actual y sobrescribe SIEMPRE la misma key en MinIO (`live/{appId}/{instancia}/{usuario}/live.webp`).
|
|
206
|
+
Una sola imagen por agente, ideal para monitoreo en tiempo real desde el frontend.
|
|
207
|
+
|
|
208
|
+
```javascript
|
|
209
|
+
import { rpa } from './lib/rpa.js';
|
|
210
|
+
|
|
211
|
+
// Captura live (se actualiza en la misma key, no acumula)
|
|
212
|
+
const result = await rpa.captureLive(page, {
|
|
213
|
+
usuario: 'agente-01',
|
|
214
|
+
instancia: process.env.HOSTNAME, // opcional, default: HOSTNAME
|
|
215
|
+
fullPage: false,
|
|
216
|
+
metadata: { caso: 'INC-123' },
|
|
217
|
+
});
|
|
218
|
+
// result → { key: 'live/.../live.webp', updatedAt: '2026-06-05T...' }
|
|
219
|
+
```
|
|
220
|
+
|
|
221
|
+
Para marcar el agente como inactivo al finalizar:
|
|
222
|
+
|
|
223
|
+
```javascript
|
|
224
|
+
await rpa.setOffline({ usuario: 'agente-01' });
|
|
225
|
+
// Sube un placeholder negro "AGENTE INACTIVO" a la misma key
|
|
226
|
+
```
|
|
227
|
+
|
|
228
|
+
### 6. Heartbeat y Estado del Agente en Redis
|
|
229
|
+
Cuando configuras `redis` en `createRpaTools()`, el agente escribe su estado automaticamente:
|
|
230
|
+
|
|
231
|
+
```javascript
|
|
232
|
+
// Heartbeat periodico (Redis con TTL - expira solo si el agente crashea)
|
|
233
|
+
await rpa.heartbeat('agente-01', { cpu: 45, memoria: 1024 });
|
|
234
|
+
|
|
235
|
+
// Verificar si el agente debe pausarse (status === 'paused')
|
|
236
|
+
const pausar = await rpa.shouldPause('agente-01');
|
|
237
|
+
|
|
238
|
+
// Actualizar el caso activo
|
|
239
|
+
await rpa.setCurrentCase('agente-01', 'INC-456');
|
|
240
|
+
```
|
|
241
|
+
|
|
242
|
+
**Keys en Redis:**
|
|
243
|
+
| Key | TTL | Descripcion |
|
|
244
|
+
| `agent:{usuario}:status` | 180s | `active` o `paused` |
|
|
245
|
+
| `agent:{usuario}:heartbeat` | 120s | Ultimo heartbeat ISO |
|
|
246
|
+
| `agent:{usuario}:current_case` | 180s | Caso activo |
|
|
247
|
+
| `agent:{usuario}:screenshot_updated_at` | - | Timestamp del ultimo live frame |
|
|
248
|
+
|
|
249
|
+
### 7. Gateway de Agentes (opcional)
|
|
250
|
+
Si configuras `gateway` en `createRpaTools()`, el agente se registra automaticamente al iniciar
|
|
251
|
+
y envia heartbeats cada 30s. El gateway centraliza el estado de todos los agentes.
|
|
252
|
+
|
|
253
|
+
```javascript
|
|
254
|
+
// El constructor ya lo hace automaticamente si gateway.url esta configurado:
|
|
255
|
+
// - POST /agents/register al iniciar
|
|
256
|
+
// - POST /agents/heartbeat cada 30s
|
|
257
|
+
// - POST /api/instances/{id}/logs para logs (via createLogStream)
|
|
258
|
+
|
|
259
|
+
// Opcionalmente, transmitir logs de Pino al gateway:
|
|
260
|
+
import pino from 'pino';
|
|
261
|
+
const logger = pino({
|
|
262
|
+
level: 'info',
|
|
263
|
+
}, pino.multistream([
|
|
264
|
+
{ stream: process.stdout },
|
|
265
|
+
{ stream: rpa.gateway.createLogStream({ batchMs: 2000 }) },
|
|
266
|
+
]));
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
**Propiedades:**
|
|
270
|
+
```javascript
|
|
271
|
+
if (rpa.gateway) {
|
|
272
|
+
console.log(rpa.gateway.gatewayUrl); // URL configurada
|
|
273
|
+
console.log(rpa.gateway.isConfigured); // true
|
|
274
|
+
}
|
|
275
|
+
```
|
|
276
|
+
|
|
277
|
+
# Opcional: Redis para live frame y heartbeat
|
|
278
|
+
REDIS_HOST=localhost
|
|
279
|
+
REDIS_PORT=6379
|
|
280
|
+
# Opcional: Gateway para registro centralizado
|
|
281
|
+
GATEWAY_URL=http://gateway:3000
|
|
282
|
+
GATEWAY_API_KEY=eyJhbGci...
|
package/package.json
CHANGED
package/src/capture.js
CHANGED
|
@@ -9,6 +9,9 @@ const os = require('os');
|
|
|
9
9
|
* - capture() → captura única con key timestampeda (histórica)
|
|
10
10
|
* - captureLive() → sobrescribe siempre la misma key (live frame)
|
|
11
11
|
* - setOffline() → sube placeholder negro a la key live (agente inactivo)
|
|
12
|
+
* - heartbeat() → escribe heartbeat del agente en Redis
|
|
13
|
+
* - shouldPause() → verifica si el agente debe pausarse
|
|
14
|
+
* - setCurrentCase()→ actualiza el caso activo en Redis
|
|
12
15
|
*/
|
|
13
16
|
class PageCapturer {
|
|
14
17
|
/**
|
|
@@ -104,7 +107,6 @@ class PageCapturer {
|
|
|
104
107
|
.webp({ quality: 70 })
|
|
105
108
|
.toBuffer();
|
|
106
109
|
|
|
107
|
-
// Sobrescribe la misma key con headers anti-cache
|
|
108
110
|
await this.minioClient.putObject(this.bucket, key, finalBuffer, finalBuffer.length, {
|
|
109
111
|
'Content-Type': 'image/webp',
|
|
110
112
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
@@ -114,7 +116,6 @@ class PageCapturer {
|
|
|
114
116
|
|
|
115
117
|
const timestamp = now.toISOString();
|
|
116
118
|
|
|
117
|
-
// Actualizar metadata en Redis
|
|
118
119
|
if (this.redis) {
|
|
119
120
|
await this.redis.set(`agent:${usuario}:screenshot_updated_at`, timestamp);
|
|
120
121
|
await this.redis.set(`agent:${usuario}:screenshot_key`, key);
|
|
@@ -158,7 +159,6 @@ class PageCapturer {
|
|
|
158
159
|
try {
|
|
159
160
|
const key = `live/${this.botId}/${instancia}/${usuario}/live.webp`;
|
|
160
161
|
|
|
161
|
-
// Generar placeholder offline vía SVG (sharp lo rasteriza)
|
|
162
162
|
let finalBuffer;
|
|
163
163
|
|
|
164
164
|
try {
|
|
@@ -177,7 +177,6 @@ class PageCapturer {
|
|
|
177
177
|
.webp({ quality: 50 })
|
|
178
178
|
.toBuffer();
|
|
179
179
|
} catch {
|
|
180
|
-
// Fallback: pixel negro 1x1 si SVG falla
|
|
181
180
|
finalBuffer = await sharp({
|
|
182
181
|
create: {
|
|
183
182
|
width: 1,
|
|
@@ -188,7 +187,6 @@ class PageCapturer {
|
|
|
188
187
|
}).webp().toBuffer();
|
|
189
188
|
}
|
|
190
189
|
|
|
191
|
-
// Sobrescribe la misma key live con headers anti-cache
|
|
192
190
|
await this.minioClient.putObject(this.bucket, key, finalBuffer, finalBuffer.length, {
|
|
193
191
|
'Content-Type': 'image/webp',
|
|
194
192
|
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
@@ -198,7 +196,6 @@ class PageCapturer {
|
|
|
198
196
|
|
|
199
197
|
const timestamp = new Date().toISOString();
|
|
200
198
|
|
|
201
|
-
// Actualizar Redis
|
|
202
199
|
if (this.redis) {
|
|
203
200
|
await this.redis.set(`agent:${usuario}:status`, 'paused');
|
|
204
201
|
await this.redis.set(`agent:${usuario}:screenshot_updated_at`, timestamp);
|
|
@@ -218,7 +215,57 @@ class PageCapturer {
|
|
|
218
215
|
}
|
|
219
216
|
|
|
220
217
|
/**
|
|
221
|
-
*
|
|
218
|
+
* Escribe heartbeat del agente en Redis.
|
|
219
|
+
* TTL automatico: si el agente crashea, las keys expiran solas.
|
|
220
|
+
*
|
|
221
|
+
* @param {string} usuario - Identificador del usuario/agente
|
|
222
|
+
* @param {object} [metadata] - Metadatos adicionales
|
|
223
|
+
*/
|
|
224
|
+
async heartbeat(usuario, metadata) {
|
|
225
|
+
if (!this.redis) return;
|
|
226
|
+
const now = new Date().toISOString();
|
|
227
|
+
const instancia = process.env.HOSTNAME || os.hostname();
|
|
228
|
+
const STATUS_TTL = 180;
|
|
229
|
+
const HEARTBEAT_TTL = 120;
|
|
230
|
+
|
|
231
|
+
await Promise.all([
|
|
232
|
+
this.redis.set('agent:' + usuario + ':status', 'active', 'EX', STATUS_TTL),
|
|
233
|
+
this.redis.set('agent:' + usuario + ':heartbeat', now, 'EX', HEARTBEAT_TTL),
|
|
234
|
+
this.redis.set('agent:' + usuario + ':instancia', instancia, 'EX', STATUS_TTL),
|
|
235
|
+
this.redis.set('agent:' + usuario + ':app_id', this.botId, 'EX', STATUS_TTL),
|
|
236
|
+
metadata && Object.keys(metadata).length > 0
|
|
237
|
+
? this.redis.set('agent:' + usuario + ':metadata', JSON.stringify(metadata), 'EX', STATUS_TTL)
|
|
238
|
+
: Promise.resolve(),
|
|
239
|
+
]);
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Verifica si el agente debe pausarse (status === 'paused').
|
|
244
|
+
* @param {string} usuario
|
|
245
|
+
* @returns {Promise<boolean>}
|
|
246
|
+
*/
|
|
247
|
+
async shouldPause(usuario) {
|
|
248
|
+
if (!this.redis) return false;
|
|
249
|
+
try {
|
|
250
|
+
const status = await this.redis.get('agent:' + usuario + ':status');
|
|
251
|
+
return status === 'paused';
|
|
252
|
+
} catch {
|
|
253
|
+
return false;
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Actualiza el caso activo del agente en Redis.
|
|
259
|
+
* @param {string} usuario
|
|
260
|
+
* @param {string} caso
|
|
261
|
+
*/
|
|
262
|
+
async setCurrentCase(usuario, caso) {
|
|
263
|
+
if (!this.redis) return;
|
|
264
|
+
await this.redis.set('agent:' + usuario + ':current_case', caso, 'EX', 180);
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Libera recursos (Redis, etc.). Llamar al cerrar el bot.
|
|
222
269
|
*/
|
|
223
270
|
async destroy() {
|
|
224
271
|
if (this.redis && typeof this.redis.quit === 'function') {
|
package/src/gateway.js
ADDED
|
@@ -0,0 +1,293 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const os = require('os');
|
|
4
|
+
const { Writable } = require('stream');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Cliente para gateway-browsers.
|
|
8
|
+
*
|
|
9
|
+
* Permite que rpa-tools se registre como agente en el gateway,
|
|
10
|
+
* envie heartbeats periodicamente, y transmita logs de Pino
|
|
11
|
+
* en tiempo real via REST API + WebSocket broadcast.
|
|
12
|
+
*
|
|
13
|
+
* Uso tipico:
|
|
14
|
+
* const gateway = new GatewayClient({
|
|
15
|
+
* gatewayUrl: 'http://gateway:3000',
|
|
16
|
+
* apiKey: process.env.GATEWAY_API_KEY,
|
|
17
|
+
* botName: 'mi-bot',
|
|
18
|
+
* instanceId: process.env.HOSTNAME || os.hostname(),
|
|
19
|
+
* });
|
|
20
|
+
* await gateway.register();
|
|
21
|
+
*/
|
|
22
|
+
class GatewayClient {
|
|
23
|
+
/**
|
|
24
|
+
* @param {object} opts
|
|
25
|
+
* @param {string} opts.gatewayUrl - URL base del gateway (ej: http://gateway:3000)
|
|
26
|
+
* @param {string} [opts.apiKey] - Token JWT para autenticacion (opcional)
|
|
27
|
+
* @param {string} opts.botName - Nombre del bot (ej: 'avaya_chat')
|
|
28
|
+
* @param {string} opts.instanceId - Identificador unico de esta instancia (pod)
|
|
29
|
+
* @param {string[]}[opts.capabilities] - Capacidades del agente (ej: ['screenshots', 'chat'])
|
|
30
|
+
* @param {object} [opts.logger] - Instancia de Pino para logs locales
|
|
31
|
+
*/
|
|
32
|
+
constructor(opts = {}) {
|
|
33
|
+
this.gatewayUrl = opts.gatewayUrl?.replace(/\/+$/, '');
|
|
34
|
+
this.apiKey = opts.apiKey;
|
|
35
|
+
this.botName = opts.botName;
|
|
36
|
+
this.instanceId = opts.instanceId;
|
|
37
|
+
this.capabilities = opts.capabilities || [];
|
|
38
|
+
this.logger = opts.logger;
|
|
39
|
+
|
|
40
|
+
this._registered = false;
|
|
41
|
+
this._instanceDbId = null;
|
|
42
|
+
this._heartbeatTimer = null;
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
get isConfigured() {
|
|
46
|
+
return !!this.gatewayUrl;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
async _fetch(path, options = {}) {
|
|
50
|
+
const url = `${this.gatewayUrl}${path}`;
|
|
51
|
+
const headers = {
|
|
52
|
+
'Content-Type': 'application/json',
|
|
53
|
+
...(options.headers || {}),
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
if (this.apiKey) {
|
|
57
|
+
headers['Authorization'] = `Bearer ${this.apiKey}`;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
const res = await fetch(url, {
|
|
61
|
+
method: options.method || 'GET',
|
|
62
|
+
headers,
|
|
63
|
+
body: options.body ? JSON.stringify(options.body) : undefined,
|
|
64
|
+
signal: options.signal,
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
if (!res.ok) {
|
|
68
|
+
const text = await res.text().catch(() => '');
|
|
69
|
+
throw new Error(`Gateway ${res.status} on ${options.method || 'GET'} ${path}: ${text}`);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
return res.json();
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* Registra esta instancia en el gateway.
|
|
77
|
+
* POST /agents/register
|
|
78
|
+
*
|
|
79
|
+
* @param {object} [opts]
|
|
80
|
+
* @param {string} [opts.hostname] - Hostname del pod
|
|
81
|
+
* @param {string} [opts.agentVersion] - Version del agente
|
|
82
|
+
* @param {string} [opts.platform] - Plataforma (linux, win32, etc.)
|
|
83
|
+
* @param {string} [opts.cdpUrl] - URL del CDP si el browser expone devtools
|
|
84
|
+
* @returns {Promise<{id: string, isNew: boolean}>}
|
|
85
|
+
*/
|
|
86
|
+
async register(opts = {}) {
|
|
87
|
+
if (!this.isConfigured) return { id: null, isNew: false };
|
|
88
|
+
|
|
89
|
+
try {
|
|
90
|
+
const body = {
|
|
91
|
+
botName: this.botName,
|
|
92
|
+
instanceId: this.instanceId,
|
|
93
|
+
capabilities: this.capabilities,
|
|
94
|
+
hostname: opts.hostname || process.env.HOSTNAME || os.hostname(),
|
|
95
|
+
agentVersion: opts.agentVersion || process.env.npm_package_version || '1.0.0',
|
|
96
|
+
platform: opts.platform || process.platform,
|
|
97
|
+
cdpUrl: opts.cdpUrl || null,
|
|
98
|
+
metadata: {
|
|
99
|
+
pid: process.pid,
|
|
100
|
+
startedAt: new Date().toISOString(),
|
|
101
|
+
},
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const result = await this._fetch('/agents/register', {
|
|
105
|
+
method: 'POST',
|
|
106
|
+
body,
|
|
107
|
+
});
|
|
108
|
+
|
|
109
|
+
this._registered = true;
|
|
110
|
+
this._instanceDbId = result.id;
|
|
111
|
+
|
|
112
|
+
if (this.logger) {
|
|
113
|
+
this.logger.info({
|
|
114
|
+
instanceDbId: result.id,
|
|
115
|
+
isNew: result.isNew,
|
|
116
|
+
gatewayUrl: this.gatewayUrl,
|
|
117
|
+
}, 'Agente registrado en gateway');
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
return { id: result.id, isNew: result.isNew };
|
|
121
|
+
} catch (err) {
|
|
122
|
+
if (this.logger) {
|
|
123
|
+
this.logger.error({ err, gatewayUrl: this.gatewayUrl }, 'Error registrando agente en gateway');
|
|
124
|
+
}
|
|
125
|
+
return { id: null, isNew: false };
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Envia un heartbeat al gateway.
|
|
131
|
+
* POST /agents/heartbeat
|
|
132
|
+
*
|
|
133
|
+
* @param {object} opts
|
|
134
|
+
* @param {string} opts.status - Estado actual (running, paused, etc.)
|
|
135
|
+
* @param {string} [opts.currentStep] - Paso actual del bot
|
|
136
|
+
* @param {object} [opts.metrics] - Metricas (cpu, memory, uptime)
|
|
137
|
+
*/
|
|
138
|
+
async heartbeat(opts = {}) {
|
|
139
|
+
if (!this.isConfigured || !this._registered) return;
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
await this._fetch('/agents/heartbeat', {
|
|
143
|
+
method: 'POST',
|
|
144
|
+
body: {
|
|
145
|
+
instanceId: this.instanceId,
|
|
146
|
+
status: opts.status || 'running',
|
|
147
|
+
currentStep: opts.currentStep || null,
|
|
148
|
+
metrics: opts.metrics || undefined,
|
|
149
|
+
},
|
|
150
|
+
});
|
|
151
|
+
} catch (err) {
|
|
152
|
+
if (this.logger) {
|
|
153
|
+
this.logger.warn({ err }, 'Fallo heartbeat al gateway');
|
|
154
|
+
}
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
/**
|
|
159
|
+
* Envia un log estructurado al gateway.
|
|
160
|
+
* POST /api/instances/{instanceDbId}/logs
|
|
161
|
+
*
|
|
162
|
+
* @param {object} entry
|
|
163
|
+
* @param {string} entry.level - trace | debug | info | warn | error | fatal
|
|
164
|
+
* @param {string} entry.message - Mensaje del log
|
|
165
|
+
* @param {string} [entry.source] - Origen (modulo/funcion)
|
|
166
|
+
* @param {object} [entry.metadata] - Metadatos adicionales
|
|
167
|
+
*/
|
|
168
|
+
async sendLog(entry = {}) {
|
|
169
|
+
if (!this.isConfigured || !this._registered || !this._instanceDbId) return;
|
|
170
|
+
|
|
171
|
+
try {
|
|
172
|
+
await this._fetch(`/api/instances/${this._instanceDbId}/logs`, {
|
|
173
|
+
method: 'POST',
|
|
174
|
+
body: {
|
|
175
|
+
level: entry.level || 'info',
|
|
176
|
+
message: entry.message || '',
|
|
177
|
+
source: entry.source || null,
|
|
178
|
+
metadata: entry.metadata || undefined,
|
|
179
|
+
},
|
|
180
|
+
});
|
|
181
|
+
} catch {
|
|
182
|
+
// Silently fail — logs al gateway no deben romper el bot
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
/**
|
|
187
|
+
* Inicia el envio automatico de heartbeats periodicos.
|
|
188
|
+
* @param {number} intervalMs - Intervalo en ms (default: 30000)
|
|
189
|
+
*/
|
|
190
|
+
startHeartbeat(intervalMs = 30000) {
|
|
191
|
+
if (!this.isConfigured || !this._registered) return;
|
|
192
|
+
this.stopHeartbeat();
|
|
193
|
+
this._heartbeatTimer = setInterval(() => {
|
|
194
|
+
this.heartbeat().catch(() => {});
|
|
195
|
+
}, intervalMs).unref();
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
/**
|
|
199
|
+
* Detiene heartbeats periodicos.
|
|
200
|
+
*/
|
|
201
|
+
stopHeartbeat() {
|
|
202
|
+
if (this._heartbeatTimer) {
|
|
203
|
+
clearInterval(this._heartbeatTimer);
|
|
204
|
+
this._heartbeatTimer = null;
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Crea un stream Writable que puede usarse como destino adicional
|
|
210
|
+
* de Pino mediante pino.multistream().
|
|
211
|
+
*
|
|
212
|
+
* Los objetos de log recibidos se envian al gateway via POST /logs.
|
|
213
|
+
*
|
|
214
|
+
* @param {object} [opts]
|
|
215
|
+
* @param {number} [opts.batchMs=1000] - Intervalo de batch en ms
|
|
216
|
+
* @returns {import('stream').Writable}
|
|
217
|
+
*/
|
|
218
|
+
createLogStream(opts = {}) {
|
|
219
|
+
const batchMs = opts.batchMs || 1000;
|
|
220
|
+
const self = this;
|
|
221
|
+
let buffer = [];
|
|
222
|
+
let timer = null;
|
|
223
|
+
|
|
224
|
+
const flush = () => {
|
|
225
|
+
const batch = buffer.splice(0);
|
|
226
|
+
if (batch.length === 0) return;
|
|
227
|
+
for (const entry of batch) {
|
|
228
|
+
self.sendLog(entry).catch(() => {});
|
|
229
|
+
}
|
|
230
|
+
};
|
|
231
|
+
|
|
232
|
+
const stream = new Writable({
|
|
233
|
+
objectMode: true,
|
|
234
|
+
write(chunk, encoding, callback) {
|
|
235
|
+
try {
|
|
236
|
+
const data = typeof chunk === 'string' ? JSON.parse(chunk) : chunk;
|
|
237
|
+
|
|
238
|
+
buffer.push({
|
|
239
|
+
level: data.level || 'info',
|
|
240
|
+
message: data.msg || data.message || JSON.stringify(data),
|
|
241
|
+
source: data.name || data.source || null,
|
|
242
|
+
metadata: {
|
|
243
|
+
...(data.err ? { error: data.err.message, stack: data.err.stack } : {}),
|
|
244
|
+
...(data.step ? { step: data.step } : {}),
|
|
245
|
+
...(data.botId ? { botId: data.botId } : {}),
|
|
246
|
+
...(data.reqId ? { reqId: data.reqId } : {}),
|
|
247
|
+
},
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
if (!timer) {
|
|
251
|
+
timer = setTimeout(() => {
|
|
252
|
+
timer = null;
|
|
253
|
+
flush();
|
|
254
|
+
}, batchMs).unref();
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
callback();
|
|
258
|
+
} catch (err) {
|
|
259
|
+
callback(err);
|
|
260
|
+
}
|
|
261
|
+
},
|
|
262
|
+
|
|
263
|
+
destroy(err, callback) {
|
|
264
|
+
if (timer) {
|
|
265
|
+
clearTimeout(timer);
|
|
266
|
+
timer = null;
|
|
267
|
+
}
|
|
268
|
+
flush();
|
|
269
|
+
callback(err);
|
|
270
|
+
},
|
|
271
|
+
|
|
272
|
+
final(callback) {
|
|
273
|
+
if (timer) {
|
|
274
|
+
clearTimeout(timer);
|
|
275
|
+
timer = null;
|
|
276
|
+
}
|
|
277
|
+
flush();
|
|
278
|
+
callback();
|
|
279
|
+
},
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
return stream;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
/**
|
|
286
|
+
* Limpia recursos (timers).
|
|
287
|
+
*/
|
|
288
|
+
destroy() {
|
|
289
|
+
this.stopHeartbeat();
|
|
290
|
+
}
|
|
291
|
+
}
|
|
292
|
+
|
|
293
|
+
module.exports = { GatewayClient };
|
package/src/index.d.ts
CHANGED
|
@@ -20,6 +20,31 @@ export interface ScreenshotConfig {
|
|
|
20
20
|
/** Calidad de captura en Playwright. Default: 90 */
|
|
21
21
|
captureQuality?: number;
|
|
22
22
|
}
|
|
23
|
+
// ── Opciones de Gateway Client ───────────────────────────────────────────────
|
|
24
|
+
export interface GatewayConfig {
|
|
25
|
+
/** URL base del gateway (ej: http://gateway:3000). Default: process.env.GATEWAY_URL */
|
|
26
|
+
url?: string;
|
|
27
|
+
/** Token JWT para autenticación. Default: process.env.GATEWAY_API_KEY */
|
|
28
|
+
apiKey?: string;
|
|
29
|
+
/** Nombre del bot en el gateway. Default: botId */
|
|
30
|
+
botName?: string;
|
|
31
|
+
/** ID de instancia (pod). Default: process.env.HOSTNAME */
|
|
32
|
+
instanceId?: string;
|
|
33
|
+
/** Capacidades del agente */
|
|
34
|
+
capabilities?: string[];
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
export interface GatewayClient {
|
|
38
|
+
gatewayUrl: string;
|
|
39
|
+
isConfigured: boolean;
|
|
40
|
+
register: (opts?: Record<string, unknown>) => Promise<{ id: string | null; isNew: boolean }>;
|
|
41
|
+
heartbeat: (opts: { status: string; currentStep?: string; metrics?: Record<string, number> }) => Promise<void>;
|
|
42
|
+
sendLog: (entry: { level: string; message: string; source?: string; metadata?: Record<string, unknown> }) => Promise<void>;
|
|
43
|
+
startHeartbeat: (intervalMs?: number) => void;
|
|
44
|
+
stopHeartbeat: () => void;
|
|
45
|
+
createLogStream: (opts?: { batchMs?: number }) => any;
|
|
46
|
+
destroy: () => void;
|
|
47
|
+
}
|
|
23
48
|
// ── Opciones de captura live ─────────────────────────────────────────────────
|
|
24
49
|
export interface CaptureLiveOptions {
|
|
25
50
|
/** Identificador del usuario/agente */
|
|
@@ -56,6 +81,8 @@ export interface RpaToolsOptions {
|
|
|
56
81
|
screenshot?: ScreenshotConfig;
|
|
57
82
|
/** Cliente Redis opcional (ioredis). Requerido para captureLive/setOffline */
|
|
58
83
|
redis?: any;
|
|
84
|
+
/** Config de gateway-browsers (opcional). Si se provee, el agente se registra automaticamente */
|
|
85
|
+
gateway?: GatewayConfig;
|
|
59
86
|
}
|
|
60
87
|
// ── Objeto retornado por createRpaTools ──────────────────────────────────────
|
|
61
88
|
export interface RpaTools {
|
|
@@ -122,6 +149,24 @@ export interface RpaTools {
|
|
|
122
149
|
* @param opts - Opciones (usuario, instancia)
|
|
123
150
|
*/
|
|
124
151
|
setOffline: (opts?: { usuario?: string; instancia?: string }) => Promise<string | null>;
|
|
152
|
+
/**
|
|
153
|
+
* Escribe heartbeat del agente en Redis.
|
|
154
|
+
* Las keys tienen TTL. Requiere redis configurado en createRpaTools().
|
|
155
|
+
* @param usuario - Identificador del usuario/agente
|
|
156
|
+
* @param metadata - Metadatos adicionales
|
|
157
|
+
*/
|
|
158
|
+
heartbeat: (usuario: string, metadata?: Record<string, unknown>) => Promise<void>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Verifica si el agente debe pausarse (status === "paused" en Redis).
|
|
162
|
+
*/
|
|
163
|
+
shouldPause: (usuario: string) => Promise<boolean>;
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* Actualiza el caso activo del agente en Redis.
|
|
167
|
+
*/
|
|
168
|
+
setCurrentCase: (usuario: string, caso: string) => Promise<void>;
|
|
169
|
+
|
|
125
170
|
/**
|
|
126
171
|
* Captura screenshot, trace y registra error en base de datos.
|
|
127
172
|
*/
|
|
@@ -131,6 +176,8 @@ export interface RpaTools {
|
|
|
131
176
|
err: Error;
|
|
132
177
|
step?: string;
|
|
133
178
|
}) => Promise<{ errorId: number | null; screenshotKey: string | null; traceKey: string | null }>;
|
|
179
|
+
/** Cliente del gateway-browsers (solo si configurado) */
|
|
180
|
+
gateway?: GatewayClient;
|
|
134
181
|
/** Detiene todos los intervalos. Llamar siempre al cerrar el bot. */
|
|
135
182
|
destroy: () => void;
|
|
136
183
|
}
|
package/src/index.js
CHANGED
|
@@ -1,10 +1,12 @@
|
|
|
1
1
|
'use strict';
|
|
2
|
+
const os = require('os');
|
|
2
3
|
const { createLogger } = require('./logger');
|
|
3
4
|
const { createMinioClient, ensureBucket } = require('./storage');
|
|
4
5
|
const { ScreenshotWatcher } = require('./watcher');
|
|
5
6
|
const { CredentialManager } = require('./credentials');
|
|
6
7
|
const { captureError } = require('./errorCapture');
|
|
7
8
|
const { PageCapturer } = require('./capture');
|
|
9
|
+
const { GatewayClient } = require('./gateway');
|
|
8
10
|
|
|
9
11
|
/**
|
|
10
12
|
* Factory principal de la librería.
|
|
@@ -16,8 +18,14 @@ const { PageCapturer } = require('./capture');
|
|
|
16
18
|
* @param {object} [opts.log] - opciones de Pino
|
|
17
19
|
* @param {object} [opts.screenshot] - opciones de captura (formato, calidad)
|
|
18
20
|
* @param {object} [opts.redis] - cliente Redis opcional (ioredis)
|
|
21
|
+
* @param {object} [opts.gateway] - config de gateway-browsers (opcional)
|
|
22
|
+
* @param {string} [opts.gateway.url] - URL del gateway (ej: http://gateway:3000)
|
|
23
|
+
* @param {string} [opts.gateway.apiKey] - Token JWT para el gateway
|
|
24
|
+
* @param {string} [opts.gateway.botName] - Nombre del bot en el gateway
|
|
25
|
+
* @param {string} [opts.gateway.instanceId] - ID de instancia (default: HOSTNAME)
|
|
26
|
+
* @param {string[]}[opts.gateway.capabilities] - Capacidades del agente
|
|
19
27
|
*
|
|
20
|
-
* @returns {Promise<{ logger, state, step, watchDebugFlag, getCredentials, getCredentialsAll, updateAppStatus, isActive, capturePage, captureLive, setOffline, captureError, destroy }>}
|
|
28
|
+
* @returns {Promise<{ logger, state, step, watchDebugFlag, getCredentials, getCredentialsAll, updateAppStatus, isActive, capturePage, captureLive, setOffline, heartbeat, shouldPause, setCurrentCase, captureError, destroy, gateway }>}
|
|
21
29
|
*/
|
|
22
30
|
async function createRpaTools(opts = {}) {
|
|
23
31
|
const {
|
|
@@ -27,6 +35,7 @@ async function createRpaTools(opts = {}) {
|
|
|
27
35
|
minio: minioCfg,
|
|
28
36
|
screenshot: screenshotCfg,
|
|
29
37
|
redis: redisClient,
|
|
38
|
+
gateway: gatewayCfg,
|
|
30
39
|
} = opts;
|
|
31
40
|
|
|
32
41
|
const minio = minioCfg ?? {
|
|
@@ -52,14 +61,41 @@ async function createRpaTools(opts = {}) {
|
|
|
52
61
|
const credentials = new CredentialManager({ botId, pool: db });
|
|
53
62
|
const capturer = new PageCapturer({ botId, minioClient, bucket, logger, redis: redisClient });
|
|
54
63
|
|
|
64
|
+
// ── Gateway Client (opcional) ──────────────────────────────────────
|
|
65
|
+
const gateway = new GatewayClient({
|
|
66
|
+
gatewayUrl: gatewayCfg?.url || process.env.GATEWAY_URL,
|
|
67
|
+
apiKey: gatewayCfg?.apiKey || process.env.GATEWAY_API_KEY,
|
|
68
|
+
botName: gatewayCfg?.botName || botId,
|
|
69
|
+
instanceId: gatewayCfg?.instanceId || process.env.HOSTNAME || os.hostname(),
|
|
70
|
+
capabilities: gatewayCfg?.capabilities || [],
|
|
71
|
+
logger,
|
|
72
|
+
});
|
|
73
|
+
|
|
74
|
+
if (gateway.isConfigured) {
|
|
75
|
+
const reg = await gateway.register({
|
|
76
|
+
hostname: process.env.HOSTNAME || os.hostname(),
|
|
77
|
+
agentVersion: process.env.npm_package_version || '1.0.0',
|
|
78
|
+
platform: process.platform,
|
|
79
|
+
});
|
|
80
|
+
|
|
81
|
+
if (reg.id) {
|
|
82
|
+
gateway.startHeartbeat(30000);
|
|
83
|
+
logger.info({ instanceDbId: reg.id, gatewayUrl: gateway.gatewayUrl }, 'Gateway conectado');
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
55
87
|
const state = { currentStep: 'Inicio' };
|
|
56
88
|
|
|
57
89
|
return {
|
|
58
90
|
logger,
|
|
59
91
|
state,
|
|
92
|
+
gateway: gateway.isConfigured ? gateway : undefined,
|
|
60
93
|
step: (name) => {
|
|
61
94
|
state.currentStep = name;
|
|
62
95
|
logger.info({ step: name }, 'Nuevo paso');
|
|
96
|
+
if (gateway.isConfigured) {
|
|
97
|
+
gateway.heartbeat({ status: 'running', currentStep: name }).catch(() => {});
|
|
98
|
+
}
|
|
63
99
|
},
|
|
64
100
|
/**
|
|
65
101
|
* Inicia el polling que activa/desactiva screenshots según bot_debug_config.
|
|
@@ -113,6 +149,9 @@ async function createRpaTools(opts = {}) {
|
|
|
113
149
|
* @returns {Promise<string|null>}
|
|
114
150
|
*/
|
|
115
151
|
setOffline: (opts) => capturer.setOffline(opts),
|
|
152
|
+
heartbeat: (usuario, metadata) => capturer.heartbeat(usuario, metadata),
|
|
153
|
+
shouldPause: (usuario) => capturer.shouldPause(usuario),
|
|
154
|
+
setCurrentCase: (usuario, caso) => capturer.setCurrentCase(usuario, caso),
|
|
116
155
|
/**
|
|
117
156
|
* Captura screenshot y registra error en base de datos.
|
|
118
157
|
*/
|
|
@@ -130,6 +169,7 @@ async function createRpaTools(opts = {}) {
|
|
|
130
169
|
destroy: () => {
|
|
131
170
|
watcher.destroy();
|
|
132
171
|
capturer.destroy().catch(() => {});
|
|
172
|
+
gateway.destroy();
|
|
133
173
|
},
|
|
134
174
|
};
|
|
135
175
|
}
|