From f2ee3bbd1175fdb91d395453f51d6fee6c63b1d4 Mon Sep 17 00:00:00 2001 From: brobert Date: Sun, 9 Nov 2025 19:29:40 +0100 Subject: [PATCH] =?UTF-8?q?feat:=20a=C3=B1adir=20health=20check=20para=20r?= =?UTF-8?q?einiciar=20instancia=20de=20Evolution=20API?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-authored-by: aider (openrouter/z-ai/glm-4.6) --- src/services/maintenance.ts | 100 ++++++++++++++++++++++++++++++++---- 1 file changed, 89 insertions(+), 11 deletions(-) diff --git a/src/services/maintenance.ts b/src/services/maintenance.ts index 1e7b53f..c33af24 100644 --- a/src/services/maintenance.ts +++ b/src/services/maintenance.ts @@ -4,6 +4,8 @@ import { toIsoSqlUTC } from '../utils/datetime'; export class MaintenanceService { private static _timer: any = null; + private static _healthCheckTimer: any = null; + private static _lastRestartAttempt: number = 0; private static get retentionDays(): number { const v = Number(process.env.GROUP_MEMBERS_INACTIVE_RETENTION_DAYS); @@ -11,19 +13,44 @@ export class MaintenanceService { return 180; // por defecto 180 días } + private static get evolutionApiConfig() { + return { + url: process.env.EVOLUTION_API_URL, + instance: process.env.EVOLUTION_API_INSTANCE, + apiKey: process.env.EVOLUTION_API_KEY, + intervalMs: Number(process.env.HEALTH_CHECK_INTERVAL_MS || '120000'), // 2 min por defecto + restartCooldownMs: Number(process.env.HEALTH_CHECK_RESTART_COOLDOWN_MS || '900000'), // 15 min por defecto + }; + } + static start(): void { if (process.env.NODE_ENV === 'test' && process.env.FORCE_SCHEDULERS !== 'true') return; - if (this.retentionDays <= 0) return; - - const intervalMs = 24 * 60 * 60 * 1000; // diario - this._timer = setInterval(() => { - this.cleanupInactiveMembersOnce().catch(err => { - console.error('❌ Error en cleanup de miembros inactivos:', err); - }); - this.reconcileAliasUsersOnce().catch(err => { - console.error('❌ Error en reconcile de alias de usuarios:', err); - }); - }, intervalMs); + + // --- Tareas diarias existentes --- + if (this.retentionDays > 0) { + const intervalMs = 24 * 60 * 60 * 1000; // diario + this._timer = setInterval(() => { + this.cleanupInactiveMembersOnce().catch(err => { + console.error('❌ Error en cleanup de miembros inactivos:', err); + }); + this.reconcileAliasUsersOnce().catch(err => { + console.error('❌ Error en reconcile de alias de usuarios:', err); + }); + }, intervalMs); + } + + // --- Nuevo Health Check de Evolution API --- + const { url, instance, apiKey, intervalMs } = this.evolutionApiConfig; + if (url && instance && apiKey) { + console.log('[MaintenanceService] Iniciando health check de Evolution API...'); + this._healthCheckTimer = setInterval(() => { + this.performEvolutionHealthCheck().catch(err => { + console.error('❌ Error en el health check de Evolution API:', err); + }); + }, intervalMs); + } else { + console.warn('[MaintenanceService] Variables de entorno para el health check de Evolution API (URL, INSTANCE, API_KEY) no encontradas. Health check desactivado.'); + } } static stop(): void { @@ -31,6 +58,10 @@ export class MaintenanceService { clearInterval(this._timer); this._timer = null; } + if (this._healthCheckTimer) { + clearInterval(this._healthCheckTimer); + this._healthCheckTimer = null; + } } static async cleanupInactiveMembersOnce(instance: Database = db, retentionDays: number = this.retentionDays): Promise { @@ -98,4 +129,51 @@ export class MaintenanceService { return 0; } } + + /** + * Verifica el estado de la instancia de Evolution API y la reinicia si es necesario. + */ + private static async performEvolutionHealthCheck(): Promise { + const { url, instance, apiKey, restartCooldownMs } = this.evolutionApiConfig; + + const stateUrl = `${url}/instance/connectionState/${instance}`; + const restartUrl = `${url}/instance/restart/${instance}`; + const headers = { apikey: apiKey }; + + try { + const response = await fetch(stateUrl, { method: 'GET', headers }); + + if (!response.ok) { + console.error(`[HealthCheck] Error al consultar estado de Evolution API: ${response.status} ${response.statusText}`); + return; + } + + const data = await response.json(); + const currentState = data?.instance?.state; + + console.log(`[HealthCheck] Estado de la instancia '${instance}': ${currentState}`); + + if (currentState !== 'open') { + const now = Date.now(); + if (now - this._lastRestartAttempt > restartCooldownMs) { + console.warn(`[HealthCheck] La instancia no está 'open'. Estado actual: ${currentState}. Intentando reiniciar...`); + try { + const restartResponse = await fetch(restartUrl, { method: 'PUT', headers }); + if (restartResponse.ok) { + console.log(`[HealthCheck] Petición de reinicio para '${instance}' enviada exitosamente.`); + this._lastRestartAttempt = now; + } else { + console.error(`[HealthCheck] Fallo al reiniciar la instancia. Status: ${restartResponse.status} ${restartResponse.statusText}`); + } + } catch (restartError) { + console.error('[HealthCheck] Error de red al intentar reiniciar la instancia:', restartError); + } + } else { + console.log(`[HealthCheck] La instancia no está 'open', pero esperando cooldown de ${Math.round(restartCooldownMs / 60000)} minutos para no sobrecargar la API.`); + } + } + } catch (error) { + console.error('[HealthCheck] Error de red o inesperado al verificar el estado de la Evolution API:', error); + } + } }