zelin-bot / src /commands.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* ============================================
* βš™οΈ commands.js β€” Comandos de AdministraciΓ³n
* ============================================
* Solo comandos de admin. Los usuarios no tienen comandos.
* Todos ephemeral (solo lo ve quien lo ejecuta).
*/
import { REST, Routes, EmbedBuilder } from 'discord.js';
import { readConfig, formatDate } from './utils.js';
import * as db from './db.js';
import * as memory from './memory.js';
import { getDailyStats, getProviderStatus } from './ai.js';
import * as minecraft from './minecraft.js';
import * as extractor from './extractor.js';
import { getCommunityPulse } from './memory.js';
import { getServerStats, getTopUsers } from './db.js';
const config = readConfig();
// ── DefiniciΓ³n de comandos ────────────────────────────────────────────────────
export const SLASH_COMMANDS = [
{
name : 'zelin_estado',
description : 'Estado completo de Zelin (stats, providers, DB)',
dm_permission : false,
default_member_permissions: '8',
},
{
name : 'zelin_usuario',
description : 'Ver perfil completo de un usuario',
dm_permission : false,
default_member_permissions: '8',
options: [{
type : 6, // USER
name : 'usuario',
description: 'Usuario a consultar',
required : true,
}],
},
{
name : 'zelin_historial',
description : 'Ver historial de moderaciΓ³n de un usuario',
dm_permission : false,
default_member_permissions: '8',
options: [{
type : 6,
name : 'usuario',
description: 'Usuario a consultar',
required : true,
}],
},
{
name : 'zelin_buscar',
description : 'Buscar mensajes en el historial',
dm_permission : false,
default_member_permissions: '8',
options: [{
type : 3, // STRING
name : 'texto',
description: 'Texto a buscar',
required : true,
}],
},
{
name : 'zelin_pulso',
description : 'Ver el estado emocional del servidor (ΓΊltimos 7 dΓ­as)',
dm_permission : false,
default_member_permissions: '8',
},
{
name : 'zelin_stats',
description : 'EstadΓ­sticas generales del servidor',
dm_permission : false,
default_member_permissions: '8',
},
{
name : 'zelin_ia',
description : 'Estado del sistema de IA (providers, cache, RAG, local AI)',
dm_permission : false,
default_member_permissions: '0', // SOLO OWNER β€” se verifica en el handler
},
{
name : 'zelin_extraer',
description : 'Sincronizar toda la info del servidor con la DB',
dm_permission : false,
default_member_permissions: '8',
},
{
name : 'zelin_mc',
description : 'Estado actual del servidor de Minecraft',
dm_permission : false,
default_member_permissions: '8',
},
{
name : 'zelin_audit',
description : 'Ver las ΓΊltimas acciones de administraciΓ³n ejecutadas por Zelin',
dm_permission : false,
default_member_permissions: '8',
},
{
name : 'zelin_learning',
description : 'Ver estadΓ­sticas de aprendizaje de Zelin',
dm_permission : false,
default_member_permissions: '8',
},
{
name : 'zelin_memoria',
description : 'Ver o editar la memoria de Zelin',
dm_permission : false,
default_member_permissions: '8',
options: [
{
type : 3,
name : 'clave',
description: 'Clave de memoria (ej: config.cleanup_days)',
required : false,
},
{
type : 3,
name : 'valor',
description: 'Nuevo valor (opcional, para editar)',
required : false,
},
],
},
];
// ── Registro de comandos ──────────────────────────────────────────────────────
export async function registerCommands() {
// Try multiple Discord API hosts β€” HF Spaces may block discord.com REST
// Note: discordapp.com only supports v9, not v10
const apiConfigs = [
{ api: 'https://discordapp.com/api', version: '9' },
{ api: 'https://discord.com/api', version: '10' },
];
for (const { api, version } of apiConfigs) {
try {
const rest = new REST({ version, retries: 5, timeout: 30_000, api })
.setToken(config.discord.token);
console.log(`[Commands] Registrando comandos slash via ${api} v${version}...`);
await rest.put(
Routes.applicationGuildCommands(config.discord.clientId, config.discord.guildId),
{ body: SLASH_COMMANDS }
);
console.log(`[Commands] βœ… Comandos registrados via ${api} v${version}`);
return; // success
} catch (err) {
console.warn(`[Commands] FallΓ³ registro via ${api} v${version}: ${err.message?.slice(0, 100)}`);
}
}
// Last resort: try without explicit API base (uses discord.js default)
try {
const rest = new REST({ version: '9', retries: 3, timeout: 30_000 }).setToken(config.discord.token);
console.log('[Commands] Intentando registro con API default...');
await rest.put(
Routes.applicationGuildCommands(config.discord.clientId, config.discord.guildId),
{ body: SLASH_COMMANDS }
);
console.log('[Commands] βœ… Comandos registrados via API default');
} catch (err) {
console.error('[Commands] ❌ No se pudieron registrar comandos en ningún host:', err.message?.slice(0, 100));
}
}
// ── Manejador de comandos ─────────────────────────────────────────────────────
export async function handleCommand(interaction) {
// Verificar que es admin (permisos Discord) O el owner exacto
// Los comandos con default_member_permissions:'0' son SOLO para el owner
const ownerOnlyCommands = ['zelin_ia'];
if (ownerOnlyCommands.includes(interaction.commandName) && interaction.user.id !== config.admin.userId) {
return interaction.reply({ content: '❌ Solo el owner puede usar este comando.', ephemeral: true });
}
// Solo admins
if (!interaction.member?.permissions?.has('Administrator')) {
return interaction.reply({ content: '❌ Solo administradores.', ephemeral: true });
}
await interaction.deferReply({ ephemeral: true });
const { commandName } = interaction;
try {
switch (commandName) {
// ── /zelin_estado ────────────────────────────────────────────────────
case 'zelin_estado': {
const stats = await getServerStats();
const mcStatus = minecraft.getServerStatus();
const pulse = await getCommunityPulse(7);
const embed = new EmbedBuilder()
.setTitle('πŸ€– Zelin v5.8 β€” Estado')
.setColor(0x5865f2)
.addFields(
{ name: 'πŸ’Ύ Base de datos', value: `Mensajes: **${stats.totalMessages}**\nUsuarios activos: **${stats.activeUsers}**\nAcciones mod: **${stats.modActions}**\nActivos hoy: **${stats.activeToday}**`, inline: true },
{ name: '⛏️ Servidor MC', value: mcStatus.online ? 'βœ… Online' : `❌ Offline (${mcStatus.failCount} fallos)`, inline: true },
{ name: '🌑️ Clima', value: `**${pulse.mood}**\n+${pulse.totals.positive} / -${pulse.totals.negative} / ⚑${pulse.totals.conflict}`, inline: true },
)
.setFooter({ text: 'Zelin v5.8 β€’ TomateSMP' })
.setTimestamp();
return interaction.editReply({ embeds: [embed] });
}
// ── /zelin_usuario ───────────────────────────────────────────────────
case 'zelin_usuario': {
const target = interaction.options.getUser('usuario');
const user = await db.getUserWithRoles(target.id);
if (!user) {
return interaction.editReply('❌ Usuario no encontrado en la DB. ¿Ha enviado algún mensaje?');
}
const observations = await memory.getUserObservations(target.id);
const embed = new EmbedBuilder()
.setTitle(`πŸ‘€ ${user.username}`)
.setThumbnail(target.displayAvatarURL({ size: 128 }))
.setColor(0x5865f2)
.addFields(
{ name: 'Nickname', value: user.nickname || 'sin nick', inline: true },
{ name: 'Mensajes', value: String(user.message_count || 0), inline: true },
{ name: 'EntrΓ³', value: formatDate(user.joined_at), inline: true },
{ name: 'Cuenta creada', value: formatDate(user.account_created), inline: true },
{ name: 'Activo', value: user.is_active ? 'βœ… SΓ­' : '❌ Se fue', inline: true },
{ name: 'Bot', value: user.bot ? 'SΓ­' : 'No', inline: true },
{ name: 'Roles', value: user.roles?.join(', ') || 'ninguno', inline: false },
)
.setFooter({ text: `ID: ${user.user_id}` });
if (observations) {
embed.addFields({ name: 'πŸ“ Notas de Zelin', value: observations, inline: false });
}
return interaction.editReply({ embeds: [embed] });
}
// ── /zelin_historial ─────────────────────────────────────────────────
case 'zelin_historial': {
const target = interaction.options.getUser('usuario');
const history = await db.getUserModHistory(target.id, 10);
if (history.length === 0) {
return interaction.editReply(`βœ… ${target.username} no tiene historial de moderaciΓ³n.`);
}
const lines = history.map(h =>
`**${h.action}** | ${formatDate(h.created_at)}\nMotivo: ${h.reason || 'sin especificar'}\n${h.rule_violated ? `Regla: ${h.rule_violated}` : ''}`
).join('\n─────\n');
const embed = new EmbedBuilder()
.setTitle(`πŸ“‹ Historial de ${target.username}`)
.setColor(0xff6b35)
.setDescription(lines.substring(0, 2000))
.setFooter({ text: `${history.length} acciones β€’ Zelin v5.8` });
return interaction.editReply({ embeds: [embed] });
}
// ── /zelin_buscar ────────────────────────────────────────────────────
case 'zelin_buscar': {
const query = interaction.options.getString('texto');
const results = await db.searchMessages(query, 10);
if (results.length === 0) {
return interaction.editReply(`πŸ” Sin resultados para: "${query}"`);
}
const lines = results.map(r =>
`**${r.username}** en #${r.channel_id} | ${formatDate(r.created_at)}\n${r.content.substring(0, 150)}`
).join('\n─────\n');
return interaction.editReply(`πŸ” **${results.length} resultados para "${query}":**\n\n${lines}`.substring(0, 2000));
}
// ── /zelin_pulso ─────────────────────────────────────────────────────
case 'zelin_pulso': {
const pulse = await getCommunityPulse(7);
const topUsers = await getTopUsers(7, 5);
const topStr = topUsers.map((u, i) =>
`${i + 1}. ${u.nickname || u.username}: **${u.count}** msgs`
).join('\n');
const moodEmoji = { positivo: '😊', neutral: '😐', tenso: '😀' }[pulse.mood] ?? '❓';
const embed = new EmbedBuilder()
.setTitle('🌑️ Pulso de la Comunidad β€” Últimos 7 dΓ­as')
.setColor(pulse.mood === 'positivo' ? 0x00ff00 : pulse.mood === 'tenso' ? 0xff0000 : 0xffff00)
.addFields(
{ name: 'Clima general', value: `${moodEmoji} **${pulse.mood}**`, inline: true },
{
name : 'Sentimientos',
value: `βœ… Positivo: ${pulse.totals.positive}\n❌ Negativo: ${pulse.totals.negative}\n⚑ Conflicto: ${pulse.totals.conflict}`,
inline: true,
},
{ name: 'MΓ‘s activos', value: topStr || 'sin datos', inline: false },
)
.setFooter({ text: 'Zelin v5.8 β€’ TomateSMP' });
return interaction.editReply({ embeds: [embed] });
}
// ── /zelin_stats ─────────────────────────────────────────────────────
case 'zelin_stats': {
const stats = await getServerStats();
const topUsers = await getTopUsers(7, 10);
const topStr = topUsers.map((u, i) =>
`${i + 1}. ${u.nickname || u.username}: ${u.count} msgs`
).join('\n');
const embed = new EmbedBuilder()
.setTitle('πŸ“Š EstadΓ­sticas de TomateSMP')
.setColor(0x5865f2)
.addFields(
{ name: 'πŸ‘₯ Usuarios activos', value: String(stats.activeUsers), inline: true },
{ name: 'πŸ’¬ Total mensajes', value: String(stats.totalMessages), inline: true },
{ name: 'πŸ›‘οΈ Acciones mod', value: String(stats.modActions), inline: true },
{ name: '🟒 Activos hoy', value: String(stats.activeToday), inline: true },
{ name: 'πŸ† Top esta semana', value: topStr || 'sin datos', inline: false },
)
.setFooter({ text: 'Zelin v5.8 β€’ TomateSMP' })
.setTimestamp();
return interaction.editReply({ embeds: [embed] });
}
// ── /zelin_extraer ───────────────────────────────────────────────────
case 'zelin_extraer': {
await interaction.editReply('⏳ Iniciando sincronización completa... (puede tardar varios minutos)');
const results = await extractor.runFullExtraction(
interaction.guild,
async (msg) => {
await interaction.editReply(`⏳ ${msg}`).catch(() => {});
}
);
return interaction.editReply(
`βœ… SincronizaciΓ³n completada:\nβ€’ ${results.members} miembros\nβ€’ ${results.roles} roles\nβ€’ ${results.channels} canales\nβ€’ ${results.messages} mensajes`
);
}
// ── /zelin_mc ────────────────────────────────────────────────────────
case 'zelin_audit': {
const r = await db.db.execute({
sql : "SELECT value FROM zelin_memory WHERE category = 'audit_log' ORDER BY key DESC LIMIT 20",
args: [],
});
if (!r.rows.length) {
return interaction.editReply('Sin acciones admin registradas todavΓ­a.');
}
const entries = r.rows.map(row => {
try {
const d = typeof row.value === 'string' ? JSON.parse(row.value) : row.value;
const ts = d.timestamp?.substring(0, 16) ?? '?';
return `**${ts}** | \`${d.action}\` | ${Object.entries(d.details ?? {}).map(([k,v]) => `${k}: ${v}`).join(', ')}`;
} catch { return String(row.value).substring(0, 120); }
});
const embed = new EmbedBuilder()
.setTitle('πŸ“‹ Audit Log β€” Acciones Admin de Zelin')
.setColor(0xf59e0b)
.setDescription(entries.join('\n').substring(0, 4000))
.setTimestamp();
return interaction.editReply({ embeds: [embed] });
}
case 'zelin_learning': {
const { getStyleProfile } = await import('./learning.js');
const profile = await getStyleProfile();
if (!profile) {
return interaction.editReply('sin datos de aprendizaje aΓΊn (necesita al menos 10 feedbacks)');
}
const embed = new EmbedBuilder()
.setTitle('🧠 Aprendizaje de Zelin')
.setColor(0x7c3aed)
.addFields(
{ name: 'Score promedio', value: String(profile.avgScore), inline: true },
{ name: 'Engagement', value: profile.engagementRate + '%', inline: true },
{ name: 'Mensajes ignorados', value: profile.ignoreRate + '%', inline: true },
{ name: 'Total feedback', value: String(profile.totalFeedback), inline: true },
{ name: 'Mensajes espontΓ‘neos', value: profile.spontaneousOK ? 'βœ… funcionan' : '⚠️ no recibidos bien', inline: true },
{ name: 'Revivir chat', value: profile.revivalOK ? 'βœ… funciona' : '⚠️ mejorar', inline: true },
);
if (profile.goodExamples.length)
embed.addFields({ name: 'πŸ‘ Ejemplos buenos', value: profile.goodExamples.map(e => 'β€’ ' + e.substring(0,80)).join('\n').substring(0,1000), inline: false });
if (profile.badExamples.length)
embed.addFields({ name: 'πŸ‘Ž Ejemplos malos', value: profile.badExamples.map(e => 'β€’ ' + e.substring(0,60)).join('\n').substring(0,1000), inline: false });
return interaction.editReply({ embeds: [embed] });
}
case 'zelin_mc': {
const status = minecraft.getServerStatus();
return interaction.editReply(
status.online
? `βœ… Servidor de Minecraft online: play.tomatesmp.pw`
: `❌ Servidor offline (${status.failCount} fallos consecutivos)`
);
}
// ── /zelin_memoria ───────────────────────────────────────────────────
case 'zelin_memoria': {
const key = interaction.options.getString('clave');
const value = interaction.options.getString('valor');
if (!key) {
// Listar categorΓ­as de memoria
const cats = await db.memGetCategory('config');
const lines = Object.entries(cats)
.map(([k, v]) => `\`${k}\`: ${JSON.stringify(v)}`)
.join('\n');
return interaction.editReply(`πŸ“¦ **Memoria de configuraciΓ³n:**\n${lines || 'vacΓ­a'}`.substring(0, 2000));
}
if (value !== null) {
await db.memSet(key, value);
return interaction.editReply(`βœ… Memoria actualizada: \`${key}\` = \`${value}\``);
}
const val = await db.memGet(key);
return interaction.editReply(
val !== null
? `πŸ“¦ \`${key}\` = \`${JSON.stringify(val)}\``
: `❌ Clave \`${key}\` no encontrada`
);
}
// ── /zelin_ia ─────────────────────────────────────────────────
case 'zelin_ia': {
const stats = getDailyStats();
const providers = getProviderStatus();
const lines = providers.map(p =>
`**${p.name}**: ${p.state} (score: ${p.score})`
).join('\n');
const embed = new EmbedBuilder()
.setTitle('🧠 Estado del Sistema de IA')
.setColor(0x5865f2)
.setDescription(lines.substring(0, 4000))
.setTimestamp();
return interaction.editReply({ embeds: [embed] });
}
default:
return interaction.editReply('❓ Comando no reconocido.');
}
} catch (err) {
console.error('[Command] Error en', commandName, ':', err.message);
return interaction.editReply(`❌ Error: ${err.message.substring(0, 200)}`);
}
}