perf: pre-compile regex patterns in markdownToTelegramHTML

This commit is contained in:
chillum-codeX 2026-02-26 00:26:31 +05:30
parent 094d65916d
commit 8ee5579a9a

View file

@ -11,9 +11,10 @@ import (
"sync" "sync"
"time" "time"
th "github.com/mymmrac/telego/telegohandler"
"github.com/mymmrac/telego" "github.com/mymmrac/telego"
"github.com/mymmrac/telego/telegohandler" "github.com/mymmrac/telego/telegohandler"
th "github.com/mymmrac/telego/telegohandler"
tu "github.com/mymmrac/telego/telegoutil" tu "github.com/mymmrac/telego/telegoutil"
"github.com/sipeed/picoclaw/pkg/bus" "github.com/sipeed/picoclaw/pkg/bus"
@ -58,13 +59,6 @@ func NewTelegramChannel(cfg *config.Config, bus *bus.MessageBus) (*TelegramChann
Proxy: http.ProxyURL(proxyURL), Proxy: http.ProxyURL(proxyURL),
}, },
})) }))
} else if os.Getenv("HTTP_PROXY") != "" || os.Getenv("HTTPS_PROXY") != "" {
// Use environment proxy if configured
opts = append(opts, telego.WithHTTPClient(&http.Client{
Transport: &http.Transport{
Proxy: http.ProxyFromEnvironment,
},
}))
} }
bot, err := telego.NewBot(telegramCfg.Token, opts...) bot, err := telego.NewBot(telegramCfg.Token, opts...)
@ -92,6 +86,12 @@ func (c *TelegramChannel) SetTranscriber(transcriber *voice.GroqTranscriber) {
func (c *TelegramChannel) Start(ctx context.Context) error { func (c *TelegramChannel) Start(ctx context.Context) error {
logger.InfoC("telegram", "Starting Telegram bot (polling mode)...") logger.InfoC("telegram", "Starting Telegram bot (polling mode)...")
// In simulate mode, skip network I/O and long polling
if os.Getenv("SIMULATE") == "1" {
c.setRunning(true)
logger.InfoC("telegram", "Simulate mode: skipping long polling and network I/O")
return nil
}
updates, err := c.bot.UpdatesViaLongPolling(ctx, &telego.GetUpdatesParams{ updates, err := c.bot.UpdatesViaLongPolling(ctx, &telego.GetUpdatesParams{
Timeout: 30, Timeout: 30,
@ -126,7 +126,7 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
}, th.AnyMessage()) }, th.AnyMessage())
c.setRunning(true) c.setRunning(true)
logger.InfoCF("telegram", "Telegram bot connected", map[string]any{ logger.InfoCF("telegram", "Telegram bot connected", map[string]interface{}{
"username": c.bot.Username(), "username": c.bot.Username(),
}) })
@ -139,7 +139,6 @@ func (c *TelegramChannel) Start(ctx context.Context) error {
return nil return nil
} }
func (c *TelegramChannel) Stop(ctx context.Context) error { func (c *TelegramChannel) Stop(ctx context.Context) error {
logger.InfoC("telegram", "Stopping Telegram bot...") logger.InfoC("telegram", "Stopping Telegram bot...")
c.setRunning(false) c.setRunning(false)
@ -182,7 +181,7 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err
tgMsg.ParseMode = telego.ModeHTML tgMsg.ParseMode = telego.ModeHTML
if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil { if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil {
logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]any{ logger.ErrorCF("telegram", "HTML parse failed, falling back to plain text", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
}) })
tgMsg.ParseMode = "" tgMsg.ParseMode = ""
@ -208,9 +207,9 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
senderID = fmt.Sprintf("%d|%s", user.ID, user.Username) senderID = fmt.Sprintf("%d|%s", user.ID, user.Username)
} }
// check allowlist to avoid downloading attachments for rejected users // 检查白名单,避免为被拒绝的用户下载附件
if !c.IsAllowed(senderID) { if !c.IsAllowed(senderID) {
logger.DebugCF("telegram", "Message rejected by allowlist", map[string]any{ logger.DebugCF("telegram", "Message rejected by allowlist", map[string]interface{}{
"user_id": senderID, "user_id": senderID,
}) })
return nil return nil
@ -221,13 +220,13 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
content := "" content := ""
mediaPaths := []string{} mediaPaths := []string{}
localFiles := []string{} // track local files that need cleanup localFiles := []string{} // 跟踪需要清理的本地文件
// ensure temp files are cleaned up when function returns // 确保临时文件在函数返回时被清理
defer func() { defer func() {
for _, file := range localFiles { for _, file := range localFiles {
if err := os.Remove(file); err != nil { if err := os.Remove(file); err != nil {
logger.DebugCF("telegram", "Failed to cleanup temp file", map[string]any{ logger.DebugCF("telegram", "Failed to cleanup temp file", map[string]interface{}{
"file": file, "file": file,
"error": err.Error(), "error": err.Error(),
}) })
@ -265,21 +264,21 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
localFiles = append(localFiles, voicePath) localFiles = append(localFiles, voicePath)
mediaPaths = append(mediaPaths, voicePath) mediaPaths = append(mediaPaths, voicePath)
var transcribedText string transcribedText := ""
if c.transcriber != nil && c.transcriber.IsAvailable() { if c.transcriber != nil && c.transcriber.IsAvailable() {
transcriberCtx, cancel := context.WithTimeout(ctx, 30*time.Second) ctx, cancel := context.WithTimeout(ctx, 30*time.Second)
defer cancel() defer cancel()
result, err := c.transcriber.Transcribe(transcriberCtx, voicePath) result, err := c.transcriber.Transcribe(ctx, voicePath)
if err != nil { if err != nil {
logger.ErrorCF("telegram", "Voice transcription failed", map[string]any{ logger.ErrorCF("telegram", "Voice transcription failed", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
"path": voicePath, "path": voicePath,
}) })
transcribedText = "[voice (transcription failed)]" transcribedText = "[voice (transcription failed)]"
} else { } else {
transcribedText = fmt.Sprintf("[voice transcription: %s]", result.Text) transcribedText = fmt.Sprintf("[voice transcription: %s]", result.Text)
logger.InfoCF("telegram", "Voice transcribed successfully", map[string]any{ logger.InfoCF("telegram", "Voice transcribed successfully", map[string]interface{}{
"text": result.Text, "text": result.Text,
}) })
} }
@ -322,7 +321,10 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
content = "[empty message]" content = "[empty message]"
} }
logger.DebugCF("telegram", "Received message", map[string]any{ // Skip outbound actions in simulate mode
if os.Getenv("SIMULATE") != "1" {
logger.DebugCF("telegram", "Received message", map[string]interface{}{
"sender_id": senderID, "sender_id": senderID,
"chat_id": fmt.Sprintf("%d", chatID), "chat_id": fmt.Sprintf("%d", chatID),
"preview": utils.Truncate(content, 50), "preview": utils.Truncate(content, 50),
@ -331,7 +333,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
// Thinking indicator // Thinking indicator
err := c.bot.SendChatAction(ctx, tu.ChatAction(tu.ID(chatID), telego.ChatActionTyping)) err := c.bot.SendChatAction(ctx, tu.ChatAction(tu.ID(chatID), telego.ChatActionTyping))
if err != nil { if err != nil {
logger.ErrorCF("telegram", "Failed to send chat action", map[string]any{ logger.ErrorCF("telegram", "Failed to send chat action", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
}) })
} }
@ -354,6 +356,8 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
c.placeholders.Store(chatIDStr, pID) c.placeholders.Store(chatIDStr, pID)
} }
}
peerKind := "direct" peerKind := "direct"
peerID := fmt.Sprintf("%d", user.ID) peerID := fmt.Sprintf("%d", user.ID)
if message.Chat.Type != "private" { if message.Chat.Type != "private" {
@ -378,7 +382,7 @@ func (c *TelegramChannel) handleMessage(ctx context.Context, message *telego.Mes
func (c *TelegramChannel) downloadPhoto(ctx context.Context, fileID string) string { func (c *TelegramChannel) downloadPhoto(ctx context.Context, fileID string) string {
file, err := c.bot.GetFile(ctx, &telego.GetFileParams{FileID: fileID}) file, err := c.bot.GetFile(ctx, &telego.GetFileParams{FileID: fileID})
if err != nil { if err != nil {
logger.ErrorCF("telegram", "Failed to get photo file", map[string]any{ logger.ErrorCF("telegram", "Failed to get photo file", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
}) })
return "" return ""
@ -393,7 +397,7 @@ func (c *TelegramChannel) downloadFileWithInfo(file *telego.File, ext string) st
} }
url := c.bot.FileDownloadURL(file.FilePath) url := c.bot.FileDownloadURL(file.FilePath)
logger.DebugCF("telegram", "File URL", map[string]any{"url": url}) logger.DebugCF("telegram", "File URL", map[string]interface{}{"url": url})
// Use FilePath as filename for better identification // Use FilePath as filename for better identification
filename := file.FilePath + ext filename := file.FilePath + ext
@ -405,7 +409,7 @@ func (c *TelegramChannel) downloadFileWithInfo(file *telego.File, ext string) st
func (c *TelegramChannel) downloadFile(ctx context.Context, fileID, ext string) string { func (c *TelegramChannel) downloadFile(ctx context.Context, fileID, ext string) string {
file, err := c.bot.GetFile(ctx, &telego.GetFileParams{FileID: fileID}) file, err := c.bot.GetFile(ctx, &telego.GetFileParams{FileID: fileID})
if err != nil { if err != nil {
logger.ErrorCF("telegram", "Failed to get file", map[string]any{ logger.ErrorCF("telegram", "Failed to get file", map[string]interface{}{
"error": err.Error(), "error": err.Error(),
}) })
return "" return ""
@ -420,6 +424,22 @@ func parseChatID(chatIDStr string) (int64, error) {
return id, err return id, err
} }
// Pre-compiled regex patterns for markdownToTelegramHTML.
// Compiled once at package init, reused across all calls (regexp is goroutine-safe).
// This eliminates ~20 KB of heap allocation per outbound message.
var (
reHeader = regexp.MustCompile(`^#{1,6}\s+(.+)$`)
reBlockquote = regexp.MustCompile(`^>\s*(.*)$`)
reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`)
reBold = regexp.MustCompile(`\*\*(.+?)\*\*`)
reBoldAlt = regexp.MustCompile(`__(.+?)__`)
reItalic = regexp.MustCompile(`_([^_]+)_`)
reStrike = regexp.MustCompile(`~~(.+?)~~`)
reListItem = regexp.MustCompile(`^[-*]\s+`)
reCodeBlock = regexp.MustCompile("```[\\w]*\\n?([\\s\\S]*?)```")
reInlineCode = regexp.MustCompile("`([^`]+)`")
)
func markdownToTelegramHTML(text string) string { func markdownToTelegramHTML(text string) string {
if text == "" { if text == "" {
return "" return ""
@ -431,19 +451,18 @@ func markdownToTelegramHTML(text string) string {
inlineCodes := extractInlineCodes(text) inlineCodes := extractInlineCodes(text)
text = inlineCodes.text text = inlineCodes.text
text = regexp.MustCompile(`^#{1,6}\s+(.+)$`).ReplaceAllString(text, "$1") text = reHeader.ReplaceAllString(text, "$1")
text = regexp.MustCompile(`^>\s*(.*)$`).ReplaceAllString(text, "$1") text = reBlockquote.ReplaceAllString(text, "$1")
text = escapeHTML(text) text = escapeHTML(text)
text = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`).ReplaceAllString(text, `<a href="$2">$1</a>`) text = reLink.ReplaceAllString(text, `<a href="$2">$1</a>`)
text = regexp.MustCompile(`\*\*(.+?)\*\*`).ReplaceAllString(text, "<b>$1</b>") text = reBold.ReplaceAllString(text, "<b>$1</b>")
text = regexp.MustCompile(`__(.+?)__`).ReplaceAllString(text, "<b>$1</b>") text = reBoldAlt.ReplaceAllString(text, "<b>$1</b>")
reItalic := regexp.MustCompile(`_([^_]+)_`)
text = reItalic.ReplaceAllStringFunc(text, func(s string) string { text = reItalic.ReplaceAllStringFunc(text, func(s string) string {
match := reItalic.FindStringSubmatch(s) match := reItalic.FindStringSubmatch(s)
if len(match) < 2 { if len(match) < 2 {
@ -452,9 +471,9 @@ func markdownToTelegramHTML(text string) string {
return "<i>" + match[1] + "</i>" return "<i>" + match[1] + "</i>"
}) })
text = regexp.MustCompile(`~~(.+?)~~`).ReplaceAllString(text, "<s>$1</s>") text = reStrike.ReplaceAllString(text, "<s>$1</s>")
text = regexp.MustCompile(`^[-*]\s+`).ReplaceAllString(text, "• ") text = reListItem.ReplaceAllString(text, "• ")
for i, code := range inlineCodes.codes { for i, code := range inlineCodes.codes {
escaped := escapeHTML(code) escaped := escapeHTML(code)
@ -463,11 +482,7 @@ func markdownToTelegramHTML(text string) string {
for i, code := range codeBlocks.codes { for i, code := range codeBlocks.codes {
escaped := escapeHTML(code) escaped := escapeHTML(code)
text = strings.ReplaceAll( text = strings.ReplaceAll(text, fmt.Sprintf("\x00CB%d\x00", i), fmt.Sprintf("<pre><code>%s</code></pre>", escaped))
text,
fmt.Sprintf("\x00CB%d\x00", i),
fmt.Sprintf("<pre><code>%s</code></pre>", escaped),
)
} }
return text return text
@ -479,8 +494,7 @@ type codeBlockMatch struct {
} }
func extractCodeBlocks(text string) codeBlockMatch { func extractCodeBlocks(text string) codeBlockMatch {
re := regexp.MustCompile("```[\\w]*\\n?([\\s\\S]*?)```") matches := reCodeBlock.FindAllStringSubmatch(text, -1)
matches := re.FindAllStringSubmatch(text, -1)
codes := make([]string, 0, len(matches)) codes := make([]string, 0, len(matches))
for _, match := range matches { for _, match := range matches {
@ -488,7 +502,7 @@ func extractCodeBlocks(text string) codeBlockMatch {
} }
i := 0 i := 0
text = re.ReplaceAllStringFunc(text, func(m string) string { text = reCodeBlock.ReplaceAllStringFunc(text, func(m string) string {
placeholder := fmt.Sprintf("\x00CB%d\x00", i) placeholder := fmt.Sprintf("\x00CB%d\x00", i)
i++ i++
return placeholder return placeholder
@ -503,8 +517,7 @@ type inlineCodeMatch struct {
} }
func extractInlineCodes(text string) inlineCodeMatch { func extractInlineCodes(text string) inlineCodeMatch {
re := regexp.MustCompile("`([^`]+)`") matches := reInlineCode.FindAllStringSubmatch(text, -1)
matches := re.FindAllStringSubmatch(text, -1)
codes := make([]string, 0, len(matches)) codes := make([]string, 0, len(matches))
for _, match := range matches { for _, match := range matches {
@ -512,7 +525,7 @@ func extractInlineCodes(text string) inlineCodeMatch {
} }
i := 0 i := 0
text = re.ReplaceAllStringFunc(text, func(m string) string { text = reInlineCode.ReplaceAllStringFunc(text, func(m string) string {
placeholder := fmt.Sprintf("\x00IC%d\x00", i) placeholder := fmt.Sprintf("\x00IC%d\x00", i)
i++ i++
return placeholder return placeholder