v1.0 - Version stable: multi-PC, détection UI-DETR-1, 3 modes exécution

- Frontend v4 accessible sur réseau local (192.168.1.40)
- Ports ouverts: 3002 (frontend), 5001 (backend), 5004 (dashboard)
- Ollama GPU fonctionnel
- Self-healing interactif
- Dashboard confiance

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
This commit is contained in:
Dom
2026-01-29 11:23:51 +01:00
parent 21bfa3b337
commit a27b74cf22
1595 changed files with 412691 additions and 400 deletions

View File

@@ -0,0 +1,382 @@
/**
* Contrats Stricts des Actions VWB - Frontend
*
* Auteur : Dom, Alice, Kiro - 23 janvier 2026
*
* Ce module définit les contrats stricts pour chaque action VWB côté frontend.
* Validation AVANT envoi au backend pour éviter les erreurs.
*
* PRINCIPE CLÉ: Si le contrat n'est pas respecté → BLOQUER l'exécution
*/
export enum ContractViolationType {
MISSING_REQUIRED = 'missing_required',
INVALID_TYPE = 'invalid_type',
INVALID_VALUE = 'invalid_value',
INCOMPATIBLE_ACTION = 'incompatible_action'
}
export interface ContractViolation {
violationType: ContractViolationType;
parameter: string;
message: string;
expected?: string;
received?: string;
}
export interface ActionContract {
actionType: string;
description: string;
requiredParams: string[];
optionalParams: string[];
validators?: Record<string, (value: any) => boolean>;
}
/**
* Vérifie si visual_anchor est présent et valide
*/
function hasVisualAnchor(params: Record<string, any>): boolean {
const anchor = params.visual_anchor || params.target || params.visualSelection;
if (!anchor) return false;
if (typeof anchor !== 'object') return false;
// Doit avoir soit une image, soit des coordonnées, soit un ID
const hasImage = !!(
anchor.screenshot ||
anchor.image ||
anchor.reference_image_base64 ||
anchor.id ||
anchor.anchor_id
);
const hasCoords = !!(
anchor.bounding_box ||
anchor.boundingBox
);
const hasServerStorage = !!(
anchor.metadata?.uses_server_storage ||
anchor.metadata?.thumbnail_url
);
return hasImage || hasCoords || hasServerStorage;
}
/**
* Vérifie si text est présent et non vide
*/
function hasText(params: Record<string, any>): boolean {
const text = params.text || params.text_to_type || params.texte;
return !!(text && typeof text === 'string' && text.trim().length > 0);
}
// =============================================================================
// DÉFINITION DES CONTRATS POUR CHAQUE ACTION VWB
// =============================================================================
export const VWB_ACTION_CONTRACTS: Record<string, ActionContract> = {
// --- ACTIONS DE CLIC ---
click_anchor: {
actionType: 'click_anchor',
description: 'Clic sur un élément identifié par ancre visuelle',
requiredParams: ['visual_anchor'],
optionalParams: ['click_type', 'click_offset_x', 'click_offset_y', 'confidence_threshold'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
double_click_anchor: {
actionType: 'double_click_anchor',
description: 'Double-clic sur un élément identifié par ancre visuelle',
requiredParams: ['visual_anchor'],
optionalParams: ['click_offset_x', 'click_offset_y', 'confidence_threshold'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
right_click_anchor: {
actionType: 'right_click_anchor',
description: 'Clic droit sur un élément identifié par ancre visuelle',
requiredParams: ['visual_anchor'],
optionalParams: ['click_offset_x', 'click_offset_y', 'confidence_threshold'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
hover_anchor: {
actionType: 'hover_anchor',
description: 'Survol d\'un élément identifié par ancre visuelle',
requiredParams: ['visual_anchor'],
optionalParams: ['hover_duration_ms', 'confidence_threshold'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
// --- ACTIONS DE SAISIE ---
type_text: {
actionType: 'type_text',
description: 'Saisie de texte (le focus doit être déjà fait)',
requiredParams: ['text'],
optionalParams: ['typing_speed_ms', 'clear_field_first', 'press_enter_after'],
validators: {
text: (v) => !!(v && typeof v === 'string')
}
},
type_secret: {
actionType: 'type_secret',
description: 'Saisie sécurisée de texte sensible',
requiredParams: ['secret_text'],
optionalParams: ['typing_speed_ms', 'clear_field_first', 'mask_in_evidence'],
validators: {
secret_text: (v) => !!(v && typeof v === 'string')
}
},
// --- ACTIONS DE FOCUS ---
focus_anchor: {
actionType: 'focus_anchor',
description: 'Donne le focus à un élément',
requiredParams: ['visual_anchor'],
optionalParams: ['focus_method', 'verify_focus', 'confidence_threshold'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
// --- ACTIONS D'ATTENTE ---
wait_for_anchor: {
actionType: 'wait_for_anchor',
description: 'Attendre qu\'un élément apparaisse ou disparaisse',
requiredParams: ['visual_anchor'],
optionalParams: ['wait_mode', 'max_wait_time_ms', 'check_interval_ms'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
// --- ACTIONS DE SCROLL ---
scroll_to_anchor: {
actionType: 'scroll_to_anchor',
description: 'Défiler jusqu\'à ce qu\'un élément soit visible',
requiredParams: ['visual_anchor'],
optionalParams: ['scroll_direction', 'scroll_speed', 'max_scroll_attempts'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
drag_drop_anchor: {
actionType: 'drag_drop_anchor',
description: 'Glisser-déposer d\'un élément vers un autre',
requiredParams: ['source_anchor', 'target_anchor'],
optionalParams: ['drag_speed', 'hold_duration_ms'],
validators: {
source_anchor: (v) => hasVisualAnchor({ visual_anchor: v }),
target_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
// --- ACTIONS CLAVIER ---
keyboard_shortcut: {
actionType: 'keyboard_shortcut',
description: 'Exécuter un raccourci clavier',
requiredParams: ['keys'],
optionalParams: ['hold_duration_ms'],
validators: {
keys: (v) => Array.isArray(v) && v.length > 0
}
},
// --- ACTIONS D'EXTRACTION ---
extract_text: {
actionType: 'extract_text',
description: 'Extraire du texte d\'une zone',
requiredParams: ['visual_anchor'],
optionalParams: ['extraction_mode', 'text_filters', 'output_format'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
extract_table: {
actionType: 'extract_table',
description: 'Extraire un tableau d\'une zone',
requiredParams: ['visual_anchor'],
optionalParams: ['table_format', 'output_variable'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
screenshot_evidence: {
actionType: 'screenshot_evidence',
description: 'Capturer une preuve visuelle',
requiredParams: [],
optionalParams: ['region', 'label', 'include_timestamp']
},
// --- ACTIONS CONDITIONNELLES ---
visual_condition: {
actionType: 'visual_condition',
description: 'Condition basée sur présence d\'élément visuel',
requiredParams: ['visual_anchor'],
optionalParams: ['condition_type', 'timeout_ms'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
loop_visual: {
actionType: 'loop_visual',
description: 'Boucle tant qu\'un élément est visible',
requiredParams: ['visual_anchor'],
optionalParams: ['max_iterations', 'timeout_ms'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
// --- ACTIONS VÉRIFICATION ---
verify_element_exists: {
actionType: 'verify_element_exists',
description: 'Vérifier qu\'un élément existe',
requiredParams: ['visual_anchor'],
optionalParams: ['timeout_ms', 'should_exist'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
},
verify_text_content: {
actionType: 'verify_text_content',
description: 'Vérifier le contenu textuel',
requiredParams: ['visual_anchor', 'expected_text'],
optionalParams: ['match_mode', 'case_sensitive'],
validators: {
visual_anchor: (v) => hasVisualAnchor({ visual_anchor: v })
}
}
};
/**
* Classe d'erreur pour les violations de contrat
*/
export class ContractValidationError extends Error {
public violations: ContractViolation[];
public actionType: string;
constructor(violations: ContractViolation[], actionType: string) {
const messages = violations.map(v => v.message).join('; ');
super(`Contrat violé pour '${actionType}': ${messages}`);
this.name = 'ContractValidationError';
this.violations = violations;
this.actionType = actionType;
}
toDict(): Record<string, any> {
return {
error: 'contract_violation',
actionType: this.actionType,
violations: this.violations,
message: this.message
};
}
}
/**
* Valide les paramètres d'une action contre son contrat
*/
export function validateActionContract(
actionType: string,
parameters: Record<string, any>
): ContractViolation[] {
const normalizedType = actionType.toLowerCase().trim();
const contract = VWB_ACTION_CONTRACTS[normalizedType];
if (!contract) {
console.warn(`⚠️ [Contract] Action '${actionType}' non reconnue dans les contrats`);
return [];
}
const violations: ContractViolation[] = [];
// Vérifier les paramètres obligatoires
for (const param of contract.requiredParams) {
const value = parameters[param];
if (value === undefined || value === null) {
violations.push({
violationType: ContractViolationType.MISSING_REQUIRED,
parameter: param,
message: `Paramètre obligatoire '${param}' manquant pour l'action '${actionType}'`,
expected: `'${param}' doit être fourni`,
received: 'absent ou null'
});
} else if (contract.validators && contract.validators[param]) {
// Valider le contenu
if (!contract.validators[param](value)) {
violations.push({
violationType: ContractViolationType.INVALID_VALUE,
parameter: param,
message: `Valeur invalide pour '${param}' dans l'action '${actionType}'`,
expected: 'valeur valide selon les règles du contrat',
received: typeof value
});
}
}
}
return violations;
}
/**
* Valide et BLOQUE si le contrat n'est pas respecté
*/
export function enforceActionContract(
actionType: string,
parameters: Record<string, any>
): void {
const violations = validateActionContract(actionType, parameters);
if (violations.length > 0) {
console.error(`🚫 [Contract] VIOLATION DÉTECTÉE pour '${actionType}':`);
violations.forEach(v => {
console.error(` - ${v.parameter}: ${v.message}`);
});
throw new ContractValidationError(violations, actionType);
}
console.log(`✅ [Contract] Contrat respecté pour '${actionType}'`);
}
/**
* Retourne le contrat d'une action
*/
export function getActionContract(actionType: string): ActionContract | undefined {
return VWB_ACTION_CONTRACTS[actionType.toLowerCase().trim()];
}
/**
* Retourne les paramètres obligatoires pour une action
*/
export function getRequiredParams(actionType: string): string[] {
const contract = getActionContract(actionType);
return contract?.requiredParams || [];
}
/**
* Vérifie si un type d'action est reconnu
*/
export function isKnownActionType(actionType: string): boolean {
return actionType.toLowerCase().trim() in VWB_ACTION_CONTRACTS;
}
/**
* Liste tous les types d'actions avec contrat
*/
export function listAllActionTypes(): string[] {
return Object.keys(VWB_ACTION_CONTRACTS);
}

View File

@@ -0,0 +1,20 @@
/**
* Module de Contrats VWB - Exports
*/
export {
ContractViolationType,
ContractValidationError,
VWB_ACTION_CONTRACTS,
validateActionContract,
enforceActionContract,
getActionContract,
getRequiredParams,
isKnownActionType,
listAllActionTypes
} from './actionContracts';
export type {
ContractViolation,
ActionContract
} from './actionContracts';