zelin-bot / src /proactive.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* 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();
}