2020import json
2121import smtplib
2222import re
23+ import time
2324import markdown2
2425from datetime import datetime
2526from typing import List , Dict , Any , Optional
@@ -2039,9 +2040,8 @@ def send_to_telegram(self, content: str) -> bool:
20392040 return False
20402041
20412042 def _send_telegram_message (self , api_url : str , chat_id : str , text : str , message_thread_id : Optional [str ] = None ) -> bool :
2042- """发送单条 Telegram 消息"""
2043- # 转换 Markdown 为 Telegram 支持的格式
2044- # Telegram 的 Markdown 格式稍有不同,做简单处理
2043+ """Send a single Telegram message with exponential backoff retry (Fixes #287)"""
2044+ # Convert Markdown to Telegram-compatible format
20452045 telegram_text = self ._convert_to_telegram_markdown (text )
20462046
20472047 payload = {
@@ -2053,35 +2053,70 @@ def _send_telegram_message(self, api_url: str, chat_id: str, text: str, message_
20532053
20542054 if message_thread_id :
20552055 payload ['message_thread_id' ] = message_thread_id
2056+
2057+ max_retries = 3
2058+ for attempt in range (1 , max_retries + 1 ):
2059+ try :
2060+ response = requests .post (api_url , json = payload , timeout = 10 )
2061+ except (requests .exceptions .ConnectionError , requests .exceptions .Timeout ) as e :
2062+ if attempt < max_retries :
2063+ delay = 2 ** attempt # 2s, 4s
2064+ logger .warning (f"Telegram request failed (attempt { attempt } /{ max_retries } ): { e } , "
2065+ f"retrying in { delay } s..." )
2066+ time .sleep (delay )
2067+ continue
2068+ else :
2069+ logger .error (f"Telegram request failed after { max_retries } attempts: { e } " )
2070+ return False
20562071
2057- response = requests .post (api_url , json = payload , timeout = 10 )
2058-
2059- if response .status_code == 200 :
2060- result = response .json ()
2061- if result .get ('ok' ):
2062- logger .info ("Telegram 消息发送成功" )
2063- return True
2064- else :
2065- error_desc = result .get ('description' , '未知错误' )
2066- logger .error (f"Telegram 返回错误: { error_desc } " )
2067-
2068- # 如果 Markdown 解析失败,尝试纯文本发送
2069- if 'parse' in error_desc .lower () or 'markdown' in error_desc .lower ():
2070- logger .info ("尝试使用纯文本格式重新发送..." )
2071- payload ['parse_mode' ] = None
2072- payload ['text' ] = text # 使用原始文本
2073- del payload ['parse_mode' ]
2072+ if response .status_code == 200 :
2073+ result = response .json ()
2074+ if result .get ('ok' ):
2075+ logger .info ("Telegram 消息发送成功" )
2076+ return True
2077+ else :
2078+ error_desc = result .get ('description' , '未知错误' )
2079+ logger .error (f"Telegram 返回错误: { error_desc } " )
20742080
2075- response = requests .post (api_url , json = payload , timeout = 10 )
2076- if response .status_code == 200 and response .json ().get ('ok' ):
2077- logger .info ("Telegram 消息发送成功(纯文本)" )
2078- return True
2079-
2081+ # If Markdown parsing failed, fall back to plain text
2082+ if 'parse' in error_desc .lower () or 'markdown' in error_desc .lower ():
2083+ logger .info ("尝试使用纯文本格式重新发送..." )
2084+ plain_payload = dict (payload )
2085+ plain_payload .pop ('parse_mode' , None )
2086+ plain_payload ['text' ] = text # Use original text
2087+
2088+ try :
2089+ response = requests .post (api_url , json = plain_payload , timeout = 10 )
2090+ if response .status_code == 200 and response .json ().get ('ok' ):
2091+ logger .info ("Telegram 消息发送成功(纯文本)" )
2092+ return True
2093+ except (requests .exceptions .ConnectionError , requests .exceptions .Timeout ) as e :
2094+ logger .error (f"Telegram plain-text fallback failed: { e } " )
2095+
2096+ return False
2097+ elif response .status_code == 429 :
2098+ # Rate limited — respect Retry-After header
2099+ retry_after = int (response .headers .get ('Retry-After' , 2 ** attempt ))
2100+ if attempt < max_retries :
2101+ logger .warning (f"Telegram rate limited, retrying in { retry_after } s "
2102+ f"(attempt { attempt } /{ max_retries } )..." )
2103+ time .sleep (retry_after )
2104+ continue
2105+ else :
2106+ logger .error (f"Telegram rate limited after { max_retries } attempts" )
2107+ return False
2108+ else :
2109+ if attempt < max_retries and response .status_code >= 500 :
2110+ delay = 2 ** attempt
2111+ logger .warning (f"Telegram server error HTTP { response .status_code } "
2112+ f"(attempt { attempt } /{ max_retries } ), retrying in { delay } s..." )
2113+ time .sleep (delay )
2114+ continue
2115+ logger .error (f"Telegram 请求失败: HTTP { response .status_code } " )
2116+ logger .error (f"响应内容: { response .text } " )
20802117 return False
2081- else :
2082- logger .error (f"Telegram 请求失败: HTTP { response .status_code } " )
2083- logger .error (f"响应内容: { response .text } " )
2084- return False
2118+
2119+ return False
20852120
20862121 def _send_telegram_chunked (self , api_url : str , chat_id : str , content : str , max_length : int , message_thread_id : Optional [str ] = None ) -> bool :
20872122 """分段发送长 Telegram 消息"""
0 commit comments