From 3a0af79f73f27fe089c1faea472f3e9b302beb41 Mon Sep 17 00:00:00 2001 From: dyike Date: Tue, 3 Mar 2026 11:17:21 +0800 Subject: [PATCH] feat(telegram): support MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit
block rendering as blockquote

- Convert LLM...
tags to Telegram
HTML with 🧠 prefix for visual distinction - Fix markdown blockquote (> lines) rendering: convert to
HTML instead of stripping the prefix - Fix reasoning content not being sent: fallback to ReasoningContent field when Reasoning is empty (supports DeepSeek and similar models) ``` --- pkg/channels/telegram/telegram.go | 77 ++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index 7feb706aa..69bae64bd 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -36,6 +36,7 @@ var ( reListItem = regexp.MustCompile(`^[-*]\s+`) reCodeBlock = regexp.MustCompile("```[\\w]*\\n?([\\s\\S]*?)```") reInlineCode = regexp.MustCompile("`([^`]+)`") + reThinkBlock = regexp.MustCompile(`(?s)(.*?)`) ) type TelegramChannel struct { @@ -615,6 +616,10 @@ func markdownToTelegramHTML(text string) string { return "" } + // Convert ... blocks to blockquote before other processing + thinkBlocks := extractThinkBlocks(text) + text = thinkBlocks.text + codeBlocks := extractCodeBlocks(text) text = codeBlocks.text @@ -623,7 +628,8 @@ func markdownToTelegramHTML(text string) string { text = reHeading.ReplaceAllString(text, "$1") - text = reBlockquote.ReplaceAllString(text, "$1") + // Convert blockquote lines (> ...) to
HTML + text = convertBlockquotes(text) text = escapeHTML(text) @@ -659,6 +665,75 @@ func markdownToTelegramHTML(text string) string { ) } + // Restore think blocks as
+ for i, content := range thinkBlocks.contents { + escaped := escapeHTML(strings.TrimSpace(content)) + text = strings.ReplaceAll( + text, + fmt.Sprintf("\x00TK%d\x00", i), + fmt.Sprintf("
🧠 %s
", escaped), + ) + } + + return text +} + +// thinkBlockMatch holds extracted blocks. +type thinkBlockMatch struct { + text string + contents []string +} + +// extractThinkBlocks extracts ... blocks and replaces them with placeholders. +func extractThinkBlocks(text string) thinkBlockMatch { + matches := reThinkBlock.FindAllStringSubmatch(text, -1) + + contents := make([]string, 0, len(matches)) + for _, match := range matches { + contents = append(contents, match[1]) + } + + i := 0 + text = reThinkBlock.ReplaceAllStringFunc(text, func(m string) string { + placeholder := fmt.Sprintf("\x00TK%d\x00", i) + i++ + return placeholder + }) + + return thinkBlockMatch{text: text, contents: contents} +} + +// convertBlockquotes groups consecutive `> ...` lines into
blocks. +func convertBlockquotes(text string) string { + lines := strings.Split(text, "\n") + var result []string + inBlockquote := false + + for _, line := range lines { + if reBlockquote.MatchString(line) { + content := reBlockquote.ReplaceAllString(line, "$1") + if !inBlockquote { + result = append(result, "\x00BQ_START\x00"+content) + inBlockquote = true + } else { + result = append(result, content) + } + } else { + if inBlockquote { + result = append(result, "\x00BQ_END\x00") + inBlockquote = false + } + result = append(result, line) + } + } + if inBlockquote { + result = append(result, "\x00BQ_END\x00") + } + + text = strings.Join(result, "\n") + text = strings.ReplaceAll(text, "\x00BQ_START\x00", "
") + text = strings.ReplaceAll(text, "\n\x00BQ_END\x00", "
") + text = strings.ReplaceAll(text, "\x00BQ_END\x00", "
") return text }