fix(vwb): Corriger l'exécution VWB pour toutes les étapes

- Ajouter liste des 20 types d'actions VWB connus pour détection fiable
- Corriger isVWBStep() pour vérifier step.type en priorité
- Corriger extraction actionId (step.type au lieu de "unknown")
- Résoudre problème stale closure en passant steps en paramètre
- Ajouter logs de débogage détaillés pour suivi exécution

Les étapes type_text sont maintenant correctement exécutées au lieu
d'être simulées.

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-20 17:35:24 +01:00
parent 7ea5d6b992
commit d2955ec1a1
2 changed files with 983 additions and 0 deletions

View File

@@ -0,0 +1,539 @@
/**
* Hook d'Exécution VWB - Gestion de l'exécution des workflows avec actions VisionOnly
* Auteur : Dom, Alice, Kiro - 10 janvier 2026
*
* Ce hook gère l'exécution complète des workflows VWB avec gestion d'état,
* feedback en temps réel et intégration avec les Evidence.
*/
import { useState, useCallback, useEffect, useRef } from 'react';
import {
vwbExecutionService,
VWBExecutionResult,
VWBExecutionOptions,
VWBExecutionContext
} from '../services/vwbExecutionService';
import {
Workflow,
Step,
StepExecutionState,
ExecutionState,
ExecutionError,
Evidence,
Variable
} from '../types';
export interface VWBExecutionState {
status: 'idle' | 'running' | 'paused' | 'completed' | 'error';
currentStepIndex: number;
currentStep: Step | null;
totalSteps: number;
completedSteps: number;
failedSteps: number;
startTime: Date | null;
endTime: Date | null;
duration: number;
progress: number;
results: VWBExecutionResult[];
errors: ExecutionError[];
evidence: Evidence[];
}
export interface VWBExecutionCallbacks {
onStepStart?: (step: Step, index: number) => void;
onStepComplete?: (step: Step, result: VWBExecutionResult) => void;
onStepError?: (step: Step, error: ExecutionError) => void;
onExecutionComplete?: (success: boolean, summary: VWBExecutionSummary) => void;
onEvidenceGenerated?: (stepId: string, evidence: Evidence[]) => void;
onProgressUpdate?: (progress: number, currentStep: Step) => void;
}
export interface VWBExecutionSummary {
totalSteps: number;
completedSteps: number;
failedSteps: number;
skippedSteps: number;
duration: number;
successRate: number;
results: VWBExecutionResult[];
errors: ExecutionError[];
evidence: Evidence[];
}
export interface UseVWBExecutionOptions {
autoValidate?: boolean;
generateEvidence?: boolean;
retryAttempts?: number;
timeout?: number;
pauseOnError?: boolean;
skipNonVWBSteps?: boolean;
}
/**
* Hook principal pour l'exécution des workflows VWB
*/
export const useVWBExecution = (
workflow: Workflow,
variables: Variable[] = [],
callbacks: VWBExecutionCallbacks = {},
options: UseVWBExecutionOptions = {}
) => {
const {
autoValidate = true,
generateEvidence = true,
retryAttempts = 3,
timeout = 30000,
pauseOnError = false,
skipNonVWBSteps = false
} = options;
// État d'exécution
const [executionState, setExecutionState] = useState<VWBExecutionState>({
status: 'idle',
currentStepIndex: 0,
currentStep: null,
totalSteps: 0,
completedSteps: 0,
failedSteps: 0,
startTime: null,
endTime: null,
duration: 0,
progress: 0,
results: [],
errors: [],
evidence: []
});
// Références pour éviter les re-renders
const executionRef = useRef<{
isRunning: boolean;
isPaused: boolean;
shouldStop: boolean;
}>({
isRunning: false,
isPaused: false,
shouldStop: false
});
// Initialiser le contexte d'exécution
useEffect(() => {
const variablesMap = variables.reduce((acc, variable) => {
acc[variable.name] = variable.value;
return acc;
}, {} as Record<string, any>);
const context: VWBExecutionContext = {
workflowId: workflow.id,
sessionId: `session_${Date.now()}`,
variables: variablesMap,
previousResults: executionState.results
};
vwbExecutionService.initializeContext(context);
}, [workflow.id, variables, executionState.results]);
// Réinitialiser l'état lors du changement de workflow
useEffect(() => {
if (executionState.status === 'idle') {
setExecutionState(prev => ({
...prev,
currentStepIndex: 0,
currentStep: null,
totalSteps: workflow.steps.length,
completedSteps: 0,
failedSteps: 0,
progress: 0,
results: [],
errors: [],
evidence: []
}));
}
}, [workflow.id, workflow.steps.length, executionState.status]);
/**
* Démarrer l'exécution du workflow
*/
const startExecution = useCallback(async () => {
console.log('🚀 [VWB] startExecution appelé', {
isRunning: executionRef.current.isRunning,
stepsLength: workflow.steps.length,
workflowId: workflow.id
});
if (executionRef.current.isRunning) {
console.log('⚠️ [VWB] Exécution déjà en cours, ignoré');
return;
}
if (workflow.steps.length === 0) {
console.log('⚠️ [VWB] Aucune étape dans le workflow, ignoré');
return;
}
// Réinitialiser l'état
executionRef.current = {
isRunning: true,
isPaused: false,
shouldStop: false
};
const startTime = new Date();
setExecutionState(prev => ({
...prev,
status: 'running',
startTime,
endTime: null,
currentStepIndex: 0,
currentStep: workflow.steps[0],
totalSteps: workflow.steps.length,
completedSteps: 0,
failedSteps: 0,
progress: 0,
results: [],
errors: [],
evidence: []
}));
try {
// Passer les steps directement pour éviter le problème de stale closure
await executeWorkflowSteps(workflow.steps);
} catch (error) {
console.error('Erreur lors de l\'exécution du workflow:', error);
// handleExecutionError défini plus bas, on gère l'erreur ici directement
executionRef.current.isRunning = false;
setExecutionState(prev => ({
...prev,
status: 'error',
endTime: new Date(),
errors: [...prev.errors, {
stepId: prev.currentStep?.id || 'unknown',
message: error instanceof Error ? error.message : 'Erreur inconnue',
timestamp: new Date()
}]
}));
}
}, [workflow.steps]);
/**
* Exécuter toutes les étapes du workflow
* @param steps - Les étapes à exécuter (passées directement pour éviter stale closure)
*/
const executeWorkflowSteps = useCallback(async (steps: Step[]) => {
const results: VWBExecutionResult[] = [];
const errors: ExecutionError[] = [];
const evidence: Evidence[] = [];
console.log('🔄 [VWB] executeWorkflowSteps démarré, nombre d\'étapes:', steps.length);
for (let i = 0; i < steps.length; i++) {
console.log(`📍 [VWB] Début boucle étape ${i + 1}/${steps.length}`);
// Vérifier si l'exécution doit s'arrêter
if (executionRef.current.shouldStop) {
console.log('⛔ [VWB] Arrêt demandé, sortie de la boucle');
break;
}
// Attendre si en pause
while (executionRef.current.isPaused && !executionRef.current.shouldStop) {
await new Promise(resolve => setTimeout(resolve, 100));
}
const step = steps[i];
console.log(`📋 [VWB] Étape ${i + 1}:`, {
id: step.id,
type: step.type,
action_id: step.action_id,
data: step.data
});
// Mettre à jour l'état actuel
setExecutionState(prev => ({
...prev,
currentStepIndex: i,
currentStep: step,
progress: (i / steps.length) * 100
}));
// Callback de début d'étape
callbacks.onStepStart?.(step, i);
callbacks.onProgressUpdate?.(i / steps.length, step);
try {
// Vérifier si c'est une étape VWB
const isVWBStep = vwbExecutionService.isVWBStep(step);
console.log(`🔍 [VWB] Étape ${step.id} isVWBStep:`, isVWBStep);
if (!isVWBStep && skipNonVWBSteps) {
console.log(`⏭️ [VWB] Étape ${step.id} ignorée (non-VWB)`);
continue;
}
let result: VWBExecutionResult;
if (isVWBStep) {
console.log(`🎯 [VWB] Exécution VWB de l'étape ${step.id}...`);
// Exécuter l'étape VWB
const executionOptions: VWBExecutionOptions = {
timeout,
retryAttempts,
validateBeforeExecution: autoValidate,
generateEvidence
};
result = await vwbExecutionService.executeStep(step, executionOptions);
console.log(`📊 [VWB] Résultat étape ${step.id}:`, result);
} else {
console.log(`🔧 [VWB] Simulation étape non-VWB ${step.id}...`);
// Simuler l'exécution pour les étapes non-VWB
result = await simulateNonVWBStep(step);
console.log(`📊 [VWB] Résultat simulation ${step.id}:`, result);
}
// Traiter le résultat
results.push(result);
if (result.success) {
setExecutionState(prev => ({
...prev,
completedSteps: prev.completedSteps + 1
}));
// Ajouter les Evidence
if (result.evidence) {
evidence.push(...result.evidence);
callbacks.onEvidenceGenerated?.(step.id, result.evidence);
}
callbacks.onStepComplete?.(step, result);
} else {
setExecutionState(prev => ({
...prev,
failedSteps: prev.failedSteps + 1
}));
if (result.error) {
errors.push(result.error);
callbacks.onStepError?.(step, result.error);
}
// Arrêter si configuré pour s'arrêter sur erreur
if (pauseOnError) {
executionRef.current.isPaused = true;
}
}
} catch (error) {
console.error(`❌ [VWB] Exception étape ${step.id}:`, error);
const executionError: ExecutionError = {
stepId: step.id,
message: error instanceof Error ? error.message : 'Erreur inconnue',
// type: 'execution_error',
timestamp: new Date(),
// context: { stepIndex: i }
};
errors.push(executionError);
callbacks.onStepError?.(step, executionError);
setExecutionState(prev => ({
...prev,
failedSteps: prev.failedSteps + 1
}));
if (pauseOnError) {
console.log('⏸️ [VWB] Pause sur erreur activée');
executionRef.current.isPaused = true;
}
}
console.log(`✅ [VWB] Fin traitement étape ${i + 1}/${steps.length}`);
}
console.log('🏁 [VWB] Boucle terminée, finalisation...');
// Finaliser l'exécution
await finalizeExecution(results, errors, evidence);
// eslint-disable-next-line react-hooks/exhaustive-deps
}, [callbacks, autoValidate, generateEvidence, timeout, retryAttempts, pauseOnError, skipNonVWBSteps]);
/**
* Simuler l'exécution d'une étape non-VWB
*/
const simulateNonVWBStep = useCallback(async (step: Step): Promise<VWBExecutionResult> => {
// Simulation simple pour les étapes non-VWB
await new Promise(resolve => setTimeout(resolve, 500));
return {
success: true,
stepId: step.id,
actionId: step.action_id || "unknown",
duration: 500,
output: { simulated: true, stepType: step.action_id }
};
}, []);
/**
* Finaliser l'exécution
*/
const finalizeExecution = useCallback(async (
results: VWBExecutionResult[],
errors: ExecutionError[],
evidence: Evidence[]
) => {
const endTime = new Date();
const duration = executionState.startTime ? endTime.getTime() - executionState.startTime.getTime() : 0;
const successRate = results.length > 0 ? (results.filter(r => r.success).length / results.length) * 100 : 0;
executionRef.current.isRunning = false;
setExecutionState(prev => ({
...prev,
status: errors.length === 0 ? 'completed' : 'error',
endTime,
duration,
progress: 100,
results,
errors,
evidence
}));
// Créer le résumé d'exécution
const summary: VWBExecutionSummary = {
totalSteps: workflow.steps.length,
completedSteps: results.filter(r => r.success).length,
failedSteps: results.filter(r => !r.success).length,
skippedSteps: workflow.steps.length - results.length,
duration,
successRate,
results,
errors,
evidence
};
callbacks.onExecutionComplete?.(errors.length === 0, summary);
}, [workflow.steps.length, executionState.startTime, callbacks]);
/**
* Mettre en pause l'exécution
*/
const pauseExecution = useCallback(() => {
if (executionRef.current.isRunning && !executionRef.current.isPaused) {
executionRef.current.isPaused = true;
setExecutionState(prev => ({ ...prev, status: 'paused' }));
}
}, []);
/**
* Reprendre l'exécution
*/
const resumeExecution = useCallback(() => {
if (executionRef.current.isRunning && executionRef.current.isPaused) {
executionRef.current.isPaused = false;
setExecutionState(prev => ({ ...prev, status: 'running' }));
}
}, []);
/**
* Arrêter l'exécution
*/
const stopExecution = useCallback(() => {
executionRef.current.shouldStop = true;
executionRef.current.isPaused = false;
vwbExecutionService.cancelExecution();
setExecutionState(prev => ({
...prev,
status: 'idle',
endTime: new Date()
}));
executionRef.current.isRunning = false;
}, []);
/**
* Gérer les erreurs d'exécution
*/
const handleExecutionError = useCallback((message: string) => {
executionRef.current.isRunning = false;
setExecutionState(prev => ({
...prev,
status: 'error',
endTime: new Date(),
errors: [...prev.errors, {
stepId: prev.currentStep?.id || 'unknown',
message,
// type: 'execution_error',
timestamp: new Date()
}]
}));
}, []);
/**
* Réinitialiser l'état d'exécution
*/
const resetExecution = useCallback(() => {
stopExecution();
setExecutionState({
status: 'idle',
currentStepIndex: 0,
currentStep: null,
totalSteps: workflow.steps.length,
completedSteps: 0,
failedSteps: 0,
startTime: null,
endTime: null,
duration: 0,
progress: 0,
results: [],
errors: [],
evidence: []
});
}, [workflow.steps.length, stopExecution]);
// Nettoyage lors du démontage
useEffect(() => {
return () => {
vwbExecutionService.cleanup();
};
}, []);
return {
// État
executionState,
isRunning: executionRef.current.isRunning,
isPaused: executionRef.current.isPaused,
canStart: !executionRef.current.isRunning && workflow.steps.length > 0,
canPause: executionRef.current.isRunning && !executionRef.current.isPaused,
canResume: executionRef.current.isRunning && executionRef.current.isPaused,
canStop: executionRef.current.isRunning,
// Actions
startExecution,
pauseExecution,
resumeExecution,
stopExecution,
resetExecution,
// Utilitaires
getExecutionSummary: () => ({
totalSteps: executionState.totalSteps,
completedSteps: executionState.completedSteps,
failedSteps: executionState.failedSteps,
skippedSteps: executionState.totalSteps - executionState.results.length,
duration: executionState.duration,
successRate: executionState.results.length > 0
? (executionState.results.filter(r => r.success).length / executionState.results.length) * 100
: 0,
results: executionState.results,
errors: executionState.errors,
evidence: executionState.evidence
} as VWBExecutionSummary),
isVWBStep: (step: Step) => vwbExecutionService.isVWBStep(step),
validateStep: (step: Step) => vwbExecutionService.validateStep(step)
};
};