# MIDIA Scanner Agent - Instalador Windows # Ejecutar PowerShell como Administrador: # iwr https://bridge.momoda.es/download/scan-agent/install.ps1 -UseBasicParsing | iex # # Version: 3.12 (06/06/2026) - MG20260606 watchdog relanza agente si el proceso cae # MG26: iex-compatible - no param() block, no args $Store = '' $ScannerIp = '' $selectedModel = '' $ErrorActionPreference = "Stop" # Fix TLS 1.2 (Win7/8 PowerShell antiguo default a TLS 1.0 - nginx ya no acepta) [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 Write-Host "" Write-Host "+============================================+" -ForegroundColor Cyan Write-Host "| MIDIA SHOPS - Scanner Agent Installer |" -ForegroundColor Cyan Write-Host "| v3.12 - Watchdog autoarranque scanner |" -ForegroundColor Cyan Write-Host "+============================================+" -ForegroundColor Cyan Write-Host "" $installDir = "C:\Program Files\MIDIA\scan-agent" $configDir = "$env:APPDATA\midia-scan-agent" $configFile = "$configDir\config.json" $bridgeUrl = "https://bridge.momoda.es" $wsUrl = "wss://bridge.momoda.es" $agentToken = "midia-agent-2026-4ec22869cafd6719" # ------------------------------------------------------------------- # 1. Check Node.js # ------------------------------------------------------------------- try { $nodeVer = (node --version) 2>&1 Write-Host "[OK] Node.js encontrado: $nodeVer" -ForegroundColor Green } catch { Write-Host "[ERR] Node.js no instalado. Descargando MSI..." -ForegroundColor Yellow $nodeUrl = "https://nodejs.org/dist/v20.11.0/node-v20.11.0-x64.msi" Invoke-WebRequest -Uri $nodeUrl -OutFile "$env:TEMP\node.msi" -UseBasicParsing Write-Host " Instalando Node.js (puede tardar 1-2 min)..." -ForegroundColor Yellow Start-Process msiexec.exe -Wait -ArgumentList "/i `"$env:TEMP\node.msi`" /quiet /norestart" $env:Path = [System.Environment]::GetEnvironmentVariable("Path","Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path","User") Write-Host "" Write-Host "[OK] Node.js instalado." -ForegroundColor Green Write-Host "[!] CIERRA esta ventana de PowerShell y abre otra NUEVA (como Admin)" -ForegroundColor Yellow Write-Host " Luego vuelve a ejecutar este instalador." -ForegroundColor Yellow Write-Host "" Read-Host "Pulsa ENTER para cerrar" exit } # ------------------------------------------------------------------- # 2. Detectar config previa # ------------------------------------------------------------------- $existingTienda = $null if (Test-Path $configFile) { try { $existing = Get-Content $configFile -Raw | ConvertFrom-Json $existingTienda = $existing.tienda Write-Host "" Write-Host "[!] Ya existe configuracion previa:" -ForegroundColor Yellow Write-Host " Tienda: $existingTienda" -ForegroundColor Yellow Write-Host " Scanner IP: $($existing.scannerIp)" -ForegroundColor Yellow Write-Host "" $reconf = Read-Host "?Reconfigurar? (s/N)" if ($reconf -ne "s" -and $reconf -ne "S") { Write-Host "-> Conservando configuracion existente" -ForegroundColor Gray $skipConfig = $true } } catch { Write-Host "[!] Config corrupta, regenerando..." -ForegroundColor Yellow } } # ------------------------------------------------------------------- # 3. Crear directorios # ------------------------------------------------------------------- New-Item -ItemType Directory -Force -Path $installDir | Out-Null New-Item -ItemType Directory -Force -Path $configDir | Out-Null # ------------------------------------------------------------------- # 4. Descargar agente # ------------------------------------------------------------------- Write-Host "" Write-Host " Descargando agente..." -ForegroundColor Cyan Invoke-WebRequest -Uri "$bridgeUrl/download/scan-agent/agent.js" -OutFile "$installDir\agent.js" -UseBasicParsing Invoke-WebRequest -Uri "$bridgeUrl/download/scan-agent/package.json" -OutFile "$installDir\package.json" -UseBasicParsing Write-Host " [OK] agent.js + package.json descargados" -ForegroundColor Green # ------------------------------------------------------------------- # 5. npm install # ------------------------------------------------------------------- Write-Host "" Write-Host " Instalando dependencias npm..." -ForegroundColor Cyan Push-Location $installDir $npmOut = & npm install --omit=dev --silent 2>&1 Pop-Location Write-Host " [OK] Dependencias OK" -ForegroundColor Green # ------------------------------------------------------------------- # 6. Menu de tiendas (si no skipConfig) # ------------------------------------------------------------------- if (-not $skipConfig) { # Lista cacheada de tiendas MIDIA retail (actualizada 20/04/2026) $tiendas = @( "M. ALBORAYA", "M. ALFAFAR CC MN4", "M. ALICANTE", "M. ALMAZORA", "M. BENETUSSER", "M. BURJASSOT", "M. BURRIANA", "M. CANALS", "M. CARTAGENA", "M. CATARROJA", "M. CENTER XATIVA", "M. MADRID GENERAL RICARDOS", "M. MURCIA", "M. OLIVA", "M. TORRENTE", "M. VLC BRASIL", "M. VLC MANUEL CANDELA", "M. VLC SERRERIA", "M. ZARAGOZA LACARRA", "M. ZARAGOZA VILLAHERMOSA" ) Write-Host "" Write-Host "===========================================" -ForegroundColor Cyan Write-Host " SELECCIONA TIENDA" -ForegroundColor Cyan Write-Host "===========================================" -ForegroundColor Cyan for ($i = 0; $i -lt $tiendas.Count; $i++) { $num = ($i + 1).ToString().PadLeft(2) Write-Host " [$num] $($tiendas[$i])" } Write-Host "" # Si viene -Store desde CLI y es una tienda valida, saltar el menu if ($Store -ne '' -and ($tiendas -contains $Store)) { $tienda = $Store Write-Host "" Write-Host " [OK] Tienda pre-seleccionada desde el ERP: $tienda" -ForegroundColor Green } else { do { $sel = Read-Host "Numero de tienda (1-$($tiendas.Count))" $selNum = 0 $valid = [int]::TryParse($sel, [ref]$selNum) -and $selNum -ge 1 -and $selNum -le $tiendas.Count if (-not $valid) { Write-Host " [ERR] Numero invalido, prueba de nuevo" -ForegroundColor Red } } while (-not $valid) $tienda = $tiendas[$selNum - 1] Write-Host "" Write-Host " [OK] Tienda: $tienda" -ForegroundColor Green } # IP del scanner (opcional; puede venir del parametro) Write-Host "" Write-Host " Scanner Brother" -ForegroundColor Cyan Write-Host "" if ($ScannerIp -ne '') { $scannerIp = $ScannerIp Write-Host " [OK] IP pre-configurada: $scannerIp" -ForegroundColor Green } else { # === ESCANEO DE SUBRED via Node.js (rapido, ~5-10 segundos) === $foundScanners = @() try { Write-Host " Escaneando red local (5-10 segundos)..." -ForegroundColor Gray # Descargar discover.js si no esta $discoverPath = Join-Path $installDir "discover.js" try { Invoke-WebRequest -Uri "$bridgeUrl/download/scan-agent/discover.js" -OutFile $discoverPath -UseBasicParsing -TimeoutSec 15 } catch { Write-Host " [!] No se pudo descargar discover.js: $($_.Exception.Message)" -ForegroundColor Yellow } if (Test-Path $discoverPath) { # Ejecutar node discover.js -> JSON (con manejo robusto de stderr) $outFile = Join-Path $env:TEMP "midia-scan-out.json" $errFile = Join-Path $env:TEMP "midia-scan-err.log" try { $prev = $ErrorActionPreference $ErrorActionPreference = 'Continue' # Usar Start-Process para capturar stdout separado de stderr $proc = Start-Process -FilePath "node" -ArgumentList "`"$discoverPath`"" -NoNewWindow -Wait -RedirectStandardOutput $outFile -RedirectStandardError $errFile -PassThru $ErrorActionPreference = $prev if (Test-Path $outFile) { $scanJson = Get-Content $outFile -Raw -Encoding UTF8 if ($scanJson) { # Limpiar cualquier texto basura antes del [ $idx = $scanJson.IndexOf('[') if ($idx -ge 0) { $scanJson = $scanJson.Substring($idx) } try { $parsed = $scanJson | ConvertFrom-Json -ErrorAction SilentlyContinue if ($parsed) { if ($parsed -is [Array]) { foreach ($item in $parsed) { if ($item.ip) { $foundScanners += [PSCustomObject]@{ ip = $item.ip; model = $item.model; port = $item.port } } } } elseif ($parsed.ip) { $foundScanners += [PSCustomObject]@{ ip = $parsed.ip; model = $parsed.model; port = $parsed.port } } } } catch { Write-Host " [!] JSON invalido (se ignora)" -ForegroundColor Yellow } } } Remove-Item $outFile -Force -ErrorAction SilentlyContinue Remove-Item $errFile -Force -ErrorAction SilentlyContinue } catch { # Ignorar errores de PowerShell al ejecutar node $ErrorActionPreference = 'Stop' } } } catch { Write-Host " [!] Error escaneando: $($_.Exception.Message)" -ForegroundColor Yellow } Write-Host "" if ($foundScanners.Count -gt 0) { Write-Host " [OK] Escaneres encontrados en la red:" -ForegroundColor Green for ($i = 0; $i -lt $foundScanners.Count; $i++) { $num = ($i + 1).ToString().PadLeft(2) Write-Host " [$num] $($foundScanners[$i].ip) - $($foundScanners[$i].model)" -ForegroundColor White } Write-Host " [ 0] Introducir IP manualmente" -ForegroundColor Gray Write-Host "" do { $sel = Read-Host "Elige un escaner (1-$($foundScanners.Count)) o 0 para manual" $selNum = -1 $valid = [int]::TryParse($sel, [ref]$selNum) -and $selNum -ge 0 -and $selNum -le $foundScanners.Count if (-not $valid) { Write-Host " [ERR] Numero invalido" -ForegroundColor Red } } while (-not $valid) if ($selNum -eq 0) { $scannerIp = Read-Host "IP del escaner (ejemplo: 192.168.1.50)" if ([string]::IsNullOrWhiteSpace($scannerIp)) { $scannerIp = "" } } else { $scannerIp = $foundScanners[$selNum - 1].ip $selectedModel = $foundScanners[$selNum - 1].model Write-Host " [OK] IP seleccionada: $scannerIp" -ForegroundColor Green if ($selectedModel) { Write-Host " [OK] Modelo seleccionado: $selectedModel" -ForegroundColor Green } } } else { Write-Host " [!] No se encontraron escaneres eSCL en la red local." -ForegroundColor Yellow Write-Host " Asegurate de que el scanner Brother esta encendido y conectado por cable de red/WiFi." -ForegroundColor Yellow Write-Host "" Write-Host " Para encontrar la IP manualmente:" -ForegroundColor Gray Write-Host " Panel Brother -> Menu -> Red -> WLAN -> Estado TCP/IP -> Direccion IP" -ForegroundColor Gray Write-Host "" $scannerIp = Read-Host "IP del escaner (ejemplo: 192.168.1.50 - ENTER para saltar)" if ([string]::IsNullOrWhiteSpace($scannerIp)) { $scannerIp = "" } } } # Confirmar Write-Host "" Write-Host "===========================================" -ForegroundColor Cyan Write-Host " CONFIRMAR" -ForegroundColor Cyan Write-Host "===========================================" -ForegroundColor Cyan Write-Host " Tienda: $tienda" Write-Host " Scanner IP: $(if ($scannerIp -eq '') { '(auto-descubrir)' } else { $scannerIp })" Write-Host " Bridge: $wsUrl" Write-Host "" $conf = Read-Host "?Correcto? (S/n)" if ($conf -eq "n" -or $conf -eq "N") { Write-Host "Cancelado. Vuelve a ejecutar el instalador." -ForegroundColor Yellow exit } # Guardar config # Preserve existing naps2Device if config already exists (don't nuke prior setup) $existingNaps2 = "" if (Test-Path $configFile) { try { $prev = Get-Content $configFile -Raw | ConvertFrom-Json if ($prev.naps2Device) { $existingNaps2 = $prev.naps2Device } } catch {} } # Detect NAPS2 installation $naps2Paths = @( "$env:ProgramFiles\NAPS2\NAPS2.Console.exe", "${env:ProgramFiles(x86)}\NAPS2\NAPS2.Console.exe", "$env:LOCALAPPDATA\Programs\NAPS2\NAPS2.Console.exe" ) $naps2Exe = $null foreach ($p in $naps2Paths) { if (Test-Path $p) { $naps2Exe = $p; break } } if (-not $naps2Exe) { Write-Host " [!] NAPS2 NO detectado. Instalando..." -ForegroundColor Yellow try { $msiUrl = "https://github.com/cyanfish/naps2/releases/download/v7.5.2/naps2-7.5.2-win-x64.msi" $msiPath = "$env:TEMP\naps2.msi" Invoke-WebRequest $msiUrl -OutFile $msiPath -UseBasicParsing Start-Process msiexec.exe -ArgumentList "/i `"$msiPath`" /quiet /norestart" -Wait Start-Sleep 3 foreach ($p in $naps2Paths) { if (Test-Path $p) { $naps2Exe = $p; break } } if ($naps2Exe) { Write-Host " [OK] NAPS2 instalado: $naps2Exe" -ForegroundColor Green } } catch { Write-Host " [ERR] No se pudo instalar NAPS2: $_" -ForegroundColor Red } } else { Write-Host " [OK] NAPS2 ya instalado: $naps2Exe" -ForegroundColor Green } # Scanner name: el usuario ya eligio modelo en el menu de red -> usalo como naps2Device # Prioridad: modelo elegido [override] > preservado del config > auto-detect NAPS2 # v3.2: consulta NAPS2 PRIMERO - el nombre WIA real incluye hash tipo [94ddf80fde17] que eSCL no devuelve # MG30.52j v3.8 - Probar driver ESCL primero (Mike confirmo que en Xativa ESCL engaga ADF correctamente) # ESCL = Apple AirPrint Scan, mejor compatibilidad ADF que WIA en Brother L3560CDW $naps2Devices = @() $detectedDriver = "wia" # default fallback if ($naps2Exe) { # 1) Intentar ESCL primero try { $rawEscl = & $naps2Exe --listdevices --driver escl 2>&1 | Out-String $devicesEscl = $rawEscl -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -and $_ -notmatch "^NAPS2" -and $_ -notmatch "^Usage" -and $_ -notmatch "^Options" -and $_ -notmatch "^Copyright" -and $_ -notmatch "^\s*$" -and $_ -notmatch "^-" } if ($devicesEscl.Count -gt 0) { Write-Host " Dispositivos ESCL vistos por NAPS2:" -ForegroundColor Cyan foreach ($d in $devicesEscl) { Write-Host " - $d" -ForegroundColor Gray } $naps2Devices = $devicesEscl $detectedDriver = "escl" } } catch { Write-Host " [!] NAPS2 --listdevices --driver escl fallo: $_" -ForegroundColor Yellow } # 2) Si ESCL no devolvio nada, fallback a WIA if ($naps2Devices.Count -eq 0) { try { $rawWia = & $naps2Exe --listdevices --driver wia 2>&1 | Out-String $devicesWia = $rawWia -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -and $_ -notmatch "^NAPS2" -and $_ -notmatch "^Usage" -and $_ -notmatch "^Options" -and $_ -notmatch "^Copyright" -and $_ -notmatch "^\s*$" -and $_ -notmatch "^-" } if ($devicesWia.Count -gt 0) { Write-Host " Dispositivos WIA vistos por NAPS2 (fallback):" -ForegroundColor Yellow foreach ($d in $devicesWia) { Write-Host " - $d" -ForegroundColor Gray } $naps2Devices = $devicesWia $detectedDriver = "wia" } } catch { Write-Host " [!] NAPS2 --listdevices --driver wia fallo: $_" -ForegroundColor Yellow } } } # Prioridad: [1] match del modelo elegido contra lista NAPS2, recupera nombre con hash # [2] preservado del config previo [3] primer Brother NAPS2 [4] WIA COM fallback $naps2Device = $null if ($selectedModel -and $naps2Devices.Count -gt 0) { # MG30.52d v3.6 - Extraer nucleo del modelo + filtrar candidatos preferiendo NO-LAN # Brother eSCL via LAN tiene bug ADF (cae a flatbed). El device "series" (USB/local) si engaga ADF. $coreModel = ($selectedModel -replace 'Brother','' -replace 'series','' -replace '\[[^\]]*\]','').Trim() if ($coreModel.Length -ge 4) { $candidates = @($naps2Devices | Where-Object { $_ -match [regex]::Escape($coreModel) }) $nonLan = @($candidates | Where-Object { $_ -notmatch '\b(LAN|WiFi|Wireless|Network|Red)\b' }) $lanOnly = @($candidates | Where-Object { $_ -match '\b(LAN|WiFi|Wireless|Network|Red)\b' }) if ($candidates.Count -eq 0) { Write-Host " [!] Ningun device NAPS2 matchea '$coreModel'" -ForegroundColor Yellow } elseif ($candidates.Count -eq 1) { $naps2Device = $candidates[0] Write-Host " [OK] Unico match NAPS2: $naps2Device" -ForegroundColor Green } elseif ($nonLan.Count -eq 1) { $naps2Device = $nonLan[0] Write-Host " [OK] Match NAPS2 sin-LAN preferido: $naps2Device" -ForegroundColor Green if ($lanOnly.Count -gt 0) { Write-Host " (descartado por bug eSCL: $($lanOnly[0]))" -ForegroundColor Gray } } else { # 2+ candidatos sin LAN -> menu interactivo Write-Host "" -ForegroundColor Cyan Write-Host " Hay varios escaneres compatibles. Cual usar?" -ForegroundColor Cyan for ($i=0; $i -lt $nonLan.Count; $i++) { Write-Host (" [{0}] {1}" -f ($i+1), $nonLan[$i]) -ForegroundColor White } if ($lanOnly.Count -gt 0) { Write-Host " (LAN omitidos por bug ADF firmware Brother:)" -ForegroundColor DarkGray foreach ($l in $lanOnly) { Write-Host " - $l" -ForegroundColor DarkGray } } $pick = Read-Host " Numero (1-$($nonLan.Count)) [Enter=1]" if ([string]::IsNullOrWhiteSpace($pick)) { $pick = "1" } $pickIdx = 0 if ([int]::TryParse($pick, [ref]$pickIdx) -and $pickIdx -ge 1 -and $pickIdx -le $nonLan.Count) { $naps2Device = $nonLan[$pickIdx-1] } else { $naps2Device = $nonLan[0] } Write-Host " [OK] Elegido: $naps2Device" -ForegroundColor Green } } if (-not $naps2Device) { $naps2Device = $selectedModel Write-Host " [!] Sin match NAPS2, usando modelo tal cual: $naps2Device" -ForegroundColor Yellow } } elseif ($selectedModel) { $naps2Device = $selectedModel Write-Host " [OK] Usando modelo elegido: $naps2Device" -ForegroundColor Green } elseif ($existingNaps2) { # Validar el preservado contra la lista NAPS2 (puede estar obsoleto) if ($naps2Devices.Count -gt 0 -and ($naps2Devices | Where-Object { $_ -eq $existingNaps2.Trim() })) { $naps2Device = $existingNaps2 Write-Host " [OK] naps2Device preservado validado: $naps2Device" -ForegroundColor Green } else { Write-Host " [!] naps2Device preservado no coincide con NAPS2 (probablemente falta [hash]). Re-detectando..." -ForegroundColor Yellow } } if ($naps2Exe) { try { $raw = & $naps2Exe --driver wia --listdevices 2>&1 | Out-String $naps2Devices = $raw -split "`r?`n" | ForEach-Object { $_.Trim() } | Where-Object { $_ -and $_ -notmatch "^NAPS2" -and $_ -notmatch "^Usage" -and $_ -notmatch "^Options" -and $_ -notmatch "^Copyright" -and $_ -notmatch "^\s*$" -and $_ -notmatch "^-" } if ($naps2Devices.Count -gt 0) { Write-Host " Dispositivos WIA vistos por NAPS2:" -ForegroundColor Gray foreach ($d in $naps2Devices) { Write-Host " - $d" -ForegroundColor Gray } } else { Write-Host " [!] NAPS2 --listdevices devolvio lista vacia" -ForegroundColor Yellow } } catch { Write-Host " [!] NAPS2 --listdevices fallo: $_" -ForegroundColor Yellow } } # Ajuste fuzzy: si el nombre elegido no aparece exacto en NAPS2, busca un match parecido if ($naps2Device -and $naps2Devices.Count -gt 0) { $exists = $naps2Devices | Where-Object { $_ -eq $naps2Device.Trim() } | Select-Object -First 1 if (-not $exists) { $modelKey = ($naps2Device -replace 'Brother','' -replace 'series','').Trim() if ($modelKey.Length -ge 4) { $fuzzy = $naps2Devices | Where-Object { $_ -match [regex]::Escape($modelKey) } | Select-Object -First 1 if ($fuzzy) { Write-Host " Ajustado al nombre WIA real: $fuzzy" -ForegroundColor Cyan; $naps2Device = $fuzzy } } } } # Si no hay valor preservado valido, usar la lista de NAPS2 (preferir Brother) if (-not $naps2Device -and $naps2Devices.Count -gt 0) { $brother = $naps2Devices | Where-Object { $_ -like "*Brother*" } | Select-Object -First 1 if ($brother) { $naps2Device = $brother Write-Host " Brother auto-detectado (NAPS2): $naps2Device" -ForegroundColor Green } else { $naps2Device = $naps2Devices[0] Write-Host " Primer dispositivo NAPS2: $naps2Device" -ForegroundColor Yellow } } # Fallback: WIA COM si NAPS2 no esta o no listo nada if (-not $naps2Device) { try { $dm = New-Object -ComObject WIA.DeviceManager $brothers = @() foreach ($di in $dm.DeviceInfos) { $n = $di.Properties("Name").Value if ($n -like "*Brother*") { $brothers += $n } } if ($brothers.Count -gt 0) { $naps2Device = ($brothers | Sort-Object Length | Select-Object -First 1) Write-Host " Fallback WIA COM: $naps2Device" -ForegroundColor Yellow } } catch { Write-Host " [!] No se pudo auto-detectar scanner WIA: $_" -ForegroundColor Yellow } } $config = @{ bridgeUrl = $wsUrl agentToken = $agentToken tienda = $tienda scannerIp = $scannerIp scannerPort = 80 naps2Device = $naps2Device naps2Driver = $detectedDriver # MG30.52j: detectado dinamicamente, prefiere ESCL } [System.IO.File]::WriteAllText($configFile, ($config | ConvertTo-Json), [System.Text.UTF8Encoding]::new($false)) Write-Host " [OK] Config guardada: $configFile" -ForegroundColor Green if ($naps2Device -and $naps2Exe) { Write-Host " Modo NAPS2 $detectedDriver activo para ADF" -ForegroundColor Cyan } # MG30.52h v3.7 - Auto-configurar perfil NAPS2 "MIDIA" con AutoSave al watchDir # (zero-touch para que el modo "GUI visual" funcione sin que el usuario configure nada) if ($naps2Device -and $naps2Exe) { try { # MG30.52k v3.9 - Escribir perfil MIDIA en TODOS los user profiles reales # (en Benetusser la deteccion de user interactivo fallo y el perfil quedo en otro user) # MG30.52m v3.11 - Matar NAPS2 si esta abierto (sino al cerrar pisa el profiles.xml nuevo con su cache memoria) try { $naps2Procs = Get-Process -Name "NAPS2" -ErrorAction SilentlyContinue if ($naps2Procs) { Write-Host " NAPS2 abierto -> cerrandolo para reset profiles.xml" -ForegroundColor Yellow $naps2Procs | ForEach-Object { try { $_.Kill() } catch {} } Start-Sleep -Seconds 2 } } catch {} $userDirs = @() try { $userDirs = @(Get-ChildItem 'C:\Users' -Directory -ErrorAction Stop | Where-Object { $_.Name -notin @('Public','Default','Default User','All Users','WDAGUtilityAccount','DefaultAppPool') }) } catch { Write-Host " [!] No se pudo enumerar C:\Users - $($_.Exception.Message)" -ForegroundColor Yellow } if ($userDirs.Count -eq 0) { # Fallback: usar $env:USERPROFILE del proceso actual $userDirs = @([PSCustomObject]@{ FullName = $env:USERPROFILE; Name = $env:USERNAME }) Write-Host " [!] Fallback: usando solo $env:USERNAME" -ForegroundColor Yellow } else { Write-Host " [OK] $($userDirs.Count) user profile(s) detectado(s) en C:\Users" -ForegroundColor Gray } $writtenCount = 0 foreach ($userDir in $userDirs) { $userName = $userDir.Name $userAppData = Join-Path $userDir.FullName "AppData\Roaming" if (-not (Test-Path $userAppData)) { continue } # User sin AppData (nunca logged in?) $naps2ProfilesDir = Join-Path $userAppData "NAPS2" $watchDir = Join-Path $userAppData "midia-scan-agent\watch" try { if (-not (Test-Path $watchDir)) { New-Item -ItemType Directory -Path $watchDir -Force | Out-Null } if (-not (Test-Path $naps2ProfilesDir)) { New-Item -ItemType Directory -Path $naps2ProfilesDir -Force | Out-Null } $profilesPath = Join-Path $naps2ProfilesDir "profiles.xml" # MG30.52m v3.11 - Limpiar estado previo: borrar profiles.xml + backups acumulados # (PCs tienda no tienen perfiles personales, solo MIDIA; mantener acumulado de .bak.midia.* mancha) if (Test-Path $profilesPath) { # 1 backup unico (sobrescribe si existe) por seguridad Copy-Item $profilesPath "$profilesPath.bak" -Force -ErrorAction SilentlyContinue Remove-Item $profilesPath -Force -ErrorAction SilentlyContinue } # Limpiar backups viejos de v3.7-v3.10 (.bak.midia.) Get-ChildItem -Path $naps2ProfilesDir -Filter "profiles.xml.bak.midia.*" -ErrorAction SilentlyContinue | Remove-Item -Force -ErrorAction SilentlyContinue # Limpiar tambien recent-images cache de NAPS2 (puede tener thumbnails de scans previos) $recentDir = Join-Path $naps2ProfilesDir "recovery" if (Test-Path $recentDir) { Remove-Item -Path "$recentDir\\*" -Recurse -Force -ErrorAction SilentlyContinue } $autoSavePath = Join-Path $watchDir "midia-`$(YYYY)`$(MM)`$(DD)_`$(hh)`$(mm)`$(ss).pdf" $xmlEsc = [System.Security.SecurityElement]::Escape($naps2Device) $autoSaveEsc = [System.Security.SecurityElement]::Escape($autoSavePath) $profilesXml = @" 2 MIDIA $detectedDriver $xmlEsc $detectedDriver Feeder Dpi300 C24Bit A4 Right 0 0 true $autoSaveEsc false true FilePerScan true false false "@ [System.IO.File]::WriteAllText($profilesPath, $profilesXml, [System.Text.UTF8Encoding]::new($false)) try { $acl = Get-Acl $profilesPath $rule = New-Object System.Security.AccessControl.FileSystemAccessRule("$userName","FullControl","Allow") $acl.SetAccessRule($rule) Set-Acl -Path $profilesPath -AclObject $acl -ErrorAction SilentlyContinue } catch {} $writtenCount++ Write-Host " [OK] Perfil MIDIA en user '$userName' (driver=$detectedDriver)" -ForegroundColor Green Write-Host " $profilesPath" -ForegroundColor Gray } catch { Write-Host " [!] Fallo escribir perfil para user '$userName': $($_.Exception.Message)" -ForegroundColor Yellow } } if ($writtenCount -eq 0) { Write-Host " [!] No se escribio perfil MIDIA en ningun user. Configura Auto-Save en NAPS2 GUI manualmente." -ForegroundColor Red } else { Write-Host " $writtenCount perfil(es) MIDIA creado(s) - escaneo NAPS2 GUI listo en cualquier user" -ForegroundColor Cyan } } catch { Write-Host " [!] No se pudo auto-crear perfil NAPS2 MIDIA: $($_.Exception.Message)" -ForegroundColor Yellow Write-Host " Configura manualmente Auto-Save en NAPS2 GUI -> Perfiles -> Editar -> Guardar" -ForegroundColor Gray } } } # ------------------------------------------------------------------- # 7. Registrar tarea programada (auto-arranque al login) # ------------------------------------------------------------------- Write-Host "" Write-Host " Registrando auto-arranque (Scheduled Task)..." -ForegroundColor Cyan # Detener tarea previa si existe try { Stop-ScheduledTask -TaskName "MidiaScanAgent" -ErrorAction SilentlyContinue Unregister-ScheduledTask -TaskName "MidiaScanAgent" -Confirm:$false -ErrorAction SilentlyContinue } catch {} # Resolver ruta completa de node.exe (la tarea programada no siempre tiene PATH) $nodePath = (Get-Command node -ErrorAction SilentlyContinue).Source if (-not $nodePath) { $candidates = @( "$env:ProgramFiles\nodejs\node.exe", "$env:ProgramFiles(x86)\nodejs\node.exe", "$env:LOCALAPPDATA\Programs\nodejs\node.exe" ) foreach ($c in $candidates) { if (Test-Path $c) { $nodePath = $c; break } } } if (-not $nodePath) { Write-Host " [!] No se encontro node.exe, usando 'node' (puede fallar en scheduled task)" -ForegroundColor Yellow $nodePath = "node" } Write-Host " node.exe en: $nodePath" -ForegroundColor Gray # MG30.47 - Wrapper VBS para arrancar oculto (sin ventana cmd visible) # Razon: SYSTEM no veia drivers escaner ni red local. Usar BUILTIN\Users + AtLogOn + VBS hidden. # MG20260606 - VBS watchdog: si agent.js ya esta vivo, no duplica; si falta, lo relanza. # Esto evita tener que reinstalar cuando el proceso cae o tras una OTA que deja la tarea finalizada. $vbsContent = @" Set objShell = CreateObject("WScript.Shell") objShell.CurrentDirectory = "$installDir" Q = Chr(34) target = LCase("$installDir\agent.js") On Error Resume Next Set svc = GetObject("winmgmts:\\.\root\cimv2") If Err.Number = 0 Then Set procs = svc.ExecQuery("SELECT ProcessId, CommandLine FROM Win32_Process WHERE Name='node.exe'") If Err.Number = 0 Then For Each p In procs cmd = LCase("" & p.CommandLine) If InStr(cmd, target) > 0 Then WScript.Quit 0 Next End If End If Err.Clear On Error GoTo 0 objShell.Run Q & "$nodePath" & Q & " " & Q & "$installDir\agent.js" & Q, 0, False "@ $vbsPath = Join-Path $installDir 'run-hidden.vbs' # Borrar version vieja (v3.4 corrupto) antes de escribir if (Test-Path $vbsPath) { Remove-Item $vbsPath -Force -ErrorAction SilentlyContinue } [System.IO.File]::WriteAllText($vbsPath, $vbsContent.Replace("`n","`r`n"), [System.Text.Encoding]::ASCII) # Action: wscript.exe + .vbs en vez de node.exe directo (evita ventana cmd) $action = New-ScheduledTaskAction -Execute 'wscript.exe' -Argument "`"$vbsPath`"" -WorkingDirectory $installDir # Trigger: AtLogOn + watchdog cada 5 min. AtStartup como SYSTEM falla por drivers escaner. $triggerLogin = New-ScheduledTaskTrigger -AtLogOn $triggerWatch = New-ScheduledTaskTrigger -Once -At (Get-Date).AddMinutes(1) -RepetitionInterval (New-TimeSpan -Minutes 5) -RepetitionDuration (New-TimeSpan -Days 3650) $trigger = @($triggerLogin, $triggerWatch) # Principal: BUILTIN\Users (S-1-5-32-545) con RunLevel Highest. Compat ES/EN. $principal = New-ScheduledTaskPrincipal -GroupId 'S-1-5-32-545' -RunLevel Highest # Settings: la tarea es un watchdog corto; el proceso node queda detached. $settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable -RestartCount 99 -RestartInterval (New-TimeSpan -Minutes 1) -ExecutionTimeLimit (New-TimeSpan -Days 365) -Hidden $task = New-ScheduledTask -Action $action -Trigger $trigger -Settings $settings -Principal $principal Register-ScheduledTask -TaskName "MidiaScanAgent" -InputObject $task -Force | Out-Null Write-Host " [OK] Tarea programada registrada" -ForegroundColor Green # ------------------------------------------------------------------- # 8. Arrancar agente INMEDIATAMENTE (detached) + verificar conectividad # ------------------------------------------------------------------- Write-Host "" Write-Host " Arrancando agente..." -ForegroundColor Cyan # Parar cualquier instancia previa Get-Process -Name "node" -ErrorAction SilentlyContinue | Where-Object { try { $_.CommandLine -like "*scan-agent*agent.js*" } catch { $false } } | ForEach-Object { try { Stop-Process -Id $_.Id -Force -ErrorAction SilentlyContinue } catch {} } # Lanzar node agent.js como proceso en background, logs a archivo $logFile = Join-Path $installDir "agent.log" $agentArgs = @("`"$installDir\agent.js`"") try { $proc = Start-Process -FilePath $nodePath -ArgumentList $agentArgs -WorkingDirectory $installDir -WindowStyle Hidden -PassThru -RedirectStandardOutput $logFile -RedirectStandardError "$installDir\agent.err.log" Write-Host " [OK] Agente iniciado (PID $($proc.Id))" -ForegroundColor Green } catch { Write-Host " [!] No se pudo arrancar: $($_.Exception.Message)" -ForegroundColor Yellow } # Arrancar tambien la scheduled task (no pasa nada si falla, el proceso ya esta corriendo) try { Start-ScheduledTask -TaskName "MidiaScanAgent" -ErrorAction SilentlyContinue } catch {} # Dar unos segundos para que conecte Write-Host " Esperando que el agente conecte al bridge..." -ForegroundColor Gray Start-Sleep -Seconds 8 # Verificar que hay un node.exe corriendo con agent.js $running = $false try { $running = (Get-Process -Name "node" -ErrorAction SilentlyContinue | Measure-Object).Count -gt 0 } catch {} if ($running) { Write-Host " [OK] Agente corriendo" -ForegroundColor Green Write-Host " Log: $logFile" -ForegroundColor Gray } else { Write-Host " [!] El agente no parece estar corriendo" -ForegroundColor Yellow Write-Host " Revisa el log en: $logFile" -ForegroundColor Yellow } Write-Host "" Write-Host "+============================================+" -ForegroundColor Green Write-Host "| [OK] INSTALACION COMPLETADA |" -ForegroundColor Green Write-Host "+============================================+" -ForegroundColor Green Write-Host "" Write-Host " Config: $configFile" Write-Host " Logs: $configDir\log.txt" Write-Host "" Write-Host " Comandos utiles:" -ForegroundColor Gray Write-Host " Get-Content `"$configDir\log.txt`" -Tail 30 -Wait -> ver logs en vivo" Write-Host " Stop-ScheduledTask -TaskName MidiaScanAgent -> parar" Write-Host " Start-ScheduledTask -TaskName MidiaScanAgent -> arrancar" Write-Host " Unregister-ScheduledTask -TaskName MidiaScanAgent -Confirm:`$false -> desinstalar" Write-Host "" Write-Host " Para verificar conexion al ERP, abre:" Write-Host " https://gestion.midiashops.com/midia.html -> Admin -> Agentes Scanner" -ForegroundColor Cyan Write-Host ""