## 🎯 Nouveau système d'erreurs graduées (architecture SmartTouch) ### Architecture procédurale intelligente : - **3 niveaux de gravité** : Légère (50%) → Moyenne (30%) → Grave (10%) - **14 types d'erreurs** réalistes et subtiles - **Sélection procédurale** selon contexte (longueur, technique, heure) - **Distribution contrôlée** : max 1 grave, 2 moyennes, 3 légères par article ### 1. Erreurs GRAVES (10% articles max) : - Accord sujet-verbe : "ils sont" → "ils est" - Mot manquant : "pour garantir la qualité" → "pour garantir qualité" - Double mot : "pour garantir" → "pour pour garantir" - Négation oubliée : "n'est pas" → "est pas" ### 2. Erreurs MOYENNES (30% articles) : - Accord pluriel : "plaques résistantes" → "plaques résistant" - Virgule manquante : "Ainsi, il" → "Ainsi il" - Registre inapproprié : "Par conséquent" → "Du coup" - Préposition incorrecte : "résistant aux" → "résistant des" - Connecteur illogique : "cependant" → "donc" ### 3. Erreurs LÉGÈRES (50% articles) : - Double espace : "de votre" → "de votre" - Trait d'union : "c'est-à-dire" → "c'est à dire" - Espace ponctuation : "qualité ?" → "qualité?" - Majuscule : "Toutenplaque" → "toutenplaque" - Apostrophe droite : "l'article" → "l'article" ## ✅ Système anti-répétition complet : ### Corrections critiques : - **HumanSimulationTracker.js** : Tracker centralisé global - **Word boundaries (\b)** sur TOUS les regex → FIX "maison" → "néanmoinson" - **Protection 30+ expressions idiomatiques** françaises - **Anti-répétition** : max 2× même mot, jamais 2× même développement - **Diversification** : 48 variantes (hésitations, développements, connecteurs) ### Nouvelle structure (comme SmartTouch) : ``` lib/human-simulation/ ├── error-profiles/ (NOUVEAU) │ ├── ErrorProfiles.js (définitions + probabilités) │ ├── ErrorGrave.js (10% articles) │ ├── ErrorMoyenne.js (30% articles) │ ├── ErrorLegere.js (50% articles) │ └── ErrorSelector.js (sélection procédurale) ├── HumanSimulationCore.js (orchestrateur) ├── HumanSimulationTracker.js (anti-répétition) └── [autres modules] ``` ## 🔄 Remplace ancien système : - ❌ SpellingErrors.js (basique, répétitif, "et" → "." × 8) - ✅ error-profiles/ (gradué, procédural, intelligent, diversifié) ## 🎲 Fonctionnalités procédurales : - Analyse contexte : longueur texte, complexité technique, heure rédaction - Multiplicateurs adaptatifs selon contexte - Conditions application intelligentes - Tracking global par batch (respecte limites 10%/30%/50%) ## 📊 Résultats validation : Sur 100 articles → ~40-50 avec erreurs subtiles et diverses (plus de spam répétitif) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
1120 lines
30 KiB
JavaScript
1120 lines
30 KiB
JavaScript
/**
|
|
* Contrôleur API RESTful pour SEO Generator
|
|
* Centralise toute la logique API métier
|
|
*/
|
|
|
|
const { logSh } = require('./ErrorReporting');
|
|
const { handleFullWorkflow } = require('./Main');
|
|
const { getPersonalities, readInstructionsData } = require('./BrainConfig');
|
|
const { getStoredArticle, getRecentArticles } = require('./ArticleStorage');
|
|
const { DynamicPromptEngine } = require('./prompt-engine/DynamicPromptEngine');
|
|
const { TrendManager } = require('./trend-prompts/TrendManager');
|
|
const { WorkflowEngine } = require('./workflow-configuration/WorkflowEngine');
|
|
const { ValidatorCore } = require('./validation/ValidatorCore');
|
|
const fs = require('fs').promises;
|
|
const path = require('path');
|
|
|
|
class APIController {
|
|
constructor() {
|
|
this.articles = new Map(); // Cache articles en mémoire
|
|
this.projects = new Map(); // Cache projets
|
|
this.templates = new Map(); // Cache templates
|
|
|
|
// Initialize prompt engine components
|
|
this.promptEngine = new DynamicPromptEngine();
|
|
this.trendManager = new TrendManager();
|
|
this.workflowEngine = new WorkflowEngine();
|
|
|
|
// ✅ PHASE 3: Validation tracking
|
|
this.activeValidations = new Map(); // Track running validations
|
|
this.validationHistory = []; // Store completed validations
|
|
this.wsServer = null; // WebSocket server reference (injected by ManualServer)
|
|
}
|
|
|
|
/**
|
|
* ✅ PHASE 3: Injecte le serveur WebSocket pour broadcasting
|
|
*/
|
|
setWebSocketServer(wsServer) {
|
|
this.wsServer = wsServer;
|
|
logSh('📡 WebSocket server injecté dans APIController', 'DEBUG');
|
|
}
|
|
|
|
/**
|
|
* ✅ PHASE 3: Broadcast un message aux clients WebSocket
|
|
*/
|
|
broadcastToClients(data) {
|
|
if (!this.wsServer || !this.wsServer.clients) {
|
|
return;
|
|
}
|
|
|
|
const message = JSON.stringify(data);
|
|
let sent = 0;
|
|
|
|
this.wsServer.clients.forEach(client => {
|
|
if (client.readyState === 1) { // OPEN state
|
|
try {
|
|
client.send(message);
|
|
sent++;
|
|
} catch (error) {
|
|
logSh(`⚠️ Erreur envoi WebSocket: ${error.message}`, 'WARN');
|
|
}
|
|
}
|
|
});
|
|
|
|
if (sent > 0) {
|
|
logSh(`📡 Message broadcast à ${sent} clients`, 'TRACE');
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// GESTION ARTICLES
|
|
// ========================================
|
|
|
|
/**
|
|
* GET /api/articles - Liste tous les articles
|
|
*/
|
|
async getArticles(req, res) {
|
|
try {
|
|
const { limit = 50, offset = 0, project, status } = req.query;
|
|
|
|
logSh(`📋 Récupération articles: limit=${limit}, offset=${offset}`, 'DEBUG');
|
|
|
|
// Récupération depuis Google Sheets
|
|
const articles = await getRecentArticles(parseInt(limit));
|
|
|
|
// Filtrage optionnel
|
|
let filteredArticles = articles;
|
|
if (project) {
|
|
filteredArticles = articles.filter(a => a.project === project);
|
|
}
|
|
if (status) {
|
|
filteredArticles = filteredArticles.filter(a => a.status === status);
|
|
}
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
articles: filteredArticles.slice(offset, offset + limit),
|
|
total: filteredArticles.length,
|
|
limit: parseInt(limit),
|
|
offset: parseInt(offset)
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération articles: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération des articles',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/articles/:id - Récupère un article spécifique
|
|
*/
|
|
async getArticle(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
const { format = 'json' } = req.query || {};
|
|
|
|
logSh(`📄 Récupération article ID: ${id}`, 'DEBUG');
|
|
|
|
const article = await getStoredArticle(id);
|
|
|
|
if (!article) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Article non trouvé',
|
|
id
|
|
});
|
|
}
|
|
|
|
// Format de réponse
|
|
if (format === 'html') {
|
|
res.setHeader('Content-Type', 'text/html; charset=utf-8');
|
|
res.send(article.htmlContent || article.content);
|
|
} else if (format === 'text') {
|
|
res.setHeader('Content-Type', 'text/plain; charset=utf-8');
|
|
res.send(article.textContent || article.content);
|
|
} else {
|
|
res.json({
|
|
success: true,
|
|
data: article,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération article ${req.params.id}: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération de l\'article',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/articles - Créer un nouvel article
|
|
*/
|
|
async createArticle(req, res) {
|
|
try {
|
|
const {
|
|
keyword,
|
|
rowNumber,
|
|
project = 'api',
|
|
config = {},
|
|
template,
|
|
personalityPreference
|
|
} = req.body;
|
|
|
|
// Validation
|
|
if (!keyword && !rowNumber) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Mot-clé ou numéro de ligne requis'
|
|
});
|
|
}
|
|
|
|
logSh(`✨ Création article: ${keyword || `ligne ${rowNumber}`}`, 'INFO');
|
|
|
|
// Configuration par défaut
|
|
const workflowConfig = {
|
|
rowNumber: rowNumber || 2,
|
|
source: 'api',
|
|
project,
|
|
selectiveStack: config.selectiveStack || 'standardEnhancement',
|
|
adversarialMode: config.adversarialMode || 'light',
|
|
humanSimulationMode: config.humanSimulationMode || 'none',
|
|
patternBreakingMode: config.patternBreakingMode || 'none',
|
|
personalityPreference,
|
|
template,
|
|
...config
|
|
};
|
|
|
|
// Si mot-clé fourni, créer données temporaires
|
|
if (keyword && !rowNumber) {
|
|
workflowConfig.csvData = {
|
|
mc0: keyword,
|
|
t0: `Guide complet ${keyword}`,
|
|
personality: personalityPreference || { nom: 'Marc', style: 'professionnel' }
|
|
};
|
|
}
|
|
|
|
// Exécution du workflow
|
|
const result = await handleFullWorkflow(workflowConfig);
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: {
|
|
id: result.id || result.slug,
|
|
article: result,
|
|
config: workflowConfig
|
|
},
|
|
message: 'Article créé avec succès',
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur création article: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la création de l\'article',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// GESTION PROJETS
|
|
// ========================================
|
|
|
|
/**
|
|
* GET /api/projects - Liste tous les projets
|
|
*/
|
|
async getProjects(req, res) {
|
|
try {
|
|
const projects = Array.from(this.projects.values());
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
projects,
|
|
total: projects.length
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération projets: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération des projets',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/projects - Créer un nouveau projet
|
|
*/
|
|
async createProject(req, res) {
|
|
try {
|
|
// Validation body null/undefined
|
|
if (!req.body) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Corps de requête requis'
|
|
});
|
|
}
|
|
|
|
const { name, description, config = {} } = req.body;
|
|
|
|
if (!name) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Nom du projet requis'
|
|
});
|
|
}
|
|
|
|
const project = {
|
|
id: `project_${Date.now()}`,
|
|
name,
|
|
description,
|
|
config,
|
|
createdAt: new Date().toISOString(),
|
|
articlesCount: 0
|
|
};
|
|
|
|
this.projects.set(project.id, project);
|
|
|
|
logSh(`📁 Projet créé: ${name}`, 'INFO');
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: project,
|
|
message: 'Projet créé avec succès'
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur création projet: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la création du projet',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// GESTION TEMPLATES
|
|
// ========================================
|
|
|
|
/**
|
|
* GET /api/templates - Liste tous les templates
|
|
*/
|
|
async getTemplates(req, res) {
|
|
try {
|
|
const templates = Array.from(this.templates.values());
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
templates,
|
|
total: templates.length
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération templates: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération des templates',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/templates - Créer un nouveau template
|
|
*/
|
|
async createTemplate(req, res) {
|
|
try {
|
|
const { name, content, description, category = 'custom' } = req.body;
|
|
|
|
if (!name || !content) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Nom et contenu du template requis'
|
|
});
|
|
}
|
|
|
|
const template = {
|
|
id: `template_${Date.now()}`,
|
|
name,
|
|
content,
|
|
description,
|
|
category,
|
|
createdAt: new Date().toISOString()
|
|
};
|
|
|
|
this.templates.set(template.id, template);
|
|
|
|
logSh(`📋 Template créé: ${name}`, 'INFO');
|
|
|
|
res.status(201).json({
|
|
success: true,
|
|
data: template,
|
|
message: 'Template créé avec succès'
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur création template: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la création du template',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// CONFIGURATION & MONITORING
|
|
// ========================================
|
|
|
|
/**
|
|
* GET /api/config/personalities - Configuration personnalités
|
|
*/
|
|
async getPersonalitiesConfig(req, res) {
|
|
try {
|
|
const personalities = await getPersonalities();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
personalities,
|
|
total: personalities.length
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur config personnalités: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération des personnalités',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/health - Health check
|
|
*/
|
|
async getHealth(req, res) {
|
|
try {
|
|
const health = {
|
|
status: 'healthy',
|
|
timestamp: new Date().toISOString(),
|
|
version: '1.0.0',
|
|
uptime: process.uptime(),
|
|
memory: process.memoryUsage(),
|
|
environment: process.env.NODE_ENV || 'development'
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
data: health
|
|
});
|
|
|
|
} catch (error) {
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Health check failed',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/metrics - Métriques système
|
|
*/
|
|
async getMetrics(req, res) {
|
|
try {
|
|
const metrics = {
|
|
articles: {
|
|
total: this.articles.size,
|
|
recent: Array.from(this.articles.values()).filter(
|
|
a => new Date(a.createdAt) > new Date(Date.now() - 24 * 60 * 60 * 1000)
|
|
).length
|
|
},
|
|
projects: {
|
|
total: this.projects.size
|
|
},
|
|
templates: {
|
|
total: this.templates.size
|
|
},
|
|
system: {
|
|
uptime: process.uptime(),
|
|
memory: process.memoryUsage(),
|
|
platform: process.platform,
|
|
nodeVersion: process.version
|
|
}
|
|
};
|
|
|
|
res.json({
|
|
success: true,
|
|
data: metrics,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur métriques: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération des métriques',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// PROMPT ENGINE API
|
|
// ========================================
|
|
|
|
/**
|
|
* POST /api/generate-prompt - Génère un prompt adaptatif
|
|
*/
|
|
async generatePrompt(req, res) {
|
|
try {
|
|
const {
|
|
templateType = 'technical',
|
|
content = {},
|
|
csvData = null,
|
|
trend = null,
|
|
layerConfig = {},
|
|
customVariables = {}
|
|
} = req.body;
|
|
|
|
logSh(`🧠 Génération prompt: template=${templateType}, trend=${trend}`, 'INFO');
|
|
|
|
// Apply trend if specified
|
|
if (trend) {
|
|
await this.trendManager.setTrend(trend);
|
|
}
|
|
|
|
// Generate adaptive prompt
|
|
const result = await this.promptEngine.generateAdaptivePrompt({
|
|
templateType,
|
|
content,
|
|
csvData,
|
|
trend: this.trendManager.getCurrentTrend(),
|
|
layerConfig,
|
|
customVariables
|
|
});
|
|
|
|
res.json({
|
|
success: true,
|
|
prompt: result.prompt,
|
|
metadata: result.metadata,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
logSh(`✅ Prompt généré: ${result.prompt.length} caractères`, 'DEBUG');
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur génération prompt: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la génération du prompt',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/trends - Liste toutes les tendances disponibles
|
|
*/
|
|
async getTrends(req, res) {
|
|
try {
|
|
const trends = this.trendManager.getAvailableTrends();
|
|
const currentTrend = this.trendManager.getCurrentTrend();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
trends,
|
|
currentTrend,
|
|
total: trends.length
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération tendances: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération des tendances',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/trends/:trendId - Applique une tendance
|
|
*/
|
|
async setTrend(req, res) {
|
|
try {
|
|
const { trendId } = req.params;
|
|
const { customConfig = null } = req.body;
|
|
|
|
logSh(`🎯 Application tendance: ${trendId}`, 'INFO');
|
|
|
|
const trend = await this.trendManager.setTrend(trendId, customConfig);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
trend,
|
|
applied: true
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur application tendance ${req.params.trendId}: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de l\'application de la tendance',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/prompt-engine/status - Status du moteur de prompts
|
|
*/
|
|
async getPromptEngineStatus(req, res) {
|
|
try {
|
|
const engineStatus = this.promptEngine.getEngineStatus();
|
|
const trendStatus = this.trendManager.getStatus();
|
|
const workflowStatus = this.workflowEngine.getEngineStatus();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
engine: engineStatus,
|
|
trends: trendStatus,
|
|
workflow: workflowStatus,
|
|
health: 'operational'
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur status prompt engine: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération du status',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/workflow/sequences - Liste toutes les séquences de workflow
|
|
*/
|
|
async getWorkflowSequences(req, res) {
|
|
try {
|
|
const sequences = this.workflowEngine.getAvailableSequences();
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
sequences,
|
|
total: sequences.length
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération séquences workflow: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération des séquences workflow',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/workflow/sequences - Crée une séquence de workflow personnalisée
|
|
*/
|
|
async createWorkflowSequence(req, res) {
|
|
try {
|
|
const { name, sequence } = req.body;
|
|
|
|
if (!name || !sequence) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Nom et séquence requis'
|
|
});
|
|
}
|
|
|
|
if (!this.workflowEngine.validateSequence(sequence)) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Séquence invalide'
|
|
});
|
|
}
|
|
|
|
const createdSequence = this.workflowEngine.createCustomSequence(name, sequence);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
name,
|
|
sequence: createdSequence
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur création séquence workflow: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la création de la séquence workflow',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/workflow/execute - Exécute un workflow configurable
|
|
*/
|
|
async executeConfigurableWorkflow(req, res) {
|
|
try {
|
|
const {
|
|
content,
|
|
sequenceName = 'default',
|
|
customSequence = null,
|
|
selectiveConfig = {},
|
|
adversarialConfig = {},
|
|
humanConfig = {},
|
|
patternConfig = {},
|
|
csvData = {},
|
|
personalities = {}
|
|
} = req.body;
|
|
|
|
if (!content || typeof content !== 'object') {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Contenu requis (objet)'
|
|
});
|
|
}
|
|
|
|
logSh(`🔄 Exécution workflow configurable: ${customSequence ? 'custom' : sequenceName}`, 'INFO');
|
|
|
|
const result = await this.workflowEngine.executeConfigurableWorkflow(content, {
|
|
sequenceName,
|
|
customSequence,
|
|
selectiveConfig,
|
|
adversarialConfig,
|
|
humanConfig,
|
|
patternConfig,
|
|
csvData,
|
|
personalities
|
|
});
|
|
|
|
res.json({
|
|
success: result.success,
|
|
data: {
|
|
content: result.content,
|
|
stats: result.stats
|
|
},
|
|
error: result.error || null,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur exécution workflow configurable: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de l\'exécution du workflow configurable',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
// ========================================
|
|
// VALIDATION API (PHASE 3)
|
|
// ========================================
|
|
|
|
/**
|
|
* POST /api/validation/start - Démarre une nouvelle validation
|
|
*/
|
|
async startValidation(req, res) {
|
|
try {
|
|
const {
|
|
pipelineConfig,
|
|
rowNumber = 2,
|
|
config = {}
|
|
} = req.body;
|
|
|
|
// Validation
|
|
if (!pipelineConfig) {
|
|
return res.status(400).json({
|
|
success: false,
|
|
error: 'Configuration pipeline requise'
|
|
});
|
|
}
|
|
|
|
logSh(`🚀 Démarrage validation: ${pipelineConfig.name || 'Sans nom'}`, 'INFO');
|
|
|
|
// ✅ PHASE 3: Créer nouvelle instance ValidatorCore avec broadcast callback
|
|
const validator = new ValidatorCore({
|
|
broadcastCallback: (data) => this.broadcastToClients(data)
|
|
});
|
|
|
|
// Démarrer validation en arrière-plan
|
|
const validationPromise = validator.runValidation(config, pipelineConfig, rowNumber);
|
|
|
|
// Stocker la validation active
|
|
this.activeValidations.set(validator.validationId, {
|
|
validator,
|
|
promise: validationPromise,
|
|
startTime: Date.now(),
|
|
pipelineConfig,
|
|
rowNumber,
|
|
status: 'running'
|
|
});
|
|
|
|
// Gérer la completion en arrière-plan
|
|
validationPromise.then(result => {
|
|
const validation = this.activeValidations.get(validator.validationId);
|
|
if (validation) {
|
|
validation.status = result.success ? 'completed' : 'error';
|
|
validation.result = result;
|
|
validation.endTime = Date.now();
|
|
|
|
// Déplacer vers historique après 5min
|
|
setTimeout(() => {
|
|
this.validationHistory.push(validation);
|
|
this.activeValidations.delete(validator.validationId);
|
|
}, 5 * 60 * 1000);
|
|
}
|
|
}).catch(error => {
|
|
const validation = this.activeValidations.get(validator.validationId);
|
|
if (validation) {
|
|
validation.status = 'error';
|
|
validation.error = error.message;
|
|
validation.endTime = Date.now();
|
|
}
|
|
});
|
|
|
|
// Répondre immédiatement avec validation ID
|
|
res.status(202).json({
|
|
success: true,
|
|
data: {
|
|
validationId: validator.validationId,
|
|
status: 'running',
|
|
message: 'Validation démarrée en arrière-plan'
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur démarrage validation: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors du démarrage de la validation',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/validation/status/:id - Récupère le statut d'une validation
|
|
*/
|
|
async getValidationStatus(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
logSh(`📊 Récupération statut validation: ${id}`, 'DEBUG');
|
|
|
|
// Chercher dans validations actives
|
|
const activeValidation = this.activeValidations.get(id);
|
|
if (activeValidation) {
|
|
const status = activeValidation.validator.getStatus();
|
|
|
|
return res.json({
|
|
success: true,
|
|
data: {
|
|
validationId: id,
|
|
status: activeValidation.status,
|
|
progress: status.progress,
|
|
startTime: activeValidation.startTime,
|
|
duration: Date.now() - activeValidation.startTime,
|
|
pipelineName: activeValidation.pipelineConfig.name,
|
|
result: activeValidation.result || null
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Chercher dans historique
|
|
const historicalValidation = this.validationHistory.find(v => v.validator.validationId === id);
|
|
if (historicalValidation) {
|
|
return res.json({
|
|
success: true,
|
|
data: {
|
|
validationId: id,
|
|
status: historicalValidation.status,
|
|
startTime: historicalValidation.startTime,
|
|
endTime: historicalValidation.endTime,
|
|
duration: historicalValidation.endTime - historicalValidation.startTime,
|
|
pipelineName: historicalValidation.pipelineConfig.name,
|
|
result: historicalValidation.result
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
}
|
|
|
|
// Non trouvé
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Validation non trouvée',
|
|
validationId: id
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération statut validation: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération du statut',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* POST /api/validation/stop/:id - Arrête une validation en cours
|
|
*/
|
|
async stopValidation(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
logSh(`🛑 Arrêt validation: ${id}`, 'INFO');
|
|
|
|
const validation = this.activeValidations.get(id);
|
|
if (!validation) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Validation non trouvée ou déjà terminée',
|
|
validationId: id
|
|
});
|
|
}
|
|
|
|
// Note: Pour l'instant, on ne peut pas vraiment interrompre une validation en cours
|
|
// On marque juste le statut comme "stopped"
|
|
validation.status = 'stopped';
|
|
validation.endTime = Date.now();
|
|
|
|
logSh(`⚠️ Validation ${id} marquée comme arrêtée (le processus continue en arrière-plan)`, 'WARN');
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
validationId: id,
|
|
status: 'stopped',
|
|
message: 'Validation marquée comme arrêtée'
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur arrêt validation: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de l\'arrêt de la validation',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/validation/list - Liste toutes les validations
|
|
*/
|
|
async listValidations(req, res) {
|
|
try {
|
|
const { status, limit = 50 } = req.query;
|
|
|
|
logSh(`📋 Récupération liste validations`, 'DEBUG');
|
|
|
|
// Collecter validations actives
|
|
const activeList = Array.from(this.activeValidations.values()).map(v => ({
|
|
validationId: v.validator.validationId,
|
|
status: v.status,
|
|
startTime: v.startTime,
|
|
duration: Date.now() - v.startTime,
|
|
pipelineName: v.pipelineConfig.name,
|
|
progress: v.validator.getStatus().progress
|
|
}));
|
|
|
|
// Collecter historique
|
|
const historyList = this.validationHistory.map(v => ({
|
|
validationId: v.validator.validationId,
|
|
status: v.status,
|
|
startTime: v.startTime,
|
|
endTime: v.endTime,
|
|
duration: v.endTime - v.startTime,
|
|
pipelineName: v.pipelineConfig.name
|
|
}));
|
|
|
|
// Combiner et filtrer
|
|
let allValidations = [...activeList, ...historyList];
|
|
if (status) {
|
|
allValidations = allValidations.filter(v => v.status === status);
|
|
}
|
|
|
|
// Limiter résultats
|
|
const limitedValidations = allValidations.slice(0, parseInt(limit));
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
validations: limitedValidations,
|
|
total: allValidations.length,
|
|
active: activeList.length,
|
|
historical: historyList.length
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur liste validations: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération de la liste',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/validation/:id/report - Récupère le rapport complet d'une validation
|
|
*/
|
|
async getValidationReport(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
logSh(`📊 Récupération rapport validation: ${id}`, 'DEBUG');
|
|
|
|
// Chercher le dossier de validation
|
|
const validationDir = path.join(process.cwd(), 'validations', id);
|
|
const reportPath = path.join(validationDir, 'report.json');
|
|
|
|
try {
|
|
const reportContent = await fs.readFile(reportPath, 'utf8');
|
|
const report = JSON.parse(reportContent);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: report,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (fileError) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Rapport de validation non trouvé',
|
|
validationId: id,
|
|
message: fileError.message
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération rapport: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération du rapport',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* GET /api/validation/:id/evaluations - Récupère les évaluations détaillées
|
|
*/
|
|
async getValidationEvaluations(req, res) {
|
|
try {
|
|
const { id } = req.params;
|
|
|
|
logSh(`📊 Récupération évaluations validation: ${id}`, 'DEBUG');
|
|
|
|
const validationDir = path.join(process.cwd(), 'validations', id);
|
|
const evaluationsPath = path.join(validationDir, 'results', 'evaluations.json');
|
|
|
|
try {
|
|
const evaluationsContent = await fs.readFile(evaluationsPath, 'utf8');
|
|
const evaluations = JSON.parse(evaluationsContent);
|
|
|
|
res.json({
|
|
success: true,
|
|
data: evaluations,
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (fileError) {
|
|
return res.status(404).json({
|
|
success: false,
|
|
error: 'Évaluations non trouvées',
|
|
validationId: id,
|
|
message: fileError.message
|
|
});
|
|
}
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération évaluations: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération des évaluations',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
|
|
/**
|
|
* ✅ NOUVEAU: GET /api/validation/presets - Récupère les presets disponibles
|
|
*/
|
|
async getValidationPresets(req, res) {
|
|
try {
|
|
const { VALIDATION_PRESETS } = require('./validation/ValidatorCore');
|
|
|
|
logSh(`📋 Récupération presets validation`, 'DEBUG');
|
|
|
|
res.json({
|
|
success: true,
|
|
data: {
|
|
presets: VALIDATION_PRESETS,
|
|
count: Object.keys(VALIDATION_PRESETS).length
|
|
},
|
|
timestamp: new Date().toISOString()
|
|
});
|
|
|
|
} catch (error) {
|
|
logSh(`❌ Erreur récupération presets: ${error.message}`, 'ERROR');
|
|
res.status(500).json({
|
|
success: false,
|
|
error: 'Erreur lors de la récupération des presets',
|
|
message: error.message
|
|
});
|
|
}
|
|
}
|
|
}
|
|
|
|
module.exports = { APIController }; |