• 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>
429 lines
13 KiB
JavaScript
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 }; |