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