[CmdletBinding()] param ( [string]$InstallPath ) $DistroInstallLocation = Join-Path $env:LOCALAPPDATA "NekroAgent" if (-not [string]::IsNullOrWhiteSpace($InstallPath)) { $DistroInstallLocation = $InstallPath } if (-NOT ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)) { Write-Host "此脚本需要管理员权限。" Write-Host "正在尝试以管理员身份重新运行..." try { $scriptPath = $MyInvocation.PSCommandPath if ([string]::IsNullOrEmpty($scriptPath)) { $scriptPath = $MyInvocation.MyCommand.Definition } if ([string]::IsNullOrEmpty($scriptPath)) { Write-Host "无法确定脚本路径以进行提权。请以管理员身份手动运行此脚本。" Read-Host "按 Enter 键退出..." Exit 1 } Start-Process PowerShell -Verb RunAs -ArgumentList "-NoProfile -ExecutionPolicy Bypass -File `"$scriptPath`" `"$InstallPath`"" -ErrorAction Stop Exit } catch { Write-Error "$($_.Exception.Message)" Read-Host "按 Enter 键退出..." Exit 1 } } Write-Host "NekroAgent WSL 安装脚本" Write-Host "脚本以管理员权限运行" $Global:RebootNeeded = $false $Global:FeatureState = 0 $Global:Arch = $null $Global:ArchString = $null $DebianLink = "https://aka.ms/wsl-debian-gnulinux" $DistroName = "nekro-agent" $WorkDir = (Join-Path $env:TEMP ("NekroAgent_" + (Get-Date -Format "yyyyMMddHH") + (Get-Random))) $DistroFilePath = Join-Path $WorkDir "distro.zip" $AppxFilePath = Join-Path $WorkDir "appx.zip" $TarballFilePath = Join-Path $WorkDir "install.tar.gz" # Distro 内部执行操作的状态 $InstallStatus = [PSCustomObject]@{ CreatedUser = $false UpdateWSLConf = $false ReplaceSource = $false DepsInstalled = $false NAInstalled = $false } # 注册脚本退出清理操作 $action = { if (Test-Path -Path $WorkDir -PathType Container) { Write-Host "正在清理临时目录..." Remove-Item $WorkDir -Recurse -Force -ErrorAction SilentlyContinue } } Register-EngineEvent -SourceIdentifier PowerShell.Exiting -Action $action -SupportEvent # 确认退出 function Confirm-ExitWithEnter { param ( [Parameter()] [int]$ExitCode = 0 ) Write-Host "" Write-Host "按 Enter 键退出脚本..." -NoNewline -ForegroundColor Cyan Read-Host Exit $ExitCode } # 下载 Distro 文件 function Download-WslDistroZip { [CmdletBinding(SupportsShouldProcess=$true)] param ( [Parameter(Mandatory=$true, Position=0)] [string]$AkaMsLink, [Parameter(Mandatory=$true, Position=1)] [string]$OutputFilePath ) try { if ($PSCmdlet.ShouldProcess($AkaMsLink, "下载文件到 '$OutputFilePath'")) { Write-Host "开始下载 Linux 发行版文件..." Invoke-WebRequest -Uri $AkaMsLink -OutFile $OutputFilePath -ErrorAction Stop } else { Write-Warning "下载操作已被用户通过 ShouldProcess 中止。" return $false } } catch { Write-Error "下载过程中发生错误: $($_.Exception.Message)" if ($_.Exception.StackTrace) { Write-Verbose "堆栈跟踪: $($_.Exception.StackTrace)" } return $null } return $true } # 从存档提取文件 function Extract-FileFromArchive { [CmdletBinding()] param ( [Parameter(Mandatory=$true, Position=0)] [string]$ArchivePath, [Parameter(Mandatory=$true, Position=1)] [string]$EntryPathRegexPattern, [Parameter(Mandatory=$true, Position=2)] [string]$DestinationFilePath ) if (-not (Test-Path -Path $ArchivePath -PathType Leaf)) { Write-Host "'$ArchivePath' 未找到或不是一个文件。" return $false } try { Add-Type -AssemblyName System.IO.Compression.FileSystem -ErrorAction Stop } catch {} $archive = $null $fileExtracted = $false try { $archive = [System.IO.Compression.ZipFile]::OpenRead($ArchivePath) $matchingEntries = $archive.Entries | Where-Object { $_.FullName -match $EntryPathRegexPattern } if ($matchingEntries.Count -eq 1) { [System.IO.Compression.ZipFileExtensions]::ExtractToFile($matchingEntries[0], $DestinationFilePath, $true) Write-Host "文件 '$($matchingEntries.FullName)' 已成功提取" $fileExtracted = $true } else { Write-Error "在文件 '$ArchivePath' 中未找到或找到多个匹配文件。" } } catch { Write-Error "$($_.Exception.Message)" } finally { if ($null -ne $archive) { $archive.Dispose() } } return $fileExtracted } # 启用 Windows 功能并检查是否需要重启 function Ensure-WindowsFeatureEnabled { param ( [Parameter(Mandatory=$true)] [string]$FeatureName, [Parameter(Mandatory=$true)] [string]$FeatureDisplayName ) Write-Host "正在安装:'$FeatureDisplayName'" try { $result = Enable-WindowsOptionalFeature -Online -FeatureName $FeatureName -NoRestart -ErrorAction Stop -WarningAction SilentlyContinue $info = Get-WindowsOptionalFeature -Online -FeatureName $FeatureName -ErrorAction Stop if (($info.State -ne "Enabled") -and ($info.State -ne "EnablePending")) { Write-Warning "安装失败:'$FeatureDisplayName'" return } $Global:FeatureState += 1 if ($result.RestartNeeded) { Write-Warning "启用 '$FeatureDisplayName' 后需要重启计算机。" $Global:RebootNeeded = $Global:RebootNeeded -or $result.RestartNeeded } } catch { Write-Error "安装 '$FeatureDisplayName' 失败: $($_.Exception.Message)" Confirm-ExitWithEnter 1 } } # 在 WSL 执行代码 function Invoke-WslCommand { [CmdletBinding()] param( [Parameter(Mandatory=$true)] [string]$DistributionName, [Parameter(Mandatory=$true)] [string]$Command, [Parameter()] [string]$Username = "root" ) $wslArgs = @( "-d", $DistributionName, "-u", $Username, "-e", "bash", "-c", $Command ) try { $console = [console]::OutputEncoding [console]::OutputEncoding = New-Object System.Text.UTF8Encoding wsl.exe @wslArgs } catch {} finally { if ($null -ne $console) { [console]::OutputEncoding = $console } } if ($LASTEXITCODE -ne 0) { Write-Host "命令 '$Command' 执行失败 (退出代码: $LASTEXITCODE)" -ForegroundColor Red } } # 安装 WSL2 function Install-Wsl { try { Write-Host "正在尝试使用 'wsl.exe' 安装 WSL 功能..." wsl.exe --install --no-distribution if ($LASTEXITCODE -ne 0) { Throw } $Global:RebootNeeded = $true } catch { Write-Warning "尝试使用 `wsl.exe` 安装失败,尝试手动启用组件..." Ensure-WindowsFeatureEnabled -FeatureName "Microsoft-Windows-Subsystem-Linux" -FeatureDisplayName "适用于 Linux 的 Windows 子系统" Ensure-WindowsFeatureEnabled -FeatureName "VirtualMachinePlatform" -FeatureDisplayName "虚拟机平台" if ($Global:FeatureState -lt 2) { Write-Warning "功能启用失败或部分失败,请尝试重新运行" Confirm-ExitWithEnter 1 } } } # 检测 WSL2 可用性 function Test-WslAvailability { param( [string]$DistroName ) $status = [PSCustomObject]@{ Enabled = $true Version = $null DistroInstalled = $false } $console = ([console]::OutputEncoding) [console]::OutputEncoding = New-Object System.Text.UnicodeEncoding try { $installedDistributions = wsl.exe --list --quiet --all } catch {} if ($LASTEXITCODE -ne 0) { $status.Enabled = $false } if ($installedDistributions -contains $DistroName) { $status.DistroInstalled = $true } try { $version = (wsl.exe --status)[-1][-1] } catch {} if ($LASTEXITCODE -eq 0) { $status.Version = $version } [console]::OutputEncoding = $console return $status } # 用于选择文件夹 function Select-FolderDialog { $fixedDescription = "请选择一个文件夹用于存储 WSL 虚拟磁盘文件" try { Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop $FolderBrowser = New-Object System.Windows.Forms.FolderBrowserDialog $FolderBrowser.Description = $fixedDescription $FolderBrowser.ShowNewFolderButton = $true $DialogResult = $null if ($Host.Name -eq 'ConsoleHost' -or $Host.Name -eq 'Windows PowerShell ISE') { $DialogResult = $FolderBrowser.ShowDialog((New-Object System.Windows.Forms.NativeWindow)) } else { $DialogResult = $FolderBrowser.ShowDialog() } if ($DialogResult -eq [System.Windows.Forms.DialogResult]::OK) { return $FolderBrowser.SelectedPath } else { return $null } } catch { Write-Error "无法显示文件夹选择对话框:$($_.Exception.Message)" return $null } finally { if ($FolderBrowser -ne $null) { $FolderBrowser.Dispose() } } } # 检测环境可用性 Write-Host "" Write-Host "正在检测环境..." $WSLStatus = Test-WslAvailability $DistroName try { $processorInfo = Get-CimInstance -Class Win32_Processor -ErrorAction Stop | Select-Object -First 1 $processorArchitectureId = $processorInfo.Architecture $Global:Arch = $processorArchitectureId switch ($processorArchitectureId) { 0 { $Global:ArchString = "x86" } 5 { $Global:ArchString = "ARM" } 6 { $Global:ArchString = "ia64" } 9 { $Global:ArchString = "x64" } 12 { $Global:ArchString = "ARM64" } default {} } } catch { Write-Warning "无法获取架构信息: $($_.Exception.Message)" } Write-Host "处理器架构: $Global:ArchString" if (($Global:Arch -ne 9) -and ($Global:Arch -ne 12)) { Write-Warning "设备架构可能不支持,ID:$Global:Arch" } if ($null -eq $Global:ArchString) { Write-Error "未知架构,暂不支持..." Confirm-ExitWithEnter 1 } $Global:HypervisorPresent = Get-CimInstance -ClassName Win32_ComputerSystem | Select-Object -ExpandProperty HypervisorPresent $Global:VirtualizationFirmwareEnabled = Get-CimInstance -ClassName Win32_Processor | Select-Object -ExpandProperty VirtualizationFirmwareEnabled if ($Global:HypervisorPresent -or $Global:VirtualizationFirmwareEnabled) { Write-Host "虚拟化状态:已启用" } else { Write-Host "虚拟化状态:未启用或不支持" Write-Warning "可能会导致后续步骤失败(未验证)" } if ($WSLStatus.Enabled) { Write-Host "WSL 可用性:已启用" } else { Write-Host "WSL 可用性:未启用" } if (-not $WSLStatus.DistroInstalled) { Write-Host "`'$DistroName`':未安装" } else { Write-Host "`'$DistroName`':已安装" Confirm-ExitWithEnter } Write-Host ("-"*40) Write-Warning "NekroAgent 将安装在 '$DistroInstallLocation'" Write-Host "虚拟磁盘文件占用较大,建议选择非系统盘" $userConfirmation = Read-Host -Prompt "取消可选择指定安装路径,是否继续安装? (Y/n)" if (($userConfirmation -eq "") -or ($userConfirmation.ToLower() -ne 'y')) { $DistroInstallLocation = Select-FolderDialog if ($null -ne $DistroInstallLocation) { Write-Warning "NekroAgent 将安装在 '$DistroInstallLocation'" } else { Write-Host "未选择任何路径,已取消" Confirm-ExitWithEnter } } # 确保功能启用 if (-not $WSLStatus.Enabled) { Install-Wsl if ($Global:RebootNeeded) { Write-Host "--------------------------------------------------------------------" Write-Host "WSL 安装成功" Write-Host "Windows 功能更改需要重启计算机才能生效。" Write-Host "请保存所有工作并重启您的计算机。" -ForegroundColor RED Write-Host "重启后,请再次以管理员身份运行此脚本以完成 WSL2 的配置。" Write-Host "--------------------------------------------------------------------" Confirm-ExitWithEnter } } # 更新 WSL Write-Host "正在更新 WSL (此过程可能需要几分钟)..." wsl.exe --update if ($LASTEXITCODE -eq 0) { Write-Host "WSL 内核已更新 (或已是最新)。" } else { Write-Host "WSL 内核更新失败。错误详情: $output" -ForegroundColor RED Write-Host "可能原因: 网络连接问题,或 WSL 组件需要修复。" -ForegroundColor RED # 考虑脚本处理 Write-Warning "建议: 尝试手动从以下链接下载并安装 WSL2 Linux 内核更新包:" Write-Warning "https://wslstorestorage.blob.core.windows.net/wslblob/wsl_update_x64.msi" Write-Warning "安装后再尝试手动运行 'wsl --update' 命令。" } # 设置 WSL 默认版本为 2 if ($WSLStatus.Version -ne "2") { Write-Host "正在设置 WSL 默认版本为 2..." wsl.exe --set-default-version 2 if ($LASTEXITCODE -eq 0) { Write-Host "WSL 默认版本已成功设置为 2。" } else { Write-Host "设置 WSL 默认版本为 2 失败。错误输出: $output" -ForegroundColor RED Write-Host "NekroAgent 的使用依赖于 WSL2,可能导致后续 Docker 功能无法使用" -ForegroundColor RED Write-Host "请尝试执行 'wsl.exe --set-default-version 2' 以手动设置" -ForegroundColor RED Write-Host "或使用 'wsl --set-version $DistroName 2' 单独为 NekroAgent 设置" } } if (-not (Test-Path -Path $WorkDir -PathType Container)) { try { New-Item -ItemType Directory -Path $WorkDir -Force -ErrorAction Stop | Out-Null } catch { Write-Error "无法创建工作目录 '$WorkDir'。错误信息: $($_.Exception.Message)" return $null } } Write-Host ("-"*40) # 获取 tarball 文件 try { $result = Download-WslDistroZip -AkaMsLink $DebianLink -OutputFilePath $DistroFilePath if (-not $result) { Throw "文件下载失败:'$DebianLink'" } $result = Extract-FileFromArchive -ArchivePath "$DistroFilePath" -EntryPathRegexPattern ".*_${Global:ArchString}.appx" -DestinationFilePath "$AppxFilePath" if (-not $result) { Throw "文件提取失败" } $result = Extract-FileFromArchive -ArchivePath "$AppxFilePath" -EntryPathRegexPattern "install.tar.gz" -DestinationFilePath "$TarballFilePath" if (-not $result) { Throw "文件提取失败:'install.tar.gz'" } } catch { Write-Host $($_.Exception.Message) -ForegroundColor Red Confirm-ExitWithEnter 1 } # 导入 WSL wsl.exe --import $DistroName $DistroInstallLocation $TarballFilePath 2>&1 if ($LASTEXITCODE -eq 0) { Write-Host "WSL 发行版 '$DistroName' 导入成功。" -ForegroundColor Green } else { Confirm-ExitWithEnter 1 } Write-Host ("-"*40) # 定制 $newUserName = "nekro" $newUserPassword = '$1$gslhAKeE$ymfRbdU08IdKMRobb3hpy0' $createUserCommand = "useradd -m -s /bin/bash $newUserName -p '$newUserPassword' -G sudo" Write-Host "正在配置 '$DistroName',部分操作涉及网络,请耐心等待..." Write-Host "正在创建 '$newUserName' 用户..." Invoke-WslCommand -DistributionName $DistroName -Command $createUserCommand if ($LASTEXITCODE -eq 0) { Write-Host "创建成功" -ForegroundColor Green $InstallStatus.CreatedUser = $true } else { Write-Host "创建失败" -ForegroundColor Red } $wslConfContent = @" [user] default=$newUserName [network] hostname = Nekro-Agent [interop] enabled = true appendWindowsPath = false [boot] systemd = true "@ $setWslConfCommand = "echo `'$wslConfContent`' | tee /etc/wsl.conf >/dev/null" Write-Host "正在写入配置 '/etc/wsl.conf'" Invoke-WslCommand -DistributionName $DistroName -Command $setWslConfCommand if ($LASTEXITCODE -eq 0) { Write-Host "写入成功" -ForegroundColor Green $InstallStatus.UpdateWSLConf = $true } else { Write-Host "写入失败" -ForegroundColor Red } Write-Host "正在替换为清华源..." $replaceSourceCommand = @' #!/bin/bash cp /etc/apt/sources.list /etc/apt/sources.list.bak sed -i 's#deb.debian.org#mirrors.tuna.tsinghua.edu.cn#g' /etc/apt/sources.list sed -i '/bullseye-backports/s/^/#/' /etc/apt/sources.list '@ Invoke-WslCommand -DistributionName $DistroName -Command $replaceSourceCommand if ($LASTEXITCODE -eq 0) { Write-Host "替换成功" -ForegroundColor Green $InstallStatus.ReplaceSource = $true } else { Write-Host "替换失败" -ForegroundColor Red } Write-Host "正在安装依赖:curl" $installDepsCommand = "apt-get update && apt-get install -y curl" Invoke-WslCommand -DistributionName $DistroName -Command $installDepsCommand if ($LASTEXITCODE -eq 0) { Write-Host "依赖安装成功" -ForegroundColor Green $InstallStatus.DepsInstalled = $true } else { Write-Host "依赖安装失败" -ForegroundColor Red } Write-Host "正在重启 `"$DistroName`" WSL 实例以生效配置" wsl.exe --terminate $DistroName $installNACommand = @' #!/bin/bash cd "$HOME" || exit 1 url=https://raw.githubusercontent.com/KroMiose/nekro-agent/main/docker/install.sh echo 正在下载安装脚本至 "$PWD/install.sh" if curl -fsSL -m 30 "$url" -o install.sh; then echo '用户 nekro 密码为 123456' sudo bash "$PWD/install.sh" else echo '脚本下载失败,退出...' >&2 exit 1 fi '@ if ($InstallStatus.DepsInstalled) { Write-Host "正在安装 NekroAgent..." Invoke-WslCommand -DistributionName $DistroName -Username $newUserName -Command $installNACommand if ($LASTEXITCODE -eq 0) { Write-Host "NekroAgent 安装成功" -ForegroundColor Green $InstallStatus.NAInstalled = $true } else { Write-Host "NekroAgent 安装失败" -ForegroundColor Red } } else {} Write-Host ("-"*40) if ($InstallStatus.NAInstalled) { Write-Host "NekroAgent 已安装成功!" } else { if (-not $InstallStatus.DepsInstalled) { Write-Host "依赖未安装:curl" } Write-Host "NekroAgent 安装失败" Write-Host "请前往 WSL 实例 `"$DistroName`" 手动完成安装" Write-Host "若存在 '/home/nekro/install.sh' 文件,请执行 'sudo bash /home/nekro/install.sh'" Write-Host "若不存在,请参考文档站 WSL2部署教程" } Write-Host "可使用 'wsl -d `"$DistroName`"' 进入 NekroAgent 所在 WSL 实例" if ($InstallStatus.CreatedUser) { Write-Host "用户 '$newUserName' 的密码为 " -NoNewline Write-Host "123456" -ForegroundColor Blue } else { Write-Host "用户 '$newUserName' 创建失败,建议创建非 root 用户使用" } Write-Host "若安装过程遇到错误请重新执行脚本" Write-Host "或前往 Github/QQ 反馈" Write-Host ("-"*40) Write-Host "脚本执行完毕。" Confirm-ExitWithEnter