Przeglądaj źródła

Historique : chargement robuste et copies de secours datées

Un fichier d'historique d'une forme inattendue (liste JSON, « channels »
qui n'est pas un dictionnaire, mauvais encodage, messages d'un salon à
null) faisait planter le bot au démarrage. Il est maintenant mis de côté
et Rika repart d'une mémoire vide ; un salon mal formé est seulement
ignoré. Les copies de secours sont datées et ne s'écrasent plus.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Johnounet 4 dni temu
rodzic
commit
376ba67846
2 zmienionych plików z 36 dodań i 17 usunięć
  1. 31 17
      bot/history.py
  2. 5 0
      changelog.txt

+ 31 - 17
bot/history.py

@@ -6,6 +6,7 @@ import asyncio
 import json
 import logging
 import os
+from datetime import datetime
 from pathlib import Path
 
 logger = logging.getLogger("chatbot.history")
@@ -77,29 +78,42 @@ class ConversationHistory:
         if not self.file_path.exists():
             return {}
         try:
-            raw = json.loads(self.file_path.read_text(encoding="utf-8"))
-        except (json.JSONDecodeError, OSError) as exc:
-            backup = self.file_path.with_suffix(self.file_path.suffix + ".corrompu")
-            logger.error("Fichier d'historique illisible (%s). Copie de secours : %s", exc, backup)
-            try:
-                os.replace(self.file_path, backup)
-            except OSError:
-                pass
+            channels = self._channels(json.loads(self.file_path.read_text(encoding="utf-8")))
+        except (ValueError, OSError) as exc:  # JSON invalide, mauvais encodage ou structure inattendue
+            self._set_aside(exc)
             return {}
-
-        if isinstance(raw, dict) and raw.get("version") == FILE_VERSION:
-            channels = raw.get("channels", {})
-        else:
-            logger.info("Ancien format d'historique détecté : conversion automatique.")
-            channels = {cid: (val or {}).get("messages", []) for cid, val in (raw or {}).items()
-                        if isinstance(val, dict)}
-
         return {str(cid): self._trim(self._sanitize(msgs)) for cid, msgs in channels.items()}
 
     @staticmethod
-    def _sanitize(messages: list) -> list[dict]:
+    def _channels(raw) -> dict:
+        """Messages de chaque salon, quel que soit le format du fichier (ValueError s'il est inattendu)."""
+        if not isinstance(raw, dict):
+            raise ValueError(f"le fichier contient un {type(raw).__name__} au lieu d'un dictionnaire")
+        if raw.get("version") == FILE_VERSION:
+            channels = raw.get("channels") or {}
+            if not isinstance(channels, dict):
+                raise ValueError(f"« channels » est un {type(channels).__name__} au lieu d'un dictionnaire")
+            return channels
+        logger.info("Ancien format d'historique détecté : conversion automatique.")
+        return {cid: val.get("messages") for cid, val in raw.items() if isinstance(val, dict)}
+
+    def _set_aside(self, exc: Exception) -> None:
+        """Met de côté un fichier illisible, sous un nom daté : il sera remplacé à la prochaine
+        sauvegarde, et les copies précédentes ne sont jamais écrasées."""
+        stamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S")
+        backup = self.file_path.with_name(f"{self.file_path.name}.corrompu-{stamp}")
+        logger.error("Fichier d'historique illisible (%s). Copie de secours : %s", exc, backup)
+        try:
+            os.replace(self.file_path, backup)
+        except OSError as err:
+            logger.error("Copie de secours impossible : %s", err)
+
+    @staticmethod
+    def _sanitize(messages) -> list[dict]:
         """Convertit des messages (éventuellement de l'ancien format Mistral) en texte simple,
         et fusionne les messages consécutifs d'un même rôle."""
+        if not isinstance(messages, list):
+            return []
         clean: list[dict] = []
         for msg in messages:
             if not isinstance(msg, dict) or msg.get("role") not in ("user", "assistant"):

+ 5 - 0
changelog.txt

@@ -48,6 +48,11 @@ Corrections
   Leur empreinte est gardée dans slash_commands.sha256, à côté de
   l'historique (le supprimer force un nouvel envoi). Un échec de l'envoi
   n'empêche plus le bot de démarrer : il réessaie au démarrage suivant.
+- Historique : un fichier conversation_history.json d'une forme inattendue
+  (liste, « channels » qui n'est pas un dictionnaire, mauvais encodage)
+  n'empêche plus le bot de démarrer : il est mis de côté et Rika repart
+  d'une mémoire vide. Les copies de secours sont datées
+  (.corrompu-AAAA-MM-JJ_HH-MM-SS) et ne s'écrasent plus entre elles.
 
 
 Version 5.6 — 30/10/2026