Fix: Ajout de toutes les actions VWB à la liste de détection
Mise à jour KNOWN_VWB_ACTIONS pour inclure: - double_click_anchor, right_click_anchor, hover_anchor, drag_drop_anchor - keyboard_shortcut - ai_analyze_text - extract_table, download_to_folder - db_save_data, db_read_data - visual_condition, loop_visual Corrige le problème "Type d'étape non reconnu" pour ces actions. Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,418 @@
|
||||
/**
|
||||
* Hook d'intégration des étapes VWB - Gestion des actions du catalogue dans le workflow
|
||||
* Auteur : Dom, Alice, Kiro - 12 janvier 2026
|
||||
*
|
||||
* Ce hook gère l'intégration complète des actions VisionOnly du catalogue
|
||||
* dans le système de workflow, incluant la création, la configuration et l'exécution.
|
||||
*
|
||||
* Version 2.0 - Intégration avec StepTypeResolver pour une détection robuste
|
||||
*/
|
||||
|
||||
import { useState, useCallback, useMemo, useRef } from 'react';
|
||||
import { catalogService } from '../services/catalogService';
|
||||
import { VWBCatalogAction } from '../types/catalog';
|
||||
import { Step, StepType, StepData, StepExecutionState } from '../types';
|
||||
import { stepTypeResolver, StepTypeResolutionResult } from '../services/StepTypeResolver';
|
||||
// Import du catalogue statique pour fallback direct
|
||||
import { getStaticActionById, findActionWithFallback } from '../data/staticCatalog';
|
||||
|
||||
interface VWBStepIntegrationState {
|
||||
vwbActions: Map<string, VWBCatalogAction>;
|
||||
isLoading: boolean;
|
||||
error: string | null;
|
||||
resolutionCache: Map<string, StepTypeResolutionResult>;
|
||||
lastCacheUpdate: number;
|
||||
}
|
||||
|
||||
interface VWBStepIntegrationMethods {
|
||||
createVWBStep: (actionId: string, position: { x: number; y: number }) => Promise<Step | null>;
|
||||
isVWBAction: (stepType: string) => boolean;
|
||||
getVWBAction: (actionId: string) => VWBCatalogAction | null;
|
||||
loadVWBAction: (actionId: string) => Promise<VWBCatalogAction | null>;
|
||||
validateVWBStep: (step: Step) => Promise<boolean>;
|
||||
convertDragDataToVWBStep: (dragData: string, position: { x: number; y: number }) => Promise<Step | null>;
|
||||
resolveStepType: (step: Step) => Promise<StepTypeResolutionResult>;
|
||||
invalidateResolutionCache: () => void;
|
||||
}
|
||||
|
||||
/**
|
||||
* Hook pour l'intégration des étapes VWB dans le workflow
|
||||
* Version 2.0 avec intégration StepTypeResolver
|
||||
*/
|
||||
export const useVWBStepIntegration = (): {
|
||||
state: VWBStepIntegrationState;
|
||||
methods: VWBStepIntegrationMethods;
|
||||
} => {
|
||||
const [vwbActions, setVwbActions] = useState<Map<string, VWBCatalogAction>>(new Map());
|
||||
const [isLoading, setIsLoading] = useState(false);
|
||||
const [error, setError] = useState<string | null>(null);
|
||||
const [resolutionCache, setResolutionCache] = useState<Map<string, StepTypeResolutionResult>>(new Map());
|
||||
const [lastCacheUpdate, setLastCacheUpdate] = useState(Date.now());
|
||||
|
||||
// Références pour optimisation
|
||||
const loadingActionsRef = useRef<Set<string>>(new Set());
|
||||
const debounceTimeoutRef = useRef<ReturnType<typeof setTimeout> | undefined>(undefined);
|
||||
|
||||
// Créer une étape VWB à partir d'une action du catalogue
|
||||
const createVWBStep = useCallback(async (
|
||||
actionId: string,
|
||||
position: { x: number; y: number }
|
||||
): Promise<Step | null> => {
|
||||
setIsLoading(true);
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
// Charger les détails de l'action depuis le catalogue
|
||||
const action = await catalogService.getActionDetails(actionId);
|
||||
if (!action) {
|
||||
throw new Error(`Action VWB non trouvée : ${actionId}`);
|
||||
}
|
||||
|
||||
// Mettre en cache l'action
|
||||
setVwbActions(prev => new Map(prev).set(actionId, action));
|
||||
|
||||
// Créer les paramètres par défaut
|
||||
const defaultParameters: Record<string, any> = {};
|
||||
Object.entries(action.parameters).forEach(([paramName, paramConfig]) => {
|
||||
if (paramConfig.default !== undefined) {
|
||||
defaultParameters[paramName] = paramConfig.default;
|
||||
}
|
||||
});
|
||||
|
||||
// Créer les données de l'étape
|
||||
const stepData: StepData = {
|
||||
label: action.name,
|
||||
stepType: actionId as StepType, // Utiliser l'ID de l'action comme type
|
||||
parameters: defaultParameters,
|
||||
isVWBCatalogAction: true,
|
||||
vwbActionId: actionId,
|
||||
};
|
||||
|
||||
// Créer l'étape complète
|
||||
const step: Step = {
|
||||
id: `vwb_step_${Date.now()}_${Math.random().toString(36).substring(2, 9)}`,
|
||||
type: actionId as StepType,
|
||||
name: action.name,
|
||||
position,
|
||||
data: stepData,
|
||||
executionState: StepExecutionState.IDLE,
|
||||
validationErrors: [],
|
||||
};
|
||||
|
||||
return step;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Erreur lors de la création de l\'étape VWB';
|
||||
setError(errorMessage);
|
||||
console.error('Erreur lors de la création de l\'étape VWB:', err);
|
||||
return null;
|
||||
} finally {
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Vérifier si un type d'étape correspond à une action VWB (utilise StepTypeResolver)
|
||||
const isVWBAction = useCallback((stepType: string): boolean => {
|
||||
try {
|
||||
// Créer une étape temporaire pour la détection
|
||||
const tempStep: Step = {
|
||||
id: 'temp',
|
||||
type: stepType as StepType,
|
||||
name: 'temp',
|
||||
position: { x: 0, y: 0 },
|
||||
data: {
|
||||
label: 'temp',
|
||||
stepType: stepType as StepType,
|
||||
parameters: {}
|
||||
},
|
||||
executionState: StepExecutionState.IDLE,
|
||||
validationErrors: []
|
||||
};
|
||||
|
||||
// Utiliser le StepTypeResolver pour une détection robuste
|
||||
return stepTypeResolver.isVWBAction(tempStep);
|
||||
} catch (error) {
|
||||
console.error('❌ [useVWBStepIntegration] Erreur détection VWB:', error);
|
||||
|
||||
// Fallback vers l'ancienne logique
|
||||
return vwbActions.has(stepType) ||
|
||||
stepType.startsWith('vwb_') ||
|
||||
stepType.includes('catalog_') ||
|
||||
stepType.includes('_anchor') ||
|
||||
stepType.includes('_text');
|
||||
}
|
||||
}, [vwbActions]);
|
||||
|
||||
// Obtenir une action VWB depuis le cache
|
||||
const getVWBAction = useCallback((actionId: string): VWBCatalogAction | null => {
|
||||
return vwbActions.get(actionId) || null;
|
||||
}, [vwbActions]);
|
||||
|
||||
// Charger une action VWB - Priorité au catalogue statique (instantané)
|
||||
const loadVWBAction = useCallback(async (actionId: string): Promise<VWBCatalogAction | null> => {
|
||||
// Vérifier d'abord le cache mémoire
|
||||
const cachedAction = vwbActions.get(actionId);
|
||||
if (cachedAction) {
|
||||
console.log('📦 [useVWBStepIntegration] Action trouvée en cache:', actionId);
|
||||
return cachedAction;
|
||||
}
|
||||
|
||||
// Éviter les chargements multiples simultanés
|
||||
if (loadingActionsRef.current.has(actionId)) {
|
||||
// Attendre brièvement que le chargement en cours se termine (max 500ms)
|
||||
return new Promise((resolve) => {
|
||||
let attempts = 0;
|
||||
const checkInterval = setInterval(() => {
|
||||
attempts++;
|
||||
if (!loadingActionsRef.current.has(actionId) || attempts > 10) {
|
||||
clearInterval(checkInterval);
|
||||
resolve(vwbActions.get(actionId) || null);
|
||||
}
|
||||
}, 50);
|
||||
});
|
||||
}
|
||||
|
||||
loadingActionsRef.current.add(actionId);
|
||||
setIsLoading(true);
|
||||
|
||||
try {
|
||||
// PRIORITÉ 1: Catalogue statique (instantané, pas de réseau)
|
||||
let staticAction = getStaticActionById(actionId);
|
||||
|
||||
// Si pas trouvé exactement, essayer avec recherche fuzzy
|
||||
if (!staticAction) {
|
||||
staticAction = findActionWithFallback(actionId);
|
||||
}
|
||||
|
||||
if (staticAction) {
|
||||
setVwbActions(prev => new Map(prev).set(actionId, staticAction as VWBCatalogAction));
|
||||
console.log('✅ [useVWBStepIntegration] Action VWB chargée (statique):', actionId, staticAction.name);
|
||||
setError(null);
|
||||
return staticAction as VWBCatalogAction;
|
||||
}
|
||||
|
||||
// PRIORITÉ 2: Service catalogue (si statique n'a pas trouvé)
|
||||
// Avec timeout court pour ne pas bloquer l'UI
|
||||
try {
|
||||
const timeoutPromise = new Promise<null>((_, reject) =>
|
||||
setTimeout(() => reject(new Error('Timeout')), 1500)
|
||||
);
|
||||
const servicePromise = catalogService.getActionDetails(actionId);
|
||||
|
||||
const action = await Promise.race([servicePromise, timeoutPromise]);
|
||||
if (action) {
|
||||
setVwbActions(prev => new Map(prev).set(actionId, action));
|
||||
console.log('✅ [useVWBStepIntegration] Action VWB chargée (service):', actionId);
|
||||
return action;
|
||||
}
|
||||
} catch (serviceErr) {
|
||||
console.warn('⚠️ [useVWBStepIntegration] Service catalogue indisponible:', actionId);
|
||||
}
|
||||
|
||||
console.warn('⚠️ [useVWBStepIntegration] Action VWB non trouvée:', actionId);
|
||||
setError(`Action VWB non trouvée: ${actionId}`);
|
||||
return null;
|
||||
} catch (err) {
|
||||
const errorMessage = err instanceof Error ? err.message : 'Erreur de chargement';
|
||||
console.error('❌ [useVWBStepIntegration] Erreur chargement action:', errorMessage);
|
||||
setError(`Erreur chargement action ${actionId}: ${errorMessage}`);
|
||||
return null;
|
||||
} finally {
|
||||
loadingActionsRef.current.delete(actionId);
|
||||
setIsLoading(false);
|
||||
}
|
||||
}, [vwbActions]);
|
||||
|
||||
// Valider une étape VWB
|
||||
const validateVWBStep = useCallback(async (step: Step): Promise<boolean> => {
|
||||
if (!step.data.isVWBCatalogAction || !step.data.vwbActionId) {
|
||||
return true; // Pas une étape VWB, validation OK
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await catalogService.validateAction({
|
||||
type: step.data.vwbActionId,
|
||||
parameters: step.data.parameters,
|
||||
});
|
||||
|
||||
return result.is_valid;
|
||||
} catch (err) {
|
||||
console.error('Erreur lors de la validation de l\'étape VWB:', err);
|
||||
return false;
|
||||
}
|
||||
}, []);
|
||||
|
||||
// Résoudre le type d'une étape avec le StepTypeResolver
|
||||
const resolveStepType = useCallback(async (step: Step): Promise<StepTypeResolutionResult> => {
|
||||
const cacheKey = `${step.type}_${step.data?.vwbActionId || 'none'}`;
|
||||
|
||||
// Vérifier le cache local
|
||||
const cached = resolutionCache.get(cacheKey);
|
||||
if (cached && Date.now() - cached.timestamp < 30000) { // Cache 30 secondes
|
||||
return cached;
|
||||
}
|
||||
|
||||
try {
|
||||
const result = await stepTypeResolver.resolveParameterConfig(step, {
|
||||
enableCache: true,
|
||||
enableLogging: process.env.NODE_ENV === 'development',
|
||||
fallbackToEmpty: true
|
||||
});
|
||||
|
||||
// Mettre en cache le résultat
|
||||
setResolutionCache(prev => new Map(prev).set(cacheKey, result));
|
||||
setLastCacheUpdate(Date.now());
|
||||
|
||||
return result;
|
||||
} catch (error) {
|
||||
console.error('❌ [useVWBStepIntegration] Erreur résolution type:', error);
|
||||
|
||||
// Fallback basique
|
||||
const fallbackResult: StepTypeResolutionResult = {
|
||||
stepType: step.type as string,
|
||||
isVWBAction: isVWBAction(step.type as string),
|
||||
isStandardType: false,
|
||||
parameterConfig: [],
|
||||
detectionMethods: { fallback: true },
|
||||
resolutionSource: 'fallback',
|
||||
timestamp: Date.now()
|
||||
};
|
||||
|
||||
return fallbackResult;
|
||||
}
|
||||
}, [resolutionCache, isVWBAction]);
|
||||
|
||||
// Invalider le cache de résolution
|
||||
const invalidateResolutionCache = useCallback(() => {
|
||||
setResolutionCache(new Map());
|
||||
setLastCacheUpdate(Date.now());
|
||||
stepTypeResolver.invalidateCache();
|
||||
console.log('🗑️ [useVWBStepIntegration] Cache de résolution invalidé');
|
||||
}, []);
|
||||
// Convertir les données de drag-and-drop en étape VWB
|
||||
const convertDragDataToVWBStep = useCallback(async (
|
||||
dragData: string,
|
||||
position: { x: number; y: number }
|
||||
): Promise<Step | null> => {
|
||||
// Vérifier si c'est une action du catalogue
|
||||
if (dragData.startsWith('catalog:')) {
|
||||
const actionId = dragData.replace('catalog:', '');
|
||||
return await createVWBStep(actionId, position);
|
||||
}
|
||||
|
||||
// Vérifier si c'est une action VWB directe
|
||||
if (isVWBAction(dragData)) {
|
||||
return await createVWBStep(dragData, position);
|
||||
}
|
||||
|
||||
return null; // Pas une action VWB
|
||||
}, [createVWBStep, isVWBAction]);
|
||||
|
||||
// État exposé (mémorisé pour éviter les re-renders inutiles)
|
||||
const state: VWBStepIntegrationState = useMemo(() => ({
|
||||
vwbActions,
|
||||
isLoading,
|
||||
error,
|
||||
resolutionCache,
|
||||
lastCacheUpdate,
|
||||
}), [vwbActions, isLoading, error, resolutionCache, lastCacheUpdate]);
|
||||
|
||||
// Méthodes exposées (mémorisées pour éviter les boucles infinies dans les useEffect)
|
||||
const methods: VWBStepIntegrationMethods = useMemo(() => ({
|
||||
createVWBStep,
|
||||
isVWBAction,
|
||||
getVWBAction,
|
||||
loadVWBAction,
|
||||
validateVWBStep,
|
||||
convertDragDataToVWBStep,
|
||||
resolveStepType,
|
||||
invalidateResolutionCache,
|
||||
}), [
|
||||
createVWBStep,
|
||||
isVWBAction,
|
||||
getVWBAction,
|
||||
loadVWBAction,
|
||||
validateVWBStep,
|
||||
convertDragDataToVWBStep,
|
||||
resolveStepType,
|
||||
invalidateResolutionCache,
|
||||
]);
|
||||
|
||||
return { state, methods };
|
||||
};
|
||||
|
||||
// Liste des actions VWB connues pour détection basée sur le type
|
||||
const KNOWN_VWB_ACTIONS = [
|
||||
// Vision UI
|
||||
'click_anchor', 'double_click_anchor', 'right_click_anchor',
|
||||
'hover_anchor', 'drag_drop_anchor',
|
||||
'type_text', 'type_secret', 'wait_for_anchor',
|
||||
'extract_text', 'screenshot_evidence', 'scroll_to_anchor',
|
||||
'focus_anchor', 'hotkey', 'keyboard_shortcut',
|
||||
// Navigation
|
||||
'navigate_to_url', 'browser_back',
|
||||
// Validation
|
||||
'verify_element_exists', 'verify_text_content',
|
||||
// Intelligence
|
||||
'ai_analyze_text',
|
||||
// Data
|
||||
'extract_table', 'download_to_folder',
|
||||
// Database
|
||||
'db_save_data', 'db_read_data',
|
||||
// Control
|
||||
'visual_condition', 'loop_visual'
|
||||
];
|
||||
|
||||
/**
|
||||
* Fonction utilitaire pour détecter si un type est une action VWB
|
||||
*/
|
||||
const isVWBActionType = (stepType: string): boolean => {
|
||||
if (KNOWN_VWB_ACTIONS.includes(stepType)) return true;
|
||||
if (stepType.includes('_anchor')) return true;
|
||||
if (stepType.includes('_text') && stepType !== 'type') return true;
|
||||
if (stepType.includes('_secret')) return true;
|
||||
if (stepType.startsWith('vwb_')) return true;
|
||||
if (stepType.includes('catalog_')) return true;
|
||||
return false;
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook utilitaire pour vérifier si une étape est une action VWB
|
||||
*/
|
||||
export const useIsVWBStep = (step: Step | null): boolean => {
|
||||
return useMemo(() => {
|
||||
if (!step) return false;
|
||||
return Boolean(
|
||||
step.data.isVWBCatalogAction ||
|
||||
step.data.vwbActionId ||
|
||||
isVWBActionType(step.type as string)
|
||||
);
|
||||
}, [step]);
|
||||
};
|
||||
|
||||
/**
|
||||
* Hook utilitaire pour obtenir l'ID de l'action VWB d'une étape
|
||||
* Amélioration: détecte aussi les actions VWB basées sur le type même sans flag
|
||||
*/
|
||||
export const useVWBActionId = (step: Step | null): string | null => {
|
||||
return useMemo(() => {
|
||||
if (!step) return null;
|
||||
|
||||
// Si le flag est explicitement défini, l'utiliser
|
||||
if (step.data.isVWBCatalogAction) {
|
||||
return step.data.vwbActionId || step.type;
|
||||
}
|
||||
|
||||
// Si vwbActionId est défini, l'utiliser
|
||||
if (step.data.vwbActionId) {
|
||||
return step.data.vwbActionId;
|
||||
}
|
||||
|
||||
// Fallback: détecter basé sur le type d'étape
|
||||
if (isVWBActionType(step.type as string)) {
|
||||
console.log('🔍 [useVWBActionId] Détection VWB par type:', step.type);
|
||||
return step.type;
|
||||
}
|
||||
|
||||
return null;
|
||||
}, [step]);
|
||||
};
|
||||
Reference in New Issue
Block a user