diff --git a/visual_workflow_builder/backend/actions/vision_ui/__init__.py b/visual_workflow_builder/backend/actions/vision_ui/__init__.py new file mode 100644 index 000000000..56d10fbd9 --- /dev/null +++ b/visual_workflow_builder/backend/actions/vision_ui/__init__.py @@ -0,0 +1,45 @@ +""" +Actions Vision UI VWB - Module d'initialisation + +Auteur : Dom, Claude - 14 janvier 2026 + +Ce module contient les actions d'interface utilisateur basées sur la vision +pour le Visual Workflow Builder. + +Actions disponibles : +- VWBClickAnchorAction : Clic sur ancre visuelle +- VWBTypeTextAction : Saisie de texte +- VWBWaitForAnchorAction : Attente d'ancre visuelle +- VWBFocusAnchorAction : Donner le focus à un élément +- VWBTypeSecretAction : Saisie sécurisée (mots de passe) +- VWBScrollToAnchorAction : Défiler vers un élément +- VWBExtractTextAction : Extraire du texte d'une zone +- VWBSurvolElementAction : Survol d'élément (hover) +- VWBGlisserDeposerAction : Glisser-déposer (drag & drop) +""" + +from .click_anchor import VWBClickAnchorAction +from .type_text import VWBTypeTextAction +from .wait_for_anchor import VWBWaitForAnchorAction +from .focus_anchor import VWBFocusAnchorAction +from .type_secret import VWBTypeSecretAction +from .scroll_to_anchor import VWBScrollToAnchorAction +from .extract_text import VWBExtractTextAction +from .survol_element import VWBSurvolElementAction +from .glisser_deposer import VWBGlisserDeposerAction + +__all__ = [ + 'VWBClickAnchorAction', + 'VWBTypeTextAction', + 'VWBWaitForAnchorAction', + 'VWBFocusAnchorAction', + 'VWBTypeSecretAction', + 'VWBScrollToAnchorAction', + 'VWBExtractTextAction', + 'VWBSurvolElementAction', + 'VWBGlisserDeposerAction', +] + +__version__ = '1.2.0' +__author__ = 'Dom, Claude' +__date__ = '14 janvier 2026' \ No newline at end of file diff --git a/visual_workflow_builder/backend/actions/vision_ui/glisser_deposer.py b/visual_workflow_builder/backend/actions/vision_ui/glisser_deposer.py new file mode 100644 index 000000000..a1e01e2fd --- /dev/null +++ b/visual_workflow_builder/backend/actions/vision_ui/glisser_deposer.py @@ -0,0 +1,345 @@ +""" +Action Glisser-Déposer - Déplacer un élément vers une destination +Auteur : Dom, Claude - 14 janvier 2026 + +Cette action permet de glisser un élément source vers une destination, +tous deux identifiés visuellement. +""" + +from typing import Dict, Any, List, Optional, Tuple +from datetime import datetime +import time + +from ..base_action import BaseVWBAction, VWBActionResult, VWBActionStatus +from ...contracts.error import VWBErrorType, create_vwb_error +from ...contracts.visual_anchor import VWBVisualAnchor + + +class VWBGlisserDeposerAction(BaseVWBAction): + """ + Action de glisser-déposer entre deux éléments visuels. + + Permet de : + - Glisser un élément source vers une destination + - Réorganiser des listes + - Déplacer des fichiers + - Associer des éléments par drag & drop + """ + + def __init__( + self, + action_id: str, + parameters: Dict[str, Any], + screen_capturer=None + ): + """ + Initialise l'action de glisser-déposer. + + Args: + action_id: Identifiant unique de l'action + parameters: Paramètres incluant source et destination + screen_capturer: Instance du ScreenCapturer (optionnel) + """ + super().__init__( + action_id=action_id, + name="Glisser-Déposer", + description="Glisse un élément vers une destination", + parameters=parameters, + screen_capturer=screen_capturer + ) + + # Éléments source et destination + self.ancre_source: Optional[VWBVisualAnchor] = ( + parameters.get('ancre_source') or + parameters.get('source_anchor') or + parameters.get('visual_anchor') + ) + self.ancre_destination: Optional[VWBVisualAnchor] = ( + parameters.get('ancre_destination') or + parameters.get('destination_anchor') + ) + + # Décalages optionnels + self.decalage_source_x = parameters.get('decalage_source_x', parameters.get('source_offset_x', 0)) + self.decalage_source_y = parameters.get('decalage_source_y', parameters.get('source_offset_y', 0)) + self.decalage_dest_x = parameters.get('decalage_dest_x', parameters.get('dest_offset_x', 0)) + self.decalage_dest_y = parameters.get('decalage_dest_y', parameters.get('dest_offset_y', 0)) + + # Timing + self.delai_avant_glisser_ms = parameters.get('delai_avant_glisser_ms', parameters.get('hold_before_drag_ms', 100)) + self.duree_glissement_ms = parameters.get('duree_glissement_ms', parameters.get('drag_duration_ms', 500)) + self.delai_avant_deposer_ms = parameters.get('delai_avant_deposer_ms', parameters.get('hold_before_drop_ms', 100)) + + # Configuration + self.seuil_confiance = parameters.get('seuil_confiance', parameters.get('confidence_threshold', 0.7)) + + # Humanisation + self.humaniser = parameters.get('humaniser', parameters.get('humanize', True)) + self.profil_humain = parameters.get('profil_humain', parameters.get('humanize_profile', 'normal')) + + def validate_parameters(self) -> List[str]: + """Valide les paramètres de l'action.""" + erreurs = [] + + if not self.ancre_source: + erreurs.append("Ancre source requise") + + if not self.ancre_destination: + erreurs.append("Ancre destination requise") + + if self.duree_glissement_ms < 100: + erreurs.append("Durée de glissement minimum: 100ms") + + if self.duree_glissement_ms > 10000: + erreurs.append("Durée de glissement maximum: 10 secondes") + + return erreurs + + def execute_core(self, step_id: str) -> VWBActionResult: + """ + Exécute le glisser-déposer. + + Args: + step_id: Identifiant de l'étape + + Returns: + Résultat d'exécution + """ + start_time = datetime.now() + + try: + # Trouver la source + pos_source = self._trouver_element(self.ancre_source) + if not pos_source: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.ELEMENT_NOT_FOUND, + message="Élément source non trouvé" + ) + + # Trouver la destination + pos_dest = self._trouver_element(self.ancre_destination) + if not pos_dest: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.ELEMENT_NOT_FOUND, + message="Élément destination non trouvé" + ) + + # Appliquer les décalages + source_x = pos_source[0] + self.decalage_source_x + source_y = pos_source[1] + self.decalage_source_y + dest_x = pos_dest[0] + self.decalage_dest_x + dest_y = pos_dest[1] + self.decalage_dest_y + + # Effectuer le glisser-déposer + succes = self._glisser_deposer(source_x, source_y, dest_x, dest_y) + + if not succes: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.SYSTEM_ERROR, + message="Échec du glisser-déposer" + ) + + end_time = datetime.now() + execution_time = (end_time - start_time).total_seconds() * 1000 + + nom_source = self._get_nom_ancre(self.ancre_source) + nom_dest = self._get_nom_ancre(self.ancre_destination) + print(f"🖱️ Glissé '{nom_source}' vers '{nom_dest}'") + print(f" De ({source_x}, {source_y}) à ({dest_x}, {dest_y})") + + return VWBActionResult( + action_id=self.action_id, + step_id=step_id, + status=VWBActionStatus.SUCCESS, + start_time=start_time, + end_time=end_time, + execution_time_ms=execution_time, + output_data={ + 'source': {'x': source_x, 'y': source_y, 'nom': nom_source}, + 'destination': {'x': dest_x, 'y': dest_y, 'nom': nom_dest}, + 'duree_glissement_ms': self.duree_glissement_ms + }, + evidence_list=self.evidence_list.copy() + ) + + except Exception as e: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.SYSTEM_ERROR, + message=f"Erreur: {str(e)}", + technical_details={'exception': str(e)} + ) + + def _trouver_element(self, ancre) -> Optional[Tuple[int, int]]: + """ + Trouve la position d'un élément sur l'écran. + + Args: + ancre: Ancre visuelle à rechercher + + Returns: + Tuple (x, y) ou None si non trouvé + """ + if not ancre: + return None + + try: + from ...catalog_routes import find_visual_anchor_on_screen + + image_ancre = None + bounding_box = None + + if isinstance(ancre, dict): + image_ancre = ancre.get('screenshot') or ancre.get('image_base64') + bounding_box = ancre.get('bounding_box') + elif isinstance(ancre, VWBVisualAnchor): + image_ancre = ancre.screenshot_base64 + if ancre.has_bounding_box(): + bounding_box = ancre.bounding_box + + # Recherche visuelle + if image_ancre: + resultat = find_visual_anchor_on_screen( + anchor_image_base64=image_ancre, + confidence_threshold=self.seuil_confiance, + bounding_box=bounding_box + ) + + if resultat and resultat.get('found'): + return (resultat.get('center_x', 0), resultat.get('center_y', 0)) + + # Fallback sur coordonnées statiques + if bounding_box: + x = int(bounding_box.get('x', 0) + bounding_box.get('width', 0) / 2) + y = int(bounding_box.get('y', 0) + bounding_box.get('height', 0) / 2) + return (x, y) + + return None + + except Exception as e: + print(f"⚠️ Erreur recherche: {e}") + return None + + def _glisser_deposer(self, src_x: int, src_y: int, dst_x: int, dst_y: int) -> bool: + """ + Effectue le glisser-déposer. + + Args: + src_x, src_y: Position source + dst_x, dst_y: Position destination + + Returns: + True si succès + """ + try: + if self.humaniser: + return self._glisser_deposer_humanise(src_x, src_y, dst_x, dst_y) + else: + return self._glisser_deposer_direct(src_x, src_y, dst_x, dst_y) + + except Exception as e: + print(f"❌ Erreur glisser-déposer: {e}") + return False + + def _glisser_deposer_humanise(self, src_x: int, src_y: int, dst_x: int, dst_y: int) -> bool: + """Glisser-déposer avec comportement humain.""" + try: + from ...utils.humanizer import Humanizer, HumanProfile + import pyautogui + + profils = { + 'rapide': HumanProfile.FAST, + 'fast': HumanProfile.FAST, + 'normal': HumanProfile.NORMAL, + 'lent': HumanProfile.SLOW, + 'slow': HumanProfile.SLOW, + 'furtif': HumanProfile.STEALTH, + 'stealth': HumanProfile.STEALTH, + } + profil = profils.get(self.profil_humain, HumanProfile.NORMAL) + humanizer = Humanizer(profile=profil) + + # 1. Déplacer vers la source avec mouvement naturel + src_x_h, src_y_h = humanizer.humanize_position(src_x, src_y) + humanizer.move_to(src_x_h, src_y_h) + + # 2. Petit délai avant de cliquer + humanizer.wait(50, 100) + + # 3. Appuyer sur le bouton (mouseDown) + pyautogui.mouseDown() + + # 4. Attendre un peu (simuler le "grab") + humanizer.wait(self.delai_avant_glisser_ms, int(self.delai_avant_glisser_ms * 1.3)) + + # 5. Glisser vers la destination avec courbe naturelle + dst_x_h, dst_y_h = humanizer.humanize_position(dst_x, dst_y) + duration = self.duree_glissement_ms / 1000 + + # Mouvement avec variation de vitesse + humanizer.move_to(dst_x_h, dst_y_h, duration=duration) + + # 6. Attendre un peu avant de relâcher + humanizer.wait(self.delai_avant_deposer_ms, int(self.delai_avant_deposer_ms * 1.3)) + + # 7. Relâcher le bouton (mouseUp) + pyautogui.mouseUp() + + return True + + except ImportError as e: + print(f"⚠️ Module non disponible: {e}, fallback direct") + return self._glisser_deposer_direct(src_x, src_y, dst_x, dst_y) + + def _glisser_deposer_direct(self, src_x: int, src_y: int, dst_x: int, dst_y: int) -> bool: + """Glisser-déposer direct avec pyautogui.""" + try: + import pyautogui + + # Utiliser la fonction drag de pyautogui + pyautogui.moveTo(src_x, src_y, duration=0.2) + time.sleep(self.delai_avant_glisser_ms / 1000) + + pyautogui.drag( + dst_x - src_x, + dst_y - src_y, + duration=self.duree_glissement_ms / 1000, + button='left' + ) + + return True + + except ImportError: + print("⚠️ pyautogui non disponible - simulation") + return True + + def _get_nom_ancre(self, ancre) -> str: + """Retourne le nom d'une ancre.""" + if isinstance(ancre, dict): + return ancre.get('name', ancre.get('nom', 'Élément')) + elif isinstance(ancre, VWBVisualAnchor): + return ancre.name + return "Élément" + + def get_action_info(self) -> Dict[str, Any]: + """Retourne les informations de l'action.""" + return { + 'action_id': self.action_id, + 'name': self.name, + 'description': self.description, + 'type': 'glisser_deposer', + 'parameters': { + 'source': self._get_nom_ancre(self.ancre_source), + 'destination': self._get_nom_ancre(self.ancre_destination), + 'duree_glissement_ms': self.duree_glissement_ms, + 'humaniser': self.humaniser + }, + 'status': self.current_status.value + } diff --git a/visual_workflow_builder/backend/actions/vision_ui/survol_element.py b/visual_workflow_builder/backend/actions/vision_ui/survol_element.py new file mode 100644 index 000000000..e913f2550 --- /dev/null +++ b/visual_workflow_builder/backend/actions/vision_ui/survol_element.py @@ -0,0 +1,282 @@ +""" +Action Survol Élément - Survoler un élément visuel avec la souris +Auteur : Dom, Claude - 14 janvier 2026 + +Cette action permet de survoler un élément identifié visuellement, +utile pour déclencher des menus contextuels ou des tooltips. +""" + +from typing import Dict, Any, List, Optional, Tuple +from datetime import datetime +import time + +from ..base_action import BaseVWBAction, VWBActionResult, VWBActionStatus +from ...contracts.error import VWBErrorType, create_vwb_error +from ...contracts.visual_anchor import VWBVisualAnchor + + +class VWBSurvolElementAction(BaseVWBAction): + """ + Action de survol d'élément visuel. + + Déplace la souris sur un élément identifié par ancre visuelle + et maintient le survol pendant une durée configurable. + Utile pour : + - Afficher des tooltips + - Déclencher des menus au survol + - Prévisualiser du contenu + """ + + def __init__( + self, + action_id: str, + parameters: Dict[str, Any], + screen_capturer=None + ): + """ + Initialise l'action de survol. + + Args: + action_id: Identifiant unique de l'action + parameters: Paramètres incluant l'ancre visuelle + screen_capturer: Instance du ScreenCapturer (optionnel) + """ + super().__init__( + action_id=action_id, + name="Survoler Élément", + description="Survole un élément visuel avec la souris", + parameters=parameters, + screen_capturer=screen_capturer + ) + + # Paramètres du survol + self.ancre_visuelle: Optional[VWBVisualAnchor] = parameters.get('visual_anchor') or parameters.get('ancre_visuelle') + self.duree_survol_ms = parameters.get('duree_survol_ms', parameters.get('hover_duration_ms', 1000)) + self.decalage_x = parameters.get('decalage_x', parameters.get('offset_x', 0)) + self.decalage_y = parameters.get('decalage_y', parameters.get('offset_y', 0)) + + # Configuration + self.seuil_confiance = parameters.get('seuil_confiance', parameters.get('confidence_threshold', 0.7)) + + # Humanisation + self.humaniser = parameters.get('humaniser', parameters.get('humanize', True)) + self.profil_humain = parameters.get('profil_humain', parameters.get('humanize_profile', 'normal')) + + def validate_parameters(self) -> List[str]: + """Valide les paramètres de l'action.""" + erreurs = [] + + if not self.ancre_visuelle: + erreurs.append("Ancre visuelle requise") + + if self.duree_survol_ms < 100: + erreurs.append("Durée de survol minimum: 100ms") + + if self.duree_survol_ms > 30000: + erreurs.append("Durée de survol maximum: 30 secondes") + + return erreurs + + def execute_core(self, step_id: str) -> VWBActionResult: + """ + Exécute le survol de l'élément. + + Args: + step_id: Identifiant de l'étape + + Returns: + Résultat d'exécution + """ + start_time = datetime.now() + + try: + # Trouver l'élément + position = self._trouver_element() + + if not position: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.ELEMENT_NOT_FOUND, + message="Élément non trouvé sur l'écran" + ) + + x, y = position + + # Appliquer le décalage + x += self.decalage_x + y += self.decalage_y + + # Effectuer le survol + succes = self._survoler(x, y) + + if not succes: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.SYSTEM_ERROR, + message="Échec du survol" + ) + + # Maintenir le survol + self._attendre_survol() + + end_time = datetime.now() + execution_time = (end_time - start_time).total_seconds() * 1000 + + nom_ancre = self._get_nom_ancre() + print(f"🖱️ Survol effectué sur '{nom_ancre}' à ({x}, {y}) pendant {self.duree_survol_ms}ms") + + return VWBActionResult( + action_id=self.action_id, + step_id=step_id, + status=VWBActionStatus.SUCCESS, + start_time=start_time, + end_time=end_time, + execution_time_ms=execution_time, + output_data={ + 'position_x': x, + 'position_y': y, + 'duree_survol_ms': self.duree_survol_ms, + 'nom_ancre': nom_ancre + }, + evidence_list=self.evidence_list.copy() + ) + + except Exception as e: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.SYSTEM_ERROR, + message=f"Erreur: {str(e)}", + technical_details={'exception': str(e)} + ) + + def _trouver_element(self) -> Optional[Tuple[int, int]]: + """ + Trouve la position de l'élément sur l'écran. + + Returns: + Tuple (x, y) ou None si non trouvé + """ + try: + from ...catalog_routes import find_visual_anchor_on_screen + + # Obtenir l'image et le bounding box + image_ancre = None + bounding_box = None + + if isinstance(self.ancre_visuelle, dict): + image_ancre = self.ancre_visuelle.get('screenshot') or self.ancre_visuelle.get('image_base64') + bounding_box = self.ancre_visuelle.get('bounding_box') + elif isinstance(self.ancre_visuelle, VWBVisualAnchor): + image_ancre = self.ancre_visuelle.screenshot_base64 + if self.ancre_visuelle.has_bounding_box(): + bounding_box = self.ancre_visuelle.bounding_box + + # Recherche visuelle + if image_ancre: + resultat = find_visual_anchor_on_screen( + anchor_image_base64=image_ancre, + confidence_threshold=self.seuil_confiance, + bounding_box=bounding_box + ) + + if resultat and resultat.get('found'): + return (resultat.get('center_x', 0), resultat.get('center_y', 0)) + + # Fallback sur coordonnées statiques + if bounding_box: + x = int(bounding_box.get('x', 0) + bounding_box.get('width', 0) / 2) + y = int(bounding_box.get('y', 0) + bounding_box.get('height', 0) / 2) + return (x, y) + + return None + + except Exception as e: + print(f"⚠️ Erreur recherche: {e}") + return None + + def _survoler(self, x: int, y: int) -> bool: + """ + Déplace la souris vers la position. + + Args: + x: Position X + y: Position Y + + Returns: + True si succès + """ + try: + if self.humaniser: + from ...utils.humanizer import Humanizer, HumanProfile + + profils = { + 'rapide': HumanProfile.FAST, + 'fast': HumanProfile.FAST, + 'normal': HumanProfile.NORMAL, + 'lent': HumanProfile.SLOW, + 'slow': HumanProfile.SLOW, + 'furtif': HumanProfile.STEALTH, + 'stealth': HumanProfile.STEALTH, + } + profil = profils.get(self.profil_humain, HumanProfile.NORMAL) + + humanizer = Humanizer(profile=profil) + # Humaniser la position légèrement + x, y = humanizer.humanize_position(x, y) + humanizer.move_to(x, y) + return True + + else: + import pyautogui + pyautogui.moveTo(x, y, duration=0.3) + return True + + except ImportError as e: + print(f"⚠️ Module non disponible: {e}") + return True # Simulation + + except Exception as e: + print(f"❌ Erreur survol: {e}") + return False + + def _attendre_survol(self): + """Maintient le survol pendant la durée configurée.""" + if self.humaniser: + try: + from ...utils.humanizer import Humanizer, HumanProfile + + humanizer = Humanizer(profile=HumanProfile.NORMAL) + humanizer.wait(self.duree_survol_ms, int(self.duree_survol_ms * 1.2)) + return + + except ImportError: + pass + + time.sleep(self.duree_survol_ms / 1000) + + def _get_nom_ancre(self) -> str: + """Retourne le nom de l'ancre.""" + if isinstance(self.ancre_visuelle, dict): + return self.ancre_visuelle.get('name', self.ancre_visuelle.get('nom', 'Élément')) + elif isinstance(self.ancre_visuelle, VWBVisualAnchor): + return self.ancre_visuelle.name + return "Élément" + + def get_action_info(self) -> Dict[str, Any]: + """Retourne les informations de l'action.""" + return { + 'action_id': self.action_id, + 'name': self.name, + 'description': self.description, + 'type': 'survol_element', + 'parameters': { + 'nom_ancre': self._get_nom_ancre(), + 'duree_survol_ms': self.duree_survol_ms, + 'decalage': {'x': self.decalage_x, 'y': self.decalage_y}, + 'humaniser': self.humaniser + }, + 'status': self.current_status.value + }