/** * proactive.js — Motor Proactivo de Zelin v5.8 * FIXES: * - canales buscados por nombre con find() en vez de get() por ID * - cooldown aplicado correctamente */ import * as db from './db.js'; import { callAI, callAIBackground } from './ai.js'; import { readConfig } from './utils.js'; const config = readConfig(); // ── Cron minimalista sin dependencias externas ──────────────────────────────── function scheduleCron(cronStr, fn) { const tick = () => { const now = new Date(); const min = now.getMinutes(); const hour = now.getHours(); const day = now.getDay(); const parts = cronStr.trim().split(' '); const matchMin = parts[0]==='*' ? true : parts[0].startsWith('*/') ? min % parseInt(parts[0].slice(2)) === 0 : parseInt(parts[0]) === min; const matchHour = parts[1]==='*' ? true : parseInt(parts[1]) === hour; const matchDay = parts[4]==='*' ? true : parseInt(parts[4]) === day; if (matchMin && matchHour && matchDay) fn().catch(e=>console.error('[PROACTIVE] cron error:',e.message)); }; setInterval(tick, 60_000); } // ── Helpers para buscar canales por nombre O por ID ─────────────────────────── // FIX: config.channels guarda nombres, no IDs — hay que buscar con find() function findChannel(client, nameOrId) { if (!nameOrId || !client) return null; // Intentar por ID primero const byId = client.channels.cache.get(nameOrId); if (byId) return byId; // Buscar por nombre (parcial o exacto) const q = nameOrId.toLowerCase().replace('#', ''); return client.channels.cache.find(c => c.isTextBased?.() && ( c.name.toLowerCase() === q || c.name.toLowerCase().includes(q) ) ) || null; } // ── ProactiveEngine ──────────────────────────────────────────────────────────── export class ProactiveEngine { constructor(client) { this.client = client; this.lastSpoke = {}; // channelId → timestamp this.COOLDOWN = 30 * 60 * 1000; } init() { const feats = config.features ?? {}; if (!feats.proactive) return; scheduleCron('0 20 * * *', () => this.eveningRecap()); scheduleCron('0 12 * * 1', () => this.weeklyNews()); scheduleCron('*/30 * * * *', () => this.checkAlerts()); setInterval(() => this.checkMinecraftEvents().catch(()=>{}), 60_000); this.setupActivityMonitor(); console.log('[PROACTIVE] Motor proactivo iniciado'); } canSpeak(channelId) { const last = this.lastSpoke[channelId] || 0; return Date.now() - last > this.COOLDOWN; } // FIX: buscar canal por nombre con findChannel() async speak(channelNameOrId, prompt, context='') { const ch = findChannel(this.client, channelNameOrId); if (!ch) return; if (!this.canSpeak(ch.id)) return; try { const dec = await callAIBackground([{ role:'user', content:'Eres Zelin. Contexto: "'+context+'". ¿Deberías mandar un mensaje ahora? SOLO JSON: {"speak":true/false,"reason":"..."}', }], 'fast', 60, context); const parsed = JSON.parse(dec.replace(/```json|```/g,'').trim()); if (!parsed.speak) return; } catch { return; } const message = await callAIBackground([ {role:'system', content:'Eres Zelin, asistente de TomateSMP. Sé natural y breve.'}, {role:'user', content: prompt}, ], 'chat', 300, prompt); await ch.send(message).catch(()=>{}); this.lastSpoke[ch.id] = Date.now(); } async eveningRecap() { const chRef = config.channels?.general || config.channels?.announcements; if (!chRef) return; try { const stats = await db.getServerStats(); await this.speak(chRef, 'Genera un resumen divertido y muy breve del día en TomateSMP con estos datos: '+JSON.stringify(stats), 'Resumen diario del servidor'); } catch {} } async weeklyNews() { const chRef = config.channels?.general || config.channels?.announcements; if (!chRef) return; try { const stats = await db.getServerStats(); await this.speak(chRef, 'Genera un breve mensaje de inicio de semana para TomateSMP. Datos de la semana: '+JSON.stringify(stats), 'Inicio de semana en el servidor'); } catch {} } async checkAlerts() { const chRef = config.channels?.staffChat; if (!chRef) return; try { const r = await db.db.execute({sql:"SELECT key,value FROM zelin_memory WHERE category='alerts' AND key LIKE 'alert.pending.%' LIMIT 5",args:[]}); for (const row of r.rows) { let alert; try { alert = typeof row.value==='string' ? JSON.parse(row.value) : row.value; } catch { continue; } await this.speak(chRef, 'Informa sobre esta alerta de forma clara y concisa: '+JSON.stringify(alert), 'Alerta automática del sistema'); await db.db.execute({sql:"UPDATE zelin_memory SET category='alerts_done' WHERE key=?",args:[row.key]}).catch(()=>{}); } } catch {} } async checkMinecraftEvents() { const chRef = config.channels?.welcome || config.channels?.general; if (!chRef) return; try { const r = await db.db.execute({sql:"SELECT key,value FROM zelin_memory WHERE category='mc_events' AND key LIKE 'mcevent.pending.%' LIMIT 10",args:[]}); for (const row of r.rows) { let event; try { event = typeof row.value==='string' ? JSON.parse(row.value) : row.value; } catch { continue; } if (event.type==='new_player') { await this.speak(chRef, 'Dale la bienvenida a '+event.playerName+' que acaba de unirse al servidor TomateSMP por primera vez. Sé cálido y breve.', 'Nuevo jugador en el servidor'); } await db.db.execute({sql:"UPDATE zelin_memory SET category='mc_events_done' WHERE key=?",args:[row.key]}).catch(()=>{}); } } catch {} } setupActivityMonitor() { this.client.on('messageCreate', async (msg) => { if (msg.author.bot) return; if (!this.canSpeak(msg.channelId)) return; if (Math.random() > 0.10) return; try { const rel = await callAIBackground([{ role:'user', content:'Mensaje Discord: "'+msg.content.slice(0,200)+'". ¿Es sobre TomateSMP o algo donde Zelin aporte algo útil? SOLO JSON: {"relevant":true/false}', }], 'fast', 40, msg.content); const parsed = JSON.parse(rel.replace(/```json|```/g,'').trim()); if (!parsed.relevant) return; const reply = await callAIBackground([ {role:'system', content:'Eres Zelin. Participa brevemente y de forma natural en la conversación.'}, {role:'user', content: msg.content}, ], 'chat', 200, msg.content); await msg.channel.send(reply).catch(()=>{}); this.lastSpoke[msg.channelId] = Date.now(); } catch {} }); } } export let proactiveEngine = null; export function initProactive(client) { const feats = config.features ?? {}; if (!feats.proactive) return; proactiveEngine = new ProactiveEngine(client); proactiveEngine.init(); }