@gnpdev/rpa-tools 1.1.3 → 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/README.md +6 -2
- package/package.json +8 -5
- package/src/capture.js +177 -7
- package/src/credentials.js +17 -0
- package/src/errorCapture.js +5 -2
- package/src/index.d.ts +45 -22
- package/src/index.js +37 -9
- 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.
|
|
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
|
|
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
|
|
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
|
|
24
|
-
*
|
|
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>}
|
|
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/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
|
@@ -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 ────────────────────────────────────────────────────────
|
|
@@ -9,11 +7,10 @@ export interface MinioConfig {
|
|
|
9
7
|
endPoint: string;
|
|
10
8
|
port?: number;
|
|
11
9
|
useSSL?: boolean;
|
|
12
|
-
accessKey: string
|
|
13
|
-
secretKey: string
|
|
14
|
-
bucket?: string
|
|
10
|
+
accessKey: string;
|
|
11
|
+
secretKey: string;
|
|
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,15 +20,28 @@ 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
|
-
level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent'
|
|
40
|
+
level?: 'trace' | 'debug' | 'info' | 'warn' | 'error' | 'fatal' | 'silent';
|
|
30
41
|
/** Activar pino-pretty (solo dev). Default: false */
|
|
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,13 +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
|
-
* Recupera credenciales de una aplicación desde la base de datos.
|
|
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
|
-
|
|
85
|
+
/**
|
|
86
|
+
* Recupera TODAS las credenciales que coincidan con el nombre.
|
|
87
|
+
* @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
|
|
88
|
+
*/
|
|
89
|
+
getCredentialsAll: (nombre: string) => Promise<Array<{ nombre: string; usuario: string; password: string; idUsuario: string; status: boolean }>>;
|
|
79
90
|
/**
|
|
80
91
|
* Actualiza el estado y las observaciones de una aplicación.
|
|
81
92
|
* @param nombre - Nombre de la aplicación (ej. 'Portal CRM')
|
|
@@ -83,20 +94,34 @@ export interface RpaTools {
|
|
|
83
94
|
* @param observations - Observaciones/Errores
|
|
84
95
|
*/
|
|
85
96
|
updateAppStatus: (nombre: string, status: boolean, observations: string) => Promise<boolean>;
|
|
86
|
-
|
|
87
97
|
/**
|
|
88
98
|
* Consulta si el bot está activo en la base de datos (columna 'estado').
|
|
89
99
|
* @returns {Promise<boolean>}
|
|
90
100
|
*/
|
|
91
101
|
isActive: () => Promise<boolean>;
|
|
92
|
-
|
|
93
102
|
/**
|
|
94
|
-
* Captura
|
|
103
|
+
* Captura única — sube a MinIO con key timestampeda.
|
|
95
104
|
* @param page - Instancia de Page (Playwright o Puppeteer)
|
|
96
105
|
* @param nombre - Nombre del elemento/captura
|
|
97
106
|
*/
|
|
98
107
|
capturePage: (page: any, nombre: string) => Promise<string | null>;
|
|
99
|
-
|
|
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>;
|
|
100
125
|
/**
|
|
101
126
|
* Captura screenshot, trace y registra error en base de datos.
|
|
102
127
|
*/
|
|
@@ -106,11 +131,9 @@ export interface RpaTools {
|
|
|
106
131
|
err: Error;
|
|
107
132
|
step?: string;
|
|
108
133
|
}) => Promise<{ errorId: number | null; screenshotKey: string | null; traceKey: string | null }>;
|
|
109
|
-
|
|
110
134
|
/** Detiene todos los intervalos. Llamar siempre al cerrar el bot. */
|
|
111
135
|
destroy: () => void;
|
|
112
136
|
}
|
|
113
|
-
|
|
114
137
|
// ── Export principal ─────────────────────────────────────────────────────────
|
|
115
138
|
/**
|
|
116
139
|
* Inicializa la librería rpa-tools.
|
package/src/index.js
CHANGED
|
@@ -10,12 +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
|
+
* @param {object} [opts.screenshot] - opciones de captura (formato, calidad)
|
|
18
|
+
* @param {object} [opts.redis] - cliente Redis opcional (ioredis)
|
|
17
19
|
*
|
|
18
|
-
* @returns {{ logger, state, step, watchDebugFlag, getCredentials, updateAppStatus, isActive, capturePage, captureError, destroy }}
|
|
20
|
+
* @returns {Promise<{ logger, state, step, watchDebugFlag, getCredentials, getCredentialsAll, updateAppStatus, isActive, capturePage, captureLive, setOffline, captureError, destroy }>}
|
|
19
21
|
*/
|
|
20
22
|
async function createRpaTools(opts = {}) {
|
|
21
23
|
const {
|
|
@@ -23,6 +25,8 @@ async function createRpaTools(opts = {}) {
|
|
|
23
25
|
log: logCfg = {},
|
|
24
26
|
db,
|
|
25
27
|
minio: minioCfg,
|
|
28
|
+
screenshot: screenshotCfg,
|
|
29
|
+
redis: redisClient,
|
|
26
30
|
} = opts;
|
|
27
31
|
|
|
28
32
|
const minio = minioCfg ?? {
|
|
@@ -44,9 +48,9 @@ async function createRpaTools(opts = {}) {
|
|
|
44
48
|
await ensureBucket(minioClient, bucket);
|
|
45
49
|
logger.info({ bucket }, 'MinIO listo');
|
|
46
50
|
|
|
47
|
-
const watcher = new ScreenshotWatcher({ botId, pool: db, minioClient, bucket, logger });
|
|
51
|
+
const watcher = new ScreenshotWatcher({ botId, pool: db, minioClient, bucket, logger, screenshot: screenshotCfg });
|
|
48
52
|
const credentials = new CredentialManager({ botId, pool: db });
|
|
49
|
-
const capturer = new PageCapturer({ botId, minioClient, bucket, logger });
|
|
53
|
+
const capturer = new PageCapturer({ botId, minioClient, bucket, logger, redis: redisClient });
|
|
50
54
|
|
|
51
55
|
const state = { currentStep: 'Inicio' };
|
|
52
56
|
|
|
@@ -55,7 +59,7 @@ async function createRpaTools(opts = {}) {
|
|
|
55
59
|
state,
|
|
56
60
|
step: (name) => {
|
|
57
61
|
state.currentStep = name;
|
|
58
|
-
logger.info(name
|
|
62
|
+
logger.info({ step: name }, 'Nuevo paso');
|
|
59
63
|
},
|
|
60
64
|
/**
|
|
61
65
|
* Inicia el polling que activa/desactiva screenshots según bot_debug_config.
|
|
@@ -64,6 +68,7 @@ async function createRpaTools(opts = {}) {
|
|
|
64
68
|
*/
|
|
65
69
|
watchDebugFlag: (page, pollMs) => watcher.watch(page, pollMs),
|
|
66
70
|
getCredentials: (nombre) => credentials.getAppCredentials(nombre),
|
|
71
|
+
getCredentialsAll: (nombre) => credentials.getAppCredentialsAll(nombre),
|
|
67
72
|
updateAppStatus: (nombre, status, observations) => credentials.updateAppStatus(nombre, status, observations),
|
|
68
73
|
/**
|
|
69
74
|
* Consulta si el bot está activo en la base de datos (columna 'estado').
|
|
@@ -82,14 +87,34 @@ async function createRpaTools(opts = {}) {
|
|
|
82
87
|
}
|
|
83
88
|
},
|
|
84
89
|
/**
|
|
85
|
-
* Captura
|
|
90
|
+
* Captura única — sube a MinIO con key timestampeda.
|
|
86
91
|
* @param {any} page - Instancia de Page
|
|
87
92
|
* @param {string} nombre - Nombre del elemento/captura
|
|
88
93
|
*/
|
|
89
94
|
capturePage: (page, nombre) => capturer.capture(page, nombre),
|
|
90
95
|
/**
|
|
91
|
-
* Captura
|
|
92
|
-
*
|
|
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.
|
|
93
118
|
*/
|
|
94
119
|
captureError: ({ page, context, err, step }) => captureError({
|
|
95
120
|
page,
|
|
@@ -102,7 +127,10 @@ async function createRpaTools(opts = {}) {
|
|
|
102
127
|
bucket,
|
|
103
128
|
logger
|
|
104
129
|
}),
|
|
105
|
-
destroy: () =>
|
|
130
|
+
destroy: () => {
|
|
131
|
+
watcher.destroy();
|
|
132
|
+
capturer.destroy().catch(() => {});
|
|
133
|
+
},
|
|
106
134
|
};
|
|
107
135
|
}
|
|
108
136
|
|
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
|
|