fix: переписан split_message с отслеживанием состояния блока кода

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
mirivlad 2026-02-27 19:06:40 +08:00
parent ba13eb2a1a
commit 4b69ee0310
1 changed files with 59 additions and 103 deletions

View File

@ -69,18 +69,16 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple
- code_closed: True если блок кода ЗАКРЫТ в этом сообщении (есть закрывающий ```)
Алгоритм:
1. Находим все блоки кода
2. Стараемся не разрывать блоки кода
3. Если блок кода не влезает отправляем отдельным сообщением без Markdown
1. Разбиваем текст на строки
2. Накапливаем строки до достижения лимита
3. Отслеживаем состояние блока кода (внутри/снаружи)
"""
def calc_code_flags(txt: str) -> Tuple[bool, bool, bool]:
"""Вычислить флаги code для данного текста."""
has_code = '```' in txt
backtick_count = txt.count('```')
# code_opened: есть хотя бы один ```
code_opened = backtick_count >= 1
# code_closed: есть хотя бы 2 ``` (пара) или нечётное количество (открыт и закрыт в одном)
code_closed = backtick_count >= 2
code_closed = backtick_count >= 2 or (backtick_count == 1 and txt.rstrip().endswith('```'))
return has_code, code_opened, code_closed
if len(text) <= max_length:
@ -88,95 +86,63 @@ def split_message(text: str, max_length: int = MAX_MESSAGE_LENGTH) -> List[Tuple
return [(text, has_code, code_opened, code_closed)]
parts = []
code_blocks = find_code_blocks(text)
# Текущая позиция в тексте
pos = 0
lines = text.split('\n')
current = ""
in_code_block = False # Состояние: внутри блока кода или нет
code_block_started_in_current = False # Блок кода был открыт в текущей части
for block_start, block_end in code_blocks:
# Текст до блока кода
before_code = text[pos:block_start]
for line in lines:
# Проверяем, содержит ли строка ```
backticks_in_line = line.count('```')
# Сам блок кода (включая ```)
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 backticks_in_line % 2 == 1:
# Эта строка содержит ``` который меняет состояние
if in_code_block:
# Были внутри блока — эта строка закрывает его
test_line = current + ('\n' if current else '') + line
if len(test_line) > max_length - RESERVED_FOR_HEADER:
# Сначала отправляем текущее (блок не закрыт в этой части!)
if current:
has_code, code_opened, _ = calc_code_flags(current)
# code_closed=False потому что блок продолжится
parts.append((current, has_code, code_opened, False))
current = line
code_block_started_in_current = False
else:
current = test_line
code_block_started_in_current = True
in_code_block = False
else:
# Были снаружи — эта строка открывает блок
test_line = current + ('\n' if current else '') + line
if len(test_line) > max_length - RESERVED_FOR_HEADER:
if current:
has_code, code_opened, code_closed = calc_code_flags(current)
parts.append((current, has_code, code_opened, code_closed))
current = line
code_block_started_in_current = True
else:
current += ('\n' if current else '') + line
# Проверяем влезет ли блок кода
if len(current) + len(code_block) + 1 > max_length - RESERVED_FOR_HEADER:
# Отправляем что накопилось
current = test_line
code_block_started_in_current = True
in_code_block = True
else:
# Строка не меняет состояние
test_line = current + ('\n' if current else '') + line
if len(test_line) > max_length - RESERVED_FOR_HEADER:
if current:
has_code, code_opened, code_closed = calc_code_flags(current)
parts.append((current, has_code, code_opened, code_closed))
# Если блок кода слишком длинный — режем его на части
if len(code_block) > max_length - RESERVED_FOR_HEADER:
# Блок кода включает ```, нужно резать правильно
# block_start и block_end включают оба ```
# Находим позицию открывающего и закрывающего ```
open_end = code_block.find('```', 3) + 3 # Конец открывающего ```
close_start = code_block.rfind('```') # Начало закрывающего ```
# Содержимое блока без ```
code_content = code_block[3:close_start] # Только содержимое
# Режем содержимое на части
content_max_len = max_length - RESERVED_FOR_HEADER - 3 # -3 для ```
chunks = []
for i in range(0, len(code_content), content_max_len):
chunks.append(code_content[i:i + content_max_len])
# Создаём части с правильными ```
for i, chunk in enumerate(chunks):
if i == 0 and len(chunks) == 1:
# Один чанк — полный блок
full_block = f"```{chunk}```"
parts.append((full_block, True, True, True))
elif i == 0:
# Первая часть — только открывающий ```
first_part = f"```{chunk}"
parts.append((first_part, True, True, False))
elif i == len(chunks) - 1:
# Последняя часть — только закрывающий ```
last_part = f"{chunk}```"
parts.append((last_part, True, False, True))
else:
# Средняя часть — без ```
parts.append((chunk, False, False, False))
current = ""
else:
# Блок влезает в следующее сообщение
current = code_block
else:
# Блок кода влезает в текущее сообщение
current += ('\n' if current else '') + code_block
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:
has_code, code_opened, code_closed = calc_code_flags(current)
has_code, code_opened, _ = calc_code_flags(current)
# Если мы внутри блока кода — block не закрыт
code_closed = not in_code_block
parts.append((current, has_code, code_opened, code_closed))
current = line
code_block_started_in_current = in_code_block
else:
current += ('\n' if current else '') + line
current = test_line
if current:
has_code, code_opened, code_closed = calc_code_flags(current)
has_code, code_opened, _ = calc_code_flags(current)
code_closed = not in_code_block
parts.append((current, has_code, code_opened, code_closed))
return parts
@ -208,30 +174,20 @@ async def send_long_message(update: Update, text: str, parse_mode: str = None, p
for i, (part, has_code, code_opened, code_closed) in enumerate(parts):
# Определяем parse_mode для этого сообщения
actual_parse_mode = parse_mode if parse_mode and (has_code or code_opened or code_closed) else None
actual_parse_mode = parse_mode if parse_mode and has_code else None
# Логика работы с блоками кода между сообщениями:
need_prepend = False
need_append = False
# Если предыдущее сообщение не закрыло блок — нужно открыть в этом
# Если текущее сообщение не закрыло блок — нужно закрыть в следующем
if total > 1 and actual_parse_mode:
# Проверяем нужно ли открыть блок в начале этого сообщения
if i > 0 and not parts[i-1][3]: # предыдущее не закрыло блок (parts[i-1][3] = code_closed)
need_prepend = True
if i > 0 and not parts[i-1][3]: # предыдущее не закрыло блок
part = "```\n" + part
# Проверяем нужно ли закрыть блок в конце этого сообщения
# Если текущее не закрыло блок и следующее не имеет кода/не закроет
if i < total - 1 and not code_closed:
next_has = parts[i+1][1]
next_opened = parts[i+1][2]
# Закрываем если следующее не имеет кода или не открывает свой блок
if not next_has:
need_append = True
# Применяем модификации
if need_prepend:
part = "```\n" + part
if need_append:
# Следующее сообщение не начинается с ``` — закрываем блок
if not parts[i+1][2]: # следующее не открывает блок
part = part + "\n```"
# Добавляем номер части ПОСЛЕ работы с блоками кода