diff --git a/CLAUDE.md b/CLAUDE.md index e7c03e8..7952451 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -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 diff --git a/TODO.md b/TODO.md index b1a9fb2..3956dc4 100644 --- a/TODO.md +++ b/TODO.md @@ -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 diff --git a/lib/BrainConfig.js b/lib/BrainConfig.js index ad6328b..93ae382 100644 --- a/lib/BrainConfig.js +++ b/lib/BrainConfig.js @@ -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() || '', diff --git a/lib/ElementExtraction.js b/lib/ElementExtraction.js index 87a1721..6c331cf 100644 --- a/lib/ElementExtraction.js +++ b/lib/ElementExtraction.js @@ -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; } diff --git a/lib/ErrorReporting.js b/lib/ErrorReporting.js index 641f998..367da30 100644 --- a/lib/ErrorReporting.js +++ b/lib/ErrorReporting.js @@ -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(); diff --git a/lib/LLMManager.js b/lib/LLMManager.js index 55c99d2..6f3baba 100644 --- a/lib/LLMManager.js +++ b/lib/LLMManager.js @@ -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, diff --git a/lib/Main.js b/lib/Main.js index 64f3cd1..7b7df7b 100644 --- a/lib/Main.js +++ b/lib/Main.js @@ -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 } }; } diff --git a/lib/StepExecutor.js b/lib/StepExecutor.js index 177a335..82e3a89 100644 --- a/lib/StepExecutor.js +++ b/lib/StepExecutor.js @@ -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'); diff --git a/lib/adversarial-generation/AdversarialCore.js b/lib/adversarial-generation/AdversarialCore.js index 2d845ef..0ade63c 100644 --- a/lib/adversarial-generation/AdversarialCore.js +++ b/lib/adversarial-generation/AdversarialCore.js @@ -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); diff --git a/lib/adversarial-generation/AdversarialLayers.js b/lib/adversarial-generation/AdversarialLayers.js index e8435df..b2ff5cd 100644 --- a/lib/adversarial-generation/AdversarialLayers.js +++ b/lib/adversarial-generation/AdversarialLayers.js @@ -159,6 +159,7 @@ async function applyLayerPipeline(content, layers = [], globalOptions = {}) { return { content: currentContent, stats: pipelineStats, + modifications: pipelineStats.totalModifications, // ✅ AJOUTÉ: Mapping pour PipelineExecutor original: content }; diff --git a/lib/human-simulation/HumanSimulationCore.js b/lib/human-simulation/HumanSimulationCore.js index 9fda112..b4d5be8 100644 --- a/lib/human-simulation/HumanSimulationCore.js +++ b/lib/human-simulation/HumanSimulationCore.js @@ -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 diff --git a/lib/human-simulation/HumanSimulationLayers.js b/lib/human-simulation/HumanSimulationLayers.js index d358156..8497c84 100644 --- a/lib/human-simulation/HumanSimulationLayers.js +++ b/lib/human-simulation/HumanSimulationLayers.js @@ -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 } }; diff --git a/lib/modes/ManualServer.js b/lib/modes/ManualServer.js index 7402c0c..75727d5 100644 --- a/lib/modes/ManualServer.js +++ b/lib/modes/ManualServer.js @@ -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 {

📊 Monitoring & API

Endpoints disponibles en mode MANUAL.

+ 🤖 LLM Monitoring 📊 Status API 📈 Statistiques diff --git a/lib/modes/ModeManager.js b/lib/modes/ModeManager.js index a22922c..f34a357 100644 --- a/lib/modes/ModeManager.js +++ b/lib/modes/ModeManager.js @@ -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'); } /** diff --git a/lib/pattern-breaking/PatternBreakingCore.js b/lib/pattern-breaking/PatternBreakingCore.js index ddb36c6..0cf4ad6 100644 --- a/lib/pattern-breaking/PatternBreakingCore.js +++ b/lib/pattern-breaking/PatternBreakingCore.js @@ -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 }; diff --git a/lib/pattern-breaking/PatternBreakingLayers.js b/lib/pattern-breaking/PatternBreakingLayers.js index 3c4540b..57bee14 100644 --- a/lib/pattern-breaking/PatternBreakingLayers.js +++ b/lib/pattern-breaking/PatternBreakingLayers.js @@ -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 }; diff --git a/lib/pipeline/PipelineDefinition.js b/lib/pipeline/PipelineDefinition.js index 92b6b6a..30901c9 100644 --- a/lib/pipeline/PipelineDefinition.js +++ b/lib/pipeline/PipelineDefinition.js @@ -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' } } } }; diff --git a/lib/selective-enhancement/StyleLayer.js b/lib/selective-enhancement/StyleLayer.js index 9ac1821..54055a5 100644 --- a/lib/selective-enhancement/StyleLayer.js +++ b/lib/selective-enhancement/StyleLayer.js @@ -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; diff --git a/lib/selective-enhancement/TechnicalLayer.js b/lib/selective-enhancement/TechnicalLayer.js index 386cf79..e84aaf5 100644 --- a/lib/selective-enhancement/TechnicalLayer.js +++ b/lib/selective-enhancement/TechnicalLayer.js @@ -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 } diff --git a/public/index.html b/public/index.html index 2662683..1e3433d 100644 --- a/public/index.html +++ b/public/index.html @@ -217,33 +217,7 @@
- -
-
🔧
-

Éditeur de Configuration

-

⚠️ ANCIEN SYSTÈME - Désactivé

-
    -
  • 4 couches modulaires configurables
  • -
  • Save/Load des configurations
  • -
  • Test en direct avec logs temps réel
  • -
  • Preview JSON de la configuration
  • -
-
- - -
-
🚀
-

Runner de Production

-

⚠️ ANCIEN SYSTÈME - Désactivé

-
    -
  • Load configuration sauvegardée
  • -
  • Sélection ligne Google Sheets
  • -
  • Logs temps réel pendant l'exécution
  • -
  • Résultats et lien direct vers GSheets
  • -
-
- - +
🎨

Pipeline Builder

@@ -256,7 +230,7 @@
- +

Pipeline Runner

@@ -268,6 +242,19 @@
  • Logs d'exécution complets
  • + + +
    +
    🤖
    +

    LLM Monitoring

    +

    Surveiller la santé et les performances de vos modèles LLM

    +
      +
    • Status en temps réel (9 LLMs)
    • +
    • Latences moyennes et barres de progression
    • +
    • Crédits restants par plateforme
    • +
    • Auto-refresh toutes les 30 secondes
    • +
    +
    diff --git a/public/pipeline-builder.html b/public/pipeline-builder.html index ae9744e..3400e06 100644 --- a/public/pipeline-builder.html +++ b/public/pipeline-builder.html @@ -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; + } } diff --git a/public/pipeline-builder.js b/public/pipeline-builder.js index 4f82284..f2dbf9e 100644 --- a/public/pipeline-builder.js +++ b/public/pipeline-builder.js @@ -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' } ]; } } diff --git a/public/pipeline-runner.html b/public/pipeline-runner.html index ec6e8cd..4136034 100644 --- a/public/pipeline-runner.html +++ b/public/pipeline-runner.html @@ -345,6 +345,23 @@
    +

    📄 Contenu Final Généré

    +
    +

    Le contenu apparaîtra ici après l'exécution

    +
    + +

    📦 Versions Sauvegardées

    +
    +

    Aucune version disponible

    +
    + + +

    📝 Log d'Exécution

    diff --git a/public/pipeline-runner.js b/public/pipeline-runner.js index bcc412f..d300680 100644 --- a/public/pipeline-runner.js +++ b/public/pipeline-runner.js @@ -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 = `⚠️ Génération incomplète: ${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 = `Métadonnées: ${contentLength} caractères, ~${wordCount} mots`; + finalContentContainer.appendChild(metaDiv); + } + } else { + finalContentContainer.innerHTML = '

    ⚠️ Aucun contenu final disponible dans le résultat

    '; + } + + // 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 = '

    Aucune version sauvegardée

    '; + } + + // 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 = ''; diff --git a/server.js b/server.js index 0bc39f5..6eb7d7c 100644 --- a/server.js +++ b/server.js @@ -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); }); } diff --git a/tests/validators/AntiDetectionValidator.js b/tests/validators/AntiDetectionValidator.js index 570a587..b4b3146 100644 --- a/tests/validators/AntiDetectionValidator.js +++ b/tests/validators/AntiDetectionValidator.js @@ -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 }); diff --git a/tests/validators/PersonalityValidator.js b/tests/validators/PersonalityValidator.js index a7208c4..fd136b3 100644 --- a/tests/validators/PersonalityValidator.js +++ b/tests/validators/PersonalityValidator.js @@ -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 }); diff --git a/tests/validators/QualityMetrics.js b/tests/validators/QualityMetrics.js index 9c84a89..3b24d13 100644 --- a/tests/validators/QualityMetrics.js +++ b/tests/validators/QualityMetrics.js @@ -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 }); diff --git a/tools/analyze-skipped-exports.js b/tools/analyze-skipped-exports.js new file mode 100644 index 0000000..73577ae --- /dev/null +++ b/tools/analyze-skipped-exports.js @@ -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(); diff --git a/tools/apply-claude-exports-fuzzy.js b/tools/apply-claude-exports-fuzzy.js new file mode 100644 index 0000000..97bb09d --- /dev/null +++ b/tools/apply-claude-exports-fuzzy.js @@ -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(); diff --git a/tools/apply-claude-exports.js b/tools/apply-claude-exports.js new file mode 100644 index 0000000..2b01fe4 --- /dev/null +++ b/tools/apply-claude-exports.js @@ -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(); diff --git a/tools/audit-unused.cjs b/tools/audit-unused.cjs index b557a6c..18f0642 100644 --- a/tools/audit-unused.cjs +++ b/tools/audit-unused.cjs @@ -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() {