seo-generator-server/lib/ElementExtraction.js
StillHammer 64fb319e65 refactor: Synchronisation complète du codebase - Application de tous les patches
Application systématique et méthodique de tous les patches historiques.

##  FICHIERS SYNCHRONISÉS (19 fichiers)

### Core & Infrastructure:
- server.js (14 patches) - Lazy loading ModeManager, SIGINT hard kill, timing logs
- ModeManager.js (4 patches) - Instrumentation complète avec timing détaillé

### Pipeline System:
- PipelineDefinition.js (6 patches) - Source unique getLLMProvidersList()
- pipeline-builder.js (8 patches) - Standardisation LLM providers
- pipeline-runner.js (6 patches) - Affichage résultats structurés + debug console
- pipeline-builder.html (2 patches) - Fallback providers synchronisés
- pipeline-runner.html (3 patches) - UI améliorée résultats

### Enhancement Layers:
- TechnicalLayer.js (1 patch) - defaultLLM: 'gpt-4o-mini'
- StyleLayer.js (1 patch) - Type safety vocabulairePref
- PatternBreakingCore.js (1 patch) - Mapping modifications
- PatternBreakingLayers.js (1 patch) - LLM standardisé

### Validators & Tests:
- QualityMetrics.js (1 patch) - callLLM('gpt-4o-mini')
- PersonalityValidator.js (1 patch) - Provider gpt-4o-mini
- AntiDetectionValidator.js - Synchronisé

### Documentation:
- TODO.md (1 patch) - Section LiteLLM pour tracking coûts
- CLAUDE.md - Documentation à jour

### Tools:
- tools/analyze-skipped-exports.js (nouveau)
- tools/apply-claude-exports.js (nouveau)
- tools/apply-claude-exports-fuzzy.js (nouveau)

## 🎯 Changements principaux:
-  Standardisation LLM providers (openai → gpt-4o-mini, claude → claude-sonnet-4-5)
-  Lazy loading optimisé (ModeManager chargé à la demande)
-  SIGINT immediate exit (pas de graceful shutdown)
-  Type safety renforcé (conversions string explicites)
-  Instrumentation timing complète
-  Debug logging amélioré (console.log résultats pipeline)

🤖 Generated with [Claude Code](https://claude.com/claude-code)

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-12 20:36:17 +08:00

547 lines
19 KiB
JavaScript

// ========================================
// FICHIER: lib/element-extraction.js - CONVERTI POUR NODE.JS
// Description: Extraction et parsing des éléments XML
// ========================================
// 🔄 NODE.JS IMPORTS
const { logSh } = require('./ErrorReporting');
const { logElementsList } = require('./selective-enhancement/SelectiveUtils');
// ============= EXTRACTION PRINCIPALE =============
async function extractElements(xmlTemplate, csvData) {
try {
await logSh('Extraction éléments avec séparation tag/contenu...', 'DEBUG');
const regex = /\|([^|]+)\|/g;
const elements = [];
let match;
while ((match = regex.exec(xmlTemplate)) !== null) {
const originalMatch = match[1];
let fullMatch = match[1]; // Ex: "Titre_H1_1{{T0}}" ou "Titre_H3_3{{MC+1_3}}"
// RÉPARER les variables cassées par les balises HTML AVANT de les chercher
// Ex: <strong>{{</strong>MC+1_1}} → {{MC+1_1}}
fullMatch = fullMatch
.replace(/<strong>\{\{<\/strong>/g, '{{')
.replace(/<strong>\{<\/strong>/g, '{')
.replace(/<code><strong>\{\{<\/strong><\/code>/g, '{{')
.replace(/<strong><strong>\{\{<\/strong>/g, '{{')
.replace(/<\/strong>\}\}<\/strong>/g, '}}')
.replace(/<\/strong>\}<\/strong>/g, '}')
.replace(/<\/strong>/g, '') // Enlever </strong> orphelins
.replace(/<strong>/g, '') // Enlever <strong> orphelins
.replace(/<code>/g, '') // Enlever <code> orphelins
.replace(/<\/code>/g, ''); // Enlever </code> orphelins
// Log debug si changement
if (originalMatch !== fullMatch && originalMatch.includes('{{')) {
await logSh(` 🔧 Réparation HTML: "${originalMatch.substring(0, 80)}" → "${fullMatch.substring(0, 80)}"`, 'DEBUG');
}
// Séparer nom du tag et variables
const nameMatch = fullMatch.match(/^([^{]+)/);
let tagName = nameMatch ? nameMatch[1].trim() : fullMatch.split('{')[0];
tagName = tagName.replace(/<\/?strong>/g, ''); // Nettoyage
const variablesMatch = fullMatch.match(/\{\{([^}]+)\}\}/g);
// CAPTURER les instructions EN GARDANT les {{variables}} intactes
// Stratégie : d'abord enlever temporairement toutes les {{variables}},
// trouver la position de {instruction}, puis revenir au texte original
let instructionsMatch = null;
// Créer une version sans {{variables}} pour trouver où est {instruction}
const withoutVars = fullMatch.replace(/\{\{[^}]+\}\}/g, '');
const tempInstructionMatch = withoutVars.match(/\{([^}]+)\}/);
if (tempInstructionMatch) {
// On a trouvé une instruction dans la version sans variables
// Trouver le PREMIER { qui n'est PAS suivi de { (= début instruction)
let instructionStart = -1;
for (let idx = 0; idx < fullMatch.length - 1; idx++) {
if (fullMatch[idx] === '{' && fullMatch[idx + 1] !== '{') {
instructionStart = idx;
break;
}
}
if (instructionStart !== -1) {
// Capturer jusqu'à la } de fermeture (en ignorant les }} de variables)
let depth = 0;
let instructionEnd = -1;
let i = instructionStart;
while (i < fullMatch.length) {
if (fullMatch[i] === '{') {
if (fullMatch[i+1] === '{') {
// C'est une variable, skip les deux {
i += 2;
continue;
} else {
depth++;
}
} else if (fullMatch[i] === '}') {
if (fullMatch[i+1] === '}') {
// Fin de variable, skip les deux }
i += 2;
continue;
} else {
depth--;
if (depth === 0) {
instructionEnd = i;
break;
}
}
}
i++;
}
if (instructionEnd !== -1) {
const instructionContent = fullMatch.substring(instructionStart + 1, instructionEnd);
instructionsMatch = [fullMatch.substring(instructionStart, instructionEnd + 1), instructionContent];
// Log debug instruction capturée
await logSh(` 📜 Instruction capturée (${tagName}): ${instructionContent.substring(0, 80)}...`, 'DEBUG');
}
}
}
// TAG PUR (sans variables)
const pureTag = `|${tagName}|`;
// RÉSOUDRE le contenu des variables
const resolvedContent = resolveVariablesContent(variablesMatch, csvData);
// RÉSOUDRE aussi les variables DANS les instructions
let resolvedInstructions = instructionsMatch ? instructionsMatch[1] : null;
if (resolvedInstructions) {
const originalInstruction = resolvedInstructions;
// Remplacer chaque variable {{XX}} par sa valeur résolue
resolvedInstructions = resolvedInstructions.replace(/\{\{([^}]+)\}\}/g, (match, variable) => {
const singleVarMatch = [match];
return resolveVariablesContent(singleVarMatch, csvData);
});
// Log si changement
if (originalInstruction !== resolvedInstructions && originalInstruction.includes('{{')) {
await logSh(` ✨ Instructions résolues (${tagName}): ${originalInstruction.substring(0, 60)}${resolvedInstructions.substring(0, 60)}`, 'DEBUG');
}
}
elements.push({
originalTag: pureTag, // ← TAG PUR : |Titre_H3_3|
name: tagName, // ← Titre_H3_3
variables: variablesMatch || [], // ← [{{MC+1_3}}]
resolvedContent: resolvedContent, // ← "Plaque de rue en aluminium"
instructions: resolvedInstructions, // ← Instructions avec variables résolues
type: getElementType(tagName),
originalFullMatch: fullMatch // ← Backup si besoin
});
await logSh(`Tag séparé: ${pureTag} → "${resolvedContent}"`, 'DEBUG');
}
await logSh(`${elements.length} éléments extraits avec séparation`, 'INFO');
// 📊 DÉTAIL DES ÉLÉMENTS EXTRAITS
logElementsList(elements, 'ÉLÉMENTS EXTRAITS (depuis XML + Google Sheets)');
return elements;
} catch (error) {
await logSh(`Erreur extractElements: ${error}`, 'ERROR');
return [];
}
}
// ============= RÉSOLUTION VARIABLES - IDENTIQUE =============
function resolveVariablesContent(variablesMatch, csvData) {
if (!variablesMatch || variablesMatch.length === 0) {
return ""; // Pas de variables à résoudre
}
let resolvedContent = "";
variablesMatch.forEach(variable => {
const cleanVar = variable.replace(/[{}]/g, ''); // Enlever {{ }}
switch (cleanVar) {
case 'T0':
resolvedContent += csvData.t0;
break;
case 'MC0':
resolvedContent += csvData.mc0;
break;
case 'T-1':
resolvedContent += csvData.tMinus1;
break;
case 'L-1':
resolvedContent += csvData.lMinus1;
break;
default:
// Gérer MC+1_1, MC+1_2, etc.
if (cleanVar.startsWith('MC+1_')) {
const index = parseInt(cleanVar.split('_')[1]) - 1;
const mcPlus1 = csvData.mcPlus1.split(',').map(s => s.trim());
resolvedContent += mcPlus1[index] || `[${cleanVar} non défini]`;
}
else if (cleanVar.startsWith('T+1_')) {
const index = parseInt(cleanVar.split('_')[1]) - 1;
const tPlus1 = csvData.tPlus1.split(',').map(s => s.trim());
resolvedContent += tPlus1[index] || `[${cleanVar} non défini]`;
}
else if (cleanVar.startsWith('L+1_')) {
const index = parseInt(cleanVar.split('_')[1]) - 1;
const lPlus1 = csvData.lPlus1.split(',').map(s => s.trim());
resolvedContent += lPlus1[index] || `[${cleanVar} non défini]`;
}
else {
resolvedContent += `[${cleanVar} non résolu]`;
}
break;
}
});
return resolvedContent;
}
// ============= CLASSIFICATION ÉLÉMENTS - IDENTIQUE =============
function getElementType(name) {
if (name.includes('Titre_H1')) return 'titre_h1';
if (name.includes('Titre_H2')) return 'titre_h2';
if (name.includes('Titre_H3')) return 'titre_h3';
if (name.includes('Intro_')) return 'intro';
if (name.includes('Txt_')) return 'texte';
if (name.includes('Faq_q')) return 'faq_question';
if (name.includes('Faq_a')) return 'faq_reponse';
if (name.includes('Faq_H3')) return 'faq_titre';
return 'autre';
}
// ============= GÉNÉRATION SÉQUENTIELLE - ADAPTÉE =============
async function generateAllContent(elements, csvData, xmlTemplate) {
await logSh(`Début génération pour ${elements.length} éléments`, 'INFO');
const generatedContent = {};
for (let index = 0; index < elements.length; index++) {
const element = elements[index];
try {
await logSh(`Élément ${index + 1}/${elements.length}: ${element.name}`, 'DEBUG');
const prompt = createPromptForElement(element, csvData);
// 🔄 NODE.JS : Import callOpenAI depuis LLM manager (le prompt/réponse seront loggés par LLMManager)
const { callLLM } = require('./LLMManager');
const content = await callLLM('gpt-4o-mini', prompt, {}, csvData.personality);
generatedContent[element.originalTag] = content;
// 🔄 NODE.JS : Pas de Utilities.sleep(), les appels API gèrent leur rate limiting
} catch (error) {
await logSh(`ERREUR élément ${element.name}: ${error.toString()}`, 'ERROR');
generatedContent[element.originalTag] = `[Erreur génération: ${element.name}]`;
}
}
await logSh(`Génération terminée. ${Object.keys(generatedContent).length} éléments`, 'INFO');
return generatedContent;
}
// ============= PARSING STRUCTURE - IDENTIQUE =============
function parseElementStructure(element) {
// NETTOYER le nom : enlever <strong>, </strong>, {{...}}, {...}
let cleanName = element.name
.replace(/<\/?strong>/g, '') // ← ENLEVER <strong>
.replace(/\{\{[^}]*\}\}/g, '') // Enlever {{MC0}}
.replace(/\{[^}]*\}/g, ''); // Enlever {instructions}
const parts = cleanName.split('_');
return {
type: parts[0],
level: parts[1],
indices: parts.slice(2).map(Number),
hierarchyPath: parts.slice(1).join('_'),
originalElement: element,
variables: element.variables || [],
instructions: element.instructions
};
}
// ============= HIÉRARCHIE INTELLIGENTE - ADAPTÉE =============
async function buildSmartHierarchy(elements) {
await logSh(`🏗️ CONSTRUCTION HIÉRARCHIE - Début avec ${elements.length} éléments`, 'INFO');
const hierarchy = {};
elements.forEach((element, index) => {
const structure = parseElementStructure(element);
const path = structure.hierarchyPath;
// 📊 LOG: Détailler chaque élément traité
logSh(` [${index + 1}/${elements.length}] ${element.name}`, 'DEBUG');
logSh(` 📍 Path: ${path}`, 'DEBUG');
logSh(` 📝 Type: ${structure.type}`, 'DEBUG');
logSh(` 📄 ResolvedContent: "${element.resolvedContent}"`, 'DEBUG');
logSh(` 📜 Instructions: "${element.instructions ? element.instructions.substring(0, 80) : 'AUCUNE'}"`, 'DEBUG');
if (!hierarchy[path]) {
hierarchy[path] = {
title: null,
text: null,
questions: [],
children: {}
};
}
// Associer intelligemment
if (structure.type === 'Titre') {
hierarchy[path].title = structure; // Tout l'objet avec variables + instructions
logSh(` ✅ Assigné comme TITRE dans hiérarchie[${path}].title`, 'DEBUG');
} else if (structure.type === 'Txt') {
hierarchy[path].text = structure;
logSh(` ✅ Assigné comme TEXTE dans hiérarchie[${path}].text`, 'DEBUG');
} else if (structure.type === 'Intro') {
hierarchy[path].text = structure;
logSh(` ✅ Assigné comme INTRO dans hiérarchie[${path}].text`, 'DEBUG');
} else if (structure.type === 'Faq') {
hierarchy[path].questions.push(structure);
logSh(` ✅ Ajouté comme FAQ dans hiérarchie[${path}].questions`, 'DEBUG');
}
});
// 📊 LOG: Résumé de la hiérarchie construite
const mappingSummary = Object.keys(hierarchy).map(path => {
const section = hierarchy[path];
return `${path}:[T:${section.title ? '✓' : '✗'} Txt:${section.text ? '✓' : '✗'} FAQ:${section.questions.length}]`;
}).join(' | ');
await logSh(`📊 HIÉRARCHIE CONSTRUITE: ${Object.keys(hierarchy).length} sections`, 'INFO');
await logSh(` ${mappingSummary}`, 'INFO');
// 📊 LOG: Détail complet d'une section exemple
const firstPath = Object.keys(hierarchy)[0];
if (firstPath) {
const firstSection = hierarchy[firstPath];
await logSh(`📋 EXEMPLE SECTION [${firstPath}]:`, 'DEBUG');
if (firstSection.title) {
await logSh(` 📌 Title.instructions: "${firstSection.title.instructions ? firstSection.title.instructions.substring(0, 100) : 'AUCUNE'}"`, 'DEBUG');
}
if (firstSection.text) {
await logSh(` 📌 Text.instructions: "${firstSection.text.instructions ? firstSection.text.instructions.substring(0, 100) : 'AUCUNE'}"`, 'DEBUG');
}
}
return hierarchy;
}
// ============= PARSERS RÉPONSES - ADAPTÉS =============
async function parseTitlesResponse(response, allTitles) {
const results = {};
// Utiliser regex pour extraire [TAG] contenu
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = match[2].trim();
// Nettoyer le contenu (enlever # et balises HTML si présentes)
const cleanContent = content
.replace(/^#+\s*/, '') // Enlever # du début
.replace(/<\/?[^>]+(>|$)/g, ""); // Enlever balises HTML
results[`|${tag}|`] = cleanContent;
await logSh(`✓ Titre parsé [${tag}]: "${cleanContent}"`, 'DEBUG');
}
// Fallback si parsing échoue
if (Object.keys(results).length === 0) {
await logSh('Parsing titres échoué, fallback ligne par ligne', 'WARNING');
const lines = response.split('\n').filter(line => line.trim());
allTitles.forEach((titleInfo, index) => {
if (lines[index]) {
results[titleInfo.tag] = lines[index].trim();
}
});
}
return results;
}
async function parseTextsResponse(response, allTexts) {
const results = {};
await logSh('Parsing réponse textes avec vrais tags...', 'DEBUG');
// Utiliser regex pour extraire [TAG] contenu avec les vrais noms
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = match[2].trim();
// Nettoyer le contenu
const cleanContent = content.replace(/^#+\s*/, '').replace(/<\/?[^>]+(>|$)/g, "");
results[`|${tag}|`] = cleanContent;
await logSh(`✓ Texte parsé [${tag}]: "${cleanContent}"`, 'DEBUG');
}
// Fallback si parsing échoue - mapper par position
if (Object.keys(results).length === 0) {
await logSh('Parsing textes échoué, fallback ligne par ligne', 'WARNING');
const lines = response.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && !line.startsWith('['));
for (let index = 0; index < allTexts.length; index++) {
const textInfo = allTexts[index];
if (index < lines.length) {
let content = lines[index];
content = content.replace(/^\d+\.\s*/, ''); // Enlever "1. " si présent
results[textInfo.tag] = content;
await logSh(`✓ Texte fallback ${index + 1}${textInfo.tag}: "${content}"`, 'DEBUG');
} else {
await logSh(`✗ Pas assez de lignes pour ${textInfo.tag}`, 'WARNING');
results[textInfo.tag] = `[Texte manquant ${index + 1}]`;
}
}
}
return results;
}
// ============= PARSER FAQ SPÉCIALISÉ - ADAPTÉ =============
async function parseFAQPairsResponse(response, faqPairs) {
const results = {};
await logSh('Parsing réponse paires FAQ...', 'DEBUG');
// Parser avec regex pour capturer question + réponse
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
const parsedItems = {};
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = match[2].trim();
const cleanContent = content.replace(/^#+\s*/, '').replace(/<\/?[^>]+(>|$)/g, "");
parsedItems[tag] = cleanContent;
await logSh(`✓ Item FAQ parsé [${tag}]: "${cleanContent}"`, 'DEBUG');
}
// Mapper aux tags originaux avec |
Object.keys(parsedItems).forEach(cleanTag => {
const content = parsedItems[cleanTag];
results[`|${cleanTag}|`] = content;
});
// Vérification de cohérence paires
let pairsCompletes = 0;
for (const pair of faqPairs) {
const hasQuestion = results[pair.question.tag];
const hasAnswer = results[pair.answer.tag];
if (hasQuestion && hasAnswer) {
pairsCompletes++;
await logSh(`✓ Paire FAQ ${pair.number} complète: Q+R`, 'DEBUG');
} else {
await logSh(`⚠ Paire FAQ ${pair.number} incomplète: Q=${!!hasQuestion} R=${!!hasAnswer}`, 'WARNING');
}
}
await logSh(`${pairsCompletes}/${faqPairs.length} paires FAQ complètes`, 'INFO');
// FATAL si paires FAQ manquantes
if (pairsCompletes < faqPairs.length) {
const manquantes = faqPairs.length - pairsCompletes;
await logSh(`❌ FATAL: ${manquantes} paires FAQ manquantes sur ${faqPairs.length}`, 'ERROR');
throw new Error(`FATAL: Génération FAQ incomplète (${manquantes}/${faqPairs.length} manquantes) - arrêt du workflow`);
}
return results;
}
async function parseOtherElementsResponse(response, allOtherElements) {
const results = {};
await logSh('Parsing réponse autres éléments...', 'DEBUG');
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
while ((match = regex.exec(response)) !== null) {
const tag = match[1].trim();
const content = match[2].trim();
const cleanContent = content.replace(/^#+\s*/, '').replace(/<\/?[^>]+(>|$)/g, "");
results[`|${tag}|`] = cleanContent;
await logSh(`✓ Autre élément parsé [${tag}]: "${cleanContent}"`, 'DEBUG');
}
// Fallback si parsing partiel
if (Object.keys(results).length < allOtherElements.length) {
await logSh('Parsing autres éléments partiel, complétion fallback', 'WARNING');
const lines = response.split('\n')
.map(line => line.trim())
.filter(line => line.length > 0 && !line.startsWith('['));
allOtherElements.forEach((element, index) => {
if (!results[element.tag] && lines[index]) {
results[element.tag] = lines[index];
}
});
}
return results;
}
// ============= HELPER FUNCTIONS - ADAPTÉES =============
function createPromptForElement(element, csvData) {
// Cette fonction sera probablement définie dans content-generation.js
// Pour l'instant, retour basique
return `Génère du contenu pour ${element.type}: ${element.resolvedContent}`;
}
// 🔄 NODE.JS EXPORTS
module.exports = {
extractElements,
resolveVariablesContent,
getElementType,
generateAllContent,
parseElementStructure,
buildSmartHierarchy,
parseTitlesResponse,
parseTextsResponse,
parseFAQPairsResponse,
parseOtherElementsResponse,
createPromptForElement
};