seo-generator-server/lib/MissingKeywords.js
2025-09-03 15:29:19 +08:00

241 lines
7.6 KiB
JavaScript

// ========================================
// FICHIER: MissingKeywords.js - Version Node.js
// Description: Génération automatique des mots-clés manquants
// ========================================
const { logSh } = require('./ErrorReporting');
const { callLLM } = require('./LLMManager');
/**
* Génère automatiquement les mots-clés manquants pour les éléments non définis
* @param {Array} elements - Liste des éléments extraits
* @param {Object} csvData - Données CSV avec personnalité
* @returns {Object} Éléments mis à jour avec nouveaux mots-clés
*/
async function generateMissingKeywords(elements, csvData) {
logSh('>>> GÉNÉRATION MOTS-CLÉS MANQUANTS <<<', 'INFO');
// 1. IDENTIFIER tous les éléments manquants
const missingElements = [];
elements.forEach(element => {
if (element.resolvedContent.includes('non défini') ||
element.resolvedContent.includes('non résolu') ||
element.resolvedContent.trim() === '') {
missingElements.push({
tag: element.originalTag,
name: element.name,
type: element.type,
currentContent: element.resolvedContent,
context: getElementContext(element, elements, csvData)
});
}
});
if (missingElements.length === 0) {
logSh('Aucun mot-clé manquant détecté', 'INFO');
return {};
}
logSh(`${missingElements.length} mots-clés manquants détectés`, 'INFO');
// 2. ANALYSER le contexte global disponible
const contextAnalysis = analyzeAvailableContext(elements, csvData);
// 3. GÉNÉRER tous les manquants en UN SEUL appel IA
const generatedKeywords = await callOpenAIForMissingKeywords(missingElements, contextAnalysis, csvData);
// 4. METTRE À JOUR les éléments avec les nouveaux mots-clés
const updatedElements = updateElementsWithKeywords(elements, generatedKeywords);
logSh(`Mots-clés manquants générés: ${Object.keys(generatedKeywords).length}`, 'INFO');
return updatedElements;
}
/**
* Analyser le contexte disponible pour guider la génération
* @param {Array} elements - Tous les éléments
* @param {Object} csvData - Données CSV
* @returns {Object} Analyse contextuelle
*/
function analyzeAvailableContext(elements, csvData) {
const availableKeywords = [];
const availableContent = [];
// Récupérer tous les mots-clés/contenu déjà disponibles
elements.forEach(element => {
if (element.resolvedContent &&
!element.resolvedContent.includes('non défini') &&
!element.resolvedContent.includes('non résolu') &&
element.resolvedContent.trim() !== '') {
if (element.type.includes('titre')) {
availableKeywords.push(element.resolvedContent);
} else {
availableContent.push(element.resolvedContent.substring(0, 100));
}
}
});
return {
mainKeyword: csvData.mc0,
mainTitle: csvData.t0,
availableKeywords: availableKeywords,
availableContent: availableContent,
theme: csvData.mc0, // Thème principal
businessContext: "Autocollant.fr - signalétique personnalisée, plaques"
};
}
/**
* Obtenir le contexte spécifique d'un élément
* @param {Object} element - Élément à analyser
* @param {Array} allElements - Tous les éléments
* @param {Object} csvData - Données CSV
* @returns {Object} Contexte de l'élément
*/
function getElementContext(element, allElements, csvData) {
const context = {
elementType: element.type,
hierarchyLevel: element.name,
nearbyElements: []
};
// Trouver les éléments proches dans la hiérarchie
const elementParts = element.name.split('_');
if (elementParts.length >= 2) {
const baseLevel = elementParts.slice(0, 2).join('_'); // Ex: "Titre_H3"
allElements.forEach(otherElement => {
if (otherElement.name.startsWith(baseLevel) &&
otherElement.resolvedContent &&
!otherElement.resolvedContent.includes('non défini')) {
context.nearbyElements.push(otherElement.resolvedContent);
}
});
}
return context;
}
/**
* Appel IA pour générer tous les mots-clés manquants en un seul batch
* @param {Array} missingElements - Éléments manquants
* @param {Object} contextAnalysis - Analyse contextuelle
* @param {Object} csvData - Données CSV avec personnalité
* @returns {Object} Mots-clés générés
*/
async function callOpenAIForMissingKeywords(missingElements, contextAnalysis, csvData) {
const personality = csvData.personality;
let prompt = `Tu es ${personality.nom} (${personality.description}). Style: ${personality.style}
MISSION: GÉNÈRE ${missingElements.length} MOTS-CLÉS/EXPRESSIONS MANQUANTS pour ${contextAnalysis.mainKeyword}
CONTEXTE:
- Sujet: ${contextAnalysis.mainKeyword}
- Entreprise: Autocollant.fr (signalétique)
- Mots-clés existants: ${contextAnalysis.availableKeywords.slice(0, 3).join(', ')}
ÉLÉMENTS MANQUANTS:
`;
missingElements.forEach((missing, index) => {
prompt += `${index + 1}. [${missing.name}] → Mot-clé SEO\n`;
});
prompt += `\nCONSIGNES:
- Thème: ${contextAnalysis.mainKeyword}
- Mots-clés SEO naturels
- Varie les termes
- Évite répétitions
FORMAT:
[${missingElements[0].name}]
mot-clé
[${missingElements[1] ? missingElements[1].name : 'exemple'}]
mot-clé
etc...`;
try {
logSh('Génération mots-clés manquants...', 'DEBUG');
// Utilisation du LLM Manager avec fallback
const response = await callLLM('openai', prompt, {
temperature: 0.7,
maxTokens: 2000
}, personality);
// Parser la réponse
const generatedKeywords = parseMissingKeywordsResponse(response, missingElements);
return generatedKeywords;
} catch (error) {
logSh(`❌ FATAL: Génération mots-clés manquants échouée: ${error}`, 'ERROR');
throw new Error(`FATAL: Génération mots-clés LLM impossible - arrêt du workflow: ${error}`);
}
}
/**
* Parser la réponse IA pour extraire les mots-clés générés
* @param {string} response - Réponse de l'IA
* @param {Array} missingElements - Éléments manquants
* @returns {Object} Mots-clés parsés
*/
function parseMissingKeywordsResponse(response, missingElements) {
const results = {};
const regex = /\[([^\]]+)\]\s*\n([^[]*?)(?=\n\[|$)/gs;
let match;
while ((match = regex.exec(response)) !== null) {
const elementName = match[1].trim();
const generatedKeyword = match[2].trim();
results[elementName] = generatedKeyword;
logSh(`✓ Mot-clé généré [${elementName}]: "${generatedKeyword}"`, 'DEBUG');
}
// FATAL si parsing partiel
if (Object.keys(results).length < missingElements.length) {
const manquants = missingElements.length - Object.keys(results).length;
logSh(`❌ FATAL: Parsing mots-clés partiel - ${manquants}/${missingElements.length} manquants`, 'ERROR');
throw new Error(`FATAL: Parsing mots-clés incomplet (${manquants}/${missingElements.length} manquants) - arrêt du workflow`);
}
return results;
}
/**
* Mettre à jour les éléments avec les nouveaux mots-clés générés
* @param {Array} elements - Éléments originaux
* @param {Object} generatedKeywords - Nouveaux mots-clés
* @returns {Array} Éléments mis à jour
*/
function updateElementsWithKeywords(elements, generatedKeywords) {
const updatedElements = elements.map(element => {
const newKeyword = generatedKeywords[element.name];
if (newKeyword) {
return {
...element,
resolvedContent: newKeyword
};
}
return element;
});
logSh('Éléments mis à jour avec nouveaux mots-clés', 'INFO');
return updatedElements;
}
// Exports CommonJS
module.exports = {
generateMissingKeywords
};