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 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)
|
||||||
|
|
|
||||||
Loading…
Reference in New Issue