From d2955ec1a1e254c30ee629d1cbfb7c4437777448 Mon Sep 17 00:00:00 2001 From: Dom Date: Tue, 20 Jan 2026 17:35:24 +0100 Subject: [PATCH] =?UTF-8?q?fix(vwb):=20Corriger=20l'ex=C3=A9cution=20VWB?= =?UTF-8?q?=20pour=20toutes=20les=20=C3=A9tapes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 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 --- .../frontend/src/hooks/useVWBExecution.ts | 539 ++++++++++++++++++ .../src/services/vwbExecutionService.ts | 444 +++++++++++++++ 2 files changed, 983 insertions(+) create mode 100644 visual_workflow_builder/frontend/src/hooks/useVWBExecution.ts create mode 100644 visual_workflow_builder/frontend/src/services/vwbExecutionService.ts diff --git a/visual_workflow_builder/frontend/src/hooks/useVWBExecution.ts b/visual_workflow_builder/frontend/src/hooks/useVWBExecution.ts new file mode 100644 index 000000000..1f4132ed4 --- /dev/null +++ b/visual_workflow_builder/frontend/src/hooks/useVWBExecution.ts @@ -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({ + 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); + + 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 => { + // 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) + }; +}; \ No newline at end of file diff --git a/visual_workflow_builder/frontend/src/services/vwbExecutionService.ts b/visual_workflow_builder/frontend/src/services/vwbExecutionService.ts new file mode 100644 index 000000000..e6c0fcaca --- /dev/null +++ b/visual_workflow_builder/frontend/src/services/vwbExecutionService.ts @@ -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; + 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 { + 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 { + 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, + timeout: number, + signal: AbortSignal + ): Promise { + 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 { + 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, + variables: Record + ): Record { + 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 { + 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 { + 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(), + }; +}; \ No newline at end of file