implement advanced memory optimization system with configurable settings

This commit is contained in:
2025-09-25 22:57:24 -05:00
parent eb3ab7c4e6
commit f56c98535b
22 changed files with 483 additions and 102 deletions

View File

@@ -158,10 +158,13 @@ ${userHistory.messages.slice(-3).join('\n')}`;
const response = await genAI.models.generateContent({
model: "gemini-2.5-flash",
contents: baseSystemPrompt,
maxOutputTokens: dynamicOutputTokens,
temperature: 0.7, // Reducido para respuestas más consistentes
topP: 0.8,
topK: 30,
// @ts-ignore
generationConfig: {
maxOutputTokens: dynamicOutputTokens,
temperature: 0.7, // Reducido para respuestas más consistentes
topP: 0.8,
topK: 30,
}
});
// Extraer el texto de la respuesta

View File

@@ -1,6 +1,9 @@
import { CommandMessage } from "../../../core/types/commands";
// @ts-ignore
import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message, MessageFlags } from "discord.js";
import {
ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, Message, MessageFlags,
AnyComponentBuilder
} from "discord.js";
import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars";
/**
@@ -356,7 +359,7 @@ export const command: CommandMessage = {
.setMaxLength(256)
.setRequired(true);
const firstActionRow = new ActionRowBuilder().addComponents(titleInput);
const firstActionRow = new ActionRowBuilder<TextInputBuilder>().addComponents(titleInput);
modal.addComponents(firstActionRow);
//@ts-ignore
@@ -380,7 +383,7 @@ export const command: CommandMessage = {
.setMaxLength(2000)
.setRequired(true);
const firstActionRow = new ActionRowBuilder().addComponents(descInput);
const firstActionRow: ActionRowBuilder<AnyComponentBuilder> = new ActionRowBuilder().addComponents(descInput);
modal.addComponents(firstActionRow);
//@ts-ignore
@@ -403,7 +406,7 @@ export const command: CommandMessage = {
.setMaxLength(7)
.setRequired(false);
const firstActionRow = new ActionRowBuilder().addComponents(colorInput);
const firstActionRow: ActionRowBuilder<TextInputBuilder> = new ActionRowBuilder().addComponents(colorInput);
modal.addComponents(firstActionRow);
//@ts-ignore

View File

@@ -2,6 +2,7 @@ import { CommandMessage } from "../../../core/types/commands";
// @ts-ignore
import { ComponentType, ButtonStyle, ModalBuilder, TextInputBuilder, TextInputStyle, ActionRowBuilder, MessageFlags } from "discord.js";
import { replaceVars, isValidUrlOrVariable, listVariables } from "../../../core/lib/vars";
import {Block} from "../../../core/types/block";
// Botones de edición (máx 5 por fila)
const btns = (disabled = false) => ([
@@ -141,10 +142,14 @@ export const command: CommandMessage = {
return;
}
let blockState: any = {
title: existingBlock.config?.title || `Block: ${blockName}`,
color: existingBlock.config?.color ?? null,
let blockState: Block = {
//@ts-ignore
title: existingBlock.config?.title ?? `## Block: ${blockName}`,
//@ts-ignore
color: existingBlock.config?.color ?? 0x427AE3,
//@ts-ignore
coverImage: existingBlock.config?.coverImage ?? null,
//@ts-ignore
components: Array.isArray(existingBlock.config?.components) ? existingBlock.config.components : []
};
@@ -183,6 +188,7 @@ export const command: CommandMessage = {
await i.deferUpdate();
await client.prisma.blockV2Config.update({
where: { guildId_name: { guildId: message.guildId!, name: blockName } },
//@ts-ignore
data: { config: blockState }
});
await updateEditor(editorMessage, {
@@ -210,6 +216,7 @@ export const command: CommandMessage = {
const modal = new ModalBuilder().setCustomId('edit_title_modal').setTitle('📝 Editar Título del Block');
const titleInput = new TextInputBuilder().setCustomId('title_input').setLabel('Nuevo Título').setStyle(TextInputStyle.Short).setPlaceholder('Escribe el nuevo título aquí...').setValue(blockState.title || '').setMaxLength(256).setRequired(true);
const row = new ActionRowBuilder().addComponents(titleInput);
//@ts-ignore
modal.addComponents(row);
// @ts-ignore
await i.showModal(modal);
@@ -217,10 +224,12 @@ export const command: CommandMessage = {
}
case "edit_description": {
const modal = new ModalBuilder().setCustomId('edit_description_modal').setTitle('📄 Editar Descripción');
//@ts-ignore
const descComp = blockState.components.find((c: any) => c.type === 10);
const currentDesc = descComp ? descComp.content : '';
const descInput = new TextInputBuilder().setCustomId('description_input').setLabel('Nueva Descripción').setStyle(TextInputStyle.Paragraph).setPlaceholder('Escribe la nueva descripción aquí...').setValue(currentDesc || '').setMaxLength(2000).setRequired(true);
const row = new ActionRowBuilder().addComponents(descInput);
//@ts-ignore
modal.addComponents(row);
// @ts-ignore
await i.showModal(modal);
@@ -231,6 +240,7 @@ export const command: CommandMessage = {
const currentColor = blockState.color ? `#${blockState.color.toString(16).padStart(6, '0')}` : '';
const colorInput = new TextInputBuilder().setCustomId('color_input').setLabel('Color en formato HEX').setStyle(TextInputStyle.Short).setPlaceholder('#FF5733 o FF5733').setValue(currentColor).setMaxLength(7).setRequired(false);
const row = new ActionRowBuilder().addComponents(colorInput);
//@ts-ignore
modal.addComponents(row);
// @ts-ignore
await i.showModal(modal);
@@ -240,6 +250,7 @@ export const command: CommandMessage = {
const modal = new ModalBuilder().setCustomId('add_content_modal').setTitle(' Agregar Nuevo Contenido');
const contentInput = new TextInputBuilder().setCustomId('content_input').setLabel('Contenido del Texto').setStyle(TextInputStyle.Paragraph).setPlaceholder('Escribe el contenido aquí...').setMaxLength(2000).setRequired(true);
const row = new ActionRowBuilder().addComponents(contentInput);
//@ts-ignore
modal.addComponents(row);
// @ts-ignore
await i.showModal(modal);
@@ -249,6 +260,7 @@ export const command: CommandMessage = {
const modal = new ModalBuilder().setCustomId('add_image_modal').setTitle('🖼️ Agregar Nueva Imagen');
const imageUrlInput = new TextInputBuilder().setCustomId('image_url_input').setLabel('URL de la Imagen').setStyle(TextInputStyle.Short).setPlaceholder('https://ejemplo.com/imagen.png').setMaxLength(2000).setRequired(true);
const row = new ActionRowBuilder().addComponents(imageUrlInput);
//@ts-ignore
modal.addComponents(row);
// @ts-ignore
await i.showModal(modal);
@@ -270,10 +282,12 @@ export const command: CommandMessage = {
const modal = new ModalBuilder().setCustomId('edit_cover_modal').setTitle('🖼️ Editar Imagen de Portada');
const coverInput = new TextInputBuilder().setCustomId('cover_input').setLabel('URL de la Imagen de Portada').setStyle(TextInputStyle.Short).setPlaceholder('https://ejemplo.com/portada.png').setValue(blockState.coverImage || '').setMaxLength(2000).setRequired(true);
const row = new ActionRowBuilder().addComponents(coverInput);
//@ts-ignore
modal.addComponents(row);
// @ts-ignore
await b.showModal(modal);
} else if (b.customId === 'delete_cover') {
//@ts-ignore
blockState.coverImage = null;
await b.update({ content: '✅ Imagen de portada eliminada.', components: [] });
await updateEditor(editorMessage, { // @ts-ignore
@@ -287,6 +301,7 @@ export const command: CommandMessage = {
const modal = new ModalBuilder().setCustomId('add_cover_modal').setTitle('🖼️ Agregar Imagen de Portada');
const coverInput = new TextInputBuilder().setCustomId('cover_input').setLabel('URL de la Imagen de Portada').setStyle(TextInputStyle.Short).setPlaceholder('https://ejemplo.com/portada.png').setMaxLength(2000).setRequired(true);
const row = new ActionRowBuilder().addComponents(coverInput);
//@ts-ignore
modal.addComponents(row);
// @ts-ignore
await i.showModal(modal);
@@ -294,6 +309,7 @@ export const command: CommandMessage = {
break;
}
case "move_block": {
//@ts-ignore
const options = blockState.components.map((c: any, idx: number) => ({
label: c.type === 10 ? `Texto: ${c.content?.slice(0, 30) || '...'}` : c.type === 14 ? 'Separador' : c.type === 12 ? `Imagen: ${c.url?.slice(-30) || '...'}` : `Componente ${c.type}`,
value: String(idx),
@@ -317,8 +333,11 @@ export const command: CommandMessage = {
if (b.customId.startsWith('move_up_')) {
const i2 = parseInt(b.customId.replace('move_up_', ''));
if (i2 > 0) {
//@ts-ignore
const item = blockState.components[i2];
//@ts-ignore
blockState.components.splice(i2, 1);
//@ts-ignore
blockState.components.splice(i2 - 1, 0, item);
}
await b.update({ content: '✅ Bloque movido arriba.', components: [] });
@@ -364,10 +383,12 @@ export const command: CommandMessage = {
selCollector.on('collect', async (sel: any) => {
const selectedValue = sel.values[0];
if (selectedValue === 'cover_image') {
//@ts-ignore
blockState.coverImage = null;
await sel.update({ content: '✅ Imagen de portada eliminada.', components: [] });
} else {
const idx = parseInt(selectedValue);
//@ts-ignore
blockState.components.splice(idx, 1);
await sel.update({ content: '✅ Elemento eliminado.', components: [] });
}
@@ -445,6 +466,7 @@ export const command: CommandMessage = {
const modal = new ModalBuilder().setCustomId('import_json_modal').setTitle('📥 Importar JSON');
const jsonInput = new TextInputBuilder().setCustomId('json_input').setLabel('Pega tu configuración JSON aquí').setStyle(TextInputStyle.Paragraph).setPlaceholder('{"title": "...", "components": [...]}').setMaxLength(4000).setRequired(true);
const row = new ActionRowBuilder().addComponents(jsonInput);
//@ts-ignore
modal.addComponents(row);
// @ts-ignore
await i.showModal(modal);
@@ -463,6 +485,7 @@ export const command: CommandMessage = {
const spacingInput = new TextInputBuilder().setCustomId('separator_spacing').setLabel('Espaciado (1-3)').setStyle(TextInputStyle.Short).setPlaceholder('1, 2 o 3').setValue('1').setMaxLength(1).setRequired(false);
const r1 = new ActionRowBuilder().addComponents(visibleInput);
const r2 = new ActionRowBuilder().addComponents(spacingInput);
//@ts-ignore
modal.addComponents(r1, r2);
// @ts-ignore
await i.showModal(modal);
@@ -489,6 +512,7 @@ export const command: CommandMessage = {
const modal = new ModalBuilder().setCustomId(`edit_thumbnail_modal_${idx}`).setTitle('📎 Editar Thumbnail');
const thumbnailInput = new TextInputBuilder().setCustomId('thumbnail_input').setLabel('URL del Thumbnail').setStyle(TextInputStyle.Short).setPlaceholder('https://ejemplo.com/thumbnail.png o dejar vacío para eliminar').setValue(textComp?.thumbnail || '').setMaxLength(2000).setRequired(false);
const row = new ActionRowBuilder().addComponents(thumbnailInput);
//@ts-ignore
modal.addComponents(row);
// Abrir modal directamente sin update previo
// @ts-ignore
@@ -497,6 +521,7 @@ export const command: CommandMessage = {
break;
}
case "edit_link_button": {
//@ts-ignore
const textDisplays = blockState.components.map((c: any, idx: number) => ({ c, idx })).filter(({ c }: any) => c.type === 10);
if (textDisplays.length === 0) {
await i.deferReply({ flags: 64 });
@@ -535,6 +560,7 @@ export const command: CommandMessage = {
const r1 = new ActionRowBuilder().addComponents(urlInput);
const r2 = new ActionRowBuilder().addComponents(labelInput);
const r3 = new ActionRowBuilder().addComponents(emojiInput);
//@ts-ignore
modal.addComponents(r1, r2, r3);
// Abrir modal directamente sobre el botón sin update previo
// @ts-ignore
@@ -556,6 +582,7 @@ export const command: CommandMessage = {
const r1 = new ActionRowBuilder().addComponents(urlInput);
const r2 = new ActionRowBuilder().addComponents(labelInput);
const r3 = new ActionRowBuilder().addComponents(emojiInput);
//@ts-ignore
modal.addComponents(r1, r2, r3);
// Abrir modal directamente sin update previo
// @ts-ignore

View File

@@ -319,7 +319,7 @@ export const command: CommandMessage = {
// Verificar que el bloque existe
const blockConfig = await client.prisma.blockV2Config.findFirst({
where: {
guildId: message.guildId,
guildId: message.guildId || undefined,
name: blockName
}
});

View File

@@ -1,21 +1,23 @@
import {ButtonInteraction, MessageFlags} from 'discord.js';
import {clearGlobalCommands} from '../../core/api/discordAPI';
import type { Button } from '../../core/types/components';
import type Amayo from '../../core/client';
const OWNER_ID = '327207082203938818';
let running = false;
export default {
customId: 'cmd_clear_global',
run: async (interaction: ButtonInteraction) => {
run: async (interaction: ButtonInteraction, client: Amayo) => {
if (interaction.user.id !== OWNER_ID) {
return interaction.reply({ content: '❌ No autorizado.', flags: MessageFlags.Ephemeral });
}
if (running) {
return interaction.reply({ content: '⏳ Limpieza GLOBAL en progreso, espera.', ephemeral: true });
return interaction.reply({ content: '⏳ Limpieza GLOBAL en progreso, espera.', flags: MessageFlags.Ephemeral });
}
running = true;
try {
await interaction.deferReply({ ephemeral: true });
await interaction.deferReply({ flags: MessageFlags.Ephemeral });
await clearGlobalCommands();
await interaction.editReply('🧹 Comandos GLOBAL eliminados.');
} catch (e: any) {
@@ -23,11 +25,10 @@ export default {
if (interaction.deferred || interaction.replied) {
await interaction.editReply('❌ Error limpiando comandos globales.');
} else {
await interaction.reply({ content: '❌ Error limpiando comandos globales.', ephemeral: true });
await interaction.reply({ content: '❌ Error limpiando comandos globales.', flags: MessageFlags.Ephemeral });
}
} finally {
running = false;
}
}
};
} satisfies Button;

View File

@@ -1,12 +1,14 @@
import {ButtonInteraction, MessageFlags} from 'discord.js';
import { clearAllCommands } from '../../core/api/discordAPI';
import type { Button } from '../../core/types/components';
import type Amayo from '../../core/client';
const OWNER_ID = '327207082203938818';
let running = false;
export default {
customId: 'cmd_clear_guild',
run: async (interaction: ButtonInteraction) => {
run: async (interaction: ButtonInteraction, client: Amayo) => {
if (interaction.user.id !== OWNER_ID) {
return interaction.reply({ content: '❌ No autorizado.', flags: MessageFlags.Ephemeral});
}
@@ -29,5 +31,4 @@ export default {
running = false;
}
}
};
} satisfies Button;

View File

@@ -1,12 +1,14 @@
import {ButtonInteraction, MessageFlags} from 'discord.js';
import { registeringCommands } from '../../core/api/discordAPI';
import type { Button } from '../../core/types/components';
import type Amayo from '../../core/client';
const OWNER_ID = '327207082203938818';
let running = false;
export default {
customId: 'cmd_reg_guild',
run: async (interaction: ButtonInteraction) => {
run: async (interaction: ButtonInteraction, client: Amayo) => {
if (interaction.user.id !== OWNER_ID) {
return interaction.reply({ content: '❌ No autorizado.', flags: MessageFlags.Ephemeral });
}
@@ -15,7 +17,7 @@ export default {
}
running = true;
try {
await interaction.deferReply({ flags: MessageFlags.Ephemeral});
await interaction.deferReply({ flags: MessageFlags.Ephemeral});
await registeringCommands();
await interaction.editReply('✅ Comandos de GUILD registrados correctamente.');
} catch (e: any) {
@@ -29,5 +31,4 @@ export default {
running = false;
}
}
};
} satisfies Button;

View File

@@ -1,10 +1,11 @@
import type {ButtonInteraction} from "discord.js";
//@ts-ignore
import { ActionRowBuilder, Events, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js'
import { ActionRowBuilder, ModalBuilder, TextInputBuilder, TextInputStyle } from 'discord.js';
import type { Button } from '../../core/types/components';
import type Amayo from '../../core/client';
export default {
customId: "prefixsettings",
run: async(interaction: ButtonInteraction) => {
run: async (interaction: ButtonInteraction, client: Amayo) => {
const modal = new ModalBuilder()
.setCustomId('prefixsettingsmodal')
.setTitle('Prefix');
@@ -14,9 +15,9 @@ export default {
.setLabel("Change Prefix")
.setStyle(TextInputStyle.Short);
const secondActionRow = new ActionRowBuilder().addComponents(prefixInput);
const secondActionRow = new ActionRowBuilder<TextInputBuilder>().addComponents(prefixInput);
modal.addComponents(secondActionRow);
await interaction.showModal(modal);
}
}
} satisfies Button;

View File

@@ -1,10 +1,32 @@
import {ModalSubmitInteraction} from "discord.js";
import {ModalSubmitInteraction, MessageFlags} from "discord.js";
import type { Modal } from '../../core/types/components';
import type Amayo from '../../core/client';
export default {
export default {
customId: "prefixsettingsmodal",
run: async (interaction: ModalSubmitInteraction) => {
const newPrefix = interaction.fields.getTextInputValue("prefixInput")
run: async (interaction: ModalSubmitInteraction, client: Amayo) => {
const newPrefix = interaction.fields.getTextInputValue("prefixInput");
if (!newPrefix || newPrefix.length > 10) {
return interaction.reply({
content: '❌ El prefix debe tener entre 1 y 10 caracteres.',
flags: MessageFlags.Ephemeral
});
}
try {
// Aquí puedes guardar el prefix en la base de datos usando client.prisma
// Por ahora solo confirmamos el cambio
await interaction.reply({
content: `✅ Prefix cambiado a: \`${newPrefix}\``,
flags: MessageFlags.Ephemeral
});
} catch (error) {
console.error('Error cambiando prefix:', error);
await interaction.reply({
content: '❌ Error al cambiar el prefix.',
flags: MessageFlags.Ephemeral
});
}
}
}
} satisfies Modal;

View File

@@ -1,14 +1,13 @@
// @ts-ignore
import { Client, GatewayIntentBits, Options, Partials } from 'discord.js';
// 1. Importa PrismaClient (singleton)
// @ts-ignore
import { prisma, ensurePrismaConnection } from './prisma';
process.loadEnvFile();
// Verificar si process.loadEnvFile existe (Node.js 20.6+)
if (typeof process.loadEnvFile === 'function') {
process.loadEnvFile();
}
class Amayo extends Client {
public key: string;
// 2. Propiedad prisma apuntando al singleton
public prisma = prisma;
constructor() {
@@ -18,11 +17,9 @@ class Amayo extends Client {
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent
// Eliminado GuildMessageTyping para reducir tráfico/memoria si no se usa
],
partials: [Partials.Channel, Partials.Message], // Permite recibir eventos sin cachear todo
partials: [Partials.Channel, Partials.Message],
makeCache: Options.cacheWithLimits({
// Limitar el tamaño de los managers más pesados
MessageManager: parseInt(process.env.CACHE_MESSAGES_LIMIT || '50', 10),
GuildMemberManager: parseInt(process.env.CACHE_MEMBERS_LIMIT || '100', 10),
ThreadManager: 10,
@@ -33,25 +30,24 @@ class Amayo extends Client {
}),
sweepers: {
messages: {
// Cada 5 min barrer mensajes más antiguos que 15 min (ajustable por env)
interval: parseInt(process.env.SWEEP_MESSAGES_INTERVAL_SECONDS || '300', 10),
lifetime: parseInt(process.env.SWEEP_MESSAGES_LIFETIME_SECONDS || '900', 10)
},
users: {
interval: 60 * 30, // cada 30 minutos
interval: 60 * 30,
filter: () => (user) => user.bot && user.id !== this.user?.id
}
},
rest: {
retries: 5 // bajar un poco para evitar colas largas en memoria
retries: 5
}
});
this.key = process.env.TOKEN ?? '';
}
async play () {
if(!this.key) {
async play() {
if (!this.key) {
console.error('No key provided');
throw new Error('Missing DISCORD TOKEN');
} else {
@@ -61,7 +57,7 @@ class Amayo extends Client {
await this.login(this.key);
} catch (error) {
console.error('Failed to connect to DB or login to Discord:', error);
throw error; // Propaga para que withRetry en main.ts reintente
throw error;
}
}
}

View File

@@ -1,11 +1,12 @@
import * as fs from "node:fs";
import * as path from "node:path";
import { Collection } from "discord.js";
import type { Button, Modal, SelectMenu, ContextMenu } from "./types/components";
export const buttons: Collection<string, any> = new Collection<string, any>();
export const modals = new Collection<string, any>();
export const selectmenus = new Collection<string, any>();
export const contextmenus = new Collection<string, any>();
export const buttons: Collection<string, Button> = new Collection<string, Button>();
export const modals: Collection<string, Modal> = new Collection<string, Modal>();
export const selectmenus: Collection<string, SelectMenu> = new Collection<string, SelectMenu>();
export const contextmenus: Collection<string, ContextMenu> = new Collection<string, ContextMenu>();
export function loadComponents(dir: string = path.join(__dirname, "..", "components")) {
const files = fs.readdirSync(dir);
@@ -21,29 +22,33 @@ export function loadComponents(dir: string = path.join(__dirname, "..", "compone
if (!file.endsWith(".ts") && !file.endsWith(".js")) continue;
const imported = require(fullPath);
const component = imported.default ?? imported;
try {
const imported = require(fullPath);
const component = imported.default ?? imported;
if (!component?.customId) {
console.warn(`⚠️ Archivo ignorado: ${file} (no tiene "customId")`);
continue;
}
if (!component?.customId) {
console.warn(`⚠️ Archivo ignorado: ${file} (no tiene "customId")`);
continue;
}
// Detectamos el tipo según la carpeta en la que está
if (fullPath.includes("buttons")) {
buttons.set(component.customId, component);
console.log(`🔘 Botón cargado: ${component.customId}`);
} else if (fullPath.includes("modals")) {
modals.set(component.customId, component);
console.log(`📄 Modal cargado: ${component.customId}`);
} else if (fullPath.includes("selectmenus")) {
selectmenus.set(component.customId, component);
console.log(`📜 SelectMenu cargado: ${component.customId}`);
} else if (fullPath.includes("contextmenu")) {
contextmenus.set(component.customId, component);
console.log(`📑 ContextMenu cargado: ${component.customId}`);
} else {
console.log(`⚠️ Componente desconocido: ${component.customId}`);
// Detectamos el tipo según la carpeta en la que está
if (fullPath.includes("buttons")) {
buttons.set(component.customId, component as Button);
console.log(`🔘 Botón cargado: ${component.customId}`);
} else if (fullPath.includes("modals")) {
modals.set(component.customId, component as Modal);
console.log(`📄 Modal cargado: ${component.customId}`);
} else if (fullPath.includes("selectmenus")) {
selectmenus.set(component.customId, component as SelectMenu);
console.log(`📜 SelectMenu cargado: ${component.customId}`);
} else if (fullPath.includes("contextmenu")) {
contextmenus.set(component.customId, component as ContextMenu);
console.log(`📑 ContextMenu cargado: ${component.customId}`);
} else {
console.log(`⚠️ Componente desconocido: ${component.customId}`);
}
} catch (error) {
console.error(`❌ Error cargando componente ${file}:`, error);
}
}
}

View File

@@ -0,0 +1,78 @@
// Sistema adicional de optimización de memoria para complementar el monitor existente
export interface MemoryOptimizerOptions {
forceGCInterval?: number; // minutos
maxHeapUsageBeforeGC?: number; // MB
logGCStats?: boolean;
}
export class MemoryOptimizer {
private gcTimer?: NodeJS.Timeout;
private options: Required<MemoryOptimizerOptions>;
constructor(options: MemoryOptimizerOptions = {}) {
this.options = {
forceGCInterval: options.forceGCInterval ?? 15, // cada 15 min por defecto
maxHeapUsageBeforeGC: options.maxHeapUsageBeforeGC ?? 200, // 200MB
logGCStats: options.logGCStats ?? false
};
}
start() {
// Solo habilitar si está disponible el GC manual
if (typeof global.gc !== 'function') {
console.warn('⚠️ Manual GC no disponible. Inicia con --expose-gc para habilitar optimizaciones adicionales.');
return;
}
// Timer para GC forzado periódico
if (this.options.forceGCInterval > 0) {
this.gcTimer = setInterval(() => {
this.performGC('scheduled');
}, this.options.forceGCInterval * 60 * 1000);
this.gcTimer.unref(); // No bloquear el cierre del proceso
}
console.log(`✅ Memory Optimizer iniciado - GC cada ${this.options.forceGCInterval}min, umbral: ${this.options.maxHeapUsageBeforeGC}MB`);
}
stop() {
if (this.gcTimer) {
clearInterval(this.gcTimer);
this.gcTimer = undefined;
}
}
// Método público para forzar GC cuando sea necesario
checkAndOptimize() {
const memUsage = process.memoryUsage();
const heapUsedMB = memUsage.heapUsed / 1024 / 1024;
if (heapUsedMB > this.options.maxHeapUsageBeforeGC) {
this.performGC('threshold');
return true;
}
return false;
}
private performGC(reason: string) {
if (typeof global.gc !== 'function') return;
const before = process.memoryUsage();
const startTime = Date.now();
global.gc();
if (this.options.logGCStats) {
const after = process.memoryUsage();
const duration = Date.now() - startTime;
const heapFreed = (before.heapUsed - after.heapUsed) / 1024 / 1024;
console.log(`🗑️ GC ${reason}: liberó ${heapFreed.toFixed(1)}MB en ${duration}ms`);
}
}
}
// Instancia singleton exportable
export const memoryOptimizer = new MemoryOptimizer();

8
src/core/types/block.ts Normal file
View File

@@ -0,0 +1,8 @@
export interface Block {
title?: string,
color?: any,
coverImage?: string,
icon?: string,
components?: any[],
}

View File

@@ -1,5 +1,5 @@
import type {ChatInputCommandInteraction, Client, Message} from "discord.js";
import Amayo from "../client";
import type {ChatInputCommandInteraction, Message} from "discord.js";
import type Amayo from "../client";
export interface CommandMessage {
name: string;
@@ -13,7 +13,7 @@ export interface CommandSlash {
name: string;
description: string;
type: 'slash';
options?: string[];
options?: any[];
cooldown?: number;
run: (i: ChatInputCommandInteraction, client: Client) => Promise<void>;
run: (i: ChatInputCommandInteraction, client: Amayo) => Promise<void>;
}

View File

@@ -1,7 +1,29 @@
import type {ButtonInteraction} from "discord.js";
import type {
ButtonInteraction,
ModalSubmitInteraction,
AnySelectMenuInteraction,
ContextMenuCommandInteraction
} from "discord.js";
import type Amayo from "../client";
export interface button {
export interface Button {
customId: string;
run: (interaction: ButtonInteraction) => Promise<void>;
run: (interaction: ButtonInteraction, client: Amayo) => Promise<unknown>;
}
export interface Modal {
customId: string;
run: (interaction: ModalSubmitInteraction, client: Amayo) => Promise<unknown>;
}
export interface SelectMenu {
customId: string;
run: (interaction: AnySelectMenuInteraction, client: Amayo) => Promise<unknown>;
}
export interface ContextMenu {
name: string;
type: 'USER' | 'MESSAGE';
run: (interaction: ContextMenuCommandInteraction, client: Amayo) => Promise<unknown>;
}

View File

@@ -10,10 +10,10 @@ bot.on(Events.MessageCreate, async (message) => {
await alliance(message);
const server = await bot.prisma.guild.upsert({
where: {
id: message.guildId
id: message.guildId || undefined
},
create: {
id: message.guildId,
id: message!.guildId || message.guild!.id,
name: message.guild!.name
},
update: {}

View File

@@ -4,7 +4,8 @@ import { loadEvents } from "./core/loaderEvents";
import { redis, redisConnect } from "./core/redis";
import { registeringCommands } from "./core/api/discordAPI";
import {loadComponents} from "./core/components";
import { startMemoryMonitor } from "./core/memoryMonitor"; // añadido
import { startMemoryMonitor } from "./core/memoryMonitor";
import {memoryOptimizer} from "./core/memoryOptimizer";
// Activar monitor de memoria si se define la variable
const __memInt = parseInt(process.env.MEMORY_LOG_INTERVAL_SECONDS || '0', 10);
@@ -12,11 +13,17 @@ if (__memInt > 0) {
startMemoryMonitor({ intervalSeconds: __memInt });
}
// Activar optimizador de memoria adicional
if (process.env.ENABLE_MEMORY_OPTIMIZER === 'true') {
memoryOptimizer.start();
}
export const bot = new Amayo();
// Listeners de robustez del cliente Discord
bot.on('error', (e) => console.error('🐞 Discord client error:', e));
bot.on('warn', (m) => console.warn('⚠️ Discord warn:', m));
// Evitar reintentos de re-login simultáneos
let relogging = false;
// Cuando la sesión es invalidada, intentamos reconectar/login
@@ -115,6 +122,9 @@ async function gracefulShutdown() {
shuttingDown = true;
console.log('🛑 Apagado controlado iniciado...');
try {
// Detener optimizador de memoria
memoryOptimizer.stop();
// Cerrar Redis si procede
try {
if (redis?.isOpen) {