新增 Setup.ps1(13 段 post-install);FirstLogonCommands 改從 Ventoy USB 拷腳本

把分散在 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) <noreply@anthropic.com>
This commit is contained in:
2026-04-27 13:36:13 +08:00
parent b3329d4889
commit c84e18bd9d
7 changed files with 565 additions and 108 deletions

View File

@@ -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 `<component>`. ARM64 deployments require a global find-and-replace to `arm64`.
- **Password is plaintext** (`<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.

142
DEPLOY.md
View File

@@ -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 個 AppxPackageBingNews/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 ServerOrder 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` 然後跳過。原因通常是:
### TelegramOrder 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 4USB 掃描結果
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系統EdgeOffice、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 段都會 ERRPhase 1 的 8 段防火牆、RDP、ICMP、WinRM、OneDrive 移除、Cortana 關閉、Default profile UI/IME、改名隱藏帳號仍會成功完成。
- **公開 KMS 啟用**`Setup.ps1` 預設用 `kms.digiboy.ir` 等第三方 KMS這對內網/正式環境不適合。把 `$InternalKmsServers` 改成你自己的 KMS 主機就好。

View File

@@ -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 當第一顆
| 第一次重開 | 黑屏 → 「請稍候」 | 12 分鐘 |
| OOBE 略過 | **不該出現任何選單**;直接跳過 | 秒殺 |
| AutoLogon | 以 `Admin` 自動登入到桌面顯示名稱是「Administrator」 | — |
| FirstLogonCommands | 閃過一連串 PowerShell / cmd 黑視窗(`Setup.ps1`、建 user 帳號、裝 OpenSSH、裝 Telegram | 13 分鐘(含 FoD 下載 |
| FirstLogonCommands | 閃過一連串 PowerShell / cmd 黑視窗(密碼永不過期、改主機名、建 user 帳號、從 USB 拷 Setup.ps1、跑 Setup.ps1 的 13 段 post-install | 515 分鐘(含 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 4Setup.ps1 是否從 Ventoy USB 複製成功
notepad C:\Windows\Temp\firstlogon-runsetup.log # Order 5Setup.ps1 找不到才會有此檔
notepad C:\Windows\Temp\setup.log # Setup.ps1 主 log13 段每段都寫 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
# WinRM5985HTTPAllowUnencrypted=trueTrustedHosts=*
```
`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 語法

View File

@@ -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` 靜默安裝 Telegramlog 寫到 `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=*` 是測試環境設定;正式環境要改 HTTPS5986+ 限制 TrustedHosts。
- KMS 預設 fallback 到第三方公開伺服器(`kms.digiboy.ir` 等);正式環境改 `Setup.ps1``$InternalKmsServers`
---

View File

@@ -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、6OpenSSH、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 個 AppxPackageBingNews、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是**當前脈絡下的合理起點**。要產品化就對照這張表一個一個升級。

340
Setup.ps1 Normal file
View File

@@ -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

View File

@@ -148,44 +148,45 @@
</Password>
</AutoLogon>
<!-- 首次登入執行的自訂腳本 -->
<!-- 首次登入執行的自訂腳本
前 4 條為「不需網路、可獨立成功」的本機指令;
Order 5 從 Ventoy USB 把 Setup.ps1 複製到 C:\Scripts\
Order 6 跑 Setup.ps113 項 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 Serverlog: 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.ps113 項 post-installSSH/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 Desktoplog: firstlogon-telegram.log</Description>
<RequiresUserInput>false</RequiresUserInput>
</SynchronousCommand>
<SynchronousCommand wcm:action="add">
<Order>7</Order>
<CommandLine>shutdown /r /t 5</CommandLine>
<Description>完成後重開機</Description>
</SynchronousCommand>