From ae100d3da81f9e95631139e2257d9ac5e1910c94 Mon Sep 17 00:00:00 2001 From: Dom Date: Wed, 14 Jan 2026 23:11:42 +0100 Subject: [PATCH] =?UTF-8?q?Feat:=20Actions=20double=5Fclic=20et=20clic=5Fd?= =?UTF-8?q?roit=20(fran=C3=A7ais)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Nouvelles actions de clic avec nommage français: - double_clic: Double-clic avec intervalle configurable - clic_droit: Clic droit pour menus contextuels Support des profils français: rapide, normal, lent, furtif Intégration Humanizer anti-détection Co-Authored-By: Claude Opus 4.5 --- .../backend/actions/vision_ui/__init__.py | 8 +- .../backend/actions/vision_ui/clic_droit.py | 263 +++++++++++++++++ .../backend/actions/vision_ui/double_clic.py | 274 ++++++++++++++++++ 3 files changed, 544 insertions(+), 1 deletion(-) create mode 100644 visual_workflow_builder/backend/actions/vision_ui/clic_droit.py create mode 100644 visual_workflow_builder/backend/actions/vision_ui/double_clic.py diff --git a/visual_workflow_builder/backend/actions/vision_ui/__init__.py b/visual_workflow_builder/backend/actions/vision_ui/__init__.py index 56d10fbd9..d96950906 100644 --- a/visual_workflow_builder/backend/actions/vision_ui/__init__.py +++ b/visual_workflow_builder/backend/actions/vision_ui/__init__.py @@ -16,6 +16,8 @@ Actions disponibles : - VWBExtractTextAction : Extraire du texte d'une zone - VWBSurvolElementAction : Survol d'élément (hover) - VWBGlisserDeposerAction : Glisser-déposer (drag & drop) +- VWBDoubleClicAction : Double-clic +- VWBClicDroitAction : Clic droit (menu contextuel) """ from .click_anchor import VWBClickAnchorAction @@ -27,6 +29,8 @@ from .scroll_to_anchor import VWBScrollToAnchorAction from .extract_text import VWBExtractTextAction from .survol_element import VWBSurvolElementAction from .glisser_deposer import VWBGlisserDeposerAction +from .double_clic import VWBDoubleClicAction +from .clic_droit import VWBClicDroitAction __all__ = [ 'VWBClickAnchorAction', @@ -38,8 +42,10 @@ __all__ = [ 'VWBExtractTextAction', 'VWBSurvolElementAction', 'VWBGlisserDeposerAction', + 'VWBDoubleClicAction', + 'VWBClicDroitAction', ] -__version__ = '1.2.0' +__version__ = '1.3.0' __author__ = 'Dom, Claude' __date__ = '14 janvier 2026' \ No newline at end of file diff --git a/visual_workflow_builder/backend/actions/vision_ui/clic_droit.py b/visual_workflow_builder/backend/actions/vision_ui/clic_droit.py new file mode 100644 index 000000000..ca2b44878 --- /dev/null +++ b/visual_workflow_builder/backend/actions/vision_ui/clic_droit.py @@ -0,0 +1,263 @@ +""" +Action Clic Droit - Clic droit sur un élément visuel +Auteur : Dom, Claude - 14 janvier 2026 + +Cette action permet de faire un clic droit sur un élément identifié visuellement, +utile pour ouvrir des menus contextuels. +""" + +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 VWBClicDroitAction(BaseVWBAction): + """ + Action de clic droit sur élément visuel. + + Permet de : + - Ouvrir des menus contextuels + - Accéder aux options avancées + - Copier/Coller via menu + - Actions spécifiques au clic droit + """ + + def __init__( + self, + action_id: str, + parameters: Dict[str, Any], + screen_capturer=None + ): + """ + Initialise l'action de clic droit. + + 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="Clic Droit", + description="Effectue un clic droit sur un élément visuel", + parameters=parameters, + screen_capturer=screen_capturer + ) + + # Paramètres du clic droit + self.ancre_visuelle: Optional[VWBVisualAnchor] = ( + parameters.get('visual_anchor') or + parameters.get('ancre_visuelle') + ) + self.decalage_x = parameters.get('decalage_x', parameters.get('offset_x', 0)) + self.decalage_y = parameters.get('decalage_y', parameters.get('offset_y', 0)) + self.delai_apres_clic_ms = parameters.get('delai_apres_clic_ms', parameters.get('wait_after_click_ms', 500)) + + # 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") + + return erreurs + + def execute_core(self, step_id: str) -> VWBActionResult: + """ + Exécute le clic droit. + + 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 clic droit + succes = self._clic_droit(x, y) + + if not succes: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.CLICK_FAILED, + message="Échec du clic droit" + ) + + # Attendre après le clic (important pour menus contextuels) + if self.delai_apres_clic_ms > 0: + time.sleep(self.delai_apres_clic_ms / 1000) + + end_time = datetime.now() + execution_time = (end_time - start_time).total_seconds() * 1000 + + nom_ancre = self._get_nom_ancre() + print(f"🖱️➡️ Clic droit effectué sur '{nom_ancre}' à ({x}, {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={ + 'position_x': x, + 'position_y': y, + '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é + """ + if not self.ancre_visuelle: + return None + + 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 _clic_droit(self, x: int, y: int) -> bool: + """ + Effectue le clic droit. + + 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) + real_x, real_y = humanizer.click(x, y, button='right') + return True + + else: + import pyautogui + pyautogui.rightClick(x, y) + return True + + except ImportError as e: + print(f"⚠️ Module non disponible: {e}") + return True # Simulation + + except Exception as e: + print(f"❌ Erreur clic droit: {e}") + return False + + 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': 'clic_droit', + 'parameters': { + 'nom_ancre': self._get_nom_ancre(), + 'decalage': {'x': self.decalage_x, 'y': self.decalage_y}, + 'humaniser': self.humaniser + }, + 'status': self.current_status.value + } diff --git a/visual_workflow_builder/backend/actions/vision_ui/double_clic.py b/visual_workflow_builder/backend/actions/vision_ui/double_clic.py new file mode 100644 index 000000000..82a5bb6d1 --- /dev/null +++ b/visual_workflow_builder/backend/actions/vision_ui/double_clic.py @@ -0,0 +1,274 @@ +""" +Action Double-Clic - Double-cliquer sur un élément visuel +Auteur : Dom, Claude - 14 janvier 2026 + +Cette action permet de double-cliquer sur un élément identifié visuellement, +utile pour ouvrir des fichiers, éditer du texte, etc. +""" + +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 VWBDoubleClicAction(BaseVWBAction): + """ + Action de double-clic sur élément visuel. + + Permet de : + - Ouvrir des fichiers/dossiers + - Éditer du texte inline + - Sélectionner des mots + - Déclencher des actions spécifiques au double-clic + """ + + def __init__( + self, + action_id: str, + parameters: Dict[str, Any], + screen_capturer=None + ): + """ + Initialise l'action de double-clic. + + 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="Double-Clic", + description="Double-clique sur un élément visuel", + parameters=parameters, + screen_capturer=screen_capturer + ) + + # Paramètres du double-clic + self.ancre_visuelle: Optional[VWBVisualAnchor] = ( + parameters.get('visual_anchor') or + parameters.get('ancre_visuelle') + ) + self.decalage_x = parameters.get('decalage_x', parameters.get('offset_x', 0)) + self.decalage_y = parameters.get('decalage_y', parameters.get('offset_y', 0)) + self.delai_apres_clic_ms = parameters.get('delai_apres_clic_ms', parameters.get('wait_after_click_ms', 500)) + + # Intervalle entre les deux clics + self.intervalle_clics_ms = parameters.get('intervalle_clics_ms', parameters.get('click_interval_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_visuelle: + erreurs.append("Ancre visuelle requise") + + if self.intervalle_clics_ms < 50: + erreurs.append("Intervalle entre clics minimum: 50ms") + + if self.intervalle_clics_ms > 500: + erreurs.append("Intervalle entre clics maximum: 500ms") + + return erreurs + + def execute_core(self, step_id: str) -> VWBActionResult: + """ + Exécute le double-clic. + + 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 double-clic + succes = self._double_cliquer(x, y) + + if not succes: + return self._create_error_result( + step_id=step_id, + start_time=start_time, + error_type=VWBErrorType.CLICK_FAILED, + message="Échec du double-clic" + ) + + # Attendre après le clic + if self.delai_apres_clic_ms > 0: + time.sleep(self.delai_apres_clic_ms / 1000) + + end_time = datetime.now() + execution_time = (end_time - start_time).total_seconds() * 1000 + + nom_ancre = self._get_nom_ancre() + print(f"🖱️🖱️ Double-clic effectué sur '{nom_ancre}' à ({x}, {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={ + 'position_x': x, + 'position_y': y, + 'nom_ancre': nom_ancre, + 'intervalle_clics_ms': self.intervalle_clics_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) -> Optional[Tuple[int, int]]: + """ + Trouve la position de l'élément sur l'écran. + + Returns: + Tuple (x, y) ou None si non trouvé + """ + if not self.ancre_visuelle: + return None + + 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 _double_cliquer(self, x: int, y: int) -> bool: + """ + Effectue le double-clic. + + 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) + real_x, real_y = humanizer.double_click(x, y) + return True + + else: + import pyautogui + pyautogui.doubleClick(x, y, interval=self.intervalle_clics_ms / 1000) + return True + + except ImportError as e: + print(f"⚠️ Module non disponible: {e}") + return True # Simulation + + except Exception as e: + print(f"❌ Erreur double-clic: {e}") + return False + + 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': 'double_clic', + 'parameters': { + 'nom_ancre': self._get_nom_ancre(), + 'decalage': {'x': self.decalage_x, 'y': self.decalage_y}, + 'intervalle_clics_ms': self.intervalle_clics_ms, + 'humaniser': self.humaniser + }, + 'status': self.current_status.value + }