feat: add support for custom emojis in messages and improve metadata handling
This commit is contained in:
@@ -1,6 +1,6 @@
|
|||||||
import logger from "../../../core/lib/logger";
|
import logger from "../../../core/lib/logger";
|
||||||
import { CommandMessage } from "../../../core/types/commands";
|
import { CommandMessage } from "../../../core/types/commands";
|
||||||
import { TextChannel, DMChannel, ThreadChannel, ChannelType } from "discord.js";
|
import { TextChannel, DMChannel, ThreadChannel, ChannelType, GuildEmoji } from "discord.js";
|
||||||
import { aiService } from "../../../core/services/AIService";
|
import { aiService } from "../../../core/services/AIService";
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -54,7 +54,57 @@ function smartChunkText(text: string, maxLength: number): string[] {
|
|||||||
return chunks;
|
return chunks;
|
||||||
}
|
}
|
||||||
|
|
||||||
function buildMessageMeta(message: any): string {
|
function replaceShortcodesWithEmojisOutsideCode(text: string, emojiMap: Record<string, string>): string {
|
||||||
|
if (!text) return text;
|
||||||
|
// Split by triple backticks to avoid code blocks
|
||||||
|
const parts = text.split(/```/);
|
||||||
|
for (let i = 0; i < parts.length; i++) {
|
||||||
|
// Only replace in non-code blocks (even indices)
|
||||||
|
if (i % 2 === 0) {
|
||||||
|
// Also avoid inline code wrapped in single backticks by a simple pass
|
||||||
|
const inlineParts = parts[i].split(/`/);
|
||||||
|
for (let j = 0; j < inlineParts.length; j++) {
|
||||||
|
if (j % 2 === 0) {
|
||||||
|
inlineParts[j] = inlineParts[j].replace(/:([a-zA-Z0-9_]{2,32}):/g, (match, p1: string) => {
|
||||||
|
const key = p1;
|
||||||
|
const found = emojiMap[key];
|
||||||
|
return found ? found : match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
parts[i] = inlineParts.join('`');
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return parts.join('```');
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getGuildCustomEmojis(message: any): Promise<{ names: string[]; map: Record<string, string> }> {
|
||||||
|
const result = { names: [] as string[], map: {} as Record<string, string> };
|
||||||
|
try {
|
||||||
|
const guild = message.guild;
|
||||||
|
if (!guild) return result;
|
||||||
|
// Ensure emojis are fetched
|
||||||
|
const emojis = await guild.emojis.fetch();
|
||||||
|
const list = Array.from(emojis.values()) as GuildEmoji[];
|
||||||
|
for (const e of list) {
|
||||||
|
const name = e.name ?? undefined;
|
||||||
|
const id = e.id;
|
||||||
|
if (!name || !id) continue;
|
||||||
|
const tag = e.animated ? `<a:${name}:${id}>` : `<:${name}:${id}>`;
|
||||||
|
if (!(name in result.map)) {
|
||||||
|
result.map[name] = tag;
|
||||||
|
result.names.push(name);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Limit names to 25 for meta context brevity
|
||||||
|
result.names = result.names.slice(0, 25);
|
||||||
|
} catch {
|
||||||
|
// ignore
|
||||||
|
}
|
||||||
|
return result;
|
||||||
|
}
|
||||||
|
|
||||||
|
function buildMessageMeta(message: any, emojiNames?: string[]): string {
|
||||||
try {
|
try {
|
||||||
const parts: string[] = [];
|
const parts: string[] = [];
|
||||||
const inGuild = !!message.guild;
|
const inGuild = !!message.guild;
|
||||||
@@ -106,6 +156,10 @@ function buildMessageMeta(message: any): string {
|
|||||||
parts.push(`Adjuntos: ${info}`);
|
parts.push(`Adjuntos: ${info}`);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if (emojiNames && emojiNames.length) {
|
||||||
|
parts.push(`Emojis personalizados disponibles (usa :nombre:): ${emojiNames.join(', ')}`);
|
||||||
|
}
|
||||||
|
|
||||||
const metaRaw = parts.join(' | ');
|
const metaRaw = parts.join(' | ');
|
||||||
return metaRaw.length > 800 ? metaRaw.slice(0, 800) : metaRaw;
|
return metaRaw.length > 800 ? metaRaw.slice(0, 800) : metaRaw;
|
||||||
} catch {
|
} catch {
|
||||||
@@ -141,8 +195,11 @@ export const command: CommandMessage = {
|
|||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
|
||||||
// Construir metadatos del mensaje para mejor contexto
|
// Emojis personalizados del servidor
|
||||||
const meta = buildMessageMeta(message);
|
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
|
// Indicador de escritura mejorado
|
||||||
const typingInterval = setInterval(() => {
|
const typingInterval = setInterval(() => {
|
||||||
@@ -153,7 +210,7 @@ export const command: CommandMessage = {
|
|||||||
// Usar el servicio mejorado con manejo de prioridad
|
// Usar el servicio mejorado con manejo de prioridad
|
||||||
const priority = message.member?.permissions.has('Administrator') ? 'high' : 'normal';
|
const priority = message.member?.permissions.has('Administrator') ? 'high' : 'normal';
|
||||||
|
|
||||||
const aiResponse = await aiService.processAIRequest(
|
let aiResponse = await aiService.processAIRequest(
|
||||||
userId,
|
userId,
|
||||||
prompt,
|
prompt,
|
||||||
guildId,
|
guildId,
|
||||||
@@ -161,6 +218,11 @@ export const command: CommandMessage = {
|
|||||||
{ meta }
|
{ meta }
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// Reemplazar :nombre: por el tag real del emoji, evitando bloques de código
|
||||||
|
if (emojiNames.length > 0) {
|
||||||
|
aiResponse = replaceShortcodesWithEmojisOutsideCode(aiResponse, emojiMap);
|
||||||
|
}
|
||||||
|
|
||||||
// Discord limita el contenido a ~2000 caracteres
|
// Discord limita el contenido a ~2000 caracteres
|
||||||
const MAX_CONTENT = 2000;
|
const MAX_CONTENT = 2000;
|
||||||
if (aiResponse.length > MAX_CONTENT) {
|
if (aiResponse.length > MAX_CONTENT) {
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ export class AIService {
|
|||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Obtener prompt de rol de IA por guild con cache
|
* Obtener prompt de rol de IA por guild con caché
|
||||||
*/
|
*/
|
||||||
public async getGuildAiPrompt(guildId: string): Promise<string | null> {
|
public async getGuildAiPrompt(guildId: string): Promise<string | null> {
|
||||||
try {
|
try {
|
||||||
@@ -107,6 +107,7 @@ export class AIService {
|
|||||||
if (cached && (now - cached.fetchedAt) < this.config.guildConfigTTL) {
|
if (cached && (now - cached.fetchedAt) < this.config.guildConfigTTL) {
|
||||||
return cached.prompt;
|
return cached.prompt;
|
||||||
}
|
}
|
||||||
|
// @ts-ignore
|
||||||
const guild = await prisma.guild.findUnique({ where: { id: guildId }, select: { aiRolePrompt: true } });
|
const guild = await prisma.guild.findUnique({ where: { id: guildId }, select: { aiRolePrompt: true } });
|
||||||
//@ts-ignore
|
//@ts-ignore
|
||||||
const prompt = guild?.aiRolePrompt ?? null;
|
const prompt = guild?.aiRolePrompt ?? null;
|
||||||
@@ -321,7 +322,7 @@ export class AIService {
|
|||||||
meta?: string
|
meta?: string
|
||||||
): string {
|
): string {
|
||||||
const recentMessages = context.messages
|
const recentMessages = context.messages
|
||||||
.slice(-4) // Solo los últimos 4 mensajes
|
.slice(-4)
|
||||||
.map(msg => `${msg.role === 'user' ? 'Usuario' : 'Asistente'}: ${msg.content}`)
|
.map(msg => `${msg.role === 'user' ? 'Usuario' : 'Asistente'}: ${msg.content}`)
|
||||||
.join('\n');
|
.join('\n');
|
||||||
|
|
||||||
@@ -334,6 +335,8 @@ export class AIService {
|
|||||||
- USA **markdown de Discord**: **negrita**, *cursiva*, \`código\`, \`\`\`bloques\`\`\`
|
- USA **markdown de Discord**: **negrita**, *cursiva*, \`código\`, \`\`\`bloques\`\`\`
|
||||||
- NUNCA uses LaTeX ($$)
|
- NUNCA uses LaTeX ($$)
|
||||||
- Máximo 2-3 emojis por respuesta
|
- Máximo 2-3 emojis por respuesta
|
||||||
|
- Prefiere emojis Unicode estándar (🙂, 🎯, etc.) cuando no haya más contexto
|
||||||
|
- Si se te proporciona una lista de "Emojis personalizados disponibles", puedes usarlos escribiendo :nombre: exactamente como aparece; NO inventes nombres
|
||||||
- Respuestas concisas y claras
|
- Respuestas concisas y claras
|
||||||
|
|
||||||
${isImageRequest ? `
|
${isImageRequest ? `
|
||||||
|
|||||||
Reference in New Issue
Block a user