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)
};
};

View File

@@ -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(),
};
};