From 1b189f382ed290d7b3b604576f03c336d1baa3a6 Mon Sep 17 00:00:00 2001 From: Aleksandr Bortnikov Date: Fri, 6 Mar 2026 14:43:58 +0300 Subject: [PATCH] feat: added flag use_markdown_v2, corrected config, updated documentation --- README.fr.md | 11 +- README.ja.md | 9 +- README.md | 7 +- README.pt-br.md | 3 +- README.vi.md | 9 +- README.zh.md | 3 +- config/config.example.json | 5 +- .../telegram/parse_markdown_to_md_v2.go | 160 ++++++++++++++ ...est.go => parse_markdown_to_md_v2_test.go} | 18 +- .../telegram/parser_markdown_to_html.go | 111 ++++++++++ pkg/channels/telegram/telegram.go | 208 ++++-------------- pkg/config/config.go | 1 + pkg/config/defaults.go | 1 + .../sources/openclaw/openclaw_config.go | 26 ++- 14 files changed, 377 insertions(+), 195 deletions(-) create mode 100644 pkg/channels/telegram/parse_markdown_to_md_v2.go rename pkg/channels/telegram/{telegram_test.go => parse_markdown_to_md_v2_test.go} (69%) create mode 100644 pkg/channels/telegram/parser_markdown_to_html.go diff --git a/README.fr.md b/README.fr.md index 2bec768fc..de0af0dd1 100644 --- a/README.fr.md +++ b/README.fr.md @@ -238,7 +238,8 @@ picoclaw onboard "telegram": { "enabled": true, "token": "VOTRE_TOKEN_BOT", - "allow_from": ["VOTRE_USER_ID"] + "allow_from": ["VOTRE_USER_ID"], + "use_markdown_v2": false } }, "tools": { @@ -307,7 +308,8 @@ Discutez avec votre PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom "telegram": { "enabled": true, "token": "VOTRE_TOKEN_BOT", - "allow_from": ["VOTRE_USER_ID"] + "allow_from": ["VOTRE_USER_ID"], + "use_markdown_v2": false } } } @@ -350,7 +352,7 @@ picoclaw gateway "discord": { "enabled": true, "token": "VOTRE_TOKEN_BOT", - "allow_from": ["VOTRE_USER_ID"] + "*.md": ["VOTRE_USER_ID"] } } } @@ -866,7 +868,8 @@ picoclaw agent -m "Bonjour, comment ça va ?" "telegram": { "enabled": true, "token": "123456:ABC...", - "allow_from": ["123456789"] + "allow_from": ["123456789"], + "use_markdown_v2": false }, "discord": { "enabled": true, diff --git a/README.ja.md b/README.ja.md index 15ed1f649..c65a9006f 100644 --- a/README.ja.md +++ b/README.ja.md @@ -200,7 +200,8 @@ picoclaw onboard "telegram": { "enabled": true, "token": "YOUR_TELEGRAM_BOT_TOKEN", - "allow_from": [] + "allow_from": [], + "use_markdown_v2": false } }, "tools": { @@ -276,7 +277,8 @@ Telegram、Discord、QQ、DingTalk、LINE、WeCom で PicoClaw と会話でき "telegram": { "enabled": true, "token": "YOUR_BOT_TOKEN", - "allow_from": ["YOUR_USER_ID"] + "allow_from": ["YOUR_USER_ID"], + "use_markdown_v2": false } } } @@ -819,7 +821,8 @@ HEARTBEAT_OK 応答 ユーザーが直接結果を受け取る "telegram": { "enabled": true, "token": "123456:ABC...", - "allow_from": ["123456789"] + "allow_from": ["123456789"], + "use_markdown_v2": false }, "discord": { "enabled": true, diff --git a/README.md b/README.md index 2fc60343b..0ec8ef8e8 100644 --- a/README.md +++ b/README.md @@ -324,13 +324,15 @@ Talk to your picoclaw through Telegram, Discord, WhatsApp, DingTalk, LINE, or We "telegram": { "enabled": true, "token": "YOUR_BOT_TOKEN", - "allow_from": ["YOUR_USER_ID"] + "allow_from": ["YOUR_USER_ID"], + "use_markdown_v2": false } } } ``` > Get your user ID from `@userinfobot` on Telegram. +> Set `use_markdown_v2` to true could improve message formatting. **3. Run** @@ -1140,7 +1142,8 @@ picoclaw agent -m "Hello" "telegram": { "enabled": true, "token": "123456:ABC...", - "allow_from": ["123456789"] + "allow_from": ["123456789"], + "use_markdown_v2": false }, "discord": { "enabled": true, diff --git a/README.pt-br.md b/README.pt-br.md index 611a61281..8911cb680 100644 --- a/README.pt-br.md +++ b/README.pt-br.md @@ -301,7 +301,8 @@ Converse com seu PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom. "telegram": { "enabled": true, "token": "YOUR_BOT_TOKEN", - "allow_from": ["YOUR_USER_ID"] + "allow_from": ["YOUR_USER_ID"], + "use_markdown_v2": false } } } diff --git a/README.vi.md b/README.vi.md index e836e30f0..dfc546074 100644 --- a/README.vi.md +++ b/README.vi.md @@ -219,7 +219,8 @@ picoclaw onboard "telegram": { "enabled": true, "token": "YOUR_TELEGRAM_BOT_TOKEN", - "allow_from": [] + "allow_from": [], + "use_markdown_v2": false } } } @@ -275,7 +276,8 @@ Trò chuyện với PicoClaw qua Telegram, Discord, DingTalk, LINE hoặc WeCom. "telegram": { "enabled": true, "token": "YOUR_BOT_TOKEN", - "allow_from": ["YOUR_USER_ID"] + "allow_from": ["YOUR_USER_ID"], + "use_markdown_v2": false } } } @@ -834,7 +836,8 @@ picoclaw agent -m "Xin chào" "telegram": { "enabled": true, "token": "123456:ABC...", - "allow_from": ["123456789"] + "allow_from": ["123456789"], + "use_markdown_v2": false }, "discord": { "enabled": true, diff --git a/README.zh.md b/README.zh.md index 95984bbdf..3862d0719 100644 --- a/README.zh.md +++ b/README.zh.md @@ -713,7 +713,8 @@ picoclaw agent -m "你好" "telegram": { "enabled": true, "token": "123456:ABC...", - "allow_from": ["123456789"] + "allow_from": ["123456789"], + "use_markdown_v2": false }, "discord": { "enabled": true, diff --git a/config/config.example.json b/config/config.example.json index d885ef94b..1235d1b9d 100644 --- a/config/config.example.json +++ b/config/config.example.json @@ -50,9 +50,8 @@ "enabled": false, "token": "YOUR_TELEGRAM_BOT_TOKEN", "proxy": "", - "allow_from": [ - "YOUR_USER_ID" - ], + "allow_from": ["YOUR_USER_ID"], + "use_markdown_v2": false, "reasoning_channel_id": "" }, "discord": { diff --git a/pkg/channels/telegram/parse_markdown_to_md_v2.go b/pkg/channels/telegram/parse_markdown_to_md_v2.go new file mode 100644 index 000000000..1bebf700d --- /dev/null +++ b/pkg/channels/telegram/parse_markdown_to_md_v2.go @@ -0,0 +1,160 @@ +package telegram + +import "strings" + +// markdownToTelegramMarkdownV2 takes a standardized markdown string and +// strictly escapes or transforms it to fit Telegram's MarkdownV2 requirements. +// https://core.telegram.org/bots/api#formatting-options +func markdownToTelegramMarkdownV2(text string) string { + // replace Heading to bolding + text = reHeading.ReplaceAllString(text, "*$1*") + + var result strings.Builder + runes := []rune(text) + length := len(runes) + + // List of characters that must be escaped in standard text contexts + needsNormalEscape := func(r rune) bool { + switch r { + case '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!': + return true + } + return false + } + + i := 0 + for i < length { + // 1. Check for Pre-formatted Code Block (```...```) + if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' { + result.WriteString("```") + i += 3 + // Find closing ``` + for i < length { + if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' { + result.WriteString("```") + i += 3 + break + } + // Inside code blocks, escape `\` and `\` + if runes[i] == '\\' || runes[i] == '`' { + result.WriteRune('\\') + } + result.WriteRune(runes[i]) + i++ + } + continue + } + + // 2. Check for Inline Code (`...`) + if runes[i] == '`' { + result.WriteRune('`') + i++ + for i < length { + if runes[i] == '`' { + result.WriteRune('`') + i++ + break + } + if runes[i] == '\\' || runes[i] == '`' { + result.WriteRune('\\') + } + result.WriteRune(runes[i]) + i++ + } + continue + } + + // 3. Link or Custom Emoji definition: URL part (...) + // We detect this by checking if the previous non-space character closed a bracket ']', + // and we are currently on '('. To keep logic linear, we handle it as we traverse. + // NOTE: A true deep-parser would link `[` to `](...)`. For safety, whenever we see `(`, + // if it looks like a URL part, we escape it via URL rules. Let's do a basic lookbehind. + if runes[i] == '(' && i > 0 && runes[i-1] == ']' { + result.WriteRune('(') + i++ + for i < length { + if runes[i] == ')' { + // Unescaped closing bracket ends the URL + result.WriteRune(')') + i++ + break + } + // In URL part, escape `\` and `)` + if runes[i] == '\\' || runes[i] == ')' { + result.WriteRune('\\') + } + result.WriteRune(runes[i]) + i++ + } + continue + } + + // 4. Handle blockquotes starts + if runes[i] == '>' && (i == 0 || runes[i-1] == '\n') { + result.WriteRune('>') + i++ + continue + } + + // 5. Handle expandable block quotation starts + if runes[i] == '>' && runes[i-1] == '*' && runes[i-2] == '*' && (i == 0 || runes[i-3] == '\n') { + result.WriteRune(runes[i]) + i++ + continue + } + + // 6. Handle standard Markdown Entities Boundaries + // If they are part of valid markdown boundaries, we write them as-is. + // We trust the syntax rules: * _ ~ || [ ] + // (Assuming the text is a valid markdown, we don't escape these if formatting is intended) + + // Note on Ambiguity (__ vs _): + // Telegram parses `__` from left to right greedily. + if i+1 < length && runes[i] == '_' && runes[i+1] == '_' { + result.WriteString("__") + i += 2 + continue + } + + if i+1 < length && runes[i] == '|' && runes[i+1] == '|' { + result.WriteString("||") + i += 2 + continue + } + + // Standard single-char boundaries + if runes[i] == '*' || runes[i] == '_' || runes[i] == '~' || runes[i] == '[' || runes[i] == ']' { + result.WriteRune(runes[i]) + i++ + continue + } + + // Custom emoji boundary check `![` + if i+1 < length && runes[i] == '!' && runes[i+1] == '[' { + result.WriteString("![") + i += 2 + continue + } + + // 7. Handle plain text characters + // Escape remaining special characters if they aren't forming intended valid markup + if needsNormalEscape(runes[i]) { + // Check if it's already escaped; if an escape character exists, consume it legitimately + if runes[i] == '\\' && i+1 < length && needsNormalEscape(runes[i+1]) { + // Keep the backslash and the escaped char as is, avoiding double escaping + result.WriteRune('\\') + result.WriteRune(runes[i+1]) + i += 2 + continue + } + + // Auto-escape the character + result.WriteRune('\\') + } + + result.WriteRune(runes[i]) + i++ + } + + return result.String() +} diff --git a/pkg/channels/telegram/telegram_test.go b/pkg/channels/telegram/parse_markdown_to_md_v2_test.go similarity index 69% rename from pkg/channels/telegram/telegram_test.go rename to pkg/channels/telegram/parse_markdown_to_md_v2_test.go index 08ae14932..3d21a90c0 100644 --- a/pkg/channels/telegram/telegram_test.go +++ b/pkg/channels/telegram/parse_markdown_to_md_v2_test.go @@ -2,7 +2,6 @@ package telegram import ( _ "embed" - "fmt" "testing" "github.com/stretchr/testify/require" @@ -13,29 +12,44 @@ var md2AllFormats string func Test_markdownToTelegramMarkdownV2(t *testing.T) { cases := []struct { + name string input string expected string }{ { + name: "heading -> bolding", input: `## HeadingH2 #`, expected: "*HeadingH2 \\#*", }, { + name: "strikethrough", input: "~strikethroughMD~", expected: "~strikethroughMD~", }, { + name: "inline URL", input: "[inline URL](http://www.example.com/)", expected: "[inline URL](http://www.example.com/)", }, { + name: "all telegram formats", input: md2AllFormats, expected: md2AllFormats, }, + { + name: "empty", + input: "", + expected: "", + }, + { + name: "one letter", + input: "o", + expected: "o", + }, } for _, tc := range cases { - t.Run(fmt.Sprintf("formating %s -> %s", tc.input, tc.expected), func(t *testing.T) { + t.Run(tc.name, func(t *testing.T) { actual := markdownToTelegramMarkdownV2(tc.input) require.EqualValues(t, tc.expected, actual) diff --git a/pkg/channels/telegram/parser_markdown_to_html.go b/pkg/channels/telegram/parser_markdown_to_html.go new file mode 100644 index 000000000..bdaa51807 --- /dev/null +++ b/pkg/channels/telegram/parser_markdown_to_html.go @@ -0,0 +1,111 @@ +package telegram + +import ( + "fmt" + "strings" +) + +func markdownToTelegramHTML(text string) string { + if text == "" { + return "" + } + + codeBlocks := extractCodeBlocks(text) + text = codeBlocks.text + + inlineCodes := extractInlineCodes(text) + text = inlineCodes.text + + text = reHeading.ReplaceAllString(text, "$1") + + text = reBlockquote.ReplaceAllString(text, "$1") + + text = escapeHTML(text) + + text = reLink.ReplaceAllString(text, `$1`) + + text = reBoldStar.ReplaceAllString(text, "$1") + + text = reBoldUnder.ReplaceAllString(text, "$1") + + text = reItalic.ReplaceAllStringFunc(text, func(s string) string { + match := reItalic.FindStringSubmatch(s) + if len(match) < 2 { + return s + } + return "" + match[1] + "" + }) + + text = reStrike.ReplaceAllString(text, "$1") + + text = reListItem.ReplaceAllString(text, "• ") + + for i, code := range inlineCodes.codes { + escaped := escapeHTML(code) + text = strings.ReplaceAll(text, fmt.Sprintf("\x00IC%d\x00", i), fmt.Sprintf("%s", escaped)) + } + + for i, code := range codeBlocks.codes { + escaped := escapeHTML(code) + text = strings.ReplaceAll( + text, + fmt.Sprintf("\x00CB%d\x00", i), + fmt.Sprintf("
%s
", escaped), + ) + } + + return text +} + +type codeBlockMatch struct { + text string + codes []string +} + +func extractCodeBlocks(text string) codeBlockMatch { + matches := reCodeBlock.FindAllStringSubmatch(text, -1) + + codes := make([]string, 0, len(matches)) + for _, match := range matches { + codes = append(codes, match[1]) + } + + i := 0 + text = reCodeBlock.ReplaceAllStringFunc(text, func(m string) string { + placeholder := fmt.Sprintf("\x00CB%d\x00", i) + i++ + return placeholder + }) + + return codeBlockMatch{text: text, codes: codes} +} + +type inlineCodeMatch struct { + text string + codes []string +} + +func extractInlineCodes(text string) inlineCodeMatch { + matches := reInlineCode.FindAllStringSubmatch(text, -1) + + codes := make([]string, 0, len(matches)) + for _, match := range matches { + codes = append(codes, match[1]) + } + + i := 0 + text = reInlineCode.ReplaceAllStringFunc(text, func(m string) string { + placeholder := fmt.Sprintf("\x00IC%d\x00", i) + i++ + return placeholder + }) + + return inlineCodeMatch{text: text, codes: codes} +} + +func escapeHTML(text string) string { + text = strings.ReplaceAll(text, "&", "&") + text = strings.ReplaceAll(text, "<", "<") + text = strings.ReplaceAll(text, ">", ">") + return text +} diff --git a/pkg/channels/telegram/telegram.go b/pkg/channels/telegram/telegram.go index 9b439cc02..af0a4bbb8 100644 --- a/pkg/channels/telegram/telegram.go +++ b/pkg/channels/telegram/telegram.go @@ -25,7 +25,18 @@ import ( "github.com/sipeed/picoclaw/pkg/utils" ) -var reHeading = regexp.MustCompile(`(?m)^#{1,6}\s+([^\n]+)`) +var ( + reHeading = regexp.MustCompile(`(?m)^#{1,6}\s+([^\n]+)`) + reBlockquote = regexp.MustCompile(`^>\s*(.*)$`) + reLink = regexp.MustCompile(`\[([^\]]+)\]\(([^)]+)\)`) + reBoldStar = regexp.MustCompile(`\*\*(.+?)\*\*`) + reBoldUnder = regexp.MustCompile(`__(.+?)__`) + reItalic = regexp.MustCompile(`_([^_]+)_`) + reStrike = regexp.MustCompile(`~~(.+?)~~`) + reListItem = regexp.MustCompile(`^[-*]\s+`) + reCodeBlock = regexp.MustCompile("```[\\w]*\\n?([\\s\\S]*?)```") + reInlineCode = regexp.MustCompile("`([^`]+)`") +) type TelegramChannel struct { *channels.BaseChannel @@ -162,15 +173,13 @@ func (c *TelegramChannel) Send(ctx context.Context, msg bus.OutboundMessage) err return fmt.Errorf("invalid chat ID %s: %w", msg.ChatID, channels.ErrSendFailed) } - markdownV2Content := markdownToTelegramMarkdownV2(msg.Content) + content := c.parseContent(msg.Content) - tgMsg := tu.Message(tu.ID(chatID), markdownV2Content). + tgMsg := tu.Message(tu.ID(chatID), content). WithParseMode(telego.ModeMarkdownV2) if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil { - logger.ErrorCF("telegram", "MarkdownV2 parse failed, falling back to plain text", map[string]any{ - "error": err.Error(), - }) + logParseFailed(c.config.Channels.Telegram.UseMarkdownV2, err) if _, err = c.bot.SendMessage(ctx, tu.Message(tu.ID(chatID), msg.Content)); err != nil { return fmt.Errorf("telegram send: %w", channels.ErrTemporary) } @@ -219,10 +228,15 @@ func (c *TelegramChannel) EditMessage(ctx context.Context, chatID string, messag if err != nil { return err } - md2Content := markdownToTelegramMarkdownV2(content) - editMsg := tu.EditMessageText(tu.ID(cid), mid, md2Content). + parsedContent := c.parseContent(content) + editMsg := tu.EditMessageText(tu.ID(cid), mid, parsedContent). WithParseMode(telego.ModeMarkdownV2) _, err = c.bot.EditMessageText(ctx, editMsg) + if err != nil { + logParseFailed(c.config.Channels.Telegram.UseMarkdownV2, err) + _, err = c.bot.EditMessageText(ctx, tu.EditMessageText(tu.ID(cid), mid, content)) + } + return err } @@ -535,169 +549,33 @@ func (c *TelegramChannel) downloadFile(ctx context.Context, fileID, ext string) return c.downloadFileWithInfo(file, ext) } +func (c *TelegramChannel) parseContent(text string) string { + if c.config.Channels.Telegram.UseMarkdownV2 { + return markdownToTelegramMarkdownV2(text) + } + + return markdownToTelegramHTML(text) +} + +func logParseFailed(useMarkdownV2 bool, err error) { + parsingName := "HTML" + if useMarkdownV2 { + parsingName = "MarkdownV2" + } + logger.ErrorCF("telegram", + fmt.Sprintf("%s parse failed, falling back to plain text", parsingName), + map[string]any{ + "error": err.Error(), + }, + ) +} + func parseChatID(chatIDStr string) (int64, error) { var id int64 _, err := fmt.Sscanf(chatIDStr, "%d", &id) return id, err } -// markdownToTelegramMarkdownV2 takes a standardized markdown string and -// strictly escapes or transforms it to fit Telegram's MarkdownV2 requirements. -// https://core.telegram.org/bots/api#formatting-options -func markdownToTelegramMarkdownV2(text string) string { - // replace Heading to bolding - text = reHeading.ReplaceAllString(text, "*$1*") - - var result strings.Builder - runes := []rune(text) - length := len(runes) - - // List of characters that must be escaped in standard text contexts - needsNormalEscape := func(r rune) bool { - switch r { - case '_', '*', '[', ']', '(', ')', '~', '`', '>', '#', '+', '-', '=', '|', '{', '}', '.', '!': - return true - } - return false - } - - i := 0 - for i < length { - // 1. Check for Pre-formatted Code Block (```...```) - if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' { - result.WriteString("```") - i += 3 - // Find closing ``` - for i < length { - if i+2 < length && runes[i] == '`' && runes[i+1] == '`' && runes[i+2] == '`' { - result.WriteString("```") - i += 3 - break - } - // Inside code blocks, escape `\` and `\` - if runes[i] == '\\' || runes[i] == '`' { - result.WriteRune('\\') - } - result.WriteRune(runes[i]) - i++ - } - continue - } - - // 2. Check for Inline Code (`...`) - if runes[i] == '`' { - result.WriteRune('`') - i++ - for i < length { - if runes[i] == '`' { - result.WriteRune('`') - i++ - break - } - if runes[i] == '\\' || runes[i] == '`' { - result.WriteRune('\\') - } - result.WriteRune(runes[i]) - i++ - } - continue - } - - // 3. Link or Custom Emoji definition: URL part (...) - // We detect this by checking if the previous non-space character closed a bracket ']', - // and we are currently on '('. To keep logic linear, we handle it as we traverse. - // NOTE: A true deep-parser would link `[` to `](...)`. For safety, whenever we see `(`, - // if it looks like a URL part, we escape it via URL rules. Let's do a basic lookbehind. - if runes[i] == '(' && i > 0 && runes[i-1] == ']' { - result.WriteRune('(') - i++ - for i < length { - if runes[i] == ')' { - // Unescaped closing bracket ends the URL - result.WriteRune(')') - i++ - break - } - // In URL part, escape `\` and `)` - if runes[i] == '\\' || runes[i] == ')' { - result.WriteRune('\\') - } - result.WriteRune(runes[i]) - i++ - } - continue - } - - // 4. Handle blockquotes starts - if runes[i] == '>' && (i == 0 || runes[i-1] == '\n') { - result.WriteRune('>') - i++ - continue - } - - // 5. Handle expandable block quotation starts - if runes[i] == '>' && runes[i-1] == '*' && runes[i-2] == '*' && (i == 0 || runes[i-3] == '\n') { - result.WriteRune(runes[i]) - i++ - continue - } - - // 6. Handle standard Markdown Entities Boundaries - // If they are part of valid markdown boundaries, we write them as-is. - // We trust the syntax rules: * _ ~ || [ ] - // (Assuming the text is a valid markdown, we don't escape these if formatting is intended) - - // Note on Ambiguity (__ vs _): - // Telegram parses `__` from left to right greedily. - if i+1 < length && runes[i] == '_' && runes[i+1] == '_' { - result.WriteString("__") - i += 2 - continue - } - - if i+1 < length && runes[i] == '|' && runes[i+1] == '|' { - result.WriteString("||") - i += 2 - continue - } - - // Standard single-char boundaries - if runes[i] == '*' || runes[i] == '_' || runes[i] == '~' || runes[i] == '[' || runes[i] == ']' { - result.WriteRune(runes[i]) - i++ - continue - } - - // Custom emoji boundary check `![` - if i+1 < length && runes[i] == '!' && runes[i+1] == '[' { - result.WriteString("![") - i += 2 - continue - } - - // 7. Handle plain text characters - // Escape remaining special characters if they aren't forming intended valid markup - if needsNormalEscape(runes[i]) { - // Check if it's already escaped; if an escape character exists, consume it legitimately - if runes[i] == '\\' && i+1 < length && needsNormalEscape(runes[i+1]) { - // Keep the backslash and the escaped char as is, avoiding double escaping - result.WriteRune('\\') - result.WriteRune(runes[i+1]) - i += 2 - continue - } - - // Auto-escape the character - result.WriteRune('\\') - } - - result.WriteRune(runes[i]) - i++ - } - - return result.String() -} - // isBotMentioned checks if the bot is mentioned in the message via entities. func (c *TelegramChannel) isBotMentioned(message *telego.Message) bool { botUsername := c.bot.Username() diff --git a/pkg/config/config.go b/pkg/config/config.go index e6a842595..fe9eb5efa 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -242,6 +242,7 @@ type TelegramConfig struct { Typing TypingConfig `json:"typing,omitempty"` Placeholder PlaceholderConfig `json:"placeholder,omitempty"` ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"` + UseMarkdownV2 bool `json:"use_markdown_v2" env:"PICOCLAW_CHANNELS_TELEGRAM_USE_MARKDOWN_V2"` } type FeishuConfig struct { diff --git a/pkg/config/defaults.go b/pkg/config/defaults.go index 44f4de7e9..ca5406fdd 100644 --- a/pkg/config/defaults.go +++ b/pkg/config/defaults.go @@ -56,6 +56,7 @@ func DefaultConfig() *Config { Enabled: true, Text: "Thinking... 💭", }, + UseMarkdownV2: false, }, Feishu: FeishuConfig{ Enabled: false, diff --git a/pkg/migrate/sources/openclaw/openclaw_config.go b/pkg/migrate/sources/openclaw/openclaw_config.go index 39ad48fad..a188fe8fe 100644 --- a/pkg/migrate/sources/openclaw/openclaw_config.go +++ b/pkg/migrate/sources/openclaw/openclaw_config.go @@ -132,11 +132,12 @@ type OpenClawChannels struct { } type OpenClawTelegramConfig struct { - BotToken *string `json:"botToken"` - AllowFrom []string `json:"allowFrom"` - GroupPolicy *string `json:"groupPolicy"` - DmPolicy *string `json:"dmPolicy"` - Enabled *bool `json:"enabled"` + BotToken *string `json:"botToken"` + AllowFrom []string `json:"allowFrom"` + GroupPolicy *string `json:"groupPolicy"` + DmPolicy *string `json:"dmPolicy"` + Enabled *bool `json:"enabled"` + UseMarkdownV2 *bool `json:"useMarkdownV2"` } type OpenClawDiscordConfig struct { @@ -637,10 +638,11 @@ type WhatsAppConfig struct { } type TelegramConfig struct { - Enabled bool `json:"enabled"` - Token string `json:"token"` - Proxy string `json:"proxy"` - AllowFrom []string `json:"allow_from"` + Enabled bool `json:"enabled"` + Token string `json:"token"` + Proxy string `json:"proxy"` + AllowFrom []string `json:"allow_from"` + UseMarkdownV2 bool `json:"use_markdown_v2"` } type FeishuConfig struct { @@ -758,9 +760,11 @@ func (c *OpenClawConfig) convertChannels(warnings *[]string) ChannelsConfig { if c.Channels.Telegram != nil { enabled := c.Channels.Telegram.Enabled == nil || *c.Channels.Telegram.Enabled + useMarkdownV2 := c.Channels.Telegram.UseMarkdownV2 != nil && *c.Channels.Telegram.UseMarkdownV2 channels.Telegram = TelegramConfig{ - Enabled: enabled, - AllowFrom: c.Channels.Telegram.AllowFrom, + Enabled: enabled, + AllowFrom: c.Channels.Telegram.AllowFrom, + UseMarkdownV2: useMarkdownV2, } if c.Channels.Telegram.BotToken != nil { channels.Telegram.Token = *c.Channels.Telegram.BotToken