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:
@@ -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);
|
||||
}
|
||||
20
visual_workflow_builder/frontend/src/contracts/index.ts
Normal file
20
visual_workflow_builder/frontend/src/contracts/index.ts
Normal 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';
|
||||
Reference in New Issue
Block a user