Feat: Actions survol_element et glisser_deposer (français)

Nouvelles actions avec nommage français pour l'interface:
- survol_element: Survol avec durée configurable et humanisation
- glisser_deposer: Drag & drop entre deux ancres visuelles

Support des profils français: rapide, normal, lent, furtif

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-14 23:02:11 +01:00
parent a2aecf4ba3
commit 34f279cbc1
3 changed files with 672 additions and 0 deletions

View File

@@ -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'

View File

@@ -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
}

View File

@@ -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
}