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:
539
visual_workflow_builder/frontend/src/hooks/useVWBExecution.ts
Normal file
539
visual_workflow_builder/frontend/src/hooks/useVWBExecution.ts
Normal 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)
|
||||
};
|
||||
};
|
||||
Reference in New Issue
Block a user