seo-generator-server/lib/batch/DigitalOceanTemplates.js
StillHammer b2fe9e0b7b Fix workflow production avec XML Digital Ocean et format Google Sheets
Corrections majeures:
- Digital Ocean: Récupération réelle XML depuis /wp-content/XML/ (86k chars au lieu de mock 1k)
- Nettoyage tags: Suppression <strong> dans extractElements() pour éviter parsing errors
- Doublons résilients: Tolérance doublons XML avec validation tags uniques
- Hiérarchie complète: StepExecutor génère 36 éléments depuis hierarchy.originalElement.name
- Format Google Sheets: Adaptation colonnes selon useVersionedSheet (17 legacy vs 21 versioned)
- Range Google Sheets: Force A1 avec INSERT_ROWS pour éviter décalage U:AO
- xmlTemplate optimisé: Exclusion du JSON metadata pour limite 50k chars

Résultat: 2151 mots, 36 éléments, sauvegarde correcte A-Q

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-10-08 14:52:19 +08:00

443 lines
14 KiB
JavaScript

// ========================================
// DIGITAL OCEAN TEMPLATES - RÉCUPÉRATION XML
// Responsabilité: Récupération et cache des templates XML depuis DigitalOcean Spaces
// ========================================
require('dotenv').config();
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const fs = require('fs').promises;
const path = require('path');
const axios = require('axios');
const AWS = require('aws-sdk');
/**
* DIGITAL OCEAN TEMPLATES MANAGER
* Gestion récupération, cache et fallback des templates XML
*/
class DigitalOceanTemplates {
constructor() {
this.cacheDir = path.join(__dirname, '../../cache/templates');
// Extraire bucket du endpoint si présent (ex: https://autocollant.fra1.digitaloceanspaces.com)
let endpoint = process.env.DO_ENDPOINT || process.env.DO_SPACES_ENDPOINT || 'https://fra1.digitaloceanspaces.com';
let bucket = process.env.DO_BUCKET_NAME || process.env.DO_SPACES_BUCKET || 'autocollant';
// Si endpoint contient le bucket, le retirer
if (endpoint.includes(`${bucket}.`)) {
endpoint = endpoint.replace(`${bucket}.`, '');
}
this.config = {
endpoint: endpoint,
bucket: bucket,
region: process.env.DO_REGION || process.env.DO_SPACES_REGION || 'fra1',
accessKey: process.env.DO_ACCESS_KEY_ID || process.env.DO_SPACES_KEY,
secretKey: process.env.DO_SECRET_ACCESS_KEY || process.env.DO_SPACES_SECRET,
timeout: 10000 // 10 secondes
};
// Cache en mémoire
this.memoryCache = new Map();
this.cacheExpiry = 5 * 60 * 1000; // 5 minutes
// Templates par défaut
this.defaultTemplates = {
'default.xml': this.getDefaultTemplate(),
'simple.xml': this.getSimpleTemplate(),
'advanced.xml': this.getAdvancedTemplate()
};
this.initializeTemplateManager();
}
/**
* Initialise le gestionnaire de templates
*/
async initializeTemplateManager() {
try {
// Créer le dossier cache
await fs.mkdir(this.cacheDir, { recursive: true });
// Vérifier la configuration DO
this.checkConfiguration();
logSh('🌊 DigitalOceanTemplates initialisé', 'DEBUG');
} catch (error) {
logSh(`❌ Erreur initialisation DigitalOceanTemplates: ${error.message}`, 'ERROR');
}
}
/**
* Vérifie la configuration Digital Ocean
*/
checkConfiguration() {
const hasCredentials = this.config.accessKey && this.config.secretKey;
if (!hasCredentials) {
logSh('⚠️ Credentials Digital Ocean manquantes, utilisation cache/fallback uniquement', 'WARNING');
} else {
logSh('✅ Configuration Digital Ocean OK', 'DEBUG');
}
return hasCredentials;
}
// ========================================
// RÉCUPÉRATION TEMPLATES
// ========================================
/**
* Récupère un template XML (avec cache et fallback)
*/
async getTemplate(filename) {
return tracer.run('DigitalOceanTemplates.getTemplate', async () => {
if (!filename) {
throw new Error('Nom de fichier template requis');
}
logSh(`📋 Récupération template: ${filename}`, 'DEBUG');
try {
// 1. Vérifier le cache mémoire
const memoryCached = this.getFromMemoryCache(filename);
if (memoryCached) {
logSh(`⚡ Template ${filename} trouvé en cache mémoire`, 'DEBUG');
return memoryCached;
}
// 2. Vérifier le cache fichier
const fileCached = await this.getFromFileCache(filename);
if (fileCached) {
logSh(`💾 Template ${filename} trouvé en cache fichier`, 'DEBUG');
this.setMemoryCache(filename, fileCached);
return fileCached;
}
// 3. Récupérer depuis Digital Ocean
if (this.checkConfiguration()) {
try {
const template = await this.fetchFromDigitalOcean(filename);
if (template) {
logSh(`🌊 Template ${filename} récupéré depuis Digital Ocean`, 'INFO');
// Sauvegarder en cache
await this.saveToFileCache(filename, template);
this.setMemoryCache(filename, template);
return template;
}
} catch (doError) {
logSh(`⚠️ Erreur Digital Ocean pour ${filename}: ${doError.message}`, 'WARNING');
}
}
// 4. Fallback sur template par défaut
const defaultTemplate = this.getDefaultTemplateForFile(filename);
logSh(`🔄 Utilisation template par défaut pour ${filename}`, 'WARNING');
return defaultTemplate;
} catch (error) {
logSh(`❌ Erreur récupération template ${filename}: ${error.message}`, 'ERROR');
// Fallback ultime
return this.getDefaultTemplate();
}
});
}
/**
* Récupère depuis Digital Ocean Spaces
*/
async fetchFromDigitalOcean(filename) {
return tracer.run('DigitalOceanTemplates.fetchFromDigitalOcean', async () => {
const fileKey = `wp-content/XML/${filename}`;
logSh(`🌊 Récupération DO avec authentification S3: ${fileKey}`, 'DEBUG');
try {
// Configuration S3 pour Digital Ocean Spaces
const s3 = new AWS.S3({
endpoint: this.config.endpoint,
accessKeyId: this.config.accessKey,
secretAccessKey: this.config.secretKey,
region: this.config.region,
s3ForcePathStyle: false,
signatureVersion: 'v4'
});
const params = {
Bucket: this.config.bucket,
Key: fileKey
};
logSh(`🔑 S3 getObject: bucket=${this.config.bucket}, key=${fileKey}`, 'DEBUG');
const data = await s3.getObject(params).promise();
const template = data.Body.toString('utf-8');
logSh(`✅ Template ${filename} récupéré depuis DO (${template.length} chars)`, 'INFO');
return template;
} catch (error) {
logSh(`❌ Digital Ocean S3 error: ${error.message} (code: ${error.code})`, 'WARNING');
throw error;
}
});
}
// ========================================
// GESTION CACHE
// ========================================
/**
* Récupère depuis le cache mémoire
*/
getFromMemoryCache(filename) {
const cached = this.memoryCache.get(filename);
if (cached && Date.now() - cached.timestamp < this.cacheExpiry) {
return cached.content;
}
if (cached) {
this.memoryCache.delete(filename);
}
return null;
}
/**
* Sauvegarde en cache mémoire
*/
setMemoryCache(filename, content) {
this.memoryCache.set(filename, {
content,
timestamp: Date.now()
});
}
/**
* Récupère depuis le cache fichier
*/
async getFromFileCache(filename) {
try {
const cachePath = path.join(this.cacheDir, filename);
const stats = await fs.stat(cachePath);
// Cache valide pendant 1 heure
const maxAge = 60 * 60 * 1000;
if (Date.now() - stats.mtime.getTime() < maxAge) {
const content = await fs.readFile(cachePath, 'utf8');
return content;
}
} catch (error) {
// Fichier cache n'existe pas ou erreur
}
return null;
}
/**
* Sauvegarde en cache fichier
*/
async saveToFileCache(filename, content) {
try {
const cachePath = path.join(this.cacheDir, filename);
await fs.writeFile(cachePath, content, 'utf8');
logSh(`💾 Template ${filename} sauvé en cache`, 'DEBUG');
} catch (error) {
logSh(`⚠️ Erreur sauvegarde cache ${filename}: ${error.message}`, 'WARNING');
}
}
// ========================================
// TEMPLATES PAR DÉFAUT
// ========================================
/**
* Retourne le template par défaut approprié
*/
getDefaultTemplateForFile(filename) {
const lowerFilename = filename.toLowerCase();
if (lowerFilename.includes('simple')) {
return this.defaultTemplates['simple.xml'];
} else if (lowerFilename.includes('advanced') || lowerFilename.includes('complet')) {
return this.defaultTemplates['advanced.xml'];
}
return this.defaultTemplates['default.xml'];
}
/**
* Template par défaut standard
*/
getDefaultTemplate() {
return `<?xml version='1.0' encoding='UTF-8'?>
<article>
<h1>|Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|</h1>
<intro>|Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases qui présente {{MC0}} et donne envie de lire la suite. Ton {{personality.style}}}|</intro>
<section>
<h2>|Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|</h2>
<p>|Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|</p>
</section>
<section>
<h2>|Titre_H2_2{{MC+1_2}}{Crée un titre H2 informatif sur {{MC+1_2}}. Style {{personality.style}}}|</h2>
<p>|Paragraphe_2{{MC+1_2}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_2}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|</p>
</section>
<conclusion>|Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|</conclusion>
</article>`;
}
/**
* Template simple
*/
getSimpleTemplate() {
return `<?xml version='1.0' encoding='UTF-8'?>
<article>
<h1>|Titre_H1{{T0}}{Titre principal pour {{MC0}}}|</h1>
<intro>|Introduction{{MC0}}{Introduction pour {{MC0}}}|</intro>
<content>|Contenu_Principal{{MC0}}{Contenu principal sur {{MC0}}}|</content>
<conclusion>|Conclusion{{MC0}}{Conclusion sur {{MC0}}}|</conclusion>
</article>`;
}
/**
* Template avancé
*/
getAdvancedTemplate() {
return `<?xml version='1.0' encoding='UTF-8'?>
<article>
<h1>|Titre_Principal{{T0}}{Rédige un titre H1 accrocheur de maximum 10 mots pour {{MC0}}. Style {{personality.style}}}|</h1>
<intro>|Introduction{{MC0}}{Rédige une introduction engageante de 2-3 phrases qui présente {{MC0}} et donne envie de lire la suite. Ton {{personality.style}}}|</intro>
<section>
<h2>|Titre_H2_1{{MC+1_1}}{Crée un titre H2 informatif sur {{MC+1_1}}. Style {{personality.style}}}|</h2>
<p>|Paragraphe_1{{MC+1_1}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_1}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|</p>
</section>
<section>
<h2>|Titre_H2_2{{MC+1_2}}{Crée un titre H2 informatif sur {{MC+1_2}}. Style {{personality.style}}}|</h2>
<p>|Paragraphe_2{{MC+1_2}}{Rédige un paragraphe détaillé de 4-5 phrases sur {{MC+1_2}}. Explique les avantages et caractéristiques. Ton {{personality.style}}}|</p>
</section>
<section>
<h2>|Titre_H2_3{{MC+1_3}}{Crée un titre H2 informatif sur {{MC+1_3}}. Style {{personality.style}}}|</h2>
<p>|Paragraphe_3{{MC+1_3}}{Explique en 4-5 phrases les avantages de {{MC+1_3}} pour {{MC0}}. Ton {{personality.style}}}|</p>
</section>
<faq>
<h2>|FAQ_Titre{Titre de section FAQ accrocheur sur {{MC0}}}|</h2>
<question>
<q>|Faq_q_1{{MC+1_1}}{Question fréquente sur {{MC+1_1}} et {{MC0}}}|</q>
<a>|Faq_a_1{{MC+1_1}}{Réponse claire et précise. 2-3 phrases. Ton {{personality.style}}}|</a>
</question>
<question>
<q>|Faq_q_2{{MC+1_2}}{Question pratique sur {{MC+1_2}} en lien avec {{MC0}}}|</q>
<a>|Faq_a_2{{MC+1_2}}{Réponse détaillée et utile. 2-3 phrases explicatives. Ton {{personality.style}}}|</a>
</question>
<question>
<q>|Faq_q_3{{MC+1_3}}{Question sur {{MC+1_3}} que se posent les clients}|</q>
<a>|Faq_a_3{{MC+1_3}}{Réponse complète qui rassure et informe. 2-3 phrases. Ton {{personality.style}}}|</a>
</question>
</faq>
<conclusion>|Conclusion{{MC0}}{Conclusion engageante de 2 phrases sur {{MC0}}. Appel à l'action subtil. Ton {{personality.style}}}|</conclusion>
</article>`;
}
// ========================================
// UTILITAIRES
// ========================================
/**
* Liste les templates disponibles
*/
async listAvailableTemplates() {
const templates = [];
// Templates par défaut
Object.keys(this.defaultTemplates).forEach(name => {
templates.push({
name,
source: 'default',
cached: true
});
});
// Templates en cache
try {
const cacheFiles = await fs.readdir(this.cacheDir);
cacheFiles.forEach(file => {
if (file.endsWith('.xml')) {
templates.push({
name: file,
source: 'cache',
cached: true
});
}
});
} catch (error) {
// Dossier cache n'existe pas
}
return templates;
}
/**
* Vide le cache
*/
async clearCache() {
try {
// Vider cache mémoire
this.memoryCache.clear();
// Vider cache fichier
const cacheFiles = await fs.readdir(this.cacheDir);
for (const file of cacheFiles) {
if (file.endsWith('.xml')) {
await fs.unlink(path.join(this.cacheDir, file));
}
}
logSh('🗑️ Cache templates vidé', 'INFO');
} catch (error) {
logSh(`❌ Erreur vidage cache: ${error.message}`, 'ERROR');
}
}
/**
* Retourne les statistiques du cache
*/
getCacheStats() {
return {
memoryCache: {
size: this.memoryCache.size,
expiry: this.cacheExpiry
},
config: {
hasCredentials: this.checkConfiguration(),
endpoint: this.config.endpoint,
bucket: this.config.bucket,
timeout: this.config.timeout
},
defaultTemplates: Object.keys(this.defaultTemplates).length
};
}
}
// ============= EXPORTS =============
module.exports = { DigitalOceanTemplates };