refactor: Synchronisation complète du codebase - Application de tous les patches

Application systématique et méthodique de tous les patches historiques.

##  FICHIERS SYNCHRONISÉS (19 fichiers)

### Core & Infrastructure:
- server.js (14 patches) - Lazy loading ModeManager, SIGINT hard kill, timing logs
- ModeManager.js (4 patches) - Instrumentation complète avec timing détaillé

### Pipeline System:
- PipelineDefinition.js (6 patches) - Source unique getLLMProvidersList()
- pipeline-builder.js (8 patches) - Standardisation LLM providers
- pipeline-runner.js (6 patches) - Affichage résultats structurés + debug console
- pipeline-builder.html (2 patches) - Fallback providers synchronisés
- pipeline-runner.html (3 patches) - UI améliorée résultats

### Enhancement Layers:
- TechnicalLayer.js (1 patch) - defaultLLM: 'gpt-4o-mini'
- StyleLayer.js (1 patch) - Type safety vocabulairePref
- PatternBreakingCore.js (1 patch) - Mapping modifications
- PatternBreakingLayers.js (1 patch) - LLM standardisé

### Validators & Tests:
- QualityMetrics.js (1 patch) - callLLM('gpt-4o-mini')
- PersonalityValidator.js (1 patch) - Provider gpt-4o-mini
- AntiDetectionValidator.js - Synchronisé

### Documentation:
- TODO.md (1 patch) - Section LiteLLM pour tracking coûts
- CLAUDE.md - Documentation à jour

### Tools:
- tools/analyze-skipped-exports.js (nouveau)
- tools/apply-claude-exports.js (nouveau)
- tools/apply-claude-exports-fuzzy.js (nouveau)

## 🎯 Changements principaux:
-  Standardisation LLM providers (openai → gpt-4o-mini, claude → claude-sonnet-4-5)
-  Lazy loading optimisé (ModeManager chargé à la demande)
-  SIGINT immediate exit (pas de graceful shutdown)
-  Type safety renforcé (conversions string explicites)
-  Instrumentation timing complète
-  Debug logging amélioré (console.log résultats pipeline)

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

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
StillHammer 2025-10-12 20:36:17 +08:00
parent cd79ca9a4a
commit 64fb319e65
32 changed files with 1938 additions and 291 deletions

View File

@ -339,7 +339,7 @@ Voir `API.md` pour documentation complète avec exemples.
**Assets**: `public/` (11 web interfaces), `configs/` (saved configs/pipelines), `tools/` (logViewer, bundler, audit), `tests/` (comprehensive test suite), `.env` (credentials)
## Dependencies & Workflow Sources
**Deps**: googleapis, axios, dotenv, express, nodemailer
**Deps**: google-spreadsheet, google-auth-library, axios, dotenv, express, nodemailer
**Sources**: production (Google Sheets), test_random_personality, node_server
## Git Push Configuration

62
TODO.md
View File

@ -62,7 +62,67 @@ async function enhanceWithPersonalityRecovery(content, personality, attempt = 1)
---
## 📋 PRIORITÉ 3 - AUTRES AMÉLIORATIONS
## 📋 PRIORITÉ 3 - INTÉGRATION LITELLM POUR TRACKING COÛTS
### PROBLÈME ACTUEL
- Impossible de récupérer les crédits restants via les APIs des providers (OpenAI, Anthropic, etc.)
- OpenAI a supprimé l'endpoint `/v1/dashboard/billing/credit_grants` pour les soldes USD
- Anthropic n'a aucune API pour la balance (feature request ouverte depuis longtemps)
- Pas de visibilité centralisée sur les coûts multi-providers
### SOLUTION REQUISE
**Intégrer LiteLLM comme proxy pour tracking automatique des coûts**
#### Pourquoi LiteLLM :
- ✅ **Standard de l'industrie** : Utilisé par la majorité des projets multi-LLM
- ✅ **Support 100+ LLMs** : OpenAI, Anthropic, Google, Deepseek, Moonshot, Mistral, etc.
- ✅ **Tracking automatique** : Intercepte tous les appels et calcule les coûts
- ✅ **Dashboard unifié** : Vue centralisée par user/team/API key
- ✅ **API de métriques** : Récupération programmatique des stats
#### Implémentation suggérée :
```bash
# Installation
pip install litellm[proxy]
# Démarrage proxy
litellm --config litellm_config.yaml
```
```yaml
# litellm_config.yaml
model_list:
- model_name: gpt-5
litellm_params:
model: openai/gpt-5
api_key: ${OPENAI_API_KEY}
- model_name: claude-sonnet-4-5
litellm_params:
model: anthropic/claude-sonnet-4-5-20250929
api_key: ${ANTHROPIC_API_KEY}
# ... autres models
```
#### Changements dans notre code :
1. **LLMManager.js** : Router tous les appels via LiteLLM proxy (localhost:8000)
2. **LLM Monitoring** : Récupérer les stats via l'API LiteLLM
3. **Dashboard** : Afficher "Dépensé ce mois" au lieu de "Crédits restants"
#### Alternatives évaluées :
- **Langfuse** : Bien mais moins de models supportés
- **Portkey** : Commercial, pas open source
- **Helicone** : Plus basique
- **Tracking maison** : Trop de maintenance, risque d'erreurs de calcul
#### Avantages supplémentaires :
- 🔄 **Load balancing** : Rotation automatique entre plusieurs clés API
- 📊 **Analytics** : Métriques détaillées par endpoint/user/model
- 🚨 **Alertes** : Notifications quand budget dépassé
- 💾 **Caching** : Cache intelligent pour réduire les coûts
---
## 📋 PRIORITÉ 4 - AUTRES AMÉLIORATIONS
### A. Monitoring des échecs IA
- **Logging détaillé** : Quel LLM échoue, quand, pourquoi

View File

@ -87,32 +87,36 @@ async function getBrainConfig(data) {
async function readInstructionsData(rowNumber = 2) {
try {
logSh(`📊 Lecture Google Sheet ligne ${rowNumber}...`, 'INFO');
// NOUVEAU : Lecture directe depuis Google Sheets
const { google } = require('googleapis');
// Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problème TLS
// ⚡ OPTIMISÉ : google-spreadsheet (18x plus rapide que googleapis)
const { GoogleSpreadsheet } = require('google-spreadsheet');
const { JWT } = require('google-auth-library');
const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
const auth = new google.auth.GoogleAuth({
const serviceAccountAuth = new JWT({
keyFile: keyFilePath,
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
});
logSh('🔑 Utilisation fichier JSON pour contourner problème TLS OAuth', 'INFO');
const sheets = google.sheets({ version: 'v4', auth });
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
// Récupérer la ligne spécifique (A à I au minimum)
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_ID,
range: `Instructions!A${rowNumber}:I${rowNumber}` // Ligne spécifique A-I
});
if (!response.data.values || response.data.values.length === 0) {
const doc = new GoogleSpreadsheet(SHEET_ID, serviceAccountAuth);
await doc.loadInfo();
const sheet = doc.sheetsByTitle['instructions'];
if (!sheet) {
throw new Error('Onglet "instructions" non trouvé dans Google Sheet');
}
const rows = await sheet.getRows();
const targetRow = rows[rowNumber - 2]; // -2 car index 0 = ligne 2 du sheet
if (!targetRow) {
throw new Error(`Ligne ${rowNumber} non trouvée dans Google Sheet`);
}
const row = response.data.values[0];
// ✅ Même format que googleapis : tableau de valeurs
const row = targetRow._rawData;
logSh(`✅ Ligne ${rowNumber} récupérée: ${row.length} colonnes`, 'INFO');
const xmlTemplateValue = row[8] || '';
@ -166,33 +170,38 @@ async function readInstructionsData(rowNumber = 2) {
async function getPersonalities() {
try {
logSh('📊 Lecture personnalités depuis Google Sheet (onglet Personnalites)...', 'INFO');
// Configuration auth Google Sheets - FORCE utilisation fichier JSON pour éviter problème TLS
const { google } = require('googleapis');
// ⚡ OPTIMISÉ : google-spreadsheet (18x plus rapide que googleapis)
const { GoogleSpreadsheet } = require('google-spreadsheet');
const { JWT } = require('google-auth-library');
const keyFilePath = path.join(__dirname, '..', 'seo-generator-470715-85d4a971c1af.json');
const auth = new google.auth.GoogleAuth({
const serviceAccountAuth = new JWT({
keyFile: keyFilePath,
scopes: ['https://www.googleapis.com/auth/spreadsheets.readonly']
});
logSh('🔑 Utilisation fichier JSON pour contourner problème TLS OAuth (personnalités)', 'INFO');
const sheets = google.sheets({ version: 'v4', auth });
const SHEET_ID = process.env.GOOGLE_SHEETS_ID || '1iA2GvWeUxX-vpnAMfVm3ZMG9LhaC070SdGssEcXAh2c';
// Récupérer toutes les personnalités (après la ligne d'en-tête)
const response = await sheets.spreadsheets.values.get({
spreadsheetId: SHEET_ID,
range: 'Personnalites!A2:O' // Colonnes A à O pour inclure les nouvelles colonnes IA
});
if (!response.data.values || response.data.values.length === 0) {
const doc = new GoogleSpreadsheet(SHEET_ID, serviceAccountAuth);
await doc.loadInfo();
const sheet = doc.sheetsByTitle['Personnalites'];
if (!sheet) {
throw new Error('Onglet "Personnalites" non trouvé dans Google Sheet');
}
const rows = await sheet.getRows();
if (!rows || rows.length === 0) {
throw new Error('Aucune personnalité trouvée dans l\'onglet Personnalites');
}
const personalities = [];
// Traiter chaque ligne de personnalité
response.data.values.forEach((row, index) => {
// Traiter chaque ligne de personnalité (✅ même logique qu'avant)
rows.forEach((rowObj, index) => {
const row = rowObj._rawData; // ✅ Même format tableau que googleapis
if (row[0] && row[0].toString().trim() !== '') { // Si nom existe (colonne A)
const personality = {
nom: row[0]?.toString().trim() || '',

View File

@ -5,6 +5,7 @@
// 🔄 NODE.JS IMPORTS
const { logSh } = require('./ErrorReporting');
const { logElementsList } = require('./selective-enhancement/SelectiveUtils');
// ============= EXTRACTION PRINCIPALE =============
@ -140,8 +141,12 @@ async function extractElements(xmlTemplate, csvData) {
await logSh(`Tag séparé: ${pureTag} → "${resolvedContent}"`, 'DEBUG');
}
await logSh(`${elements.length} éléments extraits avec séparation`, 'INFO');
// 📊 DÉTAIL DES ÉLÉMENTS EXTRAITS
logElementsList(elements, 'ÉLÉMENTS EXTRAITS (depuis XML + Google Sheets)');
return elements;
} catch (error) {
@ -228,15 +233,12 @@ async function generateAllContent(elements, csvData, xmlTemplate) {
try {
await logSh(`Élément ${index + 1}/${elements.length}: ${element.name}`, 'DEBUG');
const prompt = createPromptForElement(element, csvData);
await logSh(`Prompt créé: ${prompt}`, 'DEBUG');
// 🔄 NODE.JS : Import callOpenAI depuis LLM manager
// 🔄 NODE.JS : Import callOpenAI depuis LLM manager (le prompt/réponse seront loggés par LLMManager)
const { callLLM } = require('./LLMManager');
const content = await callLLM('openai', prompt, {}, csvData.personality);
await logSh(`Contenu reçu: ${content}`, 'DEBUG');
const content = await callLLM('gpt-4o-mini', prompt, {}, csvData.personality);
generatedContent[element.originalTag] = content;
@ -277,12 +279,21 @@ function parseElementStructure(element) {
// ============= HIÉRARCHIE INTELLIGENTE - ADAPTÉE =============
async function buildSmartHierarchy(elements) {
await logSh(`🏗️ CONSTRUCTION HIÉRARCHIE - Début avec ${elements.length} éléments`, 'INFO');
const hierarchy = {};
elements.forEach(element => {
elements.forEach((element, index) => {
const structure = parseElementStructure(element);
const path = structure.hierarchyPath;
// 📊 LOG: Détailler chaque élément traité
logSh(` [${index + 1}/${elements.length}] ${element.name}`, 'DEBUG');
logSh(` 📍 Path: ${path}`, 'DEBUG');
logSh(` 📝 Type: ${structure.type}`, 'DEBUG');
logSh(` 📄 ResolvedContent: "${element.resolvedContent}"`, 'DEBUG');
logSh(` 📜 Instructions: "${element.instructions ? element.instructions.substring(0, 80) : 'AUCUNE'}"`, 'DEBUG');
if (!hierarchy[path]) {
hierarchy[path] = {
title: null,
@ -291,26 +302,44 @@ async function buildSmartHierarchy(elements) {
children: {}
};
}
// Associer intelligemment
if (structure.type === 'Titre') {
hierarchy[path].title = structure; // Tout l'objet avec variables + instructions
logSh(` ✅ Assigné comme TITRE dans hiérarchie[${path}].title`, 'DEBUG');
} else if (structure.type === 'Txt') {
hierarchy[path].text = structure;
logSh(` ✅ Assigné comme TEXTE dans hiérarchie[${path}].text`, 'DEBUG');
} else if (structure.type === 'Intro') {
hierarchy[path].text = structure;
hierarchy[path].text = structure;
logSh(` ✅ Assigné comme INTRO dans hiérarchie[${path}].text`, 'DEBUG');
} else if (structure.type === 'Faq') {
hierarchy[path].questions.push(structure);
logSh(` ✅ Ajouté comme FAQ dans hiérarchie[${path}].questions`, 'DEBUG');
}
});
// ← LIGNE COMPILÉE
// 📊 LOG: Résumé de la hiérarchie construite
const mappingSummary = Object.keys(hierarchy).map(path => {
const section = hierarchy[path];
return `${path}:[T:${section.title ? '✓' : '✗'} Txt:${section.text ? '✓' : '✗'} FAQ:${section.questions.length}]`;
}).join(' | ');
await logSh('Correspondances: ' + mappingSummary, 'DEBUG');
await logSh(`📊 HIÉRARCHIE CONSTRUITE: ${Object.keys(hierarchy).length} sections`, 'INFO');
await logSh(` ${mappingSummary}`, 'INFO');
// 📊 LOG: Détail complet d'une section exemple
const firstPath = Object.keys(hierarchy)[0];
if (firstPath) {
const firstSection = hierarchy[firstPath];
await logSh(`📋 EXEMPLE SECTION [${firstPath}]:`, 'DEBUG');
if (firstSection.title) {
await logSh(` 📌 Title.instructions: "${firstSection.title.instructions ? firstSection.title.instructions.substring(0, 100) : 'AUCUNE'}"`, 'DEBUG');
}
if (firstSection.text) {
await logSh(` 📌 Text.instructions: "${firstSection.text.instructions ? firstSection.text.instructions.substring(0, 100) : 'AUCUNE'}"`, 'DEBUG');
}
}
return hierarchy;
}

View File

@ -25,47 +25,48 @@ const timestamp = now.toISOString().slice(0, 10) + '_' +
now.toLocaleTimeString('fr-FR').replace(/:/g, '-');
const logFile = path.join(__dirname, '..', 'logs', `seo-generator-${timestamp}.log`);
// File destination with dated filename - JSON format
const fileDest = pino.destination({
dest: logFile,
mkdir: true,
sync: false,
minLength: 0
});
// Console destination - Pretty format
const prettyStream = pretty({
colorize: true,
translateTime: 'HH:MM:ss.l',
ignore: 'pid,hostname',
destination: 1 // stdout
});
const tee = new PassThrough();
// Lazy loading des pipes console (évite blocage à l'import)
let consolePipeInitialized = false;
// File destination with dated filename - FORCE DEBUG LEVEL
const fileDest = pino.destination({
dest: logFile,
mkdir: true,
sync: false,
minLength: 0 // Force immediate write even for small logs
});
tee.pipe(fileDest);
// Custom levels for Pino to include TRACE, PROMPT, and LLM
// Custom levels for Pino
const customLevels = {
trace: 5, // Below debug (10)
trace: 5,
debug: 10,
info: 20,
prompt: 25, // New level for prompts (between info and warn)
llm: 26, // New level for LLM interactions (between prompt and warn)
prompt: 25,
llm: 26,
warn: 30,
error: 40,
fatal: 50
};
// Pino logger instance with enhanced configuration and custom levels
// ✅ Multistream: pretty sur console + JSON dans fichier (pas de duplication)
const logger = pino(
{
level: 'debug', // FORCE DEBUG LEVEL for file logging
{
level: 'debug',
base: undefined,
timestamp: pino.stdTimeFunctions.isoTime,
customLevels: customLevels,
useOnlyCustomLevels: true
useOnlyCustomLevels: true,
browser: { disabled: true }
},
tee
pino.multistream([
{ level: 'debug', stream: prettyStream }, // Console: pretty format
{ level: 'debug', stream: fileDest } // Fichier: JSON format
])
);
// Initialize WebSocket server (only when explicitly requested)
@ -155,13 +156,7 @@ async function logSh(message, level = 'INFO') {
if (!wsServer) {
initWebSocketServer();
}
// Initialize console pipe if needed (lazy loading)
if (!consolePipeInitialized && process.env.ENABLE_CONSOLE_LOG === 'true') {
tee.pipe(prettyStream).pipe(process.stdout);
consolePipeInitialized = true;
}
// Convert level to lowercase for Pino
const pinoLevel = level.toLowerCase();

View File

@ -11,25 +11,82 @@ const { logSh } = require('./ErrorReporting');
require('dotenv').config();
// ============= CONFIGURATION CENTRALISÉE =============
// IDs basés sur les MODÈLES (pas les providers) pour garantir la reproductibilité
const LLM_CONFIG = {
openai: {
// OpenAI Models - GPT-5 Series (August 2025)
'gpt-5': {
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
endpoint: 'https://api.openai.com/v1/chat/completions',
model: 'gpt-4o-mini',
model: 'gpt-5',
displayName: 'GPT-5',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
maxTokens: 16000, // GPT-5 utilise reasoning tokens (reasoning_effort=minimal forcé)
timeout: 300000,
retries: 3
},
claude: {
'gpt-5-mini': {
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
endpoint: 'https://api.openai.com/v1/chat/completions',
model: 'gpt-5-mini',
displayName: 'GPT-5 Mini',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
maxTokens: 8000, // GPT-5-mini utilise reasoning tokens (reasoning_effort=minimal forcé)
timeout: 300000,
retries: 3
},
'gpt-5-nano': {
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
endpoint: 'https://api.openai.com/v1/chat/completions',
model: 'gpt-5-nano',
displayName: 'GPT-5 Nano',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
maxTokens: 4000, // GPT-5-nano utilise reasoning tokens (reasoning_effort=minimal forcé)
timeout: 300000,
retries: 3
},
// OpenAI Models - GPT-4o Series
'gpt-4o-mini': {
provider: 'openai',
apiKey: process.env.OPENAI_API_KEY,
endpoint: 'https://api.openai.com/v1/chat/completions',
model: 'gpt-4o-mini',
displayName: 'GPT-4o Mini',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
maxTokens: 6000, // Augmenté pour contraintes de longueur
timeout: 300000,
retries: 3
},
// Claude Models
'claude-sonnet-4-5': {
provider: 'anthropic',
apiKey: process.env.ANTHROPIC_API_KEY,
endpoint: 'https://api.anthropic.com/v1/messages',
model: 'claude-sonnet-4-20250514',
model: 'claude-sonnet-4-5-20250929',
displayName: 'Claude Sonnet 4.5',
headers: {
'x-api-key': '{API_KEY}',
'Content-Type': 'application/json',
@ -37,54 +94,78 @@ const LLM_CONFIG = {
},
temperature: 0.7,
maxTokens: 6000,
timeout: 300000, // 5 minutes
timeout: 300000,
retries: 6
},
deepseek: {
// Google Models
'gemini-pro': {
provider: 'google',
apiKey: process.env.GOOGLE_API_KEY,
endpoint: 'https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent',
model: 'gemini-pro',
displayName: 'Google Gemini Pro',
headers: {
'Content-Type': 'application/json'
},
temperature: 0.7,
maxTokens: 6000, // Augmenté pour contraintes de longueur
timeout: 300000,
retries: 3
},
// Deepseek Models
'deepseek-chat': {
provider: 'deepseek',
apiKey: process.env.DEEPSEEK_API_KEY,
endpoint: 'https://api.deepseek.com/v1/chat/completions',
model: 'deepseek-chat',
displayName: 'Deepseek Chat',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
maxTokens: 6000, // Augmenté pour contraintes de longueur
timeout: 300000,
retries: 3
},
moonshot: {
// Moonshot Models
'moonshot-v1-32k': {
provider: 'moonshot',
apiKey: process.env.MOONSHOT_API_KEY,
endpoint: 'https://api.moonshot.ai/v1/chat/completions',
model: 'moonshot-v1-32k',
displayName: 'Moonshot v1 32K',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
temperature: 0.7,
timeout: 300000, // 5 minutes
maxTokens: 6000, // Augmenté pour contraintes de longueur
timeout: 300000,
retries: 3
},
mistral: {
// Mistral Models
'mistral-small': {
provider: 'mistral',
apiKey: process.env.MISTRAL_API_KEY,
endpoint: 'https://api.mistral.ai/v1/chat/completions',
model: 'mistral-small-latest',
displayName: 'Mistral Small',
headers: {
'Authorization': 'Bearer {API_KEY}',
'Content-Type': 'application/json'
},
max_tokens: 5000,
temperature: 0.7,
timeout: 300000, // 5 minutes
maxTokens: 5000,
timeout: 300000,
retries: 3
}
};
// Alias pour compatibilité avec le code existant
LLM_CONFIG.gpt4 = LLM_CONFIG.openai;
// ============= HELPER FUNCTIONS =============
const sleep = (ms) => new Promise(resolve => setTimeout(resolve, ms));
@ -114,29 +195,23 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
throw new Error(`Clé API manquante pour ${llmProvider}`);
}
logSh(`🤖 Appel LLM: ${llmProvider.toUpperCase()} (${config.model}) | Personnalité: ${personality?.nom || 'aucune'}`, 'DEBUG');
// 📢 AFFICHAGE PROMPT COMPLET POUR DEBUG AVEC INFO IA
logSh(`\n🔍 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
// 📤 LOG PROMPT (une seule fois)
logSh(`\n📤 ===== PROMPT ENVOYÉ À ${llmProvider.toUpperCase()} (${config.model}) | PERSONNALITÉ: ${personality?.nom || 'AUCUNE'} =====`, 'PROMPT');
logSh(prompt, 'PROMPT');
// 📤 LOG LLM REQUEST COMPLET
logSh(`📤 LLM REQUEST [${llmProvider.toUpperCase()}] (${config.model}) | Personnalité: ${personality?.nom || 'AUCUNE'}`, 'LLM');
logSh(prompt, 'LLM');
// Préparer la requête selon le provider
const requestData = buildRequestData(llmProvider, prompt, options, personality);
// Effectuer l'appel avec retry logic
const response = await callWithRetry(llmProvider, requestData, config);
// Parser la réponse selon le format du provider
const content = parseResponse(llmProvider, response);
// 📥 LOG LLM RESPONSE COMPLET
logSh(`📥 LLM RESPONSE [${llmProvider.toUpperCase()}] (${config.model}) | Durée: ${Date.now() - startTime}ms`, 'LLM');
// 📥 LOG RESPONSE
logSh(`\n📥 ===== RÉPONSE REÇUE DE ${llmProvider.toUpperCase()} (${config.model}) | Durée: ${Date.now() - startTime}ms =====`, 'LLM');
logSh(content, 'LLM');
const duration = Date.now() - startTime;
logSh(`${llmProvider.toUpperCase()} (${personality?.nom || 'sans personnalité'}) réponse en ${duration}ms`, 'INFO');
@ -158,34 +233,59 @@ async function callLLM(llmProvider, prompt, options = {}, personality = null) {
// ============= CONSTRUCTION DES REQUÊTES =============
function buildRequestData(provider, prompt, options, personality) {
const config = LLM_CONFIG[provider];
function buildRequestData(modelId, prompt, options, personality) {
const config = LLM_CONFIG[modelId];
const temperature = options.temperature || config.temperature;
const maxTokens = options.maxTokens || config.maxTokens;
let maxTokens = options.maxTokens || config.maxTokens;
// GPT-5: Force minimum tokens (reasoning tokens + content tokens)
if (modelId.startsWith('gpt-5')) {
const MIN_GPT5_TOKENS = 1500; // Minimum pour reasoning + contenu
if (maxTokens < MIN_GPT5_TOKENS) {
logSh(` ⚠️ GPT-5: maxTokens augmenté de ${maxTokens} à ${MIN_GPT5_TOKENS} (minimum pour reasoning)`, 'WARNING');
maxTokens = MIN_GPT5_TOKENS;
}
}
// Construire le système prompt si personnalité fournie
const systemPrompt = personality ?
`Tu es ${personality.nom}. ${personality.description}. Style: ${personality.style}` :
const systemPrompt = personality ?
`Tu es ${personality.nom}. ${personality.description}. Style: ${personality.style}` :
'Tu es un assistant expert.';
switch (provider) {
// Switch sur le PROVIDER (pas le modelId)
switch (config.provider) {
case 'openai':
case 'gpt4':
case 'deepseek':
case 'moonshot':
case 'mistral':
return {
// GPT-5 models use max_completion_tokens instead of max_tokens
const tokenField = modelId.startsWith('gpt-5') ? 'max_completion_tokens' : 'max_tokens';
// GPT-5 models only support temperature: 1 (default)
const requestBody = {
model: config.model,
messages: [
{ role: 'system', content: systemPrompt },
{ role: 'user', content: prompt }
],
max_tokens: maxTokens,
temperature: temperature,
[tokenField]: maxTokens,
stream: false
};
case 'claude':
// Only add temperature if NOT GPT-5 (GPT-5 only supports default temperature=1)
if (!modelId.startsWith('gpt-5')) {
requestBody.temperature = temperature;
}
// GPT-5: Force minimal reasoning effort to reduce reasoning tokens
if (modelId.startsWith('gpt-5')) {
requestBody.reasoning_effort = 'minimal';
logSh(` 🧠 GPT-5: reasoning_effort=minimal, max_completion_tokens=${maxTokens}`, 'DEBUG');
}
return requestBody;
case 'anthropic':
return {
model: config.model,
max_tokens: maxTokens,
@ -195,10 +295,21 @@ function buildRequestData(provider, prompt, options, personality) {
{ role: 'user', content: prompt }
]
};
case 'google':
// Format spécifique Gemini
return {
contents: [{
parts: [{ text: systemPrompt + '\n\n' + prompt }]
}],
generationConfig: {
temperature: temperature,
maxOutputTokens: maxTokens
}
};
default:
throw new Error(`Format de requête non supporté pour ${provider}`);
throw new Error(`Format de requête non supporté pour provider ${config.provider}`);
}
}
@ -258,26 +369,30 @@ async function callWithRetry(provider, requestData, config) {
// ============= PARSING DES RÉPONSES =============
function parseResponse(provider, responseData) {
function parseResponse(modelId, responseData) {
const config = LLM_CONFIG[modelId];
try {
switch (provider) {
switch (config.provider) {
case 'openai':
case 'gpt4':
case 'deepseek':
case 'moonshot':
case 'mistral':
return responseData.choices[0].message.content.trim();
case 'claude':
case 'anthropic':
return responseData.content[0].text.trim();
case 'google':
return responseData.candidates[0].content.parts[0].text.trim();
default:
throw new Error(`Parser non supporté pour ${provider}`);
throw new Error(`Parser non supporté pour provider ${config.provider}`);
}
} catch (error) {
logSh(`❌ Erreur parsing ${provider}: ${error.toString()}`, 'ERROR');
logSh(`❌ Erreur parsing ${modelId} (${config.provider}): ${error.toString()}`, 'ERROR');
logSh(`Response brute: ${JSON.stringify(responseData)}`, 'DEBUG');
throw new Error(`Impossible de parser la réponse ${provider}: ${error.toString()}`);
throw new Error(`Impossible de parser la réponse ${modelId}: ${error.toString()}`);
}
}
@ -285,6 +400,12 @@ function parseResponse(provider, responseData) {
async function recordUsageStats(provider, promptTokens, responseTokens, duration, error = null) {
try {
// Vérifier que le provider existe dans la config
if (!LLM_CONFIG[provider]) {
logSh(`⚠ Stats: Provider inconnu "${provider}", skip stats`, 'DEBUG');
return;
}
// TODO: Adapter selon votre système de stockage Node.js
// Peut être une base de données, un fichier, MongoDB, etc.
const statsData = {
@ -296,12 +417,12 @@ async function recordUsageStats(provider, promptTokens, responseTokens, duration
duration: duration,
error: error || ''
};
// Exemple: log vers console ou fichier
logSh(`📊 Stats: ${JSON.stringify(statsData)}`, 'DEBUG');
// TODO: Implémenter sauvegarde réelle (DB, fichier, etc.)
} catch (statsError) {
// Ne pas faire planter le workflow si les stats échouent
logSh(`⚠ Erreur enregistrement stats: ${statsError.toString()}`, 'WARNING');
@ -351,17 +472,37 @@ async function testAllLLMs() {
*/
function getAvailableProviders() {
const available = [];
Object.keys(LLM_CONFIG).forEach(provider => {
const config = LLM_CONFIG[provider];
if (config.apiKey && !config.apiKey.startsWith('VOTRE_CLE_')) {
available.push(provider);
}
});
return available;
}
/**
* Obtenir la liste complète des providers pour UI/validation
* @returns {Array} Liste des providers avec id, name, model
*/
function getLLMProvidersList() {
const providers = [];
Object.entries(LLM_CONFIG).forEach(([id, config]) => {
providers.push({
id: id,
name: config.displayName,
model: config.model,
provider: config.provider,
default: id === 'claude-sonnet-4-5' // Claude Sonnet 4.5 par défaut
});
});
return providers;
}
/**
* Obtenir des statistiques d'usage par provider
*/
@ -383,7 +524,7 @@ async function getUsageStats() {
* Maintient la même signature pour ne pas casser votre code existant
*/
async function callOpenAI(prompt, personality) {
return await callLLM('openai', prompt, {}, personality);
return await callLLM('gpt-4o-mini', prompt, {}, personality);
}
// ============= EXPORTS POUR TESTS =============
@ -420,7 +561,7 @@ async function testLLMManager() {
// Test spécifique OpenAI (compatibilité avec ancien code)
try {
logSh('🎯 Test spécifique OpenAI (compatibilité)...', 'DEBUG');
const response = await callLLM('openai', 'Dis juste "Test OK"');
const response = await callLLM('gpt-4o-mini', 'Dis juste "Test OK"');
logSh('✅ Test OpenAI compatibilité: ' + response, 'INFO');
} catch (error) {
logSh('❌ Test OpenAI compatibilité échoué: ' + error.toString(), 'ERROR');
@ -537,6 +678,7 @@ module.exports = {
callOpenAI,
testAllLLMs,
getAvailableProviders,
getLLMProvidersList,
getUsageStats,
testLLMManager,
testLLMManagerComplete,

View File

@ -810,7 +810,7 @@ async function handleModularWorkflow(config = {}) {
* BENCHMARK COMPARATIF STACKS
*/
async function benchmarkStacks(rowNumber = 2) {
console.log('\n⚡ === BENCHMARK STACKS MODULAIRES ===\n');
logSh('\n⚡ === BENCHMARK STACKS MODULAIRES ===\n', 'INFO');
const stacks = getAvailableStacks();
const adversarialModes = ['none', 'light', 'standard'];
@ -1005,10 +1005,14 @@ module.exports = {
logSh(`🎨 Détection pipeline flexible: ${data.pipelineConfig.name}`, 'INFO');
const executor = new PipelineExecutor();
const result = await executor.execute(
data.pipelineConfig,
data.rowNumber || 2,
{ stopOnError: data.stopOnError }
{
stopOnError: data.stopOnError,
saveIntermediateSteps: data.saveIntermediateSteps || false // ✅ Passer saveIntermediateSteps
}
);
// Formater résultat pour compatibilité
@ -1021,7 +1025,8 @@ module.exports = {
personality: result.metadata.personality,
pipelineName: result.metadata.pipelineName,
totalSteps: result.metadata.totalSteps,
successfulSteps: result.metadata.successfulSteps
successfulSteps: result.metadata.successfulSteps,
versionHistory: result.versionHistory // ✅ Inclure versionHistory
}
};
}

View File

@ -4,6 +4,7 @@
// ========================================
const { logSh } = require('./ErrorReporting');
const { validateElement, hasUnresolvedPlaceholders } = require('./ValidationGuards');
/**
* EXECUTEUR D'ÉTAPES MODULAIRES
@ -96,25 +97,88 @@ class StepExecutor {
* Construire la structure de contenu depuis la hiérarchie réelle
*/
buildContentStructureFromHierarchy(inputData, hierarchy) {
logSh(`🏗️ BUILD CONTENT STRUCTURE - Début`, 'INFO');
logSh(` 📊 Input: mc0="${inputData.mc0}"`, 'DEBUG');
logSh(` 📊 Hiérarchie: ${hierarchy ? Object.keys(hierarchy).length : 0} sections`, 'DEBUG');
const contentStructure = {};
// Si hiérarchie disponible, l'utiliser
if (hierarchy && Object.keys(hierarchy).length > 0) {
logSh(`🔍 Hiérarchie debug: ${Object.keys(hierarchy).length} sections`, 'DEBUG');
logSh(`🔍 Hiérarchie reçue: ${Object.keys(hierarchy).length} sections`, 'INFO');
logSh(`🔍 Première section sample: ${JSON.stringify(Object.values(hierarchy)[0]).substring(0, 200)}`, 'DEBUG');
Object.entries(hierarchy).forEach(([path, section]) => {
let validationErrors = 0;
let elementCount = 0;
Object.entries(hierarchy).forEach(([path, section], sectionIndex) => {
logSh(`📂 SECTION [${sectionIndex + 1}/${Object.keys(hierarchy).length}] path="${path}"`, 'DEBUG');
// Générer pour le titre si présent
if (section.title && section.title.originalElement) {
elementCount++;
// ✅ SOLUTION D: Validation guard avant utilisation
try {
validateElement(section.title.originalElement, {
strict: true,
checkInstructions: true,
context: `StepExecutor buildContent - path: ${path} (title)`
});
} catch (validationError) {
validationErrors++;
logSh(`⚠️ Validation échouée pour titre [${section.title.originalElement.name}]: ${validationError.message}`, 'WARNING');
// Ne pas bloquer, utiliser fallback
}
const tag = section.title.originalElement.name;
const instruction = section.title.instructions || section.title.originalElement.instructions || `Rédige un titre pour ${inputData.mc0}`;
// 📊 LOG: Détailler l'instruction extraite
logSh(` 📌 TITRE [${elementCount}] tag="${tag}"`, 'DEBUG');
logSh(` 🔹 section.title.instructions: "${section.title.instructions ? section.title.instructions.substring(0, 80) : 'NULL'}"`, 'DEBUG');
logSh(` 🔹 section.title.originalElement.instructions: "${section.title.originalElement.instructions ? section.title.originalElement.instructions.substring(0, 80) : 'NULL'}"`, 'DEBUG');
logSh(` ➡️ INSTRUCTION FINALE: "${instruction.substring(0, 100)}"`, 'INFO');
// ✅ Double-vérification des instructions avant ajout
const instructionCheck = hasUnresolvedPlaceholders(instruction);
if (instructionCheck.hasIssues) {
logSh(`⚠️ Instruction pour [${tag}] contient des placeholders: ${instructionCheck.placeholders.join(', ')}`, 'WARNING');
}
contentStructure[tag] = instruction;
}
// Générer pour le texte si présent
if (section.text && section.text.originalElement) {
elementCount++;
// ✅ SOLUTION D: Validation guard
try {
validateElement(section.text.originalElement, {
strict: true,
checkInstructions: true,
context: `StepExecutor buildContent - path: ${path} (text)`
});
} catch (validationError) {
validationErrors++;
logSh(`⚠️ Validation échouée pour texte [${section.text.originalElement.name}]: ${validationError.message}`, 'WARNING');
}
const tag = section.text.originalElement.name;
const instruction = section.text.instructions || section.text.originalElement.instructions || `Rédige du contenu sur ${inputData.mc0}`;
// 📊 LOG: Détailler l'instruction extraite
logSh(` 📌 TEXTE [${elementCount}] tag="${tag}"`, 'DEBUG');
logSh(` 🔹 section.text.instructions: "${section.text.instructions ? section.text.instructions.substring(0, 80) : 'NULL'}"`, 'DEBUG');
logSh(` 🔹 section.text.originalElement.instructions: "${section.text.originalElement.instructions ? section.text.originalElement.instructions.substring(0, 80) : 'NULL'}"`, 'DEBUG');
logSh(` ➡️ INSTRUCTION FINALE: "${instruction.substring(0, 100)}"`, 'INFO');
const instructionCheck = hasUnresolvedPlaceholders(instruction);
if (instructionCheck.hasIssues) {
logSh(`⚠️ Instruction pour [${tag}] contient des placeholders: ${instructionCheck.placeholders.join(', ')}`, 'WARNING');
}
contentStructure[tag] = instruction;
}
@ -122,15 +186,45 @@ class StepExecutor {
if (section.questions && section.questions.length > 0) {
section.questions.forEach(q => {
if (q.originalElement) {
// ✅ SOLUTION D: Validation guard
try {
validateElement(q.originalElement, {
strict: true,
checkInstructions: true,
context: `StepExecutor buildContent - path: ${path} (question)`
});
} catch (validationError) {
validationErrors++;
logSh(`⚠️ Validation échouée pour question [${q.originalElement.name}]: ${validationError.message}`, 'WARNING');
}
const tag = q.originalElement.name;
const instruction = q.instructions || q.originalElement.instructions || `Rédige une question/réponse FAQ sur ${inputData.mc0}`;
const instructionCheck = hasUnresolvedPlaceholders(instruction);
if (instructionCheck.hasIssues) {
logSh(`⚠️ Instruction pour [${tag}] contient des placeholders: ${instructionCheck.placeholders.join(', ')}`, 'WARNING');
}
contentStructure[tag] = instruction;
}
});
}
});
logSh(`🏗️ Structure depuis hiérarchie: ${Object.keys(contentStructure).length} éléments`, 'DEBUG');
if (validationErrors > 0) {
logSh(`⚠️ ${validationErrors} erreurs de validation détectées lors de la construction de la structure`, 'WARNING');
logSh(`💡 Cela indique que MissingKeywords.js n'a pas correctement synchronisé resolvedContent et instructions`, 'WARNING');
}
logSh(`✅ STRUCTURE CONSTRUITE: ${Object.keys(contentStructure).length} éléments prêts pour génération`, 'INFO');
logSh(`📊 RÉSUMÉ INSTRUCTIONS:`, 'INFO');
// 📊 LOG: Afficher toutes les instructions finales
Object.entries(contentStructure).forEach(([tag, instruction], idx) => {
const shortInstr = instruction.length > 80 ? instruction.substring(0, 80) + '...' : instruction;
logSh(` [${idx + 1}] ${tag}: "${shortInstr}"`, 'INFO');
});
} else {
// Fallback: structure générique si pas de hiérarchie
logSh(`⚠️ Pas de hiérarchie, utilisation structure générique`, 'WARNING');

View File

@ -24,7 +24,8 @@ async function applyAdversarialLayer(existingContent, config = {}) {
method = 'regeneration', // 'regeneration' | 'enhancement' | 'hybrid'
preserveStructure = true,
csvData = null,
context = {}
context = {},
llmProvider = 'gemini-pro' // ✅ AJOUTÉ: Extraction llmProvider avec fallback
} = config;
await tracer.annotate({
@ -32,29 +33,31 @@ async function applyAdversarialLayer(existingContent, config = {}) {
detectorTarget,
intensity,
method,
llmProvider,
elementsCount: Object.keys(existingContent).length
});
const startTime = Date.now();
logSh(`🎯 APPLICATION COUCHE ADVERSARIALE: ${detectorTarget} (${method})`, 'INFO');
logSh(` 📊 ${Object.keys(existingContent).length} éléments | Intensité: ${intensity}`, 'INFO');
logSh(` 📊 ${Object.keys(existingContent).length} éléments | Intensité: ${intensity} | LLM: ${llmProvider}`, 'INFO');
try {
// Initialiser stratégie détecteur
const strategy = DetectorStrategyFactory.createStrategy(detectorTarget);
// Appliquer méthode adversariale choisie
// Appliquer méthode adversariale choisie avec LLM spécifié
let adversarialContent = {};
const methodConfig = { ...config, llmProvider }; // ✅ Assurer propagation llmProvider
switch (method) {
case 'regeneration':
adversarialContent = await applyRegenerationMethod(existingContent, config, strategy);
adversarialContent = await applyRegenerationMethod(existingContent, methodConfig, strategy);
break;
case 'enhancement':
adversarialContent = await applyEnhancementMethod(existingContent, config, strategy);
adversarialContent = await applyEnhancementMethod(existingContent, methodConfig, strategy);
break;
case 'hybrid':
adversarialContent = await applyHybridMethod(existingContent, config, strategy);
adversarialContent = await applyHybridMethod(existingContent, methodConfig, strategy);
break;
default:
throw new Error(`Méthode adversariale inconnue: ${method}`);
@ -77,6 +80,7 @@ async function applyAdversarialLayer(existingContent, config = {}) {
return {
content: adversarialContent,
stats,
modifications: stats.elementsModified, // ✅ AJOUTÉ: Mapping pour PipelineExecutor
original: existingContent,
config
};
@ -102,22 +106,23 @@ async function applyAdversarialLayer(existingContent, config = {}) {
* MÉTHODE RÉGÉNÉRATION - Réécrire complètement avec prompts adversariaux
*/
async function applyRegenerationMethod(existingContent, config, strategy) {
logSh(`🔄 Méthode régénération adversariale`, 'DEBUG');
const llmToUse = config.llmProvider || 'gemini-pro';
logSh(`🔄 Méthode régénération adversariale (LLM: ${llmToUse})`, 'DEBUG');
const results = {};
const contentEntries = Object.entries(existingContent);
// Traiter en chunks pour éviter timeouts
const chunks = chunkArray(contentEntries, 4);
for (let chunkIndex = 0; chunkIndex < chunks.length; chunkIndex++) {
const chunk = chunks[chunkIndex];
logSh(` 📦 Régénération chunk ${chunkIndex + 1}/${chunks.length}: ${chunk.length} éléments`, 'DEBUG');
try {
const regenerationPrompt = createRegenerationPrompt(chunk, config, strategy);
const response = await callLLM(config.llmProvider || 'gemini', regenerationPrompt, {
const response = await callLLM(llmToUse, regenerationPrompt, {
temperature: 0.7 + (config.intensity * 0.2), // Température variable selon intensité
maxTokens: 2000 * chunk.length
}, config.csvData?.personality);
@ -149,22 +154,23 @@ async function applyRegenerationMethod(existingContent, config, strategy) {
* MÉTHODE ENHANCEMENT - Améliorer sans réécrire complètement
*/
async function applyEnhancementMethod(existingContent, config, strategy) {
logSh(`🔧 Méthode enhancement adversarial`, 'DEBUG');
const llmToUse = config.llmProvider || 'gemini-pro';
logSh(`🔧 Méthode enhancement adversarial (LLM: ${llmToUse})`, 'DEBUG');
const results = { ...existingContent }; // Base: contenu original
const elementsToEnhance = selectElementsForEnhancement(existingContent, config);
if (elementsToEnhance.length === 0) {
logSh(` ⏭️ Aucun élément nécessite enhancement`, 'DEBUG');
return results;
}
logSh(` 📋 ${elementsToEnhance.length} éléments sélectionnés pour enhancement`, 'DEBUG');
const enhancementPrompt = createEnhancementPrompt(elementsToEnhance, config, strategy);
try {
const response = await callLLM(config.llmProvider || 'gemini', enhancementPrompt, {
const response = await callLLM(llmToUse, enhancementPrompt, {
temperature: 0.5 + (config.intensity * 0.3),
maxTokens: 3000
}, config.csvData?.personality);

View File

@ -159,6 +159,7 @@ async function applyLayerPipeline(content, layers = [], globalOptions = {}) {
return {
content: currentContent,
stats: pipelineStats,
modifications: pipelineStats.totalModifications, // ✅ AJOUTÉ: Mapping pour PipelineExecutor
original: content
};

View File

@ -167,6 +167,7 @@ async function applyHumanSimulationLayer(content, options = {}) {
return {
content: simulatedContent,
stats: simulationStats,
modifications: simulationStats.totalModifications, // ✅ AJOUTÉ: Mapping pour PipelineExecutor
fallback: simulationStats.fallbackUsed,
qualityScore: simulationStats.qualityScore,
duration

View File

@ -249,6 +249,7 @@ async function applyPredefinedSimulation(content, stackName, options = {}) {
return {
content,
stats: { fallbackUsed: true, error: error.message },
modifications: 0, // ✅ AJOUTÉ: Mapping pour PipelineExecutor (fallback = 0 modifs)
fallback: true,
stackInfo: { name: stack.name, error: error.message }
};

View File

@ -4,15 +4,43 @@
// FONCTIONNALITÉS: Dashboard, tests modulaires, API complète
// ========================================
const express = require('express');
const cors = require('cors');
const path = require('path');
const WebSocket = require('ws');
// ⏱️ Timing chargement modules (logs avant/après chaque require)
const _t0 = Date.now();
console.log(`[${new Date().toISOString()}] ⏱️ [require] Début chargement ManualServer modules...`);
const _t1 = Date.now();
const express = require('express');
console.log(`[${new Date().toISOString()}] ✓ [require] express en ${Date.now() - _t1}ms`);
const _t2 = Date.now();
const cors = require('cors');
console.log(`[${new Date().toISOString()}] ✓ [require] cors en ${Date.now() - _t2}ms`);
const _t3 = Date.now();
const path = require('path');
console.log(`[${new Date().toISOString()}] ✓ [require] path en ${Date.now() - _t3}ms`);
const _t4 = Date.now();
const WebSocket = require('ws');
console.log(`[${new Date().toISOString()}] ✓ [require] ws (WebSocket) en ${Date.now() - _t4}ms`);
const _t5 = Date.now();
const { logSh } = require('../ErrorReporting');
console.log(`[${new Date().toISOString()}] ✓ [require] ErrorReporting en ${Date.now() - _t5}ms`);
const _t6 = Date.now();
const { handleModularWorkflow, benchmarkStacks } = require('../Main');
console.log(`[${new Date().toISOString()}] ✓ [require] Main en ${Date.now() - _t6}ms`);
const _t7 = Date.now();
const { APIController } = require('../APIController');
console.log(`[${new Date().toISOString()}] ✓ [require] APIController en ${Date.now() - _t7}ms`);
const _t8 = Date.now();
const { BatchController } = require('../batch/BatchController');
console.log(`[${new Date().toISOString()}] ✓ [require] BatchController en ${Date.now() - _t8}ms`);
console.log(`[${new Date().toISOString()}] ✅ [require] TOTAL ManualServer modules chargés en ${Date.now() - _t0}ms`);
/**
* SERVEUR MODE MANUAL
@ -43,6 +71,10 @@ class ManualServer {
this.isRunning = false;
this.apiController = new APIController();
this.batchController = new BatchController();
// Cache pour status LLMs (évite d'appeler trop souvent)
this.llmStatusCache = null;
this.llmStatusCacheTime = null;
}
// ========================================
@ -57,32 +89,57 @@ class ManualServer {
logSh('⚠️ ManualServer déjà en cours d\'exécution', 'WARNING');
return;
}
const startTime = Date.now();
logSh('🎯 Démarrage ManualServer...', 'INFO');
try {
// 1. Configuration Express
logSh('⏱️ [1/7] Configuration Express...', 'INFO');
const t1 = Date.now();
await this.setupExpressApp();
logSh(`✓ Express configuré en ${Date.now() - t1}ms`, 'INFO');
// 2. Routes API
logSh('⏱️ [2/7] Configuration routes API...', 'INFO');
const t2 = Date.now();
this.setupAPIRoutes();
logSh(`✓ Routes API configurées en ${Date.now() - t2}ms`, 'INFO');
// 3. Interface Web
logSh('⏱️ [3/7] Configuration interface web...', 'INFO');
const t3 = Date.now();
this.setupWebInterface();
logSh(`✓ Interface web configurée en ${Date.now() - t3}ms`, 'INFO');
// 4. WebSocket pour logs temps réel
logSh('⏱️ [4/7] Démarrage WebSocket serveur...', 'INFO');
const t4 = Date.now();
await this.setupWebSocketServer();
logSh(`✓ WebSocket démarré en ${Date.now() - t4}ms`, 'INFO');
// 5. Démarrage serveur HTTP
logSh('⏱️ [5/7] Démarrage serveur HTTP...', 'INFO');
const t5 = Date.now();
await this.startHTTPServer();
logSh(`✓ Serveur HTTP démarré en ${Date.now() - t5}ms`, 'INFO');
// 6. Monitoring
logSh('⏱️ [6/7] Démarrage monitoring...', 'INFO');
const t6 = Date.now();
this.startMonitoring();
logSh(`✓ Monitoring démarré en ${Date.now() - t6}ms`, 'INFO');
// 7. Initialisation status LLMs au démarrage (en background)
logSh('⏱️ [7/7] Initialisation LLM status (background)...', 'INFO');
const t7 = Date.now();
this.initializeLLMStatus();
logSh(`✓ LLM init lancé en ${Date.now() - t7}ms`, 'INFO');
this.isRunning = true;
this.stats.startTime = Date.now();
logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port}`, 'INFO');
logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port} (total: ${Date.now() - startTime}ms)`, 'INFO');
logSh(`📡 WebSocket logs sur ws://localhost:${this.config.wsPort}`, 'INFO');
} catch (error) {
@ -97,19 +154,31 @@ class ManualServer {
*/
async stop() {
if (!this.isRunning) return;
logSh('🛑 Arrêt ManualServer...', 'INFO');
try {
// Arrêter le monitoring
if (this.monitorInterval) {
clearInterval(this.monitorInterval);
this.monitorInterval = null;
}
// Arrêter le refresh status LLMs
if (this.llmStatusInterval) {
clearInterval(this.llmStatusInterval);
this.llmStatusInterval = null;
}
// Déconnecter tous les clients WebSocket
this.disconnectAllClients();
// Arrêter WebSocket server
if (this.wsServer) {
this.wsServer.close();
this.wsServer = null;
}
// Arrêter serveur HTTP
if (this.server) {
await new Promise((resolve) => {
@ -117,11 +186,11 @@ class ManualServer {
});
this.server = null;
}
this.isRunning = false;
logSh('✅ ManualServer arrêté', 'INFO');
} catch (error) {
logSh(`⚠️ Erreur arrêt ManualServer: ${error.message}`, 'WARNING');
}
@ -823,6 +892,11 @@ class ManualServer {
await this.apiController.getMetrics(req, res);
});
// === LLM MONITORING API ===
this.app.get('/api/llm/status', async (req, res) => {
await this.handleLLMStatus(req, res);
});
// === PROMPT ENGINE API ===
this.app.post('/api/generate-prompt', async (req, res) => {
await this.apiController.generatePrompt(req, res);
@ -1483,6 +1557,187 @@ class ManualServer {
}
}
/**
* Initialise le status LLMs au démarrage (en background)
* OPTIMISÉ: Mode rapide au démarrage, complet après 30s
*/
async initializeLLMStatus() {
logSh('🚀 Initialisation status LLMs (mode rapide)...', 'DEBUG');
// ⚡ Phase 1: Vérification RAPIDE des clés API (immédiat, sans réseau)
setImmediate(async () => {
try {
await this.refreshLLMStatus(true); // quickMode = true
logSh('✅ Status LLMs (rapide) initialisé - vérification clés API OK', 'INFO');
// ⚡ Phase 2: Test COMPLET avec appels réseau après 30 secondes
setTimeout(async () => {
try {
logSh('🔄 Démarrage vérification complète status LLMs (avec tests réseau)...', 'DEBUG');
await this.refreshLLMStatus(false); // quickMode = false
logSh('✅ Status LLMs (complet) mis à jour avec tests réseau', 'INFO');
} catch (error) {
logSh(`⚠️ Erreur vérification complète LLMs: ${error.message}`, 'WARNING');
}
}, 30000); // Attendre 30 secondes avant le test complet
} catch (error) {
logSh(`⚠️ Erreur initialisation rapide status LLMs: ${error.message}`, 'WARNING');
}
});
// Rafraîchir toutes les 30 minutes (1800000ms) en mode complet
this.llmStatusInterval = setInterval(async () => {
try {
await this.refreshLLMStatus(false); // Mode complet pour les refreshs périodiques
} catch (error) {
logSh(`⚠️ Erreur refresh status LLMs: ${error.message}`, 'WARNING');
}
}, 1800000);
}
/**
* Rafraîchit le cache du status LLMs
* OPTIMISÉ: Test rapide sans appels LLM réels au démarrage
*/
async refreshLLMStatus(quickMode = false) {
const { getLLMProvidersList } = require('../LLMManager');
logSh(`📊 Récupération status LLMs${quickMode ? ' (mode rapide)' : ''}...`, 'DEBUG');
const providers = getLLMProvidersList();
const providersWithStatus = [];
if (quickMode) {
// ⚡ MODE RAPIDE: Vérifier juste les clés API sans appels réseau
for (const provider of providers) {
const hasApiKey = this.checkProviderApiKey(provider.id);
providersWithStatus.push({
...provider,
status: hasApiKey ? 'unknown' : 'no_key',
latency: null,
lastTest: null,
credits: 'unlimited',
calls: 0,
successRate: hasApiKey ? null : 0,
quickMode: true
});
}
logSh('⚡ Status rapide LLMs (sans appels réseau) - vérification clés API uniquement', 'DEBUG');
} else {
// MODE COMPLET: Test réseau réel
for (const provider of providers) {
const startTime = Date.now();
let status = 'offline';
let latency = null;
let lastTest = null;
try {
const { callLLM } = require('../LLMManager');
await callLLM(provider.id, 'Test ping', { maxTokens: 10 });
latency = Date.now() - startTime;
status = 'online';
lastTest = new Date().toLocaleTimeString('fr-FR');
} catch (error) {
logSh(`⚠️ Provider ${provider.id} offline: ${error.message}`, 'DEBUG');
status = 'offline';
}
providersWithStatus.push({
...provider,
status,
latency,
lastTest,
credits: 'unlimited',
calls: 0,
successRate: status === 'online' ? 100 : 0
});
}
}
// Mettre à jour le cache
this.llmStatusCache = {
success: true,
providers: providersWithStatus,
summary: {
total: providersWithStatus.length,
online: providersWithStatus.filter(p => p.status === 'online').length,
offline: providersWithStatus.filter(p => p.status === 'offline').length,
unknown: providersWithStatus.filter(p => p.status === 'unknown').length,
no_key: providersWithStatus.filter(p => p.status === 'no_key').length
},
quickMode: quickMode,
timestamp: new Date().toISOString()
};
this.llmStatusCacheTime = Date.now();
if (quickMode) {
logSh(`⚡ Status LLMs (rapide): ${this.llmStatusCache.summary.unknown} providers avec clés, ${this.llmStatusCache.summary.no_key} sans clés`, 'INFO');
} else {
logSh(`✅ Status LLMs (complet): ${this.llmStatusCache.summary.online}/${this.llmStatusCache.summary.total} online`, 'INFO');
}
}
/**
* Vérifie si un provider a une clé API configurée
*/
checkProviderApiKey(providerId) {
const keyMap = {
'claude-sonnet-4-5': 'ANTHROPIC_API_KEY',
'claude-3-5-sonnet-20241022': 'ANTHROPIC_API_KEY',
'gpt-4o': 'OPENAI_API_KEY',
'gpt-4o-mini': 'OPENAI_API_KEY',
'gemini-2-0-flash-exp': 'GOOGLE_API_KEY',
'gemini-pro': 'GOOGLE_API_KEY',
'deepseek-chat': 'DEEPSEEK_API_KEY',
'moonshot-v1-8k': 'MOONSHOT_API_KEY',
'mistral-small-latest': 'MISTRAL_API_KEY'
};
const envKey = keyMap[providerId];
if (!envKey) return false;
const apiKey = process.env[envKey];
return apiKey && apiKey.length > 10;
}
/**
* Handler pour status et monitoring des LLMs
*/
async handleLLMStatus(req, res) {
try {
// Si on a un cache, le retourner directement
if (this.llmStatusCache) {
res.json(this.llmStatusCache);
} else {
// Pas encore de cache, retourner une réponse vide
res.json({
success: true,
providers: [],
summary: {
total: 0,
online: 0,
offline: 0
},
timestamp: new Date().toISOString(),
message: 'Status LLMs en cours de chargement...'
});
}
} catch (error) {
logSh(`❌ Erreur status LLMs: ${error.message}`, 'ERROR');
res.status(500).json({
success: false,
error: 'Erreur récupération status LLMs',
message: error.message,
timestamp: new Date().toISOString()
});
}
}
/**
* 🆕 Handler pour génération simple d'article avec mot-clé
*/
@ -1700,6 +1955,7 @@ class ManualServer {
<div class="section">
<h2>📊 Monitoring & API</h2>
<p>Endpoints disponibles en mode MANUAL.</p>
<a href="/llm-monitoring.html" target="_blank" class="button warning">🤖 LLM Monitoring</a>
<a href="/api/status" target="_blank" class="button">📊 Status API</a>
<a href="/api/stats" target="_blank" class="button">📈 Statistiques</a>
<button onclick="testConnection()" class="button success">🔍 Test Connexion</button>

View File

@ -47,25 +47,36 @@ class ModeManager {
* @param {string} initialMode - Mode initial (manual|auto|detect)
*/
static async initialize(initialMode = 'detect') {
const startTime = Date.now();
logSh('🎛️ Initialisation ModeManager...', 'INFO');
try {
// Détecter mode selon arguments ou config
logSh('⏱️ Détection mode...', 'INFO');
const t1 = Date.now();
const detectedMode = this.detectIntendedMode(initialMode);
logSh(`🎯 Mode détecté: ${detectedMode.toUpperCase()}`, 'INFO');
logSh(`✓ Mode détecté: ${detectedMode.toUpperCase()} en ${Date.now() - t1}ms`, 'INFO');
// Nettoyer état précédent si nécessaire
logSh('⏱️ Nettoyage état précédent...', 'INFO');
const t2 = Date.now();
await this.cleanupPreviousState();
logSh(`✓ Nettoyage terminé en ${Date.now() - t2}ms`, 'INFO');
// Basculer vers le mode détecté
logSh(`⏱️ Basculement vers mode ${detectedMode.toUpperCase()}...`, 'INFO');
const t3 = Date.now();
await this.switchToMode(detectedMode);
logSh(`✓ Basculement terminé en ${Date.now() - t3}ms`, 'INFO');
// Sauvegarder état
logSh('⏱️ Sauvegarde état...', 'INFO');
const t4 = Date.now();
this.saveModeState();
logSh(`✅ ModeManager initialisé en mode ${this.currentMode.toUpperCase()}`, 'INFO');
logSh(`✓ État sauvegardé en ${Date.now() - t4}ms`, 'INFO');
logSh(`✅ ModeManager initialisé en mode ${this.currentMode.toUpperCase()} (total: ${Date.now() - startTime}ms)`, 'INFO');
return this.currentMode;
} catch (error) {
@ -246,14 +257,22 @@ class ModeManager {
* Démarre le mode MANUAL
*/
static async startManualMode() {
const t1 = Date.now();
logSh('⏱️ Chargement module ManualServer...', 'INFO');
const { ManualServer } = require('./ManualServer');
logSh('🎯 Démarrage ManualServer...', 'DEBUG');
logSh(`✓ ManualServer chargé en ${Date.now() - t1}ms`, 'INFO');
const t2 = Date.now();
logSh('⏱️ Instanciation ManualServer...', 'INFO');
this.activeServices.manualServer = new ManualServer();
logSh(`✓ ManualServer instancié en ${Date.now() - t2}ms`, 'INFO');
const t3 = Date.now();
logSh('⏱️ Démarrage ManualServer.start()...', 'INFO');
await this.activeServices.manualServer.start();
logSh('✅ Mode MANUAL démarré', 'DEBUG');
logSh(`✓ ManualServer.start() terminé en ${Date.now() - t3}ms`, 'INFO');
logSh('✅ Mode MANUAL démarré', 'INFO');
}
/**

View File

@ -277,6 +277,7 @@ async function applyPatternBreakingLayer(content, options = {}) {
return {
content: processedContent,
stats: patternStats,
modifications: patternStats.totalModifications, // ✅ AJOUTÉ: Mapping pour PipelineExecutor
fallback: patternStats.fallbackUsed,
duration
};

View File

@ -178,6 +178,7 @@ async function applyPatternBreakingStack(stackName, content, overrides = {}) {
stackDescription: stack.description,
expectedReduction: stack.expectedReduction
},
modifications: result.modifications || result.stats?.totalModifications || 0, // ✅ AJOUTÉ: Propagation modifications
fallback: result.fallback,
stackUsed: stackName
};

View File

@ -6,18 +6,12 @@
*/
const { logSh } = require('../ErrorReporting');
const { getLLMProvidersList } = require('../LLMManager');
/**
* Providers LLM disponibles
* Providers LLM disponibles (source unique depuis LLMManager)
*/
const AVAILABLE_LLM_PROVIDERS = [
{ id: 'claude', name: 'Claude (Anthropic)', default: true },
{ id: 'openai', name: 'OpenAI GPT-4' },
{ id: 'gemini', name: 'Google Gemini' },
{ id: 'deepseek', name: 'Deepseek' },
{ id: 'moonshot', name: 'Moonshot' },
{ id: 'mistral', name: 'Mistral AI' }
];
const AVAILABLE_LLM_PROVIDERS = getLLMProvidersList();
/**
* Modules disponibles dans le pipeline
@ -28,9 +22,9 @@ const AVAILABLE_MODULES = {
description: 'Génération initiale du contenu',
modes: ['simple'],
defaultIntensity: 1.0,
defaultLLM: 'claude',
defaultLLM: 'claude-sonnet-4-5',
parameters: {
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'claude' }
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'claude-sonnet-4-5' }
}
},
selective: {
@ -45,10 +39,10 @@ const AVAILABLE_MODULES = {
'adaptive'
],
defaultIntensity: 1.0,
defaultLLM: 'openai',
defaultLLM: 'gpt-4o-mini',
parameters: {
layers: { type: 'array', description: 'Couches spécifiques à appliquer' },
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'openai' }
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gpt-4o-mini' }
}
},
adversarial: {
@ -56,11 +50,11 @@ const AVAILABLE_MODULES = {
description: 'Techniques anti-détection',
modes: ['none', 'light', 'standard', 'heavy', 'adaptive'],
defaultIntensity: 1.0,
defaultLLM: 'gemini',
defaultLLM: 'gemini-pro',
parameters: {
detector: { type: 'string', enum: ['general', 'gptZero', 'originality'], default: 'general' },
method: { type: 'string', enum: ['enhancement', 'regeneration', 'hybrid'], default: 'regeneration' },
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gemini' }
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'gemini-pro' }
}
},
human: {
@ -76,11 +70,11 @@ const AVAILABLE_MODULES = {
'temporalFocus'
],
defaultIntensity: 1.0,
defaultLLM: 'mistral',
defaultLLM: 'mistral-small',
parameters: {
fatigueLevel: { type: 'number', min: 0, max: 1, default: 0.5 },
errorRate: { type: 'number', min: 0, max: 1, default: 0.3 },
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'mistral' }
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'mistral-small' }
}
},
pattern: {
@ -96,10 +90,10 @@ const AVAILABLE_MODULES = {
'connectorsFocus'
],
defaultIntensity: 1.0,
defaultLLM: 'deepseek',
defaultLLM: 'deepseek-chat',
parameters: {
focus: { type: 'string', enum: ['syntax', 'connectors', 'both'], default: 'both' },
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'deepseek' }
llmProvider: { type: 'string', enum: AVAILABLE_LLM_PROVIDERS.map(p => p.id), default: 'deepseek-chat' }
}
}
};

View File

@ -332,8 +332,13 @@ class StyleLayer {
*/
analyzePersonalityAlignment(text, personality) {
if (!personality.vocabulairePref) return 1;
const preferredWords = personality.vocabulairePref.toLowerCase().split(',');
// Convertir en string si ce n'est pas déjà le cas
const vocabPref = typeof personality.vocabulairePref === 'string'
? personality.vocabulairePref
: String(personality.vocabulairePref);
const preferredWords = vocabPref.toLowerCase().split(',');
const contentLower = text.toLowerCase();
let alignmentScore = 0;

View File

@ -15,7 +15,7 @@ const { chunkArray, sleep } = require('./SelectiveUtils');
class TechnicalLayer {
constructor() {
this.name = 'TechnicalEnhancement';
this.defaultLLM = 'openai';
this.defaultLLM = 'gpt-4o-mini';
this.priority = 1; // Haute priorité - appliqué en premier généralement
}

View File

@ -217,33 +217,7 @@
<main>
<div class="card-container">
<!-- Card 1: Configuration Editor (DÉSACTIVÉ - ancien système) -->
<div class="card" style="opacity: 0.5; cursor: not-allowed;" onclick="alert('⚠️ Ancien système désactivé. Utilisez Pipeline Builder à la place.')">
<div class="card-icon">🔧</div>
<h2>Éditeur de Configuration</h2>
<p style="color: var(--warning);">⚠️ ANCIEN SYSTÈME - Désactivé</p>
<ul>
<li>4 couches modulaires configurables</li>
<li>Save/Load des configurations</li>
<li>Test en direct avec logs temps réel</li>
<li>Preview JSON de la configuration</li>
</ul>
</div>
<!-- Card 2: Production Runner (DÉSACTIVÉ - ancien système) -->
<div class="card" style="opacity: 0.5; cursor: not-allowed;" onclick="alert('⚠️ Ancien système désactivé. Utilisez Pipeline Runner à la place.')">
<div class="card-icon">🚀</div>
<h2>Runner de Production</h2>
<p style="color: var(--warning);">⚠️ ANCIEN SYSTÈME - Désactivé</p>
<ul>
<li>Load configuration sauvegardée</li>
<li>Sélection ligne Google Sheets</li>
<li>Logs temps réel pendant l'exécution</li>
<li>Résultats et lien direct vers GSheets</li>
</ul>
</div>
<!-- Card 3: Pipeline Builder -->
<!-- Card 1: Pipeline Builder -->
<div class="card" onclick="navigateTo('pipeline-builder.html')">
<div class="card-icon">🎨</div>
<h2>Pipeline Builder</h2>
@ -256,7 +230,7 @@
</ul>
</div>
<!-- Card 4: Pipeline Runner -->
<!-- Card 2: Pipeline Runner -->
<div class="card" onclick="navigateTo('pipeline-runner.html')">
<div class="card-icon"></div>
<h2>Pipeline Runner</h2>
@ -268,6 +242,19 @@
<li>Logs d'exécution complets</li>
</ul>
</div>
<!-- Card 3: LLM Monitoring -->
<div class="card" onclick="navigateTo('llm-monitoring.html')">
<div class="card-icon">🤖</div>
<h2>LLM Monitoring</h2>
<p>Surveiller la santé et les performances de vos modèles LLM</p>
<ul>
<li>Status en temps réel (9 LLMs)</li>
<li>Latences moyennes et barres de progression</li>
<li>Crédits restants par plateforme</li>
<li>Auto-refresh toutes les 30 secondes</li>
</ul>
</div>
</div>
<div class="stats-panel">

View File

@ -399,10 +399,38 @@
white-space: pre-wrap;
}
@media (max-width: 1400px) {
/* Responsive Layouts */
@media (max-width: 1400px) and (min-width: 900px) {
.builder-layout {
grid-template-columns: 280px 1fr;
grid-template-rows: auto;
}
.modules-palette {
grid-row: 1 / 3;
height: calc(100vh - 180px);
}
.side-panel {
height: auto;
max-height: none;
}
}
@media (max-width: 899px) {
.builder-layout {
grid-template-columns: 1fr;
}
.modules-palette,
.side-panel {
height: auto;
max-height: 400px;
}
.panel {
padding: 15px;
}
}
</style>
</head>

View File

@ -74,14 +74,14 @@ async function loadLLMProviders() {
}
} catch (error) {
console.error('Erreur chargement LLM providers:', error);
// Fallback providers si l'API échoue
// Fallback providers si l'API échoue (synchronisé avec LLMManager)
state.llmProviders = [
{ id: 'claude', name: 'Claude (Anthropic)', default: true },
{ id: 'openai', name: 'OpenAI GPT-4' },
{ id: 'gemini', name: 'Google Gemini' },
{ id: 'deepseek', name: 'Deepseek' },
{ id: 'moonshot', name: 'Moonshot' },
{ id: 'mistral', name: 'Mistral AI' }
{ id: 'claude-sonnet-4-5', name: 'Claude Sonnet 4.5', default: true },
{ id: 'gpt-4o-mini', name: 'GPT-4o Mini' },
{ id: 'gemini-pro', name: 'Google Gemini Pro' },
{ id: 'deepseek-chat', name: 'Deepseek Chat' },
{ id: 'moonshot-v1-32k', name: 'Moonshot v1 32K' },
{ id: 'mistral-small', name: 'Mistral Small' }
];
}
}

View File

@ -345,6 +345,23 @@
</div>
</div>
<h3 style="margin-top: 25px; margin-bottom: 10px;">📄 Contenu Final Généré</h3>
<div id="finalContentContainer" style="background: var(--bg-light); padding: 15px; border-radius: 8px; margin-bottom: 20px; max-height: 400px; overflow-y: auto;">
<p style="color: var(--text-light);">Le contenu apparaîtra ici après l'exécution</p>
</div>
<h3 style="margin-top: 25px; margin-bottom: 10px;">📦 Versions Sauvegardées</h3>
<div id="versionHistory" style="background: var(--bg-light); padding: 15px; border-radius: 8px; margin-bottom: 20px;">
<p style="color: var(--text-light);">Aucune version disponible</p>
</div>
<div id="gsheetsLinkContainer" style="display: none; margin-bottom: 20px;">
<a id="gsheetsLink" href="#" target="_blank"
style="display: inline-block; padding: 12px 20px; background: linear-gradient(135deg, #34d399, #10b981); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; transition: all 0.2s;">
📊 Ouvrir dans Google Sheets
</a>
</div>
<h3 style="margin-top: 25px; margin-bottom: 10px;">📝 Log d'Exécution</h3>
<div class="execution-log" id="executionLog"></div>
</div>

View File

@ -182,6 +182,14 @@ function displayResults(result) {
const resultsSection = document.getElementById('resultsSection');
resultsSection.style.display = 'block';
// Log complet des résultats dans la console
console.log('=== RÉSULTAT PIPELINE ===');
console.log('Contenu final:', result.finalContent || result.content);
console.log('Stats:', result.stats);
console.log('Version history:', result.versionHistory);
console.log('Résultat complet:', result);
console.log('========================');
// Stats
document.getElementById('statDuration').textContent =
`${result.stats.totalDuration}ms`;
@ -192,6 +200,130 @@ function displayResults(result) {
document.getElementById('statPersonality').textContent =
result.stats.personality || 'N/A';
// Final Content Display
const finalContentContainer = document.getElementById('finalContentContainer');
let rawContent = result.finalContent || result.content || result.organicContent;
// Extraire le texte si c'est un objet
let finalContent;
let isStructuredContent = false;
if (typeof rawContent === 'string') {
finalContent = rawContent;
} else if (rawContent && typeof rawContent === 'object') {
// Vérifier si c'est un contenu structuré (H2_1, H3_2, etc.)
const keys = Object.keys(rawContent);
if (keys.some(k => k.match(/^(H2|H3|P)_\d+$/))) {
isStructuredContent = true;
// Formater le contenu structuré
finalContent = keys
.sort((a, b) => {
// Trier par type (H2, H3, P) puis par numéro
const aMatch = a.match(/^([A-Z]+)_(\d+)$/);
const bMatch = b.match(/^([A-Z]+)_(\d+)$/);
if (!aMatch || !bMatch) return 0;
if (aMatch[1] !== bMatch[1]) return aMatch[1].localeCompare(bMatch[1]);
return parseInt(aMatch[2]) - parseInt(bMatch[2]);
})
.map(key => {
const match = key.match(/^([A-Z0-9]+)_(\d+)$/);
if (match) {
const tag = match[1];
return `[${tag}]\n${rawContent[key]}\n`;
}
return `${key}: ${rawContent[key]}\n`;
})
.join('\n');
} else {
// Si c'est un objet, essayer d'extraire le texte
finalContent = rawContent.text || rawContent.content || rawContent.organicContent || JSON.stringify(rawContent, null, 2);
}
}
if (finalContent) {
finalContentContainer.innerHTML = '';
// Warning si contenu incomplet
const elementCount = Object.keys(rawContent || {}).length;
if (isStructuredContent && elementCount < 30) {
const warningDiv = document.createElement('div');
warningDiv.style.cssText = 'padding: 10px; margin-bottom: 15px; background: #fed7d7; border: 1px solid #f56565; border-radius: 6px; color: #822727;';
warningDiv.innerHTML = `⚠️ <strong>Génération incomplète:</strong> ${elementCount} éléments générés (attendu ~33). Vérifiez les logs pour plus de détails.`;
finalContentContainer.appendChild(warningDiv);
}
// Créer un élément pre pour préserver le formatage
const contentDiv = document.createElement('div');
contentDiv.style.cssText = 'white-space: pre-wrap; line-height: 1.6; color: var(--text-dark); font-size: 14px;';
contentDiv.textContent = finalContent;
// Ajouter un bouton pour copier
const copyBtn = document.createElement('button');
copyBtn.textContent = '📋 Copier le contenu';
copyBtn.style.cssText = 'margin-bottom: 15px; padding: 8px 16px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;';
copyBtn.onclick = () => {
navigator.clipboard.writeText(finalContent);
copyBtn.textContent = '✓ Copié!';
setTimeout(() => { copyBtn.textContent = '📋 Copier le contenu'; }, 2000);
};
finalContentContainer.appendChild(copyBtn);
finalContentContainer.appendChild(contentDiv);
// Ajouter les métadonnées si disponibles
if (result.stats) {
const metaDiv = document.createElement('div');
metaDiv.style.cssText = 'margin-top: 15px; padding: 10px; background: white; border-radius: 6px; font-size: 12px; color: var(--text-light);';
const contentLength = finalContent.length;
const wordCount = finalContent.split(/\s+/).length;
metaDiv.innerHTML = `<strong>Métadonnées:</strong> ${contentLength} caractères, ~${wordCount} mots`;
finalContentContainer.appendChild(metaDiv);
}
} else {
finalContentContainer.innerHTML = '<p style="color: var(--warning);">⚠️ Aucun contenu final disponible dans le résultat</p>';
}
// Version History
const versionHistoryContainer = document.getElementById('versionHistory');
versionHistoryContainer.innerHTML = '';
if (result.versionHistory && result.versionHistory.length > 0) {
result.versionHistory.forEach(version => {
const div = document.createElement('div');
div.style.cssText = 'padding: 10px; margin-bottom: 8px; background: white; border-radius: 6px; border-left: 4px solid var(--success);';
const versionLabel = document.createElement('strong');
versionLabel.textContent = `Version ${version.version}`;
versionLabel.style.color = 'var(--primary)';
const articleId = document.createElement('span');
articleId.textContent = ` - Article ID: ${version.articleId}`;
articleId.style.color = 'var(--text-dark)';
const modifications = document.createElement('span');
modifications.textContent = ` - ${version.modifications || 0} modifications`;
modifications.style.color = 'var(--text-light)';
modifications.style.fontSize = '13px';
modifications.style.marginLeft = '10px';
div.appendChild(versionLabel);
div.appendChild(articleId);
div.appendChild(modifications);
versionHistoryContainer.appendChild(div);
});
} else {
versionHistoryContainer.innerHTML = '<p style="color: var(--text-light);">Aucune version sauvegardée</p>';
}
// Google Sheets Link
if (result.gsheetsLink) {
document.getElementById('gsheetsLinkContainer').style.display = 'block';
document.getElementById('gsheetsLink').href = result.gsheetsLink;
} else {
document.getElementById('gsheetsLinkContainer').style.display = 'none';
}
// Execution log
const logContainer = document.getElementById('executionLog');
logContainer.innerHTML = '';

View File

@ -4,10 +4,27 @@
// MODES: MANUAL (interface client) | AUTO (batch GSheets)
// ========================================
const startupTime = Date.now();
console.log(`[${Date.now() - startupTime}ms] Chargement dotenv...`);
require('dotenv').config();
console.log(`[${Date.now() - startupTime}ms] Chargement ErrorReporting...`);
const { logSh } = require('./lib/ErrorReporting');
const { ModeManager } = require('./lib/modes/ModeManager');
console.log(`[${Date.now() - startupTime}ms] Chargement ModeManager...`);
// ⚡ LAZY LOADING: Charger ModeManager seulement quand nécessaire
let ModeManager = null;
function getModeManager() {
if (!ModeManager) {
const loadStart = Date.now();
logSh('⚡ Chargement ModeManager (lazy)...', 'DEBUG');
ModeManager = require('./lib/modes/ModeManager').ModeManager;
console.log(`[${Date.now() - startupTime}ms] ModeManager chargé !`);
logSh(`⚡ ModeManager chargé en ${Date.now() - loadStart}ms`, 'DEBUG');
}
return ModeManager;
}
/**
* SERVEUR SEO GENERATOR - MODES EXCLUSIFS
@ -30,9 +47,10 @@ class SEOGeneratorServer {
// Gestion signaux système
this.setupSignalHandlers();
// Initialisation du gestionnaire de modes
const mode = await ModeManager.initialize();
const MM = getModeManager();
const mode = await MM.initialize();
logSh(`🎯 Serveur démarré en mode ${mode.toUpperCase()}`, 'INFO');
logSh(`⏱️ Temps de démarrage: ${Date.now() - this.startTime}ms`, 'DEBUG');
@ -57,7 +75,8 @@ class SEOGeneratorServer {
const version = require('./package.json').version || '1.0.0';
const nodeVersion = process.version;
const platform = process.platform;
// Bannière visuelle en console.log (pas de logging structuré)
console.log(`
SEO GENERATOR SERVER
@ -75,7 +94,7 @@ class SEOGeneratorServer {
SERVER_MODE=auto npm start
`);
logSh('🚀 === SEO GENERATOR SERVER - DÉMARRAGE ===', 'INFO');
logSh(`📦 Version: ${version} | Node: ${nodeVersion}`, 'INFO');
}
@ -84,17 +103,22 @@ class SEOGeneratorServer {
* Configure la gestion des signaux système
*/
setupSignalHandlers() {
// Arrêt propre sur SIGTERM/SIGINT
// SIGINT (Ctrl+C) : Kill immédiat sans graceful shutdown
process.on('SIGINT', () => {
console.log('\n🛑 SIGINT reçu - Arrêt immédiat (hard kill)');
process.exit(0);
});
// Arrêt propre sur SIGTERM
process.on('SIGTERM', () => this.handleShutdownSignal('SIGTERM'));
process.on('SIGINT', () => this.handleShutdownSignal('SIGINT'));
// Gestion erreurs non capturées
process.on('uncaughtException', (error) => {
logSh(`❌ ERREUR NON CAPTURÉE: ${error.message}`, 'ERROR');
logSh(`Stack: ${error.stack}`, 'ERROR');
this.gracefulShutdown(1);
});
process.on('unhandledRejection', (reason, promise) => {
logSh(`❌ PROMESSE REJETÉE: ${reason}`, 'ERROR');
logSh(`Promise: ${promise}`, 'DEBUG');
@ -131,7 +155,8 @@ class SEOGeneratorServer {
}
// Arrêter le mode actuel via ModeManager
await ModeManager.stopCurrentMode();
const MM = getModeManager();
await MM.stopCurrentMode();
// Nettoyage final
await this.finalCleanup();
@ -160,7 +185,8 @@ class SEOGeneratorServer {
async finalCleanup() {
try {
// Sauvegarder l'état final
ModeManager.saveModeState();
const MM = getModeManager();
MM.saveModeState();
// Autres nettoyages si nécessaire
@ -190,7 +216,8 @@ class SEOGeneratorServer {
* Vérifie la santé du système
*/
performHealthCheck() {
const status = ModeManager.getStatus();
const MM = getModeManager();
const status = MM.getStatus();
const memUsage = process.memoryUsage();
const uptime = process.uptime();
@ -212,9 +239,10 @@ class SEOGeneratorServer {
*/
async switchMode(newMode, force = false) {
logSh(`🔄 Demande changement mode vers: ${newMode}`, 'INFO');
try {
await ModeManager.switchToMode(newMode, force);
const MM = getModeManager();
await MM.switchToMode(newMode, force);
logSh(`✅ Changement mode réussi vers: ${newMode}`, 'INFO');
return true;
@ -237,7 +265,7 @@ class SEOGeneratorServer {
platform: process.platform,
pid: process.pid
},
mode: ModeManager.getStatus(),
mode: getModeManager().getStatus(),
memory: process.memoryUsage(),
timestamp: new Date().toISOString()
};
@ -253,7 +281,7 @@ if (require.main === module) {
const server = new SEOGeneratorServer();
server.start().catch((error) => {
console.error('💥 ERREUR CRITIQUE DÉMARRAGE:', error.message);
logSh(`💥 ERREUR CRITIQUE DÉMARRAGE: ${error.message}`, 'ERROR');
process.exit(1);
});
}

View File

@ -181,7 +181,7 @@ RÉPONSE JSON STRICTE:
SCORE: 0-100 (0=clairement IA, 100=clairement humain)`;
const response = await LLMManager.callLLM('openai', prompt, {
const response = await LLMManager.callLLM('gpt-4o-mini', prompt, {
temperature: 0.1,
max_tokens: 500
});

View File

@ -257,7 +257,7 @@ RÉPONSE JSON STRICTE:
SCORE: 0-100 (0=pas du tout cette personnalité, 100=parfaitement aligné)`;
const response = await LLMManager.callLLM('openai', prompt, {
const response = await LLMManager.callLLM('gpt-4o-mini', prompt, {
temperature: 0.1,
max_tokens: 400
});

View File

@ -127,7 +127,7 @@ RÉPONSE JSON STRICTE:
SCORE: 0-100 (qualité globale perçue par un lecteur)`;
const response = await LLMManager.callLLM('openai', prompt, {
const response = await LLMManager.callLLM('gpt-4o-mini', prompt, {
temperature: 0.1,
max_tokens: 300
});

View File

@ -0,0 +1,212 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const EXPORTS_DIR = path.join(__dirname, '../claude-exports-last-3-days');
/**
* Parse un fichier de session pour extraire les tool uses
*/
function parseSessionFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const tools = [];
// Chercher tous les blocs JSON qui contiennent des tool uses
const jsonBlockRegex = /\[\s*\{[\s\S]*?"type":\s*"tool_use"[\s\S]*?\}\s*\]/g;
const matches = content.match(jsonBlockRegex);
if (!matches) return tools;
for (const match of matches) {
try {
const parsed = JSON.parse(match);
for (const item of parsed) {
if (item.type === 'tool_use' && (item.name === 'Edit' || item.name === 'Write')) {
tools.push({
name: item.name,
input: item.input
});
}
}
} catch (e) {
// Skip invalid JSON
}
}
return tools;
}
/**
* Analyse pourquoi un Edit a été skippé
*/
function analyzeSkippedEdit(filePath, oldString) {
if (!fs.existsSync(filePath)) {
return { reason: 'FILE_NOT_EXIST', details: 'Fichier n\'existe pas' };
}
const content = fs.readFileSync(filePath, 'utf-8');
if (!content.includes(oldString)) {
// Vérifier si une partie de old_string existe
const oldLines = oldString.split('\n').filter(l => l.trim());
const matchingLines = oldLines.filter(line => content.includes(line.trim()));
if (matchingLines.length > 0) {
return {
reason: 'PARTIAL_MATCH',
details: `${matchingLines.length}/${oldLines.length} lignes trouvées - code probablement modifié`
};
} else {
return {
reason: 'NO_MATCH',
details: 'Code complètement différent - changement déjà appliqué ou code refactorisé'
};
}
}
return { reason: 'OK', details: 'Devrait fonctionner' };
}
/**
* Main
*/
function main() {
console.log('🔍 Analyse des exports Claude skippés...\n');
const sessionFiles = fs.readdirSync(EXPORTS_DIR)
.filter(f => f.endsWith('-session.md'))
.sort((a, b) => {
const numA = parseInt(a.split('-')[0]);
const numB = parseInt(b.split('-')[0]);
return numB - numA;
});
const skippedAnalysis = {
FILE_NOT_EXIST: [],
PARTIAL_MATCH: [],
NO_MATCH: [],
FILE_EXISTS: [] // Pour les Write
};
let totalSkipped = 0;
for (const sessionFile of sessionFiles) {
const filePath = path.join(EXPORTS_DIR, sessionFile);
const tools = parseSessionFile(filePath);
for (const tool of tools) {
if (tool.name === 'Edit') {
const { file_path, old_string } = tool.input;
if (!fs.existsSync(file_path)) {
skippedAnalysis.FILE_NOT_EXIST.push({
session: sessionFile,
file: file_path,
preview: old_string.substring(0, 80)
});
totalSkipped++;
} else {
const content = fs.readFileSync(file_path, 'utf-8');
if (!content.includes(old_string)) {
const analysis = analyzeSkippedEdit(file_path, old_string);
if (analysis.reason === 'PARTIAL_MATCH') {
skippedAnalysis.PARTIAL_MATCH.push({
session: sessionFile,
file: file_path,
details: analysis.details,
preview: old_string.substring(0, 80)
});
} else {
skippedAnalysis.NO_MATCH.push({
session: sessionFile,
file: file_path,
preview: old_string.substring(0, 80)
});
}
totalSkipped++;
}
}
} else if (tool.name === 'Write') {
const { file_path } = tool.input;
if (fs.existsSync(file_path)) {
skippedAnalysis.FILE_EXISTS.push({
session: sessionFile,
file: file_path
});
totalSkipped++;
}
}
}
}
console.log(`📊 Total skippés: ${totalSkipped}\n`);
console.log('═══════════════════════════════════════════════════════');
console.log('🚫 FICHIERS N\'EXISTANT PAS (Edit)');
console.log(` ${skippedAnalysis.FILE_NOT_EXIST.length} cas\n`);
const fileNotExistByFile = {};
for (const item of skippedAnalysis.FILE_NOT_EXIST) {
if (!fileNotExistByFile[item.file]) {
fileNotExistByFile[item.file] = 0;
}
fileNotExistByFile[item.file]++;
}
Object.entries(fileNotExistByFile)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.forEach(([file, count]) => {
console.log(` ${count}x - ${file}`);
});
console.log('\n═══════════════════════════════════════════════════════');
console.log('⚠️ CORRESPONDANCE PARTIELLE (Edit - code probablement modifié)');
console.log(` ${skippedAnalysis.PARTIAL_MATCH.length} cas\n`);
const partialByFile = {};
for (const item of skippedAnalysis.PARTIAL_MATCH) {
if (!partialByFile[item.file]) {
partialByFile[item.file] = 0;
}
partialByFile[item.file]++;
}
Object.entries(partialByFile)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.forEach(([file, count]) => {
console.log(` ${count}x - ${file}`);
});
console.log('\n═══════════════════════════════════════════════════════');
console.log('❌ AUCUNE CORRESPONDANCE (Edit - changement déjà appliqué)');
console.log(` ${skippedAnalysis.NO_MATCH.length} cas\n`);
const noMatchByFile = {};
for (const item of skippedAnalysis.NO_MATCH) {
if (!noMatchByFile[item.file]) {
noMatchByFile[item.file] = 0;
}
noMatchByFile[item.file]++;
}
Object.entries(noMatchByFile)
.sort((a, b) => b[1] - a[1])
.slice(0, 10)
.forEach(([file, count]) => {
console.log(` ${count}x - ${file}`);
});
console.log('\n═══════════════════════════════════════════════════════');
console.log('✅ FICHIERS DÉJÀ EXISTANTS (Write - comportement normal)');
console.log(` ${skippedAnalysis.FILE_EXISTS.length} cas\n`);
skippedAnalysis.FILE_EXISTS.forEach(item => {
console.log(` ${item.session}${item.file}`);
});
console.log('\n═══════════════════════════════════════════════════════');
console.log('💡 CONCLUSION:\n');
console.log(` ✅ Write skippés: ${skippedAnalysis.FILE_EXISTS.length} (NORMAL - ne pas écraser)`);
console.log(` ❌ Edit skippés (NO_MATCH): ${skippedAnalysis.NO_MATCH.length} (changements déjà appliqués)`);
console.log(` ⚠️ Edit skippés (PARTIAL_MATCH): ${skippedAnalysis.PARTIAL_MATCH.length} (code modifié depuis)`);
console.log(` 🚫 Edit skippés (FILE_NOT_EXIST): ${skippedAnalysis.FILE_NOT_EXIST.length} (fichiers supprimés?)\n`);
}
main();

View File

@ -0,0 +1,470 @@
#!/usr/bin/env node
/**
* apply-claude-exports-fuzzy.js
*
* Applique les exports Claude avec fuzzy matching amélioré
*
* AMÉLIORATIONS:
* - Normalisation des line endings (\r\n, \r, \n \n unifié)
* - Ignore les différences d'espacement (espaces multiples, tabs)
* - Score de similarité abaissé à 85% pour plus de flexibilité
* - Matching robuste qui ne casse pas sur les variations d'espaces
*
* Usage:
* node tools/apply-claude-exports-fuzzy.js # Apply changes
* node tools/apply-claude-exports-fuzzy.js --dry-run # Preview only
*/
const fs = require('fs');
const path = require('path');
const EXPORTS_DIR = path.join(__dirname, '../claude-exports-last-3-days');
const LOGS_DIR = path.join(__dirname, '../logs');
// Créer dossier logs si nécessaire
if (!fs.existsSync(LOGS_DIR)) {
fs.mkdirSync(LOGS_DIR, { recursive: true });
}
// Fichier de log avec timestamp
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
const LOG_FILE = path.join(LOGS_DIR, `apply-exports-fuzzy-${timestamp}.log`);
// Configuration fuzzy matching
const FUZZY_CONFIG = {
minSimilarity: 0.85, // Minimum 85% de similarité pour accepter le match (abaissé de 95% pour plus de flexibilité)
contextLines: 3, // Lignes de contexte avant/après
ignoreWhitespace: true, // Ignorer les différences d'espacement (espaces multiples, tabs)
ignoreComments: false, // Ignorer les différences dans les commentaires
normalizeLineEndings: true // Unifier \r\n, \r, \n en \n (activé par défaut)
};
/**
* Logger dans console ET fichier
*/
function log(message, onlyFile = false) {
const line = `${message}\n`;
// Écrire dans le fichier
fs.appendFileSync(LOG_FILE, line, 'utf-8');
// Écrire aussi dans la console (sauf si onlyFile)
if (!onlyFile) {
process.stdout.write(line);
}
}
/**
* Parse un fichier de session pour extraire les tool uses
*/
function parseSessionFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const tools = [];
const jsonBlockRegex = /\[\s*\{[\s\S]*?"type":\s*"tool_use"[\s\S]*?\}\s*\]/g;
const matches = content.match(jsonBlockRegex);
if (!matches) return tools;
for (const match of matches) {
try {
const parsed = JSON.parse(match);
for (const item of parsed) {
if (item.type === 'tool_use' && (item.name === 'Edit' || item.name === 'Write')) {
tools.push({
name: item.name,
input: item.input
});
}
}
} catch (e) {
// Skip invalid JSON
}
}
return tools;
}
/**
* Normaliser un texte complet pour la comparaison
* - Unifie les line endings (\r\n, \r, \n \n)
* - Ignore les différences d'espacement selon config
*/
function normalizeText(text) {
let normalized = text;
// Unifier tous les retours à la ligne si configuré
if (FUZZY_CONFIG.normalizeLineEndings) {
normalized = normalized.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
}
if (FUZZY_CONFIG.ignoreWhitespace) {
// Réduire les espaces/tabs multiples en un seul espace
normalized = normalized.replace(/[ \t]+/g, ' ');
// Nettoyer les espaces en début/fin de chaque ligne
normalized = normalized.split('\n').map(line => line.trim()).join('\n');
}
return normalized;
}
/**
* Normaliser une ligne pour la comparaison
*/
function normalizeLine(line) {
if (FUZZY_CONFIG.ignoreWhitespace) {
// Trim + réduire les espaces multiples à un seul
return line.trim().replace(/\s+/g, ' ');
}
return line;
}
/**
* Calculer la similarité entre deux chaînes (Levenshtein simplifié)
*/
function calculateSimilarity(str1, str2) {
const len1 = str1.length;
const len2 = str2.length;
if (len1 === 0) return len2 === 0 ? 1.0 : 0.0;
if (len2 === 0) return 0.0;
// Matrice de distance
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
for (let i = 1; i <= len1; i++) {
for (let j = 1; j <= len2; j++) {
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
matrix[i][j] = Math.min(
matrix[i - 1][j] + 1, // deletion
matrix[i][j - 1] + 1, // insertion
matrix[i - 1][j - 1] + cost // substitution
);
}
}
const distance = matrix[len1][len2];
const maxLen = Math.max(len1, len2);
return 1 - (distance / maxLen);
}
/**
* Créer un diff détaillé ligne par ligne
*/
function createDiff(oldLines, newLines) {
const diff = [];
const maxLen = Math.max(oldLines.length, newLines.length);
for (let i = 0; i < maxLen; i++) {
const oldLine = oldLines[i];
const newLine = newLines[i];
if (oldLine === undefined) {
// Ligne ajoutée
diff.push({ type: 'add', line: newLine, lineNum: i });
} else if (newLine === undefined) {
// Ligne supprimée
diff.push({ type: 'del', line: oldLine, lineNum: i });
} else if (oldLine !== newLine) {
// Ligne modifiée
diff.push({ type: 'mod', oldLine, newLine, lineNum: i });
} else {
// Ligne identique
diff.push({ type: 'same', line: oldLine, lineNum: i });
}
}
return diff;
}
/**
* Trouver la meilleure position pour un fuzzy match
*/
function findFuzzyMatch(content, oldString) {
// 🔥 CRITICAL FIX: Unifier SEULEMENT line endings (comme dans applyEdit)
// pour que les positions correspondent au même format de texte
// On normalise les espaces SEULEMENT pour la COMPARAISON (ligne par ligne)
const contentWithUnifiedLineEndings = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const oldStringWithUnifiedLineEndings = oldString.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const contentLines = contentWithUnifiedLineEndings.split('\n');
const oldLines = oldStringWithUnifiedLineEndings.split('\n');
if (oldLines.length === 0) return null;
let bestMatch = null;
let bestScore = 0;
// Chercher dans tout le contenu
for (let startLine = 0; startLine <= contentLines.length - oldLines.length; startLine++) {
const candidateLines = contentLines.slice(startLine, startLine + oldLines.length);
// Calculer le score de similarité ligne par ligne
let totalScore = 0;
let matchedLines = 0;
for (let i = 0; i < oldLines.length; i++) {
const oldNorm = normalizeLine(oldLines[i]);
const candidateNorm = normalizeLine(candidateLines[i]);
const similarity = calculateSimilarity(oldNorm, candidateNorm);
totalScore += similarity;
if (similarity > 0.8) matchedLines++;
}
const avgScore = totalScore / oldLines.length;
const matchRatio = matchedLines / oldLines.length;
// Score combiné: moyenne de similarité + ratio de lignes matchées
const combinedScore = (avgScore * 0.7) + (matchRatio * 0.3);
if (combinedScore > bestScore && combinedScore >= FUZZY_CONFIG.minSimilarity) {
bestScore = combinedScore;
bestMatch = {
startLine,
endLine: startLine + oldLines.length,
score: combinedScore,
matchedLines,
totalLines: oldLines.length
};
}
}
return bestMatch;
}
/**
* Appliquer un Edit avec fuzzy matching
*/
function applyEdit(filePath, oldString, newString, dryRun = false) {
try {
if (!fs.existsSync(filePath)) {
log(`⏭️ SKIP Edit - Fichier n'existe pas: ${filePath}`);
return { success: false, reason: 'FILE_NOT_EXIST' };
}
const content = fs.readFileSync(filePath, 'utf-8');
// 🔥 Essayer d'abord un match exact SANS normalisation (le plus rapide et sûr)
if (content.includes(oldString)) {
const newContent = content.replace(oldString, newString);
if (!dryRun) {
fs.writeFileSync(filePath, newContent, 'utf-8');
}
log(`✅ EDIT EXACT appliqué: ${filePath}`);
return { success: true, reason: 'EXACT_MATCH', method: 'exact' };
}
// Si pas de match exact, essayer le fuzzy matching (avec normalisation)
const fuzzyMatch = findFuzzyMatch(content, oldString);
if (fuzzyMatch) {
// 🔥 IMPORTANT: fuzzyMatch a trouvé les positions avec normalisation
// Mais on applique le remplacement sur les versions ORIGINALES (espaces préservés)
// On unifie SEULEMENT les line endings (\r\n → \n) pour que les positions correspondent
// Unifier line endings UNIQUEMENT (garder espaces originaux)
const contentWithUnifiedLineEndings = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const newStringWithUnifiedLineEndings = newString.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
const contentLines = contentWithUnifiedLineEndings.split('\n');
const newLines = newStringWithUnifiedLineEndings.split('\n');
// Capturer les lignes matchées ORIGINALES (AVANT remplacement)
const matchedLines = contentLines.slice(fuzzyMatch.startLine, fuzzyMatch.endLine);
// Remplacer la zone identifiée avec le patch ORIGINAL
const before = contentLines.slice(0, fuzzyMatch.startLine);
const after = contentLines.slice(fuzzyMatch.endLine);
const newContent = [...before, ...newLines, ...after].join('\n');
if (!dryRun) {
fs.writeFileSync(filePath, newContent, 'utf-8');
}
log(`🎯 EDIT FUZZY appliqué: ${filePath} (score: ${(fuzzyMatch.score * 100).toFixed(1)}%, lignes ${fuzzyMatch.startLine}-${fuzzyMatch.endLine})`);
// Créer un diff détaillé
const diff = createDiff(matchedLines, newLines);
log(`┌─ 📝 DIFF DÉTAILLÉ ────────────────────────────────────────────────`);
diff.forEach((item, idx) => {
const lineNum = String(fuzzyMatch.startLine + idx + 1).padStart(4, ' ');
if (item.type === 'same') {
// Ligne identique
log(`${lineNum}${item.line}`);
} else if (item.type === 'add') {
// Ligne ajoutée
log(`${lineNum} │ + ${item.line}`);
} else if (item.type === 'del') {
// Ligne supprimée
log(`${lineNum} │ - ${item.line}`);
} else if (item.type === 'mod') {
// Ligne modifiée - afficher les deux
log(`${lineNum} │ - ${item.oldLine}`);
log(`${lineNum} │ + ${item.newLine}`);
}
});
log(`└────────────────────────────────────────────────────────────────────`);
log('');
return {
success: true,
reason: 'FUZZY_MATCH',
method: 'fuzzy',
score: fuzzyMatch.score,
lines: `${fuzzyMatch.startLine}-${fuzzyMatch.endLine}`
};
}
log(`⏭️ SKIP Edit - Aucun match trouvé: ${filePath}`);
return { success: false, reason: 'NO_MATCH' };
} catch (e) {
log(`❌ ERREUR Edit sur ${filePath}: ${e.message}`);
return { success: false, reason: 'ERROR', error: e.message };
}
}
/**
* Appliquer un Write sur un fichier
*/
function applyWrite(filePath, content, dryRun = false) {
try {
if (fs.existsSync(filePath)) {
log(`⏭️ SKIP Write - Fichier existe déjà: ${filePath}`);
return { success: false, reason: 'FILE_EXISTS' };
}
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
if (!dryRun) {
fs.mkdirSync(dir, { recursive: true });
}
}
if (!dryRun) {
fs.writeFileSync(filePath, content, 'utf-8');
}
log(`✅ WRITE appliqué: ${filePath}`);
return { success: true, reason: 'CREATED' };
} catch (e) {
log(`❌ ERREUR Write sur ${filePath}: ${e.message}`);
return { success: false, reason: 'ERROR', error: e.message };
}
}
/**
* Main
*/
function main() {
// Check for dry-run mode
const dryRun = process.argv.includes('--dry-run');
log(`📝 Logs sauvegardés dans: ${LOG_FILE}`);
log('');
if (dryRun) {
log('🔍 MODE DRY-RUN: Aucun fichier ne sera modifié');
log('');
}
log('🔄 Application des exports Claude avec FUZZY MATCHING...');
log(`⚙️ Config: minSimilarity=${FUZZY_CONFIG.minSimilarity * 100}%, ignoreWhitespace=${FUZZY_CONFIG.ignoreWhitespace}`);
log('');
// Lire tous les fichiers de session
const sessionFiles = fs.readdirSync(EXPORTS_DIR)
.filter(f => f.endsWith('-session.md'))
.sort((a, b) => {
const numA = parseInt(a.split('-')[0]);
const numB = parseInt(b.split('-')[0]);
return numB - numA; // Ordre inverse: 15 -> 1
});
log(`📁 ${sessionFiles.length} fichiers de session trouvés`);
log(`📋 Ordre de traitement: ${sessionFiles.join(', ')}`);
log('');
const stats = {
totalEdits: 0,
totalWrites: 0,
exactMatches: 0,
fuzzyMatches: 0,
successWrites: 0,
skipped: 0,
errors: 0
};
for (const sessionFile of sessionFiles) {
const filePath = path.join(EXPORTS_DIR, sessionFile);
log('');
log(`📄 Traitement de: ${sessionFile}`);
const tools = parseSessionFile(filePath);
log(` ${tools.length} tool use(s) trouvé(s)`);
for (const tool of tools) {
if (tool.name === 'Edit') {
stats.totalEdits++;
const { file_path, old_string, new_string } = tool.input;
const result = applyEdit(file_path, old_string, new_string, dryRun);
if (result.success) {
if (result.method === 'exact') {
stats.exactMatches++;
} else if (result.method === 'fuzzy') {
stats.fuzzyMatches++;
}
} else {
if (result.reason === 'ERROR') {
stats.errors++;
} else {
stats.skipped++;
}
}
} else if (tool.name === 'Write') {
stats.totalWrites++;
const { file_path, content } = tool.input;
const result = applyWrite(file_path, content, dryRun);
if (result.success) {
stats.successWrites++;
} else {
stats.skipped++;
}
}
}
}
log('');
log('');
log('📊 RÉSUMÉ:');
log(` Edit Exact: ${stats.exactMatches}/${stats.totalEdits} appliqués`);
log(` Edit Fuzzy: ${stats.fuzzyMatches}/${stats.totalEdits} appliqués`);
log(` Write: ${stats.successWrites}/${stats.totalWrites} appliqués`);
log(` Skippés: ${stats.skipped}`);
log(` Erreurs: ${stats.errors}`);
log(` Total: ${stats.exactMatches + stats.fuzzyMatches + stats.successWrites}/${stats.totalEdits + stats.totalWrites} opérations réussies`);
log('');
if (dryRun) {
log('💡 Pour appliquer réellement, relancez sans --dry-run');
} else {
log('✨ Terminé!');
}
log('');
log(`📝 Logs complets: ${LOG_FILE}`);
}
main();

View File

@ -0,0 +1,146 @@
#!/usr/bin/env node
const fs = require('fs');
const path = require('path');
const EXPORTS_DIR = path.join(__dirname, '../claude-exports-last-3-days');
/**
* Parse un fichier de session pour extraire les tool uses
*/
function parseSessionFile(filePath) {
const content = fs.readFileSync(filePath, 'utf-8');
const tools = [];
// Chercher tous les blocs JSON qui contiennent des tool uses
const jsonBlockRegex = /\[\s*\{[\s\S]*?"type":\s*"tool_use"[\s\S]*?\}\s*\]/g;
const matches = content.match(jsonBlockRegex);
if (!matches) return tools;
for (const match of matches) {
try {
const parsed = JSON.parse(match);
for (const item of parsed) {
if (item.type === 'tool_use' && (item.name === 'Edit' || item.name === 'Write')) {
tools.push({
name: item.name,
input: item.input
});
}
}
} catch (e) {
// Skip invalid JSON
}
}
return tools;
}
/**
* Applique un Edit sur un fichier
*/
function applyEdit(filePath, oldString, newString) {
try {
if (!fs.existsSync(filePath)) {
console.log(`⏭️ SKIP Edit - Fichier n'existe pas: ${filePath}`);
return false;
}
const content = fs.readFileSync(filePath, 'utf-8');
if (!content.includes(oldString)) {
console.log(`⏭️ SKIP Edit - old_string non trouvée dans: ${filePath}`);
return false;
}
const newContent = content.replace(oldString, newString);
fs.writeFileSync(filePath, newContent, 'utf-8');
console.log(`✅ EDIT appliqué: ${filePath}`);
return true;
} catch (e) {
console.log(`❌ ERREUR Edit sur ${filePath}: ${e.message}`);
return false;
}
}
/**
* Applique un Write sur un fichier
*/
function applyWrite(filePath, content) {
try {
if (fs.existsSync(filePath)) {
console.log(`⏭️ SKIP Write - Fichier existe déjà: ${filePath}`);
return false;
}
// Créer les dossiers parents si nécessaire
const dir = path.dirname(filePath);
if (!fs.existsSync(dir)) {
fs.mkdirSync(dir, { recursive: true });
}
fs.writeFileSync(filePath, content, 'utf-8');
console.log(`✅ WRITE appliqué: ${filePath}`);
return true;
} catch (e) {
console.log(`❌ ERREUR Write sur ${filePath}: ${e.message}`);
return false;
}
}
/**
* Main
*/
function main() {
console.log('🔄 Application des exports Claude...\n');
// Lire tous les fichiers de session
const sessionFiles = fs.readdirSync(EXPORTS_DIR)
.filter(f => f.endsWith('-session.md'))
.sort((a, b) => {
const numA = parseInt(a.split('-')[0]);
const numB = parseInt(b.split('-')[0]);
return numB - numA; // Ordre inverse: 15 -> 1
});
console.log(`📁 ${sessionFiles.length} fichiers de session trouvés`);
console.log(`📋 Ordre de traitement: ${sessionFiles.join(', ')}\n`);
let totalEdits = 0;
let totalWrites = 0;
let successEdits = 0;
let successWrites = 0;
for (const sessionFile of sessionFiles) {
const filePath = path.join(EXPORTS_DIR, sessionFile);
console.log(`\n📄 Traitement de: ${sessionFile}`);
const tools = parseSessionFile(filePath);
console.log(` ${tools.length} tool use(s) trouvé(s)`);
for (const tool of tools) {
if (tool.name === 'Edit') {
totalEdits++;
const { file_path, old_string, new_string } = tool.input;
if (applyEdit(file_path, old_string, new_string)) {
successEdits++;
}
} else if (tool.name === 'Write') {
totalWrites++;
const { file_path, content } = tool.input;
if (applyWrite(file_path, content)) {
successWrites++;
}
}
}
}
console.log('\n\n📊 RÉSUMÉ:');
console.log(` Edit: ${successEdits}/${totalEdits} appliqués`);
console.log(` Write: ${successWrites}/${totalWrites} appliqués`);
console.log(` Total: ${successEdits + successWrites}/${totalEdits + totalWrites} opérations réussies`);
console.log('\n✨ Terminé!');
}
main();

View File

@ -23,6 +23,14 @@ const EXCLUSION_PATTERNS = [
/\.spec\./, // Spec files
/^scripts\//, // Build/deploy scripts
/^docs?\//, // Documentation
/^public\//, // Static files served by Express
/^reports\//, // Generated reports
/^cache\//, // Cache directory
/^configs\//, // Configuration files
/^logs\//, // Log files
/code\.js$/, // Generated bundle
/\.original\./, // Backup/original files
/\.refactored\./, // Refactored versions kept for reference
];
function getEntrypoints() {