refactor: Synchronisation complète du codebase - Application de tous les patches
Application systématique et méthodique de tous les patches historiques. ## ✅ FICHIERS SYNCHRONISÉS (19 fichiers) ### Core & Infrastructure: - server.js (14 patches) - Lazy loading ModeManager, SIGINT hard kill, timing logs - ModeManager.js (4 patches) - Instrumentation complète avec timing détaillé ### Pipeline System: - PipelineDefinition.js (6 patches) - Source unique getLLMProvidersList() - pipeline-builder.js (8 patches) - Standardisation LLM providers - pipeline-runner.js (6 patches) - Affichage résultats structurés + debug console - pipeline-builder.html (2 patches) - Fallback providers synchronisés - pipeline-runner.html (3 patches) - UI améliorée résultats ### Enhancement Layers: - TechnicalLayer.js (1 patch) - defaultLLM: 'gpt-4o-mini' - StyleLayer.js (1 patch) - Type safety vocabulairePref - PatternBreakingCore.js (1 patch) - Mapping modifications - PatternBreakingLayers.js (1 patch) - LLM standardisé ### Validators & Tests: - QualityMetrics.js (1 patch) - callLLM('gpt-4o-mini') - PersonalityValidator.js (1 patch) - Provider gpt-4o-mini - AntiDetectionValidator.js - Synchronisé ### Documentation: - TODO.md (1 patch) - Section LiteLLM pour tracking coûts - CLAUDE.md - Documentation à jour ### Tools: - tools/analyze-skipped-exports.js (nouveau) - tools/apply-claude-exports.js (nouveau) - tools/apply-claude-exports-fuzzy.js (nouveau) ## 🎯 Changements principaux: - ✅ Standardisation LLM providers (openai → gpt-4o-mini, claude → claude-sonnet-4-5) - ✅ Lazy loading optimisé (ModeManager chargé à la demande) - ✅ SIGINT immediate exit (pas de graceful shutdown) - ✅ Type safety renforcé (conversions string explicites) - ✅ Instrumentation timing complète - ✅ Debug logging amélioré (console.log résultats pipeline) 🤖 Generated with [Claude Code](https://claude.com/claude-code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
parent
cd79ca9a4a
commit
64fb319e65
@ -339,7 +339,7 @@ Voir `API.md` pour documentation complète avec exemples.
|
||||
**Assets**: `public/` (11 web interfaces), `configs/` (saved configs/pipelines), `tools/` (logViewer, bundler, audit), `tests/` (comprehensive test suite), `.env` (credentials)
|
||||
|
||||
## Dependencies & Workflow Sources
|
||||
**Deps**: googleapis, axios, dotenv, express, nodemailer
|
||||
**Deps**: google-spreadsheet, google-auth-library, axios, dotenv, express, nodemailer
|
||||
**Sources**: production (Google Sheets), test_random_personality, node_server
|
||||
|
||||
## Git Push Configuration
|
||||
|
||||
62
TODO.md
62
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
|
||||
|
||||
@ -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() || '',
|
||||
|
||||
@ -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;
|
||||
}
|
||||
|
||||
@ -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();
|
||||
|
||||
|
||||
@ -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,
|
||||
|
||||
11
lib/Main.js
11
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
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
@ -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');
|
||||
|
||||
@ -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);
|
||||
|
||||
@ -159,6 +159,7 @@ async function applyLayerPipeline(content, layers = [], globalOptions = {}) {
|
||||
return {
|
||||
content: currentContent,
|
||||
stats: pipelineStats,
|
||||
modifications: pipelineStats.totalModifications, // ✅ AJOUTÉ: Mapping pour PipelineExecutor
|
||||
original: content
|
||||
};
|
||||
|
||||
|
||||
@ -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
|
||||
|
||||
@ -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 }
|
||||
};
|
||||
|
||||
@ -4,15 +4,43 @@
|
||||
// FONCTIONNALITÉS: Dashboard, tests modulaires, API complète
|
||||
// ========================================
|
||||
|
||||
const express = require('express');
|
||||
const cors = require('cors');
|
||||
const path = require('path');
|
||||
const WebSocket = require('ws');
|
||||
// ⏱️ Timing chargement modules (logs avant/après chaque require)
|
||||
const _t0 = Date.now();
|
||||
console.log(`[${new Date().toISOString()}] ⏱️ [require] Début chargement ManualServer modules...`);
|
||||
|
||||
const _t1 = Date.now();
|
||||
const express = require('express');
|
||||
console.log(`[${new Date().toISOString()}] ✓ [require] express en ${Date.now() - _t1}ms`);
|
||||
|
||||
const _t2 = Date.now();
|
||||
const cors = require('cors');
|
||||
console.log(`[${new Date().toISOString()}] ✓ [require] cors en ${Date.now() - _t2}ms`);
|
||||
|
||||
const _t3 = Date.now();
|
||||
const path = require('path');
|
||||
console.log(`[${new Date().toISOString()}] ✓ [require] path en ${Date.now() - _t3}ms`);
|
||||
|
||||
const _t4 = Date.now();
|
||||
const WebSocket = require('ws');
|
||||
console.log(`[${new Date().toISOString()}] ✓ [require] ws (WebSocket) en ${Date.now() - _t4}ms`);
|
||||
|
||||
const _t5 = Date.now();
|
||||
const { logSh } = require('../ErrorReporting');
|
||||
console.log(`[${new Date().toISOString()}] ✓ [require] ErrorReporting en ${Date.now() - _t5}ms`);
|
||||
|
||||
const _t6 = Date.now();
|
||||
const { handleModularWorkflow, benchmarkStacks } = require('../Main');
|
||||
console.log(`[${new Date().toISOString()}] ✓ [require] Main en ${Date.now() - _t6}ms`);
|
||||
|
||||
const _t7 = Date.now();
|
||||
const { APIController } = require('../APIController');
|
||||
console.log(`[${new Date().toISOString()}] ✓ [require] APIController en ${Date.now() - _t7}ms`);
|
||||
|
||||
const _t8 = Date.now();
|
||||
const { BatchController } = require('../batch/BatchController');
|
||||
console.log(`[${new Date().toISOString()}] ✓ [require] BatchController en ${Date.now() - _t8}ms`);
|
||||
|
||||
console.log(`[${new Date().toISOString()}] ✅ [require] TOTAL ManualServer modules chargés en ${Date.now() - _t0}ms`);
|
||||
|
||||
/**
|
||||
* SERVEUR MODE MANUAL
|
||||
@ -43,6 +71,10 @@ class ManualServer {
|
||||
this.isRunning = false;
|
||||
this.apiController = new APIController();
|
||||
this.batchController = new BatchController();
|
||||
|
||||
// Cache pour status LLMs (évite d'appeler trop souvent)
|
||||
this.llmStatusCache = null;
|
||||
this.llmStatusCacheTime = null;
|
||||
}
|
||||
|
||||
// ========================================
|
||||
@ -57,32 +89,57 @@ class ManualServer {
|
||||
logSh('⚠️ ManualServer déjà en cours d\'exécution', 'WARNING');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
const startTime = Date.now();
|
||||
logSh('🎯 Démarrage ManualServer...', 'INFO');
|
||||
|
||||
|
||||
try {
|
||||
// 1. Configuration Express
|
||||
logSh('⏱️ [1/7] Configuration Express...', 'INFO');
|
||||
const t1 = Date.now();
|
||||
await this.setupExpressApp();
|
||||
|
||||
logSh(`✓ Express configuré en ${Date.now() - t1}ms`, 'INFO');
|
||||
|
||||
// 2. Routes API
|
||||
logSh('⏱️ [2/7] Configuration routes API...', 'INFO');
|
||||
const t2 = Date.now();
|
||||
this.setupAPIRoutes();
|
||||
|
||||
logSh(`✓ Routes API configurées en ${Date.now() - t2}ms`, 'INFO');
|
||||
|
||||
// 3. Interface Web
|
||||
logSh('⏱️ [3/7] Configuration interface web...', 'INFO');
|
||||
const t3 = Date.now();
|
||||
this.setupWebInterface();
|
||||
|
||||
logSh(`✓ Interface web configurée en ${Date.now() - t3}ms`, 'INFO');
|
||||
|
||||
// 4. WebSocket pour logs temps réel
|
||||
logSh('⏱️ [4/7] Démarrage WebSocket serveur...', 'INFO');
|
||||
const t4 = Date.now();
|
||||
await this.setupWebSocketServer();
|
||||
|
||||
logSh(`✓ WebSocket démarré en ${Date.now() - t4}ms`, 'INFO');
|
||||
|
||||
// 5. Démarrage serveur HTTP
|
||||
logSh('⏱️ [5/7] Démarrage serveur HTTP...', 'INFO');
|
||||
const t5 = Date.now();
|
||||
await this.startHTTPServer();
|
||||
|
||||
logSh(`✓ Serveur HTTP démarré en ${Date.now() - t5}ms`, 'INFO');
|
||||
|
||||
// 6. Monitoring
|
||||
logSh('⏱️ [6/7] Démarrage monitoring...', 'INFO');
|
||||
const t6 = Date.now();
|
||||
this.startMonitoring();
|
||||
|
||||
logSh(`✓ Monitoring démarré en ${Date.now() - t6}ms`, 'INFO');
|
||||
|
||||
// 7. Initialisation status LLMs au démarrage (en background)
|
||||
logSh('⏱️ [7/7] Initialisation LLM status (background)...', 'INFO');
|
||||
const t7 = Date.now();
|
||||
this.initializeLLMStatus();
|
||||
logSh(`✓ LLM init lancé en ${Date.now() - t7}ms`, 'INFO');
|
||||
|
||||
this.isRunning = true;
|
||||
this.stats.startTime = Date.now();
|
||||
|
||||
logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port}`, 'INFO');
|
||||
|
||||
logSh(`✅ ManualServer démarré sur http://localhost:${this.config.port} (total: ${Date.now() - startTime}ms)`, 'INFO');
|
||||
logSh(`📡 WebSocket logs sur ws://localhost:${this.config.wsPort}`, 'INFO');
|
||||
|
||||
} catch (error) {
|
||||
@ -97,19 +154,31 @@ class ManualServer {
|
||||
*/
|
||||
async stop() {
|
||||
if (!this.isRunning) return;
|
||||
|
||||
|
||||
logSh('🛑 Arrêt ManualServer...', 'INFO');
|
||||
|
||||
|
||||
try {
|
||||
// Arrêter le monitoring
|
||||
if (this.monitorInterval) {
|
||||
clearInterval(this.monitorInterval);
|
||||
this.monitorInterval = null;
|
||||
}
|
||||
|
||||
// Arrêter le refresh status LLMs
|
||||
if (this.llmStatusInterval) {
|
||||
clearInterval(this.llmStatusInterval);
|
||||
this.llmStatusInterval = null;
|
||||
}
|
||||
|
||||
// Déconnecter tous les clients WebSocket
|
||||
this.disconnectAllClients();
|
||||
|
||||
|
||||
// Arrêter WebSocket server
|
||||
if (this.wsServer) {
|
||||
this.wsServer.close();
|
||||
this.wsServer = null;
|
||||
}
|
||||
|
||||
|
||||
// Arrêter serveur HTTP
|
||||
if (this.server) {
|
||||
await new Promise((resolve) => {
|
||||
@ -117,11 +186,11 @@ class ManualServer {
|
||||
});
|
||||
this.server = null;
|
||||
}
|
||||
|
||||
|
||||
this.isRunning = false;
|
||||
|
||||
|
||||
logSh('✅ ManualServer arrêté', 'INFO');
|
||||
|
||||
|
||||
} catch (error) {
|
||||
logSh(`⚠️ Erreur arrêt ManualServer: ${error.message}`, 'WARNING');
|
||||
}
|
||||
@ -823,6 +892,11 @@ class ManualServer {
|
||||
await this.apiController.getMetrics(req, res);
|
||||
});
|
||||
|
||||
// === LLM MONITORING API ===
|
||||
this.app.get('/api/llm/status', async (req, res) => {
|
||||
await this.handleLLMStatus(req, res);
|
||||
});
|
||||
|
||||
// === PROMPT ENGINE API ===
|
||||
this.app.post('/api/generate-prompt', async (req, res) => {
|
||||
await this.apiController.generatePrompt(req, res);
|
||||
@ -1483,6 +1557,187 @@ class ManualServer {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Initialise le status LLMs au démarrage (en background)
|
||||
* ⚡ OPTIMISÉ: Mode rapide au démarrage, complet après 30s
|
||||
*/
|
||||
async initializeLLMStatus() {
|
||||
logSh('🚀 Initialisation status LLMs (mode rapide)...', 'DEBUG');
|
||||
|
||||
// ⚡ Phase 1: Vérification RAPIDE des clés API (immédiat, sans réseau)
|
||||
setImmediate(async () => {
|
||||
try {
|
||||
await this.refreshLLMStatus(true); // quickMode = true
|
||||
logSh('✅ Status LLMs (rapide) initialisé - vérification clés API OK', 'INFO');
|
||||
|
||||
// ⚡ Phase 2: Test COMPLET avec appels réseau après 30 secondes
|
||||
setTimeout(async () => {
|
||||
try {
|
||||
logSh('🔄 Démarrage vérification complète status LLMs (avec tests réseau)...', 'DEBUG');
|
||||
await this.refreshLLMStatus(false); // quickMode = false
|
||||
logSh('✅ Status LLMs (complet) mis à jour avec tests réseau', 'INFO');
|
||||
} catch (error) {
|
||||
logSh(`⚠️ Erreur vérification complète LLMs: ${error.message}`, 'WARNING');
|
||||
}
|
||||
}, 30000); // Attendre 30 secondes avant le test complet
|
||||
|
||||
} catch (error) {
|
||||
logSh(`⚠️ Erreur initialisation rapide status LLMs: ${error.message}`, 'WARNING');
|
||||
}
|
||||
});
|
||||
|
||||
// Rafraîchir toutes les 30 minutes (1800000ms) en mode complet
|
||||
this.llmStatusInterval = setInterval(async () => {
|
||||
try {
|
||||
await this.refreshLLMStatus(false); // Mode complet pour les refreshs périodiques
|
||||
} catch (error) {
|
||||
logSh(`⚠️ Erreur refresh status LLMs: ${error.message}`, 'WARNING');
|
||||
}
|
||||
}, 1800000);
|
||||
}
|
||||
|
||||
/**
|
||||
* Rafraîchit le cache du status LLMs
|
||||
* ⚡ OPTIMISÉ: Test rapide sans appels LLM réels au démarrage
|
||||
*/
|
||||
async refreshLLMStatus(quickMode = false) {
|
||||
const { getLLMProvidersList } = require('../LLMManager');
|
||||
|
||||
logSh(`📊 Récupération status LLMs${quickMode ? ' (mode rapide)' : ''}...`, 'DEBUG');
|
||||
|
||||
const providers = getLLMProvidersList();
|
||||
const providersWithStatus = [];
|
||||
|
||||
if (quickMode) {
|
||||
// ⚡ MODE RAPIDE: Vérifier juste les clés API sans appels réseau
|
||||
for (const provider of providers) {
|
||||
const hasApiKey = this.checkProviderApiKey(provider.id);
|
||||
|
||||
providersWithStatus.push({
|
||||
...provider,
|
||||
status: hasApiKey ? 'unknown' : 'no_key',
|
||||
latency: null,
|
||||
lastTest: null,
|
||||
credits: 'unlimited',
|
||||
calls: 0,
|
||||
successRate: hasApiKey ? null : 0,
|
||||
quickMode: true
|
||||
});
|
||||
}
|
||||
|
||||
logSh('⚡ Status rapide LLMs (sans appels réseau) - vérification clés API uniquement', 'DEBUG');
|
||||
} else {
|
||||
// MODE COMPLET: Test réseau réel
|
||||
for (const provider of providers) {
|
||||
const startTime = Date.now();
|
||||
let status = 'offline';
|
||||
let latency = null;
|
||||
let lastTest = null;
|
||||
|
||||
try {
|
||||
const { callLLM } = require('../LLMManager');
|
||||
await callLLM(provider.id, 'Test ping', { maxTokens: 10 });
|
||||
|
||||
latency = Date.now() - startTime;
|
||||
status = 'online';
|
||||
lastTest = new Date().toLocaleTimeString('fr-FR');
|
||||
} catch (error) {
|
||||
logSh(`⚠️ Provider ${provider.id} offline: ${error.message}`, 'DEBUG');
|
||||
status = 'offline';
|
||||
}
|
||||
|
||||
providersWithStatus.push({
|
||||
...provider,
|
||||
status,
|
||||
latency,
|
||||
lastTest,
|
||||
credits: 'unlimited',
|
||||
calls: 0,
|
||||
successRate: status === 'online' ? 100 : 0
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
// Mettre à jour le cache
|
||||
this.llmStatusCache = {
|
||||
success: true,
|
||||
providers: providersWithStatus,
|
||||
summary: {
|
||||
total: providersWithStatus.length,
|
||||
online: providersWithStatus.filter(p => p.status === 'online').length,
|
||||
offline: providersWithStatus.filter(p => p.status === 'offline').length,
|
||||
unknown: providersWithStatus.filter(p => p.status === 'unknown').length,
|
||||
no_key: providersWithStatus.filter(p => p.status === 'no_key').length
|
||||
},
|
||||
quickMode: quickMode,
|
||||
timestamp: new Date().toISOString()
|
||||
};
|
||||
this.llmStatusCacheTime = Date.now();
|
||||
|
||||
if (quickMode) {
|
||||
logSh(`⚡ Status LLMs (rapide): ${this.llmStatusCache.summary.unknown} providers avec clés, ${this.llmStatusCache.summary.no_key} sans clés`, 'INFO');
|
||||
} else {
|
||||
logSh(`✅ Status LLMs (complet): ${this.llmStatusCache.summary.online}/${this.llmStatusCache.summary.total} online`, 'INFO');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Vérifie si un provider a une clé API configurée
|
||||
*/
|
||||
checkProviderApiKey(providerId) {
|
||||
const keyMap = {
|
||||
'claude-sonnet-4-5': 'ANTHROPIC_API_KEY',
|
||||
'claude-3-5-sonnet-20241022': 'ANTHROPIC_API_KEY',
|
||||
'gpt-4o': 'OPENAI_API_KEY',
|
||||
'gpt-4o-mini': 'OPENAI_API_KEY',
|
||||
'gemini-2-0-flash-exp': 'GOOGLE_API_KEY',
|
||||
'gemini-pro': 'GOOGLE_API_KEY',
|
||||
'deepseek-chat': 'DEEPSEEK_API_KEY',
|
||||
'moonshot-v1-8k': 'MOONSHOT_API_KEY',
|
||||
'mistral-small-latest': 'MISTRAL_API_KEY'
|
||||
};
|
||||
|
||||
const envKey = keyMap[providerId];
|
||||
if (!envKey) return false;
|
||||
|
||||
const apiKey = process.env[envKey];
|
||||
return apiKey && apiKey.length > 10;
|
||||
}
|
||||
|
||||
/**
|
||||
* Handler pour status et monitoring des LLMs
|
||||
*/
|
||||
async handleLLMStatus(req, res) {
|
||||
try {
|
||||
// Si on a un cache, le retourner directement
|
||||
if (this.llmStatusCache) {
|
||||
res.json(this.llmStatusCache);
|
||||
} else {
|
||||
// Pas encore de cache, retourner une réponse vide
|
||||
res.json({
|
||||
success: true,
|
||||
providers: [],
|
||||
summary: {
|
||||
total: 0,
|
||||
online: 0,
|
||||
offline: 0
|
||||
},
|
||||
timestamp: new Date().toISOString(),
|
||||
message: 'Status LLMs en cours de chargement...'
|
||||
});
|
||||
}
|
||||
|
||||
} catch (error) {
|
||||
logSh(`❌ Erreur status LLMs: ${error.message}`, 'ERROR');
|
||||
res.status(500).json({
|
||||
success: false,
|
||||
error: 'Erreur récupération status LLMs',
|
||||
message: error.message,
|
||||
timestamp: new Date().toISOString()
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 🆕 Handler pour génération simple d'article avec mot-clé
|
||||
*/
|
||||
@ -1700,6 +1955,7 @@ class ManualServer {
|
||||
<div class="section">
|
||||
<h2>📊 Monitoring & API</h2>
|
||||
<p>Endpoints disponibles en mode MANUAL.</p>
|
||||
<a href="/llm-monitoring.html" target="_blank" class="button warning">🤖 LLM Monitoring</a>
|
||||
<a href="/api/status" target="_blank" class="button">📊 Status API</a>
|
||||
<a href="/api/stats" target="_blank" class="button">📈 Statistiques</a>
|
||||
<button onclick="testConnection()" class="button success">🔍 Test Connexion</button>
|
||||
|
||||
@ -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');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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
|
||||
};
|
||||
|
||||
@ -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' }
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
@ -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;
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
|
||||
@ -217,33 +217,7 @@
|
||||
|
||||
<main>
|
||||
<div class="card-container">
|
||||
<!-- Card 1: Configuration Editor (DÉSACTIVÉ - ancien système) -->
|
||||
<div class="card" style="opacity: 0.5; cursor: not-allowed;" onclick="alert('⚠️ Ancien système désactivé. Utilisez Pipeline Builder à la place.')">
|
||||
<div class="card-icon">🔧</div>
|
||||
<h2>Éditeur de Configuration</h2>
|
||||
<p style="color: var(--warning);">⚠️ ANCIEN SYSTÈME - Désactivé</p>
|
||||
<ul>
|
||||
<li>4 couches modulaires configurables</li>
|
||||
<li>Save/Load des configurations</li>
|
||||
<li>Test en direct avec logs temps réel</li>
|
||||
<li>Preview JSON de la configuration</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Card 2: Production Runner (DÉSACTIVÉ - ancien système) -->
|
||||
<div class="card" style="opacity: 0.5; cursor: not-allowed;" onclick="alert('⚠️ Ancien système désactivé. Utilisez Pipeline Runner à la place.')">
|
||||
<div class="card-icon">🚀</div>
|
||||
<h2>Runner de Production</h2>
|
||||
<p style="color: var(--warning);">⚠️ ANCIEN SYSTÈME - Désactivé</p>
|
||||
<ul>
|
||||
<li>Load configuration sauvegardée</li>
|
||||
<li>Sélection ligne Google Sheets</li>
|
||||
<li>Logs temps réel pendant l'exécution</li>
|
||||
<li>Résultats et lien direct vers GSheets</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: Pipeline Builder -->
|
||||
<!-- Card 1: Pipeline Builder -->
|
||||
<div class="card" onclick="navigateTo('pipeline-builder.html')">
|
||||
<div class="card-icon">🎨</div>
|
||||
<h2>Pipeline Builder</h2>
|
||||
@ -256,7 +230,7 @@
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Card 4: Pipeline Runner -->
|
||||
<!-- Card 2: Pipeline Runner -->
|
||||
<div class="card" onclick="navigateTo('pipeline-runner.html')">
|
||||
<div class="card-icon">⚡</div>
|
||||
<h2>Pipeline Runner</h2>
|
||||
@ -268,6 +242,19 @@
|
||||
<li>Logs d'exécution complets</li>
|
||||
</ul>
|
||||
</div>
|
||||
|
||||
<!-- Card 3: LLM Monitoring -->
|
||||
<div class="card" onclick="navigateTo('llm-monitoring.html')">
|
||||
<div class="card-icon">🤖</div>
|
||||
<h2>LLM Monitoring</h2>
|
||||
<p>Surveiller la santé et les performances de vos modèles LLM</p>
|
||||
<ul>
|
||||
<li>Status en temps réel (9 LLMs)</li>
|
||||
<li>Latences moyennes et barres de progression</li>
|
||||
<li>Crédits restants par plateforme</li>
|
||||
<li>Auto-refresh toutes les 30 secondes</li>
|
||||
</ul>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="stats-panel">
|
||||
|
||||
@ -399,10 +399,38 @@
|
||||
white-space: pre-wrap;
|
||||
}
|
||||
|
||||
@media (max-width: 1400px) {
|
||||
/* Responsive Layouts */
|
||||
@media (max-width: 1400px) and (min-width: 900px) {
|
||||
.builder-layout {
|
||||
grid-template-columns: 280px 1fr;
|
||||
grid-template-rows: auto;
|
||||
}
|
||||
|
||||
.modules-palette {
|
||||
grid-row: 1 / 3;
|
||||
height: calc(100vh - 180px);
|
||||
}
|
||||
|
||||
.side-panel {
|
||||
height: auto;
|
||||
max-height: none;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 899px) {
|
||||
.builder-layout {
|
||||
grid-template-columns: 1fr;
|
||||
}
|
||||
|
||||
.modules-palette,
|
||||
.side-panel {
|
||||
height: auto;
|
||||
max-height: 400px;
|
||||
}
|
||||
|
||||
.panel {
|
||||
padding: 15px;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
@ -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' }
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@ -345,6 +345,23 @@
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 25px; margin-bottom: 10px;">📄 Contenu Final Généré</h3>
|
||||
<div id="finalContentContainer" style="background: var(--bg-light); padding: 15px; border-radius: 8px; margin-bottom: 20px; max-height: 400px; overflow-y: auto;">
|
||||
<p style="color: var(--text-light);">Le contenu apparaîtra ici après l'exécution</p>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 25px; margin-bottom: 10px;">📦 Versions Sauvegardées</h3>
|
||||
<div id="versionHistory" style="background: var(--bg-light); padding: 15px; border-radius: 8px; margin-bottom: 20px;">
|
||||
<p style="color: var(--text-light);">Aucune version disponible</p>
|
||||
</div>
|
||||
|
||||
<div id="gsheetsLinkContainer" style="display: none; margin-bottom: 20px;">
|
||||
<a id="gsheetsLink" href="#" target="_blank"
|
||||
style="display: inline-block; padding: 12px 20px; background: linear-gradient(135deg, #34d399, #10b981); color: white; text-decoration: none; border-radius: 8px; font-weight: 600; transition: all 0.2s;">
|
||||
📊 Ouvrir dans Google Sheets
|
||||
</a>
|
||||
</div>
|
||||
|
||||
<h3 style="margin-top: 25px; margin-bottom: 10px;">📝 Log d'Exécution</h3>
|
||||
<div class="execution-log" id="executionLog"></div>
|
||||
</div>
|
||||
|
||||
@ -182,6 +182,14 @@ function displayResults(result) {
|
||||
const resultsSection = document.getElementById('resultsSection');
|
||||
resultsSection.style.display = 'block';
|
||||
|
||||
// Log complet des résultats dans la console
|
||||
console.log('=== RÉSULTAT PIPELINE ===');
|
||||
console.log('Contenu final:', result.finalContent || result.content);
|
||||
console.log('Stats:', result.stats);
|
||||
console.log('Version history:', result.versionHistory);
|
||||
console.log('Résultat complet:', result);
|
||||
console.log('========================');
|
||||
|
||||
// Stats
|
||||
document.getElementById('statDuration').textContent =
|
||||
`${result.stats.totalDuration}ms`;
|
||||
@ -192,6 +200,130 @@ function displayResults(result) {
|
||||
document.getElementById('statPersonality').textContent =
|
||||
result.stats.personality || 'N/A';
|
||||
|
||||
// Final Content Display
|
||||
const finalContentContainer = document.getElementById('finalContentContainer');
|
||||
let rawContent = result.finalContent || result.content || result.organicContent;
|
||||
|
||||
// Extraire le texte si c'est un objet
|
||||
let finalContent;
|
||||
let isStructuredContent = false;
|
||||
|
||||
if (typeof rawContent === 'string') {
|
||||
finalContent = rawContent;
|
||||
} else if (rawContent && typeof rawContent === 'object') {
|
||||
// Vérifier si c'est un contenu structuré (H2_1, H3_2, etc.)
|
||||
const keys = Object.keys(rawContent);
|
||||
if (keys.some(k => k.match(/^(H2|H3|P)_\d+$/))) {
|
||||
isStructuredContent = true;
|
||||
// Formater le contenu structuré
|
||||
finalContent = keys
|
||||
.sort((a, b) => {
|
||||
// Trier par type (H2, H3, P) puis par numéro
|
||||
const aMatch = a.match(/^([A-Z]+)_(\d+)$/);
|
||||
const bMatch = b.match(/^([A-Z]+)_(\d+)$/);
|
||||
if (!aMatch || !bMatch) return 0;
|
||||
if (aMatch[1] !== bMatch[1]) return aMatch[1].localeCompare(bMatch[1]);
|
||||
return parseInt(aMatch[2]) - parseInt(bMatch[2]);
|
||||
})
|
||||
.map(key => {
|
||||
const match = key.match(/^([A-Z0-9]+)_(\d+)$/);
|
||||
if (match) {
|
||||
const tag = match[1];
|
||||
return `[${tag}]\n${rawContent[key]}\n`;
|
||||
}
|
||||
return `${key}: ${rawContent[key]}\n`;
|
||||
})
|
||||
.join('\n');
|
||||
} else {
|
||||
// Si c'est un objet, essayer d'extraire le texte
|
||||
finalContent = rawContent.text || rawContent.content || rawContent.organicContent || JSON.stringify(rawContent, null, 2);
|
||||
}
|
||||
}
|
||||
|
||||
if (finalContent) {
|
||||
finalContentContainer.innerHTML = '';
|
||||
|
||||
// Warning si contenu incomplet
|
||||
const elementCount = Object.keys(rawContent || {}).length;
|
||||
if (isStructuredContent && elementCount < 30) {
|
||||
const warningDiv = document.createElement('div');
|
||||
warningDiv.style.cssText = 'padding: 10px; margin-bottom: 15px; background: #fed7d7; border: 1px solid #f56565; border-radius: 6px; color: #822727;';
|
||||
warningDiv.innerHTML = `⚠️ <strong>Génération incomplète:</strong> ${elementCount} éléments générés (attendu ~33). Vérifiez les logs pour plus de détails.`;
|
||||
finalContentContainer.appendChild(warningDiv);
|
||||
}
|
||||
|
||||
// Créer un élément pre pour préserver le formatage
|
||||
const contentDiv = document.createElement('div');
|
||||
contentDiv.style.cssText = 'white-space: pre-wrap; line-height: 1.6; color: var(--text-dark); font-size: 14px;';
|
||||
contentDiv.textContent = finalContent;
|
||||
|
||||
// Ajouter un bouton pour copier
|
||||
const copyBtn = document.createElement('button');
|
||||
copyBtn.textContent = '📋 Copier le contenu';
|
||||
copyBtn.style.cssText = 'margin-bottom: 15px; padding: 8px 16px; background: var(--primary); color: white; border: none; border-radius: 6px; cursor: pointer; font-weight: 600;';
|
||||
copyBtn.onclick = () => {
|
||||
navigator.clipboard.writeText(finalContent);
|
||||
copyBtn.textContent = '✓ Copié!';
|
||||
setTimeout(() => { copyBtn.textContent = '📋 Copier le contenu'; }, 2000);
|
||||
};
|
||||
|
||||
finalContentContainer.appendChild(copyBtn);
|
||||
finalContentContainer.appendChild(contentDiv);
|
||||
|
||||
// Ajouter les métadonnées si disponibles
|
||||
if (result.stats) {
|
||||
const metaDiv = document.createElement('div');
|
||||
metaDiv.style.cssText = 'margin-top: 15px; padding: 10px; background: white; border-radius: 6px; font-size: 12px; color: var(--text-light);';
|
||||
const contentLength = finalContent.length;
|
||||
const wordCount = finalContent.split(/\s+/).length;
|
||||
metaDiv.innerHTML = `<strong>Métadonnées:</strong> ${contentLength} caractères, ~${wordCount} mots`;
|
||||
finalContentContainer.appendChild(metaDiv);
|
||||
}
|
||||
} else {
|
||||
finalContentContainer.innerHTML = '<p style="color: var(--warning);">⚠️ Aucun contenu final disponible dans le résultat</p>';
|
||||
}
|
||||
|
||||
// Version History
|
||||
const versionHistoryContainer = document.getElementById('versionHistory');
|
||||
versionHistoryContainer.innerHTML = '';
|
||||
|
||||
if (result.versionHistory && result.versionHistory.length > 0) {
|
||||
result.versionHistory.forEach(version => {
|
||||
const div = document.createElement('div');
|
||||
div.style.cssText = 'padding: 10px; margin-bottom: 8px; background: white; border-radius: 6px; border-left: 4px solid var(--success);';
|
||||
|
||||
const versionLabel = document.createElement('strong');
|
||||
versionLabel.textContent = `Version ${version.version}`;
|
||||
versionLabel.style.color = 'var(--primary)';
|
||||
|
||||
const articleId = document.createElement('span');
|
||||
articleId.textContent = ` - Article ID: ${version.articleId}`;
|
||||
articleId.style.color = 'var(--text-dark)';
|
||||
|
||||
const modifications = document.createElement('span');
|
||||
modifications.textContent = ` - ${version.modifications || 0} modifications`;
|
||||
modifications.style.color = 'var(--text-light)';
|
||||
modifications.style.fontSize = '13px';
|
||||
modifications.style.marginLeft = '10px';
|
||||
|
||||
div.appendChild(versionLabel);
|
||||
div.appendChild(articleId);
|
||||
div.appendChild(modifications);
|
||||
|
||||
versionHistoryContainer.appendChild(div);
|
||||
});
|
||||
} else {
|
||||
versionHistoryContainer.innerHTML = '<p style="color: var(--text-light);">Aucune version sauvegardée</p>';
|
||||
}
|
||||
|
||||
// Google Sheets Link
|
||||
if (result.gsheetsLink) {
|
||||
document.getElementById('gsheetsLinkContainer').style.display = 'block';
|
||||
document.getElementById('gsheetsLink').href = result.gsheetsLink;
|
||||
} else {
|
||||
document.getElementById('gsheetsLinkContainer').style.display = 'none';
|
||||
}
|
||||
|
||||
// Execution log
|
||||
const logContainer = document.getElementById('executionLog');
|
||||
logContainer.innerHTML = '';
|
||||
|
||||
60
server.js
60
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);
|
||||
});
|
||||
}
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
@ -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
|
||||
});
|
||||
|
||||
212
tools/analyze-skipped-exports.js
Normal file
212
tools/analyze-skipped-exports.js
Normal file
@ -0,0 +1,212 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const EXPORTS_DIR = path.join(__dirname, '../claude-exports-last-3-days');
|
||||
|
||||
/**
|
||||
* Parse un fichier de session pour extraire les tool uses
|
||||
*/
|
||||
function parseSessionFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const tools = [];
|
||||
|
||||
// Chercher tous les blocs JSON qui contiennent des tool uses
|
||||
const jsonBlockRegex = /\[\s*\{[\s\S]*?"type":\s*"tool_use"[\s\S]*?\}\s*\]/g;
|
||||
const matches = content.match(jsonBlockRegex);
|
||||
|
||||
if (!matches) return tools;
|
||||
|
||||
for (const match of matches) {
|
||||
try {
|
||||
const parsed = JSON.parse(match);
|
||||
for (const item of parsed) {
|
||||
if (item.type === 'tool_use' && (item.name === 'Edit' || item.name === 'Write')) {
|
||||
tools.push({
|
||||
name: item.name,
|
||||
input: item.input
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Analyse pourquoi un Edit a été skippé
|
||||
*/
|
||||
function analyzeSkippedEdit(filePath, oldString) {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
return { reason: 'FILE_NOT_EXIST', details: 'Fichier n\'existe pas' };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
if (!content.includes(oldString)) {
|
||||
// Vérifier si une partie de old_string existe
|
||||
const oldLines = oldString.split('\n').filter(l => l.trim());
|
||||
const matchingLines = oldLines.filter(line => content.includes(line.trim()));
|
||||
|
||||
if (matchingLines.length > 0) {
|
||||
return {
|
||||
reason: 'PARTIAL_MATCH',
|
||||
details: `${matchingLines.length}/${oldLines.length} lignes trouvées - code probablement modifié`
|
||||
};
|
||||
} else {
|
||||
return {
|
||||
reason: 'NO_MATCH',
|
||||
details: 'Code complètement différent - changement déjà appliqué ou code refactorisé'
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return { reason: 'OK', details: 'Devrait fonctionner' };
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
console.log('🔍 Analyse des exports Claude skippés...\n');
|
||||
|
||||
const sessionFiles = fs.readdirSync(EXPORTS_DIR)
|
||||
.filter(f => f.endsWith('-session.md'))
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.split('-')[0]);
|
||||
const numB = parseInt(b.split('-')[0]);
|
||||
return numB - numA;
|
||||
});
|
||||
|
||||
const skippedAnalysis = {
|
||||
FILE_NOT_EXIST: [],
|
||||
PARTIAL_MATCH: [],
|
||||
NO_MATCH: [],
|
||||
FILE_EXISTS: [] // Pour les Write
|
||||
};
|
||||
|
||||
let totalSkipped = 0;
|
||||
|
||||
for (const sessionFile of sessionFiles) {
|
||||
const filePath = path.join(EXPORTS_DIR, sessionFile);
|
||||
const tools = parseSessionFile(filePath);
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool.name === 'Edit') {
|
||||
const { file_path, old_string } = tool.input;
|
||||
|
||||
if (!fs.existsSync(file_path)) {
|
||||
skippedAnalysis.FILE_NOT_EXIST.push({
|
||||
session: sessionFile,
|
||||
file: file_path,
|
||||
preview: old_string.substring(0, 80)
|
||||
});
|
||||
totalSkipped++;
|
||||
} else {
|
||||
const content = fs.readFileSync(file_path, 'utf-8');
|
||||
if (!content.includes(old_string)) {
|
||||
const analysis = analyzeSkippedEdit(file_path, old_string);
|
||||
|
||||
if (analysis.reason === 'PARTIAL_MATCH') {
|
||||
skippedAnalysis.PARTIAL_MATCH.push({
|
||||
session: sessionFile,
|
||||
file: file_path,
|
||||
details: analysis.details,
|
||||
preview: old_string.substring(0, 80)
|
||||
});
|
||||
} else {
|
||||
skippedAnalysis.NO_MATCH.push({
|
||||
session: sessionFile,
|
||||
file: file_path,
|
||||
preview: old_string.substring(0, 80)
|
||||
});
|
||||
}
|
||||
totalSkipped++;
|
||||
}
|
||||
}
|
||||
} else if (tool.name === 'Write') {
|
||||
const { file_path } = tool.input;
|
||||
if (fs.existsSync(file_path)) {
|
||||
skippedAnalysis.FILE_EXISTS.push({
|
||||
session: sessionFile,
|
||||
file: file_path
|
||||
});
|
||||
totalSkipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log(`📊 Total skippés: ${totalSkipped}\n`);
|
||||
|
||||
console.log('═══════════════════════════════════════════════════════');
|
||||
console.log('🚫 FICHIERS N\'EXISTANT PAS (Edit)');
|
||||
console.log(` ${skippedAnalysis.FILE_NOT_EXIST.length} cas\n`);
|
||||
const fileNotExistByFile = {};
|
||||
for (const item of skippedAnalysis.FILE_NOT_EXIST) {
|
||||
if (!fileNotExistByFile[item.file]) {
|
||||
fileNotExistByFile[item.file] = 0;
|
||||
}
|
||||
fileNotExistByFile[item.file]++;
|
||||
}
|
||||
Object.entries(fileNotExistByFile)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
.forEach(([file, count]) => {
|
||||
console.log(` ${count}x - ${file}`);
|
||||
});
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════');
|
||||
console.log('⚠️ CORRESPONDANCE PARTIELLE (Edit - code probablement modifié)');
|
||||
console.log(` ${skippedAnalysis.PARTIAL_MATCH.length} cas\n`);
|
||||
const partialByFile = {};
|
||||
for (const item of skippedAnalysis.PARTIAL_MATCH) {
|
||||
if (!partialByFile[item.file]) {
|
||||
partialByFile[item.file] = 0;
|
||||
}
|
||||
partialByFile[item.file]++;
|
||||
}
|
||||
Object.entries(partialByFile)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
.forEach(([file, count]) => {
|
||||
console.log(` ${count}x - ${file}`);
|
||||
});
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════');
|
||||
console.log('❌ AUCUNE CORRESPONDANCE (Edit - changement déjà appliqué)');
|
||||
console.log(` ${skippedAnalysis.NO_MATCH.length} cas\n`);
|
||||
const noMatchByFile = {};
|
||||
for (const item of skippedAnalysis.NO_MATCH) {
|
||||
if (!noMatchByFile[item.file]) {
|
||||
noMatchByFile[item.file] = 0;
|
||||
}
|
||||
noMatchByFile[item.file]++;
|
||||
}
|
||||
Object.entries(noMatchByFile)
|
||||
.sort((a, b) => b[1] - a[1])
|
||||
.slice(0, 10)
|
||||
.forEach(([file, count]) => {
|
||||
console.log(` ${count}x - ${file}`);
|
||||
});
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════');
|
||||
console.log('✅ FICHIERS DÉJÀ EXISTANTS (Write - comportement normal)');
|
||||
console.log(` ${skippedAnalysis.FILE_EXISTS.length} cas\n`);
|
||||
skippedAnalysis.FILE_EXISTS.forEach(item => {
|
||||
console.log(` ${item.session} → ${item.file}`);
|
||||
});
|
||||
|
||||
console.log('\n═══════════════════════════════════════════════════════');
|
||||
console.log('💡 CONCLUSION:\n');
|
||||
console.log(` ✅ Write skippés: ${skippedAnalysis.FILE_EXISTS.length} (NORMAL - ne pas écraser)`);
|
||||
console.log(` ❌ Edit skippés (NO_MATCH): ${skippedAnalysis.NO_MATCH.length} (changements déjà appliqués)`);
|
||||
console.log(` ⚠️ Edit skippés (PARTIAL_MATCH): ${skippedAnalysis.PARTIAL_MATCH.length} (code modifié depuis)`);
|
||||
console.log(` 🚫 Edit skippés (FILE_NOT_EXIST): ${skippedAnalysis.FILE_NOT_EXIST.length} (fichiers supprimés?)\n`);
|
||||
}
|
||||
|
||||
main();
|
||||
470
tools/apply-claude-exports-fuzzy.js
Normal file
470
tools/apply-claude-exports-fuzzy.js
Normal file
@ -0,0 +1,470 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
/**
|
||||
* apply-claude-exports-fuzzy.js
|
||||
*
|
||||
* Applique les exports Claude avec fuzzy matching amélioré
|
||||
*
|
||||
* AMÉLIORATIONS:
|
||||
* - Normalisation des line endings (\r\n, \r, \n → \n unifié)
|
||||
* - Ignore les différences d'espacement (espaces multiples, tabs)
|
||||
* - Score de similarité abaissé à 85% pour plus de flexibilité
|
||||
* - Matching robuste qui ne casse pas sur les variations d'espaces
|
||||
*
|
||||
* Usage:
|
||||
* node tools/apply-claude-exports-fuzzy.js # Apply changes
|
||||
* node tools/apply-claude-exports-fuzzy.js --dry-run # Preview only
|
||||
*/
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const EXPORTS_DIR = path.join(__dirname, '../claude-exports-last-3-days');
|
||||
const LOGS_DIR = path.join(__dirname, '../logs');
|
||||
|
||||
// Créer dossier logs si nécessaire
|
||||
if (!fs.existsSync(LOGS_DIR)) {
|
||||
fs.mkdirSync(LOGS_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
// Fichier de log avec timestamp
|
||||
const timestamp = new Date().toISOString().replace(/:/g, '-').replace(/\..+/, '');
|
||||
const LOG_FILE = path.join(LOGS_DIR, `apply-exports-fuzzy-${timestamp}.log`);
|
||||
|
||||
// Configuration fuzzy matching
|
||||
const FUZZY_CONFIG = {
|
||||
minSimilarity: 0.85, // Minimum 85% de similarité pour accepter le match (abaissé de 95% pour plus de flexibilité)
|
||||
contextLines: 3, // Lignes de contexte avant/après
|
||||
ignoreWhitespace: true, // Ignorer les différences d'espacement (espaces multiples, tabs)
|
||||
ignoreComments: false, // Ignorer les différences dans les commentaires
|
||||
normalizeLineEndings: true // Unifier \r\n, \r, \n en \n (activé par défaut)
|
||||
};
|
||||
|
||||
/**
|
||||
* Logger dans console ET fichier
|
||||
*/
|
||||
function log(message, onlyFile = false) {
|
||||
const line = `${message}\n`;
|
||||
|
||||
// Écrire dans le fichier
|
||||
fs.appendFileSync(LOG_FILE, line, 'utf-8');
|
||||
|
||||
// Écrire aussi dans la console (sauf si onlyFile)
|
||||
if (!onlyFile) {
|
||||
process.stdout.write(line);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Parse un fichier de session pour extraire les tool uses
|
||||
*/
|
||||
function parseSessionFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const tools = [];
|
||||
|
||||
const jsonBlockRegex = /\[\s*\{[\s\S]*?"type":\s*"tool_use"[\s\S]*?\}\s*\]/g;
|
||||
const matches = content.match(jsonBlockRegex);
|
||||
|
||||
if (!matches) return tools;
|
||||
|
||||
for (const match of matches) {
|
||||
try {
|
||||
const parsed = JSON.parse(match);
|
||||
for (const item of parsed) {
|
||||
if (item.type === 'tool_use' && (item.name === 'Edit' || item.name === 'Write')) {
|
||||
tools.push({
|
||||
name: item.name,
|
||||
input: item.input
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliser un texte complet pour la comparaison
|
||||
* - Unifie les line endings (\r\n, \r, \n → \n)
|
||||
* - Ignore les différences d'espacement selon config
|
||||
*/
|
||||
function normalizeText(text) {
|
||||
let normalized = text;
|
||||
|
||||
// Unifier tous les retours à la ligne si configuré
|
||||
if (FUZZY_CONFIG.normalizeLineEndings) {
|
||||
normalized = normalized.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
}
|
||||
|
||||
if (FUZZY_CONFIG.ignoreWhitespace) {
|
||||
// Réduire les espaces/tabs multiples en un seul espace
|
||||
normalized = normalized.replace(/[ \t]+/g, ' ');
|
||||
// Nettoyer les espaces en début/fin de chaque ligne
|
||||
normalized = normalized.split('\n').map(line => line.trim()).join('\n');
|
||||
}
|
||||
|
||||
return normalized;
|
||||
}
|
||||
|
||||
/**
|
||||
* Normaliser une ligne pour la comparaison
|
||||
*/
|
||||
function normalizeLine(line) {
|
||||
if (FUZZY_CONFIG.ignoreWhitespace) {
|
||||
// Trim + réduire les espaces multiples à un seul
|
||||
return line.trim().replace(/\s+/g, ' ');
|
||||
}
|
||||
return line;
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculer la similarité entre deux chaînes (Levenshtein simplifié)
|
||||
*/
|
||||
function calculateSimilarity(str1, str2) {
|
||||
const len1 = str1.length;
|
||||
const len2 = str2.length;
|
||||
|
||||
if (len1 === 0) return len2 === 0 ? 1.0 : 0.0;
|
||||
if (len2 === 0) return 0.0;
|
||||
|
||||
// Matrice de distance
|
||||
const matrix = Array(len1 + 1).fill(null).map(() => Array(len2 + 1).fill(0));
|
||||
|
||||
for (let i = 0; i <= len1; i++) matrix[i][0] = i;
|
||||
for (let j = 0; j <= len2; j++) matrix[0][j] = j;
|
||||
|
||||
for (let i = 1; i <= len1; i++) {
|
||||
for (let j = 1; j <= len2; j++) {
|
||||
const cost = str1[i - 1] === str2[j - 1] ? 0 : 1;
|
||||
matrix[i][j] = Math.min(
|
||||
matrix[i - 1][j] + 1, // deletion
|
||||
matrix[i][j - 1] + 1, // insertion
|
||||
matrix[i - 1][j - 1] + cost // substitution
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
const distance = matrix[len1][len2];
|
||||
const maxLen = Math.max(len1, len2);
|
||||
return 1 - (distance / maxLen);
|
||||
}
|
||||
|
||||
/**
|
||||
* Créer un diff détaillé ligne par ligne
|
||||
*/
|
||||
function createDiff(oldLines, newLines) {
|
||||
const diff = [];
|
||||
const maxLen = Math.max(oldLines.length, newLines.length);
|
||||
|
||||
for (let i = 0; i < maxLen; i++) {
|
||||
const oldLine = oldLines[i];
|
||||
const newLine = newLines[i];
|
||||
|
||||
if (oldLine === undefined) {
|
||||
// Ligne ajoutée
|
||||
diff.push({ type: 'add', line: newLine, lineNum: i });
|
||||
} else if (newLine === undefined) {
|
||||
// Ligne supprimée
|
||||
diff.push({ type: 'del', line: oldLine, lineNum: i });
|
||||
} else if (oldLine !== newLine) {
|
||||
// Ligne modifiée
|
||||
diff.push({ type: 'mod', oldLine, newLine, lineNum: i });
|
||||
} else {
|
||||
// Ligne identique
|
||||
diff.push({ type: 'same', line: oldLine, lineNum: i });
|
||||
}
|
||||
}
|
||||
|
||||
return diff;
|
||||
}
|
||||
|
||||
/**
|
||||
* Trouver la meilleure position pour un fuzzy match
|
||||
*/
|
||||
function findFuzzyMatch(content, oldString) {
|
||||
// 🔥 CRITICAL FIX: Unifier SEULEMENT line endings (comme dans applyEdit)
|
||||
// pour que les positions correspondent au même format de texte
|
||||
// On normalise les espaces SEULEMENT pour la COMPARAISON (ligne par ligne)
|
||||
const contentWithUnifiedLineEndings = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
const oldStringWithUnifiedLineEndings = oldString.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
|
||||
const contentLines = contentWithUnifiedLineEndings.split('\n');
|
||||
const oldLines = oldStringWithUnifiedLineEndings.split('\n');
|
||||
|
||||
if (oldLines.length === 0) return null;
|
||||
|
||||
let bestMatch = null;
|
||||
let bestScore = 0;
|
||||
|
||||
// Chercher dans tout le contenu
|
||||
for (let startLine = 0; startLine <= contentLines.length - oldLines.length; startLine++) {
|
||||
const candidateLines = contentLines.slice(startLine, startLine + oldLines.length);
|
||||
|
||||
// Calculer le score de similarité ligne par ligne
|
||||
let totalScore = 0;
|
||||
let matchedLines = 0;
|
||||
|
||||
for (let i = 0; i < oldLines.length; i++) {
|
||||
const oldNorm = normalizeLine(oldLines[i]);
|
||||
const candidateNorm = normalizeLine(candidateLines[i]);
|
||||
|
||||
const similarity = calculateSimilarity(oldNorm, candidateNorm);
|
||||
totalScore += similarity;
|
||||
if (similarity > 0.8) matchedLines++;
|
||||
}
|
||||
|
||||
const avgScore = totalScore / oldLines.length;
|
||||
const matchRatio = matchedLines / oldLines.length;
|
||||
|
||||
// Score combiné: moyenne de similarité + ratio de lignes matchées
|
||||
const combinedScore = (avgScore * 0.7) + (matchRatio * 0.3);
|
||||
|
||||
if (combinedScore > bestScore && combinedScore >= FUZZY_CONFIG.minSimilarity) {
|
||||
bestScore = combinedScore;
|
||||
bestMatch = {
|
||||
startLine,
|
||||
endLine: startLine + oldLines.length,
|
||||
score: combinedScore,
|
||||
matchedLines,
|
||||
totalLines: oldLines.length
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return bestMatch;
|
||||
}
|
||||
|
||||
/**
|
||||
* Appliquer un Edit avec fuzzy matching
|
||||
*/
|
||||
function applyEdit(filePath, oldString, newString, dryRun = false) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
log(`⏭️ SKIP Edit - Fichier n'existe pas: ${filePath}`);
|
||||
return { success: false, reason: 'FILE_NOT_EXIST' };
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
// 🔥 Essayer d'abord un match exact SANS normalisation (le plus rapide et sûr)
|
||||
if (content.includes(oldString)) {
|
||||
const newContent = content.replace(oldString, newString);
|
||||
|
||||
if (!dryRun) {
|
||||
fs.writeFileSync(filePath, newContent, 'utf-8');
|
||||
}
|
||||
|
||||
log(`✅ EDIT EXACT appliqué: ${filePath}`);
|
||||
return { success: true, reason: 'EXACT_MATCH', method: 'exact' };
|
||||
}
|
||||
|
||||
// Si pas de match exact, essayer le fuzzy matching (avec normalisation)
|
||||
const fuzzyMatch = findFuzzyMatch(content, oldString);
|
||||
|
||||
if (fuzzyMatch) {
|
||||
// 🔥 IMPORTANT: fuzzyMatch a trouvé les positions avec normalisation
|
||||
// Mais on applique le remplacement sur les versions ORIGINALES (espaces préservés)
|
||||
// On unifie SEULEMENT les line endings (\r\n → \n) pour que les positions correspondent
|
||||
|
||||
// Unifier line endings UNIQUEMENT (garder espaces originaux)
|
||||
const contentWithUnifiedLineEndings = content.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
const newStringWithUnifiedLineEndings = newString.replace(/\r\n/g, '\n').replace(/\r/g, '\n');
|
||||
|
||||
const contentLines = contentWithUnifiedLineEndings.split('\n');
|
||||
const newLines = newStringWithUnifiedLineEndings.split('\n');
|
||||
|
||||
// Capturer les lignes matchées ORIGINALES (AVANT remplacement)
|
||||
const matchedLines = contentLines.slice(fuzzyMatch.startLine, fuzzyMatch.endLine);
|
||||
|
||||
// Remplacer la zone identifiée avec le patch ORIGINAL
|
||||
const before = contentLines.slice(0, fuzzyMatch.startLine);
|
||||
const after = contentLines.slice(fuzzyMatch.endLine);
|
||||
const newContent = [...before, ...newLines, ...after].join('\n');
|
||||
|
||||
if (!dryRun) {
|
||||
fs.writeFileSync(filePath, newContent, 'utf-8');
|
||||
}
|
||||
|
||||
log(`🎯 EDIT FUZZY appliqué: ${filePath} (score: ${(fuzzyMatch.score * 100).toFixed(1)}%, lignes ${fuzzyMatch.startLine}-${fuzzyMatch.endLine})`);
|
||||
|
||||
// Créer un diff détaillé
|
||||
const diff = createDiff(matchedLines, newLines);
|
||||
|
||||
log(`┌─ 📝 DIFF DÉTAILLÉ ────────────────────────────────────────────────`);
|
||||
diff.forEach((item, idx) => {
|
||||
const lineNum = String(fuzzyMatch.startLine + idx + 1).padStart(4, ' ');
|
||||
|
||||
if (item.type === 'same') {
|
||||
// Ligne identique
|
||||
log(`│ ${lineNum} │ ${item.line}`);
|
||||
} else if (item.type === 'add') {
|
||||
// Ligne ajoutée
|
||||
log(`│ ${lineNum} │ + ${item.line}`);
|
||||
} else if (item.type === 'del') {
|
||||
// Ligne supprimée
|
||||
log(`│ ${lineNum} │ - ${item.line}`);
|
||||
} else if (item.type === 'mod') {
|
||||
// Ligne modifiée - afficher les deux
|
||||
log(`│ ${lineNum} │ - ${item.oldLine}`);
|
||||
log(`│ ${lineNum} │ + ${item.newLine}`);
|
||||
}
|
||||
});
|
||||
log(`└────────────────────────────────────────────────────────────────────`);
|
||||
log('');
|
||||
|
||||
return {
|
||||
success: true,
|
||||
reason: 'FUZZY_MATCH',
|
||||
method: 'fuzzy',
|
||||
score: fuzzyMatch.score,
|
||||
lines: `${fuzzyMatch.startLine}-${fuzzyMatch.endLine}`
|
||||
};
|
||||
}
|
||||
|
||||
log(`⏭️ SKIP Edit - Aucun match trouvé: ${filePath}`);
|
||||
return { success: false, reason: 'NO_MATCH' };
|
||||
|
||||
} catch (e) {
|
||||
log(`❌ ERREUR Edit sur ${filePath}: ${e.message}`);
|
||||
return { success: false, reason: 'ERROR', error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Appliquer un Write sur un fichier
|
||||
*/
|
||||
function applyWrite(filePath, content, dryRun = false) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
log(`⏭️ SKIP Write - Fichier existe déjà: ${filePath}`);
|
||||
return { success: false, reason: 'FILE_EXISTS' };
|
||||
}
|
||||
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
if (!dryRun) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
}
|
||||
|
||||
if (!dryRun) {
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
}
|
||||
|
||||
log(`✅ WRITE appliqué: ${filePath}`);
|
||||
return { success: true, reason: 'CREATED' };
|
||||
|
||||
} catch (e) {
|
||||
log(`❌ ERREUR Write sur ${filePath}: ${e.message}`);
|
||||
return { success: false, reason: 'ERROR', error: e.message };
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
// Check for dry-run mode
|
||||
const dryRun = process.argv.includes('--dry-run');
|
||||
|
||||
log(`📝 Logs sauvegardés dans: ${LOG_FILE}`);
|
||||
log('');
|
||||
|
||||
if (dryRun) {
|
||||
log('🔍 MODE DRY-RUN: Aucun fichier ne sera modifié');
|
||||
log('');
|
||||
}
|
||||
|
||||
log('🔄 Application des exports Claude avec FUZZY MATCHING...');
|
||||
log(`⚙️ Config: minSimilarity=${FUZZY_CONFIG.minSimilarity * 100}%, ignoreWhitespace=${FUZZY_CONFIG.ignoreWhitespace}`);
|
||||
log('');
|
||||
|
||||
// Lire tous les fichiers de session
|
||||
const sessionFiles = fs.readdirSync(EXPORTS_DIR)
|
||||
.filter(f => f.endsWith('-session.md'))
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.split('-')[0]);
|
||||
const numB = parseInt(b.split('-')[0]);
|
||||
return numB - numA; // Ordre inverse: 15 -> 1
|
||||
});
|
||||
|
||||
log(`📁 ${sessionFiles.length} fichiers de session trouvés`);
|
||||
log(`📋 Ordre de traitement: ${sessionFiles.join(', ')}`);
|
||||
log('');
|
||||
|
||||
const stats = {
|
||||
totalEdits: 0,
|
||||
totalWrites: 0,
|
||||
exactMatches: 0,
|
||||
fuzzyMatches: 0,
|
||||
successWrites: 0,
|
||||
skipped: 0,
|
||||
errors: 0
|
||||
};
|
||||
|
||||
for (const sessionFile of sessionFiles) {
|
||||
const filePath = path.join(EXPORTS_DIR, sessionFile);
|
||||
log('');
|
||||
log(`📄 Traitement de: ${sessionFile}`);
|
||||
|
||||
const tools = parseSessionFile(filePath);
|
||||
log(` ${tools.length} tool use(s) trouvé(s)`);
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool.name === 'Edit') {
|
||||
stats.totalEdits++;
|
||||
const { file_path, old_string, new_string } = tool.input;
|
||||
const result = applyEdit(file_path, old_string, new_string, dryRun);
|
||||
|
||||
if (result.success) {
|
||||
if (result.method === 'exact') {
|
||||
stats.exactMatches++;
|
||||
} else if (result.method === 'fuzzy') {
|
||||
stats.fuzzyMatches++;
|
||||
}
|
||||
} else {
|
||||
if (result.reason === 'ERROR') {
|
||||
stats.errors++;
|
||||
} else {
|
||||
stats.skipped++;
|
||||
}
|
||||
}
|
||||
} else if (tool.name === 'Write') {
|
||||
stats.totalWrites++;
|
||||
const { file_path, content } = tool.input;
|
||||
const result = applyWrite(file_path, content, dryRun);
|
||||
|
||||
if (result.success) {
|
||||
stats.successWrites++;
|
||||
} else {
|
||||
stats.skipped++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log('');
|
||||
log('');
|
||||
log('📊 RÉSUMÉ:');
|
||||
log(` Edit Exact: ${stats.exactMatches}/${stats.totalEdits} appliqués`);
|
||||
log(` Edit Fuzzy: ${stats.fuzzyMatches}/${stats.totalEdits} appliqués`);
|
||||
log(` Write: ${stats.successWrites}/${stats.totalWrites} appliqués`);
|
||||
log(` Skippés: ${stats.skipped}`);
|
||||
log(` Erreurs: ${stats.errors}`);
|
||||
log(` Total: ${stats.exactMatches + stats.fuzzyMatches + stats.successWrites}/${stats.totalEdits + stats.totalWrites} opérations réussies`);
|
||||
log('');
|
||||
|
||||
if (dryRun) {
|
||||
log('💡 Pour appliquer réellement, relancez sans --dry-run');
|
||||
} else {
|
||||
log('✨ Terminé!');
|
||||
}
|
||||
|
||||
log('');
|
||||
log(`📝 Logs complets: ${LOG_FILE}`);
|
||||
}
|
||||
|
||||
main();
|
||||
146
tools/apply-claude-exports.js
Normal file
146
tools/apply-claude-exports.js
Normal file
@ -0,0 +1,146 @@
|
||||
#!/usr/bin/env node
|
||||
|
||||
const fs = require('fs');
|
||||
const path = require('path');
|
||||
|
||||
const EXPORTS_DIR = path.join(__dirname, '../claude-exports-last-3-days');
|
||||
|
||||
/**
|
||||
* Parse un fichier de session pour extraire les tool uses
|
||||
*/
|
||||
function parseSessionFile(filePath) {
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
const tools = [];
|
||||
|
||||
// Chercher tous les blocs JSON qui contiennent des tool uses
|
||||
const jsonBlockRegex = /\[\s*\{[\s\S]*?"type":\s*"tool_use"[\s\S]*?\}\s*\]/g;
|
||||
const matches = content.match(jsonBlockRegex);
|
||||
|
||||
if (!matches) return tools;
|
||||
|
||||
for (const match of matches) {
|
||||
try {
|
||||
const parsed = JSON.parse(match);
|
||||
for (const item of parsed) {
|
||||
if (item.type === 'tool_use' && (item.name === 'Edit' || item.name === 'Write')) {
|
||||
tools.push({
|
||||
name: item.name,
|
||||
input: item.input
|
||||
});
|
||||
}
|
||||
}
|
||||
} catch (e) {
|
||||
// Skip invalid JSON
|
||||
}
|
||||
}
|
||||
|
||||
return tools;
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique un Edit sur un fichier
|
||||
*/
|
||||
function applyEdit(filePath, oldString, newString) {
|
||||
try {
|
||||
if (!fs.existsSync(filePath)) {
|
||||
console.log(`⏭️ SKIP Edit - Fichier n'existe pas: ${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const content = fs.readFileSync(filePath, 'utf-8');
|
||||
|
||||
if (!content.includes(oldString)) {
|
||||
console.log(`⏭️ SKIP Edit - old_string non trouvée dans: ${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
const newContent = content.replace(oldString, newString);
|
||||
fs.writeFileSync(filePath, newContent, 'utf-8');
|
||||
console.log(`✅ EDIT appliqué: ${filePath}`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(`❌ ERREUR Edit sur ${filePath}: ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Applique un Write sur un fichier
|
||||
*/
|
||||
function applyWrite(filePath, content) {
|
||||
try {
|
||||
if (fs.existsSync(filePath)) {
|
||||
console.log(`⏭️ SKIP Write - Fichier existe déjà: ${filePath}`);
|
||||
return false;
|
||||
}
|
||||
|
||||
// Créer les dossiers parents si nécessaire
|
||||
const dir = path.dirname(filePath);
|
||||
if (!fs.existsSync(dir)) {
|
||||
fs.mkdirSync(dir, { recursive: true });
|
||||
}
|
||||
|
||||
fs.writeFileSync(filePath, content, 'utf-8');
|
||||
console.log(`✅ WRITE appliqué: ${filePath}`);
|
||||
return true;
|
||||
} catch (e) {
|
||||
console.log(`❌ ERREUR Write sur ${filePath}: ${e.message}`);
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Main
|
||||
*/
|
||||
function main() {
|
||||
console.log('🔄 Application des exports Claude...\n');
|
||||
|
||||
// Lire tous les fichiers de session
|
||||
const sessionFiles = fs.readdirSync(EXPORTS_DIR)
|
||||
.filter(f => f.endsWith('-session.md'))
|
||||
.sort((a, b) => {
|
||||
const numA = parseInt(a.split('-')[0]);
|
||||
const numB = parseInt(b.split('-')[0]);
|
||||
return numB - numA; // Ordre inverse: 15 -> 1
|
||||
});
|
||||
|
||||
console.log(`📁 ${sessionFiles.length} fichiers de session trouvés`);
|
||||
console.log(`📋 Ordre de traitement: ${sessionFiles.join(', ')}\n`);
|
||||
|
||||
let totalEdits = 0;
|
||||
let totalWrites = 0;
|
||||
let successEdits = 0;
|
||||
let successWrites = 0;
|
||||
|
||||
for (const sessionFile of sessionFiles) {
|
||||
const filePath = path.join(EXPORTS_DIR, sessionFile);
|
||||
console.log(`\n📄 Traitement de: ${sessionFile}`);
|
||||
|
||||
const tools = parseSessionFile(filePath);
|
||||
console.log(` ${tools.length} tool use(s) trouvé(s)`);
|
||||
|
||||
for (const tool of tools) {
|
||||
if (tool.name === 'Edit') {
|
||||
totalEdits++;
|
||||
const { file_path, old_string, new_string } = tool.input;
|
||||
if (applyEdit(file_path, old_string, new_string)) {
|
||||
successEdits++;
|
||||
}
|
||||
} else if (tool.name === 'Write') {
|
||||
totalWrites++;
|
||||
const { file_path, content } = tool.input;
|
||||
if (applyWrite(file_path, content)) {
|
||||
successWrites++;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
console.log('\n\n📊 RÉSUMÉ:');
|
||||
console.log(` Edit: ${successEdits}/${totalEdits} appliqués`);
|
||||
console.log(` Write: ${successWrites}/${totalWrites} appliqués`);
|
||||
console.log(` Total: ${successEdits + successWrites}/${totalEdits + totalWrites} opérations réussies`);
|
||||
console.log('\n✨ Terminé!');
|
||||
}
|
||||
|
||||
main();
|
||||
@ -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() {
|
||||
|
||||
Loading…
Reference in New Issue
Block a user