@gnpdev/rpa-tools 1.0.16 → 1.0.18

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.0.16",
3
+ "version": "1.0.18",
4
4
  "description": "Libreria para logs y screenshot de bots",
5
5
  "author": "Sergio Antonio Trujillo del Valle",
6
6
  "main": "src/index.js",
@@ -1,149 +1,155 @@
1
- 'use strict';
2
- const fs = require('fs/promises');
3
- const path = require('path');
4
-
5
- /**
6
- * Sube un buffer o archivo local a MinIO y retorna el key generado.
7
- * @param {import('minio').Client} client
8
- * @param {string} bucket
9
- * @param {string} key
10
- * @param {Buffer|string} source - Buffer o ruta de archivo local
11
- * @param {string} contentType
12
- */
13
- async function uploadToMinio(client, bucket, key, source, contentType) {
14
- const buffer = typeof source === 'string'
15
- ? await fs.readFile(source)
16
- : source;
17
-
18
- await client.putObject(bucket, key, buffer, buffer.length, {
19
- 'Content-Type': contentType,
20
- });
21
-
22
- return key;
23
- }
24
-
25
- /**
26
- * Genera el prefijo base para los archivos de error en MinIO.
27
- * Estructura: errors/{botId}/{YYYY-MM-DD}/{ISO-timestamp}
28
- */
29
- function errorPrefix(botId) {
30
- const now = new Date();
31
- const date = now.toISOString().slice(0, 10);
32
- const ts = now.toISOString().replace(/[:.]/g, '-');
33
- return `errors/${botId}/${date}/${ts}`;
34
- }
35
-
36
- /**
37
- * Captura el estado completo cuando ocurre un error:
38
- * - Screenshot WebP de la página actual
39
- * - Trace de Playwright (.zip con video, red, consola, DOM snapshots)
40
- * - Registro en bots.tb_error_bots
41
- *
42
- * @param {object} p
43
- * @param {import('playwright').Page} p.page
44
- * @param {import('playwright').BrowserContext} p.context
45
- * @param {Error} p.err
46
- * @param {string} p.botId - UUID del bot
47
- * @param {import('pg').Pool} p.pool
48
- * @param {import('minio').Client} p.minioClient
49
- * @param {string} p.bucket
50
- * @param {import('pino').Logger} p.logger
51
- * @param {string} [p.step] - paso donde falló
52
- *
53
- * @returns {Promise<{ screenshotKey: string|null, traceKey: string|null, errorId: number|null }>}
54
- */
55
- async function captureError({ page, context, err, botId, pool, minioClient, bucket, logger, step }) {
56
- const prefix = errorPrefix(botId);
57
- const stepInfo = step ? { step } : {};
58
- const result = { screenshotKey: null, traceKey: null, errorId: null };
59
-
60
- // ── 1. Screenshot ────────────────────────────────────────────────────────
61
- try {
62
- const buffer = await page.screenshot({
63
- type: 'jpeg',
64
- quality: 90,
65
- fullPage: true,
66
- });
67
-
68
- let finalBuffer = buffer;
69
- let ext = 'jpg';
70
- let contentType = 'image/jpeg';
71
-
72
- try {
73
- const sharp = require('sharp');
74
- finalBuffer = await sharp(buffer).webp({ quality: 80 }).toBuffer();
75
- ext = 'webp';
76
- contentType = 'image/webp';
77
- } catch {
78
- // sharp no disponible, se sube en jpeg
79
- }
80
-
81
- result.screenshotKey = await uploadToMinio(
82
- minioClient, bucket,
83
- `${prefix}-error.${ext}`,
84
- finalBuffer,
85
- contentType
86
- );
87
-
88
- logger.info({ ...stepInfo, screenshotKey: result.screenshotKey }, 'Screenshot de error subido');
89
- } catch (screenshotErr) {
90
- logger.error({ err: screenshotErr }, 'No se pudo capturar screenshot de error');
91
- }
92
-
93
- // ── 2. Trace ─────────────────────────────────────────────────────────────
94
- const tracePath = path.join(require('os').tmpdir(), `trace-${botId}-${Date.now()}.zip`);
95
-
96
- try {
97
- await context.tracing.stop({ path: tracePath });
98
-
99
- result.traceKey = await uploadToMinio(
100
- minioClient, bucket,
101
- `${prefix}-trace.zip`,
102
- tracePath,
103
- 'application/zip'
104
- );
105
-
106
- logger.info({ ...stepInfo, traceKey: result.traceKey }, 'Trace de error subido');
107
- } catch (traceErr) {
108
- logger.error({ err: traceErr }, 'No se pudo capturar trace de error');
109
- } finally {
110
- await fs.unlink(tracePath).catch(() => {});
111
- }
112
-
113
- // ── 3. Registro en bots.tb_error_bots ───────────────────────────────────
114
- try {
115
- const { rows } = await pool.query(
116
- `INSERT INTO bots.tb_error_bots
117
- (bot_id, step, error_message, url, screenshot_key, trace_key)
118
- VALUES ($1::uuid, $2, $3, $4, $5, $6)
119
- RETURNING id`,
120
- [
121
- botId,
122
- step ?? null,
123
- err.message,
124
- page.url() ?? null,
125
- result.screenshotKey ?? null,
126
- result.traceKey ?? null,
127
- ]
128
- );
129
-
130
- result.errorId = rows[0].id;
131
- logger.info({ ...stepInfo, errorId: result.errorId }, 'Error registrado en BD');
132
- } catch (dbErr) {
133
- logger.error({ err: dbErr }, 'No se pudo registrar el error en BD');
134
- }
135
-
136
- // ── 4. Log estructurado final ────────────────────────────────────────────
137
- logger.error({
138
- ...stepInfo,
139
- err,
140
- url: page.url(),
141
- screenshotKey: result.screenshotKey,
142
- traceKey: result.traceKey,
143
- errorId: result.errorId,
144
- }, 'Error capturado en playground');
145
-
146
- return result;
147
- }
148
-
1
+ 'use strict';
2
+ const fs = require('fs/promises');
3
+ const path = require('path');
4
+
5
+ /**
6
+ * Sube un buffer o archivo local a MinIO y retorna el key generado.
7
+ * @param {import('minio').Client} client
8
+ * @param {string} bucket
9
+ * @param {string} key
10
+ * @param {Buffer|string} source - Buffer o ruta de archivo local
11
+ * @param {string} contentType
12
+ */
13
+ async function uploadToMinio(client, bucket, key, source, contentType) {
14
+ const buffer = typeof source === 'string'
15
+ ? await fs.readFile(source)
16
+ : source;
17
+
18
+ await client.putObject(bucket, key, buffer, buffer.length, {
19
+ 'Content-Type': contentType,
20
+ });
21
+
22
+ return key;
23
+ }
24
+
25
+ /**
26
+ * Genera el prefijo base para los archivos de error en MinIO.
27
+ * Estructura: errors/{botId}/{YYYY-MM-DD}/{ISO-timestamp}
28
+ */
29
+ function errorPrefix(botId) {
30
+ const now = new Date();
31
+ const date = now.toISOString().slice(0, 10);
32
+ const ts = now.toISOString().replace(/[:.]/g, '-');
33
+ return `errors/${botId}/${date}/${ts}`;
34
+ }
35
+
36
+ /**
37
+ * Captura el estado completo cuando ocurre un error:
38
+ * - Screenshot WebP de la página actual
39
+ * - Trace de Playwright (.zip con video, red, consola, DOM snapshots)
40
+ * - Registro en bots.tb_error_bots
41
+ *
42
+ * @param {object} p
43
+ * @param {import('playwright').Page} p.page
44
+ * @param {import('playwright').BrowserContext} p.context
45
+ * @param {Error} p.err
46
+ * @param {string} p.botId - UUID del bot
47
+ * @param {import('pg').Pool} p.pool
48
+ * @param {import('minio').Client} p.minioClient
49
+ * @param {string} p.bucket
50
+ * @param {import('pino').Logger} p.logger
51
+ * @param {string} [p.step] - paso donde falló
52
+ *
53
+ * @returns {Promise<{ screenshotKey: string|null, traceKey: string|null, errorId: number|null }>}
54
+ */
55
+ async function captureError({ page, context, err, botId, pool, minioClient, bucket, logger, step }) {
56
+ const prefix = errorPrefix(botId);
57
+ const stepInfo = step ? { step } : {};
58
+ const result = { screenshotKey: null, traceKey: null, errorId: null };
59
+
60
+ // ── 1. Screenshot ────────────────────────────────────────────────────────
61
+ try {
62
+ const buffer = await page.screenshot({
63
+ type: 'jpeg',
64
+ quality: 90,
65
+ fullPage: true,
66
+ });
67
+
68
+ let finalBuffer = buffer;
69
+ let ext = 'jpg';
70
+ let contentType = 'image/jpeg';
71
+
72
+ try {
73
+ const sharp = require('sharp');
74
+ finalBuffer = await sharp(buffer).webp({ quality: 80 }).toBuffer();
75
+ ext = 'webp';
76
+ contentType = 'image/webp';
77
+ } catch {
78
+ // sharp no disponible, se sube en jpeg
79
+ }
80
+
81
+ result.screenshotKey = await uploadToMinio(
82
+ minioClient, bucket,
83
+ `${prefix}-error.${ext}`,
84
+ finalBuffer,
85
+ contentType
86
+ );
87
+
88
+ logger.info({ ...stepInfo, screenshotKey: result.screenshotKey }, 'Screenshot de error subido');
89
+ } catch (screenshotErr) {
90
+ logger.error({ err: screenshotErr }, 'No se pudo capturar screenshot de error');
91
+ }
92
+
93
+ // ── 2. Trace ─────────────────────────────────────────────────────────────
94
+ const tracePath = path.join(require('os').tmpdir(), `trace-${botId}-${Date.now()}.zip`);
95
+
96
+ try {
97
+ if (context?.tracing) {
98
+ await context.tracing.stop({ path: tracePath });
99
+
100
+ result.traceKey = await uploadToMinio(
101
+ minioClient, bucket,
102
+ `${prefix}-trace.zip`,
103
+ tracePath,
104
+ 'application/zip'
105
+ );
106
+
107
+ logger.info({ ...stepInfo, traceKey: result.traceKey }, 'Trace de error subido');
108
+ } else {
109
+ logger.debug('Tracing no disponible (Puppeteer o no activado)');
110
+ }
111
+ } catch (traceErr) {
112
+ logger.error({ err: traceErr }, 'No se pudo capturar trace de error');
113
+ } finally {
114
+ if (context?.tracing) {
115
+ await fs.unlink(tracePath).catch(() => {});
116
+ }
117
+ }
118
+
119
+ // ── 3. Registro en bots.tb_error_bots ───────────────────────────────────
120
+ try {
121
+ const { rows } = await pool.query(
122
+ `INSERT INTO bots.tb_error_bots
123
+ (bot_id, step, error_message, url, screenshot_key, trace_key)
124
+ VALUES ($1::uuid, $2, $3, $4, $5, $6)
125
+ RETURNING id`,
126
+ [
127
+ botId,
128
+ step ?? null,
129
+ err.message,
130
+ page.url() ?? null,
131
+ result.screenshotKey ?? null,
132
+ result.traceKey ?? null,
133
+ ]
134
+ );
135
+
136
+ result.errorId = rows[0].id;
137
+ logger.info({ ...stepInfo, errorId: result.errorId }, 'Error registrado en BD');
138
+ } catch (dbErr) {
139
+ logger.error({ err: dbErr }, 'No se pudo registrar el error en BD');
140
+ }
141
+
142
+ // ── 4. Log estructurado final ────────────────────────────────────────────
143
+ logger.error({
144
+ ...stepInfo,
145
+ err,
146
+ url: page.url(),
147
+ screenshotKey: result.screenshotKey,
148
+ traceKey: result.traceKey,
149
+ errorId: result.errorId,
150
+ }, 'Error capturado en playground');
151
+
152
+ return result;
153
+ }
154
+
149
155
  module.exports = { captureError };
package/src/index.d.ts CHANGED
@@ -63,10 +63,10 @@ export interface RpaTools {
63
63
  /**
64
64
  * Inicia el polling que activa/desactiva screenshots
65
65
  * según bot_debug_config en la BD.
66
- * @param page - Objeto Page de Playwright
66
+ * @param page - Objeto Page de Playwright o Puppeteer
67
67
  * @param pollMs - Intervalo de polling en ms. Default: 3000
68
68
  */
69
- watchDebugFlag: (page: Page, pollMs?: number) => void;
69
+ watchDebugFlag: (page: any, pollMs?: number) => void;
70
70
 
71
71
  /**
72
72
  * Recupera credenciales de una aplicación desde la base de datos.
@@ -78,8 +78,8 @@ export interface RpaTools {
78
78
  * Captura screenshot, trace y registra error en base de datos.
79
79
  */
80
80
  captureError: (params: {
81
- page: Page;
82
- context: any;
81
+ page: any;
82
+ context?: any;
83
83
  err: Error;
84
84
  step?: string;
85
85
  }) => Promise<{ errorId: number | null; screenshotKey: string | null; traceKey: string | null }>;
package/src/index.js CHANGED
@@ -53,7 +53,7 @@ async function createRpaTools(opts = {}) {
53
53
  state,
54
54
  step: (name) => {
55
55
  state.currentStep = name;
56
- logger.info({ step: name }, 'Nuevo paso');
56
+ logger.info(name + ' - Nuevo paso');
57
57
  },
58
58
  watchDebugFlag: (page, pollMs) => watcher.watch(page, pollMs),
59
59
  getCredentials: (nombre) => credentials.getAppCredentials(nombre),
package/src/storage.js CHANGED
@@ -1,83 +1,101 @@
1
- 'use strict';
2
- const Minio = require('minio');
3
- const sharp = require('sharp');
4
-
5
- /**
6
- * Crea y retorna el cliente de MinIO.
7
- * @param {object} cfg
8
- * @param {string} cfg.endPoint
9
- * @param {number} [cfg.port=9000]
10
- * @param {boolean} [cfg.useSSL=false]
11
- * @param {string} cfg.accessKey
12
- * @param {string} cfg.secretKey
13
- */
14
- function createMinioClient(cfg) {
15
- return new Minio.Client({
16
- endPoint: cfg.endPoint,
17
- port: cfg.port ?? 9000,
18
- useSSL: cfg.useSSL ?? false,
19
- accessKey: cfg.accessKey,
20
- secretKey: cfg.secretKey,
21
- });
22
- }
23
-
24
- /**
25
- * Garantiza que el bucket exista; lo crea si no.
26
- * @param {import('minio').Client} client
27
- * @param {string} bucket
28
- * @param {string} [region='us-east-1']
29
- */
30
- async function ensureBucket(client, bucket, region = 'us-east-1') {
31
- const exists = await client.bucketExists(bucket);
32
- if (!exists) await client.makeBucket(bucket, region);
33
- }
34
-
35
- /**
36
- * Convierte un buffer de imagen a WebP usando sharp.
37
- * @param {Buffer} buffer - buffer fuente (jpeg/png)
38
- * @param {number} [quality=70]
39
- * @returns {Promise<Buffer>}
40
- */
41
- async function toWebp(buffer, quality = 70) {
42
- return sharp(buffer)
43
- .webp({ quality })
44
- .toBuffer();
45
- }
46
-
47
- /**
48
- * Sube un screenshot a MinIO.
49
- * Si format='webp' convierte el buffer antes de subir.
50
- * Ruta generada: {botId}/{YYYY-MM-DD}/{ISO-timestamp}.{ext}
51
- *
52
- * @param {import('minio').Client} client
53
- * @param {string} bucket
54
- * @param {string} botId
55
- * @param {Buffer} buffer - buffer raw de Playwright (jpeg)
56
- * @param {object} [opts]
57
- * @param {'webp'|'jpeg'} [opts.format='webp']
58
- * @param {number} [opts.quality=70] - calidad 1-100
59
- * @returns {Promise<string>} - object key en MinIO
60
- */
61
- async function uploadScreenshot(client, bucket, botId, buffer, opts = {}) {
62
- const { format = 'webp', quality = 70 } = opts;
63
-
64
- const finalBuffer = format === 'webp'
65
- ? await toWebp(buffer, quality)
66
- : buffer;
67
-
68
- const contentType = format === 'webp' ? 'image/webp' : 'image/jpeg';
69
- const ext = format === 'webp' ? 'webp' : 'jpg';
70
-
71
- const now = new Date();
72
- const date = now.toISOString().slice(0, 10); // 2025-06-01
73
- const ts = now.toISOString().replace(/[:.]/g, '-'); // 2025-06-01T12-30-00-000Z
74
- const key = `${botId}.${ext}`;
75
-
76
- await client.putObject(bucket, key, finalBuffer, finalBuffer.length, {
77
- 'Content-Type': contentType,
78
- });
79
-
80
- return key;
81
- }
82
-
1
+ 'use strict';
2
+ const Minio = require('minio');
3
+ const sharp = require('sharp');
4
+
5
+ /**
6
+ * Crea y retorna el cliente de MinIO.
7
+ * @param {object} cfg
8
+ * @param {string} cfg.endPoint
9
+ * @param {number} [cfg.port=9000]
10
+ * @param {boolean} [cfg.useSSL=false]
11
+ * @param {string} cfg.accessKey
12
+ * @param {string} cfg.secretKey
13
+ */
14
+ function createMinioClient(cfg) {
15
+ return new Minio.Client({
16
+ endPoint: cfg.endPoint,
17
+ port: cfg.port ?? 9000,
18
+ useSSL: cfg.useSSL ?? false,
19
+ accessKey: cfg.accessKey,
20
+ secretKey: cfg.secretKey,
21
+ });
22
+ }
23
+
24
+ /**
25
+ * Garantiza que el bucket exista; lo crea si no.
26
+ * @param {import('minio').Client} client
27
+ * @param {string} bucket
28
+ * @param {string} [region='us-east-1']
29
+ */
30
+ async function ensureBucket(client, bucket, region = 'us-east-1') {
31
+ const exists = await client.bucketExists(bucket);
32
+ if (!exists) await client.makeBucket(bucket, region);
33
+ }
34
+
35
+ /**
36
+ * Convierte un buffer de imagen a WebP usando sharp.
37
+ * @param {Buffer} buffer - buffer fuente (jpeg/png)
38
+ * @param {number} [quality=70]
39
+ * @returns {Promise<Buffer>}
40
+ */
41
+ async function toWebp(buffer, quality = 70) {
42
+ return sharp(buffer)
43
+ .webp({ quality })
44
+ .toBuffer();
45
+ }
46
+
47
+ /**
48
+ * Sube un screenshot a MinIO.
49
+ * Si format='webp' convierte el buffer antes de subir.
50
+ * Ruta generada: {botId}/{YYYY-MM-DD}/{ISO-timestamp}.{ext}
51
+ *
52
+ * @param {import('minio').Client} client
53
+ * @param {string} bucket
54
+ * @param {string} botId
55
+ * @param {Buffer} buffer - buffer raw de Playwright (jpeg)
56
+ * @param {object} [opts]
57
+ * @param {'webp'|'jpeg'} [opts.format='webp']
58
+ * @param {number} [opts.quality=70] - calidad 1-100
59
+ * @param {object} [opts.pool] - Pool de base de datos
60
+ * @param {object} [opts.logger] - Instancia de logger
61
+ * @returns {Promise<string>} - object key en MinIO
62
+ */
63
+ async function uploadScreenshot(client, bucket, botId, buffer, opts = {}) {
64
+ const { format = 'webp', quality = 70, pool, logger } = opts;
65
+
66
+ const finalBuffer = format === 'webp'
67
+ ? await toWebp(buffer, quality)
68
+ : buffer;
69
+
70
+ const contentType = format === 'webp' ? 'image/webp' : 'image/jpeg';
71
+ const ext = format === 'webp' ? 'webp' : 'jpg';
72
+ const key = `${botId}.${ext}`;
73
+
74
+ await client.putObject(bucket, key, finalBuffer, finalBuffer.length, {
75
+ 'Content-Type': contentType,
76
+ });
77
+
78
+ // ── 3. Registro en base de datos ─────────────────────────────────────────
79
+ if (pool) {
80
+ try {
81
+ await pool.query(
82
+ `UPDATE bots.tb_bots
83
+ SET url_screenshot = $1,
84
+ updated_at = NOW()
85
+ WHERE id = $2::uuid`,
86
+ [key, botId]
87
+ );
88
+
89
+ } catch (err) {
90
+ if (logger) {
91
+ logger.error({ err, botId, key }, 'No se pudo actualizar url_screenshot en BD');
92
+ } else {
93
+ console.error(`[Storage] Error BD (${botId}):`, err.message);
94
+ }
95
+ }
96
+ }
97
+
98
+ return key;
99
+ }
100
+
83
101
  module.exports = { createMinioClient, ensureBucket, uploadScreenshot };
package/src/watcher.js CHANGED
@@ -1,137 +1,139 @@
1
- 'use strict';
2
- const { uploadScreenshot } = require('./storage');
3
-
4
- class ScreenshotWatcher {
5
- /**
6
- * @param {object} p
7
- * @param {string} p.botId
8
- * @param {import('pg').Pool} p.pool
9
- * @param {import('minio').Client} p.minioClient
10
- * @param {string} p.bucket
11
- * @param {import('pino').Logger} p.logger
12
- * @param {object} [p.screenshot]
13
- * @param {'webp'|'jpeg'} [p.screenshot.format='webp']
14
- * @param {number} [p.screenshot.quality=70] - calidad conversión 1-100
15
- * @param {number} [p.screenshot.captureQuality=90] - calidad captura Playwright
16
- */
17
- constructor({ botId, pool, minioClient, bucket, logger, screenshot = {} }) {
18
- this.botId = botId;
19
- this.pool = pool;
20
- this.minioClient = minioClient;
21
- this.bucket = bucket;
22
- this.logger = logger;
23
-
24
- this.screenshotOpts = {
25
- format: screenshot.format ?? 'webp',
26
- quality: screenshot.quality ?? 70,
27
- captureQuality: screenshot.captureQuality ?? 90,
28
- };
29
-
30
- this._watchTimer = null;
31
- this._shotTimer = null;
32
- }
33
-
34
- /**
35
- * Inicia el polling que activa/desactiva screenshots según bot_debug_config.
36
- * Llama una sola vez al arrancar el bot.
37
- *
38
- * @param {import('playwright').Page} page
39
- * @param {number} [pollMs=3000] - cada cuántos ms revisar la BD
40
- */
41
- watch(page, pollMs = 3000) {
42
- if (this._watchTimer) {
43
- this.logger.warn('ScreenshotWatcher ya está corriendo, ignorando llamada duplicada');
44
- return;
45
- }
46
-
47
- this.logger.info({ pollMs }, 'Iniciando polling de debug config');
48
-
49
- this._watchTimer = setInterval(async () => {
50
- try {
51
- const { rows } = await this.pool.query(
52
- `SELECT screenshots_activo, intervalo_sec
53
- FROM bots.tb_bots
54
- WHERE id = $1`,
55
- [this.botId]
56
- );
57
-
58
- const cfg = rows[0];
59
- // console.log('DEBUG CONFIG', cfg); // <-- loguear config para debug
60
-
61
- if (!cfg) {
62
- this.logger.debug('Sin config en bot_debug_config para este bot');
63
- return;
64
- }
65
-
66
- if (cfg.screenshots_activo && !this._shotTimer) {
67
- this.logger.info({ intervalSec: cfg.intervalo_sec }, 'Screenshots activados');
68
- this._startScreenshots(page, cfg.intervalo_sec * 1000);
69
- return;
70
- }
71
-
72
- if (!cfg.screenshots_activo && this._shotTimer) {
73
- this.logger.info('Screenshots desactivados');
74
- this._stopScreenshots();
75
- }
76
- } catch (err) {
77
- this.logger.error({ err }, 'Error leyendo bot_debug_config');
78
- }
79
- }, pollMs);
80
- }
81
-
82
- /**
83
- * @private
84
- */
85
- _startScreenshots(page, intervalMs) {
86
- this._shotTimer = setInterval(async () => {
87
- const t0 = Date.now();
88
-
89
- try {
90
- // Playwright captura en jpeg (más rápido), sharp convierte a WebP después
91
- const rawBuffer = await page.screenshot({
92
- type: 'jpeg',
93
- quality: this.screenshotOpts.captureQuality,
94
- fullPage: false,
95
- });
96
-
97
- const key = await uploadScreenshot(
98
- this.minioClient,
99
- this.bucket,
100
- this.botId,
101
- rawBuffer,
102
- {
103
- format: this.screenshotOpts.format,
104
- quality: this.screenshotOpts.quality,
105
- }
106
- );
107
-
108
- this.logger.debug(
109
- { key, ms: Date.now() - t0 },
110
- 'Screenshot subido'
111
- );
112
- } catch (err) {
113
- this.logger.error({ err }, 'Error capturando o subiendo screenshot');
114
- }
115
- }, intervalMs);
116
- }
117
-
118
- /**
119
- * @private
120
- */
121
- _stopScreenshots() {
122
- clearInterval(this._shotTimer);
123
- this._shotTimer = null;
124
- }
125
-
126
- /**
127
- * Detiene todos los intervalos. Llamar siempre al cerrar el bot.
128
- */
129
- destroy() {
130
- this._stopScreenshots();
131
- clearInterval(this._watchTimer);
132
- this._watchTimer = null;
133
- this.logger.info('ScreenshotWatcher destruido');
134
- }
135
- }
136
-
1
+ 'use strict';
2
+ const { uploadScreenshot } = require('./storage');
3
+
4
+ class ScreenshotWatcher {
5
+ /**
6
+ * @param {object} p
7
+ * @param {string} p.botId
8
+ * @param {import('pg').Pool} p.pool
9
+ * @param {import('minio').Client} p.minioClient
10
+ * @param {string} p.bucket
11
+ * @param {import('pino').Logger} p.logger
12
+ * @param {object} [p.screenshot]
13
+ * @param {'webp'|'jpeg'} [p.screenshot.format='webp']
14
+ * @param {number} [p.screenshot.quality=70] - calidad conversión 1-100
15
+ * @param {number} [p.screenshot.captureQuality=90] - calidad captura Playwright
16
+ */
17
+ constructor({ botId, pool, minioClient, bucket, logger, screenshot = {} }) {
18
+ this.botId = botId;
19
+ this.pool = pool;
20
+ this.minioClient = minioClient;
21
+ this.bucket = bucket;
22
+ this.logger = logger;
23
+
24
+ this.screenshotOpts = {
25
+ format: screenshot.format ?? 'webp',
26
+ quality: screenshot.quality ?? 70,
27
+ captureQuality: screenshot.captureQuality ?? 90,
28
+ };
29
+
30
+ this._watchTimer = null;
31
+ this._shotTimer = null;
32
+ }
33
+
34
+ /**
35
+ * Inicia el polling que activa/desactiva screenshots según bot_debug_config.
36
+ * Llama una sola vez al arrancar el bot.
37
+ *
38
+ * @param {import('playwright').Page} page
39
+ * @param {number} [pollMs=3000] - cada cuántos ms revisar la BD
40
+ */
41
+ watch(page, pollMs = 3000) {
42
+ if (this._watchTimer) {
43
+ this.logger.warn('ScreenshotWatcher ya está corriendo, ignorando llamada duplicada');
44
+ return;
45
+ }
46
+
47
+ this.logger.info({ pollMs }, 'Iniciando polling de debug config');
48
+
49
+ this._watchTimer = setInterval(async () => {
50
+ try {
51
+ const { rows } = await this.pool.query(
52
+ `SELECT screenshots_activo, intervalo_sec
53
+ FROM bots.tb_bots
54
+ WHERE id = $1`,
55
+ [this.botId]
56
+ );
57
+
58
+ const cfg = rows[0];
59
+ // console.log('DEBUG CONFIG', cfg); // <-- loguear config para debug
60
+
61
+ if (!cfg) {
62
+ this.logger.debug('Sin config en bot_debug_config para este bot');
63
+ return;
64
+ }
65
+
66
+ if (cfg.screenshots_activo && !this._shotTimer) {
67
+ this.logger.info({ intervalSec: cfg.intervalo_sec }, 'Screenshots activados');
68
+ this._startScreenshots(page, cfg.intervalo_sec * 1000);
69
+ return;
70
+ }
71
+
72
+ if (!cfg.screenshots_activo && this._shotTimer) {
73
+ this.logger.info('Screenshots desactivados');
74
+ this._stopScreenshots();
75
+ }
76
+ } catch (err) {
77
+ this.logger.error({ err }, 'Error leyendo bot_debug_config');
78
+ }
79
+ }, pollMs);
80
+ }
81
+
82
+ /**
83
+ * @private
84
+ */
85
+ _startScreenshots(page, intervalMs) {
86
+ this._shotTimer = setInterval(async () => {
87
+ const t0 = Date.now();
88
+
89
+ try {
90
+ // Playwright captura en jpeg (más rápido), sharp convierte a WebP después
91
+ const rawBuffer = await page.screenshot({
92
+ type: 'jpeg',
93
+ quality: this.screenshotOpts.captureQuality,
94
+ fullPage: false,
95
+ });
96
+
97
+ const key = await uploadScreenshot(
98
+ this.minioClient,
99
+ this.bucket,
100
+ this.botId,
101
+ rawBuffer,
102
+ {
103
+ format: this.screenshotOpts.format,
104
+ quality: this.screenshotOpts.quality,
105
+ pool: this.pool,
106
+ logger: this.logger,
107
+ }
108
+ );
109
+
110
+ this.logger.debug(
111
+ { key, ms: Date.now() - t0 },
112
+ 'Screenshot subido'
113
+ );
114
+ } catch (err) {
115
+ this.logger.error({ err }, 'Error capturando o subiendo screenshot');
116
+ }
117
+ }, intervalMs);
118
+ }
119
+
120
+ /**
121
+ * @private
122
+ */
123
+ _stopScreenshots() {
124
+ clearInterval(this._shotTimer);
125
+ this._shotTimer = null;
126
+ }
127
+
128
+ /**
129
+ * Detiene todos los intervalos. Llamar siempre al cerrar el bot.
130
+ */
131
+ destroy() {
132
+ this._stopScreenshots();
133
+ clearInterval(this._watchTimer);
134
+ this._watchTimer = null;
135
+ this.logger.info('ScreenshotWatcher destruido');
136
+ }
137
+ }
138
+
137
139
  module.exports = { ScreenshotWatcher };