/** * 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. [{"action":"...","params":{}}] 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(/([\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(/[\s\S]*?<\/admin_tools>\s*/g, ''); clean = clean.replace(/[\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; }