feat: outils gestion fichiers dans le VWB (📁 Fichiers)
- 5 actions : lister, créer dossier, déplacer, copier, classer par extension - Exécution sur Windows via agent port 5006 - Sécurité chemins (bloque C:\Windows, /etc, etc.) - Propriétés panel + preview canvas pour chaque action Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -1,10 +1,12 @@
|
|||||||
"""
|
"""
|
||||||
Mini serveur HTTP sur l'agent Windows pour les captures d'ecran a la demande.
|
Mini serveur HTTP sur l'agent Windows pour les captures d'ecran a la demande
|
||||||
|
et les operations fichiers.
|
||||||
|
|
||||||
Ecoute sur le port 5006 (configurable via RPA_CAPTURE_PORT).
|
Ecoute sur le port 5006 (configurable via RPA_CAPTURE_PORT).
|
||||||
Endpoints :
|
Endpoints :
|
||||||
GET /capture -> screenshot frais en base64 (JPEG)
|
GET /capture -> screenshot frais en base64 (JPEG)
|
||||||
GET /health -> {"status": "ok"}
|
GET /health -> {"status": "ok"}
|
||||||
|
POST /file-action -> operations fichiers (list, create, move, copy, sort)
|
||||||
"""
|
"""
|
||||||
import threading
|
import threading
|
||||||
import logging
|
import logging
|
||||||
@@ -21,7 +23,10 @@ CAPTURE_PORT = int(os.environ.get("RPA_CAPTURE_PORT", "5006"))
|
|||||||
|
|
||||||
|
|
||||||
class CaptureHandler(BaseHTTPRequestHandler):
|
class CaptureHandler(BaseHTTPRequestHandler):
|
||||||
"""Retourne un screenshot frais a chaque requete GET /capture."""
|
"""Retourne un screenshot frais a chaque requete GET /capture.
|
||||||
|
|
||||||
|
Gere aussi les actions fichiers via POST /file-action.
|
||||||
|
"""
|
||||||
|
|
||||||
def do_GET(self):
|
def do_GET(self):
|
||||||
if self.path == "/capture":
|
if self.path == "/capture":
|
||||||
@@ -31,6 +36,12 @@ class CaptureHandler(BaseHTTPRequestHandler):
|
|||||||
else:
|
else:
|
||||||
self._send_json(404, {"error": "not found"})
|
self._send_json(404, {"error": "not found"})
|
||||||
|
|
||||||
|
def do_POST(self):
|
||||||
|
if self.path == "/file-action":
|
||||||
|
self._handle_file_action()
|
||||||
|
else:
|
||||||
|
self._send_json(404, {"error": "not found"})
|
||||||
|
|
||||||
def do_OPTIONS(self):
|
def do_OPTIONS(self):
|
||||||
"""Gestion CORS preflight."""
|
"""Gestion CORS preflight."""
|
||||||
self.send_response(200)
|
self.send_response(200)
|
||||||
@@ -40,6 +51,37 @@ class CaptureHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
# ------------------------------------------------------------------
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
|
def _handle_file_action(self):
|
||||||
|
"""Execute une action fichier sur la machine Windows locale.
|
||||||
|
|
||||||
|
Body JSON attendu :
|
||||||
|
{"action": "file_sort_by_ext", "params": {"source_dir": "C:\\..."}}
|
||||||
|
"""
|
||||||
|
try:
|
||||||
|
content_length = int(self.headers.get("Content-Length", 0))
|
||||||
|
body = self.rfile.read(content_length)
|
||||||
|
data = json.loads(body.decode("utf-8"))
|
||||||
|
|
||||||
|
action = data.get("action", "")
|
||||||
|
params = data.get("params", {})
|
||||||
|
|
||||||
|
if not action:
|
||||||
|
self._send_json(400, {"error": "Parametre 'action' requis"})
|
||||||
|
return
|
||||||
|
|
||||||
|
handler = _FileActionHandlerLocal()
|
||||||
|
result = handler.execute(action, params)
|
||||||
|
code = 500 if "error" in result else 200
|
||||||
|
self._send_json(code, result)
|
||||||
|
|
||||||
|
except json.JSONDecodeError:
|
||||||
|
self._send_json(400, {"error": "JSON invalide"})
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur file-action : {e}")
|
||||||
|
self._send_json(500, {"error": str(e)})
|
||||||
|
|
||||||
|
# ------------------------------------------------------------------
|
||||||
|
|
||||||
def _handle_capture(self):
|
def _handle_capture(self):
|
||||||
"""Capture l'ecran principal et le renvoie en base64 JPEG."""
|
"""Capture l'ecran principal et le renvoie en base64 JPEG."""
|
||||||
t0 = time.perf_counter()
|
t0 = time.perf_counter()
|
||||||
@@ -86,7 +128,7 @@ class CaptureHandler(BaseHTTPRequestHandler):
|
|||||||
|
|
||||||
def _cors_headers(self):
|
def _cors_headers(self):
|
||||||
self.send_header("Access-Control-Allow-Origin", "*")
|
self.send_header("Access-Control-Allow-Origin", "*")
|
||||||
self.send_header("Access-Control-Allow-Methods", "GET, OPTIONS")
|
self.send_header("Access-Control-Allow-Methods", "GET, POST, OPTIONS")
|
||||||
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
self.send_header("Access-Control-Allow-Headers", "Content-Type")
|
||||||
|
|
||||||
def log_message(self, format, *args):
|
def log_message(self, format, *args):
|
||||||
@@ -94,6 +136,207 @@ class CaptureHandler(BaseHTTPRequestHandler):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Gestionnaire d'actions fichiers local (execute sur la machine Windows)
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
# Repertoires autorises sur Windows (securite anti-traversal)
|
||||||
|
_WIN_ALLOWED_ROOTS = [
|
||||||
|
"C:\\Users",
|
||||||
|
"D:\\",
|
||||||
|
"E:\\",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_win_path(path_str: str) -> str:
|
||||||
|
"""Normalise un chemin Windows."""
|
||||||
|
import ntpath
|
||||||
|
return ntpath.normpath(path_str)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_safe_win_path(path_str: str) -> bool:
|
||||||
|
"""Verifie qu'un chemin Windows est dans une zone autorisee."""
|
||||||
|
if not path_str or not path_str.strip():
|
||||||
|
return False
|
||||||
|
norm = _normalize_win_path(path_str).upper()
|
||||||
|
return any(norm.startswith(root.upper()) for root in _WIN_ALLOWED_ROOTS)
|
||||||
|
|
||||||
|
|
||||||
|
class _FileActionHandlerLocal:
|
||||||
|
"""Execute les operations fichiers sur la machine locale (Windows)."""
|
||||||
|
|
||||||
|
def execute(self, action_type: str, params: dict) -> dict:
|
||||||
|
"""Dispatch vers la bonne methode selon le type d'action."""
|
||||||
|
handlers = {
|
||||||
|
"file_list_dir": self._list_dir,
|
||||||
|
"file_create_dir": self._create_dir,
|
||||||
|
"file_move": self._move_file,
|
||||||
|
"file_copy": self._copy_file,
|
||||||
|
"file_sort_by_ext": self._sort_by_extension,
|
||||||
|
}
|
||||||
|
handler = handlers.get(action_type)
|
||||||
|
if not handler:
|
||||||
|
return {"error": f"Action fichier inconnue : {action_type}"}
|
||||||
|
try:
|
||||||
|
return handler(params)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error(f"Erreur action fichier '{action_type}' : {e}")
|
||||||
|
return {"error": str(e)}
|
||||||
|
|
||||||
|
def _list_dir(self, params: dict) -> dict:
|
||||||
|
"""Liste les fichiers d'un dossier."""
|
||||||
|
import fnmatch as _fnmatch
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
|
||||||
|
path_str = params.get("path", "")
|
||||||
|
pattern = params.get("pattern", "*")
|
||||||
|
if not path_str:
|
||||||
|
return {"error": "Parametre 'path' requis"}
|
||||||
|
if not _is_safe_win_path(path_str):
|
||||||
|
return {"error": f"Chemin non autorise : {path_str}"}
|
||||||
|
|
||||||
|
source = _Path(path_str)
|
||||||
|
if not source.exists():
|
||||||
|
return {"error": f"Dossier introuvable : {path_str}"}
|
||||||
|
if not source.is_dir():
|
||||||
|
return {"error": f"Pas un dossier : {path_str}"}
|
||||||
|
|
||||||
|
files = []
|
||||||
|
extensions = {}
|
||||||
|
for item in source.iterdir():
|
||||||
|
if item.is_file() and _fnmatch.fnmatch(item.name, pattern):
|
||||||
|
ext = item.suffix.lstrip(".").lower() or "sans_extension"
|
||||||
|
files.append({
|
||||||
|
"name": item.name,
|
||||||
|
"extension": ext,
|
||||||
|
"size": item.stat().st_size,
|
||||||
|
"path": str(item),
|
||||||
|
})
|
||||||
|
extensions[ext] = extensions.get(ext, 0) + 1
|
||||||
|
|
||||||
|
logger.info(f"Liste dossier '{path_str}' : {len(files)} fichiers")
|
||||||
|
return {"files": files, "count": len(files), "extensions": extensions, "path": path_str}
|
||||||
|
|
||||||
|
def _create_dir(self, params: dict) -> dict:
|
||||||
|
"""Cree un dossier (parents inclus)."""
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
|
||||||
|
path_str = params.get("path", "")
|
||||||
|
if not path_str:
|
||||||
|
return {"error": "Parametre 'path' requis"}
|
||||||
|
if not _is_safe_win_path(path_str):
|
||||||
|
return {"error": f"Chemin non autorise : {path_str}"}
|
||||||
|
|
||||||
|
target = _Path(path_str)
|
||||||
|
existed = target.exists()
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
logger.info(f"Dossier '{path_str}' {'existait deja' if existed else 'cree'}")
|
||||||
|
return {"created": not existed, "path": path_str, "already_existed": existed}
|
||||||
|
|
||||||
|
def _move_file(self, params: dict) -> dict:
|
||||||
|
"""Deplace ou renomme un fichier."""
|
||||||
|
import shutil as _shutil
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
|
||||||
|
src = params.get("source", "")
|
||||||
|
dst = params.get("destination", "")
|
||||||
|
if not src or not dst:
|
||||||
|
return {"error": "Parametres 'source' et 'destination' requis"}
|
||||||
|
if not _is_safe_win_path(src):
|
||||||
|
return {"error": f"Source non autorisee : {src}"}
|
||||||
|
if not _is_safe_win_path(dst):
|
||||||
|
return {"error": f"Destination non autorisee : {dst}"}
|
||||||
|
|
||||||
|
if not _Path(src).exists():
|
||||||
|
return {"error": f"Fichier source introuvable : {src}"}
|
||||||
|
|
||||||
|
_Path(dst).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
_shutil.move(src, dst)
|
||||||
|
logger.info(f"Fichier deplace : '{src}' -> '{dst}'")
|
||||||
|
return {"moved": True, "source": src, "destination": dst}
|
||||||
|
|
||||||
|
def _copy_file(self, params: dict) -> dict:
|
||||||
|
"""Copie un fichier."""
|
||||||
|
import shutil as _shutil
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
|
||||||
|
src = params.get("source", "")
|
||||||
|
dst = params.get("destination", "")
|
||||||
|
if not src or not dst:
|
||||||
|
return {"error": "Parametres 'source' et 'destination' requis"}
|
||||||
|
if not _is_safe_win_path(src):
|
||||||
|
return {"error": f"Source non autorisee : {src}"}
|
||||||
|
if not _is_safe_win_path(dst):
|
||||||
|
return {"error": f"Destination non autorisee : {dst}"}
|
||||||
|
|
||||||
|
source = _Path(src)
|
||||||
|
if not source.exists():
|
||||||
|
return {"error": f"Fichier source introuvable : {src}"}
|
||||||
|
|
||||||
|
_Path(dst).parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
if source.is_dir():
|
||||||
|
_shutil.copytree(src, dst)
|
||||||
|
else:
|
||||||
|
_shutil.copy2(src, dst)
|
||||||
|
logger.info(f"Fichier copie : '{src}' -> '{dst}'")
|
||||||
|
return {"copied": True, "source": src, "destination": dst}
|
||||||
|
|
||||||
|
def _sort_by_extension(self, params: dict) -> dict:
|
||||||
|
"""Classe les fichiers par extension dans des sous-dossiers."""
|
||||||
|
import shutil as _shutil
|
||||||
|
from pathlib import Path as _Path
|
||||||
|
|
||||||
|
source_dir_str = params.get("source_dir", "")
|
||||||
|
create_subdirs = params.get("create_subdirs", True)
|
||||||
|
|
||||||
|
if not source_dir_str:
|
||||||
|
return {"error": "Parametre 'source_dir' requis"}
|
||||||
|
if not _is_safe_win_path(source_dir_str):
|
||||||
|
return {"error": f"Chemin non autorise : {source_dir_str}"}
|
||||||
|
|
||||||
|
source = _Path(source_dir_str)
|
||||||
|
if not source.exists():
|
||||||
|
return {"error": f"Dossier introuvable : {source_dir_str}"}
|
||||||
|
if not source.is_dir():
|
||||||
|
return {"error": f"Pas un dossier : {source_dir_str}"}
|
||||||
|
|
||||||
|
moved = []
|
||||||
|
extensions = {}
|
||||||
|
|
||||||
|
for f in source.iterdir():
|
||||||
|
if f.is_file():
|
||||||
|
ext = f.suffix.lstrip(".").lower() or "sans_extension"
|
||||||
|
target_dir = source / ext
|
||||||
|
|
||||||
|
if create_subdirs:
|
||||||
|
target_dir.mkdir(exist_ok=True)
|
||||||
|
elif not target_dir.exists():
|
||||||
|
continue
|
||||||
|
|
||||||
|
dest = target_dir / f.name
|
||||||
|
# Eviter ecrasement
|
||||||
|
if dest.exists():
|
||||||
|
base = f.stem
|
||||||
|
counter = 1
|
||||||
|
while dest.exists():
|
||||||
|
dest = target_dir / f"{base}_{counter}{f.suffix}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
_shutil.move(str(f), str(dest))
|
||||||
|
moved.append({"file": f.name, "to": ext, "destination": str(dest)})
|
||||||
|
extensions[ext] = extensions.get(ext, 0) + 1
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
f"Classement par extension '{source_dir_str}' : {len(moved)} fichiers"
|
||||||
|
)
|
||||||
|
return {
|
||||||
|
"moved": moved,
|
||||||
|
"count": len(moved),
|
||||||
|
"extensions": extensions,
|
||||||
|
"source_dir": source_dir_str,
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
class CaptureServer:
|
class CaptureServer:
|
||||||
"""Serveur de capture d'ecran en temps reel (thread daemon)."""
|
"""Serveur de capture d'ecran en temps reel (thread daemon)."""
|
||||||
|
|
||||||
|
|||||||
16
visual_workflow_builder/backend/actions/files/__init__.py
Normal file
16
visual_workflow_builder/backend/actions/files/__init__.py
Normal file
@@ -0,0 +1,16 @@
|
|||||||
|
"""
|
||||||
|
Actions de gestion de fichiers pour le VWB.
|
||||||
|
|
||||||
|
Auteur : Dom, Claude — 18 mars 2026
|
||||||
|
|
||||||
|
Actions :
|
||||||
|
- file_list_dir : Lister les fichiers d'un dossier
|
||||||
|
- file_create_dir : Créer un dossier
|
||||||
|
- file_move : Déplacer un fichier
|
||||||
|
- file_copy : Copier un fichier
|
||||||
|
- file_sort_by_ext : Classer les fichiers par extension
|
||||||
|
"""
|
||||||
|
|
||||||
|
from .file_actions import FileActionHandler
|
||||||
|
|
||||||
|
__all__ = ['FileActionHandler']
|
||||||
Binary file not shown.
Binary file not shown.
353
visual_workflow_builder/backend/actions/files/file_actions.py
Normal file
353
visual_workflow_builder/backend/actions/files/file_actions.py
Normal file
@@ -0,0 +1,353 @@
|
|||||||
|
"""
|
||||||
|
Gestionnaire d'actions fichiers pour le VWB.
|
||||||
|
|
||||||
|
Auteur : Dom, Claude — 18 mars 2026
|
||||||
|
|
||||||
|
Opérations de gestion de fichiers : lister, créer dossier, déplacer,
|
||||||
|
copier, classer par extension. Compatible chemins Windows (backslash)
|
||||||
|
et Linux (slash).
|
||||||
|
|
||||||
|
Sécurité : validation des chemins pour empêcher le path traversal
|
||||||
|
en dehors des répertoires autorisés.
|
||||||
|
"""
|
||||||
|
|
||||||
|
import fnmatch
|
||||||
|
import logging
|
||||||
|
import os
|
||||||
|
import shutil
|
||||||
|
from pathlib import Path, PurePosixPath, PureWindowsPath
|
||||||
|
from typing import Any, Dict, List, Optional
|
||||||
|
|
||||||
|
logger = logging.getLogger(__name__)
|
||||||
|
|
||||||
|
# Répertoires autorisés par défaut sur Windows (racines légitimes)
|
||||||
|
# On autorise les chemins sous les profils utilisateur et les lecteurs standards
|
||||||
|
_WINDOWS_ALLOWED_ROOTS = [
|
||||||
|
"C:\\Users",
|
||||||
|
"D:\\",
|
||||||
|
"E:\\",
|
||||||
|
]
|
||||||
|
|
||||||
|
# Répertoires autorisés sur Linux
|
||||||
|
_LINUX_ALLOWED_ROOTS = [
|
||||||
|
"/home",
|
||||||
|
"/tmp",
|
||||||
|
]
|
||||||
|
|
||||||
|
|
||||||
|
def _normalize_path(path_str: str) -> str:
|
||||||
|
"""Normalise un chemin en résolvant .. et en nettoyant les séparateurs."""
|
||||||
|
# Déterminer si c'est un chemin Windows (contient : comme C:\ ou \)
|
||||||
|
if ':' in path_str[:3] or path_str.startswith('\\\\'):
|
||||||
|
# Chemin Windows : normaliser avec ntpath
|
||||||
|
import ntpath
|
||||||
|
return ntpath.normpath(path_str)
|
||||||
|
else:
|
||||||
|
return os.path.normpath(path_str)
|
||||||
|
|
||||||
|
|
||||||
|
def _is_safe_path(path_str: str) -> bool:
|
||||||
|
"""Vérifie qu'un chemin est sûr (pas de traversal hors zones autorisées).
|
||||||
|
|
||||||
|
Bloque les chemins contenant des séquences de traversal dangereuses
|
||||||
|
et vérifie que le chemin résolu reste dans une zone autorisée.
|
||||||
|
"""
|
||||||
|
normalized = _normalize_path(path_str)
|
||||||
|
|
||||||
|
# Bloquer les chemins vides
|
||||||
|
if not normalized or normalized.strip() in ('', '.'):
|
||||||
|
return False
|
||||||
|
|
||||||
|
# Déterminer le système de fichiers cible
|
||||||
|
is_windows = ':' in normalized[:3] or normalized.startswith('\\\\')
|
||||||
|
|
||||||
|
if is_windows:
|
||||||
|
allowed_roots = _WINDOWS_ALLOWED_ROOTS
|
||||||
|
# Vérifier que le chemin est sous une racine autorisée
|
||||||
|
norm_upper = normalized.upper()
|
||||||
|
return any(norm_upper.startswith(root.upper()) for root in allowed_roots)
|
||||||
|
else:
|
||||||
|
allowed_roots = _LINUX_ALLOWED_ROOTS
|
||||||
|
return any(normalized.startswith(root) for root in allowed_roots)
|
||||||
|
|
||||||
|
|
||||||
|
class FileActionHandler:
|
||||||
|
"""Gère les opérations sur les fichiers.
|
||||||
|
|
||||||
|
Peut s'exécuter en local (serveur Linux) ou les paramètres
|
||||||
|
peuvent être transmis à l'agent Windows via le proxy.
|
||||||
|
"""
|
||||||
|
|
||||||
|
def __init__(self, allowed_roots: Optional[List[str]] = None):
|
||||||
|
"""Initialise le gestionnaire.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
allowed_roots: Racines autorisées supplémentaires (optionnel).
|
||||||
|
"""
|
||||||
|
if allowed_roots:
|
||||||
|
_WINDOWS_ALLOWED_ROOTS.extend(
|
||||||
|
r for r in allowed_roots if r not in _WINDOWS_ALLOWED_ROOTS
|
||||||
|
)
|
||||||
|
|
||||||
|
def execute(self, action_type: str, params: dict, target: str = 'local') -> dict:
|
||||||
|
"""Exécute une action fichier.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
action_type: Type d'action (file_list_dir, file_create_dir, etc.)
|
||||||
|
params: Paramètres de l'action
|
||||||
|
target: 'local' pour exécution serveur, 'windows' pour l'agent
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
Dict avec le résultat ou une erreur
|
||||||
|
"""
|
||||||
|
handlers = {
|
||||||
|
'file_list_dir': self._list_dir,
|
||||||
|
'file_create_dir': self._create_dir,
|
||||||
|
'file_move': self._move_file,
|
||||||
|
'file_copy': self._copy_file,
|
||||||
|
'file_sort_by_ext': self._sort_by_extension,
|
||||||
|
}
|
||||||
|
|
||||||
|
handler = handlers.get(action_type)
|
||||||
|
if not handler:
|
||||||
|
return {'error': f"Type d'action fichier inconnu : {action_type}"}
|
||||||
|
|
||||||
|
try:
|
||||||
|
return handler(params)
|
||||||
|
except Exception as e:
|
||||||
|
logger.error("Erreur action fichier '%s' : %s", action_type, e)
|
||||||
|
return {'error': str(e)}
|
||||||
|
|
||||||
|
def _list_dir(self, params: dict) -> dict:
|
||||||
|
"""Liste les fichiers d'un dossier.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: {'path': str, 'pattern': str (optionnel, défaut '*')}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'files': [...], 'count': int, 'extensions': {...}}
|
||||||
|
"""
|
||||||
|
path_str = params.get('path', '')
|
||||||
|
pattern = params.get('pattern', '*')
|
||||||
|
|
||||||
|
if not path_str:
|
||||||
|
return {'error': "Paramètre 'path' requis"}
|
||||||
|
if not _is_safe_path(path_str):
|
||||||
|
return {'error': f"Chemin non autorisé : {path_str}"}
|
||||||
|
|
||||||
|
source = Path(path_str)
|
||||||
|
if not source.exists():
|
||||||
|
return {'error': f"Dossier introuvable : {path_str}"}
|
||||||
|
if not source.is_dir():
|
||||||
|
return {'error': f"Le chemin n'est pas un dossier : {path_str}"}
|
||||||
|
|
||||||
|
files = []
|
||||||
|
extensions: Dict[str, int] = {}
|
||||||
|
|
||||||
|
for item in source.iterdir():
|
||||||
|
if item.is_file():
|
||||||
|
# Appliquer le filtre glob
|
||||||
|
if not fnmatch.fnmatch(item.name, pattern):
|
||||||
|
continue
|
||||||
|
|
||||||
|
ext = item.suffix.lstrip('.').lower() or 'sans_extension'
|
||||||
|
size = item.stat().st_size
|
||||||
|
files.append({
|
||||||
|
'name': item.name,
|
||||||
|
'extension': ext,
|
||||||
|
'size': size,
|
||||||
|
'path': str(item),
|
||||||
|
})
|
||||||
|
extensions[ext] = extensions.get(ext, 0) + 1
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Listage dossier '%s' : %d fichiers, extensions : %s",
|
||||||
|
path_str, len(files), extensions,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'files': files,
|
||||||
|
'count': len(files),
|
||||||
|
'extensions': extensions,
|
||||||
|
'path': path_str,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _create_dir(self, params: dict) -> dict:
|
||||||
|
"""Crée un dossier (et les sous-dossiers si nécessaire).
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: {'path': str}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'created': bool, 'path': str}
|
||||||
|
"""
|
||||||
|
path_str = params.get('path', '')
|
||||||
|
if not path_str:
|
||||||
|
return {'error': "Paramètre 'path' requis"}
|
||||||
|
if not _is_safe_path(path_str):
|
||||||
|
return {'error': f"Chemin non autorisé : {path_str}"}
|
||||||
|
|
||||||
|
target = Path(path_str)
|
||||||
|
already_exists = target.exists()
|
||||||
|
target.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Dossier '%s' %s",
|
||||||
|
path_str,
|
||||||
|
"existait déjà" if already_exists else "créé",
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'created': not already_exists,
|
||||||
|
'path': path_str,
|
||||||
|
'already_existed': already_exists,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _move_file(self, params: dict) -> dict:
|
||||||
|
"""Déplace ou renomme un fichier.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: {'source': str, 'destination': str}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'moved': bool, 'source': str, 'destination': str}
|
||||||
|
"""
|
||||||
|
source_str = params.get('source', '')
|
||||||
|
dest_str = params.get('destination', '')
|
||||||
|
|
||||||
|
if not source_str or not dest_str:
|
||||||
|
return {'error': "Paramètres 'source' et 'destination' requis"}
|
||||||
|
if not _is_safe_path(source_str):
|
||||||
|
return {'error': f"Chemin source non autorisé : {source_str}"}
|
||||||
|
if not _is_safe_path(dest_str):
|
||||||
|
return {'error': f"Chemin destination non autorisé : {dest_str}"}
|
||||||
|
|
||||||
|
source = Path(source_str)
|
||||||
|
if not source.exists():
|
||||||
|
return {'error': f"Fichier source introuvable : {source_str}"}
|
||||||
|
|
||||||
|
dest = Path(dest_str)
|
||||||
|
# Créer le dossier parent si nécessaire
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
shutil.move(str(source), str(dest))
|
||||||
|
|
||||||
|
logger.info("Fichier déplacé : '%s' → '%s'", source_str, dest_str)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'moved': True,
|
||||||
|
'source': source_str,
|
||||||
|
'destination': dest_str,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _copy_file(self, params: dict) -> dict:
|
||||||
|
"""Copie un fichier vers un autre emplacement.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: {'source': str, 'destination': str}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'copied': bool, 'source': str, 'destination': str}
|
||||||
|
"""
|
||||||
|
source_str = params.get('source', '')
|
||||||
|
dest_str = params.get('destination', '')
|
||||||
|
|
||||||
|
if not source_str or not dest_str:
|
||||||
|
return {'error': "Paramètres 'source' et 'destination' requis"}
|
||||||
|
if not _is_safe_path(source_str):
|
||||||
|
return {'error': f"Chemin source non autorisé : {source_str}"}
|
||||||
|
if not _is_safe_path(dest_str):
|
||||||
|
return {'error': f"Chemin destination non autorisé : {dest_str}"}
|
||||||
|
|
||||||
|
source = Path(source_str)
|
||||||
|
if not source.exists():
|
||||||
|
return {'error': f"Fichier source introuvable : {source_str}"}
|
||||||
|
|
||||||
|
dest = Path(dest_str)
|
||||||
|
# Créer le dossier parent si nécessaire
|
||||||
|
dest.parent.mkdir(parents=True, exist_ok=True)
|
||||||
|
|
||||||
|
if source.is_dir():
|
||||||
|
shutil.copytree(str(source), str(dest))
|
||||||
|
else:
|
||||||
|
shutil.copy2(str(source), str(dest))
|
||||||
|
|
||||||
|
logger.info("Fichier copié : '%s' → '%s'", source_str, dest_str)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'copied': True,
|
||||||
|
'source': source_str,
|
||||||
|
'destination': dest_str,
|
||||||
|
}
|
||||||
|
|
||||||
|
def _sort_by_extension(self, params: dict) -> dict:
|
||||||
|
"""Classe les fichiers par extension dans des sous-dossiers.
|
||||||
|
|
||||||
|
Crée un sous-dossier par extension (pdf/, docx/, jpg/, etc.)
|
||||||
|
et déplace chaque fichier dans le sous-dossier correspondant.
|
||||||
|
|
||||||
|
Args:
|
||||||
|
params: {'source_dir': str, 'create_subdirs': bool (défaut True)}
|
||||||
|
|
||||||
|
Returns:
|
||||||
|
{'moved': [...], 'count': int, 'extensions': {...}}
|
||||||
|
"""
|
||||||
|
source_dir_str = params.get('source_dir', '')
|
||||||
|
create_subdirs = params.get('create_subdirs', True)
|
||||||
|
|
||||||
|
if not source_dir_str:
|
||||||
|
return {'error': "Paramètre 'source_dir' requis"}
|
||||||
|
if not _is_safe_path(source_dir_str):
|
||||||
|
return {'error': f"Chemin non autorisé : {source_dir_str}"}
|
||||||
|
|
||||||
|
source = Path(source_dir_str)
|
||||||
|
if not source.exists():
|
||||||
|
return {'error': f"Dossier introuvable : {source_dir_str}"}
|
||||||
|
if not source.is_dir():
|
||||||
|
return {'error': f"Le chemin n'est pas un dossier : {source_dir_str}"}
|
||||||
|
|
||||||
|
moved = []
|
||||||
|
extensions: Dict[str, int] = {}
|
||||||
|
|
||||||
|
for file in source.iterdir():
|
||||||
|
if file.is_file():
|
||||||
|
ext = file.suffix.lstrip('.').lower() or 'sans_extension'
|
||||||
|
target_dir = source / ext
|
||||||
|
|
||||||
|
if create_subdirs:
|
||||||
|
target_dir.mkdir(exist_ok=True)
|
||||||
|
elif not target_dir.exists():
|
||||||
|
logger.warning(
|
||||||
|
"Sous-dossier '%s' inexistant et create_subdirs=False, fichier ignoré : %s",
|
||||||
|
ext, file.name,
|
||||||
|
)
|
||||||
|
continue
|
||||||
|
|
||||||
|
dest = target_dir / file.name
|
||||||
|
# Éviter d'écraser un fichier existant
|
||||||
|
if dest.exists():
|
||||||
|
base = file.stem
|
||||||
|
counter = 1
|
||||||
|
while dest.exists():
|
||||||
|
dest = target_dir / f"{base}_{counter}{file.suffix}"
|
||||||
|
counter += 1
|
||||||
|
|
||||||
|
shutil.move(str(file), str(dest))
|
||||||
|
moved.append({
|
||||||
|
'file': file.name,
|
||||||
|
'to': ext,
|
||||||
|
'destination': str(dest),
|
||||||
|
})
|
||||||
|
extensions[ext] = extensions.get(ext, 0) + 1
|
||||||
|
|
||||||
|
logger.info(
|
||||||
|
"Classement par extension dans '%s' : %d fichiers déplacés, extensions : %s",
|
||||||
|
source_dir_str, len(moved), extensions,
|
||||||
|
)
|
||||||
|
|
||||||
|
return {
|
||||||
|
'moved': moved,
|
||||||
|
'count': len(moved),
|
||||||
|
'extensions': extensions,
|
||||||
|
'source_dir': source_dir_str,
|
||||||
|
}
|
||||||
@@ -77,6 +77,12 @@ _CONDITION_ACTION_TYPES = {"visual_condition"}
|
|||||||
# Actions VWB de type data loop
|
# Actions VWB de type data loop
|
||||||
_DATA_LOOP_ACTION_TYPES = {"import_excel", "db_foreach"}
|
_DATA_LOOP_ACTION_TYPES = {"import_excel", "db_foreach"}
|
||||||
|
|
||||||
|
# Actions VWB de gestion de fichiers
|
||||||
|
_FILE_ACTION_TYPES = {
|
||||||
|
"file_list_dir", "file_create_dir", "file_move",
|
||||||
|
"file_copy", "file_sort_by_ext",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _classify_step_type(action_type: str) -> StepType:
|
def _classify_step_type(action_type: str) -> StepType:
|
||||||
"""Détermine le StepType DAG à partir du action_type VWB."""
|
"""Détermine le StepType DAG à partir du action_type VWB."""
|
||||||
@@ -982,6 +988,43 @@ def execute_windows():
|
|||||||
if vwb_type in ('keyboard_shortcut', 'hotkey') and 'keys' in params:
|
if vwb_type in ('keyboard_shortcut', 'hotkey') and 'keys' in params:
|
||||||
action['keys'] = params['keys']
|
action['keys'] = params['keys']
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
# Actions fichiers → proxy vers /file-action de l'agent (port 5006)
|
||||||
|
# ---------------------------------------------------------------
|
||||||
|
if 'actions' in data:
|
||||||
|
file_actions = [a for a in data['actions'] if a.get('type', '') in _FILE_ACTION_TYPES]
|
||||||
|
if file_actions:
|
||||||
|
# Exécuter les actions fichiers via l'agent Windows
|
||||||
|
file_results = []
|
||||||
|
for fa in file_actions:
|
||||||
|
try:
|
||||||
|
fa_resp = req.post(
|
||||||
|
'http://192.168.1.11:5006/file-action',
|
||||||
|
json={
|
||||||
|
'action': fa['type'],
|
||||||
|
'params': fa.get('parameters', {}),
|
||||||
|
},
|
||||||
|
timeout=30,
|
||||||
|
)
|
||||||
|
file_results.append(fa_resp.json())
|
||||||
|
except req.ConnectionError:
|
||||||
|
file_results.append({
|
||||||
|
'error': "Agent Windows (port 5006) non disponible pour l'action fichier"
|
||||||
|
})
|
||||||
|
except Exception as e:
|
||||||
|
file_results.append({'error': str(e)})
|
||||||
|
|
||||||
|
# Si TOUTES les actions sont des actions fichiers, retourner directement
|
||||||
|
non_file_actions = [a for a in data['actions'] if a.get('type', '') not in _FILE_ACTION_TYPES]
|
||||||
|
if not non_file_actions:
|
||||||
|
return jsonify({
|
||||||
|
'success': all('error' not in r for r in file_results),
|
||||||
|
'file_results': file_results,
|
||||||
|
})
|
||||||
|
|
||||||
|
# Sinon, retirer les actions fichiers du flux principal
|
||||||
|
data['actions'] = non_file_actions
|
||||||
|
|
||||||
# Injecter le machine_id pour le ciblage multi-machine
|
# Injecter le machine_id pour le ciblage multi-machine
|
||||||
# Chercher la première machine Windows connectée si pas spécifié
|
# Chercher la première machine Windows connectée si pas spécifié
|
||||||
if 'machine_id' not in data or not data.get('machine_id'):
|
if 'machine_id' not in data or not data.get('machine_id'):
|
||||||
|
|||||||
@@ -360,6 +360,53 @@ VWB_ACTION_CONTRACTS: Dict[str, ActionContract] = {
|
|||||||
required_params=[],
|
required_params=[],
|
||||||
optional_params=["prompt", "context", "model", "temperature"],
|
optional_params=["prompt", "context", "model", "temperature"],
|
||||||
),
|
),
|
||||||
|
|
||||||
|
# --- ACTIONS GESTION DE FICHIERS ---
|
||||||
|
"file_list_dir": ActionContract(
|
||||||
|
action_type="file_list_dir",
|
||||||
|
description="Lister les fichiers d'un dossier",
|
||||||
|
required_params=["path"],
|
||||||
|
optional_params=["pattern"],
|
||||||
|
param_validators={"path": lambda p: bool(p and isinstance(p, str) and p.strip())}
|
||||||
|
),
|
||||||
|
|
||||||
|
"file_create_dir": ActionContract(
|
||||||
|
action_type="file_create_dir",
|
||||||
|
description="Créer un dossier (et les sous-dossiers si nécessaire)",
|
||||||
|
required_params=["path"],
|
||||||
|
optional_params=[],
|
||||||
|
param_validators={"path": lambda p: bool(p and isinstance(p, str) and p.strip())}
|
||||||
|
),
|
||||||
|
|
||||||
|
"file_move": ActionContract(
|
||||||
|
action_type="file_move",
|
||||||
|
description="Déplacer ou renommer un fichier",
|
||||||
|
required_params=["source", "destination"],
|
||||||
|
optional_params=[],
|
||||||
|
param_validators={
|
||||||
|
"source": lambda p: bool(p and isinstance(p, str) and p.strip()),
|
||||||
|
"destination": lambda p: bool(p and isinstance(p, str) and p.strip()),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
"file_copy": ActionContract(
|
||||||
|
action_type="file_copy",
|
||||||
|
description="Copier un fichier vers un autre emplacement",
|
||||||
|
required_params=["source", "destination"],
|
||||||
|
optional_params=[],
|
||||||
|
param_validators={
|
||||||
|
"source": lambda p: bool(p and isinstance(p, str) and p.strip()),
|
||||||
|
"destination": lambda p: bool(p and isinstance(p, str) and p.strip()),
|
||||||
|
}
|
||||||
|
),
|
||||||
|
|
||||||
|
"file_sort_by_ext": ActionContract(
|
||||||
|
action_type="file_sort_by_ext",
|
||||||
|
description="Classer les fichiers par extension dans des sous-dossiers",
|
||||||
|
required_params=["source_dir"],
|
||||||
|
optional_params=["create_subdirs"],
|
||||||
|
param_validators={"source_dir": lambda p: bool(p and isinstance(p, str) and p.strip())}
|
||||||
|
),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1155,6 +1155,140 @@ export default function PropertiesPanel({ step, onUpdateParams, onDelete }: Prop
|
|||||||
</>
|
</>
|
||||||
);
|
);
|
||||||
|
|
||||||
|
// === GESTION DE FICHIERS ===
|
||||||
|
case 'file_list_dir':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="prop-section-title">
|
||||||
|
<span className="icon">📂</span> Lister un dossier
|
||||||
|
</div>
|
||||||
|
<div className="prop-field">
|
||||||
|
<label>Chemin du dossier</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={String(params.path || '')}
|
||||||
|
onChange={(e) => updateParam('path', e.target.value)}
|
||||||
|
placeholder="C:\Users\dom\Downloads\anonymise"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="prop-field">
|
||||||
|
<label>Filtre (glob)</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={String(params.pattern || '*')}
|
||||||
|
onChange={(e) => updateParam('pattern', e.target.value)}
|
||||||
|
placeholder="*.pdf, *.*, *.docx"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'file_create_dir':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="prop-section-title">
|
||||||
|
<span className="icon">📁</span> Créer un dossier
|
||||||
|
</div>
|
||||||
|
<div className="prop-field">
|
||||||
|
<label>Chemin du dossier</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={String(params.path || '')}
|
||||||
|
onChange={(e) => updateParam('path', e.target.value)}
|
||||||
|
placeholder="C:\Users\dom\Downloads\anonymise\pdf"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="prop-info">
|
||||||
|
Les dossiers parents seront créés automatiquement si nécessaire.
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'file_move':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="prop-section-title">
|
||||||
|
<span className="icon">📎</span> Déplacer un fichier
|
||||||
|
</div>
|
||||||
|
<div className="prop-field">
|
||||||
|
<label>Chemin source</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={String(params.source || '')}
|
||||||
|
onChange={(e) => updateParam('source', e.target.value)}
|
||||||
|
placeholder="C:\Users\dom\Downloads\document.pdf"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="prop-field">
|
||||||
|
<label>Chemin destination</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={String(params.destination || '')}
|
||||||
|
onChange={(e) => updateParam('destination', e.target.value)}
|
||||||
|
placeholder="C:\Users\dom\Downloads\anonymise\pdf\document.pdf"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'file_copy':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="prop-section-title">
|
||||||
|
<span className="icon">📋</span> Copier un fichier
|
||||||
|
</div>
|
||||||
|
<div className="prop-field">
|
||||||
|
<label>Chemin source</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={String(params.source || '')}
|
||||||
|
onChange={(e) => updateParam('source', e.target.value)}
|
||||||
|
placeholder="C:\Users\dom\Downloads\document.pdf"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="prop-field">
|
||||||
|
<label>Chemin destination</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={String(params.destination || '')}
|
||||||
|
onChange={(e) => updateParam('destination', e.target.value)}
|
||||||
|
placeholder="C:\Users\dom\Archives\document.pdf"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
|
case 'file_sort_by_ext':
|
||||||
|
return (
|
||||||
|
<>
|
||||||
|
<div className="prop-section-title">
|
||||||
|
<span className="icon">🗂️</span> Classer par extension
|
||||||
|
</div>
|
||||||
|
<div className="prop-field">
|
||||||
|
<label>Dossier source</label>
|
||||||
|
<input
|
||||||
|
type="text"
|
||||||
|
value={String(params.source_dir || '')}
|
||||||
|
onChange={(e) => updateParam('source_dir', e.target.value)}
|
||||||
|
placeholder="C:\Users\dom\Downloads\anonymise"
|
||||||
|
/>
|
||||||
|
</div>
|
||||||
|
<div className="prop-field checkbox">
|
||||||
|
<label>
|
||||||
|
<input
|
||||||
|
type="checkbox"
|
||||||
|
checked={Boolean(params.create_subdirs !== false)}
|
||||||
|
onChange={(e) => updateParam('create_subdirs', e.target.checked)}
|
||||||
|
/>
|
||||||
|
Créer les sous-dossiers automatiquement
|
||||||
|
</label>
|
||||||
|
</div>
|
||||||
|
<div className="prop-info">
|
||||||
|
Les fichiers seront déplacés dans des sous-dossiers nommés par extension (pdf/, docx/, jpg/, etc.)
|
||||||
|
</div>
|
||||||
|
</>
|
||||||
|
);
|
||||||
|
|
||||||
// === VALIDATION ===
|
// === VALIDATION ===
|
||||||
case 'verify_element_exists':
|
case 'verify_element_exists':
|
||||||
return (
|
return (
|
||||||
|
|||||||
@@ -16,6 +16,7 @@ function StepNode({ data, selected }: StepNodeProps) {
|
|||||||
const isConditional = step.action_type === 'visual_condition' || step.action_type === 'loop_visual';
|
const isConditional = step.action_type === 'visual_condition' || step.action_type === 'loop_visual';
|
||||||
const isDataLoop = step.action_type === 'db_foreach';
|
const isDataLoop = step.action_type === 'db_foreach';
|
||||||
const isImport = step.action_type === 'import_excel';
|
const isImport = step.action_type === 'import_excel';
|
||||||
|
const isFileAction = step.action_type.startsWith('file_');
|
||||||
|
|
||||||
// État du tooltip d'aide
|
// État du tooltip d'aide
|
||||||
const [showHelp, setShowHelp] = useState(false);
|
const [showHelp, setShowHelp] = useState(false);
|
||||||
@@ -34,7 +35,7 @@ function StepNode({ data, selected }: StepNodeProps) {
|
|||||||
}, [showHelp]);
|
}, [showHelp]);
|
||||||
|
|
||||||
return (
|
return (
|
||||||
<div className={`step-node ${selected ? 'selected' : ''} ${isConditional ? 'conditional' : ''} ${isDataLoop ? 'data-loop' : ''} ${isImport ? 'data-import' : ''}`}>
|
<div className={`step-node ${selected ? 'selected' : ''} ${isConditional ? 'conditional' : ''} ${isDataLoop ? 'data-loop' : ''} ${isImport ? 'data-import' : ''} ${isFileAction ? 'file-action' : ''}`}>
|
||||||
{/* Bouton aide (?) */}
|
{/* Bouton aide (?) */}
|
||||||
{action && (
|
{action && (
|
||||||
<button
|
<button
|
||||||
@@ -82,7 +83,8 @@ function StepNode({ data, selected }: StepNodeProps) {
|
|||||||
title="Supprimer (Suppr)"
|
title="Supprimer (Suppr)"
|
||||||
onClick={(e) => {
|
onClick={(e) => {
|
||||||
e.stopPropagation();
|
e.stopPropagation();
|
||||||
data.onDelete?.(step.id);
|
// Dispatch un custom event que App.tsx écoute (contourne le memo)
|
||||||
|
window.dispatchEvent(new CustomEvent('rpa-delete-step', { detail: step.id }));
|
||||||
}}
|
}}
|
||||||
>
|
>
|
||||||
×
|
×
|
||||||
@@ -152,6 +154,29 @@ function StepNode({ data, selected }: StepNodeProps) {
|
|||||||
</div>
|
</div>
|
||||||
)}
|
)}
|
||||||
|
|
||||||
|
{/* Aperçu actions fichiers */}
|
||||||
|
{step.action_type === 'file_list_dir' && typeof step.parameters?.path === 'string' && step.parameters.path.length > 0 && (
|
||||||
|
<div className="step-node-params">
|
||||||
|
{`📂 ${String(step.parameters.path).split(/[\\/]/).pop() || String(step.parameters.path)}`}
|
||||||
|
{step.parameters.pattern && step.parameters.pattern !== '*' ? ` (${String(step.parameters.pattern)})` : ''}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step.action_type === 'file_create_dir' && typeof step.parameters?.path === 'string' && step.parameters.path.length > 0 && (
|
||||||
|
<div className="step-node-params">
|
||||||
|
{`📁 ${String(step.parameters.path).split(/[\\/]/).pop() || String(step.parameters.path)}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{(step.action_type === 'file_move' || step.action_type === 'file_copy') && typeof step.parameters?.source === 'string' && step.parameters.source.length > 0 && (
|
||||||
|
<div className="step-node-params">
|
||||||
|
{`${String(step.parameters.source).split(/[\\/]/).pop()} → ${String(step.parameters.destination || '?').split(/[\\/]/).pop()}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
{step.action_type === 'file_sort_by_ext' && typeof step.parameters?.source_dir === 'string' && step.parameters.source_dir.length > 0 && (
|
||||||
|
<div className="step-node-params">
|
||||||
|
{`🗂️ ${String(step.parameters.source_dir).split(/[\\/]/).pop() || String(step.parameters.source_dir)}`}
|
||||||
|
</div>
|
||||||
|
)}
|
||||||
|
|
||||||
{!step.anchor_id && action?.needsAnchor && (
|
{!step.anchor_id && action?.needsAnchor && (
|
||||||
<div className="step-node-warning">
|
<div className="step-node-warning">
|
||||||
Ancre requise
|
Ancre requise
|
||||||
|
|||||||
@@ -55,14 +55,20 @@ export type ActionType =
|
|||||||
| 'llm_analyze'
|
| 'llm_analyze'
|
||||||
| 'llm_translate'
|
| 'llm_translate'
|
||||||
| 'llm_extract_data'
|
| 'llm_extract_data'
|
||||||
| 'llm_generate';
|
| 'llm_generate'
|
||||||
|
// === Gestion de fichiers ===
|
||||||
|
| 'file_list_dir'
|
||||||
|
| 'file_create_dir'
|
||||||
|
| 'file_move'
|
||||||
|
| 'file_copy'
|
||||||
|
| 'file_sort_by_ext';
|
||||||
|
|
||||||
export interface ActionDefinition {
|
export interface ActionDefinition {
|
||||||
type: ActionType;
|
type: ActionType;
|
||||||
label: string;
|
label: string;
|
||||||
icon: string;
|
icon: string;
|
||||||
description: string;
|
description: string;
|
||||||
category: 'mouse' | 'keyboard' | 'wait' | 'data' | 'logic' | 'ai' | 'llm' | 'validation';
|
category: 'mouse' | 'keyboard' | 'wait' | 'data' | 'logic' | 'ai' | 'llm' | 'validation' | 'files';
|
||||||
needsAnchor: boolean;
|
needsAnchor: boolean;
|
||||||
params: { name: string; type: string; description: string }[];
|
params: { name: string; type: string; description: string }[];
|
||||||
}
|
}
|
||||||
@@ -200,6 +206,27 @@ export const ACTIONS: ActionDefinition[] = [
|
|||||||
{ name: 'model', type: 'string', description: 'Modèle Ollama' }
|
{ name: 'model', type: 'string', description: 'Modèle Ollama' }
|
||||||
] },
|
] },
|
||||||
|
|
||||||
|
// === GESTION DE FICHIERS ===
|
||||||
|
{ type: 'file_list_dir', label: 'Lister un dossier', icon: '📂', description: 'Liste les fichiers d\'un dossier et retourne leurs noms et extensions.', category: 'files', needsAnchor: false, params: [
|
||||||
|
{ name: 'path', type: 'string', description: 'Chemin du dossier' },
|
||||||
|
{ name: 'pattern', type: 'string', description: 'Filtre (ex: *.pdf, *.*)' },
|
||||||
|
] },
|
||||||
|
{ type: 'file_create_dir', label: 'Créer un dossier', icon: '📁', description: 'Crée un dossier (et les sous-dossiers si nécessaire).', category: 'files', needsAnchor: false, params: [
|
||||||
|
{ name: 'path', type: 'string', description: 'Chemin du dossier à créer' },
|
||||||
|
] },
|
||||||
|
{ type: 'file_move', label: 'Déplacer un fichier', icon: '📎', description: 'Déplace ou renomme un fichier.', category: 'files', needsAnchor: false, params: [
|
||||||
|
{ name: 'source', type: 'string', description: 'Chemin source' },
|
||||||
|
{ name: 'destination', type: 'string', description: 'Chemin destination' },
|
||||||
|
] },
|
||||||
|
{ type: 'file_copy', label: 'Copier un fichier', icon: '📋', description: 'Copie un fichier vers un autre emplacement.', category: 'files', needsAnchor: false, params: [
|
||||||
|
{ name: 'source', type: 'string', description: 'Chemin source' },
|
||||||
|
{ name: 'destination', type: 'string', description: 'Chemin destination' },
|
||||||
|
] },
|
||||||
|
{ type: 'file_sort_by_ext', label: 'Classer par extension', icon: '🗂️', description: 'Crée des sous-dossiers par extension et déplace les fichiers.', category: 'files', needsAnchor: false, params: [
|
||||||
|
{ name: 'source_dir', type: 'string', description: 'Dossier source' },
|
||||||
|
{ name: 'create_subdirs', type: 'boolean', description: 'Créer les sous-dossiers automatiquement' },
|
||||||
|
] },
|
||||||
|
|
||||||
// === VALIDATION ===
|
// === VALIDATION ===
|
||||||
{ type: 'verify_element_exists', label: 'Vérifier présence', icon: '✅', description: 'Vérifie qu\'un élément visuel est présent à l\'écran.', category: 'validation', needsAnchor: true, params: [
|
{ type: 'verify_element_exists', label: 'Vérifier présence', icon: '✅', description: 'Vérifie qu\'un élément visuel est présent à l\'écran.', category: 'validation', needsAnchor: true, params: [
|
||||||
{ name: 'timeout_ms', type: 'number', description: 'Délai max d\'attente en millisecondes' }
|
{ name: 'timeout_ms', type: 'number', description: 'Délai max d\'attente en millisecondes' }
|
||||||
@@ -217,6 +244,7 @@ export const ACTION_CATEGORIES = {
|
|||||||
logic: { label: 'Logique', icon: '🔀' },
|
logic: { label: 'Logique', icon: '🔀' },
|
||||||
ai: { label: 'IA', icon: '🤖' },
|
ai: { label: 'IA', icon: '🤖' },
|
||||||
llm: { label: 'IA / LLM', icon: '🧪' },
|
llm: { label: 'IA / LLM', icon: '🧪' },
|
||||||
|
files: { label: 'Fichiers', icon: '📁' },
|
||||||
validation: { label: 'Validation', icon: '✅' },
|
validation: { label: 'Validation', icon: '✅' },
|
||||||
};
|
};
|
||||||
|
|
||||||
|
|||||||
Reference in New Issue
Block a user