feat: integrate modern GenAI SDK for image generation and enhance image handling in AI requests
This commit is contained in:
@@ -227,56 +227,23 @@ export const command: CommandMessage = {
|
|||||||
|
|
||||||
// Verificar si hay imágenes adjuntas
|
// Verificar si hay imágenes adjuntas
|
||||||
const attachments = Array.from(message.attachments.values());
|
const attachments = Array.from(message.attachments.values());
|
||||||
const hasImages = attachments.length > 0 && aiService.hasImageAttachments?.(attachments);
|
const hasImages = attachments.length > 0 && aiService.hasImageAttachments(attachments);
|
||||||
|
|
||||||
// Usar el nuevo método con memoria persistente y soporte para imágenes
|
// Usar el método unificado con memoria persistente y soporte para imágenes
|
||||||
if (hasImages) {
|
aiResponse = await aiService.processAIRequestWithMemory(
|
||||||
// Agregar información sobre las imágenes a los metadatos
|
userId,
|
||||||
const imageInfo = attachments
|
prompt,
|
||||||
.filter(att => att.contentType?.startsWith('image/') ||
|
guildId,
|
||||||
['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'].some(ext =>
|
message.channel.id,
|
||||||
att.name?.toLowerCase().endsWith(ext)))
|
message.id,
|
||||||
.map(att => `${att.name} (${att.contentType || 'imagen'})`)
|
referencedMessageId,
|
||||||
.join(', ');
|
message.client,
|
||||||
|
'normal',
|
||||||
const enhancedMeta = messageMeta + (imageInfo ? ` | Imágenes adjuntas: ${imageInfo}` : '');
|
{
|
||||||
|
meta: messageMeta + (hasImages ? ` | Tiene ${attachments.length} imagen(es) adjunta(s)` : ''),
|
||||||
// Usar método específico para imágenes
|
attachments: hasImages ? attachments : undefined
|
||||||
const request = {
|
}
|
||||||
userId,
|
);
|
||||||
guildId,
|
|
||||||
channelId: message.channel.id,
|
|
||||||
prompt: prompt.trim(),
|
|
||||||
priority: 'normal' as const,
|
|
||||||
timestamp: Date.now(),
|
|
||||||
aiRolePrompt: undefined,
|
|
||||||
meta: enhancedMeta,
|
|
||||||
messageId: message.id,
|
|
||||||
referencedMessageId,
|
|
||||||
attachments: attachments,
|
|
||||||
client: message.client
|
|
||||||
};
|
|
||||||
|
|
||||||
// Procesar con imágenes
|
|
||||||
aiResponse = await new Promise((resolve, reject) => {
|
|
||||||
request.resolve = resolve;
|
|
||||||
request.reject = reject;
|
|
||||||
aiService.requestQueue.push(request as any);
|
|
||||||
});
|
|
||||||
} else {
|
|
||||||
// Método normal sin imágenes
|
|
||||||
aiResponse = await aiService.processAIRequestWithMemory(
|
|
||||||
userId,
|
|
||||||
prompt,
|
|
||||||
guildId,
|
|
||||||
message.channel.id,
|
|
||||||
message.id,
|
|
||||||
referencedMessageId,
|
|
||||||
message.client,
|
|
||||||
'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) {
|
||||||
|
|||||||
62
src/commands/messages/AI/image.ts
Normal file
62
src/commands/messages/AI/image.ts
Normal file
@@ -0,0 +1,62 @@
|
|||||||
|
import { CommandMessage } from "../../../core/types/commands";
|
||||||
|
import { aiService } from "../../../core/services/AIService";
|
||||||
|
import logger from "../../../core/lib/logger";
|
||||||
|
|
||||||
|
function parseSizeArg(arg?: string): 'square' | 'portrait' | 'landscape' {
|
||||||
|
if (!arg) return 'square';
|
||||||
|
const v = arg.toLowerCase();
|
||||||
|
if (v === 'square' || v === 'cuadrado' || v === '1:1') return 'square';
|
||||||
|
if (v === 'portrait' || v === 'vertical' || v === '9:16') return 'portrait';
|
||||||
|
if (v === 'landscape' || v === 'horizontal' || v === '16:9') return 'landscape';
|
||||||
|
return 'square';
|
||||||
|
}
|
||||||
|
|
||||||
|
export const command: CommandMessage = {
|
||||||
|
name: 'aiimg',
|
||||||
|
type: 'message',
|
||||||
|
aliases: ['img', 'imagen'],
|
||||||
|
cooldown: 5,
|
||||||
|
description: 'Genera una imagen con Gemini (gemini-2.5-flash-image).',
|
||||||
|
category: 'IA',
|
||||||
|
usage: 'aiimg [square|portrait|landscape] <prompt>',
|
||||||
|
run: async (message, args) => {
|
||||||
|
try {
|
||||||
|
if (!args || args.length === 0) {
|
||||||
|
await message.reply({
|
||||||
|
content: 'Uso: aiimg [square|portrait|landscape] <prompt>\nEjemplo: aiimg portrait un gato astronauta'
|
||||||
|
});
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
let size: 'square' | 'portrait' | 'landscape' = 'square';
|
||||||
|
let prompt = args.join(' ').trim();
|
||||||
|
|
||||||
|
// Si el primer arg es un tamaño válido, usarlo y quitarlo del prompt
|
||||||
|
const maybeSize = parseSizeArg(args[0]);
|
||||||
|
if (maybeSize !== 'square' || ['square', 'cuadrado', '1:1'].includes(args[0]?.toLowerCase?.() ?? '')) {
|
||||||
|
// Detect explicit size keyword; if first arg matches any known size token, shift it
|
||||||
|
if (['square','cuadrado','1:1','portrait','vertical','9:16','landscape','horizontal','16:9'].includes(args[0].toLowerCase())) {
|
||||||
|
size = maybeSize;
|
||||||
|
prompt = args.slice(1).join(' ').trim();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!prompt) {
|
||||||
|
await message.reply({ content: 'El prompt no puede estar vacío.' });
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
//@ts-ignore
|
||||||
|
await message.channel.sendTyping().catch(() => {});
|
||||||
|
const result = await aiService.generateImage(prompt, { size });
|
||||||
|
|
||||||
|
await message.reply({
|
||||||
|
content: `✅ Imagen generada (${size}).`,
|
||||||
|
files: [{ attachment: result.data, name: result.fileName }]
|
||||||
|
});
|
||||||
|
} catch (error: any) {
|
||||||
|
logger.error('Error generando imagen:', error);
|
||||||
|
await message.reply({ content: `❌ Error generando imagen: ${error?.message || 'Error desconocido'}` });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -172,7 +172,7 @@ export const command: CommandMessage = {
|
|||||||
]
|
]
|
||||||
};
|
};
|
||||||
|
|
||||||
const panelMessage = await message.reply({ flags: 32768, components: [helpPanel, categorySelectRow, exportRow] });
|
const panelMessage = await message.reply({ flags: 32768, components: [helpPanel, categorySelectRow] });
|
||||||
|
|
||||||
const collector = panelMessage.createMessageComponentCollector({
|
const collector = panelMessage.createMessageComponentCollector({
|
||||||
time: 600000,
|
time: 600000,
|
||||||
|
|||||||
@@ -1,4 +1,6 @@
|
|||||||
import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } from "@google/generative-ai";
|
import { GoogleGenerativeAI, HarmCategory, HarmBlockThreshold } from "@google/generative-ai";
|
||||||
|
// New: modern GenAI SDK for image generation
|
||||||
|
import { GoogleGenAI } from "@google/genai";
|
||||||
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";
|
||||||
@@ -85,6 +87,8 @@ function isAPIError(error: unknown): error is { message: string; code?: string }
|
|||||||
|
|
||||||
export class AIService {
|
export class AIService {
|
||||||
private genAI: GoogleGenerativeAI;
|
private genAI: GoogleGenerativeAI;
|
||||||
|
// New: client for modern GenAI features (images)
|
||||||
|
private genAIv2: any;
|
||||||
private conversations = new Collection<string, ConversationContext>();
|
private conversations = new Collection<string, ConversationContext>();
|
||||||
private requestQueue: AIRequest[] = [];
|
private requestQueue: AIRequest[] = [];
|
||||||
private processing = false;
|
private processing = false;
|
||||||
@@ -95,8 +99,8 @@ export class AIService {
|
|||||||
|
|
||||||
// Configuración mejorada y escalable
|
// Configuración mejorada y escalable
|
||||||
private readonly config = {
|
private readonly config = {
|
||||||
maxInputTokens: 1048576, // 1M tokens Gemini 2.5 Flash
|
maxInputTokens: 1048576, // 1M tokens Gemini 2.5 Flash (entrada)
|
||||||
maxOutputTokens: 8192, // Reducido para mejor rendimiento
|
maxOutputTokens: 65536, // 65,536 salida (según aclaración del usuario para preview 09-2025)
|
||||||
tokenResetThreshold: 0.80, // Más conservador
|
tokenResetThreshold: 0.80, // Más conservador
|
||||||
maxConversationAge: 30 * 60 * 1000, // 30 minutos
|
maxConversationAge: 30 * 60 * 1000, // 30 minutos
|
||||||
maxMessageHistory: 8, // Reducido para mejor memoria
|
maxMessageHistory: 8, // Reducido para mejor memoria
|
||||||
@@ -117,6 +121,12 @@ export class AIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
this.genAI = new GoogleGenerativeAI(apiKey);
|
this.genAI = new GoogleGenerativeAI(apiKey);
|
||||||
|
// Initialize modern SDK (lo tratamos como any para compatibilidad de tipos)
|
||||||
|
try {
|
||||||
|
this.genAIv2 = new GoogleGenAI({ apiKey });
|
||||||
|
} catch {
|
||||||
|
this.genAIv2 = null;
|
||||||
|
}
|
||||||
this.startCleanupService();
|
this.startCleanupService();
|
||||||
this.startQueueProcessor();
|
this.startQueueProcessor();
|
||||||
}
|
}
|
||||||
@@ -278,6 +288,91 @@ export class AIService {
|
|||||||
}, this.config.cleanupInterval);
|
}, this.config.cleanupInterval);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Parser de errores (versión legacy no utilizada)
|
||||||
|
*/
|
||||||
|
private parseAPIErrorLegacy(error: unknown): string {
|
||||||
|
// Delegar a la versión nueva
|
||||||
|
return this.parseAPIError(error);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Versión legacy de processAIRequestWithMemory (sin uso externo)
|
||||||
|
*/
|
||||||
|
async processAIRequestWithMemoryLegacy(
|
||||||
|
userId: string,
|
||||||
|
prompt: string,
|
||||||
|
guildId?: string,
|
||||||
|
channelId?: string,
|
||||||
|
messageId?: string,
|
||||||
|
referencedMessageId?: string,
|
||||||
|
client?: any,
|
||||||
|
priority: 'low' | 'normal' | 'high' = 'normal',
|
||||||
|
options?: { aiRolePrompt?: string; meta?: string; attachments?: any[] }
|
||||||
|
): 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 & { client?: any; attachments?: any[] } = {
|
||||||
|
userId,
|
||||||
|
guildId,
|
||||||
|
channelId,
|
||||||
|
prompt: prompt.trim(),
|
||||||
|
priority,
|
||||||
|
timestamp: Date.now(),
|
||||||
|
resolve,
|
||||||
|
reject,
|
||||||
|
aiRolePrompt: options?.aiRolePrompt,
|
||||||
|
meta: options?.meta,
|
||||||
|
messageId,
|
||||||
|
referencedMessageId,
|
||||||
|
client,
|
||||||
|
attachments: options?.attachments
|
||||||
|
};
|
||||||
|
|
||||||
|
// 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());
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Parser mejorado de errores de API - Type-safe sin ts-ignore
|
* Parser mejorado de errores de API - Type-safe sin ts-ignore
|
||||||
*/
|
*/
|
||||||
@@ -344,7 +439,7 @@ export class AIService {
|
|||||||
referencedMessageId?: string,
|
referencedMessageId?: string,
|
||||||
client?: any,
|
client?: any,
|
||||||
priority: 'low' | 'normal' | 'high' = 'normal',
|
priority: 'low' | 'normal' | 'high' = 'normal',
|
||||||
options?: { aiRolePrompt?: string; meta?: string }
|
options?: { aiRolePrompt?: string; meta?: string; attachments?: any[] }
|
||||||
): Promise<string> {
|
): Promise<string> {
|
||||||
// Validaciones exhaustivas
|
// Validaciones exhaustivas
|
||||||
if (!prompt?.trim()) {
|
if (!prompt?.trim()) {
|
||||||
@@ -371,7 +466,7 @@ export class AIService {
|
|||||||
|
|
||||||
// Agregar a la queue con Promise
|
// Agregar a la queue con Promise
|
||||||
return new Promise((resolve, reject) => {
|
return new Promise((resolve, reject) => {
|
||||||
const request: AIRequest = {
|
const request: AIRequest & { client?: any; attachments?: any[] } = {
|
||||||
userId,
|
userId,
|
||||||
guildId,
|
guildId,
|
||||||
channelId,
|
channelId,
|
||||||
@@ -384,6 +479,8 @@ export class AIService {
|
|||||||
meta: options?.meta,
|
meta: options?.meta,
|
||||||
messageId,
|
messageId,
|
||||||
referencedMessageId,
|
referencedMessageId,
|
||||||
|
client,
|
||||||
|
attachments: options?.attachments
|
||||||
};
|
};
|
||||||
|
|
||||||
// Insertar según prioridad
|
// Insertar según prioridad
|
||||||
@@ -468,15 +565,9 @@ export class AIService {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Usar la API correcta de Google Generative AI
|
// Usar gemini-2.5-flash-preview-09-2025 que puede leer imágenes y responder con texto
|
||||||
// Para imágenes, usar gemini-2.5-flash (soporta multimodal)
|
|
||||||
// Para solo texto, usar gemini-2.5-flash-preview-09-2025
|
|
||||||
const modelName = hasImages && imageAttachments.length > 0
|
|
||||||
? "gemini-2.5-flash-image"
|
|
||||||
: "gemini-2.5-flash-preview-09-2025";
|
|
||||||
|
|
||||||
const model = this.genAI.getGenerativeModel({
|
const model = this.genAI.getGenerativeModel({
|
||||||
model: modelName,
|
model: "gemini-2.5-flash-preview-09-2025",
|
||||||
generationConfig: {
|
generationConfig: {
|
||||||
maxOutputTokens: Math.min(this.config.maxOutputTokens, Math.max(1024, estimatedTokens * 0.5)),
|
maxOutputTokens: Math.min(this.config.maxOutputTokens, Math.max(1024, estimatedTokens * 0.5)),
|
||||||
temperature: 0.7,
|
temperature: 0.7,
|
||||||
@@ -613,6 +704,18 @@ Responde de forma directa y útil:`;
|
|||||||
return true;
|
return true;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Estimación de tokens más precisa
|
||||||
|
*/
|
||||||
|
private estimateTokens(text: string): number {
|
||||||
|
// Aproximación mejorada basada en la tokenización real
|
||||||
|
const words = text.split(/\s+/).length;
|
||||||
|
const chars = text.length;
|
||||||
|
|
||||||
|
// Fórmula híbrida más precisa
|
||||||
|
return Math.ceil((words * 1.3) + (chars * 0.25));
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detección mejorada de requests de imagen
|
* Detección mejorada de requests de imagen
|
||||||
*/
|
*/
|
||||||
@@ -628,9 +731,28 @@ Responde de forma directa y útil:`;
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Detectar si hay imágenes adjuntas en el mensaje para análisis
|
* Detectar si hay imágenes adjuntas en el mensaje para análisis (método público)
|
||||||
*/
|
*/
|
||||||
private hasImageAttachments(attachments?: any[]): boolean {
|
public hasImageAttachments(attachments?: any[]): boolean {
|
||||||
|
if (!attachments || attachments.length === 0) return false;
|
||||||
|
|
||||||
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
|
||||||
|
const imageMimeTypes = ['image/jpeg', 'image/png', 'image/gif', 'image/webp', 'image/bmp'];
|
||||||
|
|
||||||
|
return attachments.some(attachment => {
|
||||||
|
const hasImageExtension = imageExtensions.some(ext =>
|
||||||
|
attachment.name?.toLowerCase().endsWith(ext)
|
||||||
|
);
|
||||||
|
const hasImageMimeType = imageMimeTypes.includes(attachment.contentType?.toLowerCase());
|
||||||
|
|
||||||
|
return hasImageExtension || hasImageMimeType;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Detectar si hay imágenes adjuntas en el mensaje para análisis (método privado)
|
||||||
|
*/
|
||||||
|
private hasImageAttachmentsPrivate(attachments?: any[]): boolean {
|
||||||
if (!attachments || attachments.length === 0) return false;
|
if (!attachments || attachments.length === 0) return false;
|
||||||
|
|
||||||
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
|
const imageExtensions = ['.jpg', '.jpeg', '.png', '.gif', '.webp', '.bmp'];
|
||||||
@@ -1243,6 +1365,77 @@ Responde de forma directa y útil:`;
|
|||||||
return '';
|
return '';
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Generar imagen usando gemini-2.5-flash-image (Google Gen AI SDK moderno)
|
||||||
|
* Retorna un objeto con los bytes de la imagen y el tipo MIME.
|
||||||
|
*/
|
||||||
|
public async generateImage(prompt: string, options?: { size?: 'square' | 'portrait' | 'landscape'; mimeType?: string }): Promise<{ data: Buffer; mimeType: string; fileName: string; }> {
|
||||||
|
if (!prompt?.trim()) {
|
||||||
|
throw new Error('El prompt de imagen no puede estar vacío');
|
||||||
|
}
|
||||||
|
if (!this.genAIv2) {
|
||||||
|
throw new Error('El SDK moderno (@google/genai) no está inicializado');
|
||||||
|
}
|
||||||
|
|
||||||
|
// Mapear tamaño a hints simples (si el modelo los admite)
|
||||||
|
const sizeHint = options?.size ?? 'square';
|
||||||
|
const mimeType = options?.mimeType ?? 'image/png';
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Llamada flexible usando any para compatibilidad de tipos entre versiones
|
||||||
|
const res: any = await (this.genAIv2 as any).models.generateImages({
|
||||||
|
model: 'gemini-2.5-flash-image',
|
||||||
|
// La API moderna acepta `contents` o `prompt` según versión; usamos ambos por compatibilidad
|
||||||
|
contents: prompt,
|
||||||
|
prompt,
|
||||||
|
config: {
|
||||||
|
// Algunos despliegues soportan indicar formato de salida
|
||||||
|
responseMimeType: mimeType,
|
||||||
|
// Hints de tamaño en metadatos si aplica
|
||||||
|
// Nota: si no es soportado, el backend lo ignorará sin fallar
|
||||||
|
aspectRatio: sizeHint === 'portrait' ? '9:16' : sizeHint === 'landscape' ? '16:9' : '1:1',
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Intentar obtener el primer resultado de imagen en varias formas comunes
|
||||||
|
let imageDataBase64: string | undefined;
|
||||||
|
let resultMime: string = mimeType;
|
||||||
|
let fileName = `gen_${Date.now()}.${mimeType.includes('png') ? 'png' : mimeType.includes('jpeg') ? 'jpg' : 'img'}`;
|
||||||
|
|
||||||
|
if (res?.image?.data) {
|
||||||
|
imageDataBase64 = res.image.data;
|
||||||
|
resultMime = res.image.mimeType ?? resultMime;
|
||||||
|
} else if (Array.isArray(res?.images) && res.images.length > 0) {
|
||||||
|
// Some SDKs return { images: [{ data, mimeType }] }
|
||||||
|
imageDataBase64 = res.images[0].data ?? res.images[0].inlineData?.data;
|
||||||
|
resultMime = res.images[0].mimeType ?? res.images[0].inlineData?.mimeType ?? resultMime;
|
||||||
|
} else if (res?.response?.candidates?.[0]?.content?.parts) {
|
||||||
|
// Fallback: imagen como inlineData en parts
|
||||||
|
const parts = res.response.candidates[0].content.parts;
|
||||||
|
const imgPart = parts.find((p: any) => p.inlineData && p.inlineData.data);
|
||||||
|
if (imgPart) {
|
||||||
|
imageDataBase64 = imgPart.inlineData.data;
|
||||||
|
resultMime = imgPart.inlineData.mimeType ?? resultMime;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!imageDataBase64) {
|
||||||
|
throw new Error('No se recibió imagen del modelo');
|
||||||
|
}
|
||||||
|
|
||||||
|
const buffer = Buffer.from(imageDataBase64, 'base64');
|
||||||
|
// Ajustar nombre de archivo segun mimetype real
|
||||||
|
if (resultMime.includes('png')) fileName = fileName.replace(/\.[^.]+$/, '.png');
|
||||||
|
else if (resultMime.includes('jpeg') || resultMime.includes('jpg')) fileName = fileName.replace(/\.[^.]+$/, '.jpg');
|
||||||
|
else if (resultMime.includes('webp')) fileName = fileName.replace(/\.[^.]+$/, '.webp');
|
||||||
|
|
||||||
|
return { data: buffer, mimeType: resultMime, fileName };
|
||||||
|
} catch (e) {
|
||||||
|
const msg = this.parseAPIError(e);
|
||||||
|
throw new Error(msg);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Instancia singleton
|
// Instancia singleton
|
||||||
|
|||||||
@@ -104,7 +104,11 @@ async function handleAIReply(message: any) {
|
|||||||
|
|
||||||
const messageMeta = buildMessageMeta(message, emojiResult.names);
|
const messageMeta = buildMessageMeta(message, emojiResult.names);
|
||||||
|
|
||||||
// Procesar con el servicio de AI usando memoria persistente
|
// Verificar si hay imágenes adjuntas
|
||||||
|
const attachments = Array.from(message.attachments.values());
|
||||||
|
const hasImages = attachments.length > 0 && aiService.hasImageAttachments(attachments);
|
||||||
|
|
||||||
|
// Procesar con el servicio de AI usando memoria persistente y soporte para imágenes
|
||||||
const aiResponse = await aiService.processAIRequestWithMemory(
|
const aiResponse = await aiService.processAIRequestWithMemory(
|
||||||
message.author.id,
|
message.author.id,
|
||||||
message.content,
|
message.content,
|
||||||
@@ -114,7 +118,10 @@ async function handleAIReply(message: any) {
|
|||||||
message.reference.messageId,
|
message.reference.messageId,
|
||||||
message.client,
|
message.client,
|
||||||
'normal',
|
'normal',
|
||||||
{ meta: messageMeta }
|
{
|
||||||
|
meta: messageMeta + (hasImages ? ` | Tiene ${attachments.length} imagen(es) adjunta(s)` : ''),
|
||||||
|
attachments: hasImages ? attachments : undefined
|
||||||
|
}
|
||||||
);
|
);
|
||||||
|
|
||||||
// Reemplazar emojis personalizados
|
// Reemplazar emojis personalizados
|
||||||
|
|||||||
Reference in New Issue
Block a user