fix: умная разбивка длинных сообщений с сохранением форматирования

- find_code_blocks — поиск блоков кода
- split_message — возвращает (text, has_markdown) для каждого сообщения
- send_long_message — отправляет блоки кода с Markdown, остальное без разметки
- Блоки кода не разрываются между сообщениями
- Слинные блоки кода отправляются частями без Markdown
- Нумерация добавляется автоматически

Version: 0.5.2

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
mirivlad 2026-02-24 23:58:04 +08:00
parent e9186e9dd2
commit 04ac125da6
1 changed files with 112 additions and 27 deletions

View File

@ -3,13 +3,15 @@
import asyncio import asyncio
import logging import logging
from typing import List import re
from typing import List, Tuple
from telegram import Update from telegram import Update
logger = logging.getLogger(__name__) logger = logging.getLogger(__name__)
# Лимиты Telegram # Лимиты Telegram
MAX_MESSAGE_LENGTH = 4096 # Максимальная длина сообщения MAX_MESSAGE_LENGTH = 4096 # Максимальная длина сообщения
RESERVED_FOR_HEADER = 20 # Резервируем место для "(N/N) "
def escape_markdown(text: str) -> str: def escape_markdown(text: str) -> str:
@ -20,32 +22,106 @@ def escape_markdown(text: str) -> str:
return text return text
def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[str]: def find_code_blocks(text: str) -> List[Tuple[int, int]]:
""" """
Разбить длинный текст на сообщения <= max_length символов. Найти все блоки кода (```) в тексте.
Старается разбивать по границам строк или блоков кода. Возвращает список кортежей (start, end) для каждого блока.
"""
blocks = []
pattern = re.compile(r'```')
matches = list(pattern.finditer(text))
# Пары start-end для каждого блока
i = 0
while i < len(matches) - 1:
start = matches[i].start()
end = matches[i + 1].end()
blocks.append((start, end))
i += 2
return blocks
def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple[str, bool]]:
"""
Умно разбить длинный текст на сообщения <= max_length символов.
Возвращает список кортежей (text, has_markdown):
- text: текст сообщения
- has_markdown: True если сообщение содержит блоки кода
Алгоритм:
1. Находим все блоки кода
2. Стараемся не разрывать блоки кода
3. Если блок кода не влезает отправляем отдельным сообщением без Markdown
""" """
if len(text) <= max_length: if len(text) <= max_length:
return [text] return [(text, '```' in text)]
parts = [] parts = []
current = "" code_blocks = find_code_blocks(text)
for line in text.split('\n'): # Текущая позиция в тексте
# Если добавление строки превысит лимит pos = 0
if len(current) + len(line) + 1 > max_length: current = ""
current_has_code = False
for block_start, block_end in code_blocks:
# Текст до блока кода
before_code = text[pos:block_start]
# Сам блок кода (включая ```)
code_block = text[block_start:block_end]
# Обрабатываем текст до блока
for line in before_code.split('\n'):
if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER:
if current:
parts.append((current, current_has_code))
current = line
current_has_code = False
else:
current += ('\n' if current else '') + line
# Проверяем влезет ли блок кода
if len(current) + len(code_block) + 1 > max_length - RESERVED_FOR_HEADER:
# Отправляем что накопилось
if current: if current:
parts.append(current) parts.append((current, current_has_code))
# Если строка сама по себе длиннее лимита — режем её
while len(line) > max_length: # Если блок кода слишком длинный — режем его на части
parts.append(line[:max_length]) if len(code_block) > max_length - RESERVED_FOR_HEADER:
line = line[max_length:] # Отправляем блок кода частями без Markdown
current = line for i in range(0, len(code_block), max_length - RESERVED_FOR_HEADER):
chunk = code_block[i:i + max_length - RESERVED_FOR_HEADER]
parts.append((chunk, False)) # Без Markdown!
current = ""
current_has_code = False
else:
# Блок влезает в следующее сообщение
current = code_block
current_has_code = True
else: else:
current += ('\n' if current else '') + line # Блок кода влезает в текущее сообщение
current += ('\n' if current else '') + code_block
current_has_code = True
pos = block_end
# Обрабатываем остаток текста после последнего блока
if pos < len(text):
remaining = text[pos:]
for line in remaining.split('\n'):
if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER:
if current:
parts.append((current, current_has_code))
current = line
current_has_code = False
else:
current += ('\n' if current else '') + line
if current: if current:
parts.append(current) parts.append((current, current_has_code))
return parts return parts
@ -53,20 +129,29 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[str]:
async def send_long_message(update: Update, text: str, parse_mode: str = None): async def send_long_message(update: Update, text: str, parse_mode: str = None):
""" """
Отправить длинный текст, разбив на несколько сообщений. Отправить длинный текст, разбив на несколько сообщений.
Если parse_mode="Markdown" и текст содержит блоки кода отправляет без разметки.
Умная разбивка:
- Блоки кода не разрываются между сообщениями
- Если блок кода не влезает отправляется без Markdown
- Нумерация (1/3), (2/3) только если сообщений > 1
""" """
parts = split_message(text) parts = split_message(text)
total = len(parts)
for i, part in enumerate(parts): for i, (part, has_markdown) in enumerate(parts):
# Добавляем номер части если их несколько # Добавляем номер части если их несколько
if len(parts) > 1: if total > 1:
header = f"({i+1}/{len(parts)}) " header = f"({i+1}/{total}) "
if len(header) + len(part) <= MAX_MESSAGE_LENGTH: if len(header) + len(part) <= MAX_MESSAGE_LENGTH:
part = header + part part = header + part
# Если это не первая часть и был Markdown — убираем parse_mode # Определяем parse_mode для этого сообщения
# чтобы не было проблем с разорванной разметкой # Если у сообщения есть блоки кода — используем Markdown
actual_parse_mode = parse_mode if i == 0 else None # Если нет — отправляем без разметки (безопаснее)
if has_markdown and parse_mode:
actual_parse_mode = parse_mode
else:
actual_parse_mode = None
try: try:
await update.message.reply_text(part, parse_mode=actual_parse_mode) await update.message.reply_text(part, parse_mode=actual_parse_mode)