@gnpdev/rpa-tools 1.1.4 → 1.3.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnpdev/rpa-tools",
3
- "version": "1.1.4",
3
+ "version": "1.3.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,39 @@
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 de la página actual.
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)
6
12
  */
7
13
  class PageCapturer {
8
14
  /**
9
15
  * @param {object} params
10
- * @param {string} params.botId - UUID del bot
16
+ * @param {string} params.botId - UUID o identificador de la app/bot
11
17
  * @param {import('minio').Client} params.minioClient - Cliente MinIO
12
18
  * @param {string} params.bucket - Bucket de MinIO
13
19
  * @param {import('pino').Logger} [params.logger] - Instancia de Logger
20
+ * @param {object} [params.redis] - Cliente Redis opcional (ioredis)
14
21
  */
15
- constructor({ botId, minioClient, bucket, logger }) {
22
+ constructor({ botId, minioClient, bucket, logger, redis }) {
16
23
  this.botId = botId;
17
24
  this.minioClient = minioClient;
18
25
  this.bucket = bucket;
19
26
  this.logger = logger;
27
+ this.redis = redis;
20
28
  }
21
29
 
22
30
  /**
23
- * Captura la página actual y la sube a MinIO con una ruta estructurada.
24
- * Estructura: capturas/{botId}/{YYYY-MM-DD}/{nombre}.webp
25
- *
31
+ * Captura única sube a MinIO con key timestampeda.
32
+ * Ruta: capturas/{botId}/{YYYY-MM-DD}/{nombre}.webp
33
+ *
26
34
  * @param {any} page - Instancia de Page (Playwright o Puppeteer)
27
35
  * @param {string} nombre - Nombre del elemento/captura
28
- * @returns {Promise<string|null>} - Key de la captura en MinIO
36
+ * @returns {Promise<string|null>}
29
37
  */
30
38
  async capture(page, nombre) {
31
39
  try {
@@ -59,6 +67,168 @@ class PageCapturer {
59
67
  return null;
60
68
  }
61
69
  }
70
+
71
+ /**
72
+ * Live Frame — Captura y SOBRESCRIBE la misma key en MinIO.
73
+ *
74
+ * Ruta fija: live/{appId}/{instancia}/{usuario}/live.webp
75
+ *
76
+ * Una sola imagen por agente que se actualiza periódicamente
77
+ * sin acumular archivos. Headers anti-cache + Redis timestamp.
78
+ *
79
+ * @param {any} page - Instancia de Page (Playwright o Puppeteer)
80
+ * @param {object} [opts]
81
+ * @param {string} [opts.usuario='unknown'] - Identificador del usuario/agente
82
+ * @param {string} [opts.instancia] - Hostname/pod (default: HOSTNAME)
83
+ * @param {object} [opts.metadata] - Metadatos adicionales para Redis
84
+ * @param {boolean} [opts.fullPage=false] - Capturar página completa
85
+ * @returns {Promise<{key: string, updatedAt: string}|null>}
86
+ */
87
+ async captureLive(page, opts = {}) {
88
+ const usuario = opts.usuario || 'unknown';
89
+ const instancia = opts.instancia || process.env.HOSTNAME || os.hostname();
90
+ const metadata = opts.metadata || {};
91
+ const fullPage = opts.fullPage || false;
92
+
93
+ try {
94
+ const now = new Date();
95
+ const key = `live/${this.botId}/${instancia}/${usuario}/live.webp`;
96
+
97
+ const buffer = await page.screenshot({
98
+ type: 'jpeg',
99
+ quality: 80,
100
+ fullPage,
101
+ });
102
+
103
+ const finalBuffer = await sharp(buffer)
104
+ .webp({ quality: 70 })
105
+ .toBuffer();
106
+
107
+ // Sobrescribe la misma key con headers anti-cache
108
+ await this.minioClient.putObject(this.bucket, key, finalBuffer, finalBuffer.length, {
109
+ 'Content-Type': 'image/webp',
110
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
111
+ 'Pragma': 'no-cache',
112
+ 'Expires': '0',
113
+ });
114
+
115
+ const timestamp = now.toISOString();
116
+
117
+ // Actualizar metadata en Redis
118
+ if (this.redis) {
119
+ await this.redis.set(`agent:${usuario}:screenshot_updated_at`, timestamp);
120
+ await this.redis.set(`agent:${usuario}:screenshot_key`, key);
121
+ await this.redis.set(`agent:${usuario}:status`, 'active');
122
+ await this.redis.set(`agent:${usuario}:instancia`, instancia);
123
+ await this.redis.set(`agent:${usuario}:app_id`, this.botId);
124
+ if (Object.keys(metadata).length > 0) {
125
+ await this.redis.set(`agent:${usuario}:metadata`, JSON.stringify(metadata));
126
+ }
127
+ }
128
+
129
+ if (this.logger) {
130
+ this.logger.info({ key, usuario, instancia }, 'Live screenshot updated');
131
+ }
132
+
133
+ return { key, updatedAt: timestamp };
134
+ } catch (err) {
135
+ if (this.logger) {
136
+ this.logger.error({ err, usuario }, 'Error capturing live screenshot');
137
+ }
138
+ return null;
139
+ }
140
+ }
141
+
142
+ /**
143
+ * Sustituye la imagen live por un placeholder negro/offline.
144
+ *
145
+ * Se llama cuando el bot se pausa, desactiva, o termina su ejecución.
146
+ * Sobrescribe la misma key live/{appId}/{instancia}/{usuario}/live.webp
147
+ * para que el frontend refleje automáticamente el estado inactivo.
148
+ *
149
+ * @param {object} [opts]
150
+ * @param {string} [opts.usuario='unknown'] - Identificador del usuario/agente
151
+ * @param {string} [opts.instancia] - Hostname/pod (default: HOSTNAME)
152
+ * @returns {Promise<string|null>}
153
+ */
154
+ async setOffline(opts = {}) {
155
+ const usuario = opts.usuario || 'unknown';
156
+ const instancia = opts.instancia || process.env.HOSTNAME || os.hostname();
157
+
158
+ try {
159
+ const key = `live/${this.botId}/${instancia}/${usuario}/live.webp`;
160
+
161
+ // Generar placeholder offline vía SVG (sharp lo rasteriza)
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
+ // Fallback: pixel negro 1x1 si SVG falla
181
+ finalBuffer = await sharp({
182
+ create: {
183
+ width: 1,
184
+ height: 1,
185
+ channels: 3,
186
+ background: { r: 0, g: 0, b: 0 },
187
+ },
188
+ }).webp().toBuffer();
189
+ }
190
+
191
+ // Sobrescribe la misma key live con headers anti-cache
192
+ await this.minioClient.putObject(this.bucket, key, finalBuffer, finalBuffer.length, {
193
+ 'Content-Type': 'image/webp',
194
+ 'Cache-Control': 'no-cache, no-store, must-revalidate',
195
+ 'Pragma': 'no-cache',
196
+ 'Expires': '0',
197
+ });
198
+
199
+ const timestamp = new Date().toISOString();
200
+
201
+ // Actualizar Redis
202
+ if (this.redis) {
203
+ await this.redis.set(`agent:${usuario}:status`, 'paused');
204
+ await this.redis.set(`agent:${usuario}:screenshot_updated_at`, timestamp);
205
+ }
206
+
207
+ if (this.logger) {
208
+ this.logger.info({ key, usuario }, 'Offline placeholder set');
209
+ }
210
+
211
+ return key;
212
+ } catch (err) {
213
+ if (this.logger) {
214
+ this.logger.error({ err, usuario }, 'Error setting offline placeholder');
215
+ }
216
+ return null;
217
+ }
218
+ }
219
+
220
+ /**
221
+ * Libera recursos (Redis, etc.) — llamar al cerrar el bot.
222
+ */
223
+ async destroy() {
224
+ if (this.redis && typeof this.redis.quit === 'function') {
225
+ try {
226
+ await this.redis.quit();
227
+ } catch {
228
+ // ignore
229
+ }
230
+ }
231
+ }
62
232
  }
63
233
 
64
234
  module.exports = { PageCapturer };
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,21 @@ export interface ScreenshotConfig {
23
20
  /** Calidad de captura en Playwright. Default: 90 */
24
21
  captureQuality?: number;
25
22
  }
26
-
23
+ // ── Opciones de captura live ─────────────────────────────────────────────────
24
+ export interface CaptureLiveOptions {
25
+ /** Identificador del usuario/agente */
26
+ usuario?: string;
27
+ /** Hostname/pod (default: HOSTNAME) */
28
+ instancia?: string;
29
+ /** Metadatos adicionales para Redis */
30
+ metadata?: Record<string, unknown>;
31
+ /** Capturar página completa */
32
+ fullPage?: boolean;
33
+ }
34
+ export interface CaptureLiveResult {
35
+ key: string;
36
+ updatedAt: string;
37
+ }
27
38
  // ── Opciones de Pino ─────────────────────────────────────────────────────────
28
39
  export interface LogConfig {
29
40
  level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
@@ -31,7 +42,6 @@ export interface LogConfig {
31
42
  pretty?: boolean;
32
43
  pinoOptions?: Record<string, unknown>;
33
44
  }
34
-
35
45
  // ── Opciones principales de createRpaTools ───────────────────────────────────
36
46
  export interface RpaToolsOptions {
37
47
  /** ID único del bot. Default: process.env.RPA_BOT_ID */
@@ -44,24 +54,22 @@ export interface RpaToolsOptions {
44
54
  log?: LogConfig;
45
55
  /** Opciones de captura y compresión de screenshots */
46
56
  screenshot?: ScreenshotConfig;
57
+ /** Cliente Redis opcional (ioredis). Requerido para captureLive/setOffline */
58
+ redis?: any;
47
59
  }
48
-
49
60
  // ── Objeto retornado por createRpaTools ──────────────────────────────────────
50
61
  export interface RpaTools {
51
62
  /** Instancia de Pino lista para usar en el bot */
52
63
  logger: Logger;
53
-
54
64
  /** Estado compartido del bot */
55
65
  state: {
56
66
  currentStep: string;
57
67
  };
58
-
59
68
  /**
60
69
  * Cambia el paso actual, actualiza el estado y emite un log info.
61
70
  * @param name - Nombre del nuevo paso
62
71
  */
63
72
  step: (name: string) => void;
64
-
65
73
  /**
66
74
  * Inicia el polling que activa/desactiva screenshots
67
75
  * según bot_debug_config en la BD.
@@ -69,19 +77,16 @@ export interface RpaTools {
69
77
  * @param pollMs - Intervalo de polling en ms. Default: 3000
70
78
  */
71
79
  watchDebugFlag: (page: any, pollMs?: number) => void;
72
-
73
80
  /**
74
81
  * Recupera las credenciales de una aplicación desde la base de datos.
75
82
  * @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
76
83
  */
77
84
  getCredentials: (nombre: string) => Promise<{ nombre: string; usuario: string; password: string; idUsuario: string; status: boolean } | null>;
78
-
79
85
  /**
80
86
  * Recupera TODAS las credenciales que coincidan con el nombre.
81
87
  * @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
82
88
  */
83
89
  getCredentialsAll: (nombre: string) => Promise<Array<{ nombre: string; usuario: string; password: string; idUsuario: string; status: boolean }>>;
84
-
85
90
  /**
86
91
  * Actualiza el estado y las observaciones de una aplicación.
87
92
  * @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
@@ -89,20 +94,34 @@ export interface RpaTools {
89
94
  * @param observations - Observaciones/Errores
90
95
  */
91
96
  updateAppStatus: (nombre: string, status: boolean, observations: string) => Promise<boolean>;
92
-
93
97
  /**
94
98
  * Consulta si el bot está activo en la base de datos (columna 'estado').
95
99
  * @returns {Promise<boolean>}
96
100
  */
97
101
  isActive: () => Promise<boolean>;
98
-
99
102
  /**
100
- * Captura el estado de la página actual y lo sube a MinIO.
103
+ * Captura única sube a MinIO con key timestampeda.
101
104
  * @param page - Instancia de Page (Playwright o Puppeteer)
102
105
  * @param nombre - Nombre del elemento/captura
103
106
  */
104
107
  capturePage: (page: any, nombre: string) => Promise<string | null>;
105
-
108
+ /**
109
+ * Live Frame — Captura y sobrescribe la misma key en MinIO.
110
+ * Una imagen por (appId, instancia, usuario). Headers anti-cache.
111
+ *
112
+ * Requiere redis configurado en createRpaTools().
113
+ *
114
+ * @param page - Instancia de Page
115
+ * @param opts - Opciones de captura (usuario, instancia, metadata)
116
+ */
117
+ captureLive: (page: any, opts?: CaptureLiveOptions) => Promise<CaptureLiveResult | null>;
118
+ /**
119
+ * Sustituye la imagen live por un placeholder negro/offline.
120
+ * Se llama cuando el bot se pausa, desactiva, o termina.
121
+ *
122
+ * @param opts - Opciones (usuario, instancia)
123
+ */
124
+ setOffline: (opts?: { usuario?: string; instancia?: string }) => Promise<string | null>;
106
125
  /**
107
126
  * Captura screenshot, trace y registra error en base de datos.
108
127
  */
@@ -112,11 +131,9 @@ export interface RpaTools {
112
131
  err: Error;
113
132
  step?: string;
114
133
  }) => Promise<{ errorId: number | null; screenshotKey: string | null; traceKey: string | null }>;
115
-
116
134
  /** Detiene todos los intervalos. Llamar siempre al cerrar el bot. */
117
135
  destroy: () => void;
118
136
  }
119
-
120
137
  // ── Export principal ─────────────────────────────────────────────────────────
121
138
  /**
122
139
  * Inicializa la librería rpa-tools.
package/src/index.js CHANGED
@@ -10,14 +10,14 @@ const { PageCapturer } = require('./capture');
10
10
  * Factory principal de la librería.
11
11
  *
12
12
  * @param {object} opts
13
- * @param {string} opts.botId - identificador único del bot
13
+ * @param {string} opts.botId - identificador único del bot/app
14
14
  * @param {import('pg').Pool} opts.db - pool de pg ya conectado
15
15
  * @param {object} opts.minio - config de MinIO
16
16
  * @param {object} [opts.log] - opciones de Pino
17
- *
18
17
  * @param {object} [opts.screenshot] - opciones de captura (formato, calidad)
18
+ * @param {object} [opts.redis] - cliente Redis opcional (ioredis)
19
19
  *
20
- * @returns {{ logger, state, step, watchDebugFlag, getCredentials, getCredentialsAll, updateAppStatus, isActive, capturePage, captureError, destroy }}
20
+ * @returns {Promise<{ logger, state, step, watchDebugFlag, getCredentials, getCredentialsAll, updateAppStatus, isActive, capturePage, captureLive, setOffline, captureError, destroy }>}
21
21
  */
22
22
  async function createRpaTools(opts = {}) {
23
23
  const {
@@ -26,6 +26,7 @@ async function createRpaTools(opts = {}) {
26
26
  db,
27
27
  minio: minioCfg,
28
28
  screenshot: screenshotCfg,
29
+ redis: redisClient,
29
30
  } = opts;
30
31
 
31
32
  const minio = minioCfg ?? {
@@ -49,7 +50,7 @@ async function createRpaTools(opts = {}) {
49
50
 
50
51
  const watcher = new ScreenshotWatcher({ botId, pool: db, minioClient, bucket, logger, screenshot: screenshotCfg });
51
52
  const credentials = new CredentialManager({ botId, pool: db });
52
- const capturer = new PageCapturer({ botId, minioClient, bucket, logger });
53
+ const capturer = new PageCapturer({ botId, minioClient, bucket, logger, redis: redisClient });
53
54
 
54
55
  const state = { currentStep: 'Inicio' };
55
56
 
@@ -86,14 +87,34 @@ async function createRpaTools(opts = {}) {
86
87
  }
87
88
  },
88
89
  /**
89
- * Captura el estado de la página actual y lo sube a MinIO.
90
+ * Captura única sube a MinIO con key timestampeda.
90
91
  * @param {any} page - Instancia de Page
91
92
  * @param {string} nombre - Nombre del elemento/captura
92
93
  */
93
94
  capturePage: (page, nombre) => capturer.capture(page, nombre),
94
95
  /**
95
- * Captura screenshot y registra error en base de datos.
96
- * Soporta Playwright (con tracing opcional) y Puppeteer.
96
+ * Live Frame — Captura y sobrescribe la misma key en MinIO.
97
+ * Una imagen por (appId, instancia, usuario). Headers anti-cache.
98
+ *
99
+ * @param {any} page - Instancia de Page (Playwright o Puppeteer)
100
+ * @param {object} [opts]
101
+ * @param {string} [opts.usuario] - Identificador del usuario/agente
102
+ * @param {string} [opts.instancia] - Hostname/pod
103
+ * @param {object} [opts.metadata] - Metadatos para Redis
104
+ * @param {boolean} [opts.fullPage] - Capturar página completa
105
+ * @returns {Promise<{key: string, updatedAt: string}|null>}
106
+ */
107
+ captureLive: (page, opts) => capturer.captureLive(page, opts),
108
+ /**
109
+ * Sustituye la imagen live por un placeholder offline.
110
+ * @param {object} [opts]
111
+ * @param {string} [opts.usuario] - Identificador del usuario/agente
112
+ * @param {string} [opts.instancia] - Hostname/pod
113
+ * @returns {Promise<string|null>}
114
+ */
115
+ setOffline: (opts) => capturer.setOffline(opts),
116
+ /**
117
+ * Captura screenshot y registra error en base de datos.
97
118
  */
98
119
  captureError: ({ page, context, err, step }) => captureError({
99
120
  page,
@@ -106,7 +127,10 @@ async function createRpaTools(opts = {}) {
106
127
  bucket,
107
128
  logger
108
129
  }),
109
- destroy: () => watcher.destroy(),
130
+ destroy: () => {
131
+ watcher.destroy();
132
+ capturer.destroy().catch(() => {});
133
+ },
110
134
  };
111
135
  }
112
136