@gnpdev/rpa-tools 1.1.4 → 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 +8 -5
- package/src/capture.js +224 -7
- package/src/gateway.js +293 -0
- package/src/index.d.ts +80 -16
- package/src/index.js +72 -8
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
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@gnpdev/rpa-tools",
|
|
3
|
-
"version": "1.
|
|
3
|
+
"version": "1.5.0",
|
|
4
4
|
"description": "Libreria para logs y screenshot de bots",
|
|
5
5
|
"author": "Sergio Antonio Trujillo del Valle",
|
|
6
6
|
"main": "src/index.js",
|
|
@@ -18,21 +18,24 @@
|
|
|
18
18
|
"license": "MIT",
|
|
19
19
|
"dependencies": {
|
|
20
20
|
"minio": "^8.0.7",
|
|
21
|
+
"pg": "^8.11.0",
|
|
21
22
|
"pino": "^10.3.1",
|
|
22
23
|
"pino-pretty": "^13.1.3",
|
|
23
|
-
"sharp": "^0.34.5"
|
|
24
|
-
"pg": "^8.11.0",
|
|
25
|
-
"@types/pg": "^8.20.0"
|
|
24
|
+
"sharp": "^0.34.5"
|
|
26
25
|
},
|
|
27
26
|
"devDependencies": {
|
|
28
27
|
"@commitlint/cli": "^20.5.0",
|
|
29
28
|
"@commitlint/config-conventional": "^20.5.0",
|
|
30
29
|
"@release-it/conventional-changelog": "^10.0.6",
|
|
30
|
+
"@types/pg": "^8.20.0",
|
|
31
|
+
"@types/node": "^25.9.1",
|
|
31
32
|
"husky": "^9.1.7",
|
|
32
|
-
"release-it": "^19.2.4"
|
|
33
|
+
"release-it": "^19.2.4",
|
|
34
|
+
"typescript": "^6.0.3"
|
|
33
35
|
},
|
|
34
36
|
"scripts": {
|
|
35
37
|
"prepare": "husky",
|
|
38
|
+
"typecheck": "tsc --noEmit",
|
|
36
39
|
"release": "release-it"
|
|
37
40
|
}
|
|
38
41
|
}
|
package/src/capture.js
CHANGED
|
@@ -1,31 +1,42 @@
|
|
|
1
1
|
'use strict';
|
|
2
2
|
const sharp = require('sharp');
|
|
3
|
+
const os = require('os');
|
|
3
4
|
|
|
4
5
|
/**
|
|
5
|
-
* Gestor de capturas de pantalla
|
|
6
|
+
* Gestor de capturas de pantalla — modo histórico y modo live frame.
|
|
7
|
+
*
|
|
8
|
+
* Proporciona:
|
|
9
|
+
* - capture() → captura única con key timestampeda (histórica)
|
|
10
|
+
* - captureLive() → sobrescribe siempre la misma key (live frame)
|
|
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
|
|
6
15
|
*/
|
|
7
16
|
class PageCapturer {
|
|
8
17
|
/**
|
|
9
18
|
* @param {object} params
|
|
10
|
-
* @param {string} params.botId - UUID
|
|
19
|
+
* @param {string} params.botId - UUID o identificador de la app/bot
|
|
11
20
|
* @param {import('minio').Client} params.minioClient - Cliente MinIO
|
|
12
21
|
* @param {string} params.bucket - Bucket de MinIO
|
|
13
22
|
* @param {import('pino').Logger} [params.logger] - Instancia de Logger
|
|
23
|
+
* @param {object} [params.redis] - Cliente Redis opcional (ioredis)
|
|
14
24
|
*/
|
|
15
|
-
constructor({ botId, minioClient, bucket, logger }) {
|
|
25
|
+
constructor({ botId, minioClient, bucket, logger, redis }) {
|
|
16
26
|
this.botId = botId;
|
|
17
27
|
this.minioClient = minioClient;
|
|
18
28
|
this.bucket = bucket;
|
|
19
29
|
this.logger = logger;
|
|
30
|
+
this.redis = redis;
|
|
20
31
|
}
|
|
21
32
|
|
|
22
33
|
/**
|
|
23
|
-
* Captura
|
|
24
|
-
*
|
|
25
|
-
*
|
|
34
|
+
* Captura única — sube a MinIO con key timestampeda.
|
|
35
|
+
* Ruta: capturas/{botId}/{YYYY-MM-DD}/{nombre}.webp
|
|
36
|
+
*
|
|
26
37
|
* @param {any} page - Instancia de Page (Playwright o Puppeteer)
|
|
27
38
|
* @param {string} nombre - Nombre del elemento/captura
|
|
28
|
-
* @returns {Promise<string|null>}
|
|
39
|
+
* @returns {Promise<string|null>}
|
|
29
40
|
*/
|
|
30
41
|
async capture(page, nombre) {
|
|
31
42
|
try {
|
|
@@ -59,6 +70,212 @@ class PageCapturer {
|
|
|
59
70
|
return null;
|
|
60
71
|
}
|
|
61
72
|
}
|
|
73
|
+
|
|
74
|
+
/**
|
|
75
|
+
* Live Frame — Captura y SOBRESCRIBE la misma key en MinIO.
|
|
76
|
+
*
|
|
77
|
+
* Ruta fija: live/{appId}/{instancia}/{usuario}/live.webp
|
|
78
|
+
*
|
|
79
|
+
* Una sola imagen por agente que se actualiza periódicamente
|
|
80
|
+
* sin acumular archivos. Headers anti-cache + Redis timestamp.
|
|
81
|
+
*
|
|
82
|
+
* @param {any} page - Instancia de Page (Playwright o Puppeteer)
|
|
83
|
+
* @param {object} [opts]
|
|
84
|
+
* @param {string} [opts.usuario='unknown'] - Identificador del usuario/agente
|
|
85
|
+
* @param {string} [opts.instancia] - Hostname/pod (default: HOSTNAME)
|
|
86
|
+
* @param {object} [opts.metadata] - Metadatos adicionales para Redis
|
|
87
|
+
* @param {boolean} [opts.fullPage=false] - Capturar página completa
|
|
88
|
+
* @returns {Promise<{key: string, updatedAt: string}|null>}
|
|
89
|
+
*/
|
|
90
|
+
async captureLive(page, opts = {}) {
|
|
91
|
+
const usuario = opts.usuario || 'unknown';
|
|
92
|
+
const instancia = opts.instancia || process.env.HOSTNAME || os.hostname();
|
|
93
|
+
const metadata = opts.metadata || {};
|
|
94
|
+
const fullPage = opts.fullPage || false;
|
|
95
|
+
|
|
96
|
+
try {
|
|
97
|
+
const now = new Date();
|
|
98
|
+
const key = `live/${this.botId}/${instancia}/${usuario}/live.webp`;
|
|
99
|
+
|
|
100
|
+
const buffer = await page.screenshot({
|
|
101
|
+
type: 'jpeg',
|
|
102
|
+
quality: 80,
|
|
103
|
+
fullPage,
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
const finalBuffer = await sharp(buffer)
|
|
107
|
+
.webp({ quality: 70 })
|
|
108
|
+
.toBuffer();
|
|
109
|
+
|
|
110
|
+
await this.minioClient.putObject(this.bucket, key, finalBuffer, finalBuffer.length, {
|
|
111
|
+
'Content-Type': 'image/webp',
|
|
112
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
113
|
+
'Pragma': 'no-cache',
|
|
114
|
+
'Expires': '0',
|
|
115
|
+
});
|
|
116
|
+
|
|
117
|
+
const timestamp = now.toISOString();
|
|
118
|
+
|
|
119
|
+
if (this.redis) {
|
|
120
|
+
await this.redis.set(`agent:${usuario}:screenshot_updated_at`, timestamp);
|
|
121
|
+
await this.redis.set(`agent:${usuario}:screenshot_key`, key);
|
|
122
|
+
await this.redis.set(`agent:${usuario}:status`, 'active');
|
|
123
|
+
await this.redis.set(`agent:${usuario}:instancia`, instancia);
|
|
124
|
+
await this.redis.set(`agent:${usuario}:app_id`, this.botId);
|
|
125
|
+
if (Object.keys(metadata).length > 0) {
|
|
126
|
+
await this.redis.set(`agent:${usuario}:metadata`, JSON.stringify(metadata));
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
if (this.logger) {
|
|
131
|
+
this.logger.info({ key, usuario, instancia }, 'Live screenshot updated');
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
return { key, updatedAt: timestamp };
|
|
135
|
+
} catch (err) {
|
|
136
|
+
if (this.logger) {
|
|
137
|
+
this.logger.error({ err, usuario }, 'Error capturing live screenshot');
|
|
138
|
+
}
|
|
139
|
+
return null;
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
/**
|
|
144
|
+
* Sustituye la imagen live por un placeholder negro/offline.
|
|
145
|
+
*
|
|
146
|
+
* Se llama cuando el bot se pausa, desactiva, o termina su ejecución.
|
|
147
|
+
* Sobrescribe la misma key live/{appId}/{instancia}/{usuario}/live.webp
|
|
148
|
+
* para que el frontend refleje automáticamente el estado inactivo.
|
|
149
|
+
*
|
|
150
|
+
* @param {object} [opts]
|
|
151
|
+
* @param {string} [opts.usuario='unknown'] - Identificador del usuario/agente
|
|
152
|
+
* @param {string} [opts.instancia] - Hostname/pod (default: HOSTNAME)
|
|
153
|
+
* @returns {Promise<string|null>}
|
|
154
|
+
*/
|
|
155
|
+
async setOffline(opts = {}) {
|
|
156
|
+
const usuario = opts.usuario || 'unknown';
|
|
157
|
+
const instancia = opts.instancia || process.env.HOSTNAME || os.hostname();
|
|
158
|
+
|
|
159
|
+
try {
|
|
160
|
+
const key = `live/${this.botId}/${instancia}/${usuario}/live.webp`;
|
|
161
|
+
|
|
162
|
+
let finalBuffer;
|
|
163
|
+
|
|
164
|
+
try {
|
|
165
|
+
const svg = `<svg width="1280" height="900" xmlns="http://www.w3.org/2000/svg">
|
|
166
|
+
<rect width="1280" height="900" fill="#0a0a0a"/>
|
|
167
|
+
<style>
|
|
168
|
+
.t { fill:#333; font-family:sans-serif; font-size:48px; font-weight:bold; text-anchor:middle; }
|
|
169
|
+
.s { fill:#222; font-family:monospace; font-size:20px; text-anchor:middle; }
|
|
170
|
+
</style>
|
|
171
|
+
<text x="640" y="420" class="t">AGENTE INACTIVO</text>
|
|
172
|
+
<text x="640" y="480" class="s">${usuario}</text>
|
|
173
|
+
<text x="640" y="520" class="s">${this.botId}</text>
|
|
174
|
+
</svg>`;
|
|
175
|
+
|
|
176
|
+
finalBuffer = await sharp(Buffer.from(svg))
|
|
177
|
+
.webp({ quality: 50 })
|
|
178
|
+
.toBuffer();
|
|
179
|
+
} catch {
|
|
180
|
+
finalBuffer = await sharp({
|
|
181
|
+
create: {
|
|
182
|
+
width: 1,
|
|
183
|
+
height: 1,
|
|
184
|
+
channels: 3,
|
|
185
|
+
background: { r: 0, g: 0, b: 0 },
|
|
186
|
+
},
|
|
187
|
+
}).webp().toBuffer();
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
await this.minioClient.putObject(this.bucket, key, finalBuffer, finalBuffer.length, {
|
|
191
|
+
'Content-Type': 'image/webp',
|
|
192
|
+
'Cache-Control': 'no-cache, no-store, must-revalidate',
|
|
193
|
+
'Pragma': 'no-cache',
|
|
194
|
+
'Expires': '0',
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
const timestamp = new Date().toISOString();
|
|
198
|
+
|
|
199
|
+
if (this.redis) {
|
|
200
|
+
await this.redis.set(`agent:${usuario}:status`, 'paused');
|
|
201
|
+
await this.redis.set(`agent:${usuario}:screenshot_updated_at`, timestamp);
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
if (this.logger) {
|
|
205
|
+
this.logger.info({ key, usuario }, 'Offline placeholder set');
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
return key;
|
|
209
|
+
} catch (err) {
|
|
210
|
+
if (this.logger) {
|
|
211
|
+
this.logger.error({ err, usuario }, 'Error setting offline placeholder');
|
|
212
|
+
}
|
|
213
|
+
return null;
|
|
214
|
+
}
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
/**
|
|
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.
|
|
269
|
+
*/
|
|
270
|
+
async destroy() {
|
|
271
|
+
if (this.redis && typeof this.redis.quit === 'function') {
|
|
272
|
+
try {
|
|
273
|
+
await this.redis.quit();
|
|
274
|
+
} catch {
|
|
275
|
+
// ignore
|
|
276
|
+
}
|
|
277
|
+
}
|
|
278
|
+
}
|
|
62
279
|
}
|
|
63
280
|
|
|
64
281
|
module.exports = { PageCapturer };
|
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
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
import type { Pool } from 'pg';
|
|
2
2
|
import type { Logger } from 'pino';
|
|
3
|
-
import type { Page } from 'playwright';
|
|
4
|
-
|
|
5
3
|
export type { Logger };
|
|
6
4
|
|
|
7
5
|
// ── Opciones de MinIO ────────────────────────────────────────────────────────
|
|
@@ -13,7 +11,6 @@ export interface MinioConfig {
|
|
|
13
11
|
secretKey: string;
|
|
14
12
|
bucket?: string;
|
|
15
13
|
}
|
|
16
|
-
|
|
17
14
|
// ── Opciones de screenshot ───────────────────────────────────────────────────
|
|
18
15
|
export interface ScreenshotConfig {
|
|
19
16
|
/** Formato final del archivo en MinIO. Default: 'webp' */
|
|
@@ -23,7 +20,46 @@ export interface ScreenshotConfig {
|
|
|
23
20
|
/** Calidad de captura en Playwright. Default: 90 */
|
|
24
21
|
captureQuality?: number;
|
|
25
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
|
+
}
|
|
26
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
|
+
}
|
|
48
|
+
// ── Opciones de captura live ─────────────────────────────────────────────────
|
|
49
|
+
export interface CaptureLiveOptions {
|
|
50
|
+
/** Identificador del usuario/agente */
|
|
51
|
+
usuario?: string;
|
|
52
|
+
/** Hostname/pod (default: HOSTNAME) */
|
|
53
|
+
instancia?: string;
|
|
54
|
+
/** Metadatos adicionales para Redis */
|
|
55
|
+
metadata?: Record<string, unknown>;
|
|
56
|
+
/** Capturar página completa */
|
|
57
|
+
fullPage?: boolean;
|
|
58
|
+
}
|
|
59
|
+
export interface CaptureLiveResult {
|
|
60
|
+
key: string;
|
|
61
|
+
updatedAt: string;
|
|
62
|
+
}
|
|
27
63
|
// ── Opciones de Pino ─────────────────────────────────────────────────────────
|
|
28
64
|
export interface LogConfig {
|
|
29
65
|
level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
|
|
@@ -31,7 +67,6 @@ export interface LogConfig {
|
|
|
31
67
|
pretty?: boolean;
|
|
32
68
|
pinoOptions?: Record<string, unknown>;
|
|
33
69
|
}
|
|
34
|
-
|
|
35
70
|
// ── Opciones principales de createRpaTools ───────────────────────────────────
|
|
36
71
|
export interface RpaToolsOptions {
|
|
37
72
|
/** ID único del bot. Default: process.env.RPA_BOT_ID */
|
|
@@ -44,24 +79,24 @@ export interface RpaToolsOptions {
|
|
|
44
79
|
log?: LogConfig;
|
|
45
80
|
/** Opciones de captura y compresión de screenshots */
|
|
46
81
|
screenshot?: ScreenshotConfig;
|
|
82
|
+
/** Cliente Redis opcional (ioredis). Requerido para captureLive/setOffline */
|
|
83
|
+
redis?: any;
|
|
84
|
+
/** Config de gateway-browsers (opcional). Si se provee, el agente se registra automaticamente */
|
|
85
|
+
gateway?: GatewayConfig;
|
|
47
86
|
}
|
|
48
|
-
|
|
49
87
|
// ── Objeto retornado por createRpaTools ──────────────────────────────────────
|
|
50
88
|
export interface RpaTools {
|
|
51
89
|
/** Instancia de Pino lista para usar en el bot */
|
|
52
90
|
logger: Logger;
|
|
53
|
-
|
|
54
91
|
/** Estado compartido del bot */
|
|
55
92
|
state: {
|
|
56
93
|
currentStep: string;
|
|
57
94
|
};
|
|
58
|
-
|
|
59
95
|
/**
|
|
60
96
|
* Cambia el paso actual, actualiza el estado y emite un log info.
|
|
61
97
|
* @param name - Nombre del nuevo paso
|
|
62
98
|
*/
|
|
63
99
|
step: (name: string) => void;
|
|
64
|
-
|
|
65
100
|
/**
|
|
66
101
|
* Inicia el polling que activa/desactiva screenshots
|
|
67
102
|
* según bot_debug_config en la BD.
|
|
@@ -69,19 +104,16 @@ export interface RpaTools {
|
|
|
69
104
|
* @param pollMs - Intervalo de polling en ms. Default: 3000
|
|
70
105
|
*/
|
|
71
106
|
watchDebugFlag: (page: any, pollMs?: number) => void;
|
|
72
|
-
|
|
73
107
|
/**
|
|
74
108
|
* Recupera las credenciales de una aplicación desde la base de datos.
|
|
75
109
|
* @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
|
|
76
110
|
*/
|
|
77
111
|
getCredentials: (nombre: string) => Promise<{ nombre: string; usuario: string; password: string; idUsuario: string; status: boolean } | null>;
|
|
78
|
-
|
|
79
112
|
/**
|
|
80
113
|
* Recupera TODAS las credenciales que coincidan con el nombre.
|
|
81
114
|
* @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
|
|
82
115
|
*/
|
|
83
116
|
getCredentialsAll: (nombre: string) => Promise<Array<{ nombre: string; usuario: string; password: string; idUsuario: string; status: boolean }>>;
|
|
84
|
-
|
|
85
117
|
/**
|
|
86
118
|
* Actualiza el estado y las observaciones de una aplicación.
|
|
87
119
|
* @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
|
|
@@ -89,19 +121,51 @@ export interface RpaTools {
|
|
|
89
121
|
* @param observations - Observaciones/Errores
|
|
90
122
|
*/
|
|
91
123
|
updateAppStatus: (nombre: string, status: boolean, observations: string) => Promise<boolean>;
|
|
92
|
-
|
|
93
124
|
/**
|
|
94
125
|
* Consulta si el bot está activo en la base de datos (columna 'estado').
|
|
95
126
|
* @returns {Promise<boolean>}
|
|
96
127
|
*/
|
|
97
128
|
isActive: () => Promise<boolean>;
|
|
98
|
-
|
|
99
129
|
/**
|
|
100
|
-
* Captura
|
|
130
|
+
* Captura única — sube a MinIO con key timestampeda.
|
|
101
131
|
* @param page - Instancia de Page (Playwright o Puppeteer)
|
|
102
132
|
* @param nombre - Nombre del elemento/captura
|
|
103
133
|
*/
|
|
104
134
|
capturePage: (page: any, nombre: string) => Promise<string | null>;
|
|
135
|
+
/**
|
|
136
|
+
* Live Frame — Captura y sobrescribe la misma key en MinIO.
|
|
137
|
+
* Una imagen por (appId, instancia, usuario). Headers anti-cache.
|
|
138
|
+
*
|
|
139
|
+
* Requiere redis configurado en createRpaTools().
|
|
140
|
+
*
|
|
141
|
+
* @param page - Instancia de Page
|
|
142
|
+
* @param opts - Opciones de captura (usuario, instancia, metadata)
|
|
143
|
+
*/
|
|
144
|
+
captureLive: (page: any, opts?: CaptureLiveOptions) => Promise<CaptureLiveResult | null>;
|
|
145
|
+
/**
|
|
146
|
+
* Sustituye la imagen live por un placeholder negro/offline.
|
|
147
|
+
* Se llama cuando el bot se pausa, desactiva, o termina.
|
|
148
|
+
*
|
|
149
|
+
* @param opts - Opciones (usuario, instancia)
|
|
150
|
+
*/
|
|
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>;
|
|
105
169
|
|
|
106
170
|
/**
|
|
107
171
|
* Captura screenshot, trace y registra error en base de datos.
|
|
@@ -112,11 +176,11 @@ export interface RpaTools {
|
|
|
112
176
|
err: Error;
|
|
113
177
|
step?: string;
|
|
114
178
|
}) => Promise<{ errorId: number | null; screenshotKey: string | null; traceKey: string | null }>;
|
|
115
|
-
|
|
179
|
+
/** Cliente del gateway-browsers (solo si configurado) */
|
|
180
|
+
gateway?: GatewayClient;
|
|
116
181
|
/** Detiene todos los intervalos. Llamar siempre al cerrar el bot. */
|
|
117
182
|
destroy: () => void;
|
|
118
183
|
}
|
|
119
|
-
|
|
120
184
|
// ── Export principal ─────────────────────────────────────────────────────────
|
|
121
185
|
/**
|
|
122
186
|
* Inicializa la librería rpa-tools.
|
package/src/index.js
CHANGED
|
@@ -1,23 +1,31 @@
|
|
|
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.
|
|
11
13
|
*
|
|
12
14
|
* @param {object} opts
|
|
13
|
-
* @param {string} opts.botId - identificador único del bot
|
|
15
|
+
* @param {string} opts.botId - identificador único del bot/app
|
|
14
16
|
* @param {import('pg').Pool} opts.db - pool de pg ya conectado
|
|
15
17
|
* @param {object} opts.minio - config de MinIO
|
|
16
18
|
* @param {object} [opts.log] - opciones de Pino
|
|
17
|
-
*
|
|
18
19
|
* @param {object} [opts.screenshot] - opciones de captura (formato, calidad)
|
|
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 {{ logger, state, step, watchDebugFlag, getCredentials, getCredentialsAll, updateAppStatus, isActive, capturePage, 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 {
|
|
@@ -26,6 +34,8 @@ async function createRpaTools(opts = {}) {
|
|
|
26
34
|
db,
|
|
27
35
|
minio: minioCfg,
|
|
28
36
|
screenshot: screenshotCfg,
|
|
37
|
+
redis: redisClient,
|
|
38
|
+
gateway: gatewayCfg,
|
|
29
39
|
} = opts;
|
|
30
40
|
|
|
31
41
|
const minio = minioCfg ?? {
|
|
@@ -49,16 +59,43 @@ async function createRpaTools(opts = {}) {
|
|
|
49
59
|
|
|
50
60
|
const watcher = new ScreenshotWatcher({ botId, pool: db, minioClient, bucket, logger, screenshot: screenshotCfg });
|
|
51
61
|
const credentials = new CredentialManager({ botId, pool: db });
|
|
52
|
-
const capturer = new PageCapturer({ botId, minioClient, bucket, logger });
|
|
62
|
+
const capturer = new PageCapturer({ botId, minioClient, bucket, logger, redis: redisClient });
|
|
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
|
+
}
|
|
53
86
|
|
|
54
87
|
const state = { currentStep: 'Inicio' };
|
|
55
88
|
|
|
56
89
|
return {
|
|
57
90
|
logger,
|
|
58
91
|
state,
|
|
92
|
+
gateway: gateway.isConfigured ? gateway : undefined,
|
|
59
93
|
step: (name) => {
|
|
60
94
|
state.currentStep = name;
|
|
61
95
|
logger.info({ step: name }, 'Nuevo paso');
|
|
96
|
+
if (gateway.isConfigured) {
|
|
97
|
+
gateway.heartbeat({ status: 'running', currentStep: name }).catch(() => {});
|
|
98
|
+
}
|
|
62
99
|
},
|
|
63
100
|
/**
|
|
64
101
|
* Inicia el polling que activa/desactiva screenshots según bot_debug_config.
|
|
@@ -86,14 +123,37 @@ async function createRpaTools(opts = {}) {
|
|
|
86
123
|
}
|
|
87
124
|
},
|
|
88
125
|
/**
|
|
89
|
-
* Captura
|
|
126
|
+
* Captura única — sube a MinIO con key timestampeda.
|
|
90
127
|
* @param {any} page - Instancia de Page
|
|
91
128
|
* @param {string} nombre - Nombre del elemento/captura
|
|
92
129
|
*/
|
|
93
130
|
capturePage: (page, nombre) => capturer.capture(page, nombre),
|
|
94
131
|
/**
|
|
95
|
-
* Captura
|
|
96
|
-
*
|
|
132
|
+
* Live Frame — Captura y sobrescribe la misma key en MinIO.
|
|
133
|
+
* Una imagen por (appId, instancia, usuario). Headers anti-cache.
|
|
134
|
+
*
|
|
135
|
+
* @param {any} page - Instancia de Page (Playwright o Puppeteer)
|
|
136
|
+
* @param {object} [opts]
|
|
137
|
+
* @param {string} [opts.usuario] - Identificador del usuario/agente
|
|
138
|
+
* @param {string} [opts.instancia] - Hostname/pod
|
|
139
|
+
* @param {object} [opts.metadata] - Metadatos para Redis
|
|
140
|
+
* @param {boolean} [opts.fullPage] - Capturar página completa
|
|
141
|
+
* @returns {Promise<{key: string, updatedAt: string}|null>}
|
|
142
|
+
*/
|
|
143
|
+
captureLive: (page, opts) => capturer.captureLive(page, opts),
|
|
144
|
+
/**
|
|
145
|
+
* Sustituye la imagen live por un placeholder offline.
|
|
146
|
+
* @param {object} [opts]
|
|
147
|
+
* @param {string} [opts.usuario] - Identificador del usuario/agente
|
|
148
|
+
* @param {string} [opts.instancia] - Hostname/pod
|
|
149
|
+
* @returns {Promise<string|null>}
|
|
150
|
+
*/
|
|
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),
|
|
155
|
+
/**
|
|
156
|
+
* Captura screenshot y registra error en base de datos.
|
|
97
157
|
*/
|
|
98
158
|
captureError: ({ page, context, err, step }) => captureError({
|
|
99
159
|
page,
|
|
@@ -106,7 +166,11 @@ async function createRpaTools(opts = {}) {
|
|
|
106
166
|
bucket,
|
|
107
167
|
logger
|
|
108
168
|
}),
|
|
109
|
-
destroy: () =>
|
|
169
|
+
destroy: () => {
|
|
170
|
+
watcher.destroy();
|
|
171
|
+
capturer.destroy().catch(() => {});
|
|
172
|
+
gateway.destroy();
|
|
173
|
+
},
|
|
110
174
|
};
|
|
111
175
|
}
|
|
112
176
|
|