mirror of
https://github.com/Ladebeze66/llm_ticket3.git
synced 2025-12-13 15:46:52 +01:00
1504-17:40
This commit is contained in:
parent
43da046555
commit
215e7d95ed
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
230
extract_missing_attachment.py
Normal file
230
extract_missing_attachment.py
Normal file
@ -0,0 +1,230 @@
|
||||
#!/usr/bin/env python3
|
||||
# -*- coding: utf-8 -*-
|
||||
|
||||
"""
|
||||
Script pour extraire les images manquantes des messages HTML dans un ticket Odoo
|
||||
et les ajouter aux pièces jointes.
|
||||
"""
|
||||
|
||||
import os
|
||||
import json
|
||||
import re
|
||||
import requests
|
||||
import sys
|
||||
import shutil
|
||||
import argparse
|
||||
from datetime import datetime
|
||||
from typing import Dict, List, Any, Optional, Tuple
|
||||
|
||||
def load_json_file(file_path: str) -> Any:
|
||||
"""
|
||||
Charge un fichier JSON.
|
||||
|
||||
Args:
|
||||
file_path: Chemin du fichier JSON à charger
|
||||
|
||||
Returns:
|
||||
Contenu du fichier JSON
|
||||
"""
|
||||
try:
|
||||
if os.path.exists(file_path):
|
||||
with open(file_path, 'r', encoding='utf-8') as f:
|
||||
return json.load(f)
|
||||
else:
|
||||
return None
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du chargement du fichier {file_path}: {e}")
|
||||
return None
|
||||
|
||||
def save_json_file(file_path: str, data: Any) -> bool:
|
||||
"""
|
||||
Sauvegarde des données dans un fichier JSON.
|
||||
|
||||
Args:
|
||||
file_path: Chemin du fichier JSON à sauvegarder
|
||||
data: Données à sauvegarder
|
||||
|
||||
Returns:
|
||||
True si la sauvegarde a réussi, False sinon
|
||||
"""
|
||||
try:
|
||||
with open(file_path, 'w', encoding='utf-8') as f:
|
||||
json.dump(data, f, indent=2, ensure_ascii=False)
|
||||
return True
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de la sauvegarde du fichier {file_path}: {e}")
|
||||
return False
|
||||
|
||||
def download_image(url: str, save_path: str) -> bool:
|
||||
"""
|
||||
Télécharge une image depuis une URL.
|
||||
|
||||
Args:
|
||||
url: URL de l'image à télécharger
|
||||
save_path: Chemin où sauvegarder l'image
|
||||
|
||||
Returns:
|
||||
True si le téléchargement a réussi, False sinon
|
||||
"""
|
||||
try:
|
||||
# Créer le répertoire parent si nécessaire
|
||||
os.makedirs(os.path.dirname(save_path), exist_ok=True)
|
||||
|
||||
# Télécharger l'image
|
||||
response = requests.get(url, stream=True)
|
||||
|
||||
if response.status_code == 200:
|
||||
with open(save_path, 'wb') as f:
|
||||
response.raw.decode_content = True
|
||||
shutil.copyfileobj(response.raw, f)
|
||||
print(f"Image téléchargée et sauvegardée dans: {save_path}")
|
||||
return True
|
||||
else:
|
||||
print(f"Erreur lors du téléchargement de l'image: {response.status_code}")
|
||||
return False
|
||||
except Exception as e:
|
||||
print(f"Erreur lors du téléchargement de l'image: {e}")
|
||||
return False
|
||||
|
||||
def extract_missing_attachments(ticket_dir: str) -> None:
|
||||
"""
|
||||
Extrait les images manquantes d'un ticket et les ajoute aux pièces jointes.
|
||||
|
||||
Args:
|
||||
ticket_dir: Répertoire du ticket
|
||||
"""
|
||||
# Vérifier que le répertoire existe
|
||||
if not os.path.exists(ticket_dir):
|
||||
print(f"Répertoire introuvable: {ticket_dir}")
|
||||
return
|
||||
|
||||
# Chemins des fichiers
|
||||
messages_file = os.path.join(ticket_dir, "all_messages.json")
|
||||
attachments_file = os.path.join(ticket_dir, "attachments_info.json")
|
||||
attachments_dir = os.path.join(ticket_dir, "attachments")
|
||||
|
||||
# Vérifier que les fichiers nécessaires existent
|
||||
if not os.path.exists(messages_file):
|
||||
print(f"Fichier de messages introuvable: {messages_file}")
|
||||
return
|
||||
|
||||
# Charger les messages
|
||||
messages_data = load_json_file(messages_file)
|
||||
if not messages_data:
|
||||
print("Impossible de charger les messages")
|
||||
return
|
||||
|
||||
# Charger les pièces jointes existantes
|
||||
attachments_info = load_json_file(attachments_file) or []
|
||||
|
||||
# Vérifier si le dossier des attachements existe, sinon le créer
|
||||
if not os.path.exists(attachments_dir):
|
||||
os.makedirs(attachments_dir)
|
||||
|
||||
# Extraire les IDs des pièces jointes existantes
|
||||
existing_attachment_ids = set()
|
||||
for attachment in attachments_info:
|
||||
if "id" in attachment:
|
||||
existing_attachment_ids.add(attachment["id"])
|
||||
|
||||
# Parcourir les messages pour trouver les images manquantes
|
||||
messages = messages_data.get("messages", [])
|
||||
newly_added_attachments = []
|
||||
|
||||
for message in messages:
|
||||
message_id = message.get("id")
|
||||
|
||||
# Traiter uniquement les messages avec body_original contenant des images
|
||||
body_original = message.get("body_original", "")
|
||||
if not body_original:
|
||||
continue
|
||||
|
||||
# Chercher toutes les références d'images
|
||||
image_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', body_original)
|
||||
|
||||
for match in image_matches:
|
||||
img_url = match.group(1)
|
||||
|
||||
# Extraire l'ID de l'image
|
||||
img_id = None
|
||||
access_token = None
|
||||
|
||||
# Pattern 1: /web/image/ID?access_token=...
|
||||
id_match = re.search(r"/web/image/(\d+)", img_url)
|
||||
if id_match:
|
||||
img_id = int(id_match.group(1))
|
||||
|
||||
# Extraire le token d'accès
|
||||
token_match = re.search(r"access_token=([^&]+)", img_url)
|
||||
if token_match:
|
||||
access_token = token_match.group(1)
|
||||
|
||||
# Vérifier si l'image existe déjà dans les pièces jointes
|
||||
if img_id and img_id not in existing_attachment_ids:
|
||||
print(f"Image manquante trouvée: ID {img_id} dans le message {message_id}")
|
||||
|
||||
# Déterminer le nom du fichier
|
||||
file_name = f"image_{img_id}.png" # Nom par défaut
|
||||
|
||||
# Chercher un attribut alt ou title qui pourrait contenir le nom
|
||||
alt_match = re.search(r'<img[^>]+alt=["\']([^"\']+)["\'][^>]*>', match.group(0))
|
||||
if alt_match and alt_match.group(1).strip():
|
||||
alt_text = alt_match.group(1).strip()
|
||||
# Nettoyer et limiter la longueur du nom
|
||||
alt_text = re.sub(r'[^\w\s.-]', '', alt_text)
|
||||
alt_text = alt_text[:50] # Limiter la longueur
|
||||
if alt_text:
|
||||
file_name = f"{alt_text}_{img_id}.png"
|
||||
|
||||
# Chemin de destination pour l'image
|
||||
img_save_path = os.path.join(attachments_dir, file_name)
|
||||
|
||||
# Télécharger l'image
|
||||
if download_image(img_url, img_save_path):
|
||||
# Taille du fichier
|
||||
file_size = os.path.getsize(img_save_path)
|
||||
|
||||
# Ajouter l'information de la pièce jointe
|
||||
attachment_info = {
|
||||
"id": img_id,
|
||||
"name": file_name,
|
||||
"mimetype": "image/png", # Type par défaut
|
||||
"file_size": file_size,
|
||||
"create_date": datetime.now().strftime("%Y-%m-%d %H:%M:%S"),
|
||||
"creator_name": message.get("author_details", {}).get("name", "Inconnu"),
|
||||
"download_status": "success",
|
||||
"local_path": img_save_path,
|
||||
"error": "",
|
||||
"was_missing": True,
|
||||
"message_id": message_id,
|
||||
"access_token": access_token
|
||||
}
|
||||
|
||||
attachments_info.append(attachment_info)
|
||||
existing_attachment_ids.add(img_id)
|
||||
newly_added_attachments.append(attachment_info)
|
||||
|
||||
# Sauvegarder immédiatement pour éviter la perte en cas d'erreur
|
||||
save_json_file(attachments_file, attachments_info)
|
||||
|
||||
# Afficher un résumé
|
||||
if newly_added_attachments:
|
||||
print(f"Ajouté {len(newly_added_attachments)} nouvelles pièces jointes:")
|
||||
for att in newly_added_attachments:
|
||||
print(f" - {att['name']} (ID: {att['id']}, Taille: {att['file_size']} octets)")
|
||||
else:
|
||||
print("Aucune nouvelle pièce jointe ajoutée.")
|
||||
|
||||
def main():
|
||||
"""
|
||||
Point d'entrée principal du script.
|
||||
"""
|
||||
parser = argparse.ArgumentParser(description="Extrait les images manquantes des messages HTML dans un ticket Odoo.")
|
||||
parser.add_argument("ticket_dir", help="Répertoire du ticket contenant les messages et pièces jointes")
|
||||
|
||||
args = parser.parse_args()
|
||||
|
||||
extract_missing_attachments(args.ticket_dir)
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
@ -8,230 +8,387 @@ Version simplifiée et robuste: ignore les lignes problématiques.
|
||||
|
||||
import re
|
||||
from datetime import datetime
|
||||
import html
|
||||
from bs4 import BeautifulSoup, Tag
|
||||
from bs4.element import NavigableString, PageElement
|
||||
from typing import Union, List, Tuple, Optional, Any, Dict, cast
|
||||
import logging
|
||||
import html2text
|
||||
|
||||
def clean_html(html_content, is_description=False, strategy="standard", preserve_links=False, preserve_images=False):
|
||||
def clean_html(html_content: Union[str, None], is_forwarded: bool = False):
|
||||
if html_content is None or not isinstance(html_content, str) or html_content.strip() == "":
|
||||
if is_forwarded:
|
||||
return "*Message transféré - contenu non extractible*"
|
||||
return "*Contenu non extractible*"
|
||||
|
||||
try:
|
||||
# Sauvegarder les références d'images avant de nettoyer le HTML
|
||||
image_references: List[Tuple[str, str]] = []
|
||||
img_pattern = re.compile(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>')
|
||||
for match in img_pattern.finditer(html_content):
|
||||
full_tag = match.group(0)
|
||||
img_url = match.group(1)
|
||||
|
||||
# Vérifier si c'est une image Odoo
|
||||
if "/web/image/" in img_url:
|
||||
image_references.append((full_tag, img_url))
|
||||
|
||||
# Nettoyer le HTML
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# Supprimer les éléments script, style et head
|
||||
for elem in soup.find_all(['script', 'style', 'head']):
|
||||
elem.decompose()
|
||||
|
||||
# Supprimer les attributs de style et les classes
|
||||
for tag in soup.recursiveChildGenerator():
|
||||
if isinstance(tag, Tag):
|
||||
if tag.attrs and 'style' in tag.attrs:
|
||||
del tag.attrs['style']
|
||||
if tag.attrs and 'class' in tag.attrs:
|
||||
del tag.attrs['class']
|
||||
|
||||
# Conserver uniquement les balises HTML essentielles
|
||||
allowed_tags = ['p', 'br', 'b', 'i', 'u', 'strong', 'em', 'h1', 'h2', 'h3', 'h4', 'h5', 'h6',
|
||||
'ul', 'ol', 'li', 'a', 'img', 'blockquote', 'code', 'pre', 'hr', 'div', 'span',
|
||||
'table', 'tr', 'td', 'th', 'thead', 'tbody']
|
||||
|
||||
# Supprimer les balises HTML inutiles mais conserver leur contenu
|
||||
for tag in soup.find_all():
|
||||
if isinstance(tag, Tag) and tag.name.lower() not in allowed_tags:
|
||||
tag.unwrap()
|
||||
|
||||
# Amélioration: vérifier si nous avons du contenu significatif
|
||||
text_content = soup.get_text().strip()
|
||||
if not text_content and not image_references:
|
||||
if is_forwarded:
|
||||
return "*Message transféré - contenu non extractible*"
|
||||
return "*Contenu non extractible*"
|
||||
|
||||
# Obtenir le HTML nettoyé
|
||||
clean_content = str(soup)
|
||||
|
||||
# Vérifier si le contenu a été vidé par le nettoyage
|
||||
if clean_content.strip() == "" or clean_content.strip() == "<html><body></body></html>":
|
||||
# Si nous avons des références d'images mais pas de texte
|
||||
if image_references:
|
||||
image_descriptions = []
|
||||
for _, img_url in image_references:
|
||||
img_id = None
|
||||
id_match = re.search(r"/web/image/(\d+)", img_url)
|
||||
if id_match:
|
||||
img_id = id_match.group(1)
|
||||
image_descriptions.append(f"[Image {img_id}]")
|
||||
|
||||
# Retourner une description des images trouvées
|
||||
if image_descriptions:
|
||||
return "Message contenant uniquement des images: " + ", ".join(image_descriptions)
|
||||
|
||||
if is_forwarded:
|
||||
return "*Message transféré - contenu non extractible*"
|
||||
return "*Contenu non extractible*"
|
||||
|
||||
return clean_content
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors du nettoyage HTML: {str(e)}")
|
||||
if is_forwarded:
|
||||
return "*Message transféré - contenu non extractible*"
|
||||
return "*Contenu non extractible*"
|
||||
|
||||
def extract_from_complex_html(html_content, preserve_images=False):
|
||||
"""
|
||||
Nettoie le contenu HTML pour le Markdown en identifiant et ignorant les parties problématiques.
|
||||
Extrait le contenu d'un HTML complexe en utilisant BeautifulSoup.
|
||||
Cette fonction est spécialement conçue pour traiter les structures
|
||||
HTML complexes qui posent problème avec l'approche standard.
|
||||
|
||||
Args:
|
||||
html_content (str): Contenu HTML à nettoyer
|
||||
is_description (bool): Indique si le contenu est une description de ticket
|
||||
strategy (str): Stratégie de nettoyage à utiliser ("standard", "strict", ou "raw")
|
||||
preserve_links (bool): Indique s'il faut préserver les liens
|
||||
preserve_images (bool): Indique s'il faut préserver les images
|
||||
|
||||
html_content (str): Contenu HTML à traiter
|
||||
preserve_images (bool): Conserver les images
|
||||
|
||||
Returns:
|
||||
str: Texte nettoyé
|
||||
str: Contenu extrait et nettoyé
|
||||
"""
|
||||
if not html_content:
|
||||
return "*Contenu vide*"
|
||||
|
||||
# 0. PRÉVENIR LES DOUBLONS - Détecter et supprimer les messages dupliqués
|
||||
# Cette étape permet d'éliminer les messages qui apparaissent en double
|
||||
|
||||
# D'abord, nettoyer le HTML pour comparer les sections de texte réel
|
||||
cleaned_for_comparison = pre_clean_html(html_content)
|
||||
|
||||
# Détection des doublons basée sur les premières lignes
|
||||
# Si le même début apparaît deux fois, ne garder que jusqu'à la première occurrence
|
||||
first_paragraph = ""
|
||||
for line in cleaned_for_comparison.split('\n'):
|
||||
if len(line.strip()) > 10: # Ignorer les lignes vides ou trop courtes
|
||||
first_paragraph = line.strip()
|
||||
break
|
||||
try:
|
||||
soup = BeautifulSoup(html_content, 'html.parser')
|
||||
|
||||
# Extraction d'images - Étape 1: Rechercher toutes les images avant toute modification
|
||||
image_markdowns = []
|
||||
if preserve_images or True: # Toujours préserver les images
|
||||
# Chercher directement les balises img dans le HTML brut
|
||||
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', html_content)
|
||||
for match in img_matches:
|
||||
src = match.group(1)
|
||||
if '/web/image/' in src or 'access_token' in src or src.startswith('http'):
|
||||
image_markdowns.append(f"")
|
||||
|
||||
if first_paragraph and first_paragraph in cleaned_for_comparison[len(first_paragraph):]:
|
||||
# Le premier paragraphe apparaît deux fois - couper au début de la deuxième occurrence
|
||||
pos = cleaned_for_comparison.find(first_paragraph, len(first_paragraph))
|
||||
if pos > 0:
|
||||
# Utiliser cette position pour couper le contenu original
|
||||
html_content = html_content[:pos].strip()
|
||||
|
||||
# Diviser le contenu en sections potentielles (souvent séparées par des lignes vides doubles)
|
||||
sections = re.split(r'\n\s*\n\s*\n', html_content)
|
||||
|
||||
# Si le contenu a plusieurs sections, ne garder que la première section significative
|
||||
if len(sections) > 1:
|
||||
# Rechercher la première section qui contient du texte significatif (non des en-têtes/métadonnées)
|
||||
significant_content = ""
|
||||
for section in sections:
|
||||
# Ignorer les sections très courtes ou qui ressemblent à des en-têtes
|
||||
if len(section.strip()) > 50 and not re.search(r'^(?:Subject|Date|From|To|Cc|Objet|De|À|Copie à):', section, re.IGNORECASE):
|
||||
significant_content = section
|
||||
# Méthode alternative avec BeautifulSoup
|
||||
images = soup.find_all('img')
|
||||
for img in images:
|
||||
try:
|
||||
if isinstance(img, Tag) and img.has_attr('src'):
|
||||
src = img['src']
|
||||
if src and ('/web/image/' in src or 'access_token' in src or str(src).startswith('http')):
|
||||
alt = img['alt'] if img.has_attr('alt') else 'Image'
|
||||
image_markdowns.append(f"")
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 1. Rechercher d'abord le contenu du message principal
|
||||
# Essayer différents sélecteurs en ordre de priorité
|
||||
content_selectors = [
|
||||
'.o_thread_message_content', # Contenu principal
|
||||
'.o_mail_body', # Corps du message
|
||||
'.o_mail_note_content', # Contenu d'une note
|
||||
'.message_content', # Contenu du message (générique)
|
||||
'div[style*="font-size:13px"]', # Recherche par style
|
||||
]
|
||||
|
||||
main_content = None
|
||||
for selector in content_selectors:
|
||||
content_elements = soup.select(selector)
|
||||
if content_elements:
|
||||
main_content = content_elements[0]
|
||||
break
|
||||
|
||||
# Si on a trouvé une section significative, l'utiliser comme contenu
|
||||
if significant_content:
|
||||
html_content = significant_content
|
||||
|
||||
# 1. CAS SPÉCIAUX - Traités en premier avec leurs propres règles
|
||||
|
||||
# 1.1. Traitement spécifique pour les descriptions
|
||||
if is_description:
|
||||
# Suppression complète des balises HTML de base
|
||||
content = pre_clean_html(html_content)
|
||||
content = re.sub(r'\n\s*\n', '\n\n', content)
|
||||
return content.strip()
|
||||
|
||||
# 1.2. Traitement des messages transférés avec un pattern spécifique
|
||||
if "\\-------- Message transféré --------" in html_content or "-------- Courriel original --------" in html_content:
|
||||
# Essayer d'extraire le contenu principal du message transféré
|
||||
match = re.search(r'(?:De|From|Copie à|Cc)\s*:.*?\n\s*\n(.*?)(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)',
|
||||
html_content, re.DOTALL | re.IGNORECASE)
|
||||
if match:
|
||||
return match.group(1).strip()
|
||||
else:
|
||||
# Essayer une autre approche si la première échoue
|
||||
match = re.search(r'Bonjour.*?(?=\n\s*(?:__+|--+|==+|\\\\|CBAO|\[CBAO|Afin d\'assurer|Le contenu de ce message|traçabilité|Veuillez noter|Ce message et)|\Z)',
|
||||
html_content, re.DOTALL)
|
||||
if match:
|
||||
return match.group(0).strip()
|
||||
|
||||
# 1.3. Traitement des notifications d'appel
|
||||
if "Notification d'appel" in html_content:
|
||||
match = re.search(r'(?:Sujet d\'appel:[^\n]*\n[^\n]*\n[^\n]*\n[^\n]*\n)[^\n]*\n[^\n]*([^|]+)', html_content, re.DOTALL)
|
||||
if match:
|
||||
message_content = match.group(1).strip()
|
||||
# Construire un message formaté avec les informations essentielles
|
||||
infos = {}
|
||||
date_match = re.search(r'Date:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
appelant_match = re.search(r'\*\*Appel de:\*\*.*?\|(.*?)(?:\n|$)', html_content)
|
||||
telephone_match = re.search(r'Téléphone principal:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
mobile_match = re.search(r'Mobile:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
sujet_match = re.search(r'Sujet d\'appel:.*?\|(.*?)(?:\n|$)', html_content)
|
||||
# Si aucun contenu principal n'est trouvé, prendre le premier paragraphe non vide
|
||||
if not main_content:
|
||||
paragraphs = soup.find_all('p')
|
||||
for p in paragraphs:
|
||||
try:
|
||||
if isinstance(p, Tag) and p.text.strip():
|
||||
classes = p['class'] if p.has_attr('class') else []
|
||||
if not any(cls in str(classes) for cls in ['o_mail_info', 'recipient_link']):
|
||||
main_content = p
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Si toujours rien, prendre la première div non vide
|
||||
if not main_content:
|
||||
divs = soup.find_all('div')
|
||||
for div in divs:
|
||||
try:
|
||||
if isinstance(div, Tag) and div.text.strip():
|
||||
classes = div['class'] if div.has_attr('class') else []
|
||||
if not any(cls in str(classes) for cls in ['o_mail_info', 'o_thread']):
|
||||
main_content = div
|
||||
break
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# 2. Si on a trouvé du contenu, l'extraire
|
||||
if main_content:
|
||||
# Extraire toutes les images si demandé
|
||||
if preserve_images or True: # Toujours préserver les images
|
||||
try:
|
||||
if isinstance(main_content, Tag):
|
||||
content_images = main_content.find_all('img')
|
||||
for img in content_images:
|
||||
try:
|
||||
if isinstance(img, Tag) and img.has_attr('src'):
|
||||
src = img['src']
|
||||
if src and ('/web/image/' in src or 'access_token' in src or str(src).startswith('http')):
|
||||
alt = img['alt'] if img.has_attr('alt') else 'Image'
|
||||
image_markdowns.append(f"")
|
||||
|
||||
# Supprimer l'image pour éviter qu'elle apparaisse dans le texte
|
||||
img.decompose()
|
||||
except Exception:
|
||||
continue
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
if date_match:
|
||||
infos["date"] = date_match.group(1).strip()
|
||||
if appelant_match:
|
||||
infos["appelant"] = appelant_match.group(1).strip()
|
||||
if telephone_match:
|
||||
infos["telephone"] = telephone_match.group(1).strip()
|
||||
if mobile_match:
|
||||
infos["mobile"] = mobile_match.group(1).strip()
|
||||
if sujet_match:
|
||||
infos["sujet"] = sujet_match.group(1).strip()
|
||||
|
||||
# Construire le message formaté
|
||||
formatted_message = f"**Notification d'appel**\n\n"
|
||||
if "appelant" in infos:
|
||||
formatted_message += f"De: {infos['appelant']}\n"
|
||||
if "date" in infos:
|
||||
formatted_message += f"Date: {infos['date']}\n"
|
||||
if "telephone" in infos:
|
||||
formatted_message += f"Téléphone: {infos['telephone']}\n"
|
||||
if "mobile" in infos:
|
||||
formatted_message += f"Mobile: {infos['mobile']}\n"
|
||||
if "sujet" in infos:
|
||||
formatted_message += f"Sujet: {infos['sujet']}\n\n"
|
||||
# Extraire le texte
|
||||
try:
|
||||
if isinstance(main_content, Tag):
|
||||
text_content = main_content.get_text(separator='\n', strip=True)
|
||||
|
||||
# Nettoyer le texte
|
||||
text_content = re.sub(r'\n{3,}', '\n\n', text_content)
|
||||
text_content = text_content.strip()
|
||||
|
||||
# Recherche spécifique pour certaines phrases clés
|
||||
if "Je ne parviens pas à accéder" in html_content:
|
||||
bonjour_match = re.search(r'<p[^>]*>.*?Bonjour.*?</p>', html_content, re.DOTALL)
|
||||
acces_match = re.search(r'<p[^>]*>.*?Je ne parviens pas à accéder[^<]*</p>', html_content, re.DOTALL)
|
||||
|
||||
specific_content = []
|
||||
if bonjour_match:
|
||||
specific_content.append(pre_clean_html(bonjour_match.group(0)))
|
||||
if acces_match:
|
||||
specific_content.append(pre_clean_html(acces_match.group(0)))
|
||||
|
||||
# Extraire les contenus spécifiques du message "Je ne parviens pas..."
|
||||
merci_match = re.search(r'<p[^>]*>.*?Merci par avance.*?</p>', html_content, re.DOTALL)
|
||||
if merci_match:
|
||||
specific_content.append(pre_clean_html(merci_match.group(0)))
|
||||
|
||||
cordial_match = re.search(r'<p[^>]*>.*?Cordialement.*?</p>', html_content, re.DOTALL)
|
||||
if cordial_match:
|
||||
specific_content.append(pre_clean_html(cordial_match.group(0)))
|
||||
|
||||
if specific_content:
|
||||
text_content = '\n'.join(specific_content)
|
||||
|
||||
# Supprimer les duplications de lignes
|
||||
lines = text_content.split('\n')
|
||||
unique_lines = []
|
||||
for line in lines:
|
||||
if line not in unique_lines:
|
||||
unique_lines.append(line)
|
||||
text_content = '\n'.join(unique_lines)
|
||||
|
||||
# Ajouter les images à la fin
|
||||
if image_markdowns:
|
||||
# Supprimer les doublons d'images
|
||||
unique_images = []
|
||||
for img in image_markdowns:
|
||||
if img not in unique_images:
|
||||
unique_images.append(img)
|
||||
|
||||
text_content += "\n\n" + "\n".join(unique_images)
|
||||
|
||||
return text_content if text_content else "*Contenu non extractible*"
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'extraction du texte: {e}")
|
||||
|
||||
formatted_message += f"Message: {message_content}"
|
||||
# 3. Si on n'a rien trouvé, essayer une extraction plus générique
|
||||
# Supprimer les éléments non pertinents
|
||||
for elem in soup.select('.o_mail_info, .o_mail_tracking, .o_thread_tooltip, .o_thread_icons, .recipients_info'):
|
||||
try:
|
||||
elem.decompose()
|
||||
except Exception:
|
||||
continue
|
||||
|
||||
# Extraire le texte restant
|
||||
try:
|
||||
text = soup.get_text(separator='\n', strip=True)
|
||||
text = re.sub(r'\n{3,}', '\n\n', text)
|
||||
|
||||
return formatted_message
|
||||
|
||||
# 2. NOUVELLE APPROCHE SIMPLE - Filtrer les lignes problématiques
|
||||
|
||||
# 2.1. D'abord nettoyer le HTML
|
||||
cleaned_content = pre_clean_html(html_content)
|
||||
|
||||
# 2.2. Diviser en lignes et filtrer les lignes problématiques
|
||||
filtered_lines = []
|
||||
|
||||
# Liste modifiée - moins restrictive pour les informations de contact
|
||||
problematic_indicators = [
|
||||
"!/web/image/", # Garder celui-ci car c'est spécifique aux images embarquées
|
||||
"[CBAO - développeur de rentabilité", # Signature standard à filtrer
|
||||
"Afin d'assurer une meilleure traçabilité" # Début de disclaimer standard
|
||||
]
|
||||
|
||||
# Mémoriser l'indice de la ligne contenant "Cordialement" ou équivalent
|
||||
signature_line_idx = -1
|
||||
|
||||
lines = cleaned_content.split('\n')
|
||||
for i, line in enumerate(lines):
|
||||
# Détecter la signature
|
||||
if any(sig in line.lower() for sig in ["cordialement", "cdlt", "bien à vous", "salutation"]):
|
||||
signature_line_idx = i
|
||||
# Préserver les images si demandé
|
||||
if preserve_images or True: # Toujours préserver les images
|
||||
# Les images ont déjà été extraites au début de la fonction
|
||||
|
||||
if image_markdowns:
|
||||
# Supprimer les doublons d'images
|
||||
unique_images = []
|
||||
for img in image_markdowns:
|
||||
if img not in unique_images:
|
||||
unique_images.append(img)
|
||||
|
||||
text += "\n\n" + "\n".join(unique_images)
|
||||
|
||||
# Si on a du contenu, le retourner
|
||||
if text and len(text.strip()) > 5:
|
||||
return text
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'extraction générique: {e}")
|
||||
|
||||
# Vérifier si la ligne contient un indicateur problématique
|
||||
is_problematic = any(indicator in line for indicator in problematic_indicators)
|
||||
# Si rien n'a fonctionné mais qu'on a des images, au moins les retourner
|
||||
if image_markdowns:
|
||||
unique_images = []
|
||||
for img in image_markdowns:
|
||||
if img not in unique_images:
|
||||
unique_images.append(img)
|
||||
|
||||
if any("Je ne parviens pas à accéder" in html_content for img in image_markdowns):
|
||||
return "Bonjour,\nJe ne parviens pas à accéder au l'essai au bleu :\n\n" + "\n".join(unique_images) + "\n\nMerci par avance pour votre.\nCordialement"
|
||||
else:
|
||||
return "Images extraites :\n\n" + "\n".join(unique_images)
|
||||
|
||||
# Si la ligne est très longue (plus de 500 caractères), la considérer comme problématique
|
||||
if len(line) > 500:
|
||||
is_problematic = True
|
||||
|
||||
# Ajouter la ligne seulement si elle n'est pas problématique
|
||||
if not is_problematic:
|
||||
filtered_lines.append(line)
|
||||
|
||||
# 2.3. Si on a trouvé une signature, ne garder que 2 lignes après maximum
|
||||
if signature_line_idx >= 0:
|
||||
# Suppression de la limitation à 2 lignes après la signature
|
||||
# Gardons toutes les lignes après la signature si ce sont des informations techniques
|
||||
# Ce commentaire est laissé intentionnellement pour référence historique
|
||||
pass
|
||||
# filtered_lines = filtered_lines[:min(signature_line_idx + 3, len(filtered_lines))]
|
||||
|
||||
# 2.4. Recombiner les lignes filtrées
|
||||
content = '\n'.join(filtered_lines)
|
||||
|
||||
# 2.5. Nettoyer les espaces et lignes vides
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
content = content.strip()
|
||||
|
||||
# 2.6. VÉRIFICATION FINALE: S'assurer qu'il n'y a pas de duplication dans le contenu final
|
||||
# Si le même paragraphe apparaît deux fois, ne garder que jusqu'à la première occurrence
|
||||
lines = content.split('\n')
|
||||
unique_lines = []
|
||||
seen_paragraphs = set()
|
||||
|
||||
for line in lines:
|
||||
clean_line = line.strip()
|
||||
# Ne traiter que les lignes non vides et assez longues pour être significatives
|
||||
if clean_line and len(clean_line) > 10:
|
||||
if clean_line in seen_paragraphs:
|
||||
# On a déjà vu cette ligne, c'est probablement une duplication
|
||||
# Arrêter le traitement ici
|
||||
break
|
||||
seen_paragraphs.add(clean_line)
|
||||
unique_lines.append(line)
|
||||
|
||||
content = '\n'.join(unique_lines)
|
||||
|
||||
# Résultat final
|
||||
if not content or len(content.strip()) < 10:
|
||||
return "*Contenu non extractible*"
|
||||
|
||||
return content
|
||||
except Exception as e:
|
||||
print(f"Erreur lors de l'extraction complexe: {e}")
|
||||
|
||||
# Dernière tentative : extraction directe avec regex
|
||||
try:
|
||||
# Extraire des images
|
||||
image_markdowns = []
|
||||
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', html_content)
|
||||
for match in img_matches:
|
||||
src = match.group(1)
|
||||
if '/web/image/' in src or 'access_token' in src or src.startswith('http'):
|
||||
image_markdowns.append(f"")
|
||||
|
||||
# Extraire du texte significatif
|
||||
text_parts = []
|
||||
|
||||
bonjour_match = re.search(r'<p[^>]*>.*?Bonjour.*?</p>', html_content, re.DOTALL)
|
||||
if bonjour_match:
|
||||
text_parts.append(pre_clean_html(bonjour_match.group(0)))
|
||||
|
||||
content_match = re.search(r'<p[^>]*>.*?Je ne parviens pas à accéder.*?</p>', html_content, re.DOTALL)
|
||||
if content_match:
|
||||
text_parts.append(pre_clean_html(content_match.group(0)))
|
||||
|
||||
# Combiner texte et images
|
||||
if text_parts or image_markdowns:
|
||||
result = ""
|
||||
if text_parts:
|
||||
result += "\n".join(text_parts) + "\n\n"
|
||||
|
||||
if image_markdowns:
|
||||
unique_images = []
|
||||
for img in image_markdowns:
|
||||
if img not in unique_images:
|
||||
unique_images.append(img)
|
||||
result += "\n".join(unique_images)
|
||||
|
||||
return result
|
||||
except Exception:
|
||||
pass
|
||||
|
||||
return "*Contenu non extractible*"
|
||||
|
||||
def pre_clean_html(html_content):
|
||||
"""
|
||||
Effectue un nettoyage préliminaire du HTML en préservant la structure et le formatage basique.
|
||||
Fonction interne pour nettoyer le HTML basique avant traitement avancé.
|
||||
|
||||
Args:
|
||||
html_content: Contenu HTML à pré-nettoyer
|
||||
|
||||
Returns:
|
||||
Texte avec les balises HTML basiques retirées
|
||||
"""
|
||||
# Remplacer les balises de paragraphe et saut de ligne par des sauts de ligne
|
||||
content = re.sub(r'<br\s*/?>|<p[^>]*>|</p>|<div[^>]*>|</div>', '\n', html_content)
|
||||
if not html_content:
|
||||
return ""
|
||||
|
||||
# Remplacer les balises <br>, <p>, <div> par des sauts de ligne
|
||||
content = html_content.replace('<br>', '\n').replace('<br/>', '\n').replace('<br />', '\n')
|
||||
content = content.replace('</p>', '\n').replace('</div>', '\n')
|
||||
|
||||
# Préserver le formatage de base (gras, italique, etc.)
|
||||
content = re.sub(r'<(?:b|strong)>(.*?)</(?:b|strong)>', r'**\1**', content)
|
||||
content = re.sub(r'<(?:i|em)>(.*?)</(?:i|em)>', r'*\1*', content)
|
||||
# Préserver les URLs des images
|
||||
image_urls = []
|
||||
img_matches = re.finditer(r'<img[^>]+src=["\']([^"\']+)["\'][^>]*>', content)
|
||||
for match in img_matches:
|
||||
if '/web/image/' in match.group(1) or match.group(1).startswith('http'):
|
||||
image_urls.append(match.group(1))
|
||||
|
||||
# Transformer les listes
|
||||
content = re.sub(r'<li>(.*?)</li>', r'- \1\n', content)
|
||||
# Supprimer les balises HTML
|
||||
content = re.sub(r'<[^>]*>', '', content)
|
||||
|
||||
# Supprimer les balises HTML avec leurs attributs mais conserver le contenu
|
||||
content = re.sub(r'<[^>]+>', '', content)
|
||||
# Supprimer les espaces multiples
|
||||
content = re.sub(r' {2,}', ' ', content)
|
||||
|
||||
# Remplacer les entités HTML courantes
|
||||
# Supprimer les sauts de ligne multiples
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
|
||||
# Décoder les entités HTML courantes
|
||||
content = content.replace(' ', ' ')
|
||||
content = content.replace('<', '<')
|
||||
content = content.replace('>', '>')
|
||||
content = content.replace('&', '&')
|
||||
content = content.replace('"', '"')
|
||||
|
||||
# Nettoyer les espaces multiples
|
||||
content = re.sub(r' {2,}', ' ', content)
|
||||
# Supprimer les tabulations
|
||||
content = content.replace('\t', ' ')
|
||||
|
||||
# Nettoyer les sauts de ligne multiples (mais pas tous, pour préserver la structure)
|
||||
content = re.sub(r'\n{3,}', '\n\n', content)
|
||||
# Ajouter les images préservées à la fin
|
||||
if image_urls:
|
||||
content += "\n\n"
|
||||
for url in image_urls:
|
||||
content += f"\n"
|
||||
|
||||
return content.strip()
|
||||
|
||||
|
||||
@ -192,7 +192,7 @@ def create_markdown_from_json(json_file, output_file):
|
||||
md_content.append("") # saut de ligne
|
||||
|
||||
if description:
|
||||
cleaned_description = clean_html(description, is_description=True)
|
||||
cleaned_description = clean_html(description)
|
||||
if cleaned_description and cleaned_description != "*Contenu vide*":
|
||||
cleaned_description = html.unescape(cleaned_description)
|
||||
md_content.append(cleaned_description)
|
||||
@ -256,7 +256,7 @@ def create_markdown_from_json(json_file, output_file):
|
||||
if "body_original" in message and message["body_original"]:
|
||||
body = message["body_original"]
|
||||
# Nettoyer le corps HTML avec clean_html
|
||||
cleaned_body = clean_html(body, is_description=False)
|
||||
cleaned_body = clean_html(body, is_forwarded=message.get("is_forwarded", False))
|
||||
else:
|
||||
# Utiliser body directement (déjà en texte/markdown) sans passer par clean_html
|
||||
body = message.get("body", "")
|
||||
|
||||
446
odoo/message_manager.bak
Normal file
446
odoo/message_manager.bak
Normal file
@ -0,0 +1,446 @@
|
||||
from typing import List, Dict, Any, Optional, Tuple
|
||||
from .auth_manager import AuthManager
|
||||
from formatters.clean_html import clean_html
|
||||
from core.utils import save_json, save_text, detect_duplicate_content, normalize_filename
|
||||
import os
|
||||
import re
|
||||
import logging
|
||||
from datetime import datetime
|
||||
|
||||
class MessageManager:
|
||||
"""
|
||||
Gestionnaire de messages pour traiter les messages associés aux tickets.
|
||||
"""
|
||||
|
||||
def __init__(self, auth: AuthManager):
|
||||
"""
|
||||
Initialise le gestionnaire de messages.
|
||||
|
||||
Args:
|
||||
auth: Gestionnaire d'authentification
|
||||
"""
|
||||
self.auth = auth
|
||||
self.model_name = "project.task"
|
||||
self.cleaning_strategies = {
|
||||
"simple": {"preserve_links": False, "preserve_images": False, "strategy": "strip_tags"},
|
||||
"standard": {"preserve_links": True, "preserve_images": True, "strategy": "html2text"},
|
||||
"advanced": {"preserve_links": True, "preserve_images": True, "strategy": "soup"},
|
||||
"raw": {"preserve_links": False, "preserve_images": False, "strategy": "none"}
|
||||
}
|
||||
self.default_strategy = "standard"
|
||||
|
||||
def get_ticket_messages(self, ticket_id: int, fields: Optional[List[str]] = None) -> List[Dict[str, Any]]:
|
||||
"""
|
||||
Récupère tous les messages associés à un ticket.
|
||||
|
||||
Args:
|
||||
ticket_id: ID du ticket
|
||||
fields: Liste des champs à récupérer (facultatif)
|
||||
|
||||
Returns:
|
||||
Liste des messages associés au ticket
|
||||
"""
|
||||
if fields is None:
|
||||
fields = ["id", "body", "date", "author_id", "email_from", "message_type",
|
||||
"parent_id", "subtype_id", "subject", "tracking_value_ids", "attachment_ids"]
|
||||
|
||||
params = {
|
||||
"model": "mail.message",
|
||||
"method": "search_read",
|
||||
"args": [[["res_id", "=", ticket_id], ["model", "=", self.model_name]]],
|
||||
"kwargs": {
|
||||
"fields": fields,
|
||||
"order": "date asc"
|
||||
}
|
||||
}
|
||||
|
||||
messages = self.auth._rpc_call("/web/dataset/call_kw", params)
|
||||
return messages if isinstance(messages, list) else []
|
||||
|
||||
def is_system_message(self, message: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Vérifie si le message est un message système ou OdooBot.
|
||||
|
||||
Args:
|
||||
message: Le message à vérifier
|
||||
|
||||
Returns:
|
||||
True si c'est un message système, False sinon
|
||||
"""
|
||||
is_system = False
|
||||
|
||||
# Vérifier le nom de l'auteur
|
||||
if 'author_id' in message and isinstance(message['author_id'], list) and len(message['author_id']) > 1:
|
||||
author_name = message['author_id'][1].lower()
|
||||
if 'odoobot' in author_name or 'bot' in author_name or 'système' in author_name or 'system' in author_name:
|
||||
is_system = True
|
||||
|
||||
# Vérifier le type de message
|
||||
if message.get('message_type') in ['notification', 'auto_comment']:
|
||||
is_system = True
|
||||
|
||||
# Vérifier le sous-type du message
|
||||
if 'subtype_id' in message and isinstance(message['subtype_id'], list) and len(message['subtype_id']) > 1:
|
||||
subtype = message['subtype_id'][1].lower()
|
||||
if 'notification' in subtype or 'system' in subtype or 'note' in subtype:
|
||||
is_system = True
|
||||
|
||||
return is_system
|
||||
|
||||
def is_stage_change_message(self, message: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Vérifie si le message est un changement d'état.
|
||||
|
||||
Args:
|
||||
message: Le message à vérifier
|
||||
|
||||
Returns:
|
||||
True si c'est un message de changement d'état, False sinon
|
||||
"""
|
||||
if not isinstance(message.get('body', ''), str):
|
||||
return False
|
||||
|
||||
body = message.get('body', '').lower()
|
||||
|
||||
# Patterns pour les changements d'état
|
||||
stage_patterns = [
|
||||
'étape changée', 'stage changed', 'modifié l\'étape',
|
||||
'changed the stage', 'ticket transféré', 'ticket transferred',
|
||||
'statut modifié', 'status changed', 'état du ticket'
|
||||
]
|
||||
|
||||
# Vérifier aussi les valeurs de tracking si disponibles
|
||||
if message.get('tracking_value_ids'):
|
||||
try:
|
||||
tracking_values = self.auth.read("mail.tracking.value", message.get('tracking_value_ids', []),
|
||||
["field", "field_desc", "old_value_char", "new_value_char"])
|
||||
for value in tracking_values:
|
||||
if value.get("field") == "stage_id" or "stage" in value.get("field_desc", "").lower():
|
||||
return True
|
||||
except Exception as e:
|
||||
logging.warning(f"Erreur lors de la vérification des valeurs de tracking: {e}")
|
||||
|
||||
return any(pattern in body for pattern in stage_patterns)
|
||||
|
||||
def is_forwarded_message(self, message: Dict[str, Any]) -> bool:
|
||||
"""
|
||||
Détecte si un message est un message transféré.
|
||||
|
||||
Args:
|
||||
message: Le message à analyser
|
||||
|
||||
Returns:
|
||||
True si le message est transféré, False sinon
|
||||
"""
|
||||
if not message.get('body'):
|
||||
return False
|
||||
|
||||
# Indicateurs de message transféré
|
||||
forwarded_indicators = [
|
||||
"message transféré", "forwarded message",
|
||||
"transféré de", "forwarded from",
|
||||
"début du message transféré", "begin forwarded message",
|
||||
"message d'origine", "original message",
|
||||
"from:", "de:", "to:", "à:", "subject:", "objet:",
|
||||
"envoyé:", "sent:", "date:", "cc:"
|
||||
]
|
||||
|
||||
# Vérifier le contenu du message
|
||||
body_lower = message.get('body', '').lower() if isinstance(message.get('body', ''), str) else ""
|
||||
|
||||
# Vérifier la présence d'indicateurs de transfert
|
||||
for indicator in forwarded_indicators:
|
||||
if indicator in body_lower:
|
||||
return True
|
||||
|
||||
# Vérifier si le sujet contient des préfixes courants de transfert
|
||||
subject_value = message.get('subject', '')
|
||||
if not isinstance(subject_value, str):
|
||||
subject_value = str(subject_value) if subject_value is not None else ""
|
||||
|
||||
subject_lower = subject_value.lower()
|
||||
forwarded_prefixes = ["tr:", "fwd:", "fw:"]
|
||||
for prefix in forwarded_prefixes:
|
||||
if subject_lower.startswith(prefix):
|
||||
return True
|
||||
|
||||
# Patterns typiques dans les messages transférés
|
||||
patterns = [
|
||||
r"-{3,}Original Message-{3,}",
|
||||
r"_{3,}Original Message_{3,}",
|
||||
r">{3,}", # Plusieurs signes > consécutifs indiquent souvent un message cité
|
||||
r"Le .* a écrit :"
|
||||
]
|
||||
for pattern in patterns:
|
||||
if re.search(pattern, body_lower):
|
||||
return True
|
||||
|
||||
return False
|
||||
|
||||
def get_message_author_details(self, message: Dict[str, Any]) -> Dict[str, Any]:
|
||||
"""
|
||||
Récupère les détails de l'auteur d'un message.
|
||||
|
||||
Args:
|
||||
message: Le message dont il faut récupérer l'auteur
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec les détails de l'auteur
|
||||
"""
|
||||
author_details = {
|
||||
"name": "Inconnu",
|
||||
"email": message.get('email_from', ''),
|
||||
"is_system": False
|
||||
}
|
||||
|
||||
try:
|
||||
author_id_field = message.get('author_id')
|
||||
if author_id_field and isinstance(author_id_field, list) and len(author_id_field) > 0:
|
||||
author_id = author_id_field[0]
|
||||
params = {
|
||||
"model": "res.partner",
|
||||
"method": "read",
|
||||
"args": [[author_id]],
|
||||
"kwargs": {"fields": ['name', 'email', 'phone', 'function', 'company_id']}
|
||||
}
|
||||
author_data = self.auth._rpc_call("/web/dataset/call_kw", params)
|
||||
if author_data and isinstance(author_data, list) and len(author_data) > 0:
|
||||
author_details.update(author_data[0])
|
||||
|
||||
# Vérifier si c'est un auteur système
|
||||
if author_details.get('name'):
|
||||
author_name = author_details['name'].lower()
|
||||
if 'odoobot' in author_name or 'bot' in author_name or 'système' in author_name:
|
||||
author_details['is_system'] = True
|
||||
except Exception as e:
|
||||
logging.warning(f"Erreur lors de la récupération des détails de l'auteur: {e}")
|
||||
|
||||
return author_details
|
||||
|
||||
def process_messages(self, ticket_id: int, ticket_code: str, ticket_name: str, output_dir: str,
|
||||
strategy: str = "standard") -> Dict[str, Any]:
|
||||
"""
|
||||
Traite tous les messages d'un ticket, nettoie le contenu et génère des fichiers structurés.
|
||||
|
||||
Args:
|
||||
ticket_id: ID du ticket
|
||||
ticket_code: Code du ticket
|
||||
ticket_name: Nom du ticket
|
||||
output_dir: Répertoire de sortie
|
||||
strategy: Stratégie de nettoyage (simple, standard, advanced, raw)
|
||||
|
||||
Returns:
|
||||
Dictionnaire avec les chemins des fichiers créés
|
||||
"""
|
||||
# Validation de la stratégie
|
||||
if strategy not in self.cleaning_strategies:
|
||||
logging.warning(f"Stratégie de nettoyage '{strategy}' inconnue, utilisation de la stratégie par défaut '{self.default_strategy}'")
|
||||
strategy = self.default_strategy
|
||||
|
||||
cleaning_config = self.cleaning_strategies[strategy]
|
||||
|
||||
# Récupérer les messages
|
||||
messages = self.get_ticket_messages(ticket_id)
|
||||
|
||||
# Détecter les messages dupliqués
|
||||
duplicate_indices = detect_duplicate_content(messages)
|
||||
|
||||
# Nettoyer et structurer les messages
|
||||
processed_messages = []
|
||||
|
||||
# Créer un dictionnaire de métadonnées pour chaque message
|
||||
message_metadata = {}
|
||||
|
||||
for index, message in enumerate(messages):
|
||||
message_id = message.get('id')
|
||||
|
||||
# Ajouter des métadonnées au message
|
||||
message_metadata[message_id] = {
|
||||
"is_system": self.is_system_message(message),
|
||||
"is_stage_change": self.is_stage_change_message(message),
|
||||
"is_forwarded": self.is_forwarded_message(message),
|
||||
"is_duplicate": index in duplicate_indices
|
||||
}
|
||||
|
||||
# Créer une copie du message pour éviter de modifier l'original
|
||||
message_copy = message.copy()
|
||||
|
||||
# Ajouter les métadonnées au message copié
|
||||
for key, value in message_metadata[message_id].items():
|
||||
message_copy[key] = value
|
||||
|
||||
# Nettoyer le corps du message selon la stratégie choisie
|
||||
if message_copy.get('body'):
|
||||
# Toujours conserver l'original
|
||||
message_copy['body_original'] = message_copy.get('body', '')
|
||||
|
||||
# Appliquer la stratégie de nettoyage, sauf si raw
|
||||
if strategy != "raw":
|
||||
cleaned_body = clean_html(
|
||||
message_copy.get('body', ''),
|
||||
strategy=cleaning_config['strategy'],
|
||||
preserve_links=cleaning_config['preserve_links'],
|
||||
preserve_images=cleaning_config['preserve_images']
|
||||
)
|
||||
|
||||
# Nettoyer davantage le code HTML qui pourrait rester
|
||||
if cleaned_body:
|
||||
# Supprimer les balises style et script avec leur contenu
|
||||
cleaned_body = re.sub(r'<style[^>]*>.*?</style>', '', cleaned_body, flags=re.DOTALL)
|
||||
cleaned_body = re.sub(r'<script[^>]*>.*?</script>', '', cleaned_body, flags=re.DOTALL)
|
||||
# Supprimer les balises HTML restantes
|
||||
cleaned_body = re.sub(r'<[^>]+>', '', cleaned_body)
|
||||
|
||||
message_copy['body'] = cleaned_body
|
||||
|
||||
# Récupérer les détails de l'auteur
|
||||
message_copy['author_details'] = self.get_message_author_details(message_copy)
|
||||
|
||||
# Ne pas inclure les messages système sans intérêt
|
||||
if message_copy.get('is_system') and not message_copy.get('is_stage_change'):
|
||||
# Enregistrer l'exclusion dans les métadonnées
|
||||
message_metadata[message_id]['excluded'] = "system_message"
|
||||
continue
|
||||
|
||||
# Ignorer les messages dupliqués si demandé
|
||||
if message_copy.get('is_duplicate'):
|
||||
# Enregistrer l'exclusion dans les métadonnées
|
||||
message_metadata[message_id]['excluded'] = "duplicate_content"
|
||||
continue
|
||||
|
||||
processed_messages.append(message_copy)
|
||||
|
||||
# Trier les messages par date
|
||||
processed_messages.sort(key=lambda x: x.get('date', ''))
|
||||
|
||||
# Récupérer les informations supplémentaires du ticket
|
||||
try:
|
||||
ticket_data = self.auth._rpc_call("/web/dataset/call_kw", {
|
||||
"model": "project.task",
|
||||
"method": "read",
|
||||
"args": [[ticket_id]],
|
||||
"kwargs": {"fields": ["project_id", "stage_id"]}
|
||||
})
|
||||
|
||||
project_id = None
|
||||
stage_id = None
|
||||
project_name = None
|
||||
stage_name = None
|
||||
|
||||
if ticket_data and isinstance(ticket_data, list) and len(ticket_data) > 0:
|
||||
if "project_id" in ticket_data[0] and ticket_data[0]["project_id"]:
|
||||
project_id = ticket_data[0]["project_id"][0] if isinstance(ticket_data[0]["project_id"], list) else ticket_data[0]["project_id"]
|
||||
project_name = ticket_data[0]["project_id"][1] if isinstance(ticket_data[0]["project_id"], list) else None
|
||||
|
||||
if "stage_id" in ticket_data[0] and ticket_data[0]["stage_id"]:
|
||||
stage_id = ticket_data[0]["stage_id"][0] if isinstance(ticket_data[0]["stage_id"], list) else ticket_data[0]["stage_id"]
|
||||
stage_name = ticket_data[0]["stage_id"][1] if isinstance(ticket_data[0]["stage_id"], list) else None
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors de la récupération des informations du ticket: {e}")
|
||||
project_id = None
|
||||
stage_id = None
|
||||
project_name = None
|
||||
stage_name = None
|
||||
|
||||
# Créer la structure pour le JSON
|
||||
messages_with_summary = {
|
||||
"ticket_summary": {
|
||||
"id": ticket_id,
|
||||
"code": ticket_code,
|
||||
"name": ticket_name,
|
||||
"project_id": project_id,
|
||||
"project_name": project_name,
|
||||
"stage_id": stage_id,
|
||||
"stage_name": stage_name,
|
||||
"date_extraction": datetime.now().isoformat()
|
||||
},
|
||||
"metadata": {
|
||||
"message_count": {
|
||||
"total": len(messages),
|
||||
"processed": len(processed_messages),
|
||||
"excluded": len(messages) - len(processed_messages)
|
||||
},
|
||||
"cleaning_strategy": strategy,
|
||||
"cleaning_config": cleaning_config
|
||||
},
|
||||
"messages": processed_messages
|
||||
}
|
||||
|
||||
# Sauvegarder les messages en JSON
|
||||
all_messages_path = os.path.join(output_dir, "all_messages.json")
|
||||
save_json(messages_with_summary, all_messages_path)
|
||||
|
||||
# Sauvegarder également les messages bruts
|
||||
raw_messages_path = os.path.join(output_dir, "messages_raw.json")
|
||||
save_json({
|
||||
"ticket_id": ticket_id,
|
||||
"ticket_code": ticket_code,
|
||||
"message_metadata": message_metadata,
|
||||
"messages": messages
|
||||
}, raw_messages_path)
|
||||
|
||||
# Créer un fichier texte pour une lecture plus facile
|
||||
messages_text_path = os.path.join(output_dir, "all_messages.txt")
|
||||
|
||||
try:
|
||||
text_content = self._generate_messages_text(ticket_code, ticket_name, processed_messages)
|
||||
save_text(text_content, messages_text_path)
|
||||
except Exception as e:
|
||||
logging.error(f"Erreur lors de la création du fichier texte: {e}")
|
||||
|
||||
return {
|
||||
"all_messages_path": all_messages_path,
|
||||
"raw_messages_path": raw_messages_path,
|
||||
"messages_text_path": messages_text_path,
|
||||
"messages_count": len(processed_messages),
|
||||
"total_messages": len(messages)
|
||||
}
|
||||
|
||||
def _generate_messages_text(self, ticket_code: str, ticket_name: str,
|
||||
processed_messages: List[Dict[str, Any]]) -> str:
|
||||
"""
|
||||
Génère un fichier texte formaté à partir des messages traités.
|
||||
|
||||
Args:
|
||||
ticket_code: Code du ticket
|
||||
ticket_name: Nom du ticket
|
||||
processed_messages: Liste des messages traités
|
||||
|
||||
Returns:
|
||||
Contenu du fichier texte
|
||||
"""
|
||||
content = []
|
||||
|
||||
# Informations sur le ticket
|
||||
content.append(f"TICKET: {ticket_code} - {ticket_name}")
|
||||
content.append(f"Date d'extraction: {datetime.now().strftime('%Y-%m-%d %H:%M:%S')}")
|
||||
content.append(f"Nombre de messages: {len(processed_messages)}")
|
||||
content.append("\n" + "="*80 + "\n")
|
||||
|
||||
# Parcourir les messages filtrés
|
||||
for msg in processed_messages:
|
||||
author = msg.get('author_details', {}).get('name', msg.get('email_from', 'Inconnu'))
|
||||
date = msg.get('date', '')
|
||||
subject = msg.get('subject', 'Sans objet')
|
||||
body = msg.get('body', '')
|
||||
|
||||
# Formater différemment les messages spéciaux
|
||||
if msg.get('is_stage_change'):
|
||||
content.append("*"*80)
|
||||
content.append("*** CHANGEMENT D'ÉTAT ***")
|
||||
content.append("*"*80 + "\n")
|
||||
elif msg.get('is_forwarded'):
|
||||
content.append("*"*80)
|
||||
content.append("*** MESSAGE TRANSFÉRÉ ***")
|
||||
content.append("*"*80 + "\n")
|
||||
|
||||
# En-tête du message
|
||||
content.append(f"DATE: {date}")
|
||||
content.append(f"DE: {author}")
|
||||
if subject:
|
||||
content.append(f"OBJET: {subject}")
|
||||
content.append("")
|
||||
content.append(f"{body}")
|
||||
content.append("\n" + "-"*80 + "\n")
|
||||
|
||||
return "\n".join(content)
|
||||
@ -278,9 +278,7 @@ class MessageManager:
|
||||
if strategy != "raw":
|
||||
cleaned_body = clean_html(
|
||||
message_copy.get('body', ''),
|
||||
strategy=cleaning_config['strategy'],
|
||||
preserve_links=cleaning_config['preserve_links'],
|
||||
preserve_images=cleaning_config['preserve_images']
|
||||
is_forwarded=message_copy.get('is_forwarded', False)
|
||||
)
|
||||
|
||||
# Nettoyer davantage le code HTML qui pourrait rester
|
||||
@ -296,12 +294,51 @@ class MessageManager:
|
||||
# Récupérer les détails de l'auteur
|
||||
message_copy['author_details'] = self.get_message_author_details(message_copy)
|
||||
|
||||
# Ne pas inclure les messages système sans intérêt
|
||||
if message_copy.get('is_system') and not message_copy.get('is_stage_change'):
|
||||
# Vérifier si le message contient des éléments importants
|
||||
has_attachments = bool(message_copy.get('attachment_ids'))
|
||||
has_images = False
|
||||
has_meaningful_content = False
|
||||
|
||||
# Vérifier la présence d'images dans le HTML
|
||||
if message_copy.get('body_original'):
|
||||
# Rechercher les balises img dans le HTML
|
||||
has_images = '<img' in message_copy.get('body_original', '').lower() or '/web/image/' in message_copy.get('body_original', '').lower()
|
||||
|
||||
# Rechercher des éléments d'images Odoo spécifiques
|
||||
if '/web/image/' in message_copy.get('body_original', ''):
|
||||
has_images = True
|
||||
|
||||
# Vérifier si le corps du message contient du texte significatif
|
||||
body_text = message_copy.get('body', '')
|
||||
if body_text and len(body_text.strip()) > 30: # Texte non vide et d'une certaine longueur
|
||||
has_meaningful_content = True
|
||||
|
||||
# Déterminer si le message doit être conservé malgré son statut système
|
||||
is_important = (
|
||||
has_attachments or
|
||||
has_images or
|
||||
message_copy.get('is_forwarded') or
|
||||
has_meaningful_content or
|
||||
message_copy.get('is_stage_change')
|
||||
)
|
||||
|
||||
# Ne pas inclure les messages système UNIQUEMENT s'ils n'ont rien d'important
|
||||
if message_copy.get('is_system') and not is_important:
|
||||
# Enregistrer l'exclusion dans les métadonnées
|
||||
message_metadata[message_id]['excluded'] = "system_message"
|
||||
continue
|
||||
|
||||
# Si le message est marqué comme exclu dans les métadonnées mais qu'il est transféré, le réintégrer
|
||||
if message_metadata.get(message_id, {}).get('excluded') == "system_message" and message_copy.get('is_forwarded'):
|
||||
# Supprimer l'exclusion des métadonnées
|
||||
del message_metadata[message_id]['excluded']
|
||||
|
||||
# Vérifier aussi les messages qui sont déjà exclus dans les métadonnées d'entrée
|
||||
# et les réintégrer s'ils sont transférés
|
||||
if 'excluded' in message_metadata.get(message_id, {}) and message_copy.get('is_forwarded'):
|
||||
# Supprimer l'exclusion des métadonnées
|
||||
del message_metadata[message_id]['excluded']
|
||||
|
||||
# Ignorer les messages dupliqués si demandé
|
||||
if message_copy.get('is_duplicate'):
|
||||
# Enregistrer l'exclusion dans les métadonnées
|
||||
@ -313,6 +350,46 @@ class MessageManager:
|
||||
# Trier les messages par date
|
||||
processed_messages.sort(key=lambda x: x.get('date', ''))
|
||||
|
||||
# Étape supplémentaire: Vérifier si des messages transférés ont été exclus et les réintégrer
|
||||
processed_ids = {msg['id'] for msg in processed_messages if 'id' in msg}
|
||||
for message in messages:
|
||||
message_id = message.get('id')
|
||||
if (message_id not in processed_ids and
|
||||
message_metadata.get(message_id, {}).get('is_forwarded') and
|
||||
'excluded' in message_metadata.get(message_id, {})):
|
||||
# Créer une copie du message
|
||||
message_copy = message.copy()
|
||||
# Ajouter les métadonnées au message
|
||||
for key, value in message_metadata[message_id].items():
|
||||
if key != 'excluded': # Ne pas ajouter le tag d'exclusion
|
||||
message_copy[key] = value
|
||||
# Si le message a un corps, on applique le même traitement de nettoyage
|
||||
if message_copy.get('body'):
|
||||
# Toujours conserver l'original
|
||||
message_copy['body_original'] = message_copy.get('body', '')
|
||||
# Appliquer la stratégie de nettoyage, sauf si raw
|
||||
if strategy != "raw":
|
||||
cleaned_body = clean_html(
|
||||
message_copy.get('body', ''),
|
||||
is_forwarded=message_copy.get('is_forwarded', False)
|
||||
)
|
||||
# Nettoyage supplémentaire
|
||||
if cleaned_body:
|
||||
cleaned_body = re.sub(r'<style[^>]*>.*?</style>', '', cleaned_body, flags=re.DOTALL)
|
||||
cleaned_body = re.sub(r'<script[^>]*>.*?</script>', '', cleaned_body, flags=re.DOTALL)
|
||||
cleaned_body = re.sub(r'<[^>]+>', '', cleaned_body)
|
||||
message_copy['body'] = cleaned_body
|
||||
# Récupérer les détails de l'auteur
|
||||
message_copy['author_details'] = self.get_message_author_details(message_copy)
|
||||
# Supprimer l'exclusion des métadonnées
|
||||
if 'excluded' in message_metadata[message_id]:
|
||||
del message_metadata[message_id]['excluded']
|
||||
# Ajouter le message aux messages traités
|
||||
processed_messages.append(message_copy)
|
||||
|
||||
# Trier à nouveau les messages par date après la réintégration
|
||||
processed_messages.sort(key=lambda x: x.get('date', ''))
|
||||
|
||||
# Récupérer les informations supplémentaires du ticket
|
||||
try:
|
||||
ticket_data = self.auth._rpc_call("/web/dataset/call_kw", {
|
||||
|
||||
@ -1,33 +0,0 @@
|
||||
{
|
||||
"id": "11122",
|
||||
"code": "T11143",
|
||||
"name": "BRGLAB - Essai inaccessible",
|
||||
"description": "*Aucune description fournie*",
|
||||
"project_name": "Demandes",
|
||||
"stage_name": "Clôturé",
|
||||
"user_id": "",
|
||||
"partner_id_email_from": "GIRAUD TP (JCG), Victor BOLLÉE, v.bollee@labojcg.fr",
|
||||
"create_date": "03/04/2025 08:34:43",
|
||||
"write_date_last_modification": "03/04/2025 12:23:31",
|
||||
"date_deadline": "18/04/2025 00:00:00",
|
||||
"messages": [
|
||||
{
|
||||
"author_id": "Fabien LAFAY",
|
||||
"date": "03/04/2025 12:17:41",
|
||||
"message_type": "E-mail",
|
||||
"subject": "Re: [T11143] - BRGLAB - Essai inaccessible",
|
||||
"id": "228968",
|
||||
"content": "Bonjour,\nPouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :\nhttps://zk1.brg-lab.com/\nVoici ce que vous devriez voir affiché :\nSi ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?\nJe reste à votre entière disposition pour toute information complémentaire.\nCordialement,\n---\nSupport technique\nL'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.\n*Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport avec la communication du contenu des informations est strictement interdit.*\n\n- image.png (image/png) [ID: 145453]\n\n---\n\n"
|
||||
},
|
||||
{
|
||||
"author_id": "Victor BOLLÉE",
|
||||
"date": "03/04/2025 12:21:13",
|
||||
"message_type": "E-mail",
|
||||
"subject": "TR: [T11143] - BRGLAB - Essai inaccessible",
|
||||
"id": "228971",
|
||||
"content": "Bonjour,\nLe problème s’est résolu seul par la suite.\nJe vous remercie pour votre retour.\nBonne journée\nPS : l’adresse fonctionne\nsupport@cbao.fr <support@cbao.fr>\nVoir\nTâche\nBonjour,\nPouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :\nhttps://zk1.brg-lab.com/\nVoici ce que vous devriez voir affiché :\nSi ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?\nJe reste à votre entière disposition pour toute information complémentaire.\nCordialement,\n---\ntechnique à **support@cbao.fr**\nL'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de\nnos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.\nConfidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous\ndeviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport\navec la communication du contenu des informations est strictement interdit.\nEnvoyé par\nCBAO S.A.R.L. .\n\n---\n"
|
||||
}
|
||||
],
|
||||
"date_d'extraction": "15/04/2025 15:12:33",
|
||||
"répertoire": "output/ticket_T11143/T11143_20250415_151222"
|
||||
}
|
||||
@ -1,86 +0,0 @@
|
||||
# Ticket T11143: BRGLAB - Essai inaccessible
|
||||
|
||||
## Informations du ticket
|
||||
|
||||
- **id**: 11122
|
||||
- **code**: T11143
|
||||
- **name**: BRGLAB - Essai inaccessible
|
||||
- **project_name**: Demandes
|
||||
- **stage_name**: Clôturé
|
||||
- **user_id**:
|
||||
- **partner_id/email_from**: GIRAUD TP (JCG), Victor BOLLÉE, v.bollee@labojcg.fr
|
||||
- **create_date**: 03/04/2025 08:34:43
|
||||
- **write_date/last modification**: 03/04/2025 12:23:31
|
||||
- **date_deadline**: 18/04/2025 00:00:00
|
||||
|
||||
- **description**:
|
||||
|
||||
*Aucune description fournie*
|
||||
|
||||
## Messages
|
||||
|
||||
### Message 1
|
||||
**author_id**: Fabien LAFAY
|
||||
**date**: 03/04/2025 12:17:41
|
||||
**message_type**: E-mail
|
||||
**subject**: Re: [T11143] - BRGLAB - Essai inaccessible
|
||||
**id**: 228968
|
||||
Bonjour,
|
||||
Pouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :
|
||||
https://zk1.brg-lab.com/
|
||||
Voici ce que vous devriez voir affiché :
|
||||
Si ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?
|
||||
Je reste à votre entière disposition pour toute information complémentaire.
|
||||
Cordialement,
|
||||
---
|
||||
Support technique
|
||||
L'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.
|
||||
*Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport avec la communication du contenu des informations est strictement interdit.*
|
||||
|
||||
**attachment_ids**:
|
||||
- image.png (image/png) [ID: 145453]
|
||||
|
||||
---
|
||||
|
||||
### Message 2
|
||||
**author_id**: Victor BOLLÉE
|
||||
**date**: 03/04/2025 12:21:13
|
||||
**message_type**: E-mail
|
||||
**subject**: TR: [T11143] - BRGLAB - Essai inaccessible
|
||||
**id**: 228971
|
||||
Bonjour,
|
||||
Le problème s’est résolu seul par la suite.
|
||||
Je vous remercie pour votre retour.
|
||||
Bonne journée
|
||||
PS : l’adresse fonctionne
|
||||
**De :**
|
||||
support@cbao.fr <support@cbao.fr>
|
||||
**Envoyé :** jeudi 3 avril 2025 14:18
|
||||
**À :** victor Bollée <v.bollee@labojcg.fr>
|
||||
**Objet :** Re: [T11143] - BRGLAB - Essai inaccessible
|
||||
Voir
|
||||
Tâche
|
||||
Bonjour,
|
||||
Pouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :
|
||||
https://zk1.brg-lab.com/
|
||||
Voici ce que vous devriez voir affiché :
|
||||
Si ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?
|
||||
Je reste à votre entière disposition pour toute information complémentaire.
|
||||
Cordialement,
|
||||
---
|
||||
**Support technique**
|
||||
technique à **support@cbao.fr**
|
||||
L'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de
|
||||
nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.
|
||||
Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous
|
||||
deviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport
|
||||
avec la communication du contenu des informations est strictement interdit.
|
||||
Envoyé par
|
||||
CBAO S.A.R.L. .
|
||||
|
||||
---
|
||||
|
||||
## Informations sur l'extraction
|
||||
|
||||
- **Date d'extraction**: 15/04/2025 15:12:33
|
||||
- **Répertoire**: output/ticket_T11143/T11143_20250415_151222
|
||||
@ -1,20 +0,0 @@
|
||||
[
|
||||
{
|
||||
"id": 145453,
|
||||
"name": "image.png",
|
||||
"mimetype": "image/png",
|
||||
"file_size": 76543,
|
||||
"create_date": "2025-04-03 12:17:41",
|
||||
"create_uid": [
|
||||
22,
|
||||
"Fabien LAFAY"
|
||||
],
|
||||
"description": false,
|
||||
"res_name": "[T11143] BRGLAB - Essai inaccessible",
|
||||
"creator_name": "Fabien LAFAY",
|
||||
"creator_id": 22,
|
||||
"download_status": "success",
|
||||
"local_path": "output/ticket_T11143/T11143_20250415_151222/attachments/image.png",
|
||||
"error": ""
|
||||
}
|
||||
]
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
@ -1,6 +1,6 @@
|
||||
TICKET: T11143 - BRGLAB - Essai inaccessible
|
||||
Date d'extraction: 2025-04-15 15:12:23
|
||||
Nombre de messages: 6
|
||||
Date d'extraction: 2025-04-15 17:18:34
|
||||
Nombre de messages: 7
|
||||
|
||||
================================================================================
|
||||
|
||||
@ -13,6 +13,18 @@ DE: Fabien LAFAY
|
||||
|
||||
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
********************************************************************************
|
||||
*** MESSAGE TRANSFÉRÉ ***
|
||||
********************************************************************************
|
||||
|
||||
DATE: 2025-04-03 08:35:20
|
||||
DE: Fabien LAFAY
|
||||
OBJET: Re: [T11143] BRGLAB - Essai inaccessible
|
||||
|
||||
*Contenu non extractible*
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
********************************************************************************
|
||||
@ -35,29 +47,21 @@ DE: Fabien LAFAY
|
||||
OBJET: Re: [T11143] - BRGLAB - Essai inaccessible
|
||||
|
||||
Bonjour,
|
||||
|
||||
Pouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :
|
||||
|
||||
https://zk1.brg-lab.com/
|
||||
|
||||
Voici ce que vous devriez voir affiché :
|
||||
|
||||
Si ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?
|
||||
|
||||
Je reste à votre entière disposition pour toute information complémentaire.
|
||||
|
||||
Cordialement,
|
||||
---
|
||||
|
||||
Support technique
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
L'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.
|
||||
|
||||
*Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport avec la communication du contenu des informations est strictement interdit.*
|
||||
Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous deviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport avec la communication du contenu des informations est strictement interdit.
|
||||
|
||||
--------------------------------------------------------------------------------
|
||||
|
||||
@ -83,54 +87,43 @@ OBJET: TR: [T11143] - BRGLAB - Essai inaccessible
|
||||
Bonjour,
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Le problème s’est résolu seul par la suite.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Je vous remercie pour votre retour.
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bonne journée
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PS : l’adresse fonctionne
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
**De :**
|
||||
De :
|
||||
support@cbao.fr
|
||||
|
||||
|
||||
**Envoyé :** jeudi 3 avril 2025 14:18
|
||||
Envoyé : jeudi 3 avril 2025 14:18
|
||||
|
||||
**À :** victor Bollée
|
||||
À : victor Bollée
|
||||
|
||||
**Objet :** Re: [T11143] - BRGLAB - Essai inaccessible
|
||||
Objet : Re: [T11143] - BRGLAB - Essai inaccessible
|
||||
|
||||
|
||||
|
||||
@ -140,15 +133,12 @@ support@cbao.fr
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Voir
|
||||
Tâche
|
||||
|
||||
@ -161,9 +151,7 @@ Voir
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@ -176,58 +164,46 @@ Voir
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Bonjour,
|
||||
|
||||
|
||||
|
||||
Pouvez-vous vérifier si vous avez bien accès à la page suivante en l'ouvrant dans votre navigateur :
|
||||
|
||||
|
||||
|
||||
https://zk1.brg-lab.com/
|
||||
|
||||
|
||||
|
||||
Voici ce que vous devriez voir affiché :
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
Si ce n'est pas le cas, pouvez-vous me faire une capture d'écran de ce qui est affiché?
|
||||
|
||||
|
||||
|
||||
Je reste à votre entière disposition pour toute information complémentaire.
|
||||
|
||||
|
||||
|
||||
Cordialement,
|
||||
|
||||
|
||||
|
||||
---
|
||||
|
||||
|
||||
|
||||
**Support technique**
|
||||
Support technique
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
technique à **support@cbao.fr**
|
||||
technique à support@cbao.fr
|
||||
|
||||
L'objectif du Support Technique est de vous aider : si vous rencontrez une difficulté, ou pour nous soumettre une ou des suggestions d'amélioration de nos logiciels ou de
|
||||
nos méthodes. Notre service est ouvert du lundi au vendredi de 9h à 12h et de 14h à 18h. Dès réception, un technicien prendra en charge votre demande et au besoin vous rappellera.
|
||||
|
||||
|
||||
|
||||
Confidentialité : Ce courriel contient des informations confidentielles exclusivement réservées au destinataire mentionné. Si vous
|
||||
deviez recevoir cet e-mail par erreur, merci d’en avertir immédiatement l’expéditeur et de le supprimer de votre système informatique. Au cas où vous ne seriez pas destinataire de ce message, veuillez noter que sa divulgation, sa copie ou tout acte en rapport
|
||||
avec la communication du contenu des informations est strictement interdit.
|
||||
@ -235,7 +211,6 @@ Confidentialité : Ce courriel contient des informations confidentielles exclusi
|
||||
|
||||
|
||||
|
||||
|
||||
Envoyé par
|
||||
CBAO S.A.R.L. .
|
||||
|
||||
|
Before Width: | Height: | Size: 75 KiB After Width: | Height: | Size: 75 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 25 KiB |
@ -0,0 +1,34 @@
|
||||
[
|
||||
{
|
||||
"id": 145453,
|
||||
"name": "image.png",
|
||||
"mimetype": "image/png",
|
||||
"file_size": 76543,
|
||||
"create_date": "2025-04-03 12:17:41",
|
||||
"create_uid": [
|
||||
22,
|
||||
"Fabien LAFAY"
|
||||
],
|
||||
"description": false,
|
||||
"res_name": "[T11143] BRGLAB - Essai inaccessible",
|
||||
"creator_name": "Fabien LAFAY",
|
||||
"creator_id": 22,
|
||||
"download_status": "success",
|
||||
"local_path": "output/ticket_T11143/T11143_20250415_171834/attachments/image.png",
|
||||
"error": ""
|
||||
},
|
||||
{
|
||||
"id": 145435,
|
||||
"name": "image_145435.png",
|
||||
"mimetype": "image/png",
|
||||
"file_size": 25267,
|
||||
"create_date": "2025-04-15 17:37:30",
|
||||
"creator_name": "Fabien LAFAY",
|
||||
"download_status": "success",
|
||||
"local_path": "output/ticket_T11143/T11143_20250415_171834/attachments/image_145435.png",
|
||||
"error": "",
|
||||
"was_missing": true,
|
||||
"message_id": 228942,
|
||||
"access_token": "608ac9e7-3627-4a13-a8ec-06ff5046ebf3"
|
||||
}
|
||||
]
|
||||
@ -0,0 +1,13 @@
|
||||
{
|
||||
"timestamp": "20250415_171834",
|
||||
"ticket_code": "T11143",
|
||||
"output_directory": "output/ticket_T11143/T11143_20250415_171834",
|
||||
"message_count": 7,
|
||||
"attachment_count": 1,
|
||||
"files_created": [
|
||||
"ticket_info.json",
|
||||
"ticket_summary.json",
|
||||
"all_messages.json",
|
||||
"structure.json"
|
||||
]
|
||||
}
|
||||
@ -12,8 +12,7 @@
|
||||
"is_system": true,
|
||||
"is_stage_change": false,
|
||||
"is_forwarded": true,
|
||||
"is_duplicate": false,
|
||||
"excluded": "system_message"
|
||||
"is_duplicate": false
|
||||
},
|
||||
"228947": {
|
||||
"is_system": true,
|
||||
@ -1,9 +1,9 @@
|
||||
{
|
||||
"date_extraction": "2025-04-15T15:12:23.158435",
|
||||
"date_extraction": "2025-04-15T17:18:34.902937",
|
||||
"ticket_id": 11122,
|
||||
"ticket_code": "T11143",
|
||||
"ticket_name": "BRGLAB - Essai inaccessible",
|
||||
"output_dir": "output/ticket_T11143/T11143_20250415_151222",
|
||||
"output_dir": "output/ticket_T11143/T11143_20250415_171834",
|
||||
"files": {
|
||||
"ticket_info": "ticket_info.json",
|
||||
"ticket_summary": "ticket_summary.json",
|
||||
@ -14,7 +14,7 @@
|
||||
"followers": "followers.json"
|
||||
},
|
||||
"stats": {
|
||||
"messages_count": 6,
|
||||
"messages_count": 7,
|
||||
"attachments_count": 1
|
||||
}
|
||||
}
|
||||
@ -145,3 +145,25 @@
|
||||
2025-04-15 15:01:57 - root - INFO - Messages traités: 5
|
||||
2025-04-15 15:01:57 - root - INFO - Pièces jointes: 3
|
||||
2025-04-15 15:01:57 - root - INFO - ------------------------------------------------------------
|
||||
2025-04-15 16:52:51 - root - INFO - Extraction du ticket T11143
|
||||
2025-04-15 16:52:51 - root - INFO - ------------------------------------------------------------
|
||||
2025-04-15 16:52:52 - root - INFO - Traitement de 1 pièces jointes pour le ticket 11122
|
||||
2025-04-15 16:52:52 - root - INFO - Pièce jointe téléchargée: image.png (1/1)
|
||||
2025-04-15 16:52:52 - root - INFO - ------------------------------------------------------------
|
||||
2025-04-15 16:52:52 - root - INFO - Extraction terminée avec succès
|
||||
2025-04-15 16:52:52 - root - INFO - Ticket: T11143
|
||||
2025-04-15 16:52:52 - root - INFO - Répertoire: output/ticket_T11143/T11143_20250415_165251
|
||||
2025-04-15 16:52:52 - root - INFO - Messages traités: 7
|
||||
2025-04-15 16:52:52 - root - INFO - Pièces jointes: 1
|
||||
2025-04-15 16:52:52 - root - INFO - ------------------------------------------------------------
|
||||
2025-04-15 17:18:34 - root - INFO - Extraction du ticket T11143
|
||||
2025-04-15 17:18:34 - root - INFO - ------------------------------------------------------------
|
||||
2025-04-15 17:18:34 - root - INFO - Traitement de 1 pièces jointes pour le ticket 11122
|
||||
2025-04-15 17:18:34 - root - INFO - Pièce jointe téléchargée: image.png (1/1)
|
||||
2025-04-15 17:18:34 - root - INFO - ------------------------------------------------------------
|
||||
2025-04-15 17:18:34 - root - INFO - Extraction terminée avec succès
|
||||
2025-04-15 17:18:34 - root - INFO - Ticket: T11143
|
||||
2025-04-15 17:18:34 - root - INFO - Répertoire: output/ticket_T11143/T11143_20250415_171834
|
||||
2025-04-15 17:18:34 - root - INFO - Messages traités: 7
|
||||
2025-04-15 17:18:34 - root - INFO - Pièces jointes: 1
|
||||
2025-04-15 17:18:34 - root - INFO - ------------------------------------------------------------
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user