From c84e18bd9d0dea953b3e97743403c38ecb640f8c Mon Sep 17 00:00:00 2001 From: Timmy Date: Mon, 27 Apr 2026 13:36:13 +0800 Subject: [PATCH] =?UTF-8?q?=E6=96=B0=E5=A2=9E=20Setup.ps1=EF=BC=8813=20?= =?UTF-8?q?=E6=AE=B5=20post-install=EF=BC=89=EF=BC=9BFirstLogonCommands=20?= =?UTF-8?q?=E6=94=B9=E5=BE=9E=20Ventoy=20USB=20=E6=8B=B7=E8=85=B3=E6=9C=AC?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 把分散在 unattend.xml inline 的網路相依步驟(OpenSSH/Telegram/RemoveAI)和 新加入的 10 個 post-install 動作整合進獨立的 Setup.ps1,由 FirstLogonCommands Order 4 從 Ventoy USB(/ventoy/script/Setup.ps1)掃磁碟複製到 C:\Scripts\, Order 5 執行。腳本分兩 phase: Phase 1(不需網路):SSH 22 防火牆、RDP+3389、ICMP Echo、WinRM TrustedHosts=*、 移 OneDrive、關 Cortana/WebSearch、套 UI/IME 預設到 Default profile、user→User 改名 + 從登入畫面隱藏 Admin。 Phase 2(要網路,wait-network 後):OpenSSH Server FoD、KMS 啟用(先試 $InternalKmsServers 後 fallback 公開 KMS)、winget Chrome、RemoveWindowsAI、 Win11Debloat、22 個 UWP 移除、Telegram、刪 Panther 殘留。 每段以 Section 包裝寫獨立 START/DONE/ERR 到 C:\Windows\Temp\setup.log,方便逐段 重跑與排查。 文件全部同步:DEPLOY/QUICKSTART/SUMMARY/README/CLAUDE 都加上 Setup.ps1 的 USB 部署、cp 步驟、Order 表、排查指引。 Co-Authored-By: Claude Opus 4.7 (1M context) --- CLAUDE.md | 18 ++- DEPLOY.md | 142 ++++++++++++++------- QUICKSTART.md | 45 +++++-- README.md | 42 ++++--- SUMMARY.md | 49 ++++++-- Setup.ps1 | 340 ++++++++++++++++++++++++++++++++++++++++++++++++++ unattend.xml | 37 +++--- 7 files changed, 565 insertions(+), 108 deletions(-) create mode 100644 Setup.ps1 diff --git a/CLAUDE.md b/CLAUDE.md index 874bb56..e8edf78 100644 --- a/CLAUDE.md +++ b/CLAUDE.md @@ -4,12 +4,13 @@ This file provides guidance to Claude Code (claude.ai/code) when working with co ## Repository nature -This is a **configuration template repo**, not a code project. It ships two artifacts: +This is a **configuration template repo**, not a code project. It ships three artifacts: - `unattend.xml` — the Windows Setup answer file. +- `Setup.ps1` — the post-install PowerShell that runs at first logon (firewall/WinRM/RDP/ICMP, KMS, Win11Debloat, RemoveWindowsAI, OneDrive removal, Cortana off, Default-profile UI/IME defaults, account rename, Telegram, Chrome via winget). Lives in the repo and is also copied to the Ventoy USB at `/ventoy/script/Setup.ps1`. At first logon `unattend.xml` scans removable drives for `\ventoy\script\Setup.ps1`, copies it to `C:\Scripts\Setup.ps1`, then runs it. - `ventoy.json` — the Ventoy Auto Install plugin config that tells Ventoy which ISO to pair with which template. -Deployment is via **[Ventoy](https://www.ventoy.net/)**, not Rufus or direct ISO modification. The user keeps a Ventoy USB with multiple Windows ISOs; `ventoy.json` at `/ventoy/ventoy.json` on the USB and `unattend.xml` at `/ventoy/script/unattend.xml` let Ventoy inject the answer file at boot without touching the ISO. Assume this Ventoy-based flow when reasoning about deploy-time behavior. +Deployment is via **[Ventoy](https://www.ventoy.net/)**, not Rufus or direct ISO modification. The user keeps a Ventoy USB with multiple Windows ISOs; `ventoy.json` at `/ventoy/ventoy.json` on the USB, `unattend.xml` at `/ventoy/script/unattend.xml`, and `Setup.ps1` at `/ventoy/script/Setup.ps1` let Ventoy inject the answer file at boot without touching the ISO and have the post-install script ready next to it. Assume this Ventoy-based flow when reasoning about deploy-time behavior. There is no build, no test suite, no package manager, and no runtime. Changes are validated by: @@ -41,10 +42,19 @@ There is no way to "run" this repo on the dev machine (macOS). All verification - **`processorArchitecture="amd64"`** is hardcoded on every ``. ARM64 deployments require a global find-and-replace to `arm64`. - **Password is plaintext** (`true</PlainText>`). This is a deliberate testing-only choice documented in `SUMMARY.md`. For production, generate Base64 via Windows SIM; do not hand-encode. - **`ComputerName` does not support `%RAND:5%` or any other macro.** Windows Setup only accepts a literal name, `*` (auto-generate a random name like `DESKTOP-ABC123D`), or an empty value. `%RAND:N%` is an MDT/SCCM task-sequence variable — under the Ventoy → raw `unattend.xml` path it is passed verbatim as a literal string, fails NetBIOS character validation (`%` is not allowed), and causes the specialize pass to abort with a `Microsoft-Windows-Shell-Setup` error. If a `PC-XXXXX` style name is required, set `<ComputerName>*</ComputerName>` here and do the rename in `FirstLogonCommands` via `Rename-Computer` (a reboot is needed for it to take effect — combine with the existing `shutdown /r` step). (Distinct from `$$VT_*$$`, which *is* a real Ventoy pre-processor substitution.) -- **`FirstLogonCommands` references `C:\Scripts\Setup.ps1`** but this repo does **not** contain that script. It is expected to be placed on the target by other means (baked into the image, copied from USB earlier in `FirstLogonCommands`, or pulled from network). +- **`Setup.ps1` is shipped via the Ventoy USB, not embedded in `unattend.xml`.** `FirstLogonCommands` Order 4 scans every mounted filesystem drive for `\ventoy\script\Setup.ps1` and copies the first match to `C:\Scripts\Setup.ps1`; Order 5 runs it. Two consequences: (1) the Ventoy USB **must remain plugged in until first logon completes** — yanking it after WinPE means Setup.ps1 never makes it to disk and the post-install steps silently skip (`C:\Windows\Temp\firstlogon-copysetup.log` will say `NOT FOUND`); (2) editing `Setup.ps1` in the repo is not enough — re-copy to `/Volumes/Ventoy/ventoy/script/Setup.ps1` before each test deploy. Both `firstlogon-copysetup.log` and `setup.log` live under `C:\Windows\Temp\`. +- **`Setup.ps1` runs as the AutoLogon `Admin` session** in `FirstLogonCommands`. It modifies the Default profile's `NTUSER.DAT` (loaded via `reg load HKU\SetupDefault C:\Users\Default\NTUSER.DAT`) to push UI/IME defaults to *future* user logins — it does **not** touch the AutoLogon `Admin` session itself, and it cannot reach the just-created `user`/`User` profile because that profile dir doesn't exist until that user first logs in. Anything you want applied to `Admin`'s already-active session has to go through `HKCU` directly. +- **The `user` → `User` rename happens inside `Setup.ps1`**, not in `unattend.xml`. `unattend.xml` Order 3 creates the lowercase `user` (so it's bulletproof even if `Setup.ps1` never runs); `Setup.ps1`'s `rename-hide-accounts` section then `Rename-LocalUser`s it to capitalised `User` and hides `Admin` from the login screen via `SpecialAccounts\UserList`. If you change the account name convention, both places must be edited. ## When making changes - Preserve the `xmlns="urn:schemas-microsoft-com:unattend"` namespace and the `wcm:action="add"` attributes on list items — Windows SIM emits these and Setup requires them. - Every `<component>` needs all four identity attributes: `name`, `processorArchitecture`, `publicKeyToken="31bf3856ad364e35"`, `language="neutral"`, `versionScope="nonSxS"`. -- The Chinese-language docs (`README.md`, `QUICKSTART.md`, `SUMMARY.md`) explain the *why* behind design decisions — consult `SUMMARY.md` before proposing structural changes, as several current choices are explicit tradeoffs, not oversights. +- The Chinese-language docs (`README.md`, `QUICKSTART.md`, `SUMMARY.md`, `DEPLOY.md`) explain the *why* behind design decisions — consult `SUMMARY.md` before proposing structural changes, as several current choices are explicit tradeoffs, not oversights. + +## When making changes to `Setup.ps1` + +- Sections are wrapped with the `Section` helper (`Section 'name' { ... }`) which logs `START` / `DONE` / `ERR` to `C:\Windows\Temp\setup.log`. Each one is idempotent and tolerates partial earlier runs — preserve that property when adding new sections, because deploys do get re-tested by re-running `Setup.ps1` manually. +- Phase ordering is enforced by file order, not by metadata: Phase 1 (no network — firewall, registry, Default profile, account rename) runs first; `wait-network` then blocks up to 60 s; Phase 2 (network — OpenSSH FoD, KMS, winget, RemoveAI, Win11Debloat, UWP removal, Telegram) follows. Don't move a network-dependent section above `wait-network`. +- `$Win11DebloatConfigJson` and `$UwpRemoveList` are mirrored from `windows-remote-toolkit/config/win11debloat-config.json` and `remove-apps-config.json`. They were intentionally **inlined** rather than fetched at runtime so the script is self-contained on the Ventoy USB. If you change one, update the comment pointing at the toolkit source so the next sync isn't accidental. +- `$InternalKmsServers` is intentionally empty — the public KMS list (`kms.digiboy.ir`, `kms8.msguides.com`, `kms.03k.org`, `kms.chinancce.com`) is the fallback. If a deployment is on a network with its own KMS host, that's the only line to change. diff --git a/DEPLOY.md b/DEPLOY.md index 871ec1c..6211311 100644 --- a/DEPLOY.md +++ b/DEPLOY.md @@ -26,9 +26,12 @@ USB 根目錄兩個 ISO: └── ventoy/ ├── ventoy.json ← 對應兩個 ISO,都指向同一份 template └── script/ - └── unattend.xml ← 與 repo 根目錄的 unattend.xml 相同 + ├── unattend.xml ← 與 repo 根目錄的 unattend.xml 相同 + └── Setup.ps1 ← 與 repo 根目錄的 Setup.ps1 相同(首次登入自動複製到 C:\Scripts\) ``` +`Setup.ps1` **必須跟 unattend.xml 放在同一個 `/ventoy/script/` 目錄下**。`unattend.xml` 的 `FirstLogonCommands` Order 4 會掃所有掛載的磁碟,找到第一個有 `\ventoy\script\Setup.ps1` 的就複製到 `C:\Scripts\Setup.ps1`,所以這個路徑寫死,不能搬。 + --- ## USB 上的 `ventoy.json` @@ -80,6 +83,7 @@ USB 根目錄兩個 ISO: # 從 repo 根目錄執行 mkdir -p /Volumes/Ventoy/ventoy/script cp unattend.xml /Volumes/Ventoy/ventoy/script/unattend.xml +cp Setup.ps1 /Volumes/Ventoy/ventoy/script/Setup.ps1 # 寫 ventoy.json(見上一節內容,或直接手編) # 注意:image 檔名必須跟 USB 上的 ISO 檔名完全一致 @@ -93,17 +97,20 @@ dot_clean /Volumes/Ventoy/ventoy ```bash ls /Volumes/Ventoy/ventoy/ /Volumes/Ventoy/ventoy/script/ diff unattend.xml /Volumes/Ventoy/ventoy/script/unattend.xml # 應無輸出 +diff Setup.ps1 /Volumes/Ventoy/ventoy/script/Setup.ps1 # 應無輸出 cat /Volumes/Ventoy/ventoy/ventoy.json ``` +**改 `Setup.ps1` 後沒重新 `cp` 是部署最常見的失誤**:FirstLogonCommands 從 USB 讀的是 `/ventoy/script/Setup.ps1` 那份,repo 裡改了不會自己同步上去。每次調 Setup.ps1 都要記得跑 `cp Setup.ps1 /Volumes/Ventoy/ventoy/script/Setup.ps1` 再 `dot_clean`。 + --- ## 換 ISO 時要改什麼 1. 把新 ISO 丟到 USB 根目錄。 2. 編輯 `/Volumes/Ventoy/ventoy/ventoy.json`,把對應條目的 `image` 換成新檔名(或新增一條)。 -3. **不用**動 `/Volumes/Ventoy/ventoy/script/unattend.xml`,除非要改 Windows 版本特有設定。 -4. 若 repo 裡的 `unattend.xml` 有更新,重跑一次 `cp` 與 `dot_clean` 即可。 +3. **不用**動 `/Volumes/Ventoy/ventoy/script/unattend.xml` 與 `Setup.ps1`,除非要改 Windows 版本特有設定。 +4. 若 repo 裡的 `unattend.xml` 或 `Setup.ps1` 有更新,重跑一次 `cp` 與 `dot_clean` 即可。 --- @@ -113,53 +120,97 @@ cat /Volumes/Ventoy/ventoy/ventoy.json | Order | 動作 | 需要網路? | |---|---|---| -| 1 | `powershell -File C:\Scripts\Setup.ps1` | 看腳本內容 | -| 2 | `net accounts /maxpwage:unlimited`(密碼永不過期) | 否 | -| 3 | `Rename-Computer` 把主機名改成 `PC-XXXXX`(重開後生效) | 否 | -| 4 | `net user user 1234 /add`+`/logonpasswordchg:yes`(建立一般使用者 `user`,首次登入強制改密) | 否 | -| 5 | 等網路→`Add-WindowsCapability OpenSSH.Server`+`Set-Service sshd Automatic`+`Start-Service sshd`(log: `firstlogon-sshd.log`) | **是** | -| 6 | 等網路→下載 `https://telegram.org/dl/desktop/win64` → 靜默安裝(`/VERYSILENT /SUPPRESSMSGBOXES /NORESTART`,log: `firstlogon-telegram.log`) | **是** | -| 7 | `shutdown /r /t 5` 重開機 | 否 | +| 1 | `net accounts /maxpwage:unlimited`(密碼永不過期) | 否 | +| 2 | `Rename-Computer` 把主機名改成 `PC-XXXXX`(重開後生效) | 否 | +| 3 | `net user user 1234 /add`+`/logonpasswordchg:yes`(建立一般使用者 `user`,首次登入強制改密;之後 Setup.ps1 會把它改名成 `User`) | 否 | +| 4 | 掃磁碟找 `\ventoy\script\Setup.ps1` 並複製到 `C:\Scripts\Setup.ps1`(log: `firstlogon-copysetup.log`) | 否(從 Ventoy USB 讀) | +| 5 | `powershell -File C:\Scripts\Setup.ps1`(13 段 post-install,見下節,log: `setup.log`) | 大部分 **是** | +| 6 | `shutdown /r /t 5` 重開機 | 否 | -### 新加入的 `user` 帳號 +Order 4/5 是改版重點:把原本散在 `FirstLogonCommands` 裡的 OpenSSH / Telegram / RemoveWindowsAI 三條搬進 `Setup.ps1`,並補上 10 個新動作。 + +### `Setup.ps1` 做了什麼(13 段,按執行順序) + +每段都用 `Section` 包裝,會把 `=== <name> START/DONE/ERR ===` 寫到 `C:\Windows\Temp\setup.log`,可以照名字逐段排查。 + +**Phase 1(不需網路、本機設定):** + +| 段名 | 在做什麼 | +|---|---| +| `firewall-ssh-22` | 建 `SSH TCP 22 (Allow Inbound)` 防火牆規則,三個 profile 都啟用 | +| `rdp-enable-3389` | `fDenyTSConnections=0`、`UserAuthentication=1`、啟用 `Remote Desktop` 防火牆群組 | +| `icmp-echo-allow` | 允許 ICMPv4 Echo Request inbound(讓外部 ping 得到) | +| `winrm-enable` | `Enable-PSRemoting`+`winrm quickconfig`+`Basic`/`AllowUnencrypted=true`+`TrustedHosts=*` | +| `onedrive-remove` | 殺 OneDrive process、跑兩處 `OneDriveSetup.exe /uninstall`、清啟動鍵與 CLSID、刪殘留資料夾 | +| `cortana-websearch-off` | `AllowCortana=0`、`DisableWebSearch=1`、`ConnectedSearchUseWeb=0`、`DisableSearchBoxSuggestions=1` | +| `apply-ui-ime-default-profile` | `reg load` Default profile NTUSER.DAT,寫入:顯示副檔名/隱藏檔、工作列靠左、Dark mode、隱藏搜尋框、停 Spotlight、英數鍵盤排第一+注音排第二、CHT 預設英數模式、停切換 Hotkey | +| `rename-hide-accounts` | `Rename-LocalUser user → User`、`SpecialAccounts\UserList\Admin=0`(從登入畫面隱藏 Admin) | + +**Phase 2(需網路、最多等 60 秒):** + +| 段名 | 在做什麼 | +|---|---| +| `wait-network` | 最多 30 回合×2 秒等 ICMP 通到 8.8.8.8 | +| `openssh-server-install` | `Add-WindowsCapability OpenSSH.Server`+`Set-Service sshd Automatic`+`Start-Service sshd` | +| `kms-activate` | 依序試 `$InternalKmsServers` → `$PublicKmsServers`(kms.digiboy.ir 等),跑 `slmgr /skms`+`/ato`,遇到第一個 `successful/成功` 就跳出 | +| `winget-install-chrome` | `winget install Google.Chrome --silent --scope machine`(Win10 沒裝 App Installer 會 skip) | +| `remove-windows-ai` | `irm https://raw.githubusercontent.com/zoicware/RemoveWindowsAI/main/RemoveWindowsAi.ps1` 後 `& -nonInteractive -AllOptions -EnableLogging` | +| `win11debloat` | 下載 [Raphire/Win11Debloat](https://github.com/Raphire/Win11Debloat) `master.zip`、解壓、寫出 `unattend-config.json`(清單與 `windows-remote-toolkit/config/win11debloat-config.json` 同步)、跑 `Win11Debloat.ps1 -Silent -Config ... -NoRestartExplorer` | +| `remove-uwp-apps` | 依 `$UwpRemoveList`(22 個 AppxPackage:BingNews/BingWeather/Xbox*/MSTeams/Photos/...)逐個 `Remove-AppxPackage -AllUsers` 與 `Remove-AppxProvisionedPackage` | +| `install-telegram` | 下載 `https://telegram.org/dl/desktop/win64` 跑 `/VERYSILENT /SUPPRESSMSGBOXES /NORESTART` | +| `cleanup-unattend-files` | 刪掉 `C:\Windows\Panther\unattend.xml` 與 `C:\Windows\Panther\Unattend\unattend.xml`(含明碼密碼) | + +### `user` → `User` 帳號 - **用途**:給使用者日常使用,和部署用的管理員 `Admin` 分開。 -- **權限**:一般 `Users` 群組(非 Administrators)。需要管理員權限時走 UAC。 +- **建立**:`unattend.xml` Order 3 用 `net user user 1234 /add`(首字小寫)。 +- **改名**:`Setup.ps1` 的 `rename-hide-accounts` 段把 `user` 改成 `User`(首字大寫)。要先建小寫再改名是因為 `Rename-LocalUser` 必須對既有帳號操作;如果 `Setup.ps1` 沒跑完,最差情況是有個 `user` 還能登入。 +- **權限**:一般 `Users` 群組(非 Administrators)。需要管理員權限時走 UAC(用 `Admin/P@ssw0rd!`)。 - **首次登入強制改密**:`/logonpasswordchg:yes` 會在下次登入時跳「密碼必須變更」。可以改成複雜密碼。 -- **如果 `net user` 因為 Windows 密碼政策拒絕 `1234`**:整條 Order 4 會失敗但不會擋後面,手動於 PowerShell 以 `New-LocalUser`+更寬的密碼重跑。 +- **如果 `net user` 因為 Windows 密碼政策拒絕 `1234`**:Order 3 會失敗但不會擋後面,手動於 PowerShell 以 `New-LocalUser`+更寬的密碼重跑,再讓 `Setup.ps1` 改名。 -### OpenSSH Server(Order 5) +### Order 4 的磁碟掃描沒找到 Setup.ps1 怎麼辦 -- 走 Windows 內建 Feature on Demand,**會從 Windows Update 下載** `OpenSSH.Server~~~~0.0.1.0`。需要外網。 -- 完成後 `sshd` 服務設成 `Automatic`(每次開機自動啟動),防火牆規則由 FoD 自動建立(TCP 22 inbound allow)。 -- 驗證:`Get-Service sshd` 應該 `Running`;`Get-NetFirewallRule -Name OpenSSH-Server-In-TCP` 應該 `Enabled=True`。 -- 用來遠端登入:`ssh Admin@<機器 IP>` 或 `ssh user@<機器 IP>`。初次使用要先在 `%ProgramData%\ssh\administrators_authorized_keys` 加公鑰(Windows OpenSSH 對 Administrators 群組的授權金鑰路徑特殊,不走 `~/.ssh/authorized_keys`)。 +Order 4 會逐個檢查所有掛載中的 FileSystem PSDrive,找 `\ventoy\script\Setup.ps1`。沒找到就寫 `NOT FOUND` 到 `C:\Windows\Temp\firstlogon-copysetup.log`,Order 5 接著用 `if exist` 判斷,找不到就寫 `SETUP_NOT_FOUND` 到 `firstlogon-runsetup.log` 然後跳過。原因通常是: -### Telegram(Order 6)的注意事項 - -- **要有網路**:OOBE 裡 `SkipMachineOOBE=true` 跳過了網路設定精靈,所以目標機必須有 Ethernet+DHCP,或本身已記住 Wi-Fi。這次有 log 可以驗證—`C:\Windows\Temp\firstlogon-telegram.log` 會寫 `NetWait=?s`、`Downloaded ?B`、`Exit ?`。 -- **網路等待上限 60 秒**:Order 5 和 Order 6 都各跑一次 `Test-Connection 8.8.8.8` 迴圈,最多等 30 回合×2 秒;ICMP 被擋住的環境會等完整 60 秒但 HTTPS 其實可用,也能跑下去。 -- **per-user 安裝**:Telegram 安裝程式預設裝到 Admin 這個帳號底下,**新建的 `user` 看不到**。要全機安裝就在 `ArgumentList` 加 `'/ALLUSERS'`。 -- **抓的是 latest 版**:每次部署抓當下最新版。要鎖版本改用 `https://updates.tdesktop.com/tsetup/tsetup-x64.X.Y.Z.exe` 直鏈。 -- **順序不能搬到 reboot 後**:`shutdown /r /t 5` 只是排程重開機、立即回傳,若放在 Telegram 前面,下載安裝會被 5 秒後的關機砍掉。 - -### 上次部署 Telegram 無聲失敗如何排查 - -這次每一條都有 log:登入 `Admin` 後看 - -``` -notepad C:\Windows\Temp\firstlogon-telegram.log -notepad C:\Windows\Temp\firstlogon-sshd.log -``` - -常見會看到的值: - -| Log 內容 | 代表 | 對策 | +| 現象 | 原因 | 對策 | |---|---|---| -| `NetWait=60s` + `ERR ...` | 60 秒內 ICMP 沒通,後面可能也失敗 | 接網路線、或在 OOBE 後手動連 Wi-Fi 後重跑指令 | -| `Downloaded 0B` 或 `ERR` 含 `SSL/TLS` | TLS 交握失敗 | 檢查系統時間是否對;老機器可能要裝 Windows Update for TLS 1.2 | -| `Exit 0` 但桌面沒 Telegram 捷徑 | 裝成功但 per-user 安裝、不是登入那個帳號 | 以 Admin 登入,捷徑在 `%APPDATA%\Microsoft\Windows\Start Menu\Programs\Telegram Desktop` | -| 完全沒 log 檔 | 這條 `SynchronousCommand` 根本沒跑(通常是 `AutoLogon.Enabled=false` 或 XML 沒注入) | 查 `C:\Windows\Panther\UnattendGC\setupact.log` | +| `NOT FOUND` | Ventoy USB 在 OOBE 完成前被拔掉 | 別拔,等 reboot 後再拔 | +| `NOT FOUND` | `Setup.ps1` 沒 cp 到 `/Volumes/Ventoy/ventoy/script/` | 重新 `cp` + `dot_clean` 後再插上重裝 | +| 找到但 Order 5 報 `Cannot run script ... not digitally signed` | 機器 ExecutionPolicy 是 Restricted | 不會發生,Order 5 用 `-ExecutionPolicy Bypass` 強蓋 | +| 找到了但 Setup.ps1 早早 ERR | 看 `C:\Windows\Temp\setup.log` 的 `=== <name> ERR ===` 行 | 對照上面的段名表逐段重跑 | + +### 排查 Setup.ps1 失敗 + +```powershell +notepad C:\Windows\Temp\firstlogon-copysetup.log # Order 4:USB 掃描結果 +notepad C:\Windows\Temp\firstlogon-runsetup.log # Order 5:找不到 Setup.ps1 才會有 +notepad C:\Windows\Temp\setup.log # Setup.ps1 主 log +``` + +`setup.log` 每段都有 `=== <name> START === ... === <name> DONE === ` 或 `=== <name> ERR: <msg> ===`。失敗段可以登入 `Admin` 後手動: + +```powershell +powershell -ExecutionPolicy Bypass -File C:\Scripts\Setup.ps1 +``` + +整支重跑(每段都是冪等的);或只挑某段:把 `Setup.ps1` 開起來,把那段 `Section '...'` 區塊的內容貼進 PowerShell 視窗即可。 + +### RemoveWindowsAI 補充 + +- **在做什麼**:跑 [zoicware/RemoveWindowsAI](https://github.com/zoicware/RemoveWindowsAI) 的 `RemoveWindowsAi.ps1`,一次把 Copilot(系統+Edge+Office)、Recall+排程任務、Input Insights、AI Fabric、Click to Do、Paint 實驗、Voice Access、Gaming Copilot、Notepad Rewrite、Photos AI 等 AI 元件全數關掉或移除(AppX、CBS、Registry、Group Policy 都會動)。 +- **參數**:`-nonInteractive`(不跳互動選單)+`-AllOptions`(全部選項都跑)+`-EnableLogging`(讓腳本自己也寫 log)。 +- **執行時間**:視目標機速度,通常 30 秒–3 分鐘。移除 CBS package 那段最慢。 +- **抓的是 `main` 分支**:每次部署可能拿到不同版本。要鎖定版本就把 `Setup.ps1` 中對應 URL 的 `/main/` 換成 commit SHA。 +- **可逆性**:腳本本身有 `-revertMode` 與 `-backupMode`,要還原的話可以登入後手動以 `-revertMode` 重跑;但 `-AllOptions` 跑完有些 CBS package 已被刪,`-revertMode` 不一定救得回,需要搭配 `-RunWinUpdateRepair` 從 Windows Update 重取。 +- **風險面**:移除「Nonremovable」AppX、刪 CBS package 屬於侵入性操作,可能導致未來 Windows Update 失敗或特定功能殘缺。部署前建議先在 VM 跑過一次。 + +### Telegram 補充 + +- **要有網路**:OOBE 裡 `SkipMachineOOBE=true` 跳過了網路設定精靈,所以目標機必須有 Ethernet+DHCP,或本身已記住 Wi-Fi。 +- **網路等待上限 60 秒**:`Setup.ps1` 開頭的 `wait-network` 跑一次 `Test-Connection 8.8.8.8` 迴圈,最多等 30 回合×2 秒;ICMP 被擋住的環境會等完整 60 秒但 HTTPS 其實可用,也能跑下去。 +- **per-user 安裝**:Telegram 安裝程式預設裝到 Admin 這個帳號底下,**新建的 `User` 看不到**。要全機安裝就在 `Setup.ps1` 的 `install-telegram` 段把 `ArgumentList` 加上 `'/ALLUSERS'`。 +- **抓的是 latest 版**:每次部署抓當下最新版。要鎖版本改用 `https://updates.tdesktop.com/tsetup/tsetup-x64.X.Y.Z.exe` 直鏈。 --- @@ -168,7 +219,8 @@ notepad C:\Windows\Temp\firstlogon-sshd.log 沿用 repo 預設值,下列情況會失敗,改用前要先處理: - **目標機若是 BIOS/MBR**:`<DiskConfiguration>` 目前寫死 UEFI+GPT,分割階段會報錯。 -- **ARM64 機器**:所有 `<component>` 的 `processorArchitecture="amd64"` 要改成 `arm64`。 -- **`C:\Scripts\Setup.ps1` 未提供**:`FirstLogonCommands` 會在首次登入時報「找不到檔案」。解法:在 `FirstLogonCommands` 前段加一條從 USB 複製的指令,或另外烘進 image,或乾脆刪掉 `Order=1` 那條 `SynchronousCommand`。 +- **ARM64 機器**:所有 `<component>` 的 `processorArchitecture="amd64"` 要改成 `arm64`;同時 `Setup.ps1` 內的 `OneDriveSetup.exe` SysWOW64 路徑、`winget` Chrome `--scope machine` 等部分行為要重驗。 +- **Ventoy USB 在 reboot 前被拔掉**:Order 4 會找不到 `Setup.ps1`,13 段 post-install 全部 skip。請等到第二次重開機進到登入畫面後才拔。 - **密碼是明碼**(`<PlainText>true</PlainText>`):僅適合測試用途;正式部署請用 Windows SIM 產 Base64。 -- **Telegram 安裝需要外網**:目標機沒網路的話,Order 4 會失敗但不影響整個流程走完。 +- **Phase 2 全部需要外網**:目標機沒網路的話,從 `wait-network` 之後 7 段都會 ERR;Phase 1 的 8 段(防火牆、RDP、ICMP、WinRM、OneDrive 移除、Cortana 關閉、Default profile UI/IME、改名隱藏帳號)仍會成功完成。 +- **公開 KMS 啟用**:`Setup.ps1` 預設用 `kms.digiboy.ir` 等第三方 KMS,這對內網/正式環境不適合。把 `$InternalKmsServers` 改成你自己的 KMS 主機就好。 diff --git a/QUICKSTART.md b/QUICKSTART.md index 976ebad..b7e3694 100644 --- a/QUICKSTART.md +++ b/QUICKSTART.md @@ -13,18 +13,25 @@ - [ ] Windows 10 或 11 官方 ISO(從 [microsoft.com](https://www.microsoft.com/software-download) 取得) - [ ] 已刷好 Ventoy 的 USB 隨身碟(≥ 16 GB 建議,因為會同時放 ISO 與 script) - [ ] 一台測試機或 VM(強烈建議先在 VM 驗證) -- [ ] 本專案的 `unattend.xml` 與 `ventoy.json` +- [ ] 本專案的 `unattend.xml`、`Setup.ps1`、`ventoy.json` **編輯 `unattend.xml` 的必改項目:** | 行為 | 原值 | 要改成 | |---|---|---| -| 產品序號 | `XXXXX-XXXXX-XXXXX-XXXXX-XXXXX` | 你的序號,或 KMS 通用金鑰,或整個 `<ProductKey>` 節點刪掉 | +| 產品序號 | Win11/10 Pro GVLK(`W269N-WFGWX-...`) | 你的序號,或留 GVLK 用 KMS,或整個 `<ProductKey>` 節點刪掉 | | 管理員密碼 | `P@ssw0rd!`(兩處:`LocalAccount` 與 `AutoLogon`) | 你的密碼 | | 組織名 | `YourCompany` | 你的組織 | -| 腳本路徑 | `C:\Scripts\Setup.ps1` | 留著或改路徑;不要執行就刪 `<SynchronousCommand Order=1>` | | `DiskID` | `$$VT_WINDOWS_DISK_1ST_NONVTOY$$`(Ventoy 變數) | **保留不動**。Ventoy 會在執行期替換成「第一顆非 Ventoy USB 的磁碟」 | +**編輯 `Setup.ps1` 的常改項目(在檔案頂部):** + +| 變數 | 預設 | 要改成 | +|---|---|---| +| `$InternalKmsServers` | `@()` 空陣列 | 你內網 KMS 主機,例:`@('kms.your-corp.com')`;空陣列就 fallback 到公開 KMS | +| `$UwpRemoveList` | 22 個 Microsoft 預載 app | 想多移/少移就改這個陣列 | +| `$Win11DebloatConfigJson` | toolkit 的預設(暗色、工作列靠左、停 Bing/Copilot/Recall…) | 不喜歡哪個 tweak 就把該行 `Value` 改成 `false` | + **編輯 `ventoy.json` 的必改項目:** | 欄位 | 原值 | 要改成 | @@ -45,7 +52,8 @@ └── ventoy/ ├── ventoy.json ← 本 repo 的 ventoy.json └── script/ - └── unattend.xml ← 本 repo 的 unattend.xml(改完的) + ├── unattend.xml ← 本 repo 的 unattend.xml(改完的) + └── Setup.ps1 ← 本 repo 的 Setup.ps1(首次登入會被自動複製到 C:\Scripts\) ``` macOS / Linux 範例: @@ -56,8 +64,11 @@ cp Windows.iso /Volumes/Ventoy/Win11.iso mkdir -p /Volumes/Ventoy/ventoy/script cp ventoy.json /Volumes/Ventoy/ventoy/ventoy.json cp unattend.xml /Volumes/Ventoy/ventoy/script/unattend.xml +cp Setup.ps1 /Volumes/Ventoy/ventoy/script/Setup.ps1 ``` +> **改 `Setup.ps1` 後記得重 `cp`**。`unattend.xml` 的 FirstLogonCommands 從 USB 上的 `/ventoy/script/Setup.ps1` 抓檔案,不是從 repo。沒同步的話部署到的 Windows 會跑舊版。 + > 不用改名成 `autounattend.xml`,也不用把 XML 塞進 ISO 根目錄。Ventoy 的 Auto Install plugin 會在開機時依 `ventoy.json` 把 XML 注入給 Windows Setup。 **多個 ISO / 多套範本**:`ventoy.json` 的 `auto_install` 是陣列,每個 ISO 一條目;`template` 也可以是陣列,開機時 Ventoy 會顯示選單讓你挑。 @@ -102,9 +113,9 @@ Ventoy 支援寫進 VHD/VDI,可直接掛進 Hyper-V / VirtualBox 當第一顆 | 第一次重開 | 黑屏 → 「請稍候」 | 1–2 分鐘 | | OOBE 略過 | **不該出現任何選單**;直接跳過 | 秒殺 | | AutoLogon | 以 `Admin` 自動登入到桌面(顯示名稱是「Administrator」) | — | -| FirstLogonCommands | 閃過一連串 PowerShell / cmd 黑視窗(`Setup.ps1`、建 user 帳號、裝 OpenSSH、裝 Telegram) | 1–3 分鐘(含 FoD 下載) | +| FirstLogonCommands | 閃過一連串 PowerShell / cmd 黑視窗(密碼永不過期、改主機名、建 user 帳號、從 USB 拷 Setup.ps1、跑 Setup.ps1 的 13 段 post-install) | 5–15 分鐘(含 FoD 下載、Win11Debloat 解壓、winget Chrome、AI 元件移除) | | 重開機 | 5 秒倒數後重啟 | — | -| 第二次開機 | 看到登入畫面,有 `Administrator`(= `Admin` 帳號)和 `user` 兩個選項 | — | +| 第二次開機 | 看到登入畫面,有 `User` 一個選項(`Admin` 已被 SpecialAccounts 隱藏;輸入帳號名仍可登入) | — | **正常就是全程不需要碰鍵盤滑鼠。** 如果中途卡在 OOBE 畫面要你選語系/建帳號 → 去看第 5 節除錯。 @@ -126,14 +137,23 @@ Ventoy 支援寫進 VHD/VDI,可直接掛進 Hyper-V / VirtualBox 當第一顆 reg query "HKLM\SOFTWARE\Microsoft\Windows NT\CurrentVersion\Winlogon" /v AutoAdminLogon ``` -3. **改密碼**:範本的 `Admin/P@ssw0rd!` 和 `user/1234` 都是給自動化用的,上線前換掉。`user` 登入時會被強制改密,`Admin` 要自己 `net user Admin *` 改。 +3. **改密碼**:範本的 `Admin/P@ssw0rd!` 和 `User/1234` 都是給自動化用的,上線前換掉。`User` 登入時會被強制改密,`Admin` 要自己 `net user Admin *` 改。 -4. **看一下 FirstLogon log**(本輪新增): +4. **看一下 FirstLogon / Setup log**: ```powershell - notepad C:\Windows\Temp\firstlogon-sshd.log # OpenSSH 安裝結果 - notepad C:\Windows\Temp\firstlogon-telegram.log # Telegram 下載與安裝結果 + notepad C:\Windows\Temp\firstlogon-copysetup.log # Order 4:Setup.ps1 是否從 Ventoy USB 複製成功 + notepad C:\Windows\Temp\firstlogon-runsetup.log # Order 5:Setup.ps1 找不到才會有此檔 + notepad C:\Windows\Temp\setup.log # Setup.ps1 主 log,13 段每段都寫 START/DONE/ERR + ``` + 找 `=== <name> ERR: ... ===` 行就是失敗段;`Setup.ps1` 每段都冪等,可以登入 `Admin` 後 `powershell -ExecutionPolicy Bypass -File C:\Scripts\Setup.ps1` 整支重跑,或挑特定段貼進視窗。 + +5. **驗證遠端管理可用**(從別台機器試): + ```bash + ping <機器 IP> # ICMP 應該回應 + ssh Admin@<機器 IP> # OpenSSH 22/TCP,密碼 P@ssw0rd!(金鑰要先放 administrators_authorized_keys) + # RDP:用任何 RDP client 連 <機器 IP>:3389 + # WinRM:5985(HTTP,AllowUnencrypted=true,TrustedHosts=*) ``` - `Exit 0` 才是真的成功。前一輪 Telegram 無聲失敗就是因為當時沒有 log、無從判斷。 --- @@ -163,7 +183,8 @@ Shift + F10 在任何安裝畫面會開出命令提示字元,可以 `notepad` | `ProductKey` 錯誤 | 序號無效 | 改正確序號、用 KMS 通用金鑰、或刪掉整個 `<ProductKey>` 節點 | | OOBE 沒略過 | Windows 版本不支援該欄位 | 比對當前版本 SIM 可用欄位 | | FirstLogonCommands 沒跑 | `AutoLogon` 沒設或 `Order` 重複 | 確認 `AutoLogon.Enabled=true` 且 `Order` 不重複 | -| `Setup.ps1` 找不到 | 路徑不存在 | 先用其他方式把腳本放到 `C:\Scripts\`,或改用 `FirstLogonCommands` 從 USB 複製 | +| `Setup.ps1` 找不到(`firstlogon-copysetup.log` 寫 `NOT FOUND`) | Ventoy USB 太早被拔掉,或 `Setup.ps1` 沒 cp 到 `/ventoy/script/` | 等到第二次重開機進登入畫面後才拔;或重新 `cp Setup.ps1 /Volumes/Ventoy/ventoy/script/` | +| `Setup.ps1` 跑了但某段 ERR | 看 `C:\Windows\Temp\setup.log` 的 `=== <name> ERR: ... ===` | 修一修對應段,登入 `Admin` 後 `powershell -ExecutionPolicy Bypass -File C:\Scripts\Setup.ps1` 整支重跑 | ### 驗證 XML 語法 diff --git a/README.md b/README.md index b2f2ca0..ac11a87 100644 --- a/README.md +++ b/README.md @@ -8,8 +8,11 @@ | 檔案 | 用途 | |---|---| -| `unattend.xml` | 主回應檔,放入 USB/ISO 時需改名為 `autounattend.xml` | +| `unattend.xml` | 主回應檔,部署到 Ventoy USB 的 `/ventoy/script/unattend.xml`(**不**改名為 `autounattend.xml`,Ventoy Auto Install plugin 走 `ventoy.json` 注入) | +| `Setup.ps1` | 首次登入後的 post-install PowerShell;部署到 USB 的 `/ventoy/script/Setup.ps1`,FirstLogonCommands 會掃磁碟自動複製到 `C:\Scripts\` 後執行 | +| `ventoy.json` | Ventoy Auto Install plugin 設定,把 ISO 對到 `unattend.xml` | | `QUICKSTART.md` | 操作步驟:如何產生 USB、掛載 ISO、跑 VM 測試 | +| `DEPLOY.md` | 本機 USB 實機部署紀錄+ FirstLogonCommands / Setup.ps1 詳細排查 | | `SUMMARY.md` | 本專案的來由與設計脈絡 | --- @@ -42,7 +45,7 @@ Windows Setup 在不同階段套用不同 `<settings pass="...">`: ### specialize - **Microsoft-Windows-Shell-Setup** - - `ComputerName` — `PC-%RAND:5%`(5 位隨機數字) + - `ComputerName` — `*`(Setup 自動產生隨機名,`FirstLogonCommands` 再用 `Rename-Computer` 改成 `PC-XXXXX`,重開後生效)。**不可寫 `PC-%RAND:5%`**——那是 MDT/SCCM 變數,直接放這裡會被當字面字串送進 NetBIOS 驗證而失敗 - `TimeZone` — `Taipei Standard Time` - `RegisteredOwner` / `RegisteredOrganization` @@ -54,13 +57,18 @@ Windows Setup 在不同階段套用不同 `<settings pass="...">`: - `UserAccounts.LocalAccounts` — 建立 `Admin`(Administrators 群組,密碼 `P@ssw0rd!`,明碼) - `AutoLogon` — 以 `Admin` 自動登入一次(`LogonCount=1`) - `FirstLogonCommands` — 按 `Order` 依序執行: - 1. `powershell.exe -ExecutionPolicy Bypass -File C:\Scripts\Setup.ps1` - 2. `cmd.exe /c net accounts /maxpwage:unlimited`(密碼永不過期) - 3. `Rename-Computer` 改主機名成 `PC-XXXXX`(重開後生效) - 4. 建立本機帳號 `user`(密碼 `1234`,`net user /logonpasswordchg:yes` 強制首次登入改密) - 5. 等網路→`Add-WindowsCapability OpenSSH.Server` 安裝並 `Set-Service sshd Automatic` / `Start-Service sshd`(log 寫到 `C:\Windows\Temp\firstlogon-sshd.log`) - 6. 等網路→下載 `https://telegram.org/dl/desktop/win64` 並以 `/VERYSILENT /SUPPRESSMSGBOXES /NORESTART` 靜默安裝 Telegram(log 寫到 `C:\Windows\Temp\firstlogon-telegram.log`) - 7. `shutdown /r /t 5`(重開機) + 1. `cmd.exe /c net accounts /maxpwage:unlimited`(密碼永不過期) + 2. `Rename-Computer` 改主機名成 `PC-XXXXX`(重開後生效) + 3. 建立本機帳號 `user`(密碼 `1234`,`net user /logonpasswordchg:yes` 強制首次登入改密;之後 `Setup.ps1` 會把它改名成 `User`) + 4. 掃所有掛載中的 FileSystem PSDrive 找 `\ventoy\script\Setup.ps1`,複製到 `C:\Scripts\Setup.ps1`(log 寫到 `C:\Windows\Temp\firstlogon-copysetup.log`) + 5. 跑 `C:\Scripts\Setup.ps1`(13 段 post-install 詳見 `Setup.ps1` 與 `DEPLOY.md`,log 寫到 `C:\Windows\Temp\setup.log`;找不到檔案時改寫 `firstlogon-runsetup.log` 並跳過) + 6. `shutdown /r /t 5`(重開機) + +### `Setup.ps1` 涵蓋的 13 段(首次登入由 Order 5 執行) + +**Phase 1(不需網路):** SSH 22/TCP 防火牆、RDP+3389、ICMP Echo Inbound、WinRM(`TrustedHosts=*`)、OneDrive 解除安裝、Cortana/Web Search 關閉、Default profile 套 Dark mode+顯示副檔名/隱藏檔+工作列靠左+預設英數鍵盤+停 IME 切換熱鍵、`user`→`User` 改名+從登入畫面隱藏 `Admin`。 + +**Phase 2(需網路,`wait-network` 後執行):** OpenSSH Server FoD、KMS 啟用(先試 `$InternalKmsServers` 再 fallback 公開 KMS)、winget 裝 Chrome、`zoicware/RemoveWindowsAI -AllOptions`、`Raphire/Win11Debloat -Silent -Config`、22 個 UWP AppxPackage 移除(與 `windows-remote-toolkit/config/remove-apps-config.json` 同步)、Telegram 靜默安裝、刪 `C:\Windows\Panther\unattend.xml` 殘留。 --- @@ -82,10 +90,10 @@ Windows Setup 在不同階段套用不同 `<settings pass="...">`: | 方式 | 位置 | 檔名 | |---|---|---| -| **Ventoy Auto Install plugin(本 repo 預設)** | Ventoy USB 的 `/ventoy/script/`,並在 `/ventoy/ventoy.json` 指定 | 任意(本 repo 用 `unattend.xml`) | -| 傳統 USB 安裝碟 | USB 根目錄 | `autounattend.xml` | -| ISO `sources\` 目錄 | ISO 根 | `autounattend.xml` | -| DISM 離線注入已掛載映像 | — | 任意名稱,用 `/Apply-Unattend:` 指定 | +| **Ventoy Auto Install plugin(本 repo 預設)** | Ventoy USB 的 `/ventoy/script/unattend.xml`+`/ventoy/script/Setup.ps1`,並在 `/ventoy/ventoy.json` 指定 | 任意(本 repo 用 `unattend.xml`) | +| 傳統 USB 安裝碟 | USB 根目錄 | `autounattend.xml`(注意:FirstLogonCommands Order 4 的磁碟掃描還是會找 `\ventoy\script\Setup.ps1`,要嘛把 `Setup.ps1` 放對位置,要嘛改 Order 4 的搜尋路徑) | +| ISO `sources\` 目錄 | ISO 根 | `autounattend.xml`(同上) | +| DISM 離線注入已掛載映像 | — | 任意名稱,用 `/Apply-Unattend:` 指定(這條路徑下 `Setup.ps1` 通常會直接烘進 image 而不再走 USB 掃描) | --- @@ -99,11 +107,9 @@ Windows Setup 在不同階段套用不同 `<settings pass="...">`: ## 安全性 - 本範本密碼為明碼,僅適合內網測試;生產環境請改 Base64 或交由 MDT/SCCM 管理。 -- 部署結束後應刪除殘留的 unattend 檔,例如在 `Setup.ps1` 尾端: - ```powershell - Remove-Item C:\Windows\Panther\unattend.xml -Force -ErrorAction SilentlyContinue - Remove-Item C:\Windows\Panther\Unattend\unattend.xml -Force -ErrorAction SilentlyContinue - ``` +- `Setup.ps1` 的 `cleanup-unattend-files` 段已經會在收尾時刪掉 `C:\Windows\Panther\unattend.xml` 與 `Panther\Unattend\unattend.xml`(含明碼密碼)。 +- WinRM `AllowUnencrypted=true` + `TrustedHosts=*` 是測試環境設定;正式環境要改 HTTPS(5986)+ 限制 TrustedHosts。 +- KMS 預設 fallback 到第三方公開伺服器(`kms.digiboy.ir` 等);正式環境改 `Setup.ps1` 的 `$InternalKmsServers`。 --- diff --git a/SUMMARY.md b/SUMMARY.md index 1905454..dee4df9 100644 --- a/SUMMARY.md +++ b/SUMMARY.md @@ -44,20 +44,46 @@ XML 能做的事有上限。超過這條線(裝軟體、設 Registry、拉 Git `FirstLogonCommands` 就是交棒點: ``` -Order 1 → Setup.ps1 (所有「後續設定」的預留入口) -Order 2 → net accounts (一行搞定的雜事直接 cmd) -Order 3 → Rename-Computer (主機名改 PC-XXXXX,重開後生效) -Order 4 → net user user 1234 /add (建立一般使用者,強制首次登入改密) -Order 5 → Add-WindowsCapability OpenSSH (等網路→安裝並啟動 sshd) -Order 6 → Telegram 靜默安裝 (等網路→下載 tsetup.exe + /VERYSILENT) -Order 7 → shutdown /r (重開機收尾,讓 rename 生效) +Order 1 → net accounts (一行搞定的雜事直接 cmd,密碼永不過期) +Order 2 → Rename-Computer (主機名改 PC-XXXXX,重開後生效) +Order 3 → net user user 1234 /add (建立一般使用者,強制首次登入改密) +Order 4 → Copy Setup.ps1 from Ventoy USB (掃磁碟找 \ventoy\script\Setup.ps1 → C:\Scripts\) +Order 5 → powershell -File Setup.ps1 (13 段 post-install,看下表) +Order 6 → shutdown /r (重開機收尾,讓 rename 生效) ``` -這讓 `unattend.xml` 保持「只負責 Windows 本身的安裝」,其他事情外包給腳本。職責分離之後,改軟體清單不需要動 XML,改部署流程也不需要動 Setup 邏輯。 +`Setup.ps1` 內部分兩個 phase(用空的 `wait-network` 段切開): -> Order 5、6(OpenSSH、Telegram)是「交棒到腳本」這條原則的例外:**單一套件、需求穩定**的情況下,直接在 `FirstLogonCommands` 裡下載安裝比起硬塞進 `Setup.ps1` 單純。一旦要裝的東西變成清單(3+ 個套件、要版本鎖定、要錯誤處理),就該整批移進 `Setup.ps1` 或改用 winget/choco。 +``` +[Phase 1:本機設定,不需網路] +firewall-ssh-22 → SSH 22/TCP inbound 防火牆規則 +rdp-enable-3389 → 啟用 RDP + 3389 防火牆群組 +icmp-echo-allow → ICMPv4 Echo Request inbound(外部 ping 得到) +winrm-enable → Enable-PSRemoting + winrm quickconfig + TrustedHosts=* +onedrive-remove → /uninstall + 清啟動鍵 + 移除 CLSID +cortana-websearch-off → AllowCortana=0、DisableWebSearch=1、SearchBoxSuggestions=1 +apply-ui-ime-default-profile → reg load Default profile,套 Dark mode / 工作列靠左 / 顯示副檔名 / 預設英數鍵盤 / 停 IME 切換熱鍵 +rename-hide-accounts → user → User,從登入畫面隱藏 Admin + +[Phase 2:需要網路] +wait-network → 等 ICMP 通到 8.8.8.8,上限 60 秒 +openssh-server-install → Add-WindowsCapability OpenSSH.Server + Start-Service sshd +kms-activate → 試 $InternalKmsServers → $PublicKmsServers,第一個 successful 就停 +winget-install-chrome → winget install Google.Chrome --silent --scope machine +remove-windows-ai → irm zoicware/RemoveWindowsAI | iex -nonInteractive -AllOptions +win11debloat → 下載 Raphire/Win11Debloat → -Silent -Config +remove-uwp-apps → 22 個 AppxPackage(BingNews、Xbox*、MSTeams、Photos…) +install-telegram → 下載 telegram.org/dl/desktop/win64 + /VERYSILENT +cleanup-unattend-files → 刪 C:\Windows\Panther\unattend.xml(清明碼密碼殘留) +``` + +這讓 `unattend.xml` 保持「只負責 Windows 本身的安裝+簡單到不會壞的 cmd」,其他事情外包給 `Setup.ps1`。職責分離之後,改軟體清單不需要動 XML,改部署流程也不需要動 Setup 邏輯。 + +> 為什麼把 `Setup.ps1` 放在 Ventoy USB 上、不直接 base64 內嵌進 `unattend.xml`:(1) `unattend.xml` 仍是 Windows SIM 可驗證的純 schema XML;(2) `Setup.ps1` 改完只要 `cp` 一次就更新,不用重 escape;(3) 每段 `Section` 寫到 `setup.log` 才方便排查。代價是:USB 必須留到第一次登入完成才能拔。 > -> Order 5、6 共用「等 ICMP 通到 8.8.8.8、最多 60 秒」的網路等待迴圈,並且各自把結果寫到 `C:\Windows\Temp\firstlogon-*.log`,避免前一輪 Telegram 無聲失敗的情況再發生。 +> 為什麼是「先建小寫 user 再改 User」:`unattend.xml` 的 `net user user 1234 /add` 鐵打不會壞;`Setup.ps1` 的 `Rename-LocalUser` 萬一沒跑完,最差結果是有個小寫 `user` 還能登入。先建後改 = bulletproof。 +> +> `remove-windows-ai` 跑的是 `main` 分支的腳本,**每次部署抓的版本可能不同**。要確定性、可稽核的部署,把 `Setup.ps1` 中對應 URL 的 `/main/` 換成 commit SHA。 --- @@ -85,7 +111,8 @@ Windows Setup 有七個 pass,本檔只用三個。不是偷懶,是**三個 p | **本機帳號 + AutoLogon** | 純離線可跑 | 改成 Domain Join + 網域管理員登入 | | **ProtectYourPC=3** | 略過 OOBE 的隱私頁,預設行為 | 依公司政策調整,或用 GPO 蓋掉 | | **單一 Disk 0 + 整碟清空** | 對測試 VM 最安全的假設 | 多硬碟環境要加 `DiskID` 判斷邏輯 | -| **`Setup.ps1` 路徑硬寫 C:\Scripts\** | 簡單 | 改從 USB/網路拉,或用 MDT 的 `Applications` 機制 | +| **`Setup.ps1` 用磁碟掃描從 Ventoy USB 拷貝** | 不需要架網路服務、跨網段也能跑、USB 拔掉也能在現場補救 | 內部 IT 場合改用 SCCM/Intune 派送,或乾脆 base64 內嵌進 XML 拿到完全自包含 | +| **`$PublicKmsServers` 用第三方 KMS** | 沒授權主機也能跑通 | 把 `$InternalKmsServers` 改成自家 KMS 主機,公開那串不再被 fallback | 這些不是 bug,是**當前脈絡下的合理起點**。要產品化就對照這張表一個一個升級。 diff --git a/Setup.ps1 b/Setup.ps1 new file mode 100644 index 0000000..715df22 --- /dev/null +++ b/Setup.ps1 @@ -0,0 +1,340 @@ +# 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) +# +# 順序:先做不需網路的本機設定(防火牆 / 註冊機碼 / Default profile), +# 再等網路,最後跑下載安裝(OpenSSH FoD / KMS / winget / RemoveAI / Win11Debloat / Telegram)。 + +$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 的設定 JSON(與 toolkit/config/win11debloat-config.json 同步) +$Win11DebloatConfigJson = @' +{ + "Version": "1.0", + "Tweaks": [ + { "Name": "RemoveApps", "Value": true }, + { "Name": "RemoveGamingApps", "Value": true }, + { "Name": "RemoveCommApps", "Value": true }, + { "Name": "RemoveW11Outlook", "Value": true }, + { "Name": "DisableTelemetry", "Value": true }, + { "Name": "DisableSearchHistory", "Value": true }, + { "Name": "DisableBing", "Value": true }, + { "Name": "DisableStoreSearchSuggestions", "Value": true }, + { "Name": "DisableSearchHighlights", "Value": true }, + { "Name": "DisableLockscreenTips", "Value": true }, + { "Name": "DisableSuggestions", "Value": true }, + { "Name": "DisableDesktopSpotlight", "Value": true }, + { "Name": "DisableFindMyDevice", "Value": true }, + { "Name": "DisableEdgeAds", "Value": true }, + { "Name": "DisableBraveBloat", "Value": true }, + { "Name": "DisableSettings365Ads", "Value": true }, + { "Name": "DisableSettingsHome", "Value": true }, + { "Name": "DisableCopilot", "Value": true }, + { "Name": "DisableRecall", "Value": true }, + { "Name": "DisableClickToDo", "Value": true }, + { "Name": "DisableAISvcAutoStart", "Value": true }, + { "Name": "DisablePaintAI", "Value": true }, + { "Name": "DisableNotepadAI", "Value": true }, + { "Name": "DisableEdgeAI", "Value": true }, + { "Name": "PreventUpdateAutoReboot", "Value": true }, + { "Name": "DisableModernStandbyNetworking", "Value": true }, + { "Name": "DisableDVR", "Value": true }, + { "Name": "DisableGameBarIntegration", "Value": true }, + { "Name": "ShowHiddenFolders", "Value": true }, + { "Name": "ShowKnownFileExt", "Value": true }, + { "Name": "EnableDarkMode", "Value": true }, + { "Name": "TaskbarAlignLeft", "Value": true }, + { "Name": "HideSearchTb", "Value": true }, + { "Name": "HideTaskview", "Value": true }, + { "Name": "HideChat", "Value": true }, + { "Name": "DisableWidgets", "Value": true }, + { "Name": "DisableStartRecommended", "Value": true }, + { "Name": "Hide3dObjects", "Value": true }, + { "Name": "ExplorerToThisPC", "Value": true }, + { "Name": "HideGallery", "Value": true }, + { "Name": "DisableDragTray", "Value": true } + ], + "Deployment": [ + { "Name": "CreateRestorePoint", "Value": true }, + { "Name": "RestartExplorer", "Value": false }, + { "Name": "AppRemovalScopeIndex", "Value": 0 } + ] +} +'@ + +# ---- 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 +} + +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 '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 'rename-hide-accounts' { + $u = Get-LocalUser -Name 'user' -ErrorAction SilentlyContinue + if ($u -and $u.Name -ceq 'user') { + Rename-LocalUser -Name 'user' -NewName 'User' + Log 'Renamed user -> User' + } else { + Log "Local user 'user' not found or already renamed; skip" + } + + $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 '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 diff --git a/unattend.xml b/unattend.xml index 551a562..ca168a1 100644 --- a/unattend.xml +++ b/unattend.xml @@ -148,44 +148,45 @@ </Password> </AutoLogon> - <!-- 首次登入執行的自訂腳本 --> + <!-- 首次登入執行的自訂腳本 + + 前 4 條為「不需網路、可獨立成功」的本機指令; + Order 5 從 Ventoy USB 把 Setup.ps1 複製到 C:\Scripts\, + Order 6 跑 Setup.ps1(13 項 post-install 動作集中在那裡), + Order 7 重開機。 + + 各條都會寫獨立 log 到 C:\Windows\Temp\,方便排查無聲失敗。 --> <FirstLogonCommands> <SynchronousCommand wcm:action="add"> <Order>1</Order> - <CommandLine>powershell.exe -ExecutionPolicy Bypass -File C:\Scripts\Setup.ps1</CommandLine> - <Description>執行部署後設定腳本</Description> - <RequiresUserInput>false</RequiresUserInput> - </SynchronousCommand> - <SynchronousCommand wcm:action="add"> - <Order>2</Order> <CommandLine>cmd.exe /c net accounts /maxpwage:unlimited</CommandLine> <Description>密碼永不過期</Description> </SynchronousCommand> <SynchronousCommand wcm:action="add"> - <Order>3</Order> + <Order>2</Order> <CommandLine>powershell.exe -NoProfile -Command "Rename-Computer -NewName ('PC-' + (Get-Random -Minimum 10000 -Maximum 100000)) -Force"</CommandLine> <Description>主機名改成 PC-XXXXX(重開後生效)</Description> </SynchronousCommand> <SynchronousCommand wcm:action="add"> - <Order>4</Order> + <Order>3</Order> <CommandLine>cmd.exe /c "net user user 1234 /add &amp; net user user /logonpasswordchg:yes"</CommandLine> - <Description>建立本機帳號 user(密碼 1234,首次登入強制改密)</Description> + <Description>建立本機帳號 user(密碼 1234,首次登入強制改密;之後 Setup.ps1 會把它改名成 User)</Description> + <RequiresUserInput>false</RequiresUserInput> + </SynchronousCommand> + <SynchronousCommand wcm:action="add"> + <Order>4</Order> + <CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$log='C:\Windows\Temp\firstlogon-copysetup.log'; New-Item -ItemType Directory -Force -Path C:\Scripts | Out-Null; $src=$null; foreach ($d in (Get-PSDrive -PSProvider FileSystem -EA 0).Root) { $cand=Join-Path $d 'ventoy\script\Setup.ps1'; if (Test-Path $cand) { $src=$cand; break } }; if ($src) { Copy-Item $src 'C:\Scripts\Setup.ps1' -Force; ('OK from ' + $src + ' at ' + (Get-Date -Format o)) | Out-File $log -Append } else { ('NOT FOUND at ' + (Get-Date -Format o)) | Out-File $log -Append }"</CommandLine> + <Description>從 Ventoy USB 的 \ventoy\script\Setup.ps1 複製到 C:\Scripts\(log: firstlogon-copysetup.log)</Description> <RequiresUserInput>false</RequiresUserInput> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>5</Order> - <CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$log='C:\Windows\Temp\firstlogon-sshd.log'; $i=0; while ($i -lt 30 -and -not (Test-Connection 8.8.8.8 -Count 1 -Quiet -EA 0)) { Start-Sleep 2; $i++ }; ('NetWait=' + ($i*2) + 's') | Out-File $log -Append; try { Add-WindowsCapability -Online -Name OpenSSH.Server~~~~0.0.1.0 -EA Stop | Out-Null; Set-Service sshd -StartupType Automatic -EA Stop; Start-Service sshd -EA Stop; ('OK ' + (Get-Date -Format o)) | Out-File $log -Append } catch { ('ERR ' + $_.Exception.Message) | Out-File $log -Append }"</CommandLine> - <Description>等網路→安裝並啟用 OpenSSH Server(log: firstlogon-sshd.log)</Description> + <CommandLine>cmd.exe /c "if exist C:\Scripts\Setup.ps1 (powershell.exe -NoProfile -ExecutionPolicy Bypass -File C:\Scripts\Setup.ps1) else (echo SETUP_NOT_FOUND %DATE% %TIME% &gt; C:\Windows\Temp\firstlogon-runsetup.log)"</CommandLine> + <Description>執行 Setup.ps1(13 項 post-install:SSH/RDP/WinRM/KMS/Chrome/Debloat/Telegram/RemoveAI/UI/IME/重命名帳號…,log: C:\Windows\Temp\setup.log)</Description> <RequiresUserInput>false</RequiresUserInput> </SynchronousCommand> <SynchronousCommand wcm:action="add"> <Order>6</Order> - <CommandLine>powershell.exe -NoProfile -ExecutionPolicy Bypass -Command "$log='C:\Windows\Temp\firstlogon-telegram.log'; [Net.ServicePointManager]::SecurityProtocol=[Net.SecurityProtocolType]::Tls12; $ProgressPreference='SilentlyContinue'; $p=Join-Path $env:TEMP 'tsetup.exe'; $i=0; while ($i -lt 30 -and -not (Test-Connection 8.8.8.8 -Count 1 -Quiet -EA 0)) { Start-Sleep 2; $i++ }; ('NetWait=' + ($i*2) + 's') | Out-File $log -Append; try { ('Start ' + (Get-Date -Format o)) | Out-File $log -Append; Invoke-WebRequest -UseBasicParsing -Uri 'https://telegram.org/dl/desktop/win64' -OutFile $p; ('Downloaded ' + (Get-Item $p).Length + 'B') | Out-File $log -Append; $proc=Start-Process -FilePath $p -ArgumentList '/VERYSILENT','/SUPPRESSMSGBOXES','/NORESTART' -Wait -PassThru; ('Exit ' + $proc.ExitCode) | Out-File $log -Append } catch { ('ERR ' + $_.Exception.Message) | Out-File $log -Append }"</CommandLine> - <Description>下載並靜默安裝 Telegram Desktop(log: firstlogon-telegram.log)</Description> - <RequiresUserInput>false</RequiresUserInput> - </SynchronousCommand> - <SynchronousCommand wcm:action="add"> - <Order>7</Order> <CommandLine>shutdown /r /t 5</CommandLine> <Description>完成後重開機</Description> </SynchronousCommand>