diff --git a/deploy/installer/LICENSE.txt b/deploy/installer/LICENSE.txt new file mode 100644 index 000000000..21658bb72 --- /dev/null +++ b/deploy/installer/LICENSE.txt @@ -0,0 +1,61 @@ +============================================================ + Lea - Conditions Generales d'Utilisation +============================================================ + +Version 1.0 — Avril 2026 +Editeur : AIVANOV + +1. OBJET +-------- +Lea est un logiciel d'assistance intelligente destine a automatiser +des taches repetitives sur poste de travail Windows, pour le compte +de son employeur (AIVANOV et ses clients autorises). + +2. NATURE DES DONNEES COLLECTEES +-------------------------------- +Lors de son utilisation, Lea capture : + - Des captures d'ecran du poste de travail + - Les evenements clavier et souris + - Les metadonnees systeme (nom de la machine, processus actifs) + +Les donnees sensibles (mots de passe, numeros de securite sociale, +informations de cartes bancaires) sont automatiquement floutees +avant transmission au serveur, sauf desactivation explicite par +l'administrateur (parametre RPA_BLUR_SENSITIVE=false). + +3. TRANSMISSION ET STOCKAGE +--------------------------- +Les donnees sont transmises via HTTPS chiffre a un serveur central +gere par AIVANOV. Elles sont conservees 180 jours minimum pour des +raisons de conformite, puis purgees automatiquement. + +4. SYSTEME D'IA (AI ACT - ARTICLE 50) +------------------------------------- +Lea utilise des modeles d'intelligence artificielle pour comprendre +et automatiser les taches. Conformement a l'Article 50 du Reglement +europeen sur l'Intelligence Artificielle, l'utilisateur est informe +qu'il interagit avec un systeme d'IA. + +5. CONTROLE PAR L'UTILISATEUR +----------------------------- +L'utilisateur peut a tout moment : + - Arreter l'enregistrement (clic droit sur icone > C'est termine) + - Declencher un arret d'urgence (clic droit > ARRET D'URGENCE) + - Quitter Lea completement (clic droit > Quitter Lea) + - Desinstaller Lea via le panneau de configuration Windows + +6. RESPONSABILITE +----------------- +L'utilisateur s'engage a ne pas utiliser Lea sur des donnees qu'il +n'est pas autorise a traiter dans le cadre de ses fonctions. +AIVANOV ne pourra etre tenu responsable d'un usage non conforme. + +7. CONTACT +---------- +Pour toute question ou demande d'acces/rectification/suppression +de donnees : dpo@aivanov.com + +============================================================ +En cliquant sur "J'accepte", vous confirmez avoir pris connaissance +de ces conditions et les accepter. +============================================================ diff --git a/deploy/installer/Lea.iss b/deploy/installer/Lea.iss new file mode 100644 index 000000000..261ef4398 --- /dev/null +++ b/deploy/installer/Lea.iss @@ -0,0 +1,554 @@ +; ============================================================ +; Lea.iss — Script Inno Setup pour l'installeur Lea +; ------------------------------------------------------------ +; Compile avec Inno Setup 6.2+ (ISCC.exe Lea.iss) +; +; Ce script produit Lea-Setup-v{VERSION}.exe dans ..\releases\ +; +; Fonctions principales : +; - Page de bienvenue + licence (CGU) +; - Page custom d'enrollment (nom, email, ID AIVANOV, URL, token) +; - Generation d'un machine_id unique par poste +; - Generation automatique de config.txt +; - Installation silencieuse de Python 3.12 embedded (optionnelle) +; - Raccourci demarrage automatique (checkbox) +; - Installation silencieuse : /VERYSILENT /CONFIG=path\to\config.txt +; - Desinstallation propre (kill process, cleanup, export logs) +; +; Pre-requis staging : +; Le dossier ..\build\installer_staging\ doit contenir : +; - Le package Lea complet (agent_v1/, lea_ui/, run_agent_v1.py, Lea.bat, ...) +; - Optionnel : python-3.12-embed\ (runtime Python embedded pre-configure) +; build_installer.sh s'occupe de preparer ce staging. +; ============================================================ + +#define MyAppName "Lea" +#define MyAppVersion "1.0.0" +#define MyAppPublisher "AIVANOV" +#define MyAppURL "https://lea.labs.laurinebazin.design" +#define MyAppExeName "Lea.bat" +#define MyAppDescription "Lea - Assistante IA pour l'automatisation" + +; Chemin du staging (peut etre surcharge via ISCC /DSourceDir=...) +#ifndef SourceDir + #define SourceDir "..\build\installer_staging" +#endif + +; Chemin de sortie des installeurs +#ifndef OutputDir + #define OutputDir "..\releases" +#endif + +; Activer le bundle Python embedded si present dans le staging +#define PythonEmbedDir "python-3.12-embed" + +[Setup] +AppId={{B3F9A1E2-5C4D-4E7F-9A1B-2C3D4E5F6789} +AppName={#MyAppName} +AppVersion={#MyAppVersion} +AppVerName={#MyAppName} {#MyAppVersion} +AppPublisher={#MyAppPublisher} +AppPublisherURL={#MyAppURL} +AppSupportURL={#MyAppURL} +AppUpdatesURL={#MyAppURL} +DefaultDirName={autopf}\{#MyAppName} +DefaultGroupName={#MyAppName} +DisableProgramGroupPage=yes +OutputDir={#OutputDir} +OutputBaseFilename=Lea-Setup-v{#MyAppVersion} +; Compression correcte (pas trop aggressive pour que l'install reste rapide) +Compression=lzma2 +SolidCompression=yes +; Support HiDPI +WizardStyle=modern +; Langue FR par defaut +ShowLanguageDialog=no +; Autorise l'install en mode user si pas admin (bascule sur LOCALAPPDATA) +PrivilegesRequired=lowest +PrivilegesRequiredOverridesAllowed=dialog +; Icone de l'installeur (decommenter si disponible) +; SetupIconFile=lea.ico +; Uninstall +UninstallDisplayName={#MyAppName} {#MyAppVersion} +; UninstallDisplayIcon={app}\lea.ico ; decommenter quand l'icone sera fournie +; Architecture : 64-bit uniquement (Windows 10+ / 11) +ArchitecturesAllowed=x64compatible +ArchitecturesInstallIn64BitMode=x64compatible +; Version minimale Windows : 10 +MinVersion=10.0 +; Informations legales +VersionInfoVersion={#MyAppVersion} +VersionInfoCompany={#MyAppPublisher} +VersionInfoDescription={#MyAppDescription} +VersionInfoCopyright=Copyright (C) 2026 {#MyAppPublisher} +; Licence CGU affichee avant le choix du repertoire +LicenseFile=LICENSE.txt + +[Languages] +Name: "french"; MessagesFile: "compiler:Languages\French.isl" + +[Files] +; Package complet (code Python + .bat + requirements) +; Note : install.bat EST copie (execute par [Run] pour creer le venv Python) +; Note : config.txt n'est PAS copie depuis le staging (il est genere par [Code]) +Source: "{#SourceDir}\*"; \ + DestDir: "{app}"; \ + Flags: ignoreversion recursesubdirs createallsubdirs; \ + Excludes: "{#PythonEmbedDir}\*,config.txt,*.log,sessions\*,__pycache__\*" + +; Python 3.12 embedded (optionnel, copie conditionnelle via check) +Source: "{#SourceDir}\{#PythonEmbedDir}\*"; \ + DestDir: "{app}\python-embed"; \ + Flags: ignoreversion recursesubdirs createallsubdirs skipifsourcedoesntexist; \ + Components: pythonembed + +; Script de desinstallation custom (kill + export logs) +Source: "uninstall_lea.ps1"; DestDir: "{app}"; Flags: ignoreversion + +; Script de configuration du runtime Python embedded (optionnel) +Source: "configure_embed.ps1"; DestDir: "{app}"; Flags: ignoreversion; Components: pythonembed + +; Licence CGU (affichee dans la page licence ET conservee dans {app}) +Source: "LICENSE.txt"; DestDir: "{app}"; Flags: ignoreversion isreadme + +; Template de config pour installation silencieuse (reference) +Source: "config_template.txt"; DestDir: "{app}"; Flags: ignoreversion + +[Components] +Name: "core"; Description: "Lea (obligatoire)"; Types: full compact custom; Flags: fixed +Name: "pythonembed"; Description: "Python 3.12 embedded (recommande si Python non installe sur le poste)"; Types: full +Name: "autostart"; Description: "Demarrer Lea automatiquement au demarrage de Windows"; Types: full + +[Tasks] +Name: "desktopicon"; Description: "Creer un raccourci sur le bureau"; GroupDescription: "Raccourcis :"; Flags: unchecked +Name: "startmenuicon"; Description: "Creer un raccourci dans le menu Demarrer"; GroupDescription: "Raccourcis :" + +[Icons] +Name: "{autoprograms}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: startmenuicon +Name: "{autodesktop}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; WorkingDir: "{app}"; Tasks: desktopicon +; Raccourci autostart (shell:startup) — cree si composant autostart selectionne +Name: "{userstartup}\{#MyAppName}"; Filename: "{app}\{#MyAppExeName}"; \ + WorkingDir: "{app}"; Components: autostart + +[Run] +; Apres copie : executer install.bat pour creer le venv et installer les dependances Python +; Skip si bundle embedded (dans ce cas, on utilise python-embed directement) +Filename: "{app}\install.bat"; \ + WorkingDir: "{app}"; \ + StatusMsg: "Installation des composants Python (1-2 minutes)..."; \ + Flags: runhidden waituntilterminated; \ + Components: not pythonembed + +; Configuration Python embedded : creer un Lea.bat qui pointe sur python-embed +Filename: "{cmd}"; \ + Parameters: "/c copy /y ""{app}\Lea.bat"" ""{app}\Lea.bat.bak"" && powershell -NoProfile -ExecutionPolicy Bypass -File ""{app}\configure_embed.ps1"""; \ + WorkingDir: "{app}"; \ + StatusMsg: "Configuration du runtime Python embedded..."; \ + Flags: runhidden waituntilterminated skipifsilent; \ + Components: pythonembed + +; Lancer Lea a la fin de l'installation (optionnel) +Filename: "{app}\{#MyAppExeName}"; \ + Description: "Lancer {#MyAppName} maintenant"; \ + Flags: postinstall skipifsilent nowait shellexec + +[UninstallRun] +; Tuer le process via PID du lock avant suppression des fichiers +Filename: "powershell.exe"; \ + Parameters: "-NoProfile -ExecutionPolicy Bypass -File ""{app}\uninstall_lea.ps1"" -AppDir ""{app}"""; \ + RunOnceId: "KillLeaProcess"; \ + Flags: runhidden waituntilterminated + +[UninstallDelete] +Type: filesandordirs; Name: "{app}\.venv" +Type: filesandordirs; Name: "{app}\__pycache__" +Type: filesandordirs; Name: "{app}\agent_v1\__pycache__" +Type: filesandordirs; Name: "{app}\agent_v1\sessions" +Type: filesandordirs; Name: "{app}\agent_v1\logs" +Type: files; Name: "{app}\lea_agent.lock" +Type: files; Name: "{app}\config.txt" +Type: files; Name: "{app}\machine_id.txt" + +; ============================================================ +; Code Pascal : pages custom + generation config.txt + helpers +; ============================================================ +[Code] +const + SERVER_URL_DEFAULT = 'https://lea.labs.laurinebazin.design/api/v1'; + SERVER_HOST_DEFAULT = 'lea.labs.laurinebazin.design'; + DEFAULT_TOKEN = '86031addb338e449fccdb1a983f61807aec15d42d482b9c7748ad607dc23caab'; + +var + EnrollmentPage: TInputQueryWizardPage; + TokenPage: TInputQueryWizardPage; + MachineIdValue: string; + ConfigFilePath: string; + +// -------------------------------------------------------------------- +// Helper : ajoute des guillemets autour d'une chaine +// -------------------------------------------------------------------- +function AddQuotes(const S: string): string; +begin + Result := '"' + S + '"'; +end; + +// -------------------------------------------------------------------- +// Wrapper CreateGUIDString (via PowerShell, fallback par defaut) +// -------------------------------------------------------------------- +function CreateGUIDString(var Guid: string): Boolean; +var + ResultCode: Integer; + TmpFile: string; + Lines: TArrayOfString; +begin + Result := False; + TmpFile := ExpandConstant('{tmp}\guid.txt'); + // powershell : genere un GUID + if Exec('powershell.exe', + '-NoProfile -Command "[guid]::NewGuid().ToString() | Out-File -Encoding ASCII ' + AddQuotes(TmpFile) + '"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode) then + begin + if LoadStringsFromFile(TmpFile, Lines) and (GetArrayLength(Lines) > 0) then + begin + Guid := Trim(Lines[0]); + Result := Length(Guid) > 0; + end; + DeleteFile(TmpFile); + end; +end; + +// -------------------------------------------------------------------- +// Recupere le hostname de la machine +// -------------------------------------------------------------------- +function GetComputerNameString(): string; +var + Buffer: string; +begin + Buffer := ExpandConstant('{computername}'); + if Length(Buffer) = 0 then + Buffer := 'unknown-host'; + Result := Buffer; +end; + +// -------------------------------------------------------------------- +// Genere un identifiant machine unique : UUID4 + hostname hashe +// -------------------------------------------------------------------- +function GenerateMachineId(): string; +var + Guid: string; + Hostname: string; + I: Integer; + Hash: Cardinal; +begin + // Essaye d'utiliser le GUID genere par Windows (via PowerShell) + Guid := ''; + if CreateGUIDString(Guid) then + Result := LowerCase(StringChange(StringChange(StringChange(Guid, '{', ''), '}', ''), '-', '')) + else + Result := IntToStr(GetTickCount); + + // Ajoute un hash du hostname pour stabilite + Hostname := GetComputerNameString(); + Hash := 0; + for I := 1 to Length(Hostname) do + Hash := (Hash * 31 + Ord(Hostname[I])) and $FFFFFFFF; + + Result := Copy(Result, 1, 16) + '-' + Format('%08x', [Hash]); +end; + +// -------------------------------------------------------------------- +// Charge une configuration depuis /CONFIG=path (installation silencieuse) +// Format du fichier : NOM=valeur, une ligne par parametre +// Cles attendues : USER_NAME, USER_EMAIL, USER_ID, SERVER_URL, API_TOKEN +// -------------------------------------------------------------------- +procedure LoadConfigFromCommandLine(); forward; + +// -------------------------------------------------------------------- +// Initialisation : cree les pages custom d'enrollment +// -------------------------------------------------------------------- +procedure InitializeWizard(); +begin + // Page 1 : informations collaborateur + EnrollmentPage := CreateInputQueryPage(wpSelectTasks, + 'Identification du collaborateur', + 'Veuillez renseigner vos informations pour l''enrollment', + 'Ces informations sont envoyees au serveur Lea pour identifier votre poste. ' + + 'Elles sont stockees de maniere securisee et ne sont jamais partagees avec des tiers.'); + + EnrollmentPage.Add('Nom et prenom :', False); + EnrollmentPage.Add('Email professionnel :', False); + EnrollmentPage.Add('ID interne AIVANOV (optionnel) :', False); + + EnrollmentPage.Values[0] := ''; + EnrollmentPage.Values[1] := ''; + EnrollmentPage.Values[2] := ''; + + // Page 2 : configuration serveur (URL + token) + TokenPage := CreateInputQueryPage(EnrollmentPage.ID, + 'Connexion au serveur Lea', + 'Configuration de la connexion au serveur central', + 'L''URL du serveur est pre-remplie par defaut. Le token d''authentification ' + + 'vous est fourni par votre administrateur AIVANOV. Laissez la valeur par defaut ' + + 'si vous ne savez pas quoi mettre.'); + + TokenPage.Add('URL du serveur (avec /api/v1) :', False); + TokenPage.Add('Token d''authentification :', False); + + TokenPage.Values[0] := SERVER_URL_DEFAULT; + TokenPage.Values[1] := DEFAULT_TOKEN; + + // Si un fichier /CONFIG= est passe en ligne de commande, pre-remplir + LoadConfigFromCommandLine(); +end; + +// -------------------------------------------------------------------- +// Implementation de LoadConfigFromCommandLine (declare en forward ci-dessus) +// -------------------------------------------------------------------- +procedure LoadConfigFromCommandLine(); +var + ConfigParam: string; + Lines: TArrayOfString; + I: Integer; + Line, Key, Value: string; + EqPos: Integer; +begin + ConfigParam := ExpandConstant('{param:CONFIG}'); + if Length(ConfigParam) = 0 then Exit; + if not FileExists(ConfigParam) then Exit; + + if not LoadStringsFromFile(ConfigParam, Lines) then Exit; + + for I := 0 to GetArrayLength(Lines) - 1 do + begin + Line := Trim(Lines[I]); + if (Length(Line) = 0) or (Line[1] = '#') then Continue; + + EqPos := Pos('=', Line); + if EqPos = 0 then Continue; + + Key := Trim(Copy(Line, 1, EqPos - 1)); + Value := Trim(Copy(Line, EqPos + 1, Length(Line))); + + if Key = 'USER_NAME' then EnrollmentPage.Values[0] := Value + else if Key = 'USER_EMAIL' then EnrollmentPage.Values[1] := Value + else if Key = 'USER_ID' then EnrollmentPage.Values[2] := Value + else if Key = 'SERVER_URL' then TokenPage.Values[0] := Value + else if Key = 'API_TOKEN' then TokenPage.Values[1] := Value; + end; +end; + +// -------------------------------------------------------------------- +// Validation des pages custom (Nom/Email obligatoires, token non vide) +// -------------------------------------------------------------------- +function NextButtonClick(CurPageID: Integer): Boolean; +var + Email: string; +begin + Result := True; + + if CurPageID = EnrollmentPage.ID then + begin + if Length(Trim(EnrollmentPage.Values[0])) = 0 then + begin + MsgBox('Le nom est obligatoire.', mbError, MB_OK); + Result := False; + Exit; + end; + + Email := Trim(EnrollmentPage.Values[1]); + if (Length(Email) = 0) or (Pos('@', Email) = 0) then + begin + MsgBox('Un email valide est obligatoire.', mbError, MB_OK); + Result := False; + Exit; + end; + end; + + if CurPageID = TokenPage.ID then + begin + if Length(Trim(TokenPage.Values[0])) = 0 then + begin + MsgBox('L''URL du serveur est obligatoire.', mbError, MB_OK); + Result := False; + Exit; + end; + if Length(Trim(TokenPage.Values[1])) < 16 then + begin + if MsgBox('Le token parait court (< 16 caracteres). Continuer quand meme ?', + mbConfirmation, MB_YESNO) = IDNO then + begin + Result := False; + Exit; + end; + end; + end; +end; + +// -------------------------------------------------------------------- +// Ecrit config.txt genere dans le dossier d'installation +// -------------------------------------------------------------------- +procedure WriteGeneratedConfig(); +var + Config: string; + ServerUrl, ServerHost, Token: string; + UserName, UserEmail, UserId: string; + SlashPos: Integer; +begin + ConfigFilePath := ExpandConstant('{app}\config.txt'); + + ServerUrl := Trim(TokenPage.Values[0]); + Token := Trim(TokenPage.Values[1]); + UserName := Trim(EnrollmentPage.Values[0]); + UserEmail := Trim(EnrollmentPage.Values[1]); + UserId := Trim(EnrollmentPage.Values[2]); + + // Derive ServerHost depuis ServerUrl : https://host/api/v1 -> host + ServerHost := ServerUrl; + ServerHost := StringChange(ServerHost, 'https://', ''); + ServerHost := StringChange(ServerHost, 'http://', ''); + SlashPos := Pos('/', ServerHost); + if SlashPos > 0 then + ServerHost := Copy(ServerHost, 1, SlashPos - 1); + + Config := + '# ============================================================' + #13#10 + + '# Configuration Lea (genere par l''installeur)' + #13#10 + + '# ============================================================' + #13#10 + + '# Genere le ' + GetDateTimeString('yyyy-mm-dd hh:nn:ss', '-', ':') + #13#10 + + '# Installe par : ' + UserName + ' <' + UserEmail + '>' + #13#10 + + '# ID interne : ' + UserId + #13#10 + + '# Machine ID : ' + MachineIdValue + #13#10 + + '# ============================================================' + #13#10 + + '' + #13#10 + + '# Adresse du serveur Lea (URL complete avec /api/v1)' + #13#10 + + 'RPA_SERVER_URL=' + ServerUrl + #13#10 + + '' + #13#10 + + '# Cle d''authentification (fournie par l''administrateur)' + #13#10 + + 'RPA_API_TOKEN=' + Token + #13#10 + + '' + #13#10 + + '# Nom du serveur (sans https://, sans /api/v1)' + #13#10 + + 'RPA_SERVER_HOST=' + ServerHost + #13#10 + + '' + #13#10 + + '# Identifiant unique de cette machine (genere a l''install)' + #13#10 + + 'RPA_MACHINE_ID=' + MachineIdValue + #13#10 + + '' + #13#10 + + '# Informations collaborateur (utilisees pour l''audit cote serveur)' + #13#10 + + 'RPA_USER_NAME=' + UserName + #13#10 + + 'RPA_USER_EMAIL=' + UserEmail + #13#10; + + if Length(UserId) > 0 then + Config := Config + 'RPA_USER_ID=' + UserId + #13#10; + + Config := Config + '' + #13#10 + + '# ============================================================' + #13#10 + + '# Parametres avances (ne pas modifier sauf indication)' + #13#10 + + '# ============================================================' + #13#10 + + '' + #13#10 + + '# Flouter les zones de texte dans les captures (securite donnees)' + #13#10 + + 'RPA_BLUR_SENSITIVE=true' + #13#10 + + '' + #13#10 + + '# Duree de conservation des logs en jours (minimum 180 pour conformite)' + #13#10 + + 'RPA_LOG_RETENTION_DAYS=180' + #13#10; + + if not SaveStringToFile(ConfigFilePath, Config, False) then + MsgBox('Echec de l''ecriture de config.txt dans ' + ConfigFilePath, mbError, MB_OK); +end; + +// -------------------------------------------------------------------- +// Ecrit le machine_id.txt (identifiant du poste) +// -------------------------------------------------------------------- +procedure WriteMachineId(); +var + MachineIdFile: string; +begin + MachineIdFile := ExpandConstant('{app}\machine_id.txt'); + if not SaveStringToFile(MachineIdFile, MachineIdValue, False) then + MsgBox('Echec de l''ecriture de machine_id.txt', mbError, MB_OK); +end; + +// -------------------------------------------------------------------- +// Notifie le serveur de l'enrollment (best-effort, non bloquant) +// POST vers {SERVER_URL}/agents/enroll avec les infos collaborateur +// -------------------------------------------------------------------- +procedure NotifyServerEnrollment(); +var + ResultCode: Integer; + PsScript: string; + PsFile: string; + ServerUrl, Token: string; +begin + ServerUrl := Trim(TokenPage.Values[0]); + Token := Trim(TokenPage.Values[1]); + + PsFile := ExpandConstant('{tmp}\enroll.ps1'); + PsScript := + '$ErrorActionPreference = ''SilentlyContinue''' + #13#10 + + '$body = @{' + #13#10 + + ' machine_id = ''' + MachineIdValue + '''' + #13#10 + + ' hostname = $env:COMPUTERNAME' + #13#10 + + ' user_name = ''' + EnrollmentPage.Values[0] + '''' + #13#10 + + ' user_email = ''' + EnrollmentPage.Values[1] + '''' + #13#10 + + ' user_id = ''' + EnrollmentPage.Values[2] + '''' + #13#10 + + ' agent_version = ''' + '{#MyAppVersion}' + '''' + #13#10 + + '} | ConvertTo-Json' + #13#10 + + 'try {' + #13#10 + + ' Invoke-RestMethod -Uri ''' + ServerUrl + '/agents/enroll'' ' + + '-Method POST -Body $body -ContentType ''application/json'' ' + + '-Headers @{ Authorization = ''Bearer ' + Token + ''' } -TimeoutSec 10 | Out-Null' + #13#10 + + '} catch { exit 0 }' + #13#10; + + SaveStringToFile(PsFile, PsScript, False); + Exec('powershell.exe', + '-NoProfile -ExecutionPolicy Bypass -File ' + AddQuotes(PsFile), + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + DeleteFile(PsFile); +end; + +// -------------------------------------------------------------------- +// Hook : actions apres copie des fichiers (ssPostInstall) +// -------------------------------------------------------------------- +procedure CurStepChanged(CurStep: TSetupStep); +begin + if CurStep = ssInstall then + begin + // Genere le machine_id AVANT la copie des fichiers + MachineIdValue := GenerateMachineId(); + end; + + if CurStep = ssPostInstall then + begin + // Ecrit config.txt et machine_id.txt + WriteGeneratedConfig(); + WriteMachineId(); + // Notifie le serveur (best-effort) + NotifyServerEnrollment(); + end; +end; + +// -------------------------------------------------------------------- +// Desinstallation : proposer d'exporter les logs avant suppression +// -------------------------------------------------------------------- +function InitializeUninstall(): Boolean; +var + LogDir, ExportDir: string; + ResultCode: Integer; +begin + Result := True; + LogDir := ExpandConstant('{app}\agent_v1\logs'); + + if DirExists(LogDir) then + begin + if MsgBox('Voulez-vous exporter les logs de Lea avant la desinstallation ?' + #13#10 + + '(les logs seront copies dans votre dossier Documents)', + mbConfirmation, MB_YESNO) = IDYES then + begin + ExportDir := ExpandConstant('{userdocs}\Lea_logs_export'); + ForceDirectories(ExportDir); + Exec('powershell.exe', + '-NoProfile -Command "Copy-Item -Path ' + AddQuotes(LogDir + '\*') + + ' -Destination ' + AddQuotes(ExportDir) + ' -Recurse -Force"', + '', SW_HIDE, ewWaitUntilTerminated, ResultCode); + MsgBox('Logs exportes dans : ' + ExportDir, mbInformation, MB_OK); + end; + end; +end; diff --git a/deploy/installer/README.md b/deploy/installer/README.md new file mode 100644 index 000000000..db5cf39d3 --- /dev/null +++ b/deploy/installer/README.md @@ -0,0 +1,227 @@ +# Installeur Lea (Inno Setup) + +Installeur Windows professionnel pour Lea, remplacant le ZIP + `install.bat` artisanal. + +## Resume + +Produit `Lea-Setup-v1.0.0.exe` dans `deploy/releases/`. + +Caracteristiques : +- Interface francaise, moderne (style wizard Windows 10/11) +- Page custom d'enrollment (nom, email, ID interne, URL serveur, token) +- Generation automatique de `machine_id` unique (GUID + hash hostname) +- `config.txt` genere a partir des donnees saisies +- Option bundle Python 3.12 embedded (postes sans droits admin) +- Raccourci demarrage automatique (`shell:startup`) optionnel +- Notification serveur a l'install / desinstall (best-effort) +- Installation silencieuse : `/VERYSILENT /CONFIG=enroll.txt` +- Desinstallation propre : kill process, cleanup, export logs + +## Pre-requis pour compiler + +### Inno Setup 6.2+ + +Telecharger depuis [jrsoftware.org](https://jrsoftware.org/isinfo.php) et installer +`innosetup-6.x.exe`. Le compilateur `ISCC.exe` doit etre accessible. + +### Alternative Linux : Wine + +```bash +# Installation +winetricks innosetup +# Ou : telecharger innosetup-6.x.exe et lancer : wine innosetup-6.x.exe + +# Verifier +ls "$HOME/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe" +``` + +Le script `build_installer.sh` detecte automatiquement Wine si present. + +## Build local + +### Build complet (staging + compilation) + +```bash +cd rpa_vision_v3 +./deploy/installer/build_installer.sh +``` + +Produit `deploy/releases/Lea-Setup-v1.0.0.exe`. + +### Build staging uniquement (sans ISCC) + +```bash +./deploy/installer/build_installer.sh --stage-only +``` + +Prepare `deploy/build/installer_staging/` puis affiche la commande ISCC a executer +sur Windows. + +### Nettoyer avant + +```bash +./deploy/installer/build_installer.sh --clean +``` + +## Build sur Windows (recommande pour production) + +1. Copier le dossier `deploy/` sur le PC Windows +2. Ouvrir `deploy/installer/Lea.iss` dans Inno Setup Compiler +3. `Build > Compile` (ou F9) +4. Recuperer `deploy/releases/Lea-Setup-v1.0.0.exe` + +## Python 3.12 embedded (optionnel) + +Pour bundler Python directement dans l'installeur (evite d'exiger que les postes +aient Python installe) : + +```bash +# Sur Linux +cd deploy/installer +wget https://www.python.org/ftp/python/3.12.8/python-3.12.8-embed-amd64.zip +mkdir python-3.12-embed +unzip python-3.12.8-embed-amd64.zip -d python-3.12-embed/ +``` + +Le staging copie automatiquement ce dossier si present. Le composant +"pythonembed" devient alors selectionnable dans l'installeur. + +Le script `configure_embed.ps1` : +1. Patche `python312._pth` pour activer `import site` +2. Installe `pip` via `get-pip.py` +3. Installe `requirements_agent.txt` +4. Reecrit `Lea.bat` pour pointer sur `python-embed\pythonw.exe` + +## Installation silencieuse (deploiement de masse) + +Pour deployer sans interaction utilisateur (GPO, SCCM, script PowerShell) : + +1. Preparer un fichier `enroll.txt` par poste (ou un commun) : + + ``` + USER_NAME=Jean Dupont + USER_EMAIL=jean.dupont@aivanov.com + USER_ID=EMP-00123 + SERVER_URL=https://lea.labs.laurinebazin.design/api/v1 + API_TOKEN=xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx + ``` + +2. Lancer l'installeur : + + ```cmd + Lea-Setup-v1.0.0.exe /VERYSILENT /CONFIG=C:\temp\enroll.txt /DIR="C:\Lea" + ``` + +Parametres Inno Setup utiles : +- `/VERYSILENT` : aucune UI +- `/SILENT` : barre de progression seulement +- `/DIR="..."` : dossier d'installation +- `/LOG="install.log"` : log d'installation +- `/TASKS="startmenuicon,autostart"` : composants a installer (voir `[Tasks]` et `[Components]`) +- `/CONFIG=path` : fichier d'enrollment (custom, specifique a Lea) + +## Signature du .exe (SmartScreen) + +Sans signature, Windows SmartScreen affiche un avertissement rouge ("Cet editeur +est inconnu"). Pour eviter cela, signer l'installeur avec un certificat +code-signing. + +### Options de certificat + +1. **Certificat OV (Organization Validation)** : ~200-400 EUR/an + - Sectigo, DigiCert, GlobalSign + - SmartScreen apprend la reputation progressivement (~30 installations) + - Livre sur token USB FIPS depuis 2023 + +2. **Certificat EV (Extended Validation)** : ~400-700 EUR/an + - Reputation SmartScreen immediate (pas d'avertissement des la 1ere install) + - Strict : obligatoirement sur token USB + +### Signature manuelle (avec signtool.exe du Windows SDK) + +```cmd +signtool sign ^ + /tr http://timestamp.sectigo.com ^ + /td sha256 ^ + /fd sha256 ^ + /a ^ + "deploy\releases\Lea-Setup-v1.0.0.exe" + +signtool verify /pa /v "deploy\releases\Lea-Setup-v1.0.0.exe" +``` + +### Signature automatique dans Inno Setup + +Ajouter dans `Lea.iss` apres `[Setup]` : + +``` +SignTool=signtool $f +``` + +Et declarer le signtool via `ISCC.exe /Ssigntool=...` au build. + +### Solution interne (certif AIVANOV) + +Si AIVANOV a deja un certificat code-signing, le token USB + mot de passe +suffisent. Sinon, Sectigo OV est un bon choix d'entree de gamme. + +## Structure du dossier installer/ + +``` +deploy/installer/ +├── Lea.iss # Script Inno Setup principal +├── build_installer.sh # Helper bash (staging + ISCC) +├── uninstall_lea.ps1 # Script de desinstallation propre +├── configure_embed.ps1 # Configuration Python embedded +├── config_template.txt # Modele config pour /VERYSILENT /CONFIG= +├── LICENSE.txt # CGU affichees dans la page licence +└── README.md # Ce fichier +``` + +## Test de l'installeur + +1. **Machine de test Windows 11** (VM ou PC physique, idealement sans Python) +2. Copier `Lea-Setup-v1.0.0.exe` sur la machine +3. Double-cliquer : verifier que l'enrollment s'affiche en francais +4. Tester l'installation (avec et sans Python deja installe) +5. Verifier le fichier `C:\Program Files\Lea\config.txt` genere +6. Verifier le raccourci `shell:startup` (si option cochee) +7. Lancer Lea, verifier la connexion au serveur +8. Tester la desinstallation depuis "Ajout/suppression de programmes" + +### Test automatise (PowerShell, sur la VM) + +```powershell +# Installation silencieuse +$cfg = "C:\temp\enroll.txt" +@" +USER_NAME=Test Automatique +USER_EMAIL=test@aivanov.com +"@ | Out-File -Encoding ASCII $cfg + +.\Lea-Setup-v1.0.0.exe /VERYSILENT /CONFIG=$cfg /LOG="C:\temp\install.log" + +# Verifications +Test-Path "C:\Program Files\Lea\config.txt" +Get-Content "C:\Program Files\Lea\machine_id.txt" + +# Desinstallation silencieuse +$uninst = Get-WmiObject Win32_Product | Where-Object { $_.Name -like "Lea*" } +$uninst.Uninstall() +``` + +## Notes et limites connues + +- **Endpoint serveur `/agents/enroll` et `/agents/uninstall` :** pas encore + implemente cote serveur (avril 2026). L'installeur envoie la requete en + best-effort, une erreur est silencieusement ignoree. A implementer dans + `agent_v0/server_v1/api_stream.py` quand necessaire. +- **Python embedded :** le patch `python312._pth` + pip bootstrap fonctionne mais + augmente la taille de l'installeur (~25 MB). A reserver aux postes sans + Python. +- **Code signing :** indispensable pour deploiement hopital/client. Prevoir le + budget certificat (400-700 EUR/an) dans la roadmap commerciale. + +## Historique + +- v1.0.0 (2026-04-13) : Premiere version de l'installeur Inno Setup. diff --git a/deploy/installer/build_installer.sh b/deploy/installer/build_installer.sh new file mode 100755 index 000000000..63810ada8 --- /dev/null +++ b/deploy/installer/build_installer.sh @@ -0,0 +1,220 @@ +#!/bin/bash +# ============================================================ +# build_installer.sh — Prepare le staging et invoque ISCC +# ------------------------------------------------------------ +# +# Ce script : +# 1. Invoque build_package.sh pour generer le package classique +# 2. Copie le package dans deploy/build/installer_staging/ +# 3. Copie les helpers de l'installeur (uninstall, licence) +# 4. Appelle Inno Setup (ISCC.exe) si disponible +# (sinon, affiche les instructions pour compiler sous Windows) +# +# Usage : +# ./deploy/installer/build_installer.sh # Build complet +# ./deploy/installer/build_installer.sh --stage-only # Prepare le staging uniquement +# ./deploy/installer/build_installer.sh --clean # Nettoyer avant +# +# Pre-requis : +# - bash, rsync, zip (pour le package de base) +# - Inno Setup 6.2+ installe (Windows ou Wine) pour compiler +# +# Sur Linux, ISCC.exe peut etre execute via Wine : +# wine "/home/dom/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe" Lea.iss +# ============================================================ + +set -euo pipefail + +# Couleurs +GREEN='\033[0;32m' +YELLOW='\033[1;33m' +RED='\033[0;31m' +NC='\033[0m' + +SCRIPT_DIR="$(cd "$(dirname "${BASH_SOURCE[0]}")" && pwd)" +DEPLOY_DIR="$(dirname "$SCRIPT_DIR")" +PROJECT_ROOT="$(dirname "$DEPLOY_DIR")" + +STAGING_DIR="$DEPLOY_DIR/build/installer_staging" +RELEASES_DIR="$DEPLOY_DIR/releases" +BASE_BUILD_DIR="$DEPLOY_DIR/build/Lea" + +# Recupere la version depuis config.py +VERSION=$(grep -oP 'AGENT_VERSION\s*=\s*"([^"]+)"' "$PROJECT_ROOT/agent_v0/agent_v1/config.py" | grep -oP '"[^"]+"' | tr -d '"' || echo "1.0.0") + +echo -e "${GREEN}============================================================${NC}" +echo -e "${GREEN} Build installeur Inno Setup Lea v${VERSION}${NC}" +echo -e "${GREEN}============================================================${NC}" +echo "" + +# --------------------------------------------------------------- +# Parsing des arguments +# --------------------------------------------------------------- +STAGE_ONLY=0 +CLEAN=0 +for arg in "$@"; do + case "$arg" in + --stage-only) STAGE_ONLY=1 ;; + --clean) CLEAN=1 ;; + *) echo "Argument inconnu : $arg" ;; + esac +done + +# --------------------------------------------------------------- +# 1. Clean optionnel +# --------------------------------------------------------------- +if [[ $CLEAN -eq 1 ]]; then + echo -e "${YELLOW}[0/5] Nettoyage des anciens builds...${NC}" + rm -rf "$STAGING_DIR" + rm -rf "$BASE_BUILD_DIR" + rm -f "$RELEASES_DIR"/Lea-Setup-*.exe + echo " OK" + echo "" +fi + +mkdir -p "$RELEASES_DIR" + +# --------------------------------------------------------------- +# 2. Build du package de base (reutilise build_package.sh) +# --------------------------------------------------------------- +echo "[1/5] Build du package de base..." +if [[ ! -d "$BASE_BUILD_DIR" ]] || [[ $CLEAN -eq 1 ]]; then + bash "$DEPLOY_DIR/build_package.sh" >/dev/null +fi +if [[ ! -d "$BASE_BUILD_DIR" ]]; then + echo -e "${RED} Erreur : $BASE_BUILD_DIR n'a pas ete cree par build_package.sh${NC}" + exit 1 +fi +echo " Package de base pret : $BASE_BUILD_DIR" +echo "" + +# --------------------------------------------------------------- +# 3. Copie vers staging +# --------------------------------------------------------------- +echo "[2/5] Preparation du staging installeur..." +rm -rf "$STAGING_DIR" +mkdir -p "$STAGING_DIR" + +# Copie tout sauf config.txt (genere par l'installeur) et install.bat +# install.bat est conserve mais sera appele en mode silencieux par ISS +rsync -a \ + --exclude='__pycache__' \ + --exclude='*.pyc' \ + --exclude='.venv' \ + --exclude='sessions/' \ + --exclude='logs/' \ + "$BASE_BUILD_DIR/" \ + "$STAGING_DIR/" + +# On supprime le config.txt du staging : c'est l'installeur qui le generera +rm -f "$STAGING_DIR/config.txt" + +echo " Staging : $STAGING_DIR" +echo " Fichiers : $(find "$STAGING_DIR" -type f | wc -l)" +echo "" + +# --------------------------------------------------------------- +# 4. Copie des helpers installeur (uninstall, licence, etc.) +# --------------------------------------------------------------- +echo "[3/5] Copie des helpers installeur..." +cp "$SCRIPT_DIR/uninstall_lea.ps1" "$STAGING_DIR/" 2>/dev/null || true +cp "$SCRIPT_DIR/configure_embed.ps1" "$STAGING_DIR/" 2>/dev/null || true +cp "$SCRIPT_DIR/LICENSE.txt" "$STAGING_DIR/" 2>/dev/null || true +cp "$SCRIPT_DIR/config_template.txt" "$STAGING_DIR/config_template.txt" 2>/dev/null || true +echo " Helpers copies" +echo "" + +# --------------------------------------------------------------- +# 5. Python embedded (optionnel) +# --------------------------------------------------------------- +PYTHON_EMBED_SRC="${PYTHON_EMBED_DIR:-$SCRIPT_DIR/python-3.12-embed}" +if [[ -d "$PYTHON_EMBED_SRC" ]]; then + echo "[4/5] Copie de Python 3.12 embedded..." + rsync -a "$PYTHON_EMBED_SRC/" "$STAGING_DIR/python-3.12-embed/" + echo " Python embedded inclus" +else + echo -e "${YELLOW}[4/5] Python 3.12 embedded non trouve dans $PYTHON_EMBED_SRC${NC}" + echo " L'installeur sera produit SANS bundle Python." + echo " Pour bundler Python : voir README.md section 'Python embedded'" +fi +echo "" + +# --------------------------------------------------------------- +# 6. Stage-only : on s'arrete ici +# --------------------------------------------------------------- +if [[ $STAGE_ONLY -eq 1 ]]; then + echo -e "${GREEN} Staging pret. Utiliser ISCC pour compiler :${NC}" + echo " ISCC.exe \"$SCRIPT_DIR/Lea.iss\"" + echo "" + exit 0 +fi + +# --------------------------------------------------------------- +# 7. Compilation avec ISCC (si disponible) +# --------------------------------------------------------------- +echo "[5/5] Compilation Inno Setup..." + +# Chercher ISCC : natif Linux (rare), Wine, ou WSL +ISCC_BIN="" +if command -v iscc >/dev/null 2>&1; then + ISCC_BIN="iscc" +elif command -v ISCC.exe >/dev/null 2>&1; then + ISCC_BIN="ISCC.exe" +elif command -v wine >/dev/null 2>&1; then + # Chemins Wine courants + for path in \ + "$HOME/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe" \ + "$HOME/.wine/drive_c/Program Files/Inno Setup 6/ISCC.exe"; do + if [[ -f "$path" ]]; then + ISCC_BIN="wine \"$path\"" + break + fi + done +fi + +if [[ -z "$ISCC_BIN" ]]; then + echo "" + echo -e "${YELLOW} ISCC (Inno Setup Compiler) introuvable.${NC}" + echo "" + echo " Le staging est pret dans : $STAGING_DIR" + echo "" + echo " Pour compiler l'installeur, deux options :" + echo "" + echo " 1) Sur un PC Windows avec Inno Setup 6 installe :" + echo " - Copier le dossier deploy/ sur le PC" + echo " - Ouvrir deploy/installer/Lea.iss dans Inno Setup" + echo " - Cliquer 'Compile' (F9)" + echo " - Recuperer deploy/releases/Lea-Setup-v${VERSION}.exe" + echo "" + echo " 2) Sur Linux avec Wine :" + echo " - winetricks innosetup (ou installer le .exe manuellement)" + echo " - wine \"\$HOME/.wine/drive_c/Program Files (x86)/Inno Setup 6/ISCC.exe\" \\" + echo " \"$SCRIPT_DIR/Lea.iss\"" + echo "" + exit 0 +fi + +echo " ISCC trouve : $ISCC_BIN" +eval "$ISCC_BIN \"$SCRIPT_DIR/Lea.iss\"" + +# Verification du resultat +OUTPUT_EXE="$RELEASES_DIR/Lea-Setup-v${VERSION}.exe" +if [[ -f "$OUTPUT_EXE" ]]; then + EXE_SIZE=$(du -h "$OUTPUT_EXE" | cut -f1) + echo "" + echo -e "${GREEN}============================================================${NC}" + echo -e "${GREEN} Installeur produit !${NC}" + echo -e "${GREEN}============================================================${NC}" + echo "" + echo " Fichier : $OUTPUT_EXE" + echo " Taille : $EXE_SIZE" + echo "" + echo " Deploiement :" + echo " - Signer le .exe avec un certificat code-signing (voir README.md)" + echo " - Publier sur : https://lea.labs.laurinebazin.design/downloads/" + echo " - Installation silencieuse : Lea-Setup-v${VERSION}.exe /VERYSILENT /CONFIG=enroll.txt" + echo "" +else + echo -e "${RED} Erreur : $OUTPUT_EXE n'a pas ete produit${NC}" + exit 1 +fi diff --git a/deploy/installer/config_template.txt b/deploy/installer/config_template.txt new file mode 100644 index 000000000..f2081f249 --- /dev/null +++ b/deploy/installer/config_template.txt @@ -0,0 +1,27 @@ +# ============================================================ +# config_template.txt — Modele pour installation silencieuse +# ------------------------------------------------------------ +# +# Ce fichier est utilise en mode /VERYSILENT pour pre-remplir +# les valeurs d'enrollment sans interface graphique. +# +# Usage : +# Lea-Setup-v1.0.0.exe /VERYSILENT /CONFIG=enroll.txt +# +# L'installeur lit ce fichier au demarrage et remplit les pages +# custom (nom, email, ID, URL, token) automatiquement. +# +# Toutes les cles ci-dessous sont optionnelles. Si une cle est +# absente, la valeur par defaut de l'installeur est utilisee. +# +# Format : CLE=valeur, une ligne par parametre, # = commentaire. +# ============================================================ + +# Identite du collaborateur (obligatoires sauf USER_ID) +USER_NAME=Prenom Nom +USER_EMAIL=prenom.nom@aivanov.com +USER_ID= + +# Connexion serveur (valeurs par defaut deja pre-remplies) +SERVER_URL=https://lea.labs.laurinebazin.design/api/v1 +API_TOKEN=86031addb338e449fccdb1a983f61807aec15d42d482b9c7748ad607dc23caab diff --git a/deploy/installer/configure_embed.ps1 b/deploy/installer/configure_embed.ps1 new file mode 100644 index 000000000..28e85d13b --- /dev/null +++ b/deploy/installer/configure_embed.ps1 @@ -0,0 +1,112 @@ +# ============================================================ +# configure_embed.ps1 — Configure le runtime Python embedded +# ------------------------------------------------------------ +# +# Quand le composant 'pythonembed' est installe, on a : +# \python-embed\ <-- runtime Python 3.12 embedded +# +# Ce script : +# 1. Active l'import des packages (patch de python312._pth) +# 2. Installe pip dans le runtime embedded +# 3. Installe les dependances requirements_agent.txt +# 4. Reecrit Lea.bat pour pointer sur python-embed\pythonw.exe +# +# Doit etre execute avec le CWD = +# ============================================================ + +$ErrorActionPreference = 'Stop' + +$AppDir = Get-Location +$EmbedDir = Join-Path $AppDir "python-embed" +$PythonExe = Join-Path $EmbedDir "python.exe" + +if (-not (Test-Path $PythonExe)) { + Write-Host "Python embedded introuvable, abandon." + exit 1 +} + +Write-Host "Configuration de Python embedded..." + +# --------------------------------------------------------------- +# 1. Decommenter la ligne 'import site' dans python312._pth +# (necessaire pour que pip puisse fonctionner) +# --------------------------------------------------------------- +$PthFile = Get-ChildItem -Path $EmbedDir -Filter "python*._pth" | Select-Object -First 1 +if ($PthFile) { + $Content = Get-Content $PthFile.FullName + $NewContent = $Content -replace '^#import site', 'import site' + Set-Content -Path $PthFile.FullName -Value $NewContent + Write-Host " python._pth patche (import site active)" +} + +# --------------------------------------------------------------- +# 2. Installer pip (bootstrap via get-pip.py) +# --------------------------------------------------------------- +$GetPip = Join-Path $env:TEMP "get-pip.py" +Write-Host " Telechargement de get-pip.py..." +Invoke-WebRequest -Uri "https://bootstrap.pypa.io/get-pip.py" -OutFile $GetPip -UseBasicParsing + +Write-Host " Installation de pip..." +& $PythonExe $GetPip --no-warn-script-location +Remove-Item $GetPip -Force + +# --------------------------------------------------------------- +# 3. Installer les dependances +# --------------------------------------------------------------- +$Requirements = Join-Path $AppDir "requirements_agent.txt" +if (Test-Path $Requirements) { + Write-Host " Installation des dependances Python..." + & $PythonExe -m pip install --no-warn-script-location -r $Requirements +} + +# --------------------------------------------------------------- +# 4. Reecrire Lea.bat pour utiliser python-embed +# --------------------------------------------------------------- +$LeaBat = Join-Path $AppDir "Lea.bat" +$NewLeaBat = @" +@echo off +chcp 65001 >nul 2>&1 +title Lea - Assistante IA +cd /d "%~dp0" + +if exist "lea_agent.lock" ( + for /f "usebackq tokens=* delims=" %%i in ("lea_agent.lock") do ( + taskkill /F /PID %%i >nul 2>&1 + ) + del /f /q "lea_agent.lock" >nul 2>&1 + timeout /t 2 >nul +) + +if exist "config.txt" ( + for /f "usebackq eol=# tokens=1,* delims==" %%a in ("config.txt") do ( + if not "%%a"=="" if not "%%b"=="" set "%%a=%%b" + ) +) + +echo. +echo Demarrage de Lea (runtime embedded)... +echo. + +start "" /b "%~dp0python-embed\pythonw.exe" run_agent_v1.py + +timeout /t 3 >nul +set "LEA_ALIVE=0" +if exist "lea_agent.lock" ( + for /f "usebackq tokens=* delims=" %%i in ("lea_agent.lock") do ( + tasklist /FI "PID eq %%i" /NH 2>nul | findstr /I "pythonw" >nul && set "LEA_ALIVE=1" + ) +) +if "%LEA_ALIVE%"=="0" ( + echo. + echo Lea n'a pas demarre correctement. Affichage des erreurs : + echo. + "%~dp0python-embed\python.exe" run_agent_v1.py + pause +) +"@ + +Set-Content -Path $LeaBat -Value $NewLeaBat -Encoding ASCII +Write-Host " Lea.bat reecrit pour runtime embedded" + +Write-Host "Configuration terminee." +exit 0 diff --git a/deploy/installer/uninstall_lea.ps1 b/deploy/installer/uninstall_lea.ps1 new file mode 100644 index 000000000..0d83aa317 --- /dev/null +++ b/deploy/installer/uninstall_lea.ps1 @@ -0,0 +1,99 @@ +# ============================================================ +# uninstall_lea.ps1 — Script de desinstallation propre de Lea +# ------------------------------------------------------------ +# +# Appele par Inno Setup via [UninstallRun] AVANT la suppression +# des fichiers. Roles : +# +# 1. Tuer proprement le process Lea (via PID du lock) +# 2. Nettoyer shell:startup (supprimer le raccourci auto-start) +# 3. Notifier le serveur de la desinstallation (best-effort) +# 4. Supprimer le lock file +# +# Usage (par Inno Setup) : +# powershell.exe -NoProfile -ExecutionPolicy Bypass \ +# -File uninstall_lea.ps1 -AppDir "C:\Program Files\Lea" +# ============================================================ + +param( + [Parameter(Mandatory = $true)] + [string]$AppDir +) + +$ErrorActionPreference = 'SilentlyContinue' +Write-Host "Desinstallation de Lea en cours..." + +# --------------------------------------------------------------- +# 1. Tuer le process via PID du lock file +# --------------------------------------------------------------- +$LockFile = Join-Path $AppDir "lea_agent.lock" +if (Test-Path $LockFile) { + try { + $Pid = (Get-Content $LockFile -ErrorAction Stop | Select-Object -First 1).Trim() + if ($Pid -match '^\d+$') { + Write-Host " Arret du process Lea (PID $Pid)..." + Stop-Process -Id ([int]$Pid) -Force -ErrorAction SilentlyContinue + Start-Sleep -Seconds 1 + } + } catch { + Write-Host " Lock file illisible (ignore)." + } + Remove-Item $LockFile -Force -ErrorAction SilentlyContinue +} + +# --------------------------------------------------------------- +# 2. Nettoyer shell:startup (peut ne pas exister si composant +# non installe, on le supprime silencieusement dans tous les cas) +# --------------------------------------------------------------- +$StartupDir = [Environment]::GetFolderPath('Startup') +$StartupShortcut = Join-Path $StartupDir "Lea.lnk" +if (Test-Path $StartupShortcut) { + Write-Host " Suppression du raccourci auto-start..." + Remove-Item $StartupShortcut -Force -ErrorAction SilentlyContinue +} + +# --------------------------------------------------------------- +# 3. Notifier le serveur de la desinstallation (best-effort) +# --------------------------------------------------------------- +$ConfigFile = Join-Path $AppDir "config.txt" +$MachineIdFile = Join-Path $AppDir "machine_id.txt" +if ((Test-Path $ConfigFile) -and (Test-Path $MachineIdFile)) { + try { + $ConfigLines = Get-Content $ConfigFile + $ServerUrl = ($ConfigLines | Where-Object { $_ -match '^RPA_SERVER_URL=' } | Select-Object -First 1) -replace '^RPA_SERVER_URL=', '' + $Token = ($ConfigLines | Where-Object { $_ -match '^RPA_API_TOKEN=' } | Select-Object -First 1) -replace '^RPA_API_TOKEN=', '' + $MachineId = (Get-Content $MachineIdFile -ErrorAction Stop | Select-Object -First 1).Trim() + + if ($ServerUrl -and $Token -and $MachineId) { + Write-Host " Notification du serveur..." + $Body = @{ + machine_id = $MachineId + hostname = $env:COMPUTERNAME + event = 'uninstall' + timestamp = (Get-Date -Format "o") + } | ConvertTo-Json + + Invoke-RestMethod ` + -Uri "$ServerUrl/agents/uninstall" ` + -Method POST ` + -Body $Body ` + -ContentType 'application/json' ` + -Headers @{ Authorization = "Bearer $Token" } ` + -TimeoutSec 5 ` + -ErrorAction SilentlyContinue | Out-Null + } + } catch { + # Best-effort : on ignore toute erreur reseau/auth + Write-Host " Notification serveur echouee (ignore)." + } +} + +# --------------------------------------------------------------- +# 4. Supprimer les fichiers restants verrouilles eventuellement +# --------------------------------------------------------------- +Start-Sleep -Seconds 1 +Get-ChildItem -Path $AppDir -Filter "*.pyc" -Recurse -ErrorAction SilentlyContinue | + Remove-Item -Force -ErrorAction SilentlyContinue + +Write-Host "Desinstallation : pre-traitement termine." +exit 0