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

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
mirivlad 2026-02-27 18:11:46 +08:00
parent fbf0edc60a
commit 0a8801afec
1 changed files with 57 additions and 37 deletions

View File

@ -58,87 +58,110 @@ def find_code_blocks(text: str) -> List[Tuple[int, int]]:
return blocks return blocks
def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple[str, bool]]: def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple[str, bool, bool, bool]]:
""" """
Умно разбить длинный текст на сообщения <= max_length символов. Умно разбить длинный текст на сообщения <= max_length символов.
Возвращает список кортежей (text, has_markdown): Возвращает список кортежей (text, has_code, code_opened, code_closed):
- text: текст сообщения - text: текст сообщения
- has_markdown: True если сообщение содержит блоки кода - has_code: True если сообщение содержит часть блока кода
- code_opened: True если блок кода ОТКРЫТ в этом сообщении (есть открывающий ```)
- code_closed: True если блок кода ЗАКРЫТ в этом сообщении (есть закрывающий ```)
Алгоритм: Алгоритм:
1. Находим все блоки кода 1. Находим все блоки кода
2. Стараемся не разрывать блоки кода 2. Стараемся не разрывать блоки кода
3. Если блок кода не влезает отправляем отдельным сообщением без Markdown 3. Если блок кода не влезает отправляем отдельным сообщением без Markdown
""" """
if len(text) <= max_length: if len(text) <= max_length:
return [(text, '```' in text)] has_code = '```' in text
# Считаем количество ``` для определения открыт/закрыт
backtick_count = text.count('```')
# Если 1 или нечётное количество — блок открыт но не закрыт (или наоборот)
code_opened = backtick_count >= 1 and (backtick_count % 2 == 1)
code_closed = backtick_count >= 2 and (backtick_count % 2 == 0)
return [(text, has_code, code_opened, code_closed)]
parts = [] parts = []
code_blocks = find_code_blocks(text) code_blocks = find_code_blocks(text)
# Текущая позиция в тексте # Текущая позиция в тексте
pos = 0 pos = 0
current = "" current = ""
current_has_code = False current_has_code = False
current_code_opened = False
current_code_closed = False
for block_start, block_end in code_blocks: for block_start, block_end in code_blocks:
# Текст до блока кода # Текст до блока кода
before_code = text[pos:block_start] before_code = text[pos:block_start]
# Сам блок кода (включая ```) # Сам блок кода (включая ```)
code_block = text[block_start:block_end] code_block = text[block_start:block_end]
# Обрабатываем текст до блока # Обрабатываем текст до блока
for line in before_code.split('\n'): for line in before_code.split('\n'):
if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER: if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER:
if current: if current:
parts.append((current, current_has_code)) parts.append((current, current_has_code, current_code_opened, current_code_closed))
current = line current = line
current_has_code = False current_has_code = False
current_code_opened = False
current_code_closed = False
else: else:
current += ('\n' if current else '') + line current += ('\n' if current else '') + line
# Проверяем влезет ли блок кода # Проверяем влезет ли блок кода
if len(current) + len(code_block) + 1 > max_length - RESERVED_FOR_HEADER: if len(current) + len(code_block) + 1 > max_length - RESERVED_FOR_HEADER:
# Отправляем что накопилось # Отправляем что накопилось
if current: if current:
parts.append((current, current_has_code)) parts.append((current, current_has_code, current_code_opened, current_code_closed))
# Если блок кода слишком длинный — режем его на части # Если блок кода слишком длинный — режем его на части
if len(code_block) > max_length - RESERVED_FOR_HEADER: if len(code_block) > max_length - RESERVED_FOR_HEADER:
# Отправляем блок кода частями без Markdown # Отправляем блок кода частями без Markdown
for i in range(0, len(code_block), max_length - RESERVED_FOR_HEADER): for i in range(0, len(code_block), max_length - RESERVED_FOR_HEADER):
chunk = code_block[i:i + max_length - RESERVED_FOR_HEADER] chunk = code_block[i:i + max_length - RESERVED_FOR_HEADER]
parts.append((chunk, False)) # Без Markdown! # Первая часть имеет открывающий ```, последняя — закрывающий
first_chunk = (i == 0)
last_chunk = (i + max_length - RESERVED_FOR_HEADER >= len(code_block))
parts.append((chunk, True, first_chunk, last_chunk))
current = "" current = ""
current_has_code = False current_has_code = False
current_code_opened = False
current_code_closed = False
else: else:
# Блок влезает в следующее сообщение # Блок влезает в следующее сообщение
current = code_block current = code_block
current_has_code = True current_has_code = True
current_code_opened = True # Блок начинается с ```
current_code_closed = True # Блок заканчивается на ```
else: else:
# Блок кода влезает в текущее сообщение # Блок кода влезает в текущее сообщение
current += ('\n' if current else '') + code_block current += ('\n' if current else '') + code_block
current_has_code = True current_has_code = True
current_code_opened = True # Блок содержит открывающий ```
current_code_closed = True # Блок содержит закрывающий ```
pos = block_end pos = block_end
# Обрабатываем остаток текста после последнего блока # Обрабатываем остаток текста после последнего блока
if pos < len(text): if pos < len(text):
remaining = text[pos:] remaining = text[pos:]
for line in remaining.split('\n'): for line in remaining.split('\n'):
if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER: if len(current) + len(line) + 1 > max_length - RESERVED_FOR_HEADER:
if current: if current:
parts.append((current, current_has_code)) parts.append((current, current_has_code, current_code_opened, current_code_closed))
current = line current = line
current_has_code = False current_has_code = False
current_code_opened = False
current_code_closed = False
else: else:
current += ('\n' if current else '') + line current += ('\n' if current else '') + line
if current: if current:
parts.append((current, current_has_code)) parts.append((current, current_has_code, current_code_opened, current_code_closed))
return parts return parts
@ -150,9 +173,7 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p
- Блоки кода не разрываются между сообщениями - Блоки кода не разрываются между сообщениями
- Если блок кода не влезает отправляется без Markdown - Если блок кода не влезает отправляется без Markdown
- Нумерация (1/N), (2/N) только если сообщений > 1 - Нумерация (1/N), (2/N) только если сообщений > 1
- Если сообщение содержит блок кода, он закрывается в конце и открывается в начале следующего - КАЖДЫЕ pause_every сообщений пауза с кнопками "Продолжить" / "Отменить"
- КАЖДЫЕ pause_after сообщений пауза с кнопками "Продолжить" / "Отменить"
- После нажатия кнопки они удаляются
Args: Args:
update: Telegram update update: Telegram update
@ -168,7 +189,7 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p
messages_sent = 0 messages_sent = 0
wait_msg = None wait_msg = None
for i, (part, has_code) in enumerate(parts): for i, (part, has_code, code_opened, code_closed) in enumerate(parts):
# Добавляем номер части если их несколько # Добавляем номер части если их несколько
if total > 1: if total > 1:
header = f"({i+1}/{total}) " header = f"({i+1}/{total}) "
@ -176,20 +197,19 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p
part = header + part part = header + part
# Определяем parse_mode для этого сообщения # Определяем parse_mode для этого сообщения
if parse_mode and has_code: actual_parse_mode = parse_mode if parse_mode and has_code else None
actual_parse_mode = parse_mode
elif parse_mode and not has_code:
actual_parse_mode = parse_mode
else:
actual_parse_mode = None
# Если это не первое сообщение и предыдущее имело блок кода — открываем блок # Логика работы с блоками кода между сообщениями:
# Если это не последнее сообщение и текущее имеет блок кода — закрываем блок # - Если предыдущее сообщение не закрыло блок (code_closed=False) и имело код — открываем в этом
if total > 1 and actual_parse_mode: # - Если текущее сообщение не закрывает блок (code_closed=False) и следующее имеет код — не закрываем
if i > 0 and parts[i-1][1]: # Предыдущее имело блок кода if total > 1 and actual_parse_mode and has_code:
# Проверяем нужно ли открыть блок в начале этого сообщения
if i > 0 and not parts[i-1][3]: # предыдущее не закрыло блок (parts[i-1][3] = code_closed)
part = "```\n" + part part = "```\n" + part
if i < total - 1 and has_code: # Следующее будет иметь блок кода # Проверяем нужно ли закрыть блок в конце этого сообщения
# Закрываем только если следующее сообщение НЕ имеет кода
if i < total - 1 and not code_closed and not parts[i+1][1]: # следующее не имеет кода
part = part + "\n```" part = part + "\n```"
try: try: