Feat: Actions double_clic et clic_droit (français)
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 <noreply@anthropic.com>
This commit is contained in:
@@ -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'
|
||||
263
visual_workflow_builder/backend/actions/vision_ui/clic_droit.py
Normal file
263
visual_workflow_builder/backend/actions/vision_ui/clic_droit.py
Normal file
@@ -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
|
||||
}
|
||||
274
visual_workflow_builder/backend/actions/vision_ui/double_clic.py
Normal file
274
visual_workflow_builder/backend/actions/vision_ui/double_clic.py
Normal file
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user