Spaces:
Paused
Paused
| /** | |
| * 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(); | |
| } | |