@ssolis-ti/mcp-compra-agil 2.1.0 → 2.4.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 +31 -11
- package/dist/api/compra-agil-client.d.ts +35 -1
- package/dist/api/compra-agil-client.d.ts.map +1 -1
- package/dist/api/compra-agil-client.js +69 -4
- package/dist/api/compra-agil-client.js.map +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/reports/templates/radar-oportunidades.d.ts +1 -1
- package/dist/reports/templates/radar-oportunidades.js +2 -2
- package/dist/reports/templates/radar-oportunidades.js.map +1 -1
- package/dist/resources/glosario.d.ts.map +1 -1
- package/dist/resources/glosario.js +6 -2
- package/dist/resources/glosario.js.map +1 -1
- package/dist/services/monitor.js +1 -1
- package/dist/services/monitor.js.map +1 -1
- package/dist/tools/analizar-precios-mercado.js +2 -2
- package/dist/tools/auditar-desiertas.js +2 -2
- package/dist/tools/detalle-oc.d.ts.map +1 -1
- package/dist/tools/detalle-oc.js +2 -3
- package/dist/tools/detalle-oc.js.map +1 -1
- package/dist/tools/documentos.d.ts.map +1 -1
- package/dist/tools/documentos.js +68 -37
- 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 +96 -78
- package/dist/tools/generar-borrador.js.map +1 -1
- package/dist/tools/radar-oportunidades.d.ts +4 -1
- package/dist/tools/radar-oportunidades.d.ts.map +1 -1
- package/dist/tools/radar-oportunidades.js +24 -2
- package/dist/tools/radar-oportunidades.js.map +1 -1
- package/dist/tools/verificar-oc.d.ts.map +1 -1
- package/dist/tools/verificar-oc.js +30 -4
- package/dist/tools/verificar-oc.js.map +1 -1
- 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/error-handler.d.ts.map +1 -1
- package/dist/utils/error-handler.js +4 -6
- package/dist/utils/error-handler.js.map +1 -1
- package/dist/utils/rate-limiter.d.ts +37 -2
- package/dist/utils/rate-limiter.d.ts.map +1 -1
- package/dist/utils/rate-limiter.js +101 -13
- package/dist/utils/rate-limiter.js.map +1 -1
- package/package.json +1 -1
|
@@ -4,7 +4,8 @@ import { logger } from '../utils/logger.js';
|
|
|
4
4
|
import { safeError } from '../utils/redact.js';
|
|
5
5
|
const TOOL_NAME = 'radar_oportunidades_calientes';
|
|
6
6
|
const TOOL_DESCRIPTION = `Escanea, califica y clasifica de forma priorizada los procesos de Compra Ágil activos (publicados).
|
|
7
|
-
Utiliza una fórmula ponderada (Hot Score) basada en la falta de oferentes, el presupuesto disponible
|
|
7
|
+
Utiliza una fórmula ponderada (Hot Score) basada en la falta de oferentes, el presupuesto disponible, las horas restantes de cierre y si el proceso va en segundo llamado, para destacar los llamados más convenientes y fáciles de ganar.
|
|
8
|
+
Cada resultado incluye "llamado" (1 = primero, 2 = segundo) y el desglose de factores que explican su puntuación.`;
|
|
8
9
|
const inputSchema = {
|
|
9
10
|
region: z.string().optional().describe('Código de la región para filtrar (1-16). Ej: "13" para Metropolitana.'),
|
|
10
11
|
q: z.string().optional().describe('Término de búsqueda opcional para acotar a un rubro o producto específico (ej: "licencias").'),
|
|
@@ -15,7 +16,8 @@ const inputSchema = {
|
|
|
15
16
|
/**
|
|
16
17
|
* Calcula el "Hot Score" de una oportunidad de forma pura (sin efectos secundarios).
|
|
17
18
|
* Retorna null si el proceso debe omitirse (ya cerrado o bajo el presupuesto mínimo).
|
|
18
|
-
* Ponderación: baja competencia (máx 50) + urgencia (máx 30) + presupuesto (máx 20)
|
|
19
|
+
* Ponderación: baja competencia (máx 50) + urgencia (máx 30) + presupuesto (máx 20)
|
|
20
|
+
* + facilidad (máx 5) + segundo llamado (máx 10). Máximo teórico: 115 pts.
|
|
19
21
|
*/
|
|
20
22
|
export function evaluarOportunidad(item, now, minBudget = 0) {
|
|
21
23
|
const budget = item.montos?.monto_disponible_clp || item.montos?.monto_disponible || 0;
|
|
@@ -80,6 +82,25 @@ export function evaluarOportunidad(item, now, minBudget = 0) {
|
|
|
80
82
|
score += 5;
|
|
81
83
|
factors.push('Sin documentos adjuntos (postulación rápida sin leer bases) (+5 pts)');
|
|
82
84
|
}
|
|
85
|
+
// Factor 5: Segundo Llamado — Máx 10 pts
|
|
86
|
+
//
|
|
87
|
+
// Un segundo llamado significa que el primero no logró adjudicar. Vale
|
|
88
|
+
// señalarlo por dos razones: el comprador vuelve con más urgencia, y existe
|
|
89
|
+
// un motivo de fracaso concreto que conviene averiguar —si sabes cumplir lo
|
|
90
|
+
// que otros no pudieron, es tu ventaja; si el presupuesto era corto, es una
|
|
91
|
+
// pérdida de tiempo—.
|
|
92
|
+
//
|
|
93
|
+
// ⚠ Lo que NO implica es menos competencia, aunque suene intuitivo. Medido
|
|
94
|
+
// contra la API real: de 200 procesos activos, el único en segundo llamado
|
|
95
|
+
// tenía 11 ofertas. Un primer llamado puede caerse porque todas las ofertas
|
|
96
|
+
// fueron inadmisibles por papeleo, y entonces el segundo atrae a los mismos
|
|
97
|
+
// interesados y más. Por eso el factor puntúa moderado y la competencia
|
|
98
|
+
// real la sigue reportando el Factor 1, que es quien la mide de verdad.
|
|
99
|
+
const llamado = item.convocatoria?.estado_convocatoria ?? 1;
|
|
100
|
+
if (llamado === 2) {
|
|
101
|
+
score += 10;
|
|
102
|
+
factors.push('Segundo llamado: el primero no logró adjudicar, así que el comprador vuelve con urgencia — averigua por qué falló antes de cotizar, ahí está la ventaja o la trampa (+10 pts)');
|
|
103
|
+
}
|
|
83
104
|
return {
|
|
84
105
|
codigo: item.codigo,
|
|
85
106
|
nombre: item.nombre,
|
|
@@ -89,6 +110,7 @@ export function evaluarOportunidad(item, now, minBudget = 0) {
|
|
|
89
110
|
ofertas_recibidas: bids,
|
|
90
111
|
horas_restantes: Math.round(hoursLeft * 10) / 10,
|
|
91
112
|
fecha_cierre: item.fechas?.fecha_cierre,
|
|
113
|
+
llamado,
|
|
92
114
|
puntuacion_caliente: score,
|
|
93
115
|
factores_calificacion: factors,
|
|
94
116
|
};
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"radar-oportunidades.js","sourceRoot":"","sources":["../../src/tools/radar-oportunidades.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,SAAS,GAAG,+BAA+B,CAAC;AAElD,MAAM,gBAAgB,GAAG
|
|
1
|
+
{"version":3,"file":"radar-oportunidades.js","sourceRoot":"","sources":["../../src/tools/radar-oportunidades.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verificar-oc.d.ts","sourceRoot":"","sources":["../../src/tools/verificar-oc.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAkBhE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,
|
|
1
|
+
{"version":3,"file":"verificar-oc.d.ts","sourceRoot":"","sources":["../../src/tools/verificar-oc.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAGH,OAAO,EAAE,SAAS,EAAE,MAAM,yCAAyC,CAAC;AACpE,OAAO,EAAE,gBAAgB,EAAE,MAAM,8BAA8B,CAAC;AAkBhE,wBAAgB,mBAAmB,CAAC,MAAM,EAAE,SAAS,EAAE,MAAM,EAAE,gBAAgB,GAAG,IAAI,CA0GrF"}
|
|
@@ -13,9 +13,9 @@ import { logger } from '../utils/logger.js';
|
|
|
13
13
|
import { esGanador } from '../utils/quotation.js';
|
|
14
14
|
import { safeError } from '../utils/redact.js';
|
|
15
15
|
const TOOL_NAME = 'verificar_orden_compra';
|
|
16
|
-
const TOOL_DESCRIPTION = `
|
|
17
|
-
⚠ LIMITACIÓN VERIFICADA
|
|
18
|
-
Un
|
|
16
|
+
const TOOL_DESCRIPTION = `Informa si una Compra Ágil tiene Orden de Compra emitida, leyendo id_orden_compra del detalle.
|
|
17
|
+
⚠ LIMITACIÓN VERIFICADA (julio 2026, re-confirmada en septiembre): en 45 procesos inspeccionados NINGUNO traía id_orden_compra, y el filtro "proveedor_seleccionado" devuelve 0 resultados. La API de Compra Ágil no publica adjudicaciones. Por eso esta herramienta NO consulta la API por su cuenta —sería gastar cuota para responder "no puedo saberlo"—: reutiliza el detalle si ya se pidió con "obtener_detalle_compra", y en cualquier caso indica cómo confirmarlo en la ficha pública.
|
|
18
|
+
Un "sin OC" NO prueba que la OC no exista: significa que la API no la expone.`;
|
|
19
19
|
const inputSchema = {
|
|
20
20
|
codigo: z.string().describe('Código único de la Compra Ágil a verificar. Formato: XXXXXX-YYY-COTXX. Ej: "1057539-228-COT26".'),
|
|
21
21
|
};
|
|
@@ -25,7 +25,33 @@ export function registerVerificarOC(server, client) {
|
|
|
25
25
|
inputSchema: inputSchema,
|
|
26
26
|
}, async (args) => {
|
|
27
27
|
try {
|
|
28
|
-
|
|
28
|
+
// ⚠ NO se consulta la API si el detalle no está ya en caché.
|
|
29
|
+
// Esta herramienta gastaba una consulta de cuota para responder algo
|
|
30
|
+
// que la API estructuralmente no publica: en 45 procesos inspeccionados
|
|
31
|
+
// más las verificaciones de septiembre 2026, NINGUNO trajo
|
|
32
|
+
// id_orden_compra. Pagar cuota por un "no puedo saberlo" garantizado
|
|
33
|
+
// es el peor negocio posible cuando el balde de tokens es escaso.
|
|
34
|
+
//
|
|
35
|
+
// Si el detalle ya se pagó antes (flujo natural: obtener_detalle_compra
|
|
36
|
+
// y luego preguntar por la OC), se aprovecha y la respuesta va completa
|
|
37
|
+
// sin costo. Si no, se responde igual con la explicación y la ficha.
|
|
38
|
+
const detalle = client.detalleEnCache(args.codigo);
|
|
39
|
+
if (!detalle) {
|
|
40
|
+
return {
|
|
41
|
+
content: [{
|
|
42
|
+
type: 'text',
|
|
43
|
+
text: JSON.stringify({
|
|
44
|
+
codigo: args.codigo,
|
|
45
|
+
verificacion_oc: {
|
|
46
|
+
tiene_orden_compra: null,
|
|
47
|
+
motivo: 'No se consultó la API a propósito, para no gastar cuota en una respuesta que se conoce de antemano: la API de Compra Ágil no publica adjudicaciones ni órdenes de compra (0 de 45 procesos inspeccionados traían id_orden_compra, re-verificado en septiembre de 2026).',
|
|
48
|
+
como_confirmarlo: `Abre la ficha pública del proceso, que sí muestra el estado real: https://buscador.mercadopublico.cl/ficha?code=${args.codigo}`,
|
|
49
|
+
si_quieres_los_datos_del_proceso: 'Llama primero a "obtener_detalle_compra"; después esta herramienta reutiliza ese detalle sin costo adicional y te informa lo que la API sí trae.',
|
|
50
|
+
},
|
|
51
|
+
}, null, 2),
|
|
52
|
+
}],
|
|
53
|
+
};
|
|
54
|
+
}
|
|
29
55
|
const idOrdenCompra = detalle.id_orden_compra ?? detalle.orden_compra?.id_orden_compra ?? null;
|
|
30
56
|
const idOc = detalle.orden_compra?.id_oc ?? null;
|
|
31
57
|
const tieneOC = idOrdenCompra !== null && idOrdenCompra !== undefined;
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"verificar-oc.js","sourceRoot":"","sources":["../../src/tools/verificar-oc.ts"],"names":[],"mappings":"AAAA;;;;;;;;GAQG;AAEH,OAAO,EAAE,CAAC,EAAE,MAAM,KAAK,CAAC;AAGxB,OAAO,EAAE,kBAAkB,EAAE,MAAM,2BAA2B,CAAC;AAC/D,OAAO,EAAE,MAAM,EAAE,MAAM,oBAAoB,CAAC;AAC5C,OAAO,EAAE,SAAS,EAAE,MAAM,uBAAuB,CAAC;AAClD,OAAO,EAAE,SAAS,EAAE,MAAM,oBAAoB,CAAC;AAE/C,MAAM,SAAS,GAAG,wBAAwB,CAAC;AAE3C,MAAM,gBAAgB,GAAG;;
|
|
1
|
+
{"version":3,"file":"verificar-oc.js","sourceRoot":"","sources":["../../src/tools/verificar-oc.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caché de respuestas de la API, con vencimiento y persistencia en disco.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ POR QUÉ EXISTE: la cuota del ticket se agotaba en minutos. Medido en
|
|
5
|
+
* auditoría, evaluar UNA oportunidad con el flujo natural (radar → detalle →
|
|
6
|
+
* analizar_precios → generar_borrador → auditar_desiertas) costaba ~29
|
|
7
|
+
* consultas, porque las tres herramientas de análisis repiten exactamente la
|
|
8
|
+
* misma pareja de llamadas —`buscar({q, estado:'desierta'})` y luego
|
|
9
|
+
* `detalle()` sobre los primeros resultados— y no existía ninguna caché: el
|
|
10
|
+
* mismo histórico se descargaba hasta tres veces en cuestión de minutos.
|
|
11
|
+
*
|
|
12
|
+
* Un proceso ya declarado desierto es inmutable, así que reutilizarlo es
|
|
13
|
+
* gratis en exactitud y enorme en cuota.
|
|
14
|
+
*
|
|
15
|
+
* 🔒 El ticket NUNCA entra aquí: se excluye al construir la clave (el endpoint
|
|
16
|
+
* heredado de Órdenes de Compra lo lleva en el query string) y solo se
|
|
17
|
+
* guarda el cuerpo de la respuesta, que es información pública.
|
|
18
|
+
*/
|
|
19
|
+
export interface OpcionesCache {
|
|
20
|
+
/** Ruta del archivo de persistencia, o `null` para vivir solo en memoria. */
|
|
21
|
+
rutaEstado?: string | null;
|
|
22
|
+
/** Máximo de entradas retenidas; al excederlo se descartan las más antiguas. */
|
|
23
|
+
maxEntradas?: number;
|
|
24
|
+
}
|
|
25
|
+
export declare class ResponseCache {
|
|
26
|
+
private entradas;
|
|
27
|
+
private readonly rutaEstado;
|
|
28
|
+
private readonly maxEntradas;
|
|
29
|
+
private aciertos;
|
|
30
|
+
private fallos;
|
|
31
|
+
constructor(opciones?: OpcionesCache);
|
|
32
|
+
/**
|
|
33
|
+
* Clave estable para una consulta: mismo endpoint y mismos parámetros
|
|
34
|
+
* producen la misma clave sin importar el orden en que se escribieron.
|
|
35
|
+
*/
|
|
36
|
+
static clave(path: string, params?: Record<string, unknown>): string;
|
|
37
|
+
/** Devuelve el valor vigente, o `undefined` si no está o ya venció. */
|
|
38
|
+
obtener<T>(clave: string): T | undefined;
|
|
39
|
+
guardar(clave: string, valor: unknown, ttlSegundos: number): void;
|
|
40
|
+
/** Elimina lo vencido y, si aún sobra, lo más antiguo. */
|
|
41
|
+
private purgar;
|
|
42
|
+
estadisticas(): {
|
|
43
|
+
entradas: number;
|
|
44
|
+
aciertos: number;
|
|
45
|
+
fallos: number;
|
|
46
|
+
ahorroPorcentaje: number;
|
|
47
|
+
};
|
|
48
|
+
/** Vacía la caché (memoria y disco). */
|
|
49
|
+
limpiar(): void;
|
|
50
|
+
private cargar;
|
|
51
|
+
private persistir;
|
|
52
|
+
}
|
|
53
|
+
//# sourceMappingURL=cache.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.d.ts","sourceRoot":"","sources":["../../src/utils/cache.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;;GAiBG;AAcH,MAAM,WAAW,aAAa;IAC5B,6EAA6E;IAC7E,UAAU,CAAC,EAAE,MAAM,GAAG,IAAI,CAAC;IAC3B,gFAAgF;IAChF,WAAW,CAAC,EAAE,MAAM,CAAC;CACtB;AAED,qBAAa,aAAa;IACxB,OAAO,CAAC,QAAQ,CAA8B;IAC9C,OAAO,CAAC,QAAQ,CAAC,UAAU,CAAgB;IAC3C,OAAO,CAAC,QAAQ,CAAC,WAAW,CAAS;IACrC,OAAO,CAAC,QAAQ,CAAK;IACrB,OAAO,CAAC,MAAM,CAAK;gBAEP,QAAQ,GAAE,aAAkB;IAMxC;;;OAGG;IACH,MAAM,CAAC,KAAK,CAAC,IAAI,EAAE,MAAM,EAAE,MAAM,CAAC,EAAE,MAAM,CAAC,MAAM,EAAE,OAAO,CAAC,GAAG,MAAM;IASpE,uEAAuE;IACvE,OAAO,CAAC,CAAC,EAAE,KAAK,EAAE,MAAM,GAAG,CAAC,GAAG,SAAS;IAkBxC,OAAO,CAAC,KAAK,EAAE,MAAM,EAAE,KAAK,EAAE,OAAO,EAAE,WAAW,EAAE,MAAM,GAAG,IAAI;IAOjE,0DAA0D;IAC1D,OAAO,CAAC,MAAM;IAYd,YAAY,IAAI;QAAE,QAAQ,EAAE,MAAM,CAAC;QAAC,QAAQ,EAAE,MAAM,CAAC;QAAC,MAAM,EAAE,MAAM,CAAC;QAAC,gBAAgB,EAAE,MAAM,CAAA;KAAE;IAUhG,wCAAwC;IACxC,OAAO,IAAI,IAAI;IAKf,OAAO,CAAC,MAAM;IAmBd,OAAO,CAAC,SAAS;CAQlB"}
|
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Caché de respuestas de la API, con vencimiento y persistencia en disco.
|
|
3
|
+
*
|
|
4
|
+
* ⚠ POR QUÉ EXISTE: la cuota del ticket se agotaba en minutos. Medido en
|
|
5
|
+
* auditoría, evaluar UNA oportunidad con el flujo natural (radar → detalle →
|
|
6
|
+
* analizar_precios → generar_borrador → auditar_desiertas) costaba ~29
|
|
7
|
+
* consultas, porque las tres herramientas de análisis repiten exactamente la
|
|
8
|
+
* misma pareja de llamadas —`buscar({q, estado:'desierta'})` y luego
|
|
9
|
+
* `detalle()` sobre los primeros resultados— y no existía ninguna caché: el
|
|
10
|
+
* mismo histórico se descargaba hasta tres veces en cuestión de minutos.
|
|
11
|
+
*
|
|
12
|
+
* Un proceso ya declarado desierto es inmutable, así que reutilizarlo es
|
|
13
|
+
* gratis en exactitud y enorme en cuota.
|
|
14
|
+
*
|
|
15
|
+
* 🔒 El ticket NUNCA entra aquí: se excluye al construir la clave (el endpoint
|
|
16
|
+
* heredado de Órdenes de Compra lo lleva en el query string) y solo se
|
|
17
|
+
* guarda el cuerpo de la respuesta, que es información pública.
|
|
18
|
+
*/
|
|
19
|
+
import fs from 'fs';
|
|
20
|
+
import { logger } from './logger.js';
|
|
21
|
+
/** Parámetros que jamás deben formar parte de la clave ni tocar el disco. */
|
|
22
|
+
const PARAMS_SECRETOS = new Set(['ticket']);
|
|
23
|
+
export class ResponseCache {
|
|
24
|
+
entradas = new Map();
|
|
25
|
+
rutaEstado;
|
|
26
|
+
maxEntradas;
|
|
27
|
+
aciertos = 0;
|
|
28
|
+
fallos = 0;
|
|
29
|
+
constructor(opciones = {}) {
|
|
30
|
+
this.rutaEstado = opciones.rutaEstado ?? null;
|
|
31
|
+
this.maxEntradas = opciones.maxEntradas ?? 500;
|
|
32
|
+
this.cargar();
|
|
33
|
+
}
|
|
34
|
+
/**
|
|
35
|
+
* Clave estable para una consulta: mismo endpoint y mismos parámetros
|
|
36
|
+
* producen la misma clave sin importar el orden en que se escribieron.
|
|
37
|
+
*/
|
|
38
|
+
static clave(path, params) {
|
|
39
|
+
const limpios = Object.entries(params ?? {})
|
|
40
|
+
.filter(([k, v]) => !PARAMS_SECRETOS.has(k.toLowerCase()) && v !== undefined && v !== null && v !== '')
|
|
41
|
+
.sort(([a], [b]) => a.localeCompare(b))
|
|
42
|
+
.map(([k, v]) => `${k}=${String(v)}`)
|
|
43
|
+
.join('&');
|
|
44
|
+
return limpios ? `${path}?${limpios}` : path;
|
|
45
|
+
}
|
|
46
|
+
/** Devuelve el valor vigente, o `undefined` si no está o ya venció. */
|
|
47
|
+
obtener(clave) {
|
|
48
|
+
const e = this.entradas.get(clave);
|
|
49
|
+
if (!e) {
|
|
50
|
+
this.fallos++;
|
|
51
|
+
return undefined;
|
|
52
|
+
}
|
|
53
|
+
if (Date.now() >= e.expira) {
|
|
54
|
+
this.entradas.delete(clave);
|
|
55
|
+
this.fallos++;
|
|
56
|
+
return undefined;
|
|
57
|
+
}
|
|
58
|
+
// Renovar posición para que la purga descarte lo realmente frío.
|
|
59
|
+
this.entradas.delete(clave);
|
|
60
|
+
this.entradas.set(clave, e);
|
|
61
|
+
this.aciertos++;
|
|
62
|
+
return e.valor;
|
|
63
|
+
}
|
|
64
|
+
guardar(clave, valor, ttlSegundos) {
|
|
65
|
+
if (ttlSegundos <= 0)
|
|
66
|
+
return;
|
|
67
|
+
this.entradas.set(clave, { expira: Date.now() + ttlSegundos * 1000, valor });
|
|
68
|
+
this.purgar();
|
|
69
|
+
this.persistir();
|
|
70
|
+
}
|
|
71
|
+
/** Elimina lo vencido y, si aún sobra, lo más antiguo. */
|
|
72
|
+
purgar() {
|
|
73
|
+
const ahora = Date.now();
|
|
74
|
+
for (const [k, e] of this.entradas) {
|
|
75
|
+
if (ahora >= e.expira)
|
|
76
|
+
this.entradas.delete(k);
|
|
77
|
+
}
|
|
78
|
+
while (this.entradas.size > this.maxEntradas) {
|
|
79
|
+
const primera = this.entradas.keys().next();
|
|
80
|
+
if (primera.done)
|
|
81
|
+
break;
|
|
82
|
+
this.entradas.delete(primera.value);
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
estadisticas() {
|
|
86
|
+
const total = this.aciertos + this.fallos;
|
|
87
|
+
return {
|
|
88
|
+
entradas: this.entradas.size,
|
|
89
|
+
aciertos: this.aciertos,
|
|
90
|
+
fallos: this.fallos,
|
|
91
|
+
ahorroPorcentaje: total === 0 ? 0 : Math.round((this.aciertos / total) * 100),
|
|
92
|
+
};
|
|
93
|
+
}
|
|
94
|
+
/** Vacía la caché (memoria y disco). */
|
|
95
|
+
limpiar() {
|
|
96
|
+
this.entradas.clear();
|
|
97
|
+
this.persistir();
|
|
98
|
+
}
|
|
99
|
+
cargar() {
|
|
100
|
+
if (!this.rutaEstado)
|
|
101
|
+
return;
|
|
102
|
+
try {
|
|
103
|
+
if (!fs.existsSync(this.rutaEstado))
|
|
104
|
+
return;
|
|
105
|
+
const crudo = JSON.parse(fs.readFileSync(this.rutaEstado, 'utf8'));
|
|
106
|
+
const ahora = Date.now();
|
|
107
|
+
for (const [k, e] of Object.entries(crudo)) {
|
|
108
|
+
if (e && typeof e.expira === 'number' && ahora < e.expira) {
|
|
109
|
+
this.entradas.set(k, e);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
if (this.entradas.size > 0) {
|
|
113
|
+
logger.debug(`Caché: ${this.entradas.size} respuesta(s) vigente(s) recuperada(s) del disco.`);
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
catch {
|
|
117
|
+
// Una caché ilegible se descarta: es una optimización, no un requisito.
|
|
118
|
+
}
|
|
119
|
+
}
|
|
120
|
+
persistir() {
|
|
121
|
+
if (!this.rutaEstado)
|
|
122
|
+
return;
|
|
123
|
+
try {
|
|
124
|
+
fs.writeFileSync(this.rutaEstado, JSON.stringify(Object.fromEntries(this.entradas)), 'utf8');
|
|
125
|
+
}
|
|
126
|
+
catch {
|
|
127
|
+
// Si el disco no deja escribir, se sigue con la caché en memoria.
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
}
|
|
131
|
+
//# sourceMappingURL=cache.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"cache.js","sourceRoot":"","sources":["../../src/utils/cache.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Búsqueda de texto dentro de los documentos locales (manuales, guías, normativa).
|
|
3
|
+
*
|
|
4
|
+
* ⚠ POR QUÉ EXISTE ESTE MÓDULO: la primera implementación buscaba la consulta
|
|
5
|
+
* completa como subcadena literal (`texto.includes(query)`). Con una palabra
|
|
6
|
+
* suelta funcionaba, pero cualquier pregunta en lenguaje natural —que es
|
|
7
|
+
* justo lo que envía un LLM— no coincidía nunca. Peor aún, el fallo era
|
|
8
|
+
* silencioso y seguro de sí mismo: "no se encontraron coincidencias en
|
|
9
|
+
* ninguno de los 10 documentos" ante la pregunta "¿qué multas me pueden
|
|
10
|
+
* aplicar?", existiendo un PDF entero dedicado a multas y sanciones. El
|
|
11
|
+
* modelo repetía ese "no hay información" al usuario como si fuera cierto.
|
|
12
|
+
*
|
|
13
|
+
* Ahora la consulta se descompone en términos, se ignoran las palabras
|
|
14
|
+
* vacías y se puntúa cada línea por cuántos términos distintos contiene.
|
|
15
|
+
* Además se normalizan los acentos, porque en documentos en español
|
|
16
|
+
* "sanción" y "sancion" deben encontrarse mutuamente.
|
|
17
|
+
*/
|
|
18
|
+
/** Minúsculas y sin acentos, para que "sanción" y "sancion" se encuentren. */
|
|
19
|
+
export declare function normalizar(texto: string): string;
|
|
20
|
+
/**
|
|
21
|
+
* Descompone la consulta en términos buscables: normalizados, sin palabras
|
|
22
|
+
* vacías y de al menos 3 caracteres.
|
|
23
|
+
*
|
|
24
|
+
* Si al filtrar no queda ningún término (consulta hecha solo de palabras
|
|
25
|
+
* vacías, ej. "qué es esto"), se devuelven todos los de 3+ caracteres antes
|
|
26
|
+
* que dejar al usuario sin búsqueda.
|
|
27
|
+
*/
|
|
28
|
+
export declare function tokenizar(query: string): string[];
|
|
29
|
+
export interface Fragmento {
|
|
30
|
+
/** Línea que coincidió, con su contexto inmediato. */
|
|
31
|
+
texto: string;
|
|
32
|
+
/** Términos de la consulta presentes en la línea. */
|
|
33
|
+
terminos: string[];
|
|
34
|
+
puntaje: number;
|
|
35
|
+
}
|
|
36
|
+
export interface ResultadoBusqueda {
|
|
37
|
+
fragmentos: Fragmento[];
|
|
38
|
+
/** Términos en que se descompuso la consulta. */
|
|
39
|
+
terminos: string[];
|
|
40
|
+
/** Términos que no aparecen en ninguna parte del texto. */
|
|
41
|
+
ausentes: string[];
|
|
42
|
+
}
|
|
43
|
+
/**
|
|
44
|
+
* Busca los términos de `query` en `texto` y devuelve los fragmentos más
|
|
45
|
+
* relevantes, ordenados por cuántos términos distintos concentra cada uno.
|
|
46
|
+
*
|
|
47
|
+
* Una línea que contiene la consulta completa e íntegra se prioriza sobre
|
|
48
|
+
* cualquier coincidencia parcial: si el usuario acertó la frase exacta, esa
|
|
49
|
+
* es la mejor respuesta posible.
|
|
50
|
+
*/
|
|
51
|
+
export declare function buscarEnTexto(texto: string, query: string, maxFragmentos?: number): ResultadoBusqueda;
|
|
52
|
+
//# sourceMappingURL=doc-search.d.ts.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doc-search.d.ts","sourceRoot":"","sources":["../../src/utils/doc-search.ts"],"names":[],"mappings":"AAAA;;;;;;;;;;;;;;;;GAgBG;AAcH,8EAA8E;AAC9E,wBAAgB,UAAU,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,CAKhD;AAED;;;;;;;GAOG;AACH,wBAAgB,SAAS,CAAC,KAAK,EAAE,MAAM,GAAG,MAAM,EAAE,CAQjD;AAED,MAAM,WAAW,SAAS;IACxB,sDAAsD;IACtD,KAAK,EAAE,MAAM,CAAC;IACd,qDAAqD;IACrD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,OAAO,EAAE,MAAM,CAAC;CACjB;AAED,MAAM,WAAW,iBAAiB;IAChC,UAAU,EAAE,SAAS,EAAE,CAAC;IACxB,iDAAiD;IACjD,QAAQ,EAAE,MAAM,EAAE,CAAC;IACnB,2DAA2D;IAC3D,QAAQ,EAAE,MAAM,EAAE,CAAC;CACpB;AAED;;;;;;;GAOG;AACH,wBAAgB,aAAa,CAC3B,KAAK,EAAE,MAAM,EACb,KAAK,EAAE,MAAM,EACb,aAAa,SAAK,GACjB,iBAAiB,CAwCnB"}
|
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Búsqueda de texto dentro de los documentos locales (manuales, guías, normativa).
|
|
3
|
+
*
|
|
4
|
+
* ⚠ POR QUÉ EXISTE ESTE MÓDULO: la primera implementación buscaba la consulta
|
|
5
|
+
* completa como subcadena literal (`texto.includes(query)`). Con una palabra
|
|
6
|
+
* suelta funcionaba, pero cualquier pregunta en lenguaje natural —que es
|
|
7
|
+
* justo lo que envía un LLM— no coincidía nunca. Peor aún, el fallo era
|
|
8
|
+
* silencioso y seguro de sí mismo: "no se encontraron coincidencias en
|
|
9
|
+
* ninguno de los 10 documentos" ante la pregunta "¿qué multas me pueden
|
|
10
|
+
* aplicar?", existiendo un PDF entero dedicado a multas y sanciones. El
|
|
11
|
+
* modelo repetía ese "no hay información" al usuario como si fuera cierto.
|
|
12
|
+
*
|
|
13
|
+
* Ahora la consulta se descompone en términos, se ignoran las palabras
|
|
14
|
+
* vacías y se puntúa cada línea por cuántos términos distintos contiene.
|
|
15
|
+
* Además se normalizan los acentos, porque en documentos en español
|
|
16
|
+
* "sanción" y "sancion" deben encontrarse mutuamente.
|
|
17
|
+
*/
|
|
18
|
+
/** Palabras sin valor discriminante en una consulta en español. */
|
|
19
|
+
const VACIAS = new Set([
|
|
20
|
+
'que', 'qué', 'cual', 'cuál', 'cuales', 'cuáles', 'como', 'cómo', 'cuando',
|
|
21
|
+
'cuándo', 'donde', 'dónde', 'quien', 'quién', 'por', 'para', 'con', 'sin',
|
|
22
|
+
'los', 'las', 'del', 'unos', 'unas', 'una', 'uno', 'sus', 'sobre', 'entre',
|
|
23
|
+
'este', 'esta', 'esto', 'estos', 'estas', 'ese', 'esa', 'eso', 'aquel',
|
|
24
|
+
'son', 'ser', 'está', 'esta', 'estan', 'están', 'hay', 'han', 'hace',
|
|
25
|
+
'puede', 'pueden', 'debe', 'deben', 'tiene', 'tienen', 'mas', 'más',
|
|
26
|
+
'pero', 'sus', 'les', 'nos', 'mis', 'tus', 'muy', 'ya', 'les', 'algun',
|
|
27
|
+
'algún', 'alguna', 'todo', 'toda', 'todos', 'todas', 'otro', 'otra',
|
|
28
|
+
]);
|
|
29
|
+
/** Minúsculas y sin acentos, para que "sanción" y "sancion" se encuentren. */
|
|
30
|
+
export function normalizar(texto) {
|
|
31
|
+
return texto
|
|
32
|
+
.toLowerCase()
|
|
33
|
+
.normalize('NFD')
|
|
34
|
+
.replace(/[̀-ͯ]/g, '');
|
|
35
|
+
}
|
|
36
|
+
/**
|
|
37
|
+
* Descompone la consulta en términos buscables: normalizados, sin palabras
|
|
38
|
+
* vacías y de al menos 3 caracteres.
|
|
39
|
+
*
|
|
40
|
+
* Si al filtrar no queda ningún término (consulta hecha solo de palabras
|
|
41
|
+
* vacías, ej. "qué es esto"), se devuelven todos los de 3+ caracteres antes
|
|
42
|
+
* que dejar al usuario sin búsqueda.
|
|
43
|
+
*/
|
|
44
|
+
export function tokenizar(query) {
|
|
45
|
+
const crudos = normalizar(query)
|
|
46
|
+
.split(/[^a-z0-9ñ]+/)
|
|
47
|
+
.filter((t) => t.length >= 3);
|
|
48
|
+
const utiles = crudos.filter((t) => !VACIAS.has(t));
|
|
49
|
+
const elegidos = utiles.length > 0 ? utiles : crudos;
|
|
50
|
+
return [...new Set(elegidos)];
|
|
51
|
+
}
|
|
52
|
+
/**
|
|
53
|
+
* Busca los términos de `query` en `texto` y devuelve los fragmentos más
|
|
54
|
+
* relevantes, ordenados por cuántos términos distintos concentra cada uno.
|
|
55
|
+
*
|
|
56
|
+
* Una línea que contiene la consulta completa e íntegra se prioriza sobre
|
|
57
|
+
* cualquier coincidencia parcial: si el usuario acertó la frase exacta, esa
|
|
58
|
+
* es la mejor respuesta posible.
|
|
59
|
+
*/
|
|
60
|
+
export function buscarEnTexto(texto, query, maxFragmentos = 12) {
|
|
61
|
+
const terminos = tokenizar(query);
|
|
62
|
+
if (terminos.length === 0) {
|
|
63
|
+
return { fragmentos: [], terminos: [], ausentes: [] };
|
|
64
|
+
}
|
|
65
|
+
const frase = normalizar(query).trim();
|
|
66
|
+
const lineas = texto.split('\n');
|
|
67
|
+
const normalizadas = lineas.map(normalizar);
|
|
68
|
+
const ausentes = terminos.filter((t) => !normalizadas.some((l) => l.includes(t)));
|
|
69
|
+
const candidatos = [];
|
|
70
|
+
for (let i = 0; i < lineas.length; i++) {
|
|
71
|
+
const linea = lineas[i]?.trim();
|
|
72
|
+
if (!linea)
|
|
73
|
+
continue;
|
|
74
|
+
const norm = normalizadas[i];
|
|
75
|
+
const presentes = terminos.filter((t) => norm.includes(t));
|
|
76
|
+
if (presentes.length === 0)
|
|
77
|
+
continue;
|
|
78
|
+
// La frase completa vale más que la suma de sus partes.
|
|
79
|
+
const bonus = frase.length > 0 && norm.includes(frase) ? terminos.length * 10 : 0;
|
|
80
|
+
const contexto = [];
|
|
81
|
+
const anterior = lineas[i - 1]?.trim();
|
|
82
|
+
const siguiente = lineas[i + 1]?.trim();
|
|
83
|
+
if (anterior)
|
|
84
|
+
contexto.push(`[Anterior] ${anterior}`);
|
|
85
|
+
contexto.push(`[COINCIDENCIA] ${linea}`);
|
|
86
|
+
if (siguiente)
|
|
87
|
+
contexto.push(`[Siguiente] ${siguiente}`);
|
|
88
|
+
candidatos.push({
|
|
89
|
+
texto: contexto.join('\n'),
|
|
90
|
+
terminos: presentes,
|
|
91
|
+
puntaje: presentes.length + bonus,
|
|
92
|
+
});
|
|
93
|
+
}
|
|
94
|
+
candidatos.sort((a, b) => b.puntaje - a.puntaje);
|
|
95
|
+
return { fragmentos: candidatos.slice(0, maxFragmentos), terminos, ausentes };
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=doc-search.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"doc-search.js","sourceRoot":"","sources":["../../src/utils/doc-search.ts"],"names":[],"mappings":"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"}
|
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../src/utils/error-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,QAAQ,EAAE,CAAC;CACpB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtC,SAAgB,iBAAiB,EAAE,MAAM,CAAC;gBAE9B,UAAU,EAAE,MAAM,EAAE,SAAS,GAAE,QAAQ,EAAO;CAQ3D;
|
|
1
|
+
{"version":3,"file":"error-handler.d.ts","sourceRoot":"","sources":["../../src/utils/error-handler.ts"],"names":[],"mappings":"AAAA;;;;;GAKG;AAIH,MAAM,WAAW,QAAQ;IACvB,MAAM,EAAE,MAAM,CAAC;IACf,OAAO,EAAE,MAAM,CAAC;IAChB,OAAO,EAAE,MAAM,GAAG,IAAI,CAAC;CACxB;AAED,MAAM,WAAW,gBAAgB;IAC/B,OAAO,EAAE,KAAK,CAAC;IACf,KAAK,EAAE,MAAM,GAAG,IAAI,CAAC;IACrB,OAAO,EAAE,IAAI,CAAC;IACd,MAAM,EAAE,QAAQ,EAAE,CAAC;CACpB;AAED,qBAAa,kBAAmB,SAAQ,KAAK;IAC3C,SAAgB,UAAU,EAAE,MAAM,CAAC;IACnC,SAAgB,SAAS,EAAE,QAAQ,EAAE,CAAC;IACtC,SAAgB,iBAAiB,EAAE,MAAM,CAAC;gBAE9B,UAAU,EAAE,MAAM,EAAE,SAAS,GAAE,QAAQ,EAAO;CAQ3D;AAoCD;;GAEG;AACH,wBAAsB,iBAAiB,CAAC,QAAQ,EAAE,QAAQ,GAAG,OAAO,CAAC,OAAO,CAAC,CAoB5E"}
|
|
@@ -44,12 +44,10 @@ function getActionableMessage(httpStatus, apiErrors) {
|
|
|
44
44
|
}
|
|
45
45
|
}
|
|
46
46
|
function formatRateLimitMessage(detail) {
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const waitHours = Math.ceil((tomorrow.getTime() - now.getTime()) / (1000 * 60 * 60));
|
|
52
|
-
return `Cuota diaria de la API agotada. El límite se restablece al inicio del próximo día calendario UTC (en aproximadamente ${waitHours} horas). Para reducir el consumo: usa filtros más específicos (estado, región, fechas), evita consultas masivas, y usa 'ttl_cambio_ms' para sincronización incremental en vez de descargas completas. Si necesitas más cuota, contacta a ChileCompra.${detail}`;
|
|
47
|
+
// La cuota se comporta como un token bucket que se recarga solo (glosario de
|
|
48
|
+
// la guía oficial): medido en producción, la API volvió a responder ~13 min
|
|
49
|
+
// después de un 429. Por eso no se aconseja esperar al día siguiente.
|
|
50
|
+
return `Se agotaron temporalmente los tokens de cuota de la API. No es necesario esperar al día siguiente: la cuota se recarga sola, así que reintenta en unos minutos. Para gastar menos: usa filtros más específicos (estado, región, fechas), baja 'limite_analisis'/'max_paginas' en las herramientas de análisis, y prefiere 'ttl_cambio_ms' o un rango 'cambio_desde'/'cambio_hasta' para sincronización incremental en vez de descargas completas. Si el 429 persiste durante horas, la cuota diaria del ticket sí puede estar agotada: contacta a ChileCompra para uno de mayor límite.${detail}`;
|
|
53
51
|
}
|
|
54
52
|
/**
|
|
55
53
|
* Parsea la respuesta de error de la API y lanza una excepción tipada.
|
|
@@ -1 +1 @@
|
|
|
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,
|
|
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"}
|
|
@@ -5,14 +5,32 @@
|
|
|
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;
|
|
15
|
+
/** 429 seguidos sin una consulta exitosa en medio; gradúa la espera. */
|
|
16
|
+
private consecutive429;
|
|
13
17
|
private readonly maxPerMinute;
|
|
14
18
|
private requestTimestamps;
|
|
15
|
-
|
|
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;
|
|
16
34
|
private getTodayUTC;
|
|
17
35
|
/**
|
|
18
36
|
* Espera (si es necesario) hasta que haya cupo dentro de la ventana de 1 minuto,
|
|
@@ -25,8 +43,25 @@ export declare class RateLimiter {
|
|
|
25
43
|
recordRequest(): void;
|
|
26
44
|
/**
|
|
27
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ó.
|
|
28
61
|
*/
|
|
29
|
-
markLimited(): void;
|
|
62
|
+
markLimited(retryAfterSeconds?: number): void;
|
|
63
|
+
/** Espera creciente ante 429 sucesivos, para no martillar la API. */
|
|
64
|
+
private esperaPorDefecto;
|
|
30
65
|
/**
|
|
31
66
|
* Verifica si estamos actualmente limitados.
|
|
32
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"}
|