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>
443 lines
14 KiB
JavaScript
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 }; |