seo-generator-server/lib/batch/DigitalOceanTemplates.js
StillHammer a2ffe7fec5 Refactor batch processing system with shared QueueProcessor base class
• Created QueueProcessor base class for shared queue management, retry logic, and persistence
• Refactored BatchProcessor to extend QueueProcessor (385→142 lines, 63% reduction)
• Created BatchController with comprehensive API endpoints for batch operations
• Added Digital Ocean templates integration with caching
• Integrated batch endpoints into ManualServer with proper routing
• Fixed infinite recursion bug in queue status calculations
• Eliminated ~400 lines of duplicate code across processors
• Maintained backward compatibility with existing test interfaces

Architecture benefits:
- Single source of truth for queue processing logic
- Simplified maintenance and bug fixes
- Clear separation between AutoProcessor (production) and BatchProcessor (R&D)
- Extensible design for future processor types

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

Co-Authored-By: Claude <noreply@anthropic.com>
2025-09-19 02:04:48 +08:00

429 lines
13 KiB
JavaScript

// ========================================
// DIGITAL OCEAN TEMPLATES - RÉCUPÉRATION XML
// Responsabilité: Récupération et cache des templates XML depuis DigitalOcean Spaces
// ========================================
const { logSh } = require('../ErrorReporting');
const { tracer } = require('../trace');
const fs = require('fs').promises;
const path = require('path');
const axios = require('axios');
/**
* DIGITAL OCEAN TEMPLATES MANAGER
* Gestion récupération, cache et fallback des templates XML
*/
class DigitalOceanTemplates {
constructor() {
this.cacheDir = path.join(__dirname, '../../cache/templates');
this.config = {
endpoint: process.env.DO_SPACES_ENDPOINT || 'https://fra1.digitaloceanspaces.com',
bucket: process.env.DO_SPACES_BUCKET || 'autocollant',
region: process.env.DO_SPACES_REGION || 'fra1',
accessKey: process.env.DO_SPACES_KEY,
secretKey: 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', { filename }, 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', { filename }, async () => {
const url = `${this.config.endpoint}/${this.config.bucket}/templates/${filename}`;
logSh(`🌊 Récupération DO: ${url}`, 'DEBUG');
try {
// Utiliser une requête simple sans authentification S3 complexe
// Digital Ocean Spaces peut être configuré pour accès public aux templates
const response = await axios.get(url, {
timeout: this.config.timeout,
responseType: 'text',
headers: {
'Accept': 'application/xml, text/xml, text/plain'
}
});
if (response.status === 200 && response.data) {
logSh(`✅ Template ${filename} récupéré (${response.data.length} chars)`, 'DEBUG');
return response.data;
}
throw new Error(`Réponse invalide: ${response.status}`);
} catch (error) {
if (error.response) {
logSh(`❌ Digital Ocean error ${error.response.status}: ${error.response.statusText}`, 'WARNING');
} else {
logSh(`❌ Digital Ocean network error: ${error.message}`, '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 };