seo-generator-server/lib/ConfigManager.js
StillHammer 471058f731 Add flexible pipeline system with per-module LLM configuration
- New modular pipeline architecture allowing custom workflow combinations
- Per-step LLM provider configuration (Claude, OpenAI, Gemini, Deepseek, Moonshot, Mistral)
- Visual pipeline builder and runner interfaces with drag-and-drop
- 10 predefined pipeline templates (minimal-test to originality-bypass)
- Pipeline CRUD operations via ConfigManager and REST API
- Fix variable resolution in instructions (HTML tags were breaking {{variables}})
- Fix hardcoded LLM providers in AdversarialCore
- Add TESTS_LLM_PROVIDER.md documentation with validation results
- Update dashboard to disable legacy config editor

API Endpoints:
- POST /api/pipeline/save, execute, validate, estimate
- GET /api/pipeline/list, modules, templates

Backward compatible with legacy modular workflow system.

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

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

361 lines
11 KiB
JavaScript

// ========================================
// FICHIER: ConfigManager.js
// RESPONSABILITÉ: Gestion CRUD des configurations modulaires et pipelines
// STOCKAGE: Fichiers JSON dans configs/ et configs/pipelines/
// ========================================
const fs = require('fs').promises;
const path = require('path');
const { logSh } = require('./ErrorReporting');
const { PipelineDefinition } = require('./pipeline/PipelineDefinition');
class ConfigManager {
constructor() {
this.configDir = path.join(__dirname, '../configs');
this.pipelinesDir = path.join(__dirname, '../configs/pipelines');
this.ensureConfigDir();
}
async ensureConfigDir() {
try {
await fs.mkdir(this.configDir, { recursive: true });
await fs.mkdir(this.pipelinesDir, { recursive: true });
logSh(`📁 Dossiers configs vérifiés: ${this.configDir}`, 'DEBUG');
} catch (error) {
logSh(`⚠️ Erreur création dossier configs: ${error.message}`, 'WARNING');
}
}
/**
* Sauvegarder une configuration
* @param {string} name - Nom de la configuration
* @param {object} config - Configuration modulaire
* @returns {object} - { success: true, name: sanitizedName }
*/
async saveConfig(name, config) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.configDir, `${sanitizedName}.json`);
const configData = {
name: sanitizedName,
displayName: name,
config,
createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString()
};
await fs.writeFile(filePath, JSON.stringify(configData, null, 2), 'utf-8');
logSh(`💾 Config sauvegardée: ${name}${sanitizedName}.json`, 'INFO');
return { success: true, name: sanitizedName };
}
/**
* Charger une configuration
* @param {string} name - Nom de la configuration
* @returns {object} - Configuration complète
*/
async loadConfig(name) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.configDir, `${sanitizedName}.json`);
try {
const data = await fs.readFile(filePath, 'utf-8');
const configData = JSON.parse(data);
logSh(`📂 Config chargée: ${name}`, 'DEBUG');
return configData;
} catch (error) {
logSh(`❌ Config non trouvée: ${name}`, 'ERROR');
throw new Error(`Configuration "${name}" non trouvée`);
}
}
/**
* Lister toutes les configurations
* @returns {array} - Liste des configurations avec métadonnées
*/
async listConfigs() {
try {
const files = await fs.readdir(this.configDir);
const jsonFiles = files.filter(f => f.endsWith('.json'));
const configs = await Promise.all(
jsonFiles.map(async (file) => {
const filePath = path.join(this.configDir, file);
const data = await fs.readFile(filePath, 'utf-8');
const configData = JSON.parse(data);
return {
name: configData.name,
displayName: configData.displayName || configData.name,
createdAt: configData.createdAt,
updatedAt: configData.updatedAt
};
})
);
// Trier par date de mise à jour (plus récent en premier)
return configs.sort((a, b) =>
new Date(b.updatedAt) - new Date(a.updatedAt)
);
} catch (error) {
logSh(`⚠️ Erreur listing configs: ${error.message}`, 'WARNING');
return [];
}
}
/**
* Supprimer une configuration
* @param {string} name - Nom de la configuration
* @returns {object} - { success: true }
*/
async deleteConfig(name) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.configDir, `${sanitizedName}.json`);
await fs.unlink(filePath);
logSh(`🗑️ Config supprimée: ${name}`, 'INFO');
return { success: true };
}
/**
* Vérifier si une configuration existe
* @param {string} name - Nom de la configuration
* @returns {boolean}
*/
async configExists(name) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.configDir, `${sanitizedName}.json`);
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
/**
* Mettre à jour une configuration existante
* @param {string} name - Nom de la configuration
* @param {object} config - Nouvelle configuration
* @returns {object} - { success: true, name: sanitizedName }
*/
async updateConfig(name, config) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.configDir, `${sanitizedName}.json`);
// Charger config existante pour garder createdAt
const existingData = await this.loadConfig(name);
const configData = {
name: sanitizedName,
displayName: name,
config,
createdAt: existingData.createdAt, // Garder date création
updatedAt: new Date().toISOString()
};
await fs.writeFile(filePath, JSON.stringify(configData, null, 2), 'utf-8');
logSh(`♻️ Config mise à jour: ${name}`, 'INFO');
return { success: true, name: sanitizedName };
}
// ========================================
// PIPELINE MANAGEMENT
// ========================================
/**
* Sauvegarder un pipeline
* @param {object} pipelineDefinition - Définition complète du pipeline
* @returns {object} - { success: true, name: sanitizedName }
*/
async savePipeline(pipelineDefinition) {
// Validation du pipeline
const validation = PipelineDefinition.validate(pipelineDefinition);
if (!validation.valid) {
throw new Error(`Pipeline invalide: ${validation.errors.join(', ')}`);
}
const sanitizedName = pipelineDefinition.name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`);
// Ajouter metadata de sauvegarde
const pipelineData = {
...pipelineDefinition,
metadata: {
...pipelineDefinition.metadata,
savedAt: new Date().toISOString()
}
};
await fs.writeFile(filePath, JSON.stringify(pipelineData, null, 2), 'utf-8');
logSh(`💾 Pipeline sauvegardé: ${pipelineDefinition.name}${sanitizedName}.json`, 'INFO');
return { success: true, name: sanitizedName };
}
/**
* Charger un pipeline
* @param {string} name - Nom du pipeline
* @returns {object} - Pipeline complet
*/
async loadPipeline(name) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`);
try {
const data = await fs.readFile(filePath, 'utf-8');
const pipeline = JSON.parse(data);
// Validation du pipeline chargé
const validation = PipelineDefinition.validate(pipeline);
if (!validation.valid) {
throw new Error(`Pipeline chargé invalide: ${validation.errors.join(', ')}`);
}
logSh(`📂 Pipeline chargé: ${name}`, 'DEBUG');
return pipeline;
} catch (error) {
logSh(`❌ Pipeline non trouvé: ${name}`, 'ERROR');
throw new Error(`Pipeline "${name}" non trouvé`);
}
}
/**
* Lister tous les pipelines
* @returns {array} - Liste des pipelines avec métadonnées
*/
async listPipelines() {
try {
const files = await fs.readdir(this.pipelinesDir);
const jsonFiles = files.filter(f => f.endsWith('.json'));
const pipelines = await Promise.all(
jsonFiles.map(async (file) => {
const filePath = path.join(this.pipelinesDir, file);
const data = await fs.readFile(filePath, 'utf-8');
const pipeline = JSON.parse(data);
// Obtenir résumé du pipeline
const summary = PipelineDefinition.getSummary(pipeline);
return {
name: pipeline.name,
description: pipeline.description,
steps: summary.totalSteps,
summary: summary.summary,
estimatedDuration: summary.duration.formatted,
tags: pipeline.metadata?.tags || [],
createdAt: pipeline.metadata?.created,
savedAt: pipeline.metadata?.savedAt
};
})
);
// Trier par date de sauvegarde (plus récent en premier)
return pipelines.sort((a, b) => {
const dateA = new Date(a.savedAt || a.createdAt || 0);
const dateB = new Date(b.savedAt || b.createdAt || 0);
return dateB - dateA;
});
} catch (error) {
logSh(`⚠️ Erreur listing pipelines: ${error.message}`, 'WARNING');
return [];
}
}
/**
* Supprimer un pipeline
* @param {string} name - Nom du pipeline
* @returns {object} - { success: true }
*/
async deletePipeline(name) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`);
await fs.unlink(filePath);
logSh(`🗑️ Pipeline supprimé: ${name}`, 'INFO');
return { success: true };
}
/**
* Vérifier si un pipeline existe
* @param {string} name - Nom du pipeline
* @returns {boolean}
*/
async pipelineExists(name) {
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`);
try {
await fs.access(filePath);
return true;
} catch {
return false;
}
}
/**
* Mettre à jour un pipeline existant
* @param {string} name - Nom du pipeline
* @param {object} pipelineDefinition - Nouvelle définition
* @returns {object} - { success: true, name: sanitizedName }
*/
async updatePipeline(name, pipelineDefinition) {
// Validation
const validation = PipelineDefinition.validate(pipelineDefinition);
if (!validation.valid) {
throw new Error(`Pipeline invalide: ${validation.errors.join(', ')}`);
}
const sanitizedName = name.replace(/[^a-zA-Z0-9-_]/g, '_');
const filePath = path.join(this.pipelinesDir, `${sanitizedName}.json`);
// Charger pipeline existant pour garder metadata originale
let existingMetadata = {};
try {
const existing = await this.loadPipeline(name);
existingMetadata = existing.metadata || {};
} catch {
// Pipeline n'existe pas encore, on continue
}
const pipelineData = {
...pipelineDefinition,
metadata: {
...existingMetadata,
...pipelineDefinition.metadata,
created: existingMetadata.created || pipelineDefinition.metadata?.created,
updated: new Date().toISOString(),
savedAt: new Date().toISOString()
}
};
await fs.writeFile(filePath, JSON.stringify(pipelineData, null, 2), 'utf-8');
logSh(`♻️ Pipeline mis à jour: ${name}`, 'INFO');
return { success: true, name: sanitizedName };
}
/**
* Cloner un pipeline
* @param {string} sourceName - Nom du pipeline source
* @param {string} newName - Nom du nouveau pipeline
* @returns {object} - { success: true, name: sanitizedName }
*/
async clonePipeline(sourceName, newName) {
const sourcePipeline = await this.loadPipeline(sourceName);
const clonedPipeline = PipelineDefinition.clone(sourcePipeline, newName);
return await this.savePipeline(clonedPipeline);
}
}
module.exports = { ConfigManager };