Sfoglia il codice sorgente

Découpage des messages : plus de bloc de code vide au début d'un morceau

Quand la ligne qui ferme un bloc de code (« ``` ») arrivait dans un
morceau presque plein, elle gardait quand même la place d'une fermeture
artificielle (4 caractères). Elle n'entrait donc pas : le morceau était
refermé artificiellement, et le suivant commençait par la réouverture
(« ```py ») suivie tout de suite de la vraie fermeture. Discord affichait
un bloc de code vide. Rien n'était perdu, c'était un défaut d'affichage.

Une ligne de fermeture ne garde plus cette place, sauf si elle est
elle-même découpée en plusieurs morceaux : le bloc reste alors ouvert
entre ses morceaux, et sans cette place ils dépasseraient la limite.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 12 ore fa
parent
commit
9edfe6b277
3 ha cambiato i file con 30 aggiunte e 4 eliminazioni
  1. 1 1
      CHANGELOG.md
  2. 10 3
      bot/text_utils.py
  3. 19 0
      tests/test_text_utils.py

+ 1 - 1
CHANGELOG.md

@@ -52,7 +52,7 @@ Les versions les plus récentes sont en haut. Chaque entrée commence par un ré
 
 - **Une clé Anthropic ou un modèle faux arrête le bot.** Une `ANTHROPIC_API_KEY` ou un `CLAUDE_MODEL` invalide ne se voyait qu'au premier message, avec « Je suis mal configuré ». Le bot les vérifie maintenant au démarrage ; seul un crédit épuisé ne se voit toujours qu'au premier message.
 
-- **Une longue réponse avec du code n'est plus coupée.** Quand un bloc de code commençait juste à la fin d'un morceau de 2000 caractères, ce morceau dépassait la limite de Discord et la suite de la réponse n'était pas envoyée. Le bloc passe maintenant au morceau suivant ; même chose pour les fiches de `/memoire`.
+- **Une longue réponse avec du code n'est plus coupée.** Quand un bloc de code commençait juste à la fin d'un morceau de 2000 caractères, ce morceau dépassait la limite de Discord et la suite de la réponse n'était pas envoyée. Le bloc passe maintenant au morceau suivant ; même chose pour les fiches de `/memoire`. Un bloc qui se termine juste à la fin d'un morceau n'affiche plus de bloc vide au début du suivant.
 
 - **Une date sans fuseau dans `presence_state.json` n'empêche plus le démarrage.** Après une modification à la main, une date comme `2026-11-03T10:00:00` arrêtait le bot dès le lancement. Le fichier est maintenant ignoré, avec un avertissement dans le journal, et Rika tire un nouveau statut.
 

+ 10 - 3
bot/text_utils.py

@@ -59,10 +59,17 @@ def split_message(text: str, limit: int = DISCORD_LIMIT) -> list[str]:
 
     for line in text.split("\n"):
         match = _FENCE_RE.match(line)
-        # Une ligne qui ouvre un bloc doit laisser la place de le refermer.
-        room_to_close = len("\n```") if match and open_fence is None else closing_len()
         # Une ligne trop longue à elle seule est découpée en morceaux plus petits.
-        for piece in _split_long_line(line, limit - 10 - (len(open_fence) if open_fence else 0)):
+        pieces = _split_long_line(line, limit - 10 - (len(open_fence) if open_fence else 0))
+        # Une ligne qui ouvre un bloc doit laisser la place de le refermer. Celle qui le ferme n'en a pas
+        # besoin, sauf si elle est découpée : le bloc reste alors ouvert entre ses morceaux.
+        if match and open_fence is None:
+            room_to_close = len("\n```")
+        elif match and len(pieces) == 1:
+            room_to_close = 0
+        else:
+            room_to_close = closing_len()
+        for piece in pieces:
             addition = piece + "\n"
             if len(current) + len(addition) + room_to_close > limit:
                 flush()

+ 19 - 0
tests/test_text_utils.py

@@ -131,6 +131,25 @@ class SplitMessageTest(unittest.TestCase):
                 self.assert_valid(chunks)
                 self.assertFalse(chunks[0].endswith("```py\n```"), chunks[0][-20:])
 
+    def test_closing_fence_at_end_of_chunk(self):
+        # La ligne qui ferme un bloc (```) entre dans un morceau presque plein : elle n'a pas à garder
+        # la place d'une fermeture artificielle, sinon le morceau suivant commence par un bloc vide.
+        for last in range(980, 992):
+            with self.subTest(last=last):
+                text = "```py\n" + "a" * 1000 + "\n" + "b" * last + "\n```\nSuite du message."
+                chunks = split_message(text)
+                self.assert_valid(chunks)
+                self.assertFalse(any(chunk.startswith("```py\n```") for chunk in chunks), chunks[-1][:20])
+
+    def test_long_closing_fence_line(self):
+        # Une ligne de fermeture trop longue est découpée : le bloc reste ouvert entre ses morceaux,
+        # qui doivent donc garder la place de le refermer.
+        for after in range(20, 60):
+            with self.subTest(after=after):
+                text = "```py\n" + "a" * 5 + "\n```" + "x" * after + " fin\nSuite."
+                for chunk in split_message(text, 40):
+                    self.assertLessEqual(len(chunk), 40, chunk)
+
 
 class StripReasoningTest(unittest.TestCase):
     def test_think_block_at_start_removed(self):