zelin-bot / src /admin_tools.js
Z User
v5.8.5: Gemma 4, MC Wiki, MC Player, anti-hallucination, CPU optimizations
ee826ee
/**
* admin_tools.js — Herramientas de Administración Discord para Zelin
* v2.8:
* - executeAdminActions ahora resuelve nombres → IDs con identity-resolver
* - user_messages arreglado (no usa channelId null)
* - dm_history usa db importado directamente
* - Imports redundantes eliminados
*/
import { ChannelType, EmbedBuilder } from 'discord.js';
import * as db from './db.js';
import { readConfig } from './utils.js';
import { resolvePlayer } from './identity-resolver.js';
const config = readConfig();
const OWNER_ID = config.admin.userId;
// ── Registro de auditoría ─────────────────────────────────────────────────────
async function auditLog(guild, action, details, executedBy = 'zelin') {
const entry = { action, details, executedBy, timestamp: new Date().toISOString() };
await db.memSet(`audit.${Date.now()}`, entry, 'audit_log').catch(() => {});
console.log(`[AdminTools] 📋 ${action}${JSON.stringify(details)} — por ${executedBy}`);
try {
const logChannel = guild?.channels.cache.find(c =>
c.isTextBased?.() && (c.name.includes('log') || c.name.includes('audit') || c.name.includes('registro')) && !c.name.includes('ticket')
);
if (logChannel) {
const embed = new EmbedBuilder()
.setTitle(`🔧 Acción admin: ${action}`)
.setDescription(Object.entries(details).map(([k,v]) => `**${k}:** ${v}`).join('\n'))
.setFooter({ text: `Ejecutado por: ${executedBy}` })
.setTimestamp()
.setColor(0xf59e0b);
await logChannel.send({ embeds: [embed] }).catch(() => {});
}
} catch {}
}
function canActOn(guild, targetMember) {
if (!targetMember) return false;
const zelinMember = guild.members.cache.get(guild.client.user.id);
if (!zelinMember) return false;
if (targetMember.id === OWNER_ID) return false;
if (targetMember.roles.highest.position >= zelinMember.roles.highest.position) return false;
return true;
}
// ── NUEVA FUNCIÓN: Resolver userId (nombre o ID) a Discord ID real ────────────
// Fix del bug principal: si el owner dice "ban a Pedro", p.userId llega como "Pedro"
// Esta función convierte "Pedro" → "123456789012345678"
async function resolveUserId(identifier, guild) {
if (!identifier || !guild) return identifier;
// Ya es un snowflake
if (/^\d{17,19}$/.test(identifier)) return identifier;
// Mención
const mentionMatch = identifier.match(/^<@!?(\d{17,19})>$/);
if (mentionMatch) return mentionMatch[1];
// Resolver por nombre
const resolved = await resolvePlayer(identifier, guild.members.cache);
if (resolved.member && resolved.confidence >= 0.7) {
console.log(`[AdminTools] Resuelto "${identifier}" → ${resolved.member.id} (${(resolved.confidence*100).toFixed(0)}% confianza, método: ${resolved.method})`);
return resolved.member.id;
}
return identifier; // devolver tal cual si no se pudo resolver
}
// ═══════════════════════════════════════════════════════════════════════════════
// ROLES
// ═══════════════════════════════════════════════════════════════════════════════
export async function addRoleToUser(guild, userId, roleQuery, reason = 'Solicitado por owner') {
const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null);
if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` };
const role = guild.roles.cache.find(r => r.id === roleQuery || r.name.toLowerCase() === roleQuery.toLowerCase() || r.name.toLowerCase().includes(roleQuery.toLowerCase()));
if (!role) return { ok: false, error: `Rol "${roleQuery}" no encontrado` };
if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' };
try {
await member.roles.add(role, reason);
await auditLog(guild, 'add_role', { usuario: member.user.username, rol: role.name, motivo: reason });
return { ok: true, message: `✅ Rol **${role.name}** añadido a ${member.displayName}` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function removeRoleFromUser(guild, userId, roleQuery, reason = 'Solicitado por owner') {
const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null);
if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` };
const role = guild.roles.cache.find(r => r.id === roleQuery || r.name.toLowerCase() === roleQuery.toLowerCase() || r.name.toLowerCase().includes(roleQuery.toLowerCase()));
if (!role) return { ok: false, error: `Rol "${roleQuery}" no encontrado` };
if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' };
try {
await member.roles.remove(role, reason);
await auditLog(guild, 'remove_role', { usuario: member.user.username, rol: role.name });
return { ok: true, message: `✅ Rol **${role.name}** quitado a ${member.displayName}` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function createRole(guild, { name, color = null, hoist = false, mentionable = false, reason = 'Creado por Zelin' }) {
try {
const role = await guild.roles.create({ name, color: color ?? null, hoist, mentionable, reason });
await auditLog(guild, 'create_role', { nombre: role.name, id: role.id });
return { ok: true, role, message: `✅ Rol **${role.name}** creado (ID: \`${role.id}\`)` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function deleteRole(guild, roleQuery, reason = 'Eliminado por owner') {
const role = guild.roles.cache.find(r => r.id === roleQuery || r.name.toLowerCase() === roleQuery.toLowerCase());
if (!role) return { ok: false, error: `Rol "${roleQuery}" no encontrado` };
if (role.managed) return { ok: false, error: 'No se pueden eliminar roles gestionados' };
try {
const name = role.name;
await role.delete(reason);
await auditLog(guild, 'delete_role', { nombre: name, motivo: reason });
return { ok: true, message: `✅ Rol **${name}** eliminado` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function editRole(guild, roleQuery, { name, color, hoist, mentionable } = {}) {
const role = guild.roles.cache.find(r => r.id === roleQuery || r.name.toLowerCase() === roleQuery.toLowerCase());
if (!role) return { ok: false, error: `Rol "${roleQuery}" no encontrado` };
try {
const options = {};
if (name !== undefined) options.name = name;
if (color !== undefined) options.color = color;
if (hoist !== undefined) options.hoist = hoist;
if (mentionable !== undefined) options.mentionable = mentionable;
await role.edit(options);
await auditLog(guild, 'edit_role', { rol: role.name, cambios: JSON.stringify(options) });
return { ok: true, message: `✅ Rol **${role.name}** editado` };
} catch (e) { return { ok: false, error: e.message }; }
}
// ═══════════════════════════════════════════════════════════════════════════════
// CANALES
// ═══════════════════════════════════════════════════════════════════════════════
export async function createChannel(guild, { name, type = 'text', category = null, topic = null, reason = 'Creado por Zelin' }) {
const typeMap = { text: ChannelType.GuildText, voice: ChannelType.GuildVoice, forum: ChannelType.GuildForum };
const channelType = typeMap[type] ?? ChannelType.GuildText;
let parent = null;
if (category) {
parent = guild.channels.cache.find(c => c.type === ChannelType.GuildCategory && (c.id === category || c.name.toLowerCase().includes(category.toLowerCase())));
}
try {
const channel = await guild.channels.create({ name, type: channelType, parent: parent ?? null, topic: topic ?? null, reason });
await auditLog(guild, 'create_channel', { nombre: channel.name, tipo: type, categoria: parent?.name ?? 'ninguna' });
return { ok: true, channel, message: `✅ Canal **#${channel.name}** creado` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function deleteChannel(guild, channelQuery, reason = 'Eliminado por owner') {
const channel = guild.channels.cache.find(c => c.id === channelQuery || c.name.toLowerCase() === channelQuery.toLowerCase());
if (!channel) return { ok: false, error: `Canal "${channelQuery}" no encontrado` };
try {
const name = channel.name;
await channel.delete(reason);
await auditLog(guild, 'delete_channel', { nombre: name, motivo: reason });
return { ok: true, message: `✅ Canal **#${name}** eliminado` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function editChannel(guild, channelQuery, { name, topic, slowmode, nsfw } = {}) {
const channel = guild.channels.cache.find(c => c.id === channelQuery || c.name.toLowerCase() === channelQuery.toLowerCase());
if (!channel) return { ok: false, error: `Canal "${channelQuery}" no encontrado` };
try {
const options = {};
if (name !== undefined) options.name = name;
if (topic !== undefined) options.topic = topic;
if (slowmode !== undefined) options.rateLimitPerUser = slowmode;
if (nsfw !== undefined) options.nsfw = nsfw;
await channel.edit(options);
await auditLog(guild, 'edit_channel', { canal: channel.name, cambios: JSON.stringify(options) });
return { ok: true, message: `✅ Canal **#${channel.name}** editado` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function createCategory(guild, name, reason = 'Creado por Zelin') {
try {
const category = await guild.channels.create({ name, type: ChannelType.GuildCategory, reason });
await auditLog(guild, 'create_category', { nombre: category.name });
return { ok: true, category, message: `✅ Categoría **${category.name}** creada` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function purgeChannel(guild, channelQuery, amount = 10, reason = 'Limpieza por owner') {
const channel = guild.channels.cache.find(c => c.id === channelQuery || c.name.toLowerCase() === channelQuery.toLowerCase());
if (!channel) return { ok: false, error: `Canal "${channelQuery}" no encontrado` };
if (!channel.isTextBased?.()) return { ok: false, error: 'No es un canal de texto' };
const n = Math.min(Math.max(1, amount), 100);
try {
const deleted = await channel.bulkDelete(n, true);
await auditLog(guild, 'purge_channel', { canal: channel.name, mensajes: deleted.size, motivo: reason });
return { ok: true, message: `✅ **${deleted.size}** mensajes eliminados en #${channel.name}` };
} catch (e) { return { ok: false, error: e.message }; }
}
// ═══════════════════════════════════════════════════════════════════════════════
// MIEMBROS
// ═══════════════════════════════════════════════════════════════════════════════
export async function setNickname(guild, userId, nickname, reason = 'Cambiado por Zelin') {
const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null);
if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` };
if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' };
try {
const old = member.nickname ?? member.user.username;
await member.setNickname(nickname, reason);
await auditLog(guild, 'set_nickname', { usuario: member.user.username, antes: old, después: nickname });
return { ok: true, message: `✅ Nickname de **${member.user.username}** cambiado a **${nickname ?? '(original)'}**` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function kickMember(guild, userId, reason = 'Expulsado por owner') {
const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null);
if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` };
if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' };
try {
const name = member.user.username;
await member.kick(reason);
await auditLog(guild, 'kick', { usuario: name, motivo: reason });
return { ok: true, message: `✅ **${name}** expulsado del servidor` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function banMember(guild, userId, { reason = 'Baneado por owner', deleteMessageDays = 0 } = {}) {
const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null);
if (member && !canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' };
try {
const target = member?.user?.username ?? userId;
await guild.bans.create(userId, { reason, deleteMessageSeconds: deleteMessageDays * 86400 });
await auditLog(guild, 'ban', { usuario: target, motivo: reason, deleteMsgDays: deleteMessageDays });
return { ok: true, message: `✅ **${target}** baneado del servidor` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function unbanMember(guild, userId, reason = 'Desban por owner') {
try {
await guild.bans.remove(userId, reason);
await auditLog(guild, 'unban', { userId, motivo: reason });
return { ok: true, message: `✅ Usuario \`${userId}\` desbaneado` };
} catch (e) { return { ok: false, error: `No se pudo desbanear: ${e.message}` }; }
}
export async function timeoutMember(guild, userId, durationMs, reason = 'Timeout por Zelin') {
const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null);
if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` };
if (!canActOn(guild, member)) return { ok: false, error: 'Sin permisos sobre este usuario' };
const clampedMs = Math.min(durationMs, 28 * 24 * 60 * 60 * 1000);
try {
await member.timeout(clampedMs, reason);
const mins = Math.round(clampedMs / 60000);
await auditLog(guild, 'timeout', { usuario: member.user.username, duracion: `${mins}min`, motivo: reason });
return { ok: true, message: `✅ **${member.user.username}** en timeout por ${mins} minutos` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function removeTimeout(guild, userId) {
const member = guild.members.cache.get(userId) ?? await guild.members.fetch(userId).catch(() => null);
if (!member) return { ok: false, error: `Usuario ${userId} no encontrado` };
try {
await member.timeout(null);
await auditLog(guild, 'remove_timeout', { usuario: member.user.username });
return { ok: true, message: `✅ Timeout de **${member.user.username}** eliminado` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function setServerName(guild, name, reason = 'Cambiado por owner') {
try {
await guild.setName(name, reason);
await auditLog(guild, 'set_server_name', { nombre: name });
return { ok: true, message: `✅ Nombre del servidor cambiado a **${name}**` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function listBans(guild) {
try {
const bans = await guild.bans.fetch();
if (!bans.size) return { ok: true, message: 'No hay usuarios baneados actualmente', list: [] };
const list = [...bans.values()].slice(0, 20).map(b => `• **${b.user.username}** — ${b.reason ?? 'sin motivo'}`);
return { ok: true, message: `**${bans.size} bans activos:**\n${list.join('\n')}`, list };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function sendToChannel(guild, channelQuery, content) {
const channel = guild.channels.cache.find(c =>
c.id === channelQuery || c.name.toLowerCase() === channelQuery.toLowerCase() || c.name.toLowerCase().includes(channelQuery.toLowerCase())
);
if (!channel) return { ok: false, error: `Canal "${channelQuery}" no encontrado` };
if (!channel.isTextBased?.()) return { ok: false, error: 'No es un canal de texto' };
try {
await channel.send(content.substring(0, 1900));
return { ok: true, message: `✅ Mensaje enviado en #${channel.name}` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function sendDMToUser(client, userId, content) {
try {
const user = await client.users.fetch(userId);
const dm = await user.createDM();
await dm.send(content.substring(0, 1900));
return { ok: true, msg: `✅ DM enviado a ${user.username}` };
} catch (e) { return { ok: false, error: e.message }; }
}
export async function transformToZelinStyle(rawText, callAI) {
try {
const result = await callAI([
{ role: 'system', content: `Eres Zelin, una IA de Discord con personalidad única: habla de forma natural, casual y directa en español, usa jerga juvenil latinoamericana ocasionalmente, evita formalismos. Tu tarea: reescribe el mensaje que te dan EXACTAMENTE con tu estilo — misma intención, mismas ideas, pero sonando 100% a ti. No añadas ni quites información. No expliques nada. Solo devuelve el mensaje reescrito, sin comillas ni explicaciones.` },
{ role: 'user', content: rawText },
], 'spanish', 300);
return result?.trim() || rawText;
} catch { return rawText; }
}
// ── Definición de herramientas para el prompt ─────────────────────────────────
export const ADMIN_TOOL_DEFINITIONS = `## HERRAMIENTAS DE ADMINISTRACIÓN DISCORD
Solo disponibles cuando el owner las pide o hay una necesidad legítima de moderación.
<admin_tools>[{"action":"...","params":{}}]</admin_tools>
ACCIONES DISPONIBLES:
- add_role → params: { userId, role }
- remove_role → params: { userId, role }
- create_role → params: { name, color?, hoist?, mentionable? }
- delete_role → params: { role }
- edit_role → params: { role, name?, color?, hoist?, mentionable? }
- create_channel → params: { name, type?, category?, topic? }
- delete_channel → params: { channel }
- edit_channel → params: { channel, name?, topic?, slowmode? }
- create_category → params: { name }
- purge_channel → params: { channel, amount }
- set_nickname → params: { userId, nickname }
- kick → params: { userId, reason? }
- ban → params: { userId, reason?, deleteMessageDays? }
- unban → params: { userId }
- timeout → params: { userId, durationMs, reason? }
- remove_timeout → params: { userId }
- set_server_name → params: { name }
- list_bans → params: {}
- send_to_channel → params: { channel, content }
- ignore_user → params: { userId }
- unignore_user → params: { userId }
- dm_history → params: { userId, limit? }
- get_deleted → params: { userId?, channel?, limit? }
- user_messages → params: { userId, limit? }
- send_dm → params: { userId, content }
- send_dm_raw → params: { userId, content }
- create_skill → params: { request }
- skills_list → params: {}
- user_info → params: { userId }
IMPORTANTE: userId puede ser un nombre de usuario, nickname, mención o ID numérico.
Zelin resolverá automáticamente el nombre al ID correcto.
REGLAS CRÍTICAS:
- Acciones destructivas (ban/delete/kick/purge): confirmar brevemente si no hay orden explícita
- Si el owner habla contigo CASUALMENTE (saludo, pregunta, chat), NO uses admin_tools — responde normal
- Si el mensaje tiene @BotZelin es porque el owner te está hablando A TI, NO es una petición de DM
- NUNCA generes el texto "No hay usuario mencionado en tu mensaje" — no existe esa respuesta
- Si el owner menciona @roles o @usuarios en su mensaje para notificarles, NO interpretes eso como acción admin
- Solo usa admin_tools cuando el owner PIDE EXPLÍCITAMENTE una acción (banear, dar rol, crear canal, etc.)
`;
export function parseAdminTools(text) {
const match = text.match(/<admin_tools>([\s\S]*?)<\/admin_tools>/);
if (!match) return null;
try {
const raw = match[1].trim();
const clean = raw.slice(raw.indexOf('['), raw.lastIndexOf(']') + 1);
return JSON.parse(clean);
} catch { return null; }
}
export function stripAdminToolBlock(text) {
let clean = text.replace(/<admin_tools>[\s\S]*?<\/admin_tools>\s*/g, '');
clean = clean.replace(/<admin_tools>[\s\S]*/g, '');
clean = clean.replace(/<\/?admin_tools>/g, '');
return clean.trim();
}
// ── Ejecutar acciones admin ───────────────────────────────────────────────────
// FIX IDENTIDAD: verifica Discord ID real del solicitante, nunca confía en el nombre
// El parámetro requestingUserId es el message.author.id real — no el nombre
export async function executeAdminActions(actions, guild, requestedBy = 'owner', { client = null, callAI = null, requestingUserId = null } = {}) {
const results = [];
// VERIFICACIÓN CRÍTICA: si se pasa un userId, debe coincidir con el owner
// Esto previene que la IA ejecute acciones aunque alguien diga "soy tomatito"
if (requestingUserId && requestingUserId !== OWNER_ID) {
console.error(`[AdminTools] 🚨 INTENTO DE EJECUCIÓN NO AUTORIZADO — User ID: ${requestingUserId} (esperado: ${OWNER_ID})`);
return [{ action: 'blocked', ok: false, error: 'Usuario no autorizado por ID de Discord.' }];
}
for (const action of actions.slice(0, 5)) {
const p = action.params ?? {};
// ── Resolver userId si es un nombre o mención ──────────────────────────
// Aplica a todas las acciones que tienen userId
if (p.userId && guild) {
p.userId = await resolveUserId(p.userId, guild);
}
let result;
// Acciones destructivas — requieren que el userId esté verificado
const DESTRUCTIVE = ['purge_channel', 'ban', 'kick', 'delete_channel', 'delete_role'];
if (DESTRUCTIVE.includes(action.action) && requestingUserId !== OWNER_ID) {
results.push({ action: action.action, ok: false, error: 'Acción destructiva bloqueada: ID no verificado.' });
continue;
}
switch (action.action) {
case 'add_role': result = await addRoleToUser(guild, p.userId, p.role, `Solicitado por ${requestedBy}`); break;
case 'remove_role': result = await removeRoleFromUser(guild, p.userId, p.role, `Solicitado por ${requestedBy}`); break;
case 'create_role': result = await createRole(guild, { ...p, reason: `Creado por ${requestedBy}` }); break;
case 'delete_role': result = await deleteRole(guild, p.role, `Eliminado por ${requestedBy}`); break;
case 'edit_role': result = await editRole(guild, p.role, p); break;
case 'create_channel': result = await createChannel(guild, { ...p, reason: `Creado por ${requestedBy}` }); break;
case 'delete_channel': result = await deleteChannel(guild, p.channel, `Eliminado por ${requestedBy}`); break;
case 'edit_channel': result = await editChannel(guild, p.channel, p); break;
case 'create_category':result = await createCategory(guild, p.name, `Creado por ${requestedBy}`); break;
case 'purge_channel': result = await purgeChannel(guild, p.channel, p.amount ?? 10, `Limpieza por ${requestedBy}`); break;
case 'set_nickname': result = await setNickname(guild, p.userId, p.nickname, `Por ${requestedBy}`); break;
case 'kick': result = await kickMember(guild, p.userId, p.reason); break;
case 'ban': result = await banMember(guild, p.userId, p); break;
case 'unban': result = await unbanMember(guild, p.userId, p.reason); break;
case 'timeout': result = await timeoutMember(guild, p.userId, p.durationMs, p.reason); break;
case 'remove_timeout': result = await removeTimeout(guild, p.userId); break;
case 'set_server_name':result = await setServerName(guild, p.name); break;
case 'list_bans': result = await listBans(guild); break;
case 'send_to_channel':result = await sendToChannel(guild, p.channel, p.content); break;
case 'ignore_user': {
const { ignoreUser } = await import('./state.js');
await ignoreUser(p.userId);
result = { ok: true, msg: `Usuario ${p.userId} ignorado.` };
break;
}
case 'unignore_user': {
const { unignoreUser } = await import('./state.js');
await unignoreUser(p.userId);
result = { ok: true, msg: `Usuario ${p.userId} des-ignorado.` };
break;
}
// FIX: dm_history usa db directamente, no import dinámico
case 'dm_history': {
const rows = await db.getContext(`dm_${p.userId}`, p.limit ?? 20);
result = { ok: true, msg: rows.map(r => `[${r.role}] ${r.content}`).join('\n') || 'Sin DMs registrados.' };
break;
}
// FIX: user_messages ya no usa channelId null
case 'user_messages': {
try {
// Buscar mensajes del usuario en la tabla messages directamente por user_id
const r = await db.db.execute({
sql: `SELECT m.content, m.channel_id, m.created_at
FROM messages m
WHERE m.user_id = ? AND m.is_deleted = 0
ORDER BY m.created_at DESC LIMIT ?`,
args: [p.userId, p.limit ?? 30],
});
result = {
ok: true,
msg: r.rows.length
? r.rows.map(r => `[${r.created_at?.slice(0,16)}] #${r.channel_id}: ${r.content}`).join('\n')
: 'Sin mensajes encontrados para ese usuario.',
};
} catch (e) {
result = { ok: false, error: e.message };
}
break;
}
case 'get_deleted': {
const rows = await db.getDeletedMessages({ userId: p.userId, channelId: p.channel, limit: p.limit ?? 15 });
result = { ok: true, msg: rows.length ? rows.map(r => `[${r.deleted_at?.slice(0,16)}] ${r.username} en #${r.channel_name}: ${r.content}`).join('\n') : 'Sin mensajes eliminados.' };
break;
}
case 'create_skill': {
const skillsMod = await import('./skills.js');
const res = await skillsMod.generateSkill(p.request ?? 'skill genérico', guild);
result = res ? { ok: true, msg: `skill "${res.name}" creado: ${res.description}` } : { ok: false, error: 'No se pudo generar el skill' };
if (res) await skillsMod.saveSkill(res);
break;
}
case 'skills_list': {
const skillsMod = await import('./skills.js');
const list = skillsMod.listSkills();
result = { ok: true, msg: list.length ? list.map(s => `• ${s.name}: ${s.description}`).join('\n') : 'Sin skills.' };
break;
}
case 'user_info': {
const u = await db.getUserWithRoles(p.userId);
result = { ok: !!u, msg: u ? `${u.username} | msgs: ${u.message_count} | roles: ${u.roles?.join(', ') || 'ninguno'}` : 'Usuario no encontrado.' };
break;
}
case 'send_dm': {
if (!client) { result = { ok: false, error: 'client no disponible' }; break; }
const transformed = callAI ? await transformToZelinStyle(p.content, callAI) : p.content;
result = await sendDMToUser(client, p.userId, transformed);
if (result.ok) {
await db.addContext(`dm_${p.userId}`, 'assistant', transformed, null, 'Zelin').catch(() => {});
}
break;
}
case 'send_dm_raw': {
if (!client) { result = { ok: false, error: 'client no disponible' }; break; }
result = await sendDMToUser(client, p.userId, p.content);
if (result.ok) {
await db.addContext(`dm_${p.userId}`, 'assistant', p.content, null, 'Zelin').catch(() => {});
}
break;
}
default: result = { ok: false, error: `Acción desconocida: ${action.action}` };
}
results.push({ action: action.action, ...result });
}
return results;
}