@gnpdev/rpa-tools 1.1.3 → 1.1.4

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
@@ -5,14 +5,14 @@ Herramientas de automatización para bots RPA: logging estructurado con **Pino**
5
5
  ## Requisitos
6
6
 
7
7
  - **Node.js** v18+
8
- - **pnpm** (recomendado)
8
+ - **npm** o **pnpm**
9
9
  - Instancia de **MinIO** activa.
10
10
  - Base de datos **PostgreSQL**.
11
11
 
12
12
  ## Instalación
13
13
 
14
14
  ```bash
15
- pnpm add @gnpdev/rpa-tools
15
+ npm install @gnpdev/rpa-tools
16
16
  ```
17
17
 
18
18
  ## Configuración Centralizada (Best Practice)
@@ -91,6 +91,10 @@ if (credentials) {
91
91
  // usar en el login de la web
92
92
  }
93
93
 
94
+ // Si pueden existir múltiples registros con el mismo nombre:
95
+ const allCredentials = await rpa.getCredentialsAll('Portal CRM');
96
+ // allCredentials → Array de { nombre, usuario, password, idUsuario, status }
97
+
94
98
  // En caso de fallo de login o error en la aplicación
95
99
  await rpa.updateAppStatus('Portal CRM', false, 'Credenciales inválidas o bloqueo de cuenta');
96
100
  ```
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@gnpdev/rpa-tools",
3
- "version": "1.1.3",
3
+ "version": "1.1.4",
4
4
  "description": "Libreria para logs y screenshot de bots",
5
5
  "author": "Sergio Antonio Trujillo del Valle",
6
6
  "main": "src/index.js",
@@ -31,6 +31,22 @@ class CredentialManager {
31
31
  return rows.length > 0 ? rows[0] : null;
32
32
  }
33
33
 
34
+ /**
35
+ * Obtiene todas las credenciales que coincidan con el nombre y bot ID.
36
+ * @param {string} nombre - Nombre de la aplicación
37
+ * @returns {Promise<Array<{nombre: string, usuario: string, password: string, idUsuario: string, status: boolean}>>}
38
+ */
39
+ async getAppCredentialsAll(nombre) {
40
+ const { rows } = await this.pool.query(
41
+ `SELECT nombre, usuario, password, "idUsuario", status
42
+ FROM bots.tb_aplicaciones_bots
43
+ WHERE nombre = $1 AND bot_id = $2`,
44
+ [nombre, this.botId]
45
+ );
46
+
47
+ return rows;
48
+ }
49
+
34
50
  /**
35
51
  * Actualiza el estado y las observaciones de una aplicación.
36
52
  * @param {string} nombre - Nombre de la aplicación
@@ -48,6 +64,7 @@ class CredentialManager {
48
64
  );
49
65
  return true;
50
66
  } catch (err) {
67
+ console.error(`[Credentials] Error actualizando estado de "${nombre}":`, err.message);
51
68
  return false;
52
69
  }
53
70
  }
@@ -117,6 +117,9 @@ async function captureError({ page, context, err, botId, pool, minioClient, buck
117
117
  }
118
118
 
119
119
  // ── 3. Registro en bots.tb_error_bots ───────────────────────────────────
120
+ let pageUrl = null;
121
+ try { pageUrl = page.url(); } catch { /* page already closed */ }
122
+
120
123
  try {
121
124
  const { rows } = await pool.query(
122
125
  `INSERT INTO bots.tb_error_bots
@@ -127,7 +130,7 @@ async function captureError({ page, context, err, botId, pool, minioClient, buck
127
130
  botId,
128
131
  step ?? null,
129
132
  err.message,
130
- page.url() ?? null,
133
+ pageUrl,
131
134
  result.screenshotKey ?? null,
132
135
  result.traceKey ?? null,
133
136
  ]
@@ -143,7 +146,7 @@ async function captureError({ page, context, err, botId, pool, minioClient, buck
143
146
  logger.error({
144
147
  ...stepInfo,
145
148
  err,
146
- url: page.url(),
149
+ url: pageUrl,
147
150
  screenshotKey: result.screenshotKey,
148
151
  traceKey: result.traceKey,
149
152
  errorId: result.errorId,
package/src/index.d.ts CHANGED
@@ -9,9 +9,9 @@ export interface MinioConfig {
9
9
  endPoint: string;
10
10
  port?: number;
11
11
  useSSL?: boolean;
12
- accessKey: string | any;
13
- secretKey: string | any;
14
- bucket?: string | any;
12
+ accessKey: string;
13
+ secretKey: string;
14
+ bucket?: string;
15
15
  }
16
16
 
17
17
  // ── Opciones de screenshot ───────────────────────────────────────────────────
@@ -26,7 +26,7 @@ export interface ScreenshotConfig {
26
26
 
27
27
  // ── Opciones de Pino ─────────────────────────────────────────────────────────
28
28
  export interface LogConfig {
29
- level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent' | any;
29
+ level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
30
30
  /** Activar pino-pretty (solo dev). Default: false */
31
31
  pretty?: boolean;
32
32
  pinoOptions?: Record<string, unknown>;
@@ -71,11 +71,17 @@ export interface RpaTools {
71
71
  watchDebugFlag: (page: any, pollMs?: number) => void;
72
72
 
73
73
  /**
74
- * Recupera credenciales de una aplicación desde la base de datos.
74
+ * Recupera las credenciales de una aplicación desde la base de datos.
75
75
  * @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
76
76
  */
77
77
  getCredentials: (nombre: string) => Promise<{ nombre: string; usuario: string; password: string; idUsuario: string; status: boolean } | null>;
78
78
 
79
+ /**
80
+ * Recupera TODAS las credenciales que coincidan con el nombre.
81
+ * @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
82
+ */
83
+ getCredentialsAll: (nombre: string) => Promise<Array<{ nombre: string; usuario: string; password: string; idUsuario: string; status: boolean }>>;
84
+
79
85
  /**
80
86
  * Actualiza el estado y las observaciones de una aplicación.
81
87
  * @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
package/src/index.js CHANGED
@@ -15,7 +15,9 @@ const { PageCapturer } = require('./capture');
15
15
  * @param {object} opts.minio - config de MinIO
16
16
  * @param {object} [opts.log] - opciones de Pino
17
17
  *
18
- * @returns {{ logger, state, step, watchDebugFlag, getCredentials, updateAppStatus, isActive, capturePage, captureError, destroy }}
18
+ * @param {object} [opts.screenshot] - opciones de captura (formato, calidad)
19
+ *
20
+ * @returns {{ logger, state, step, watchDebugFlag, getCredentials, getCredentialsAll, updateAppStatus, isActive, capturePage, captureError, destroy }}
19
21
  */
20
22
  async function createRpaTools(opts = {}) {
21
23
  const {
@@ -23,6 +25,7 @@ async function createRpaTools(opts = {}) {
23
25
  log: logCfg = {},
24
26
  db,
25
27
  minio: minioCfg,
28
+ screenshot: screenshotCfg,
26
29
  } = opts;
27
30
 
28
31
  const minio = minioCfg ?? {
@@ -44,7 +47,7 @@ async function createRpaTools(opts = {}) {
44
47
  await ensureBucket(minioClient, bucket);
45
48
  logger.info({ bucket }, 'MinIO listo');
46
49
 
47
- const watcher = new ScreenshotWatcher({ botId, pool: db, minioClient, bucket, logger });
50
+ const watcher = new ScreenshotWatcher({ botId, pool: db, minioClient, bucket, logger, screenshot: screenshotCfg });
48
51
  const credentials = new CredentialManager({ botId, pool: db });
49
52
  const capturer = new PageCapturer({ botId, minioClient, bucket, logger });
50
53
 
@@ -55,7 +58,7 @@ async function createRpaTools(opts = {}) {
55
58
  state,
56
59
  step: (name) => {
57
60
  state.currentStep = name;
58
- logger.info(name + ' - Nuevo paso');
61
+ logger.info({ step: name }, 'Nuevo paso');
59
62
  },
60
63
  /**
61
64
  * Inicia el polling que activa/desactiva screenshots según bot_debug_config.
@@ -64,6 +67,7 @@ async function createRpaTools(opts = {}) {
64
67
  */
65
68
  watchDebugFlag: (page, pollMs) => watcher.watch(page, pollMs),
66
69
  getCredentials: (nombre) => credentials.getAppCredentials(nombre),
70
+ getCredentialsAll: (nombre) => credentials.getAppCredentialsAll(nombre),
67
71
  updateAppStatus: (nombre, status, observations) => credentials.updateAppStatus(nombre, status, observations),
68
72
  /**
69
73
  * Consulta si el bot está activo en la base de datos (columna 'estado').
package/src/storage.js CHANGED
@@ -1,6 +1,15 @@
1
1
  'use strict';
2
2
  const Minio = require('minio');
3
3
  const sharp = require('sharp');
4
+ const os = require('os');
5
+
6
+ /**
7
+ * Retorna el identificador de la instancia actual (pod en K8s).
8
+ * En Kubernetes se usa process.env.HOSTNAME (nombre del pod).
9
+ */
10
+ function getInstanceId() {
11
+ return process.env.HOSTNAME || os.hostname();
12
+ }
4
13
 
5
14
  /**
6
15
  * Crea y retorna el cliente de MinIO.
@@ -49,6 +58,10 @@ async function toWebp(buffer, quality = 70) {
49
58
  * Si format='webp' convierte el buffer antes de subir.
50
59
  * Ruta generada: {botId}/{YYYY-MM-DD}/{ISO-timestamp}.{ext}
51
60
  *
61
+ * En BD guarda un arreglo JSONB en url_screenshot con la estructura:
62
+ * [{ instancia: string, url: string, timestamp: string }]
63
+ * Cada instancia (pod) actualiza su propia entrada.
64
+ *
52
65
  * @param {import('minio').Client} client
53
66
  * @param {string} bucket
54
67
  * @param {string} botId
@@ -58,10 +71,11 @@ async function toWebp(buffer, quality = 70) {
58
71
  * @param {number} [opts.quality=70] - calidad 1-100
59
72
  * @param {object} [opts.pool] - Pool de base de datos
60
73
  * @param {object} [opts.logger] - Instancia de logger
74
+ * @param {string} [opts.instancia] - ID de instancia (default: HOSTNAME)
61
75
  * @returns {Promise<string>} - object key en MinIO
62
76
  */
63
77
  async function uploadScreenshot(client, bucket, botId, buffer, opts = {}) {
64
- const { format = 'webp', quality = 70, pool, logger } = opts;
78
+ const { format = 'webp', quality = 70, pool, logger, instancia = getInstanceId() } = opts;
65
79
 
66
80
  const finalBuffer = format === 'webp'
67
81
  ? await toWebp(buffer, quality)
@@ -69,29 +83,55 @@ async function uploadScreenshot(client, bucket, botId, buffer, opts = {}) {
69
83
 
70
84
  const contentType = format === 'webp' ? 'image/webp' : 'image/jpeg';
71
85
  const ext = format === 'webp' ? 'webp' : 'jpg';
72
- const key = `${botId}.${ext}`;
86
+ const now = new Date();
87
+ const date = now.toISOString().slice(0, 10);
88
+ const ts = now.toISOString().replace(/[:.]/g, '-');
89
+ const key = `capturas/${botId}/${date}/${ts}.${ext}`;
73
90
 
74
91
  await client.putObject(bucket, key, finalBuffer, finalBuffer.length, {
75
92
  'Content-Type': contentType,
76
93
  });
77
94
 
78
- // ── 3. Registro en base de datos ─────────────────────────────────────────
95
+ // ── 3. Registro en base de datos (arreglo JSONB por instancia) ──────────
79
96
  if (pool) {
97
+ const pgClient = await pool.connect();
80
98
  try {
81
- await pool.query(
99
+ await pgClient.query('BEGIN');
100
+
101
+ const { rows } = await pgClient.query(
102
+ `SELECT url_screenshot FROM bots.tb_bots WHERE id = $1::uuid FOR UPDATE`,
103
+ [botId]
104
+ );
105
+
106
+ const actual = (rows[0]?.url_screenshot || []);
107
+ const idx = actual.findIndex(e => e.instancia === instancia);
108
+
109
+ const entry = { instancia, url: key, timestamp: now.toISOString() };
110
+
111
+ if (idx >= 0) {
112
+ actual[idx] = entry;
113
+ } else {
114
+ actual.push(entry);
115
+ }
116
+
117
+ await pgClient.query(
82
118
  `UPDATE bots.tb_bots
83
- SET url_screenshot = $1,
84
- updated_at = NOW()
85
- WHERE id = $2::uuid`,
86
- [key, botId]
119
+ SET url_screenshot = $1::jsonb,
120
+ updated_at = NOW()
121
+ WHERE id = $2::uuid`,
122
+ [JSON.stringify(actual), botId]
87
123
  );
88
-
124
+
125
+ await pgClient.query('COMMIT');
89
126
  } catch (err) {
127
+ await pgClient.query('ROLLBACK').catch(() => {});
90
128
  if (logger) {
91
- logger.error({ err, botId, key }, 'No se pudo actualizar url_screenshot en BD');
129
+ logger.error({ err, botId, key, instancia }, 'No se pudo actualizar url_screenshot en BD');
92
130
  } else {
93
131
  console.error(`[Storage] Error BD (${botId}):`, err.message);
94
132
  }
133
+ } finally {
134
+ pgClient.release();
95
135
  }
96
136
  }
97
137