@gnpdev/rpa-tools 1.1.2 → 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 +6 -2
- package/package.json +3 -3
- package/src/credentials.js +17 -0
- package/src/errorCapture.js +5 -2
- package/src/index.d.ts +13 -5
- package/src/index.js +7 -3
- package/src/storage.js +50 -10
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**
|
|
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
|
-
|
|
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
|
+
"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",
|
|
@@ -21,10 +21,10 @@
|
|
|
21
21
|
"pino": "^10.3.1",
|
|
22
22
|
"pino-pretty": "^13.1.3",
|
|
23
23
|
"sharp": "^0.34.5",
|
|
24
|
-
"pg": "^8.11.0"
|
|
24
|
+
"pg": "^8.11.0",
|
|
25
|
+
"@types/pg": "^8.20.0"
|
|
25
26
|
},
|
|
26
27
|
"devDependencies": {
|
|
27
|
-
"@types/pg": "^8.20.0",
|
|
28
28
|
"@commitlint/cli": "^20.5.0",
|
|
29
29
|
"@commitlint/config-conventional": "^20.5.0",
|
|
30
30
|
"@release-it/conventional-changelog": "^10.0.6",
|
package/src/credentials.js
CHANGED
|
@@ -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
|
}
|
package/src/errorCapture.js
CHANGED
|
@@ -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
|
-
|
|
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:
|
|
149
|
+
url: pageUrl,
|
|
147
150
|
screenshotKey: result.screenshotKey,
|
|
148
151
|
traceKey: result.traceKey,
|
|
149
152
|
errorId: result.errorId,
|
package/src/index.d.ts
CHANGED
|
@@ -2,14 +2,16 @@ import type { Pool } from 'pg';
|
|
|
2
2
|
import type { Logger } from 'pino';
|
|
3
3
|
import type { Page } from 'playwright';
|
|
4
4
|
|
|
5
|
+
export type { Logger };
|
|
6
|
+
|
|
5
7
|
// ── Opciones de MinIO ────────────────────────────────────────────────────────
|
|
6
8
|
export interface MinioConfig {
|
|
7
9
|
endPoint: string;
|
|
8
10
|
port?: number;
|
|
9
11
|
useSSL?: boolean;
|
|
10
|
-
accessKey: string
|
|
11
|
-
secretKey: string
|
|
12
|
-
bucket?: string
|
|
12
|
+
accessKey: string;
|
|
13
|
+
secretKey: string;
|
|
14
|
+
bucket?: string;
|
|
13
15
|
}
|
|
14
16
|
|
|
15
17
|
// ── Opciones de screenshot ───────────────────────────────────────────────────
|
|
@@ -24,7 +26,7 @@ export interface ScreenshotConfig {
|
|
|
24
26
|
|
|
25
27
|
// ── Opciones de Pino ─────────────────────────────────────────────────────────
|
|
26
28
|
export interface LogConfig {
|
|
27
|
-
level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent'
|
|
29
|
+
level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
|
|
28
30
|
/** Activar pino-pretty (solo dev). Default: false */
|
|
29
31
|
pretty?: boolean;
|
|
30
32
|
pinoOptions?: Record<string, unknown>;
|
|
@@ -69,11 +71,17 @@ export interface RpaTools {
|
|
|
69
71
|
watchDebugFlag: (page: any, pollMs?: number) => void;
|
|
70
72
|
|
|
71
73
|
/**
|
|
72
|
-
* Recupera credenciales de una aplicación desde la base de datos.
|
|
74
|
+
* Recupera las credenciales de una aplicación desde la base de datos.
|
|
73
75
|
* @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
|
|
74
76
|
*/
|
|
75
77
|
getCredentials: (nombre: string) => Promise<{ nombre: string; usuario: string; password: string; idUsuario: string; status: boolean } | null>;
|
|
76
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
|
+
|
|
77
85
|
/**
|
|
78
86
|
* Actualiza el estado y las observaciones de una aplicación.
|
|
79
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
|
-
* @
|
|
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
|
|
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
|
|
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
|
|
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
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
[
|
|
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
|
|