@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 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
- 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
- }
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.0",
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",
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
- * Libera recursos (Redis, etc.) llamar al cerrar el bot.
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
  }