feat: added flag use_markdown_v2, corrected config, updated

documentation
This commit is contained in:
Aleksandr Bortnikov 2026-03-06 14:43:58 +03:00
parent dd9fb95b2a
commit 1b189f382e
14 changed files with 377 additions and 195 deletions

View file

@ -238,7 +238,8 @@ picoclaw onboard
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "VOTRE_TOKEN_BOT", "token": "VOTRE_TOKEN_BOT",
"allow_from": ["VOTRE_USER_ID"] "allow_from": ["VOTRE_USER_ID"],
"use_markdown_v2": false
} }
}, },
"tools": { "tools": {
@ -307,7 +308,8 @@ Discutez avec votre PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "VOTRE_TOKEN_BOT", "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": { "discord": {
"enabled": true, "enabled": true,
"token": "VOTRE_TOKEN_BOT", "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": { "telegram": {
"enabled": true, "enabled": true,
"token": "123456:ABC...", "token": "123456:ABC...",
"allow_from": ["123456789"] "allow_from": ["123456789"],
"use_markdown_v2": false
}, },
"discord": { "discord": {
"enabled": true, "enabled": true,

View file

@ -200,7 +200,8 @@ picoclaw onboard
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "YOUR_TELEGRAM_BOT_TOKEN", "token": "YOUR_TELEGRAM_BOT_TOKEN",
"allow_from": [] "allow_from": [],
"use_markdown_v2": false
} }
}, },
"tools": { "tools": {
@ -276,7 +277,8 @@ Telegram、Discord、QQ、DingTalk、LINE、WeCom で PicoClaw と会話でき
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "YOUR_BOT_TOKEN", "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": { "telegram": {
"enabled": true, "enabled": true,
"token": "123456:ABC...", "token": "123456:ABC...",
"allow_from": ["123456789"] "allow_from": ["123456789"],
"use_markdown_v2": false
}, },
"discord": { "discord": {
"enabled": true, "enabled": true,

View file

@ -324,13 +324,15 @@ Talk to your picoclaw through Telegram, Discord, WhatsApp, DingTalk, LINE, or We
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "YOUR_BOT_TOKEN", "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. > Get your user ID from `@userinfobot` on Telegram.
> Set `use_markdown_v2` to true could improve message formatting.
**3. Run** **3. Run**
@ -1140,7 +1142,8 @@ picoclaw agent -m "Hello"
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "123456:ABC...", "token": "123456:ABC...",
"allow_from": ["123456789"] "allow_from": ["123456789"],
"use_markdown_v2": false
}, },
"discord": { "discord": {
"enabled": true, "enabled": true,

View file

@ -301,7 +301,8 @@ Converse com seu PicoClaw via Telegram, Discord, DingTalk, LINE ou WeCom.
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "YOUR_BOT_TOKEN", "token": "YOUR_BOT_TOKEN",
"allow_from": ["YOUR_USER_ID"] "allow_from": ["YOUR_USER_ID"],
"use_markdown_v2": false
} }
} }
} }

View file

@ -219,7 +219,8 @@ picoclaw onboard
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "YOUR_TELEGRAM_BOT_TOKEN", "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": { "telegram": {
"enabled": true, "enabled": true,
"token": "YOUR_BOT_TOKEN", "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": { "telegram": {
"enabled": true, "enabled": true,
"token": "123456:ABC...", "token": "123456:ABC...",
"allow_from": ["123456789"] "allow_from": ["123456789"],
"use_markdown_v2": false
}, },
"discord": { "discord": {
"enabled": true, "enabled": true,

View file

@ -713,7 +713,8 @@ picoclaw agent -m "你好"
"telegram": { "telegram": {
"enabled": true, "enabled": true,
"token": "123456:ABC...", "token": "123456:ABC...",
"allow_from": ["123456789"] "allow_from": ["123456789"],
"use_markdown_v2": false
}, },
"discord": { "discord": {
"enabled": true, "enabled": true,

View file

@ -50,9 +50,8 @@
"enabled": false, "enabled": false,
"token": "YOUR_TELEGRAM_BOT_TOKEN", "token": "YOUR_TELEGRAM_BOT_TOKEN",
"proxy": "", "proxy": "",
"allow_from": [ "allow_from": ["YOUR_USER_ID"],
"YOUR_USER_ID" "use_markdown_v2": false,
],
"reasoning_channel_id": "" "reasoning_channel_id": ""
}, },
"discord": { "discord": {

View file

@ -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()
}

View file

@ -2,7 +2,6 @@ package telegram
import ( import (
_ "embed" _ "embed"
"fmt"
"testing" "testing"
"github.com/stretchr/testify/require" "github.com/stretchr/testify/require"
@ -13,29 +12,44 @@ var md2AllFormats string
func Test_markdownToTelegramMarkdownV2(t *testing.T) { func Test_markdownToTelegramMarkdownV2(t *testing.T) {
cases := []struct { cases := []struct {
name string
input string input string
expected string expected string
}{ }{
{ {
name: "heading -> bolding",
input: `## HeadingH2 #`, input: `## HeadingH2 #`,
expected: "*HeadingH2 \\#*", expected: "*HeadingH2 \\#*",
}, },
{ {
name: "strikethrough",
input: "~strikethroughMD~", input: "~strikethroughMD~",
expected: "~strikethroughMD~", expected: "~strikethroughMD~",
}, },
{ {
name: "inline URL",
input: "[inline URL](http://www.example.com/)", input: "[inline URL](http://www.example.com/)",
expected: "[inline URL](http://www.example.com/)", expected: "[inline URL](http://www.example.com/)",
}, },
{ {
name: "all telegram formats",
input: md2AllFormats, input: md2AllFormats,
expected: md2AllFormats, expected: md2AllFormats,
}, },
{
name: "empty",
input: "",
expected: "",
},
{
name: "one letter",
input: "o",
expected: "o",
},
} }
for _, tc := range cases { 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) actual := markdownToTelegramMarkdownV2(tc.input)
require.EqualValues(t, tc.expected, actual) require.EqualValues(t, tc.expected, actual)

View file

@ -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, `<a href="$2">$1</a>`)
text = reBoldStar.ReplaceAllString(text, "<b>$1</b>")
text = reBoldUnder.ReplaceAllString(text, "<b>$1</b>")
text = reItalic.ReplaceAllStringFunc(text, func(s string) string {
match := reItalic.FindStringSubmatch(s)
if len(match) < 2 {
return s
}
return "<i>" + match[1] + "</i>"
})
text = reStrike.ReplaceAllString(text, "<s>$1</s>")
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("<code>%s</code>", escaped))
}
for i, code := range codeBlocks.codes {
escaped := escapeHTML(code)
text = strings.ReplaceAll(
text,
fmt.Sprintf("\x00CB%d\x00", i),
fmt.Sprintf("<pre><code>%s</code></pre>", 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, "&", "&amp;")
text = strings.ReplaceAll(text, "<", "&lt;")
text = strings.ReplaceAll(text, ">", "&gt;")
return text
}

View file

@ -25,7 +25,18 @@ import (
"github.com/sipeed/picoclaw/pkg/utils" "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 { type TelegramChannel struct {
*channels.BaseChannel *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) 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) WithParseMode(telego.ModeMarkdownV2)
if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil { if _, err = c.bot.SendMessage(ctx, tgMsg); err != nil {
logger.ErrorCF("telegram", "MarkdownV2 parse failed, falling back to plain text", map[string]any{ logParseFailed(c.config.Channels.Telegram.UseMarkdownV2, err)
"error": err.Error(),
})
if _, err = c.bot.SendMessage(ctx, tu.Message(tu.ID(chatID), msg.Content)); err != nil { if _, err = c.bot.SendMessage(ctx, tu.Message(tu.ID(chatID), msg.Content)); err != nil {
return fmt.Errorf("telegram send: %w", channels.ErrTemporary) 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 { if err != nil {
return err return err
} }
md2Content := markdownToTelegramMarkdownV2(content) parsedContent := c.parseContent(content)
editMsg := tu.EditMessageText(tu.ID(cid), mid, md2Content). editMsg := tu.EditMessageText(tu.ID(cid), mid, parsedContent).
WithParseMode(telego.ModeMarkdownV2) WithParseMode(telego.ModeMarkdownV2)
_, err = c.bot.EditMessageText(ctx, editMsg) _, 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 return err
} }
@ -535,169 +549,33 @@ func (c *TelegramChannel) downloadFile(ctx context.Context, fileID, ext string)
return c.downloadFileWithInfo(file, ext) 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) { func parseChatID(chatIDStr string) (int64, error) {
var id int64 var id int64
_, err := fmt.Sscanf(chatIDStr, "%d", &id) _, err := fmt.Sscanf(chatIDStr, "%d", &id)
return id, err 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. // isBotMentioned checks if the bot is mentioned in the message via entities.
func (c *TelegramChannel) isBotMentioned(message *telego.Message) bool { func (c *TelegramChannel) isBotMentioned(message *telego.Message) bool {
botUsername := c.bot.Username() botUsername := c.bot.Username()

View file

@ -242,6 +242,7 @@ type TelegramConfig struct {
Typing TypingConfig `json:"typing,omitempty"` Typing TypingConfig `json:"typing,omitempty"`
Placeholder PlaceholderConfig `json:"placeholder,omitempty"` Placeholder PlaceholderConfig `json:"placeholder,omitempty"`
ReasoningChannelID string `json:"reasoning_channel_id" env:"PICOCLAW_CHANNELS_TELEGRAM_REASONING_CHANNEL_ID"` 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 { type FeishuConfig struct {

View file

@ -56,6 +56,7 @@ func DefaultConfig() *Config {
Enabled: true, Enabled: true,
Text: "Thinking... 💭", Text: "Thinking... 💭",
}, },
UseMarkdownV2: false,
}, },
Feishu: FeishuConfig{ Feishu: FeishuConfig{
Enabled: false, Enabled: false,

View file

@ -137,6 +137,7 @@ type OpenClawTelegramConfig struct {
GroupPolicy *string `json:"groupPolicy"` GroupPolicy *string `json:"groupPolicy"`
DmPolicy *string `json:"dmPolicy"` DmPolicy *string `json:"dmPolicy"`
Enabled *bool `json:"enabled"` Enabled *bool `json:"enabled"`
UseMarkdownV2 *bool `json:"useMarkdownV2"`
} }
type OpenClawDiscordConfig struct { type OpenClawDiscordConfig struct {
@ -641,6 +642,7 @@ type TelegramConfig struct {
Token string `json:"token"` Token string `json:"token"`
Proxy string `json:"proxy"` Proxy string `json:"proxy"`
AllowFrom []string `json:"allow_from"` AllowFrom []string `json:"allow_from"`
UseMarkdownV2 bool `json:"use_markdown_v2"`
} }
type FeishuConfig struct { type FeishuConfig struct {
@ -758,9 +760,11 @@ func (c *OpenClawConfig) convertChannels(warnings *[]string) ChannelsConfig {
if c.Channels.Telegram != nil { if c.Channels.Telegram != nil {
enabled := c.Channels.Telegram.Enabled == nil || *c.Channels.Telegram.Enabled enabled := c.Channels.Telegram.Enabled == nil || *c.Channels.Telegram.Enabled
useMarkdownV2 := c.Channels.Telegram.UseMarkdownV2 != nil && *c.Channels.Telegram.UseMarkdownV2
channels.Telegram = TelegramConfig{ channels.Telegram = TelegramConfig{
Enabled: enabled, Enabled: enabled,
AllowFrom: c.Channels.Telegram.AllowFrom, AllowFrom: c.Channels.Telegram.AllowFrom,
UseMarkdownV2: useMarkdownV2,
} }
if c.Channels.Telegram.BotToken != nil { if c.Channels.Telegram.BotToken != nil {
channels.Telegram.Token = *c.Channels.Telegram.BotToken channels.Telegram.Token = *c.Channels.Telegram.BotToken