@ssolis-ti/mcp-compra-agil 1.0.3 → 2.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 +111 -23
- package/dist/api/compra-agil-client.d.ts +37 -2
- package/dist/api/compra-agil-client.d.ts.map +1 -1
- package/dist/api/compra-agil-client.js +76 -5
- package/dist/api/compra-agil-client.js.map +1 -1
- package/dist/index.js +32 -8
- package/dist/index.js.map +1 -1
- package/dist/reports/components.d.ts +59 -0
- package/dist/reports/components.d.ts.map +1 -0
- package/dist/reports/components.js +125 -0
- package/dist/reports/components.js.map +1 -0
- package/dist/reports/export.d.ts +27 -0
- package/dist/reports/export.d.ts.map +1 -0
- package/dist/reports/export.js +47 -0
- package/dist/reports/export.js.map +1 -0
- package/dist/reports/format.d.ts +24 -0
- package/dist/reports/format.d.ts.map +1 -0
- package/dist/reports/format.js +74 -0
- package/dist/reports/format.js.map +1 -0
- package/dist/reports/render.d.ts +18 -0
- package/dist/reports/render.d.ts.map +1 -0
- package/dist/reports/render.js +30 -0
- package/dist/reports/render.js.map +1 -0
- package/dist/reports/templates/radar-oportunidades.d.ts +41 -0
- package/dist/reports/templates/radar-oportunidades.d.ts.map +1 -0
- package/dist/reports/templates/radar-oportunidades.js +170 -0
- package/dist/reports/templates/radar-oportunidades.js.map +1 -0
- package/dist/reports/theme.d.ts +83 -0
- package/dist/reports/theme.d.ts.map +1 -0
- package/dist/reports/theme.js +284 -0
- package/dist/reports/theme.js.map +1 -0
- package/dist/resources/compras-template.d.ts.map +1 -1
- package/dist/resources/compras-template.js +2 -1
- package/dist/resources/compras-template.js.map +1 -1
- package/dist/resources/documentacion.d.ts.map +1 -1
- package/dist/resources/documentacion.js +5 -36
- package/dist/resources/documentacion.js.map +1 -1
- package/dist/resources/estados.d.ts.map +1 -1
- package/dist/resources/estados.js +24 -10
- package/dist/resources/estados.js.map +1 -1
- package/dist/services/monitor.js +38 -3
- package/dist/services/monitor.js.map +1 -1
- package/dist/tools/analizar-precios-mercado.d.ts +21 -0
- package/dist/tools/analizar-precios-mercado.d.ts.map +1 -0
- package/dist/tools/analizar-precios-mercado.js +217 -0
- package/dist/tools/analizar-precios-mercado.js.map +1 -0
- package/dist/tools/auditar-desiertas.d.ts.map +1 -1
- package/dist/tools/auditar-desiertas.js +66 -48
- package/dist/tools/auditar-desiertas.js.map +1 -1
- package/dist/tools/buscar-compras.d.ts.map +1 -1
- package/dist/tools/buscar-compras.js +2 -1
- package/dist/tools/buscar-compras.js.map +1 -1
- package/dist/tools/detalle-compra.d.ts.map +1 -1
- package/dist/tools/detalle-compra.js +6 -5
- package/dist/tools/detalle-compra.js.map +1 -1
- package/dist/tools/detalle-oc.js +6 -6
- package/dist/tools/detalle-oc.js.map +1 -1
- package/dist/tools/documentos.d.ts.map +1 -1
- package/dist/tools/documentos.js +75 -81
- package/dist/tools/documentos.js.map +1 -1
- package/dist/tools/estadisticas-uso.d.ts.map +1 -1
- package/dist/tools/estadisticas-uso.js +12 -2
- package/dist/tools/estadisticas-uso.js.map +1 -1
- package/dist/tools/generar-borrador.d.ts +25 -1
- package/dist/tools/generar-borrador.d.ts.map +1 -1
- package/dist/tools/generar-borrador.js +111 -82
- package/dist/tools/generar-borrador.js.map +1 -1
- package/dist/tools/generar-informe.d.ts +13 -0
- package/dist/tools/generar-informe.d.ts.map +1 -0
- package/dist/tools/generar-informe.js +111 -0
- package/dist/tools/generar-informe.js.map +1 -0
- package/dist/tools/monitorear-cambios.d.ts +32 -2
- package/dist/tools/monitorear-cambios.d.ts.map +1 -1
- package/dist/tools/monitorear-cambios.js +79 -10
- package/dist/tools/monitorear-cambios.js.map +1 -1
- package/dist/tools/radar-oportunidades.d.ts +42 -1
- package/dist/tools/radar-oportunidades.d.ts.map +1 -1
- package/dist/tools/radar-oportunidades.js +123 -103
- package/dist/tools/radar-oportunidades.js.map +1 -1
- package/dist/tools/recomendar-precio.d.ts +13 -0
- package/dist/tools/recomendar-precio.d.ts.map +1 -1
- package/dist/tools/recomendar-precio.js +53 -57
- package/dist/tools/recomendar-precio.js.map +1 -1
- package/dist/tools/verificar-oc.d.ts.map +1 -1
- package/dist/tools/verificar-oc.js +36 -8
- package/dist/tools/verificar-oc.js.map +1 -1
- package/dist/tools/verificar-ticket.d.ts +13 -0
- package/dist/tools/verificar-ticket.d.ts.map +1 -0
- package/dist/tools/verificar-ticket.js +79 -0
- package/dist/tools/verificar-ticket.js.map +1 -0
- package/dist/utils/cache.d.ts +53 -0
- package/dist/utils/cache.d.ts.map +1 -0
- package/dist/utils/cache.js +131 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/doc-search.d.ts +52 -0
- package/dist/utils/doc-search.d.ts.map +1 -0
- package/dist/utils/doc-search.js +97 -0
- package/dist/utils/doc-search.js.map +1 -0
- package/dist/utils/docs-locator.d.ts +41 -0
- package/dist/utils/docs-locator.d.ts.map +1 -0
- package/dist/utils/docs-locator.js +82 -0
- package/dist/utils/docs-locator.js.map +1 -0
- package/dist/utils/error-handler.d.ts.map +1 -1
- package/dist/utils/error-handler.js +8 -7
- package/dist/utils/error-handler.js.map +1 -1
- package/dist/utils/logger.d.ts.map +1 -1
- package/dist/utils/logger.js +9 -6
- package/dist/utils/logger.js.map +1 -1
- package/dist/utils/quotation.d.ts +71 -0
- package/dist/utils/quotation.d.ts.map +1 -0
- package/dist/utils/quotation.js +140 -0
- package/dist/utils/quotation.js.map +1 -0
- package/dist/utils/rate-limiter.d.ts +44 -2
- package/dist/utils/rate-limiter.d.ts.map +1 -1
- package/dist/utils/rate-limiter.js +125 -13
- package/dist/utils/rate-limiter.js.map +1 -1
- package/dist/utils/redact.d.ts +48 -0
- package/dist/utils/redact.d.ts.map +1 -0
- package/dist/utils/redact.js +100 -0
- package/dist/utils/redact.js.map +1 -0
- package/package.json +55 -53
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/utils/error-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;
|
|
1
|
+
{"version":3,"file":"error-handler.js","sourceRoot":"","sources":["../../src/utils/error-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAerC,MAAM,OAAO,kBAAmB,SAAQ,KAAK;IAC3B,UAAU,CAAS;IACnB,SAAS,CAAa;IACtB,iBAAiB,CAAS;IAE1C,YAAY,UAAkB,EAAE,YAAwB,EAAE;QACxD,MAAM,UAAU,GAAG,oBAAoB,CAAC,UAAU,EAAE,SAAS,CAAC,CAAC;QAC/D,KAAK,CAAC,UAAU,CAAC,CAAC;QAClB,IAAI,CAAC,IAAI,GAAG,oBAAoB,CAAC;QACjC,IAAI,CAAC,UAAU,GAAG,UAAU,CAAC;QAC7B,IAAI,CAAC,SAAS,GAAG,SAAS,CAAC;QAC3B,IAAI,CAAC,iBAAiB,GAAG,UAAU,CAAC;IACtC,CAAC;CACF;AAED,SAAS,oBAAoB,CAAC,UAAkB,EAAE,SAAqB;IACrE,6EAA6E;IAC7E,gFAAgF;IAChF,MAAM,MAAM,GAAG,SAAS,CAAC,MAAM,GAAG,CAAC;QACjC,CAAC,CAAC,wBAAwB,MAAM,CAAC,SAAS,CAAC,CAAC,CAAC,CAAC,OAAO,IAAI,EAAE,CAAC,GAAG;QAC/D,CAAC,CAAC,EAAE,CAAC;IAEP,QAAQ,UAAU,EAAE,CAAC;QACnB,KAAK,GAAG;YACN,OAAO,qLAAqL,MAAM,EAAE,CAAC;QACvM,KAAK,GAAG;YACN,OAAO,2JAA2J,MAAM,EAAE,CAAC;QAC7K,KAAK,GAAG;YACN,OAAO,gLAAgL,MAAM,EAAE,CAAC;QAClM,KAAK,GAAG;YACN,OAAO,mKAAmK,MAAM,EAAE,CAAC;QACrL,KAAK,GAAG;YACN,OAAO,sBAAsB,CAAC,MAAM,CAAC,CAAC;QACxC,KAAK,GAAG;YACN,OAAO,4IAA4I,MAAM,EAAE,CAAC;QAC9J,KAAK,GAAG;YACN,OAAO,gHAAgH,MAAM,EAAE,CAAC;QAClI;YACE,OAAO,yBAAyB,UAAU,6BAA6B,MAAM,EAAE,CAAC;IACpF,CAAC;AACH,CAAC;AAED,SAAS,sBAAsB,CAAC,MAAc;IAC5C,6EAA6E;IAC7E,4EAA4E;IAC5E,sEAAsE;IACtE,OAAO,0jBAA0jB,MAAM,EAAE,CAAC;AAC5kB,CAAC;AAED;;GAEG;AACH,MAAM,CAAC,KAAK,UAAU,iBAAiB,CAAC,QAAkB;IACxD,IAAI,QAAQ,CAAC,EAAE,EAAE,CAAC;QAChB,MAAM,IAAI,GAAG,MAAM,QAAQ,CAAC,IAAI,EAA6C,CAAC;QAC9E,IAAI,IAAI,IAAI,IAAI,CAAC,OAAO,KAAK,KAAK,EAAE,CAAC;YACnC,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,MAAM,EAAG,IAAoC,CAAC,MAAM,CAAC,CAAC;QAC9F,CAAC;QACD,OAAO,CAAC,IAAI,IAAI,OAAO,IAAI,KAAK,QAAQ,IAAI,SAAS,IAAI,IAAI,CAAC,CAAC,CAAC,CAAC,IAAI,CAAC,OAAO,CAAC,CAAC,CAAC,IAAI,CAAC;IACvF,CAAC;IAED,IAAI,SAAS,GAAe,EAAE,CAAC;IAC/B,IAAI,CAAC;QACH,MAAM,SAAS,GAAG,MAAM,QAAQ,CAAC,IAAI,EAAsB,CAAC;QAC5D,IAAI,SAAS,CAAC,MAAM,EAAE,CAAC;YACrB,SAAS,GAAG,SAAS,CAAC,MAAM,CAAC;QAC/B,CAAC;IACH,CAAC;IAAC,MAAM,CAAC;QACP,4DAA4D;IAC9D,CAAC;IAED,MAAM,IAAI,kBAAkB,CAAC,QAAQ,CAAC,MAAM,EAAE,SAAS,CAAC,CAAC;AAC3D,CAAC"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"logger.d.ts","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAqBH,wBAAgB,YAAY,CAAC,MAAM,EAAE,GAAG,GAAG,IAAI,CAE9C;AA2BD,eAAO,MAAM,MAAM;iBACJ,MAAM,SAAS,OAAO;gBACvB,MAAM,SAAS,OAAO;gBACtB,MAAM,SAAS,OAAO;iBACrB,MAAM,SAAS,OAAO;CACpC,CAAC"}
|
package/dist/utils/logger.js
CHANGED
|
@@ -5,6 +5,7 @@
|
|
|
5
5
|
* a stdout (console.log) ya que corrompe el stream JSON-RPC.
|
|
6
6
|
* Todo el logging va exclusivamente a stderr (console.error).
|
|
7
7
|
*/
|
|
8
|
+
import { redact } from './redact.js';
|
|
8
9
|
const LOG_LEVELS = {
|
|
9
10
|
debug: 0,
|
|
10
11
|
info: 1,
|
|
@@ -23,17 +24,19 @@ function log(level, message, data) {
|
|
|
23
24
|
if (LOG_LEVELS[level] < LOG_LEVELS[currentLevel])
|
|
24
25
|
return;
|
|
25
26
|
const prefix = `[${formatTimestamp()}] [${level.toUpperCase()}]`;
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
27
|
+
// Redacción en el punto de salida: cubre tanto el mensaje como los datos
|
|
28
|
+
// adjuntos, sin importar de qué ruta provengan.
|
|
29
|
+
const cuerpo = data ? `${message} ${JSON.stringify(data)}` : message;
|
|
30
|
+
const seguro = redact(cuerpo);
|
|
29
31
|
// SIEMPRE stderr, NUNCA stdout
|
|
30
|
-
console.error(
|
|
31
|
-
//
|
|
32
|
+
console.error(`${prefix} ${seguro}`);
|
|
33
|
+
// El cliente MCP recibe los logs de forma nativa — es decir, van al contexto
|
|
34
|
+
// del LLM y a la transcripción. Se envía la versión ya redactada.
|
|
32
35
|
if (mcpServer) {
|
|
33
36
|
mcpServer.sendLoggingMessage({
|
|
34
37
|
level,
|
|
35
38
|
logger: 'mcp-compra-agil',
|
|
36
|
-
data:
|
|
39
|
+
data: seguro,
|
|
37
40
|
}).catch(() => {
|
|
38
41
|
// Ignorar fallos silenciosamente para no interrumpir el flujo principal
|
|
39
42
|
});
|
package/dist/utils/logger.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"logger.js","sourceRoot":"","sources":["../../src/utils/logger.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAIrC,MAAM,UAAU,GAA6B;IAC3C,KAAK,EAAE,CAAC;IACR,IAAI,EAAE,CAAC;IACP,IAAI,EAAE,CAAC;IACP,KAAK,EAAE,CAAC;CACT,CAAC;AAEF,MAAM,YAAY,GAAc,OAAO,CAAC,GAAG,CAAC,SAAsB,IAAI,MAAM,CAAC;AAE7E,SAAS,eAAe;IACtB,OAAO,IAAI,IAAI,EAAE,CAAC,WAAW,EAAE,CAAC;AAClC,CAAC;AAED,IAAI,SAAS,GAAQ,IAAI,CAAC;AAE1B,MAAM,UAAU,YAAY,CAAC,MAAW;IACtC,SAAS,GAAG,MAAM,CAAC;AACrB,CAAC;AAED,SAAS,GAAG,CAAC,KAAe,EAAE,OAAe,EAAE,IAAc;IAC3D,IAAI,UAAU,CAAC,KAAK,CAAC,GAAG,UAAU,CAAC,YAAY,CAAC;QAAE,OAAO;IAEzD,MAAM,MAAM,GAAG,IAAI,eAAe,EAAE,MAAM,KAAK,CAAC,WAAW,EAAE,GAAG,CAAC;IACjE,yEAAyE;IACzE,gDAAgD;IAChD,MAAM,MAAM,GAAG,IAAI,CAAC,CAAC,CAAC,GAAG,OAAO,IAAI,IAAI,CAAC,SAAS,CAAC,IAAI,CAAC,EAAE,CAAC,CAAC,CAAC,OAAO,CAAC;IACrE,MAAM,MAAM,GAAG,MAAM,CAAC,MAAM,CAAC,CAAC;IAE9B,+BAA+B;IAC/B,OAAO,CAAC,KAAK,CAAC,GAAG,MAAM,IAAI,MAAM,EAAE,CAAC,CAAC;IAErC,6EAA6E;IAC7E,kEAAkE;IAClE,IAAI,SAAS,EAAE,CAAC;QACd,SAAS,CAAC,kBAAkB,CAAC;YAC3B,KAAK;YACL,MAAM,EAAE,iBAAiB;YACzB,IAAI,EAAE,MAAM;SACb,CAAC,CAAC,KAAK,CAAC,GAAG,EAAE;YACZ,wEAAwE;QAC1E,CAAC,CAAC,CAAC;IACL,CAAC;AACH,CAAC;AAED,MAAM,CAAC,MAAM,MAAM,GAAG;IACpB,KAAK,EAAE,CAAC,GAAW,EAAE,IAAc,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC;IAC/D,IAAI,EAAE,CAAC,GAAW,EAAE,IAAc,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;IAC7D,IAAI,EAAE,CAAC,GAAW,EAAE,IAAc,EAAE,EAAE,CAAC,GAAG,CAAC,MAAM,EAAE,GAAG,EAAE,IAAI,CAAC;IAC7D,KAAK,EAAE,CAAC,GAAW,EAAE,IAAc,EAAE,EAAE,CAAC,GAAG,CAAC,OAAO,EAAE,GAAG,EAAE,IAAI,CAAC;CAChE,CAAC"}
|
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilidades de cotización: análisis de precios y detección de adjudicación.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ REALIDAD DE LA API (verificado empíricamente contra el servicio real,
|
|
5
|
+
* julio 2026 — 45 procesos inspeccionados, 52 cotizaciones — y re-confirmado
|
|
6
|
+
* en septiembre 2026 con resultados idénticos):
|
|
7
|
+
*
|
|
8
|
+
* • `proveedor_seleccionado` SÍ existe, pero su valor fue **0 en el 100%**
|
|
9
|
+
* de las cotizaciones observadas. Nunca se observó un 1.
|
|
10
|
+
* • Ningún proceso traía `id_orden_compra` (siempre null).
|
|
11
|
+
* • El filtro `estado=proveedor_seleccionado` devuelve **0 resultados**.
|
|
12
|
+
* • El filtro `estado=oc_emitida` devuelve **HTTP 400** (ni siquiera es válido).
|
|
13
|
+
* • `seleccion.*` y `estado_cotizacion.*` NO existen en la respuesta.
|
|
14
|
+
* En su lugar hay `estado` (número; siempre 3 en la muestra).
|
|
15
|
+
*
|
|
16
|
+
* CONCLUSIÓN: la API no expone procesos adjudicados. Cualquier análisis
|
|
17
|
+
* basado en "el precio que ganó" está condenado a no encontrar datos.
|
|
18
|
+
* El Ejemplo 8.6 de la Guía oficial v3.0 (detectar OCs emitidas recorriendo
|
|
19
|
+
* `estado=proveedor_seleccionado`) es irrealizable por la misma razón:
|
|
20
|
+
* su primer paso devuelve una lista vacía.
|
|
21
|
+
*
|
|
22
|
+
* Lo que SÍ hay: cotizaciones reales con `precio_unitario` y `valor_neto`
|
|
23
|
+
* (17 de 45 procesos las traían). Por eso el análisis se basa en precios
|
|
24
|
+
* COTIZADOS — señal de mercado genuina — y no en precios adjudicados.
|
|
25
|
+
*/
|
|
26
|
+
import type { ProveedorCotizando } from '../api/compra-agil-client.js';
|
|
27
|
+
/**
|
|
28
|
+
* Determina si un proveedor fue el adjudicado.
|
|
29
|
+
*
|
|
30
|
+
* Se conserva porque es correcta si la API alguna vez publica adjudicaciones,
|
|
31
|
+
* pero en la práctica retorna false SIEMPRE (ver nota de cabecera). No debe
|
|
32
|
+
* usarse como única fuente de un análisis: hazlo degradar con elegancia.
|
|
33
|
+
*/
|
|
34
|
+
export declare function esGanador(prov: ProveedorCotizando): boolean;
|
|
35
|
+
/**
|
|
36
|
+
* Una cotización es admisible si el comprador no la declaró inadmisible.
|
|
37
|
+
* Útil para separar precios "sanos" de los descartados por incumplimiento
|
|
38
|
+
* (ej: "Oferta no cumple con garantía solicitada"), que distorsionan la muestra.
|
|
39
|
+
*/
|
|
40
|
+
export declare function esAdmisible(prov: ProveedorCotizando): boolean;
|
|
41
|
+
/**
|
|
42
|
+
* Extrae el precio UNITARIO de la cotización de un proveedor para un término
|
|
43
|
+
* de búsqueda dado. Retorna null si no hay un precio unitario confiable.
|
|
44
|
+
*
|
|
45
|
+
* No cae al monto total: el precio unitario y el total NO son comparables y
|
|
46
|
+
* mezclarlos corrompe cualquier estadística agregada.
|
|
47
|
+
*/
|
|
48
|
+
export declare function extraerPrecioUnitario(prov: ProveedorCotizando, keyword?: string): number | null;
|
|
49
|
+
/**
|
|
50
|
+
* Extrae el monto NETO total de la cotización de un proveedor.
|
|
51
|
+
* Útil para estadísticas de monto cotizado (distintas de precio unitario).
|
|
52
|
+
*/
|
|
53
|
+
export declare function extraerMontoNeto(prov: ProveedorCotizando): number | null;
|
|
54
|
+
export interface EstadisticasPrecio {
|
|
55
|
+
muestras: number;
|
|
56
|
+
minimo: number;
|
|
57
|
+
maximo: number;
|
|
58
|
+
promedio: number;
|
|
59
|
+
mediana: number;
|
|
60
|
+
/** Percentil 25 — referencia para posicionarse de forma competitiva. */
|
|
61
|
+
p25: number;
|
|
62
|
+
}
|
|
63
|
+
/**
|
|
64
|
+
* Calcula estadísticas de una serie de precios.
|
|
65
|
+
* Retorna null si la serie está vacía, para poder distinguir "sin datos"
|
|
66
|
+
* de "datos que dan cero".
|
|
67
|
+
*/
|
|
68
|
+
export declare function calcularEstadisticas(valores: number[]): EstadisticasPrecio | null;
|
|
69
|
+
/** Percentil por interpolación lineal sobre una serie YA ordenada. */
|
|
70
|
+
export declare function percentil(ordenados: number[], p: number): number;
|
|
71
|
+
//# sourceMappingURL=quotation.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quotation.d.ts","sourceRoot":"","sources":["../../src/utils/quotation.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;;;;;;;GAwBG;AAEH,OAAO,KAAK,EAAE,kBAAkB,EAAE,MAAM,8BAA8B,CAAC;AAEvE;;;;;;GAMG;AACH,wBAAgB,SAAS,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAmB3D;AAED;;;;GAIG;AACH,wBAAgB,WAAW,CAAC,IAAI,EAAE,kBAAkB,GAAG,OAAO,CAG7D;AAED;;;;;;GAMG;AACH,wBAAgB,qBAAqB,CACnC,IAAI,EAAE,kBAAkB,EACxB,OAAO,CAAC,EAAE,MAAM,GACf,MAAM,GAAG,IAAI,CA4Bf;AAED;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,IAAI,EAAE,kBAAkB,GAAG,MAAM,GAAG,IAAI,CAExE;AAED,MAAM,WAAW,kBAAkB;IACjC,QAAQ,EAAE,MAAM,CAAC;IACjB,MAAM,EAAE,MAAM,CAAC;IACf,MAAM,EAAE,MAAM,CAAC;IACf,QAAQ,EAAE,MAAM,CAAC;IACjB,OAAO,EAAE,MAAM,CAAC;IAChB,wEAAwE;IACxE,GAAG,EAAE,MAAM,CAAC;CACb;AAED;;;;GAIG;AACH,wBAAgB,oBAAoB,CAAC,OAAO,EAAE,MAAM,EAAE,GAAG,kBAAkB,GAAG,IAAI,CAYjF;AAED,sEAAsE;AACtE,wBAAgB,SAAS,CAAC,SAAS,EAAE,MAAM,EAAE,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM,CAQhE"}
|
|
@@ -0,0 +1,140 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Utilidades de cotización: análisis de precios y detección de adjudicación.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ REALIDAD DE LA API (verificado empíricamente contra el servicio real,
|
|
5
|
+
* julio 2026 — 45 procesos inspeccionados, 52 cotizaciones — y re-confirmado
|
|
6
|
+
* en septiembre 2026 con resultados idénticos):
|
|
7
|
+
*
|
|
8
|
+
* • `proveedor_seleccionado` SÍ existe, pero su valor fue **0 en el 100%**
|
|
9
|
+
* de las cotizaciones observadas. Nunca se observó un 1.
|
|
10
|
+
* • Ningún proceso traía `id_orden_compra` (siempre null).
|
|
11
|
+
* • El filtro `estado=proveedor_seleccionado` devuelve **0 resultados**.
|
|
12
|
+
* • El filtro `estado=oc_emitida` devuelve **HTTP 400** (ni siquiera es válido).
|
|
13
|
+
* • `seleccion.*` y `estado_cotizacion.*` NO existen en la respuesta.
|
|
14
|
+
* En su lugar hay `estado` (número; siempre 3 en la muestra).
|
|
15
|
+
*
|
|
16
|
+
* CONCLUSIÓN: la API no expone procesos adjudicados. Cualquier análisis
|
|
17
|
+
* basado en "el precio que ganó" está condenado a no encontrar datos.
|
|
18
|
+
* El Ejemplo 8.6 de la Guía oficial v3.0 (detectar OCs emitidas recorriendo
|
|
19
|
+
* `estado=proveedor_seleccionado`) es irrealizable por la misma razón:
|
|
20
|
+
* su primer paso devuelve una lista vacía.
|
|
21
|
+
*
|
|
22
|
+
* Lo que SÍ hay: cotizaciones reales con `precio_unitario` y `valor_neto`
|
|
23
|
+
* (17 de 45 procesos las traían). Por eso el análisis se basa en precios
|
|
24
|
+
* COTIZADOS — señal de mercado genuina — y no en precios adjudicados.
|
|
25
|
+
*/
|
|
26
|
+
/**
|
|
27
|
+
* Determina si un proveedor fue el adjudicado.
|
|
28
|
+
*
|
|
29
|
+
* Se conserva porque es correcta si la API alguna vez publica adjudicaciones,
|
|
30
|
+
* pero en la práctica retorna false SIEMPRE (ver nota de cabecera). No debe
|
|
31
|
+
* usarse como única fuente de un análisis: hazlo degradar con elegancia.
|
|
32
|
+
*/
|
|
33
|
+
export function esGanador(prov) {
|
|
34
|
+
// Señal 1: flag directo (la API lo entrega como número: 0 | 1)
|
|
35
|
+
if (prov.proveedor_seleccionado === true || prov.proveedor_seleccionado === 1) {
|
|
36
|
+
return true;
|
|
37
|
+
}
|
|
38
|
+
// Señal 2: objeto `seleccion` anidado — documentado pero inexistente en la
|
|
39
|
+
// respuesta real. Se mantiene por si la API lo incorpora.
|
|
40
|
+
if (prov.seleccion?.proveedor_seleccionado === true) {
|
|
41
|
+
return true;
|
|
42
|
+
}
|
|
43
|
+
// Señal 3: estado_por_comprador === '1' (observado siempre null)
|
|
44
|
+
if (prov.estado_por_comprador === '1') {
|
|
45
|
+
return true;
|
|
46
|
+
}
|
|
47
|
+
// Señal 4: motivo/criterio de selección presente implica adjudicación
|
|
48
|
+
if (prov.seleccion?.motivo_seleccion || prov.seleccion?.criterio_seleccion) {
|
|
49
|
+
return true;
|
|
50
|
+
}
|
|
51
|
+
return false;
|
|
52
|
+
}
|
|
53
|
+
/**
|
|
54
|
+
* Una cotización es admisible si el comprador no la declaró inadmisible.
|
|
55
|
+
* Útil para separar precios "sanos" de los descartados por incumplimiento
|
|
56
|
+
* (ej: "Oferta no cumple con garantía solicitada"), que distorsionan la muestra.
|
|
57
|
+
*/
|
|
58
|
+
export function esAdmisible(prov) {
|
|
59
|
+
const j = prov.justificacion_inadmisibilidad;
|
|
60
|
+
return j === null || j === undefined || String(j).trim() === '';
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Extrae el precio UNITARIO de la cotización de un proveedor para un término
|
|
64
|
+
* de búsqueda dado. Retorna null si no hay un precio unitario confiable.
|
|
65
|
+
*
|
|
66
|
+
* No cae al monto total: el precio unitario y el total NO son comparables y
|
|
67
|
+
* mezclarlos corrompe cualquier estadística agregada.
|
|
68
|
+
*/
|
|
69
|
+
export function extraerPrecioUnitario(prov, keyword) {
|
|
70
|
+
const productos = prov.productos_cotizados;
|
|
71
|
+
if (!productos || productos.length === 0) {
|
|
72
|
+
return null;
|
|
73
|
+
}
|
|
74
|
+
// Un solo producto → su precio unitario
|
|
75
|
+
if (productos.length === 1) {
|
|
76
|
+
return normalizarPrecio(productos[0].precio_unitario);
|
|
77
|
+
}
|
|
78
|
+
// Varios productos → intentar casar con la keyword
|
|
79
|
+
if (keyword) {
|
|
80
|
+
const kw = keyword.toLowerCase();
|
|
81
|
+
const matched = productos.find((p) => (p.nombre_producto || '').toLowerCase().includes(kw));
|
|
82
|
+
if (matched) {
|
|
83
|
+
return normalizarPrecio(matched.precio_unitario);
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
// Fallback: primer producto con precio unitario válido
|
|
87
|
+
for (const p of productos) {
|
|
88
|
+
const precio = normalizarPrecio(p.precio_unitario);
|
|
89
|
+
if (precio !== null)
|
|
90
|
+
return precio;
|
|
91
|
+
}
|
|
92
|
+
return null;
|
|
93
|
+
}
|
|
94
|
+
/**
|
|
95
|
+
* Extrae el monto NETO total de la cotización de un proveedor.
|
|
96
|
+
* Útil para estadísticas de monto cotizado (distintas de precio unitario).
|
|
97
|
+
*/
|
|
98
|
+
export function extraerMontoNeto(prov) {
|
|
99
|
+
return normalizarPrecio(prov.valor_neto) ?? normalizarPrecio(prov.monto_total);
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Calcula estadísticas de una serie de precios.
|
|
103
|
+
* Retorna null si la serie está vacía, para poder distinguir "sin datos"
|
|
104
|
+
* de "datos que dan cero".
|
|
105
|
+
*/
|
|
106
|
+
export function calcularEstadisticas(valores) {
|
|
107
|
+
if (!valores || valores.length === 0)
|
|
108
|
+
return null;
|
|
109
|
+
const s = [...valores].sort((a, b) => a - b);
|
|
110
|
+
const mid = Math.floor(s.length / 2);
|
|
111
|
+
return {
|
|
112
|
+
muestras: s.length,
|
|
113
|
+
minimo: s[0],
|
|
114
|
+
maximo: s[s.length - 1],
|
|
115
|
+
promedio: Math.round(s.reduce((a, b) => a + b, 0) / s.length),
|
|
116
|
+
mediana: s.length % 2 !== 0 ? s[mid] : Math.round((s[mid - 1] + s[mid]) / 2),
|
|
117
|
+
p25: percentil(s, 25),
|
|
118
|
+
};
|
|
119
|
+
}
|
|
120
|
+
/** Percentil por interpolación lineal sobre una serie YA ordenada. */
|
|
121
|
+
export function percentil(ordenados, p) {
|
|
122
|
+
if (ordenados.length === 0)
|
|
123
|
+
return 0;
|
|
124
|
+
if (ordenados.length === 1)
|
|
125
|
+
return ordenados[0];
|
|
126
|
+
const pos = (p / 100) * (ordenados.length - 1);
|
|
127
|
+
const bajo = Math.floor(pos);
|
|
128
|
+
const alto = Math.ceil(pos);
|
|
129
|
+
if (bajo === alto)
|
|
130
|
+
return ordenados[bajo];
|
|
131
|
+
return Math.round(ordenados[bajo] + (ordenados[alto] - ordenados[bajo]) * (pos - bajo));
|
|
132
|
+
}
|
|
133
|
+
function normalizarPrecio(valor) {
|
|
134
|
+
if (valor === null || valor === undefined)
|
|
135
|
+
return null;
|
|
136
|
+
if (typeof valor !== 'number' || !Number.isFinite(valor) || valor <= 0)
|
|
137
|
+
return null;
|
|
138
|
+
return valor;
|
|
139
|
+
}
|
|
140
|
+
//# sourceMappingURL=quotation.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"quotation.js","sourceRoot":"","sources":["../../src/utils/quotation.ts"],"names":[],"mappings":"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"}
|
|
@@ -5,21 +5,63 @@
|
|
|
5
5
|
* Este módulo lleva un contador local de requests para advertir
|
|
6
6
|
* proactivamente antes de alcanzar el límite.
|
|
7
7
|
*/
|
|
8
|
+
/** Archivo donde el servidor recuerda la cuota entre reinicios. */
|
|
9
|
+
export declare const RUTA_ESTADO_POR_DEFECTO: string;
|
|
8
10
|
export declare class RateLimiter {
|
|
9
11
|
private requestCount;
|
|
10
12
|
private currentDay;
|
|
11
13
|
private isLimited;
|
|
12
14
|
private limitResetTime;
|
|
13
|
-
|
|
15
|
+
/** 429 seguidos sin una consulta exitosa en medio; gradúa la espera. */
|
|
16
|
+
private consecutive429;
|
|
17
|
+
private readonly maxPerMinute;
|
|
18
|
+
private requestTimestamps;
|
|
19
|
+
/**
|
|
20
|
+
* Ruta de persistencia, o `null` para operar solo en memoria.
|
|
21
|
+
*
|
|
22
|
+
* ⚠ POR QUÉ SE PERSISTE: el contador vivía únicamente en memoria, así que
|
|
23
|
+
* cada arranque del servidor empezaba en cero. Como un servidor MCP se
|
|
24
|
+
* reinicia cada vez que el usuario reinicia su cliente, `obtener_estadisticas_uso`
|
|
25
|
+
* informaba `isLimited: false` incluso segundos después de que la API
|
|
26
|
+
* hubiera respondido 429 por cuota agotada, y el throttle preventivo
|
|
27
|
+
* disparaba peticiones contra un muro. Detectado en auditoría.
|
|
28
|
+
*/
|
|
29
|
+
private readonly statePath;
|
|
30
|
+
constructor(maxPerMinute?: number, statePath?: string | null);
|
|
31
|
+
/** Lee el estado del día en curso. Un estado de otro día se descarta. */
|
|
32
|
+
private cargarEstado;
|
|
33
|
+
private guardarEstado;
|
|
14
34
|
private getTodayUTC;
|
|
35
|
+
/**
|
|
36
|
+
* Espera (si es necesario) hasta que haya cupo dentro de la ventana de 1 minuto,
|
|
37
|
+
* de forma proactiva, ANTES de enviar la solicitud. Evita gatillar 429 por ráfagas.
|
|
38
|
+
*/
|
|
39
|
+
throttle(): Promise<void>;
|
|
15
40
|
/**
|
|
16
41
|
* Registra un request exitoso. Resetea el contador si cambió el día.
|
|
17
42
|
*/
|
|
18
43
|
recordRequest(): void;
|
|
19
44
|
/**
|
|
20
45
|
* Marca que se recibió un error 429.
|
|
46
|
+
*
|
|
47
|
+
* ⚠ POR QUÉ NO SE BLOQUEA HASTA MAÑANA: antes, un solo 429 fijaba el reset
|
|
48
|
+
* en las 00:01 UTC del día siguiente y `checkLimit()` rechazaba localmente
|
|
49
|
+
* TODA consulta posterior — el MCP quedaba inutilizable durante horas sin
|
|
50
|
+
* siquiera intentar llegar a la API. Medido en auditoría: tras un 429, la
|
|
51
|
+
* API volvió a responder con normalidad 13 minutos después. La propia guía
|
|
52
|
+
* oficial describe la cuota como un *token bucket* que "se recarga
|
|
53
|
+
* automáticamente" (glosario) y manda esperar el header `Retry-After`
|
|
54
|
+
* (§7), aunque su §4 diga que el límite es por día calendario.
|
|
55
|
+
*
|
|
56
|
+
* Ahora se espera lo que indique `Retry-After`; si no viene, se aplica una
|
|
57
|
+
* espera creciente (15 → 30 → 60 min, tope 2 h) que se reinicia con la
|
|
58
|
+
* primera consulta exitosa.
|
|
59
|
+
*
|
|
60
|
+
* @param retryAfterSeconds Valor del header `Retry-After`, si la API lo envió.
|
|
21
61
|
*/
|
|
22
|
-
markLimited(): void;
|
|
62
|
+
markLimited(retryAfterSeconds?: number): void;
|
|
63
|
+
/** Espera creciente ante 429 sucesivos, para no martillar la API. */
|
|
64
|
+
private esperaPorDefecto;
|
|
23
65
|
/**
|
|
24
66
|
* Verifica si estamos actualmente limitados.
|
|
25
67
|
*/
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/utils/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;
|
|
1
|
+
{"version":3,"file":"rate-limiter.d.ts","sourceRoot":"","sources":["../../src/utils/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAMH,mEAAmE;AACnE,eAAO,MAAM,uBAAuB,QAAwD,CAAC;AAS7F,qBAAa,WAAW;IACtB,OAAO,CAAC,YAAY,CAAK;IACzB,OAAO,CAAC,UAAU,CAAS;IAC3B,OAAO,CAAC,SAAS,CAAS;IAC1B,OAAO,CAAC,cAAc,CAAqB;IAC3C,wEAAwE;IACxE,OAAO,CAAC,cAAc,CAAK;IAG3B,OAAO,CAAC,QAAQ,CAAC,YAAY,CAAS;IACtC,OAAO,CAAC,iBAAiB,CAAgB;IAEzC;;;;;;;;;OASG;IACH,OAAO,CAAC,QAAQ,CAAC,SAAS,CAAgB;gBAE9B,YAAY,SAAK,EAAE,SAAS,GAAE,MAAM,GAAG,IAAW;IAO9D,yEAAyE;IACzE,OAAO,CAAC,YAAY;IAiBpB,OAAO,CAAC,aAAa;IAerB,OAAO,CAAC,WAAW;IAInB;;;OAGG;IACG,QAAQ,IAAI,OAAO,CAAC,IAAI,CAAC;IAmB/B;;OAEG;IACH,aAAa,IAAI,IAAI;IAoBrB;;;;;;;;;;;;;;;;;OAiBG;IACH,WAAW,CAAC,iBAAiB,CAAC,EAAE,MAAM,GAAG,IAAI;IAiB7C,qEAAqE;IACrE,OAAO,CAAC,gBAAgB;IAMxB;;OAEG;IACH,UAAU,IAAI;QAAE,OAAO,EAAE,OAAO,CAAC;QAAC,OAAO,CAAC,EAAE,MAAM,CAAA;KAAE;IA0BpD;;OAEG;IACH,QAAQ,IAAI;QAAE,aAAa,EAAE,MAAM,CAAC;QAAC,SAAS,EAAE,OAAO,CAAC;QAAC,SAAS,EAAE,MAAM,GAAG,IAAI,CAAA;KAAE;CAOpF"}
|
|
@@ -5,18 +5,98 @@
|
|
|
5
5
|
* Este módulo lleva un contador local de requests para advertir
|
|
6
6
|
* proactivamente antes de alcanzar el límite.
|
|
7
7
|
*/
|
|
8
|
+
import fs from 'fs';
|
|
9
|
+
import path from 'path';
|
|
8
10
|
import { logger } from './logger.js';
|
|
11
|
+
/** Archivo donde el servidor recuerda la cuota entre reinicios. */
|
|
12
|
+
export const RUTA_ESTADO_POR_DEFECTO = path.resolve(process.cwd(), '.rate-limit-state.json');
|
|
9
13
|
export class RateLimiter {
|
|
10
14
|
requestCount = 0;
|
|
11
15
|
currentDay;
|
|
12
16
|
isLimited = false;
|
|
13
17
|
limitResetTime = null;
|
|
14
|
-
|
|
18
|
+
/** 429 seguidos sin una consulta exitosa en medio; gradúa la espera. */
|
|
19
|
+
consecutive429 = 0;
|
|
20
|
+
// Throttle proactivo por minuto (ventana deslizante de timestamps).
|
|
21
|
+
maxPerMinute;
|
|
22
|
+
requestTimestamps = [];
|
|
23
|
+
/**
|
|
24
|
+
* Ruta de persistencia, o `null` para operar solo en memoria.
|
|
25
|
+
*
|
|
26
|
+
* ⚠ POR QUÉ SE PERSISTE: el contador vivía únicamente en memoria, así que
|
|
27
|
+
* cada arranque del servidor empezaba en cero. Como un servidor MCP se
|
|
28
|
+
* reinicia cada vez que el usuario reinicia su cliente, `obtener_estadisticas_uso`
|
|
29
|
+
* informaba `isLimited: false` incluso segundos después de que la API
|
|
30
|
+
* hubiera respondido 429 por cuota agotada, y el throttle preventivo
|
|
31
|
+
* disparaba peticiones contra un muro. Detectado en auditoría.
|
|
32
|
+
*/
|
|
33
|
+
statePath;
|
|
34
|
+
constructor(maxPerMinute = 40, statePath = null) {
|
|
15
35
|
this.currentDay = this.getTodayUTC();
|
|
36
|
+
this.maxPerMinute = maxPerMinute;
|
|
37
|
+
this.statePath = statePath;
|
|
38
|
+
this.cargarEstado();
|
|
39
|
+
}
|
|
40
|
+
/** Lee el estado del día en curso. Un estado de otro día se descarta. */
|
|
41
|
+
cargarEstado() {
|
|
42
|
+
if (!this.statePath)
|
|
43
|
+
return;
|
|
44
|
+
try {
|
|
45
|
+
if (!fs.existsSync(this.statePath))
|
|
46
|
+
return;
|
|
47
|
+
const raw = JSON.parse(fs.readFileSync(this.statePath, 'utf8'));
|
|
48
|
+
if (raw.day !== this.currentDay)
|
|
49
|
+
return;
|
|
50
|
+
this.requestCount = raw.requestCount ?? 0;
|
|
51
|
+
this.isLimited = raw.isLimited ?? false;
|
|
52
|
+
this.limitResetTime = raw.limitResetTime ? new Date(raw.limitResetTime) : null;
|
|
53
|
+
if (this.isLimited) {
|
|
54
|
+
logger.info('Rate limiter: se recuperó un estado de cuota agotada de esta misma jornada UTC.');
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
catch {
|
|
58
|
+
// Un estado ilegible no debe impedir arrancar: se sigue en memoria.
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
guardarEstado() {
|
|
62
|
+
if (!this.statePath)
|
|
63
|
+
return;
|
|
64
|
+
try {
|
|
65
|
+
const estado = {
|
|
66
|
+
day: this.currentDay,
|
|
67
|
+
requestCount: this.requestCount,
|
|
68
|
+
isLimited: this.isLimited,
|
|
69
|
+
limitResetTime: this.limitResetTime?.toISOString() ?? null,
|
|
70
|
+
};
|
|
71
|
+
fs.writeFileSync(this.statePath, JSON.stringify(estado), 'utf8');
|
|
72
|
+
}
|
|
73
|
+
catch {
|
|
74
|
+
// Persistir es una mejora, no un requisito: si el disco no deja, se sigue.
|
|
75
|
+
}
|
|
16
76
|
}
|
|
17
77
|
getTodayUTC() {
|
|
18
78
|
return new Date().toISOString().split('T')[0];
|
|
19
79
|
}
|
|
80
|
+
/**
|
|
81
|
+
* Espera (si es necesario) hasta que haya cupo dentro de la ventana de 1 minuto,
|
|
82
|
+
* de forma proactiva, ANTES de enviar la solicitud. Evita gatillar 429 por ráfagas.
|
|
83
|
+
*/
|
|
84
|
+
async throttle() {
|
|
85
|
+
// Purgar timestamps con más de 60s de antigüedad
|
|
86
|
+
const cutoff = Date.now() - 60_000;
|
|
87
|
+
this.requestTimestamps = this.requestTimestamps.filter((t) => t > cutoff);
|
|
88
|
+
if (this.requestTimestamps.length >= this.maxPerMinute) {
|
|
89
|
+
// Esperar hasta que el request más antiguo salga de la ventana
|
|
90
|
+
const oldest = this.requestTimestamps[0];
|
|
91
|
+
const waitMs = Math.max(0, oldest + 60_000 - Date.now()) + 5;
|
|
92
|
+
logger.debug(`Rate limiter: throttling ${waitMs}ms (${this.requestTimestamps.length}/${this.maxPerMinute} req/min).`);
|
|
93
|
+
await new Promise((resolve) => setTimeout(resolve, waitMs));
|
|
94
|
+
// Repurgar tras la espera
|
|
95
|
+
const cutoff2 = Date.now() - 60_000;
|
|
96
|
+
this.requestTimestamps = this.requestTimestamps.filter((t) => t > cutoff2);
|
|
97
|
+
}
|
|
98
|
+
this.requestTimestamps.push(Date.now());
|
|
99
|
+
}
|
|
20
100
|
/**
|
|
21
101
|
* Registra un request exitoso. Resetea el contador si cambió el día.
|
|
22
102
|
*/
|
|
@@ -30,19 +110,50 @@ export class RateLimiter {
|
|
|
30
110
|
logger.info('Rate limiter: contador diario reseteado (nuevo día calendario UTC).');
|
|
31
111
|
}
|
|
32
112
|
this.requestCount++;
|
|
113
|
+
// Una consulta exitosa prueba que el balde volvió a tener fichas.
|
|
114
|
+
if (this.consecutive429 > 0 || this.isLimited) {
|
|
115
|
+
this.consecutive429 = 0;
|
|
116
|
+
this.isLimited = false;
|
|
117
|
+
this.limitResetTime = null;
|
|
118
|
+
}
|
|
33
119
|
logger.debug(`Rate limiter: request #${this.requestCount} del día.`);
|
|
120
|
+
this.guardarEstado();
|
|
34
121
|
}
|
|
35
122
|
/**
|
|
36
123
|
* Marca que se recibió un error 429.
|
|
124
|
+
*
|
|
125
|
+
* ⚠ POR QUÉ NO SE BLOQUEA HASTA MAÑANA: antes, un solo 429 fijaba el reset
|
|
126
|
+
* en las 00:01 UTC del día siguiente y `checkLimit()` rechazaba localmente
|
|
127
|
+
* TODA consulta posterior — el MCP quedaba inutilizable durante horas sin
|
|
128
|
+
* siquiera intentar llegar a la API. Medido en auditoría: tras un 429, la
|
|
129
|
+
* API volvió a responder con normalidad 13 minutos después. La propia guía
|
|
130
|
+
* oficial describe la cuota como un *token bucket* que "se recarga
|
|
131
|
+
* automáticamente" (glosario) y manda esperar el header `Retry-After`
|
|
132
|
+
* (§7), aunque su §4 diga que el límite es por día calendario.
|
|
133
|
+
*
|
|
134
|
+
* Ahora se espera lo que indique `Retry-After`; si no viene, se aplica una
|
|
135
|
+
* espera creciente (15 → 30 → 60 min, tope 2 h) que se reinicia con la
|
|
136
|
+
* primera consulta exitosa.
|
|
137
|
+
*
|
|
138
|
+
* @param retryAfterSeconds Valor del header `Retry-After`, si la API lo envió.
|
|
37
139
|
*/
|
|
38
|
-
markLimited() {
|
|
140
|
+
markLimited(retryAfterSeconds) {
|
|
39
141
|
this.isLimited = true;
|
|
40
|
-
|
|
41
|
-
const
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
this.limitResetTime =
|
|
45
|
-
|
|
142
|
+
this.consecutive429++;
|
|
143
|
+
const esperaSegundos = retryAfterSeconds && retryAfterSeconds > 0
|
|
144
|
+
? retryAfterSeconds
|
|
145
|
+
: this.esperaPorDefecto();
|
|
146
|
+
this.limitResetTime = new Date(Date.now() + esperaSegundos * 1000);
|
|
147
|
+
const origen = retryAfterSeconds && retryAfterSeconds > 0 ? 'header Retry-After' : 'espera progresiva local';
|
|
148
|
+
logger.warn(`Rate limiter: 429 recibido (#${this.consecutive429} consecutivo). ` +
|
|
149
|
+
`Reintentable a partir de ${this.limitResetTime.toISOString()} (${origen}).`);
|
|
150
|
+
this.guardarEstado();
|
|
151
|
+
}
|
|
152
|
+
/** Espera creciente ante 429 sucesivos, para no martillar la API. */
|
|
153
|
+
esperaPorDefecto() {
|
|
154
|
+
const escala = [15, 30, 60, 120]; // minutos
|
|
155
|
+
const i = Math.min(this.consecutive429 - 1, escala.length - 1);
|
|
156
|
+
return escala[Math.max(0, i)] * 60;
|
|
46
157
|
}
|
|
47
158
|
/**
|
|
48
159
|
* Verifica si estamos actualmente limitados.
|
|
@@ -57,16 +168,17 @@ export class RateLimiter {
|
|
|
57
168
|
this.limitResetTime = null;
|
|
58
169
|
this.requestCount = 0;
|
|
59
170
|
this.currentDay = this.getTodayUTC();
|
|
171
|
+
this.guardarEstado();
|
|
60
172
|
return { limited: false };
|
|
61
173
|
}
|
|
62
174
|
const waitMs = this.limitResetTime
|
|
63
175
|
? this.limitResetTime.getTime() - now.getTime()
|
|
64
176
|
: 0;
|
|
65
|
-
const
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
};
|
|
177
|
+
const waitMin = Math.max(1, Math.ceil(waitMs / 60_000));
|
|
178
|
+
const resetIn = waitMin >= 60
|
|
179
|
+
? `aproximadamente ${Math.ceil(waitMin / 60)} hora(s)`
|
|
180
|
+
: `aproximadamente ${waitMin} minuto(s)`;
|
|
181
|
+
return { limited: true, resetIn };
|
|
70
182
|
}
|
|
71
183
|
/**
|
|
72
184
|
* Obtiene estadísticas del uso actual.
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/utils/rate-limiter.ts"],"names":[],"mappings":"AAAA;;;;;;GAMG;AAEH,OAAO,EAAE,MAAM,EAAE,MAAM,aAAa,CAAC;AAErC,MAAM,OAAO,WAAW;IACd,YAAY,GAAG,CAAC,CAAC;IACjB,UAAU,CAAS;IACnB,SAAS,GAAG,KAAK,CAAC;IAClB,cAAc,GAAgB,IAAI,CAAC;
|
|
1
|
+
{"version":3,"file":"rate-limiter.js","sourceRoot":"","sources":["../../src/utils/rate-limiter.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,48 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Redacción de secretos: punto único de estrangulamiento.
|
|
3
|
+
*
|
|
4
|
+
* POR QUÉ EXISTE
|
|
5
|
+
* El ticket de acceso viaja en la query string del endpoint legado
|
|
6
|
+
* (/servicios/.../OrdenCompra.json). Se verificó empíricamente que algunos
|
|
7
|
+
* errores de `fetch` incluyen la URL completa en su mensaje:
|
|
8
|
+
*
|
|
9
|
+
* TypeError: Failed to parse URL from http://host/x?ticket=SECRETO
|
|
10
|
+
*
|
|
11
|
+
* Ese texto termina en la respuesta de la tool → contexto del LLM →
|
|
12
|
+
* transcripción, logs y capturas de pantalla. Auditar caso por caso cada
|
|
13
|
+
* `String(error)` es una batalla perdida: siempre aparecerá una ruta nueva.
|
|
14
|
+
*
|
|
15
|
+
* En vez de eso, TODO string que sale del proceso pasa por `redact()`.
|
|
16
|
+
* Aunque surja un camino de error imprevisto, el secreto no se escapa.
|
|
17
|
+
*
|
|
18
|
+
* Se aplica en tres puntos: el logger, el error-handler y los catch de tools.
|
|
19
|
+
*/
|
|
20
|
+
/**
|
|
21
|
+
* Registra un valor secreto para que sea removido de toda salida.
|
|
22
|
+
* Idempotente y seguro de llamar con undefined.
|
|
23
|
+
*/
|
|
24
|
+
export declare function registrarSecreto(valor: string | undefined | null): void;
|
|
25
|
+
/** Solo para tests: limpia los secretos registrados. */
|
|
26
|
+
export declare function _resetSecretos(): void;
|
|
27
|
+
/** Cantidad de secretos registrados (diagnóstico). */
|
|
28
|
+
export declare function secretosRegistrados(): number;
|
|
29
|
+
/**
|
|
30
|
+
* Elimina cualquier secreto conocido de un texto.
|
|
31
|
+
*
|
|
32
|
+
* Defensa en dos capas:
|
|
33
|
+
* 1. Coincidencia exacta de los secretos registrados.
|
|
34
|
+
* 2. Patrón `ticket=<valor>` en query strings — cubre el caso en que el
|
|
35
|
+
* secreto no alcanzó a registrarse (ej. error durante el arranque).
|
|
36
|
+
*/
|
|
37
|
+
export declare function redact(texto: string): string;
|
|
38
|
+
/**
|
|
39
|
+
* Convierte cualquier valor lanzado en un string seguro para mostrar.
|
|
40
|
+
* Reemplaza el patrón `String(error)` usado en los catch de las tools.
|
|
41
|
+
*/
|
|
42
|
+
export declare function safeError(error: unknown): string;
|
|
43
|
+
/**
|
|
44
|
+
* Enmascara una credencial para mostrarla como referencia sin revelarla.
|
|
45
|
+
* Ej: "abcd1234-ef56-7890" → "••••7890"
|
|
46
|
+
*/
|
|
47
|
+
export declare function pista(valor: string | undefined | null): string;
|
|
48
|
+
//# sourceMappingURL=redact.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"redact.d.ts","sourceRoot":"","sources":["../../src/utils/redact.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;;GAkBG;AASH;;;GAGG;AACH,wBAAgB,gBAAgB,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,IAAI,CAWvE;AAED,wDAAwD;AACxD,wBAAgB,cAAc,IAAI,IAAI,CAErC;AAED,sDAAsD;AACtD,wBAAgB,mBAAmB,IAAI,MAAM,CAE5C;AAED;;;;;;;GAOG;AACH,wBAAgB,MAAM,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAgB5C;AAED;;;GAGG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,OAAO,GAAG,MAAM,CAUhD;AAED;;;GAGG;AACH,wBAAgB,KAAK,CAAC,KAAK,EAAE,MAAM,GAAG,SAAS,GAAG,IAAI,GAAG,MAAM,CAK9D"}
|