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:
parent
e9186e9dd2
commit
04ac125da6
|
|
@ -3,13 +3,15 @@
|
|||
|
||||
import asyncio
|
||||
import logging
|
||||
from typing import List
|
||||
import re
|
||||
from typing import List, Tuple
|
||||
from telegram import Update
|
||||
|
||||
logger = logging.getLogger(__name__)
|
||||
|
||||
# Лимиты Telegram
|
||||
MAX_MESSAGE_LENGTH = 4096 # Максимальная длина сообщения
|
||||
RESERVED_FOR_HEADER = 20 # Резервируем место для "(N/N) "
|
||||
|
||||
|
||||
def escape_markdown(text: str) -> str:
|
||||
|
|
@ -20,53 +22,136 @@ def escape_markdown(text: str) -> str:
|
|||
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:
|
||||
return [text]
|
||||
|
||||
return [(text, '```' in text)]
|
||||
|
||||
parts = []
|
||||
code_blocks = find_code_blocks(text)
|
||||
|
||||
# Текущая позиция в тексте
|
||||
pos = 0
|
||||
current = ""
|
||||
|
||||
for line in text.split('\n'):
|
||||
# Если добавление строки превысит лимит
|
||||
if len(current) + len(line) + 1 > max_length:
|
||||
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:
|
||||
parts.append(current)
|
||||
# Если строка сама по себе длиннее лимита — режем её
|
||||
while len(line) > max_length:
|
||||
parts.append(line[:max_length])
|
||||
line = line[max_length:]
|
||||
current = line
|
||||
parts.append((current, current_has_code))
|
||||
|
||||
# Если блок кода слишком длинный — режем его на части
|
||||
if len(code_block) > max_length - RESERVED_FOR_HEADER:
|
||||
# Отправляем блок кода частями без Markdown
|
||||
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:
|
||||
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:
|
||||
parts.append(current)
|
||||
|
||||
parts.append((current, current_has_code))
|
||||
|
||||
return parts
|
||||
|
||||
|
||||
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)
|
||||
total = len(parts)
|
||||
|
||||
for i, part in enumerate(parts):
|
||||
for i, (part, has_markdown) in enumerate(parts):
|
||||
# Добавляем номер части если их несколько
|
||||
if len(parts) > 1:
|
||||
header = f"({i+1}/{len(parts)}) "
|
||||
if total > 1:
|
||||
header = f"({i+1}/{total}) "
|
||||
if len(header) + len(part) <= MAX_MESSAGE_LENGTH:
|
||||
part = header + part
|
||||
|
||||
# Если это не первая часть и был Markdown — убираем parse_mode
|
||||
# чтобы не было проблем с разорванной разметкой
|
||||
actual_parse_mode = parse_mode if i == 0 else None
|
||||
# Определяем parse_mode для этого сообщения
|
||||
# Если у сообщения есть блоки кода — используем Markdown
|
||||
# Если нет — отправляем без разметки (безопаснее)
|
||||
if has_markdown and parse_mode:
|
||||
actual_parse_mode = parse_mode
|
||||
else:
|
||||
actual_parse_mode = None
|
||||
|
||||
try:
|
||||
await update.message.reply_text(part, parse_mode=actual_parse_mode)
|
||||
|
|
|
|||
Loading…
Reference in New Issue