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:
Dom
2026-03-18 16:05:36 +01:00
parent 97d708c6f5
commit 40e5fba86c
10 changed files with 898 additions and 9 deletions

View File

@@ -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 ===
case 'verify_element_exists':
return (

View File

@@ -16,6 +16,7 @@ function StepNode({ data, selected }: StepNodeProps) {
const isConditional = step.action_type === 'visual_condition' || step.action_type === 'loop_visual';
const isDataLoop = step.action_type === 'db_foreach';
const isImport = step.action_type === 'import_excel';
const isFileAction = step.action_type.startsWith('file_');
// État du tooltip d'aide
const [showHelp, setShowHelp] = useState(false);
@@ -34,7 +35,7 @@ function StepNode({ data, selected }: StepNodeProps) {
}, [showHelp]);
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 (?) */}
{action && (
<button
@@ -82,7 +83,8 @@ function StepNode({ data, selected }: StepNodeProps) {
title="Supprimer (Suppr)"
onClick={(e) => {
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>
)}
{/* 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 && (
<div className="step-node-warning">
Ancre requise

View File

@@ -55,14 +55,20 @@ export type ActionType =
| 'llm_analyze'
| 'llm_translate'
| '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 {
type: ActionType;
label: string;
icon: string;
description: string;
category: 'mouse' | 'keyboard' | 'wait' | 'data' | 'logic' | 'ai' | 'llm' | 'validation';
category: 'mouse' | 'keyboard' | 'wait' | 'data' | 'logic' | 'ai' | 'llm' | 'validation' | 'files';
needsAnchor: boolean;
params: { name: string; type: string; description: string }[];
}
@@ -200,6 +206,27 @@ export const ACTIONS: ActionDefinition[] = [
{ 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 ===
{ 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' }
@@ -217,6 +244,7 @@ export const ACTION_CATEGORIES = {
logic: { label: 'Logique', icon: '🔀' },
ai: { label: 'IA', icon: '🤖' },
llm: { label: 'IA / LLM', icon: '🧪' },
files: { label: 'Fichiers', icon: '📁' },
validation: { label: 'Validation', icon: '✅' },
};