feat: implement persistent memory for AI conversations and enhance message handling
This commit is contained in:
1
.env
1
.env
@@ -12,6 +12,7 @@ APPWRITE_API_KEY="standard_b123c1dbaaf7d3f99aa81492509d6277da0cb89eaf86bb8c42210
|
|||||||
APPWRITE_DATABASE_ID="68d8cb9a00250607e236"
|
APPWRITE_DATABASE_ID="68d8cb9a00250607e236"
|
||||||
APPWRITE_COLLECTION_REMINDERS_ID="reminders_id"
|
APPWRITE_COLLECTION_REMINDERS_ID="reminders_id"
|
||||||
REMINDERS_POLL_INTERVAL_SECONDS="30"
|
REMINDERS_POLL_INTERVAL_SECONDS="30"
|
||||||
|
APPWRITE_COLLECTION_AI_CONVERSATIONS_ID="aiconversation"
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# CONFIGURACIÓN DE DISCORD
|
# CONFIGURACIÓN DE DISCORD
|
||||||
|
|||||||
@@ -195,28 +195,59 @@ export const command: CommandMessage = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Emojis personalizados del servidor
|
// Indicador de que está escribiendo
|
||||||
const { names: emojiNames, map: emojiMap } = await getGuildCustomEmojis(message);
|
|
||||||
|
|
||||||
// Construir metadatos del mensaje para mejor contexto (incluye emojis)
|
|
||||||
const meta = buildMessageMeta(message, emojiNames);
|
|
||||||
|
|
||||||
// Indicador de escritura mejorado
|
|
||||||
const typingInterval = setInterval(() => {
|
const typingInterval = setInterval(() => {
|
||||||
channel.sendTyping().catch(() => {});
|
channel.sendTyping().catch(() => {});
|
||||||
}, 5000);
|
}, 5000);
|
||||||
|
|
||||||
try {
|
// Emojis personalizados del servidor
|
||||||
// Usar el servicio mejorado con manejo de prioridad
|
const { names: emojiNames, map: emojiMap } = await getGuildCustomEmojis(message);
|
||||||
const priority = message.member?.permissions.has('Administrator') ? 'high' : 'normal';
|
|
||||||
|
|
||||||
let aiResponse = await aiService.processAIRequest(
|
// Construir metadatos del mensaje para mejor contexto (incluye emojis)
|
||||||
userId,
|
const messageMeta = buildMessageMeta(message, emojiNames);
|
||||||
prompt,
|
|
||||||
guildId,
|
// Verificar si es una respuesta a un mensaje de la AI
|
||||||
priority,
|
let referencedMessageId: string | undefined;
|
||||||
{ meta }
|
let isReplyToAI = false;
|
||||||
);
|
|
||||||
|
if (message.reference?.messageId) {
|
||||||
|
try {
|
||||||
|
const referencedMessage = await message.channel.messages.fetch(message.reference.messageId);
|
||||||
|
if (referencedMessage.author.id === message.client.user?.id) {
|
||||||
|
isReplyToAI = true;
|
||||||
|
referencedMessageId = message.reference.messageId;
|
||||||
|
}
|
||||||
|
} catch (error) {
|
||||||
|
// Mensaje referenciado no encontrado, ignorar
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
let aiResponse: string;
|
||||||
|
|
||||||
|
// Usar el nuevo método con memoria persistente
|
||||||
|
if (isReplyToAI || true) { // Siempre usar memoria por ahora
|
||||||
|
aiResponse = await aiService.processAIRequestWithMemory(
|
||||||
|
userId,
|
||||||
|
prompt,
|
||||||
|
guildId,
|
||||||
|
message.channel.id,
|
||||||
|
message.id,
|
||||||
|
referencedMessageId,
|
||||||
|
message.client,
|
||||||
|
'normal',
|
||||||
|
{ meta: messageMeta }
|
||||||
|
);
|
||||||
|
} else {
|
||||||
|
// Método legacy para compatibilidad
|
||||||
|
aiResponse = await aiService.processAIRequest(
|
||||||
|
userId,
|
||||||
|
prompt,
|
||||||
|
guildId,
|
||||||
|
'normal',
|
||||||
|
{ meta: messageMeta }
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
// Reemplazar :nombre: por el tag real del emoji, evitando bloques de código
|
// Reemplazar :nombre: por el tag real del emoji, evitando bloques de código
|
||||||
if (emojiNames.length > 0) {
|
if (emojiNames.length > 0) {
|
||||||
|
|||||||
@@ -19,12 +19,12 @@ export default {
|
|||||||
try {
|
try {
|
||||||
await interaction.deferUpdate();
|
await interaction.deferUpdate();
|
||||||
|
|
||||||
// Limpiar cache de conversaciones
|
// Limpiar cache pero mantener memoria persistente
|
||||||
const stats = aiService.getStats();
|
const stats = aiService.getStats();
|
||||||
const conversationsCleared = stats.activeConversations;
|
const conversationsCleared = stats.activeConversations;
|
||||||
|
|
||||||
// Aquí iría la lógica real de limpieza:
|
// Usar el nuevo método que mantiene memoria persistente
|
||||||
// aiService.clearAllConversations();
|
aiService.clearCache();
|
||||||
|
|
||||||
// Crear panel de éxito usando objetos planos
|
// Crear panel de éxito usando objetos planos
|
||||||
const successPanel = {
|
const successPanel = {
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ export default {
|
|||||||
const conversationsCleared = statsBefore.activeConversations;
|
const conversationsCleared = statsBefore.activeConversations;
|
||||||
const requestsCleared = statsBefore.queueLength;
|
const requestsCleared = statsBefore.queueLength;
|
||||||
|
|
||||||
// Aquí irían las funciones reales de reset del servicio:
|
// Usar el nuevo método que mantiene memoria persistente
|
||||||
// aiService.fullReset();
|
aiService.fullReset();
|
||||||
|
|
||||||
const resetTimestamp = new Date().toISOString().replace('T', ' ').split('.')[0];
|
const resetTimestamp = new Date().toISOString().replace('T', ' ').split('.')[0];
|
||||||
|
|
||||||
|
|||||||
@@ -8,6 +8,7 @@ const apiKey = process.env.APPWRITE_API_KEY || '';
|
|||||||
|
|
||||||
export const APPWRITE_DATABASE_ID = process.env.APPWRITE_DATABASE_ID || '';
|
export const APPWRITE_DATABASE_ID = process.env.APPWRITE_DATABASE_ID || '';
|
||||||
export const APPWRITE_COLLECTION_REMINDERS_ID = process.env.APPWRITE_COLLECTION_REMINDERS_ID || '';
|
export const APPWRITE_COLLECTION_REMINDERS_ID = process.env.APPWRITE_COLLECTION_REMINDERS_ID || '';
|
||||||
|
export const APPWRITE_COLLECTION_AI_CONVERSATIONS_ID = process.env.APPWRITE_COLLECTION_AI_CONVERSATIONS_ID || '';
|
||||||
|
|
||||||
let client: Client | null = null;
|
let client: Client | null = null;
|
||||||
let databases: Databases | null = null;
|
let databases: Databases | null = null;
|
||||||
@@ -27,3 +28,7 @@ export function getDatabases(): Databases | null {
|
|||||||
export function isAppwriteConfigured(): boolean {
|
export function isAppwriteConfigured(): boolean {
|
||||||
return Boolean(endpoint && projectId && apiKey && APPWRITE_DATABASE_ID && APPWRITE_COLLECTION_REMINDERS_ID);
|
return Boolean(endpoint && projectId && apiKey && APPWRITE_DATABASE_ID && APPWRITE_COLLECTION_REMINDERS_ID);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export function isAIConversationsConfigured(): boolean {
|
||||||
|
return Boolean(endpoint && projectId && apiKey && APPWRITE_DATABASE_ID && APPWRITE_COLLECTION_AI_CONVERSATIONS_ID);
|
||||||
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } from "@google/ge
|
|||||||
import logger from "../lib/logger";
|
import logger from "../lib/logger";
|
||||||
import { Collection } from "discord.js";
|
import { Collection } from "discord.js";
|
||||||
import { prisma } from "../database/prisma";
|
import { prisma } from "../database/prisma";
|
||||||
|
import { getDatabases, APPWRITE_DATABASE_ID, APPWRITE_COLLECTION_AI_CONVERSATIONS_ID, isAIConversationsConfigured } from "../api/appwrite";
|
||||||
|
|
||||||
// Tipos mejorados para mejor type safety
|
// Tipos mejorados para mejor type safety
|
||||||
interface ConversationContext {
|
interface ConversationContext {
|
||||||
@@ -10,17 +11,22 @@ interface ConversationContext {
|
|||||||
content: string;
|
content: string;
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
tokens: number;
|
tokens: number;
|
||||||
|
messageId?: string; // ID del mensaje de Discord
|
||||||
|
referencedMessageId?: string; // ID del mensaje al que responde
|
||||||
}>;
|
}>;
|
||||||
totalTokens: number;
|
totalTokens: number;
|
||||||
imageRequests: number;
|
imageRequests: number;
|
||||||
lastActivity: number;
|
lastActivity: number;
|
||||||
userId: string;
|
userId: string;
|
||||||
guildId?: string;
|
guildId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
conversationId?: string; // ID único de la conversación
|
||||||
}
|
}
|
||||||
|
|
||||||
interface AIRequest {
|
interface AIRequest {
|
||||||
userId: string;
|
userId: string;
|
||||||
guildId?: string;
|
guildId?: string;
|
||||||
|
channelId?: string;
|
||||||
prompt: string;
|
prompt: string;
|
||||||
priority: 'low' | 'normal' | 'high';
|
priority: 'low' | 'normal' | 'high';
|
||||||
timestamp: number;
|
timestamp: number;
|
||||||
@@ -28,6 +34,24 @@ interface AIRequest {
|
|||||||
reject: (error: Error) => void;
|
reject: (error: Error) => void;
|
||||||
aiRolePrompt?: string;
|
aiRolePrompt?: string;
|
||||||
meta?: string;
|
meta?: string;
|
||||||
|
messageId?: string;
|
||||||
|
referencedMessageId?: string;
|
||||||
|
}
|
||||||
|
|
||||||
|
interface AppwriteConversation {
|
||||||
|
userId: string;
|
||||||
|
guildId?: string;
|
||||||
|
channelId?: string;
|
||||||
|
conversationId: string;
|
||||||
|
messages: Array<{
|
||||||
|
role: 'user' | 'assistant';
|
||||||
|
content: string;
|
||||||
|
timestamp: number;
|
||||||
|
messageId?: string;
|
||||||
|
referencedMessageId?: string;
|
||||||
|
}>;
|
||||||
|
lastActivity: number;
|
||||||
|
createdAt: number;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Utility function para manejar errores de forma type-safe
|
// Utility function para manejar errores de forma type-safe
|
||||||
@@ -39,7 +63,7 @@ function getErrorMessage(error: unknown): string {
|
|||||||
return error;
|
return error;
|
||||||
}
|
}
|
||||||
if (error && typeof error === 'object' && 'message' in error) {
|
if (error && typeof error === 'object' && 'message' in error) {
|
||||||
return String(error.message);
|
return String((error as any).message);
|
||||||
}
|
}
|
||||||
return 'Error desconocido';
|
return 'Error desconocido';
|
||||||
}
|
}
|
||||||
@@ -225,13 +249,172 @@ export class AIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Procesa una request individual con manejo completo de errores
|
* Resetear conversación
|
||||||
|
*/
|
||||||
|
private resetConversation(userId: string, guildId?: string): void {
|
||||||
|
const key = `${userId}-${guildId || 'dm'}`;
|
||||||
|
this.conversations.delete(key);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Servicio de limpieza automática
|
||||||
|
*/
|
||||||
|
private startCleanupService(): void {
|
||||||
|
setInterval(() => {
|
||||||
|
const now = Date.now();
|
||||||
|
const toDelete: string[] = [];
|
||||||
|
|
||||||
|
this.conversations.forEach((context, key) => {
|
||||||
|
if (now - context.lastActivity > this.config.maxConversationAge) {
|
||||||
|
toDelete.push(key);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
toDelete.forEach(key => this.conversations.delete(key));
|
||||||
|
|
||||||
|
if (toDelete.length > 0) {
|
||||||
|
logger.info(`Limpieza automática: ${toDelete.length} conversaciones expiradas eliminadas`);
|
||||||
|
}
|
||||||
|
}, this.config.cleanupInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser mejorado de errores de API - Type-safe sin ts-ignore
|
||||||
|
*/
|
||||||
|
private parseAPIError(error: unknown): string {
|
||||||
|
// Extraer mensaje de forma type-safe
|
||||||
|
const message = getErrorMessage(error).toLowerCase();
|
||||||
|
|
||||||
|
// Verificar si es un error de API estructurado
|
||||||
|
if (isAPIError(error)) {
|
||||||
|
const apiMessage = error.message.toLowerCase();
|
||||||
|
|
||||||
|
if (apiMessage.includes('api key') || apiMessage.includes('authentication')) {
|
||||||
|
return 'Error de autenticación con la API de IA';
|
||||||
|
}
|
||||||
|
if (apiMessage.includes('quota') || apiMessage.includes('exceeded')) {
|
||||||
|
return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
|
||||||
|
}
|
||||||
|
if (apiMessage.includes('safety') || apiMessage.includes('blocked')) {
|
||||||
|
return 'Tu mensaje fue bloqueado por las políticas de seguridad';
|
||||||
|
}
|
||||||
|
if (apiMessage.includes('timeout') || apiMessage.includes('deadline')) {
|
||||||
|
return 'La solicitud tardó demasiado tiempo. Intenta de nuevo';
|
||||||
|
}
|
||||||
|
if (apiMessage.includes('model not found')) {
|
||||||
|
return 'El modelo de IA no está disponible en este momento';
|
||||||
|
}
|
||||||
|
if (apiMessage.includes('token') || apiMessage.includes('length')) {
|
||||||
|
return 'El mensaje excede los límites permitidos';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Manejo genérico para otros tipos de errores
|
||||||
|
if (message.includes('api key') || message.includes('authentication')) {
|
||||||
|
return 'Error de autenticación con la API de IA';
|
||||||
|
}
|
||||||
|
if (message.includes('quota') || message.includes('exceeded')) {
|
||||||
|
return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
|
||||||
|
}
|
||||||
|
if (message.includes('safety') || message.includes('blocked')) {
|
||||||
|
return 'Tu mensaje fue bloqueado por las políticas de seguridad';
|
||||||
|
}
|
||||||
|
if (message.includes('timeout') || message.includes('deadline')) {
|
||||||
|
return 'La solicitud tardó demasiado tiempo. Intenta de nuevo';
|
||||||
|
}
|
||||||
|
if (message.includes('model not found')) {
|
||||||
|
return 'El modelo de IA no está disponible en este momento';
|
||||||
|
}
|
||||||
|
if (message.includes('token') || message.includes('length')) {
|
||||||
|
return 'El mensaje excede los límites permitidos';
|
||||||
|
}
|
||||||
|
|
||||||
|
return 'Error temporal del servicio de IA. Intenta de nuevo';
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Procesa una request de IA con soporte para conversaciones y memoria persistente
|
||||||
|
*/
|
||||||
|
async processAIRequestWithMemory(
|
||||||
|
userId: string,
|
||||||
|
prompt: string,
|
||||||
|
guildId?: string,
|
||||||
|
channelId?: string,
|
||||||
|
messageId?: string,
|
||||||
|
referencedMessageId?: string,
|
||||||
|
client?: any,
|
||||||
|
priority: 'low' | 'normal' | 'high' = 'normal',
|
||||||
|
options?: { aiRolePrompt?: string; meta?: string }
|
||||||
|
): Promise<string> {
|
||||||
|
// Validaciones exhaustivas
|
||||||
|
if (!prompt?.trim()) {
|
||||||
|
throw new Error('El prompt no puede estar vacío');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (prompt.length > 4000) {
|
||||||
|
throw new Error('El prompt excede el límite de 4000 caracteres');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Rate limiting por usuario
|
||||||
|
if (!this.checkRateLimit(userId)) {
|
||||||
|
throw new Error('Has excedido el límite de requests. Espera un momento.');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Cooldown entre requests
|
||||||
|
const lastRequest = this.userCooldowns.get(userId) || 0;
|
||||||
|
const timeSinceLastRequest = Date.now() - lastRequest;
|
||||||
|
|
||||||
|
if (timeSinceLastRequest < this.config.cooldownMs) {
|
||||||
|
const waitTime = Math.ceil((this.config.cooldownMs - timeSinceLastRequest) / 1000);
|
||||||
|
throw new Error(`Debes esperar ${waitTime} segundos antes de hacer otra consulta`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Agregar a la queue con Promise
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
const request: AIRequest = {
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
channelId,
|
||||||
|
prompt: prompt.trim(),
|
||||||
|
priority,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
aiRolePrompt: options?.aiRolePrompt,
|
||||||
|
meta: options?.meta,
|
||||||
|
messageId,
|
||||||
|
referencedMessageId,
|
||||||
|
};
|
||||||
|
|
||||||
|
// Insertar según prioridad
|
||||||
|
if (priority === 'high') {
|
||||||
|
this.requestQueue.unshift(request);
|
||||||
|
} else {
|
||||||
|
this.requestQueue.push(request);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Timeout automático
|
||||||
|
setTimeout(() => {
|
||||||
|
const index = this.requestQueue.findIndex(r => r === request);
|
||||||
|
if (index !== -1) {
|
||||||
|
this.requestQueue.splice(index, 1);
|
||||||
|
reject(new Error('Request timeout: La solicitud tardó demasiado tiempo'));
|
||||||
|
}
|
||||||
|
}, this.config.requestTimeout);
|
||||||
|
|
||||||
|
this.userCooldowns.set(userId, Date.now());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Procesa una request individual con manejo completo de errores y memoria persistente
|
||||||
*/
|
*/
|
||||||
private async processRequest(request: AIRequest): Promise<void> {
|
private async processRequest(request: AIRequest): Promise<void> {
|
||||||
try {
|
try {
|
||||||
const { userId, prompt, guildId } = request;
|
const { userId, prompt, guildId, channelId, messageId, referencedMessageId } = request;
|
||||||
const context = this.getOrCreateContext(userId, guildId);
|
const context = await this.getOrCreateContextWithMemory(userId, guildId, channelId);
|
||||||
const isImageRequest = this.detectImageRequest(prompt);
|
const isImageRequest = this.detectImageRequest(prompt);
|
||||||
|
|
||||||
if (isImageRequest && context.imageRequests >= this.config.maxImageRequests) {
|
if (isImageRequest && context.imageRequests >= this.config.maxImageRequests) {
|
||||||
const error = new Error(`Has alcanzado el límite de ${this.config.maxImageRequests} solicitudes de imagen. La conversación se ha reiniciado.`);
|
const error = new Error(`Has alcanzado el límite de ${this.config.maxImageRequests} solicitudes de imagen. La conversación se ha reiniciado.`);
|
||||||
request.reject(error);
|
request.reject(error);
|
||||||
@@ -241,7 +424,7 @@ export class AIService {
|
|||||||
// Verificar límites de tokens
|
// Verificar límites de tokens
|
||||||
const estimatedTokens = this.estimateTokens(prompt);
|
const estimatedTokens = this.estimateTokens(prompt);
|
||||||
if (context.totalTokens + estimatedTokens > this.config.maxInputTokens * this.config.tokenResetThreshold) {
|
if (context.totalTokens + estimatedTokens > this.config.maxInputTokens * this.config.tokenResetThreshold) {
|
||||||
this.resetConversation(userId);
|
this.resetConversation(userId, guildId);
|
||||||
logger.info(`Conversación reseteada para usuario ${userId} por límite de tokens`);
|
logger.info(`Conversación reseteada para usuario ${userId} por límite de tokens`);
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -251,13 +434,26 @@ export class AIService {
|
|||||||
effectiveAiRolePrompt = (await this.getGuildAiPrompt(guildId)) ?? undefined;
|
effectiveAiRolePrompt = (await this.getGuildAiPrompt(guildId)) ?? undefined;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Obtener jerarquía de roles si está en un servidor
|
||||||
|
let roleHierarchy = '';
|
||||||
|
if (guildId) {
|
||||||
|
// Necesitamos acceso al cliente de Discord - lo pasaremos desde el comando
|
||||||
|
const client = (request as any).client;
|
||||||
|
if (client) {
|
||||||
|
roleHierarchy = await this.getGuildRoleHierarchy(guildId, client);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construir metadatos mejorados
|
||||||
|
const enhancedMeta = (request.meta || '') + roleHierarchy;
|
||||||
|
|
||||||
// Construir prompt del sistema optimizado
|
// Construir prompt del sistema optimizado
|
||||||
const systemPrompt = this.buildSystemPrompt(
|
const systemPrompt = this.buildSystemPrompt(
|
||||||
prompt,
|
prompt,
|
||||||
context,
|
context,
|
||||||
isImageRequest,
|
isImageRequest,
|
||||||
effectiveAiRolePrompt,
|
effectiveAiRolePrompt,
|
||||||
request.meta
|
enhancedMeta
|
||||||
);
|
);
|
||||||
|
|
||||||
// Usar la API correcta de Google Generative AI
|
// Usar la API correcta de Google Generative AI
|
||||||
@@ -291,8 +487,16 @@ export class AIService {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Actualizar contexto de forma eficiente
|
// Actualizar contexto con memoria persistente
|
||||||
this.updateContext(context, prompt, aiResponse, estimatedTokens, isImageRequest);
|
await this.updateContextWithMemory(
|
||||||
|
context,
|
||||||
|
prompt,
|
||||||
|
aiResponse,
|
||||||
|
estimatedTokens,
|
||||||
|
isImageRequest,
|
||||||
|
messageId,
|
||||||
|
referencedMessageId
|
||||||
|
);
|
||||||
|
|
||||||
request.resolve(aiResponse);
|
request.resolve(aiResponse);
|
||||||
|
|
||||||
@@ -404,7 +608,7 @@ Responde de forma directa y útil:`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtener o crear contexto de conversación
|
* Obtener o crear contexto de conversación (método legacy)
|
||||||
*/
|
*/
|
||||||
private getOrCreateContext(userId: string, guildId?: string): ConversationContext {
|
private getOrCreateContext(userId: string, guildId?: string): ConversationContext {
|
||||||
const key = `${userId}-${guildId || 'dm'}`;
|
const key = `${userId}-${guildId || 'dm'}`;
|
||||||
@@ -427,7 +631,7 @@ Responde de forma directa y útil:`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Actualizar contexto de forma eficiente
|
* Actualizar contexto de forma eficiente (método legacy)
|
||||||
*/
|
*/
|
||||||
private updateContext(
|
private updateContext(
|
||||||
context: ConversationContext,
|
context: ConversationContext,
|
||||||
@@ -461,87 +665,109 @@ Responde de forma directa y útil:`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Resetear conversación
|
* Actualizar contexto de forma eficiente con guardado en Appwrite
|
||||||
*/
|
*/
|
||||||
private resetConversation(userId: string, guildId?: string): void {
|
private async updateContextWithMemory(
|
||||||
|
context: ConversationContext,
|
||||||
|
userPrompt: string,
|
||||||
|
aiResponse: string,
|
||||||
|
inputTokens: number,
|
||||||
|
isImageRequest: boolean,
|
||||||
|
messageId?: string,
|
||||||
|
referencedMessageId?: string
|
||||||
|
): Promise<void> {
|
||||||
|
const outputTokens = this.estimateTokens(aiResponse);
|
||||||
|
const now = Date.now();
|
||||||
|
|
||||||
|
// Agregar mensajes con IDs de Discord
|
||||||
|
context.messages.push(
|
||||||
|
{
|
||||||
|
role: 'user',
|
||||||
|
content: userPrompt,
|
||||||
|
timestamp: now,
|
||||||
|
tokens: inputTokens,
|
||||||
|
messageId,
|
||||||
|
referencedMessageId
|
||||||
|
},
|
||||||
|
{
|
||||||
|
role: 'assistant',
|
||||||
|
content: aiResponse,
|
||||||
|
timestamp: now,
|
||||||
|
tokens: outputTokens
|
||||||
|
}
|
||||||
|
);
|
||||||
|
|
||||||
|
// Mantener solo los mensajes más recientes
|
||||||
|
if (context.messages.length > this.config.maxMessageHistory) {
|
||||||
|
const removed = context.messages.splice(0, context.messages.length - this.config.maxMessageHistory);
|
||||||
|
const removedTokens = removed.reduce((sum, msg) => sum + msg.tokens, 0);
|
||||||
|
context.totalTokens -= removedTokens;
|
||||||
|
}
|
||||||
|
|
||||||
|
context.totalTokens += inputTokens + outputTokens;
|
||||||
|
context.lastActivity = now;
|
||||||
|
|
||||||
|
if (isImageRequest) {
|
||||||
|
context.imageRequests++;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Guardar en Appwrite de forma asíncrona
|
||||||
|
this.saveConversationToAppwrite(context).catch(error => {
|
||||||
|
logger.warn(`Error guardando conversación: ${getErrorMessage(error)}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener o crear contexto de conversación con carga desde Appwrite
|
||||||
|
*/
|
||||||
|
private async getOrCreateContextWithMemory(userId: string, guildId?: string, channelId?: string): Promise<ConversationContext> {
|
||||||
const key = `${userId}-${guildId || 'dm'}`;
|
const key = `${userId}-${guildId || 'dm'}`;
|
||||||
this.conversations.delete(key);
|
let context = this.conversations.get(key);
|
||||||
|
|
||||||
|
if (!context) {
|
||||||
|
// Intentar cargar desde Appwrite
|
||||||
|
const loadedContext = await this.loadConversationFromAppwrite(userId, guildId, channelId);
|
||||||
|
|
||||||
|
if (loadedContext) {
|
||||||
|
context = loadedContext;
|
||||||
|
} else {
|
||||||
|
// Crear nuevo contexto si no existe en Appwrite
|
||||||
|
context = {
|
||||||
|
messages: [],
|
||||||
|
totalTokens: 0,
|
||||||
|
imageRequests: 0,
|
||||||
|
lastActivity: Date.now(),
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
channelId
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
this.conversations.set(key, context);
|
||||||
|
}
|
||||||
|
|
||||||
|
context.lastActivity = Date.now();
|
||||||
|
return context;
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Servicio de limpieza automática
|
* Limpiar cache pero mantener memoria persistente
|
||||||
*/
|
*/
|
||||||
private startCleanupService(): void {
|
public clearCache(): void {
|
||||||
setInterval(() => {
|
this.conversations.clear();
|
||||||
const now = Date.now();
|
this.userCooldowns.clear();
|
||||||
const toDelete: string[] = [];
|
this.rateLimitTracker.clear();
|
||||||
|
this.guildPromptCache.clear();
|
||||||
this.conversations.forEach((context, key) => {
|
logger.info('Cache de AI limpiado, memoria persistente mantenida');
|
||||||
if (now - context.lastActivity > this.config.maxConversationAge) {
|
|
||||||
toDelete.push(key);
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
toDelete.forEach(key => this.conversations.delete(key));
|
|
||||||
|
|
||||||
if (toDelete.length > 0) {
|
|
||||||
logger.info(`Limpieza automática: ${toDelete.length} conversaciones expiradas eliminadas`);
|
|
||||||
}
|
|
||||||
}, this.config.cleanupInterval);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parser mejorado de errores de API - Type-safe sin ts-ignore
|
* Reset completo pero mantener memoria persistente
|
||||||
*/
|
*/
|
||||||
private parseAPIError(error: unknown): string {
|
public fullReset(): void {
|
||||||
// Extraer mensaje de forma type-safe
|
this.clearCache();
|
||||||
const message = getErrorMessage(error).toLowerCase();
|
this.requestQueue.length = 0;
|
||||||
|
logger.info('AI completamente reseteada, memoria persistente mantenida');
|
||||||
// Verificar si es un error de API estructurado
|
|
||||||
if (isAPIError(error)) {
|
|
||||||
const apiMessage = error.message.toLowerCase();
|
|
||||||
|
|
||||||
if (apiMessage.includes('api key') || apiMessage.includes('authentication')) {
|
|
||||||
return 'Error de autenticación con la API de IA';
|
|
||||||
}
|
|
||||||
if (apiMessage.includes('quota') || apiMessage.includes('exceeded')) {
|
|
||||||
return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
|
|
||||||
}
|
|
||||||
if (apiMessage.includes('safety') || apiMessage.includes('blocked')) {
|
|
||||||
return 'Tu mensaje fue bloqueado por las políticas de seguridad';
|
|
||||||
}
|
|
||||||
if (apiMessage.includes('timeout') || apiMessage.includes('deadline')) {
|
|
||||||
return 'La solicitud tardó demasiado tiempo. Intenta de nuevo';
|
|
||||||
}
|
|
||||||
if (apiMessage.includes('model not found')) {
|
|
||||||
return 'El modelo de IA no está disponible en este momento';
|
|
||||||
}
|
|
||||||
if (apiMessage.includes('token') || apiMessage.includes('length')) {
|
|
||||||
return 'El mensaje excede los límites permitidos';
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Manejo genérico para otros tipos de errores
|
|
||||||
if (message.includes('api key') || message.includes('authentication')) {
|
|
||||||
return 'Error de autenticación con la API de IA';
|
|
||||||
}
|
|
||||||
if (message.includes('quota') || message.includes('exceeded')) {
|
|
||||||
return 'Se ha alcanzado el límite de uso de la API. Intenta más tarde';
|
|
||||||
}
|
|
||||||
if (message.includes('safety') || message.includes('blocked')) {
|
|
||||||
return 'Tu mensaje fue bloqueado por las políticas de seguridad';
|
|
||||||
}
|
|
||||||
if (message.includes('timeout') || message.includes('deadline')) {
|
|
||||||
return 'La solicitud tardó demasiado tiempo. Intenta de nuevo';
|
|
||||||
}
|
|
||||||
if (message.includes('model not found')) {
|
|
||||||
return 'El modelo de IA no está disponible en este momento';
|
|
||||||
}
|
|
||||||
if (message.includes('token') || message.includes('length')) {
|
|
||||||
return 'El mensaje excede los límites permitidos';
|
|
||||||
}
|
|
||||||
|
|
||||||
return 'Error temporal del servicio de IA. Intenta de nuevo';
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -560,6 +786,137 @@ Responde de forma directa y útil:`;
|
|||||||
averageResponseTime: 0
|
averageResponseTime: 0
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Guardar conversación en Appwrite para memoria persistente
|
||||||
|
*/
|
||||||
|
private async saveConversationToAppwrite(context: ConversationContext): Promise<void> {
|
||||||
|
if (!isAIConversationsConfigured()) {
|
||||||
|
return; // Si no está configurado, no guardamos
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const databases = getDatabases();
|
||||||
|
if (!databases) return;
|
||||||
|
|
||||||
|
const conversationId = context.conversationId || `${context.userId}-${context.guildId || 'dm'}-${Date.now()}`;
|
||||||
|
context.conversationId = conversationId;
|
||||||
|
|
||||||
|
const appwriteData: AppwriteConversation = {
|
||||||
|
userId: context.userId,
|
||||||
|
guildId: context.guildId,
|
||||||
|
channelId: context.channelId,
|
||||||
|
conversationId,
|
||||||
|
messages: context.messages.map(msg => ({
|
||||||
|
role: msg.role,
|
||||||
|
content: msg.content,
|
||||||
|
timestamp: msg.timestamp,
|
||||||
|
messageId: msg.messageId,
|
||||||
|
referencedMessageId: msg.referencedMessageId
|
||||||
|
})),
|
||||||
|
lastActivity: context.lastActivity,
|
||||||
|
createdAt: Date.now()
|
||||||
|
};
|
||||||
|
|
||||||
|
await databases.createDocument(
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_COLLECTION_AI_CONVERSATIONS_ID,
|
||||||
|
conversationId,
|
||||||
|
appwriteData
|
||||||
|
);
|
||||||
|
|
||||||
|
logger.debug(`Conversación guardada en Appwrite: ${conversationId}`);
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Error guardando conversación en Appwrite: ${getErrorMessage(error)}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Cargar conversación desde Appwrite
|
||||||
|
*/
|
||||||
|
private async loadConversationFromAppwrite(userId: string, guildId?: string, channelId?: string): Promise<ConversationContext | null> {
|
||||||
|
if (!isAIConversationsConfigured()) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
const databases = getDatabases();
|
||||||
|
if (!databases) return null;
|
||||||
|
|
||||||
|
// Buscar conversaciones recientes del usuario
|
||||||
|
const response = await databases.listDocuments(
|
||||||
|
APPWRITE_DATABASE_ID,
|
||||||
|
APPWRITE_COLLECTION_AI_CONVERSATIONS_ID,
|
||||||
|
[
|
||||||
|
`userId=${userId}`,
|
||||||
|
guildId ? `guildId=${guildId}` : '',
|
||||||
|
channelId ? `channelId=${channelId}` : ''
|
||||||
|
].filter(Boolean)
|
||||||
|
);
|
||||||
|
|
||||||
|
if (response.documents.length === 0) {
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Obtener la conversación más reciente
|
||||||
|
const latestDoc = response.documents.sort((a: any, b: any) => b.lastActivity - a.lastActivity)[0];
|
||||||
|
const data = latestDoc as any as AppwriteConversation;
|
||||||
|
|
||||||
|
// Crear contexto desde los datos de Appwrite
|
||||||
|
const context: ConversationContext = {
|
||||||
|
messages: data.messages.map(msg => ({
|
||||||
|
...msg,
|
||||||
|
tokens: this.estimateTokens(msg.content)
|
||||||
|
})),
|
||||||
|
totalTokens: data.messages.reduce((sum, msg) => sum + this.estimateTokens(msg.content), 0),
|
||||||
|
imageRequests: 0, // Resetear conteo de imágenes
|
||||||
|
lastActivity: data.lastActivity,
|
||||||
|
userId: data.userId,
|
||||||
|
guildId: data.guildId,
|
||||||
|
channelId: data.channelId,
|
||||||
|
conversationId: data.conversationId
|
||||||
|
};
|
||||||
|
|
||||||
|
logger.debug(`Conversación cargada desde Appwrite: ${data.conversationId}`);
|
||||||
|
return context;
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Error cargando conversación desde Appwrite: ${getErrorMessage(error)}`);
|
||||||
|
return null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtener jerarquía de roles de un servidor
|
||||||
|
*/
|
||||||
|
private async getGuildRoleHierarchy(guildId: string, client: any): Promise<string> {
|
||||||
|
try {
|
||||||
|
const guild = await client.guilds.fetch(guildId);
|
||||||
|
if (!guild) return '';
|
||||||
|
|
||||||
|
const roles = await guild.roles.fetch();
|
||||||
|
const sortedRoles = roles
|
||||||
|
.filter((role: any) => role.id !== guild.id) // Excluir @everyone
|
||||||
|
.sort((a: any, b: any) => b.position - a.position)
|
||||||
|
.map((role: any) => {
|
||||||
|
const permissions = [];
|
||||||
|
if (role.permissions.has('Administrator')) permissions.push('Admin');
|
||||||
|
if (role.permissions.has('ManageGuild')) permissions.push('Manage Server');
|
||||||
|
if (role.permissions.has('ManageChannels')) permissions.push('Manage Channels');
|
||||||
|
if (role.permissions.has('ManageMessages')) permissions.push('Manage Messages');
|
||||||
|
if (role.permissions.has('ModerateMembers')) permissions.push('Moderate Members');
|
||||||
|
|
||||||
|
const permStr = permissions.length > 0 ? ` (${permissions.join(', ')})` : '';
|
||||||
|
return `- ${role.name}${permStr}`;
|
||||||
|
})
|
||||||
|
.slice(0, 15) // Limitar a 15 roles principales
|
||||||
|
.join('\n');
|
||||||
|
|
||||||
|
return sortedRoles ? `\n## Jerarquía de roles del servidor:\n${sortedRoles}\n` : '';
|
||||||
|
} catch (error) {
|
||||||
|
logger.warn(`Error obteniendo jerarquía de roles: ${getErrorMessage(error)}`);
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instancia singleton
|
// Instancia singleton
|
||||||
|
|||||||
@@ -4,10 +4,186 @@ import {redis} from "../core/database/redis";
|
|||||||
import {commands} from "../core/loaders/loader";
|
import {commands} from "../core/loaders/loader";
|
||||||
import {alliance} from "./extras/alliace";
|
import {alliance} from "./extras/alliace";
|
||||||
import logger from "../core/lib/logger";
|
import logger from "../core/lib/logger";
|
||||||
|
import { aiService } from "../core/services/AIService";
|
||||||
|
|
||||||
|
// Función para manejar respuestas automáticas a la AI
|
||||||
|
async function handleAIReply(message: any) {
|
||||||
|
// Verificar si es una respuesta a un mensaje del bot
|
||||||
|
if (!message.reference?.messageId || message.author.bot) return;
|
||||||
|
|
||||||
|
try {
|
||||||
|
const referencedMessage = await message.channel.messages.fetch(message.reference.messageId);
|
||||||
|
|
||||||
|
// Verificar si el mensaje referenciado es del bot
|
||||||
|
if (referencedMessage.author.id !== message.client.user?.id) return;
|
||||||
|
|
||||||
|
// Verificar que el contenido no sea un comando (para evitar loops)
|
||||||
|
const server = await bot.prisma.guild.findUnique({
|
||||||
|
where: { id: message.guildId || undefined }
|
||||||
|
});
|
||||||
|
const PREFIX = server?.prefix || "!";
|
||||||
|
|
||||||
|
if (message.content.startsWith(PREFIX)) return;
|
||||||
|
|
||||||
|
// Verificar que el mensaje tenga contenido válido
|
||||||
|
if (!message.content || message.content.trim().length === 0) return;
|
||||||
|
|
||||||
|
// Limitar longitud del mensaje
|
||||||
|
if (message.content.length > 4000) {
|
||||||
|
await message.reply('❌ **Error:** Tu mensaje es demasiado largo (máximo 4000 caracteres).');
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
logger.info(`Respuesta automática a AI detectada - Usuario: ${message.author.id}, Guild: ${message.guildId}`);
|
||||||
|
|
||||||
|
// Indicador de que está escribiendo
|
||||||
|
const typingInterval = setInterval(() => {
|
||||||
|
message.channel.sendTyping().catch(() => {});
|
||||||
|
}, 5000);
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Obtener emojis personalizados del servidor
|
||||||
|
const emojiResult = { names: [] as string[], map: {} as Record<string, string> };
|
||||||
|
try {
|
||||||
|
const guild = message.guild;
|
||||||
|
if (guild) {
|
||||||
|
const emojis = await guild.emojis.fetch();
|
||||||
|
const list = Array.from(emojis.values());
|
||||||
|
for (const e of list) {
|
||||||
|
// @ts-ignore
|
||||||
|
const name = e.name;
|
||||||
|
// @ts-ignore
|
||||||
|
const id = e.id;
|
||||||
|
if (!name || !id) continue;
|
||||||
|
// @ts-ignore
|
||||||
|
const tag = e.animated ? `<a:${name}:${id}>` : `<:${name}:${id}>`;
|
||||||
|
if (!(name in emojiResult.map)) {
|
||||||
|
emojiResult.map[name] = tag;
|
||||||
|
emojiResult.names.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
emojiResult.names = emojiResult.names.slice(0, 25);
|
||||||
|
}
|
||||||
|
} catch {
|
||||||
|
// Ignorar errores de emojis
|
||||||
|
}
|
||||||
|
|
||||||
|
// Construir metadatos del mensaje
|
||||||
|
const buildMessageMeta = (msg: any, emojiNames?: string[]): string => {
|
||||||
|
try {
|
||||||
|
const parts: string[] = [];
|
||||||
|
|
||||||
|
if (msg.channel?.name) {
|
||||||
|
parts.push(`Canal: #${msg.channel.name}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const userMentions = msg.mentions?.users ? Array.from(msg.mentions.users.values()) : [];
|
||||||
|
const roleMentions = msg.mentions?.roles ? Array.from(msg.mentions.roles.values()) : [];
|
||||||
|
|
||||||
|
if (userMentions.length) {
|
||||||
|
parts.push(`Menciones usuario: ${userMentions.slice(0, 5).map((u: any) => u.username ?? u.tag ?? u.id).join(', ')}`);
|
||||||
|
}
|
||||||
|
if (roleMentions.length) {
|
||||||
|
parts.push(`Menciones rol: ${roleMentions.slice(0, 5).map((r: any) => r.name ?? r.id).join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (msg.reference?.messageId) {
|
||||||
|
parts.push('Es una respuesta a mensaje de AI');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (emojiNames && emojiNames.length) {
|
||||||
|
parts.push(`Emojis personalizados disponibles (usa :nombre:): ${emojiNames.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
|
const metaRaw = parts.join(' | ');
|
||||||
|
return metaRaw.length > 800 ? metaRaw.slice(0, 800) : metaRaw;
|
||||||
|
} catch {
|
||||||
|
return '';
|
||||||
|
}
|
||||||
|
};
|
||||||
|
|
||||||
|
const messageMeta = buildMessageMeta(message, emojiResult.names);
|
||||||
|
|
||||||
|
// Procesar con el servicio de AI usando memoria persistente
|
||||||
|
const aiResponse = await aiService.processAIRequestWithMemory(
|
||||||
|
message.author.id,
|
||||||
|
message.content,
|
||||||
|
message.guildId,
|
||||||
|
message.channel.id,
|
||||||
|
message.id,
|
||||||
|
message.reference.messageId,
|
||||||
|
message.client,
|
||||||
|
'normal',
|
||||||
|
{ meta: messageMeta }
|
||||||
|
);
|
||||||
|
|
||||||
|
// Reemplazar emojis personalizados
|
||||||
|
let finalResponse = aiResponse;
|
||||||
|
if (emojiResult.names.length > 0) {
|
||||||
|
finalResponse = finalResponse.replace(/:([a-zA-Z0-9_]{2,32}):/g, (match, p1: string) => {
|
||||||
|
const found = emojiResult.map[p1];
|
||||||
|
return found ? found : match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
// Enviar respuesta (dividir si es muy larga)
|
||||||
|
const MAX_CONTENT = 2000;
|
||||||
|
if (finalResponse.length > MAX_CONTENT) {
|
||||||
|
const chunks = [];
|
||||||
|
let currentChunk = '';
|
||||||
|
const lines = finalResponse.split('\n');
|
||||||
|
|
||||||
|
for (const line of lines) {
|
||||||
|
if (currentChunk.length + line.length + 1 > MAX_CONTENT) {
|
||||||
|
if (currentChunk) {
|
||||||
|
chunks.push(currentChunk.trim());
|
||||||
|
currentChunk = '';
|
||||||
|
}
|
||||||
|
}
|
||||||
|
currentChunk += (currentChunk ? '\n' : '') + line;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (currentChunk) {
|
||||||
|
chunks.push(currentChunk.trim());
|
||||||
|
}
|
||||||
|
|
||||||
|
for (let i = 0; i < chunks.length && i < 3; i++) {
|
||||||
|
if (i === 0) {
|
||||||
|
await message.reply({ content: chunks[i] });
|
||||||
|
} else {
|
||||||
|
await message.channel.send({ content: chunks[i] });
|
||||||
|
await new Promise(resolve => setTimeout(resolve, 500));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (chunks.length > 3) {
|
||||||
|
await message.channel.send({ content: "⚠️ Respuesta truncada por longitud." });
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
await message.reply({ content: finalResponse });
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error(`Error en respuesta automática AI:`, error);
|
||||||
|
await message.reply({
|
||||||
|
content: `❌ **Error:** ${error.message || 'No pude procesar tu respuesta. Intenta de nuevo.'}`
|
||||||
|
});
|
||||||
|
} finally {
|
||||||
|
clearInterval(typingInterval);
|
||||||
|
}
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
// Mensaje referenciado no encontrado o error, ignorar silenciosamente
|
||||||
|
logger.debug(`Error obteniendo mensaje referenciado: ${error}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
bot.on(Events.MessageCreate, async (message) => {
|
bot.on(Events.MessageCreate, async (message) => {
|
||||||
if (message.author.bot) return;
|
if (message.author.bot) return;
|
||||||
|
|
||||||
|
// Manejar respuestas automáticas a la AI
|
||||||
|
await handleAIReply(message);
|
||||||
|
|
||||||
await alliance(message);
|
await alliance(message);
|
||||||
const server = await bot.prisma.guild.upsert({
|
const server = await bot.prisma.guild.upsert({
|
||||||
where: {
|
where: {
|
||||||
|
|||||||
Reference in New Issue
Block a user