fix: convertir Markdown a HTML de Telegram antes de enviar mensajes del agente
El bot manda mensajes con parse_mode=HTML, pero la IA responde en Markdown (##, **negrita**, listas con "-", separadores ---), que Telegram no interpreta — salía todo literal con los símbolos. Ahora FormatearParaTelegram convierte el Markdown a las etiquetas que Telegram sí soporta (b, code, pre, a) justo antes de enviar, escapando cualquier < > & literal para que sendMessage tampoco falle por HTML inválido. Se ajustó el único lugar que ya armaba HTML a mano (/instancias) para que use el mismo formato Markdown y pase por el mismo conversor. Incluye tests con el caso real reportado. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Sonnet 5
parent
a0b2e9ae0a
commit
7b0e4b9b5c
@@ -0,0 +1,82 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"regexp"
|
||||
"strings"
|
||||
)
|
||||
|
||||
var (
|
||||
reCodeBlock = regexp.MustCompile("(?s)```(?:[a-zA-Z0-9]*\n)?(.*?)```")
|
||||
reInlineCode = regexp.MustCompile("`([^`\n]+)`")
|
||||
reBold = regexp.MustCompile(`\*\*(.+?)\*\*|__(.+?)__`)
|
||||
reHeader = regexp.MustCompile(`(?m)^#{1,6}[ \t]+(.+)$`)
|
||||
reBullet = regexp.MustCompile(`(?m)^(\s*)[-*][ \t]+`)
|
||||
reHRSimple = regexp.MustCompile(`(?m)^[ \t]*(-{3,}|_{3,}|\*{3,})[ \t]*\n?`)
|
||||
reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)\s]+)\)`)
|
||||
)
|
||||
|
||||
// FormatearParaTelegram convierte el Markdown que suele escribir la IA (##,
|
||||
// **negrita**, listas con "-", ``` código ```, separadores ---) a las pocas
|
||||
// etiquetas HTML que Telegram sí entiende con parse_mode=HTML (b, i, code, pre,
|
||||
// a). Escapa primero cualquier < > & literal del texto original para que
|
||||
// sendMessage nunca falle por HTML inválido si la IA menciona código o símbolos.
|
||||
func FormatearParaTelegram(text string) string {
|
||||
// 1. Proteger bloques de código ANTES de escapar el resto, para no
|
||||
// procesarles negrita/viñetas por accidente.
|
||||
var blocks []string
|
||||
protect := func(html string) string {
|
||||
blocks = append(blocks, html)
|
||||
return fmt.Sprintf("\x00%d\x00", len(blocks)-1)
|
||||
}
|
||||
text = reCodeBlock.ReplaceAllStringFunc(text, func(m string) string {
|
||||
sub := reCodeBlock.FindStringSubmatch(m)
|
||||
return protect("<pre>" + escapeTelegramHTML(strings.TrimSpace(sub[1])) + "</pre>")
|
||||
})
|
||||
text = reInlineCode.ReplaceAllStringFunc(text, func(m string) string {
|
||||
sub := reInlineCode.FindStringSubmatch(m)
|
||||
return protect("<code>" + escapeTelegramHTML(sub[1]) + "</code>")
|
||||
})
|
||||
|
||||
// 2. Escapar el resto del texto como HTML.
|
||||
text = escapeTelegramHTML(text)
|
||||
|
||||
// 3. Separadores horizontales estilo markdown (---, ___, ***) fuera.
|
||||
text = reHRSimple.ReplaceAllString(text, "")
|
||||
|
||||
// 4. Negrita (antes que encabezados, para no anidar <b> si un título
|
||||
// también trae **negrita** adentro).
|
||||
text = reBold.ReplaceAllStringFunc(text, func(m string) string {
|
||||
sub := reBold.FindStringSubmatch(m)
|
||||
inner := sub[1]
|
||||
if inner == "" {
|
||||
inner = sub[2]
|
||||
}
|
||||
return "<b>" + inner + "</b>"
|
||||
})
|
||||
|
||||
// 5. Encabezados (#, ##, ###...) → negrita, sin duplicar <b> si ya la trae.
|
||||
text = reHeader.ReplaceAllStringFunc(text, func(m string) string {
|
||||
sub := reHeader.FindStringSubmatch(m)
|
||||
inner := strings.NewReplacer("<b>", "", "</b>", "").Replace(sub[1])
|
||||
return "<b>" + inner + "</b>"
|
||||
})
|
||||
|
||||
// 6. Viñetas "- " o "* " al inicio de línea → "• ".
|
||||
text = reBullet.ReplaceAllString(text, "$1• ")
|
||||
|
||||
// 7. Enlaces [texto](url).
|
||||
text = reLink.ReplaceAllString(text, `<a href="$2">$1</a>`)
|
||||
|
||||
// 8. Restaurar bloques de código protegidos en el paso 1.
|
||||
for i, b := range blocks {
|
||||
text = strings.ReplaceAll(text, fmt.Sprintf("\x00%d\x00", i), b)
|
||||
}
|
||||
|
||||
// Máximo 3 saltos de línea seguidos (Markdown suele dejar párrafos sueltos).
|
||||
for strings.Contains(text, "\n\n\n\n") {
|
||||
text = strings.ReplaceAll(text, "\n\n\n\n", "\n\n\n")
|
||||
}
|
||||
|
||||
return strings.TrimSpace(text)
|
||||
}
|
||||
Reference in New Issue
Block a user