- Setup.ps1 開頭暫停 Windows Update(PauseUpdatesExpiryTime + 停 wuauserv/UsoSvc/BITS), 結尾解暫停 + UsoClient 觸發掃描,避免跟 post-install 搶頻寬/CPU - Win11Debloat config 從 here-string 改成 hashtable + ConvertTo-Json, 繞開 PowerShell 5.1 ANSI 讀取時 here-string parse 失敗的 case - Log 函式加 Write-Host,console 視窗能即時看 START/DONE - unattend.xml Order 5 把 stdout+stderr 全部 redirect 到 firstlogon-runsetup.log, Setup.ps1 連 parse error 都會留紀錄;Order 3 改空密碼 + 強制改密
359 lines
16 KiB
PowerShell
359 lines
16 KiB
PowerShell
# Setup.ps1 — Windows 自動部署完成後的 post-install 腳本
|
||
#
|
||
# 入口:unattend.xml 的 FirstLogonCommands 會把這份從 Ventoy USB(/ventoy/script/Setup.ps1)
|
||
# 複製到 C:\Scripts\Setup.ps1,然後以 Admin 帳號執行。
|
||
#
|
||
# Log: C:\Windows\Temp\setup.log(每段都寫 START/DONE/ERR)
|
||
#
|
||
# 順序:開頭先暫停 Windows Update(避免跟本腳本搶頻寬/CPU),
|
||
# 再做不需網路的本機設定(防火牆 / 註冊機碼 / Default profile),
|
||
# 接著等網路,跑下載安裝(OpenSSH FoD / KMS / winget / RemoveAI / Win11Debloat / Telegram),
|
||
# 結尾解除 WU 暫停並主動觸發掃描,這樣更新會排在所有 post-install 之後才開始下載。
|
||
|
||
$ErrorActionPreference = 'Continue'
|
||
$ProgressPreference = 'SilentlyContinue'
|
||
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
|
||
|
||
$LogFile = 'C:\Windows\Temp\setup.log'
|
||
|
||
# 內網 KMS 主機請改下面這列;保持空陣列就會用公開 KMS 清單輪流試
|
||
$InternalKmsServers = @()
|
||
$PublicKmsServers = @('kms.digiboy.ir', 'kms8.msguides.com', 'kms.03k.org', 'kms.chinancce.com')
|
||
|
||
# 要批次移除的 UWP(與 windows-remote-toolkit/config/remove-apps-config.json 同步)
|
||
$UwpRemoveList = @(
|
||
'Microsoft.BingNews','Microsoft.BingWeather','Microsoft.GetHelp',
|
||
'Microsoft.MicrosoftSolitaireCollection','Microsoft.Todos','Microsoft.Windows.DevHome',
|
||
'Microsoft.WindowsAlarms','Microsoft.WindowsCamera','Microsoft.WindowsFeedbackHub',
|
||
'Microsoft.WindowsSoundRecorder','Microsoft.Xbox.TCUI','Microsoft.XboxGameCallableUI',
|
||
'Microsoft.XboxIdentityProvider','Microsoft.XboxSpeechToTextOverlay','Microsoft.YourPhone',
|
||
'Microsoft.ZuneMusic','MicrosoftCorporationII.QuickAssist',
|
||
'MicrosoftWindows.Client.WebExperience','MicrosoftWindows.CrossDevice','MSTeams',
|
||
'Microsoft.PowerAutomateDesktop','Microsoft.Windows.Photos'
|
||
)
|
||
|
||
# Win11Debloat 的設定(與 toolkit/config/win11debloat-config.json 同步)
|
||
# 改用 hashtable + ConvertTo-Json,避免 here-string 在某些 PowerShell 環境 parse 失敗
|
||
$Win11DebloatTweakNames = @(
|
||
'RemoveApps','RemoveGamingApps','RemoveCommApps','RemoveW11Outlook',
|
||
'DisableTelemetry','DisableSearchHistory','DisableBing','DisableStoreSearchSuggestions',
|
||
'DisableSearchHighlights','DisableLockscreenTips','DisableSuggestions','DisableDesktopSpotlight',
|
||
'DisableFindMyDevice','DisableEdgeAds','DisableBraveBloat','DisableSettings365Ads',
|
||
'DisableSettingsHome','DisableCopilot','DisableRecall','DisableClickToDo',
|
||
'DisableAISvcAutoStart','DisablePaintAI','DisableNotepadAI','DisableEdgeAI',
|
||
'PreventUpdateAutoReboot','DisableModernStandbyNetworking','DisableDVR','DisableGameBarIntegration',
|
||
'ShowHiddenFolders','ShowKnownFileExt','EnableDarkMode','TaskbarAlignLeft',
|
||
'HideSearchTb','HideTaskview','HideChat','DisableWidgets','DisableStartRecommended',
|
||
'Hide3dObjects','ExplorerToThisPC','HideGallery','DisableDragTray'
|
||
)
|
||
$Win11DebloatConfig = [ordered]@{
|
||
Version = '1.0'
|
||
Tweaks = @($Win11DebloatTweakNames | ForEach-Object { [ordered]@{ Name = $_; Value = $true } })
|
||
Deployment = @(
|
||
[ordered]@{ Name = 'CreateRestorePoint'; Value = $true },
|
||
[ordered]@{ Name = 'RestartExplorer'; Value = $false },
|
||
[ordered]@{ Name = 'AppRemovalScopeIndex'; Value = 0 }
|
||
)
|
||
}
|
||
$Win11DebloatConfigJson = $Win11DebloatConfig | ConvertTo-Json -Depth 5
|
||
|
||
# ---- helpers ----
|
||
|
||
function Log {
|
||
param([string]$msg)
|
||
$line = '[{0}] {1}' -f (Get-Date -Format 'yyyy-MM-dd HH:mm:ss'), $msg
|
||
Add-Content -Path $LogFile -Value $line -Encoding UTF8
|
||
Write-Host $line
|
||
}
|
||
|
||
function Section {
|
||
param([string]$name, [scriptblock]$block)
|
||
Log "=== $name START ==="
|
||
try {
|
||
& $block
|
||
Log "=== $name DONE ==="
|
||
} catch {
|
||
Log "=== $name ERR: $($_.Exception.Message) ==="
|
||
}
|
||
}
|
||
|
||
function Wait-Network {
|
||
param([int]$timeoutSec = 60)
|
||
$i = 0
|
||
$max = [int]($timeoutSec / 2)
|
||
while ($i -lt $max -and -not (Test-Connection 8.8.8.8 -Count 1 -Quiet -ErrorAction SilentlyContinue)) {
|
||
Start-Sleep 2
|
||
$i++
|
||
}
|
||
Log "Network wait: $($i*2)s"
|
||
}
|
||
|
||
function Set-RegValue {
|
||
param([string]$Path, [string]$Name, $Value, [string]$Type = 'DWord')
|
||
if (-not (Test-Path $Path)) { New-Item -Path $Path -Force | Out-Null }
|
||
New-ItemProperty -Path $Path -Name $Name -Value $Value -PropertyType $Type -Force | Out-Null
|
||
}
|
||
|
||
# 開頭橫線(log 開始)
|
||
"`n========== Setup.ps1 starting at $(Get-Date -Format o) ==========" | Add-Content $LogFile -Encoding UTF8
|
||
|
||
# ---- Phase 1: 本機設定(不需網路)----
|
||
|
||
Section 'pause-windows-update' {
|
||
# 設定官方暫停旗標:暫停 24 小時(Setup.ps1 結尾會主動清掉並觸發掃描)
|
||
$now = (Get-Date).ToUniversalTime().ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||
$expiry = (Get-Date).ToUniversalTime().AddHours(24).ToString("yyyy-MM-ddTHH:mm:ssZ")
|
||
$ux = 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings'
|
||
Set-RegValue $ux 'PauseUpdatesStartTime' $now 'String'
|
||
Set-RegValue $ux 'PauseUpdatesExpiryTime' $expiry 'String'
|
||
Set-RegValue $ux 'PauseFeatureUpdatesStartTime' $now 'String'
|
||
Set-RegValue $ux 'PauseFeatureUpdatesEndTime' $expiry 'String'
|
||
Set-RegValue $ux 'PauseQualityUpdatesStartTime' $now 'String'
|
||
Set-RegValue $ux 'PauseQualityUpdatesEndTime' $expiry 'String'
|
||
Log "WU paused until $expiry (UTC)"
|
||
|
||
# 停掉已啟動的 WU 相關服務,避免在 Setup.ps1 期間搶頻寬/CPU
|
||
foreach ($svc in @('UsoSvc','wuauserv','BITS')) {
|
||
Stop-Service -Name $svc -Force -ErrorAction SilentlyContinue
|
||
}
|
||
}
|
||
|
||
Section 'firewall-ssh-22' {
|
||
Remove-NetFirewallRule -DisplayName 'SSH TCP 22 (Allow Inbound)' -ErrorAction SilentlyContinue
|
||
New-NetFirewallRule -DisplayName 'SSH TCP 22 (Allow Inbound)' `
|
||
-Direction Inbound -Protocol TCP -LocalPort 22 -Action Allow -Profile Any -Enabled True | Out-Null
|
||
Set-NetFirewallProfile -Profile Domain,Private,Public -Enabled True
|
||
}
|
||
|
||
Section 'rdp-enable-3389' {
|
||
Set-RegValue 'HKLM:\System\CurrentControlSet\Control\Terminal Server' 'fDenyTSConnections' 0
|
||
Set-RegValue 'HKLM:\System\CurrentControlSet\Control\Terminal Server\WinStations\RDP-Tcp' 'UserAuthentication' 1
|
||
Enable-NetFirewallRule -DisplayGroup 'Remote Desktop' -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
Section 'icmp-echo-allow' {
|
||
Remove-NetFirewallRule -DisplayName 'ICMPv4 Echo Request (Allow Inbound)' -ErrorAction SilentlyContinue
|
||
New-NetFirewallRule -DisplayName 'ICMPv4 Echo Request (Allow Inbound)' `
|
||
-Direction Inbound -Protocol ICMPv4 -IcmpType 8 -Action Allow -Profile Any -Enabled True | Out-Null
|
||
Get-NetFirewallRule -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.DisplayName -match 'Echo Request' } |
|
||
Enable-NetFirewallRule -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
Section 'winrm-enable' {
|
||
Enable-PSRemoting -Force -SkipNetworkProfileCheck | Out-Null
|
||
Set-Service -Name WinRM -StartupType Automatic
|
||
Start-Service -Name WinRM -ErrorAction SilentlyContinue
|
||
cmd.exe /c 'winrm quickconfig -quiet -force' | Out-Null
|
||
cmd.exe /c 'winrm set winrm/config/service/auth @{Basic="true"}' | Out-Null
|
||
cmd.exe /c 'winrm set winrm/config/service @{AllowUnencrypted="true"}' | Out-Null
|
||
cmd.exe /c 'winrm set winrm/config/client @{TrustedHosts="*"}' | Out-Null
|
||
Enable-NetFirewallRule -DisplayGroup 'Windows Remote Management' -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
Section 'onedrive-remove' {
|
||
Get-Process OneDrive -ErrorAction SilentlyContinue | Stop-Process -Force -ErrorAction SilentlyContinue
|
||
foreach ($p in @("$env:SystemRoot\System32\OneDriveSetup.exe", "$env:SystemRoot\SysWOW64\OneDriveSetup.exe")) {
|
||
if (Test-Path $p) { Start-Process $p -ArgumentList '/uninstall' -Wait -ErrorAction SilentlyContinue }
|
||
}
|
||
Remove-ItemProperty -Path 'HKLM:\Software\Microsoft\Windows\CurrentVersion\Run' -Name 'OneDrive' -ErrorAction SilentlyContinue
|
||
Remove-Item 'HKLM:\SOFTWARE\Classes\CLSID\{018D5C66-4533-4307-9B53-224DE2ED1FE6}' -Recurse -ErrorAction SilentlyContinue
|
||
Remove-Item 'HKLM:\SOFTWARE\Wow6432Node\Classes\CLSID\{018D5C66-4533-4307-9B53-224DE2ED1FE6}' -Recurse -ErrorAction SilentlyContinue
|
||
Remove-Item -Path 'C:\OneDriveTemp', "$env:USERPROFILE\OneDrive", "$env:LOCALAPPDATA\Microsoft\OneDrive" -Recurse -Force -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
Section 'cortana-websearch-off' {
|
||
$k1 = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Windows Search'
|
||
Set-RegValue $k1 'AllowCortana' 0
|
||
Set-RegValue $k1 'DisableWebSearch' 1
|
||
Set-RegValue $k1 'ConnectedSearchUseWeb' 0
|
||
Set-RegValue $k1 'AllowSearchToUseLocation' 0
|
||
|
||
$k2 = 'HKLM:\SOFTWARE\Policies\Microsoft\Windows\Explorer'
|
||
Set-RegValue $k2 'DisableSearchBoxSuggestions' 1
|
||
}
|
||
|
||
# 把 UI / IME 預設套到 Default profile,新使用者首次登入會繼承
|
||
Section 'apply-ui-ime-default-profile' {
|
||
$hivePath = 'C:\Users\Default\NTUSER.DAT'
|
||
$mountKey = 'HKU\SetupDefault'
|
||
reg.exe load $mountKey $hivePath | Out-Null
|
||
try {
|
||
$base = "Registry::$mountKey"
|
||
|
||
$adv = "$base\Software\Microsoft\Windows\CurrentVersion\Explorer\Advanced"
|
||
Set-RegValue $adv 'HideFileExt' 0 # 顯示副檔名
|
||
Set-RegValue $adv 'Hidden' 1 # 顯示隱藏檔
|
||
Set-RegValue $adv 'TaskbarAl' 0 # 工作列靠左
|
||
Set-RegValue $adv 'ShowTaskViewButton' 0 # 隱藏 Task View 按鈕
|
||
Set-RegValue $adv 'LaunchTo' 1 # 開啟 This PC
|
||
Set-RegValue $adv 'Start_TrackProgs' 0 # 不追蹤最近開啟的程式
|
||
|
||
$theme = "$base\Software\Microsoft\Windows\CurrentVersion\Themes\Personalize"
|
||
Set-RegValue $theme 'AppsUseLightTheme' 0
|
||
Set-RegValue $theme 'SystemUseLightTheme' 0
|
||
|
||
# 工作列搜尋框:0=隱藏, 1=圖示, 2=框, 3=圖示+標籤
|
||
$search = "$base\Software\Microsoft\Windows\CurrentVersion\Search"
|
||
Set-RegValue $search 'SearchboxTaskbarMode' 0
|
||
|
||
$cloud = "$base\Software\Policies\Microsoft\Windows\CloudContent"
|
||
Set-RegValue $cloud 'DisableSpotlightCollectionOnDesktop' 1
|
||
Set-RegValue $cloud 'DisableWindowsSpotlightFeatures' 1
|
||
|
||
# 鍵盤排序:英文鍵盤排第一(預設英數),注音排第二
|
||
$kbPre = "$base\Keyboard Layout\Preload"
|
||
Set-RegValue $kbPre '1' '00000409' 'String'
|
||
Set-RegValue $kbPre '2' '00000404' 'String'
|
||
|
||
# CHT 注音:預設英數模式、關掉雲端候選與預測
|
||
$cht = "$base\Software\Microsoft\InputMethod\Settings\CHT"
|
||
Set-RegValue $cht 'Enable Default Input Mode' 0
|
||
Set-RegValue $cht 'Enable Cand Predict' 0
|
||
Set-RegValue $cht 'Enable Cloud Cand' 0
|
||
Set-RegValue $cht 'Enable Cloud Input' 0
|
||
|
||
# 把系統 Hotkey 切換鍵停掉(3 = 不指定鍵)
|
||
$tg = "$base\Keyboard Layout\Toggle"
|
||
Set-RegValue $tg 'Hotkey' '3' 'String'
|
||
Set-RegValue $tg 'Language Hotkey' '3' 'String'
|
||
Set-RegValue $tg 'Layout Hotkey' '3' 'String'
|
||
} finally {
|
||
[GC]::Collect()
|
||
Start-Sleep 1
|
||
reg.exe unload $mountKey | Out-Null
|
||
}
|
||
}
|
||
|
||
Section 'hide-admin-account' {
|
||
# 帳號 User 在 unattend.xml Order 3 已直接以大寫建立,這裡只負責把 Admin 從登入畫面藏掉
|
||
$userListKey = 'HKLM:\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon\SpecialAccounts\UserList'
|
||
Set-RegValue $userListKey 'Admin' 0 # 0 = 從登入畫面隱藏(帳號仍存在仍可用)
|
||
}
|
||
|
||
# ---- Phase 2: 需要網路 ----
|
||
|
||
Section 'wait-network' { Wait-Network -timeoutSec 60 }
|
||
|
||
Section 'openssh-server-install' {
|
||
Add-WindowsCapability -Online -Name 'OpenSSH.Server~~~~0.0.1.0' -ErrorAction Stop | Out-Null
|
||
Set-Service sshd -StartupType Automatic
|
||
Start-Service sshd
|
||
}
|
||
|
||
Section 'kms-activate' {
|
||
$servers = @()
|
||
if ($InternalKmsServers.Count -gt 0) { $servers += $InternalKmsServers }
|
||
$servers += $PublicKmsServers
|
||
$activated = $false
|
||
foreach ($kms in $servers) {
|
||
Log "Try KMS: $kms"
|
||
cscript.exe //nologo C:\Windows\System32\slmgr.vbs /skms $kms | ForEach-Object { Log " skms: $_" }
|
||
$out = cscript.exe //nologo C:\Windows\System32\slmgr.vbs /ato 2>&1
|
||
foreach ($line in $out) { Log " ato : $line" }
|
||
if ($out -match 'successful|成功') {
|
||
Log "Activated via $kms"
|
||
$activated = $true
|
||
break
|
||
}
|
||
}
|
||
if (-not $activated) { Log 'All KMS servers failed; remaining in grace period' }
|
||
}
|
||
|
||
Section 'winget-install-chrome' {
|
||
$winget = (Get-Command winget -ErrorAction SilentlyContinue).Source
|
||
if (-not $winget) { Log 'winget not found (Win10 沒裝 App Installer?)'; return }
|
||
& $winget install --id Google.Chrome -e --silent `
|
||
--accept-source-agreements --accept-package-agreements --scope machine 2>&1 |
|
||
ForEach-Object { Log " winget: $_" }
|
||
}
|
||
|
||
Section 'remove-windows-ai' {
|
||
$sb = [scriptblock]::Create((Invoke-RestMethod 'https://raw.githubusercontent.com/zoicware/RemoveWindowsAI/main/RemoveWindowsAi.ps1'))
|
||
& $sb -nonInteractive -AllOptions -EnableLogging | Out-Null
|
||
}
|
||
|
||
Section 'win11debloat' {
|
||
$work = Join-Path $env:TEMP 'win11debloat'
|
||
$zip = "$work.zip"
|
||
if (Test-Path $work) { Remove-Item $work -Recurse -Force }
|
||
Invoke-WebRequest -UseBasicParsing -Uri 'https://github.com/Raphire/Win11Debloat/archive/refs/heads/master.zip' -OutFile $zip
|
||
Expand-Archive -Path $zip -DestinationPath $work -Force
|
||
$dir = Get-ChildItem $work -Directory | Select-Object -First 1
|
||
if (-not $dir) { throw 'Win11Debloat zip extracted but no inner dir' }
|
||
$config = Join-Path $dir.FullName 'unattend-config.json'
|
||
Set-Content -Path $config -Value $Win11DebloatConfigJson -Encoding UTF8
|
||
& "$($dir.FullName)\Win11Debloat.ps1" -Silent -Config $config -NoRestartExplorer
|
||
}
|
||
|
||
Section 'remove-uwp-apps' {
|
||
foreach ($name in $UwpRemoveList) {
|
||
Get-AppxPackage -AllUsers -Name $name -ErrorAction SilentlyContinue | ForEach-Object {
|
||
try {
|
||
Remove-AppxPackage -AllUsers -Package $_.PackageFullName -ErrorAction Stop
|
||
Log "Removed AppxPackage $name"
|
||
} catch {
|
||
Log "Skip AppxPackage $name : $($_.Exception.Message)"
|
||
}
|
||
}
|
||
Get-AppxProvisionedPackage -Online -ErrorAction SilentlyContinue |
|
||
Where-Object { $_.DisplayName -eq $name } |
|
||
ForEach-Object {
|
||
try {
|
||
Remove-AppxProvisionedPackage -Online -PackageName $_.PackageName -ErrorAction Stop | Out-Null
|
||
Log "Removed Provisioned $name"
|
||
} catch {
|
||
Log "Skip Provisioned $name : $($_.Exception.Message)"
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
Section 'install-telegram' {
|
||
$p = Join-Path $env:TEMP 'tsetup.exe'
|
||
Invoke-WebRequest -UseBasicParsing -Uri 'https://telegram.org/dl/desktop/win64' -OutFile $p
|
||
$proc = Start-Process -FilePath $p -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Wait -PassThru
|
||
Log "Telegram installer exit code: $($proc.ExitCode)"
|
||
}
|
||
|
||
Section 'resume-and-trigger-windows-update' {
|
||
# 清掉暫停旗標(pause-windows-update 設的那幾個值)
|
||
$ux = 'HKLM:\SOFTWARE\Microsoft\WindowsUpdate\UX\Settings'
|
||
foreach ($n in @('PauseUpdatesStartTime','PauseUpdatesExpiryTime',
|
||
'PauseFeatureUpdatesStartTime','PauseFeatureUpdatesEndTime',
|
||
'PauseQualityUpdatesStartTime','PauseQualityUpdatesEndTime')) {
|
||
Remove-ItemProperty -Path $ux -Name $n -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
# 確保服務可啟動
|
||
foreach ($svc in @('BITS','wuauserv','UsoSvc')) {
|
||
Start-Service -Name $svc -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
# 通知 Update Orchestrator 重新讀設定,然後立即掃描/下載/安裝
|
||
$uso = "$env:SystemRoot\System32\UsoClient.exe"
|
||
if (Test-Path $uso) {
|
||
& $uso RefreshSettings | Out-Null
|
||
& $uso StartScan | Out-Null
|
||
& $uso StartDownload | Out-Null
|
||
& $uso StartInstall | Out-Null
|
||
Log 'WU resumed; UsoClient scan/download/install triggered'
|
||
} else {
|
||
# 後備:呼叫 COM 觸發掃描
|
||
try {
|
||
(New-Object -ComObject Microsoft.Update.AutoUpdate).DetectNow()
|
||
Log 'WU resumed; AutoUpdate.DetectNow triggered (COM fallback)'
|
||
} catch {
|
||
Log "WU resumed but trigger failed: $($_.Exception.Message)"
|
||
}
|
||
}
|
||
}
|
||
|
||
# 結尾:刪掉殘留的明碼回應檔
|
||
Section 'cleanup-unattend-files' {
|
||
Remove-Item 'C:\Windows\Panther\unattend.xml' -Force -ErrorAction SilentlyContinue
|
||
Remove-Item 'C:\Windows\Panther\Unattend\unattend.xml' -Force -ErrorAction SilentlyContinue
|
||
}
|
||
|
||
"========== Setup.ps1 finished at $(Get-Date -Format o) ==========" | Add-Content $LogFile -Encoding UTF8
|