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)
|
||||||
|
};
|
||||||
|
};
|
||||||
@@ -0,0 +1,444 @@
|
|||||||
|
/**
|
||||||
|
* Service d'Exécution VWB - Gestion de l'exécution des actions VisionOnly
|
||||||
|
* Auteur : Dom, Alice, Kiro - 11 janvier 2026
|
||||||
|
*
|
||||||
|
* Ce service gère l'exécution des actions VWB avec communication avec l'API catalogue,
|
||||||
|
* gestion des Evidence et feedback en temps réel.
|
||||||
|
*/
|
||||||
|
|
||||||
|
import { catalogService } from './catalogService';
|
||||||
|
import {
|
||||||
|
Step,
|
||||||
|
StepExecutionState,
|
||||||
|
ExecutionResult,
|
||||||
|
ExecutionError,
|
||||||
|
Evidence
|
||||||
|
} from '../types';
|
||||||
|
import { VWBCatalogAction } from '../types/catalog';
|
||||||
|
|
||||||
|
interface VWBActionValidationResult {
|
||||||
|
is_valid: boolean;
|
||||||
|
errors: Array<{
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
severity: 'error' | 'warning';
|
||||||
|
}>;
|
||||||
|
warnings?: Array<{
|
||||||
|
field: string;
|
||||||
|
message: string;
|
||||||
|
}>;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VWBExecutionResult {
|
||||||
|
success: boolean;
|
||||||
|
stepId: string;
|
||||||
|
actionId: string;
|
||||||
|
duration: number;
|
||||||
|
evidence?: Evidence[];
|
||||||
|
error?: ExecutionError;
|
||||||
|
output?: any;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VWBExecutionOptions {
|
||||||
|
timeout?: number;
|
||||||
|
retryAttempts?: number;
|
||||||
|
validateBeforeExecution?: boolean;
|
||||||
|
generateEvidence?: boolean;
|
||||||
|
}
|
||||||
|
|
||||||
|
export interface VWBExecutionContext {
|
||||||
|
workflowId: string;
|
||||||
|
sessionId: string;
|
||||||
|
variables: Record<string, any>;
|
||||||
|
previousResults: VWBExecutionResult[];
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Service principal pour l'exécution des actions VWB
|
||||||
|
*/
|
||||||
|
export class VWBExecutionService {
|
||||||
|
private static instance: VWBExecutionService;
|
||||||
|
private executionContext: VWBExecutionContext | null = null;
|
||||||
|
private isExecuting = false;
|
||||||
|
private currentExecution: AbortController | null = null;
|
||||||
|
|
||||||
|
private constructor() {}
|
||||||
|
|
||||||
|
public static getInstance(): VWBExecutionService {
|
||||||
|
if (!VWBExecutionService.instance) {
|
||||||
|
VWBExecutionService.instance = new VWBExecutionService();
|
||||||
|
}
|
||||||
|
return VWBExecutionService.instance;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Initialiser le contexte d'exécution
|
||||||
|
*/
|
||||||
|
public initializeContext(context: VWBExecutionContext): void {
|
||||||
|
this.executionContext = context;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Liste des types d'actions VWB connus du catalogue
|
||||||
|
private static readonly VWB_ACTION_TYPES = new Set([
|
||||||
|
// Interactions visuelles
|
||||||
|
'click_anchor',
|
||||||
|
'double_click_anchor',
|
||||||
|
'right_click_anchor',
|
||||||
|
'hover_anchor',
|
||||||
|
'type_text',
|
||||||
|
'type_secret',
|
||||||
|
'focus_anchor',
|
||||||
|
'drag_drop_anchor',
|
||||||
|
'scroll_to_anchor',
|
||||||
|
'keyboard_shortcut',
|
||||||
|
// Contrôle de flux
|
||||||
|
'wait_for_anchor',
|
||||||
|
'visual_condition',
|
||||||
|
'loop_visual',
|
||||||
|
// Extraction de données
|
||||||
|
'extract_text',
|
||||||
|
'extract_table',
|
||||||
|
'screenshot_evidence',
|
||||||
|
'download_to_folder',
|
||||||
|
// Intelligence IA
|
||||||
|
'ai_analyze_text',
|
||||||
|
// Base de données
|
||||||
|
'db_save_data',
|
||||||
|
'db_read_data',
|
||||||
|
// Validation
|
||||||
|
'verify_element_exists',
|
||||||
|
'verify_text_content',
|
||||||
|
]);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier si une étape est une action VWB
|
||||||
|
*/
|
||||||
|
public isVWBStep(step: Step): boolean {
|
||||||
|
// Vérifier d'abord si le type est dans la liste des actions VWB connues
|
||||||
|
const stepType = step.type || step.data?.stepType;
|
||||||
|
if (stepType && VWBExecutionService.VWB_ACTION_TYPES.has(stepType)) {
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Vérification par marqueurs explicites
|
||||||
|
return Boolean(
|
||||||
|
step.data?.isVWBCatalogAction ||
|
||||||
|
step.data?.vwbActionId ||
|
||||||
|
step.action_id?.startsWith('vwb_') ||
|
||||||
|
step.action_id?.includes('catalog_')
|
||||||
|
);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Valider une étape VWB avant exécution
|
||||||
|
*/
|
||||||
|
public async validateStep(step: Step): Promise<VWBActionValidationResult> {
|
||||||
|
if (!this.isVWBStep(step)) {
|
||||||
|
throw new Error(`L'étape ${step.id} n'est pas une action VWB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraire le type d'action depuis plusieurs sources possibles
|
||||||
|
const actionId = step.type || step.data?.stepType || step.data?.vwbActionId || step.action_id || "unknown";
|
||||||
|
const parameters = step.data?.parameters || {};
|
||||||
|
|
||||||
|
try {
|
||||||
|
const validationResult = await catalogService.validateAction({
|
||||||
|
type: actionId,
|
||||||
|
parameters
|
||||||
|
});
|
||||||
|
|
||||||
|
// Vérifier que validationResult est défini
|
||||||
|
if (!validationResult) {
|
||||||
|
// Si pas de résultat, considérer comme valide (pas de validation côté serveur)
|
||||||
|
return {
|
||||||
|
is_valid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: [],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convertir vers le format VWB
|
||||||
|
return {
|
||||||
|
is_valid: validationResult.is_valid ?? true,
|
||||||
|
errors: (validationResult.errors || []).map(error => ({
|
||||||
|
field: 'unknown',
|
||||||
|
message: typeof error === 'string' ? error : error.message || 'Erreur de validation',
|
||||||
|
severity: 'error' as const,
|
||||||
|
})),
|
||||||
|
warnings: (validationResult.warnings || []).map(warning => ({
|
||||||
|
field: 'unknown',
|
||||||
|
message: typeof warning === 'string' ? warning : warning.message || 'Avertissement',
|
||||||
|
})),
|
||||||
|
};
|
||||||
|
} catch (error) {
|
||||||
|
console.error('Erreur lors de la validation VWB:', error);
|
||||||
|
// En cas d'erreur de validation (ex: API non disponible), permettre l'exécution
|
||||||
|
return {
|
||||||
|
is_valid: true,
|
||||||
|
errors: [],
|
||||||
|
warnings: [{
|
||||||
|
field: 'validation',
|
||||||
|
message: `Validation non disponible: ${error instanceof Error ? error.message : 'Erreur inconnue'}`,
|
||||||
|
}],
|
||||||
|
};
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécuter une étape VWB
|
||||||
|
*/
|
||||||
|
public async executeStep(
|
||||||
|
step: Step,
|
||||||
|
options: VWBExecutionOptions = {}
|
||||||
|
): Promise<VWBExecutionResult> {
|
||||||
|
const startTime = Date.now();
|
||||||
|
const {
|
||||||
|
timeout = 30000,
|
||||||
|
retryAttempts = 3,
|
||||||
|
validateBeforeExecution = true,
|
||||||
|
generateEvidence = true
|
||||||
|
} = options;
|
||||||
|
|
||||||
|
if (!this.isVWBStep(step)) {
|
||||||
|
throw new Error(`L'étape ${step.id} n'est pas une action VWB`);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer un contrôleur d'annulation
|
||||||
|
this.currentExecution = new AbortController();
|
||||||
|
this.isExecuting = true;
|
||||||
|
|
||||||
|
try {
|
||||||
|
// Validation préalable si demandée
|
||||||
|
if (validateBeforeExecution) {
|
||||||
|
const validation = await this.validateStep(step);
|
||||||
|
if (!validation.is_valid) {
|
||||||
|
const errorMessages = validation.errors.map(e => e.message).join(', ');
|
||||||
|
throw new Error(`Validation échouée: ${errorMessages}`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extraire le type d'action depuis plusieurs sources possibles
|
||||||
|
const actionId = step.type || step.data?.stepType || step.data?.vwbActionId || step.action_id || "unknown";
|
||||||
|
const parameters = this.prepareParameters(step);
|
||||||
|
|
||||||
|
// Exécuter l'action avec retry
|
||||||
|
let lastError: Error | null = null;
|
||||||
|
for (let attempt = 1; attempt <= retryAttempts; attempt++) {
|
||||||
|
try {
|
||||||
|
const result = await this.executeActionWithTimeout(
|
||||||
|
actionId,
|
||||||
|
parameters,
|
||||||
|
timeout,
|
||||||
|
this.currentExecution.signal
|
||||||
|
);
|
||||||
|
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
|
||||||
|
// Traiter les Evidence si disponibles
|
||||||
|
const evidence = generateEvidence ? await this.processEvidence(result, startTime) : undefined;
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
stepId: step.id,
|
||||||
|
actionId: actionId,
|
||||||
|
duration,
|
||||||
|
evidence,
|
||||||
|
output: result
|
||||||
|
};
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
lastError = error instanceof Error ? error : new Error('Erreur inconnue');
|
||||||
|
|
||||||
|
if (this.currentExecution.signal.aborted) {
|
||||||
|
throw new Error('Exécution annulée par l\'utilisateur');
|
||||||
|
}
|
||||||
|
|
||||||
|
if (attempt < retryAttempts) {
|
||||||
|
console.warn(`Tentative ${attempt} échouée pour ${actionId}, retry...`);
|
||||||
|
await this.delay(1000 * attempt); // Backoff exponentiel
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Toutes les tentatives ont échoué
|
||||||
|
throw lastError || new Error('Échec après toutes les tentatives');
|
||||||
|
|
||||||
|
} catch (error) {
|
||||||
|
const duration = Date.now() - startTime;
|
||||||
|
const executionError: ExecutionError = {
|
||||||
|
stepId: step.id,
|
||||||
|
message: error instanceof Error ? error.message : 'Erreur inconnue',
|
||||||
|
timestamp: new Date(),
|
||||||
|
};
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: false,
|
||||||
|
stepId: step.id,
|
||||||
|
actionId: step.type || step.data?.stepType || step.data?.vwbActionId || step.action_id || "unknown",
|
||||||
|
duration,
|
||||||
|
error: executionError
|
||||||
|
};
|
||||||
|
|
||||||
|
} finally {
|
||||||
|
this.isExecuting = false;
|
||||||
|
this.currentExecution = null;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Exécuter une action avec timeout
|
||||||
|
*/
|
||||||
|
private async executeActionWithTimeout(
|
||||||
|
actionId: string,
|
||||||
|
parameters: Record<string, any>,
|
||||||
|
timeout: number,
|
||||||
|
signal: AbortSignal
|
||||||
|
): Promise<any> {
|
||||||
|
const timeoutPromise = new Promise((_, reject) => {
|
||||||
|
const timeoutId = setTimeout(() => {
|
||||||
|
reject(new Error(`Timeout après ${timeout}ms`));
|
||||||
|
}, timeout);
|
||||||
|
|
||||||
|
signal.addEventListener('abort', () => {
|
||||||
|
clearTimeout(timeoutId);
|
||||||
|
reject(new Error('Exécution annulée'));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
|
||||||
|
const executionPromise = catalogService.executeAction({
|
||||||
|
type: actionId,
|
||||||
|
parameters
|
||||||
|
});
|
||||||
|
|
||||||
|
return Promise.race([executionPromise, timeoutPromise]);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Préparer les paramètres pour l'exécution
|
||||||
|
*/
|
||||||
|
private prepareParameters(step: Step): Record<string, any> {
|
||||||
|
let parameters = { ...step.data?.parameters || {} };
|
||||||
|
|
||||||
|
// Résoudre les variables si contexte disponible
|
||||||
|
if (this.executionContext?.variables) {
|
||||||
|
parameters = this.resolveVariables(parameters, this.executionContext.variables);
|
||||||
|
}
|
||||||
|
|
||||||
|
return parameters;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Résoudre les variables dans les paramètres
|
||||||
|
*/
|
||||||
|
private resolveVariables(
|
||||||
|
parameters: Record<string, any>,
|
||||||
|
variables: Record<string, any>
|
||||||
|
): Record<string, any> {
|
||||||
|
const resolved = { ...parameters };
|
||||||
|
|
||||||
|
for (const [key, value] of Object.entries(resolved)) {
|
||||||
|
if (typeof value === 'string' && value.includes('${')) {
|
||||||
|
// Remplacer les variables ${variableName}
|
||||||
|
resolved[key] = value.replace(/\$\{([^}]+)\}/g, (match, varName) => {
|
||||||
|
return variables[varName] !== undefined ? variables[varName] : match;
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return resolved;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Traiter les Evidence de l'exécution
|
||||||
|
*/
|
||||||
|
private async processEvidence(result: any, executionStartTime: number): Promise<Evidence[]> {
|
||||||
|
const evidence: Evidence[] = [];
|
||||||
|
|
||||||
|
if (result?.evidence) {
|
||||||
|
// Si l'API retourne des Evidence directement
|
||||||
|
if (Array.isArray(result.evidence)) {
|
||||||
|
evidence.push(...result.evidence);
|
||||||
|
} else {
|
||||||
|
evidence.push(result.evidence);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Créer une Evidence de base si aucune n'est fournie
|
||||||
|
if (evidence.length === 0 && result?.success) {
|
||||||
|
evidence.push({
|
||||||
|
contract: 'vwb_evidence',
|
||||||
|
version: '1.0',
|
||||||
|
id: `evidence_${Date.now()}`,
|
||||||
|
action_id: 'execution_success',
|
||||||
|
captured_at: new Date().toISOString(),
|
||||||
|
screenshot_base64: '',
|
||||||
|
execution_time_ms: Date.now() - executionStartTime,
|
||||||
|
success: true,
|
||||||
|
timestamp: new Date().toISOString(),
|
||||||
|
data: {
|
||||||
|
result: result,
|
||||||
|
message: 'Action exécutée avec succès'
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
return evidence;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Annuler l'exécution en cours
|
||||||
|
*/
|
||||||
|
public cancelExecution(): void {
|
||||||
|
if (this.currentExecution) {
|
||||||
|
this.currentExecution.abort();
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Vérifier si une exécution est en cours
|
||||||
|
*/
|
||||||
|
public isExecutionRunning(): boolean {
|
||||||
|
return this.isExecuting;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Obtenir le contexte d'exécution actuel
|
||||||
|
*/
|
||||||
|
public getExecutionContext(): VWBExecutionContext | null {
|
||||||
|
return this.executionContext;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Nettoyer le contexte d'exécution
|
||||||
|
*/
|
||||||
|
public cleanup(): void {
|
||||||
|
this.cancelExecution();
|
||||||
|
this.executionContext = null;
|
||||||
|
this.isExecuting = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Utilitaire pour attendre
|
||||||
|
*/
|
||||||
|
private delay(ms: number): Promise<void> {
|
||||||
|
return new Promise(resolve => setTimeout(resolve, ms));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Instance singleton
|
||||||
|
export const vwbExecutionService = VWBExecutionService.getInstance();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Hook pour utiliser le service d'exécution VWB
|
||||||
|
*/
|
||||||
|
export const useVWBExecutionService = () => {
|
||||||
|
return {
|
||||||
|
service: vwbExecutionService,
|
||||||
|
isVWBStep: (step: Step) => vwbExecutionService.isVWBStep(step),
|
||||||
|
executeStep: (step: Step, options?: VWBExecutionOptions) =>
|
||||||
|
vwbExecutionService.executeStep(step, options),
|
||||||
|
validateStep: (step: Step) => vwbExecutionService.validateStep(step),
|
||||||
|
cancelExecution: () => vwbExecutionService.cancelExecution(),
|
||||||
|
isExecutionRunning: () => vwbExecutionService.isExecutionRunning(),
|
||||||
|
};
|
||||||
|
};
|
||||||
Reference in New Issue
Block a user