# 如何在 macOS 使用 Windows App CLI 自動化遠端桌面連線

最近我在整理 macOS 上的 VPN 與遠端桌面連線流程，希望把每天都會重複操作的步驟，整合成幾個簡單的 `make` 命令。VPN 可以透過命令列連線，那麼平常用來連接 Windows 主機的 **Windows App**，是不是也有 CLI 可以用？能不能順便帶入帳號密碼，讓我不用每次都重新輸入？

查了一下，Windows App for macOS 還真的有內建 CLI，而且可以管理遠端桌面書籤、帳號密碼、Remote Desktop Gateway 與資源訂閱。不過，要把這些功能串成一個完整的自動化流程，有幾個地方需要先弄清楚。尤其是「儲存連線設定」、「開啟遠端桌面」與「登入遠端主機」，這三件事分別由不同的功能負責，不能只看見 `--password` 參數，就以為後面的事情都處理好了。

這篇文章就來整理 Windows App CLI 的使用方式，以及我從 RDP URI 開始嘗試，最後改成 Makefile 搭配 macOS UI 自動化的過程。中間看似已經完成的幾個步驟，實際連線時卻不如預期，這也是這次最值得記錄的地方。

![Mac 終端機透過 Windows App 連接兩個遠端 Windows 桌面，並以鑰匙圖示呈現儲存認證的概念](https://stwillblogassets.blob.core.windows.net/files/images/2026/10/windows-app-cli/windows-app-cli-banner.webp)

> 本文以 **Windows App for macOS 11.4.1** 為測試版本。命令列範例使用 macOS 的 zsh，最終連線腳本使用 JavaScript for Automation（JXA），由 `osascript` 執行。文中的主機名稱、網域與帳號都是示意資料，使用時請換成自己的環境。

### 先確認你要自動化的是哪一種連線

Windows App 可以連接一般 Windows 電腦，也可以連接 Azure Virtual Desktop、Windows 365、Microsoft Dev Box 等服務。但這不表示每一種連線，都能使用相同的 CLI 參數登入。

如果你要連的是一台已經知道主機名稱或 IP 位址的 Windows 電腦，通常會用到本文的 `bookmark` 模組。這種遠端 PC 連線不需要先在 Windows App 登入 Microsoft 工作或學校帳戶，遠端主機的帳號密碼可以另外設定。詳見 [Windows App 連線說明](https://learn.microsoft.com/en-us/windows-app/get-started-connect-devices-desktops-apps)。

如果你要做的是 Azure Virtual Desktop、Windows 365 或 Microsoft Dev Box 的帳戶登入，則不能把本文的 `--username` 與 `--password` 當成雲端服務登入 API。微軟在 [macOS CLI 文件](https://learn.microsoft.com/en-us/windows-app/cli-macos#module-tips) 特別說明，`feed` 模組只支援使用帳號密碼認證的 Remote Desktop Services 資源訂閱，不能用來新增上述採用宣告式認證的雲端資源。

還有一件事：本文的目標是從終端機**啟動圖形化的遠端桌面工作階段**。連線建立後，畫面仍然會出現在 Windows App 裡面。如果你的需求是登入 Windows 後執行命令，應該另外研究 PowerShell Remoting 或 SSH，這不是 Windows App 這組 CLI 提供的功能。

### 找到 Windows App 的 CLI 入口

Windows App 並沒有額外安裝一個可以直接輸入的 `windows-app` 命令。它的 CLI 就藏在 App 套件的執行檔裡面：

```bash
"/Applications/Windows App.app/Contents/MacOS/Windows App" --script
```

由於路徑裡面有空白，記得加上引號。

為了讓後面的命令短一點，我會先在目前的 Shell 定義一個變數：

```bash
winapp="/Applications/Windows App.app/Contents/MacOS/Windows App"
```

之後就可以這樣執行：

```bash
"$winapp" --script
```

這裡的 `winapp` 只是我自己定義的 Shell 變數，不是另一套需要安裝的工具。以下範例都假設你已經先定義好這個變數。

目前這個版本會列出四個模組：

| 模組  | 用途  |
| --- | --- |
| `bookmark` | 建立、修改、列出、匯出與刪除遠端桌面書籤 |
| `feed` | 管理 Remote Desktop Services 資源訂閱 |
| `gateway` | 管理 Remote Desktop Gateway 設定 |
| `defaults` | 列出可以透過 macOS`defaults` 命令調整的用戶端設定 |

完整語法可以從 [微軟的 CLI 文件](https://learn.microsoft.com/en-us/windows-app/cli-macos) 找到入口。不過，真正要寫腳本時，我比較建議直接查目前安裝版本的 `help`，因為每個模組還有下一層的命令與參數。

```bash
# 查看 bookmark 模組支援哪些命令
"$winapp" --script bookmark help

# 查看建立或修改書籤的完整參數
"$winapp" --script bookmark write help

# 查看匯出書籤的參數
"$winapp" --script bookmark export help
```

💡 這組 CLI 使用的是 `help` 子命令。若要查看 `write` 的參數，就把 `help` 放在 `write` 後面。

### 使用 bookmark 管理遠端桌面連線

Windows App 裡面看到的遠端 PC，在這組 CLI 裡稱為 **Bookmark**。基本語法如下：

```
--script bookmark <command> <unique ID> <parameters>
```

其中 `<unique ID>` 是書籤的識別碼，不是主機名稱，也不是顯示在畫面上的連線名稱。

#### 列出已經儲存的連線

```bash
"$winapp" --script bookmark list
```

先用這個命令找到你要操作的書籤 ID，再帶入後面的修改、匯出或刪除命令。

#### 建立一個新的連線

```bash
bookmark_id="$(uuidgen)"

"$winapp" --script bookmark write "$bookmark_id" \
  --hostname 'pc-a.example.com' \
  --friendlyname '開發主機 A' \
  --group 'Work PCs' \
  --fullscreen false \
  --resolution '1600 900' \
  --dynamicdisplay true
```

幾個值得注意的地方：

-   `--hostname` 是新建書籤時需要的主機資訊，可以是主機名稱或 IP 位址。
-   `--friendlyname` 是你在 Windows App 看到的名稱。
-   `--group` 指定書籤群組；群組不存在時會建立。
-   `--resolution` 的寬高用空白分隔，而且必須包在同一組引號裡面。
-   `--dynamicdisplay true` 用來啟用動態解析度，遠端系統也必須支援。

這段命令執行完，是把連線設定存進 Windows App，還沒有開始連線。

#### 修改既有連線

`write` 同時負責新增與修改。只要沿用相同 ID，就會更新同一個書籤：

```bash
"$winapp" --script bookmark write "$bookmark_id" \
  --friendlyname '開發主機 A - 視窗模式' \
  --fullscreen false
```

如果你要把命令寫進每天執行的腳本，就不要每次都用 `uuidgen` 產生新的 ID，否則會一直新增書籤。後面的完整範例會用固定輸入產生固定 UUID，讓重複執行可以更新同一筆資料。

#### 設定帳號密碼

`bookmark write` 支援 `--username` 與 `--password`。以下是在 zsh 先讀取一次密碼，再交給 Windows App 儲存的寫法：

```bash
read -rs 'rdp_password?請輸入遠端桌面密碼：'
printf '\n'

"$winapp" --script bookmark write "$bookmark_id" \
  --username 'CONTOSO\alice' \
  --password "$rdp_password"

unset rdp_password
```

這裡的 `read -rs` 是 **zsh 語法**，不要直接拿到 Bash 當成相同用法。

根據本機 `help` 的說明，Windows App 會嘗試尋找既有帳號，找不到時才建立。因此，如果你在多個連線中使用相同的帳號名稱，要留意它們是否共用同一筆儲存認證，尤其是「帳號名稱一樣，密碼卻不同」的環境。

另外，使用 Shell 變數可以避免把真正的密碼直接寫進命令歷史，但密碼仍然會作為 `--password` 的程序參數傳入 Windows App。這組 CLI 並不是透過 stdin 接收密碼，兩者的差別要知道。

#### 匯出成 RDP 檔案

```bash
"$winapp" --script bookmark export "$bookmark_id" > ./server.rdp
```

預設輸出是 RDP 設定格式。如果想要輸出成 URI，可以加上 `--uri`：

```bash
"$winapp" --script bookmark export "$bookmark_id" --uri
```

這兩個命令都把內容寫到標準輸出。只有第一個範例因為加了 `>`，才會建立檔案。

#### 刪除書籤

```bash
"$winapp" --script bookmark delete "$bookmark_id"
```

刪除的對象是指定書籤，不是遠端主機。至於儲存的帳號密碼是否還被其他連線使用，應另外到 Windows App 的設定確認，不要把刪除書籤當成完整的認證清除程序。

### bookmark write 的參數整理

以下依據本機 Windows App 11.4.1 的 `bookmark write help` 整理。大部分的開關使用 `true` 或 `false`，但剪貼簿與音訊選項使用數字，這兩種不要混用。

#### 主機、帳號與閘道

| 參數  | 說明  |
| --- | --- |
| `--hostname <string>` | 遠端主機名稱或位址 |
| `--username <string>` | 登入遠端主機的帳號 |
| `--password <string>` | 該帳號的密碼 |
| `--friendlyname <string>` | 連線顯示名稱 |
| `--group <string>` | 書籤群組名稱 |
| `--gateway <ID>` | 使用既有 Gateway 的 ID；找不到該 ID 時會忽略此參數 |
| `--gatewayhostname <string>` | 依名稱尋找 Gateway，不存在時會建立；有多筆同名資料時會選第一筆 |
| `--bypassgateway <bool>` | 主機與用戶端位於相同網路時，是否略過 Gateway；預設`true` |
| `--admin <bool>` | 是否使用系統管理工作階段 |
| `--autoreconnect <bool>` | 斷線後是否自動重新連線；預設`true` |

#### 畫面與輸入

| 參數  | 說明  |
| --- | --- |
| `--fullscreen <bool>` | 是否使用全螢幕；預設`true` |
| `--useallmonitors <bool>` | 是否使用所有本機螢幕 |
| `--resolution "寬 高"` | 指定解析度，例如`"1600 900"` |
| `--scaling <bool>` | 是否將遠端畫面縮放到用戶端視窗內 |
| `--dynamicdisplay <bool>` | 視窗大小變更時，是否動態調整遠端解析度 |
| `--retina <bool>` | 是否針對 Retina 顯示最佳化 |
| `--colordepth <16\|32>` | 色彩深度 |
| `--swapmousebuttons <bool>` | 是否交換滑鼠左右鍵 |

`--scaling` 與 `--dynamicdisplay` 雖然都和畫面大小有關，但用途不同。前者處理畫面的縮放，後者處理遠端解析度隨視窗變化的行為。如果你發現字體只是被放大，遠端桌面的可用空間沒有改變，就要確認自己調整的是哪一個選項。

#### 音訊、裝置與剪貼簿重新導向

| 參數  | 說明  |
| --- | --- |
| `--audioplayback <0\|1\|2>` | `0` 在本機播放、`1` 在遠端播放、`2` 不播放；預設 `0` |
| `--redirectmicrophones <bool>` | 麥克風重新導向 |
| `--redirectcameras <bool>` | 攝影機重新導向 |
| `--redirectprinters <bool>` | 印表機重新導向 |
| `--redirectfolders <bool>` | 資料夾重新導向 |
| `--redirectsmartcards <bool>` | 智慧卡讀卡機重新導向 |
| `--redirectclipboard <0\|1\|2\|3>` | 剪貼簿的資料傳送方向 |

剪貼簿的數值如下：

| 數值  | 意義  |
| --- | --- |
| `0` | 關閉剪貼簿重新導向 |
| `1` | 雙向傳送，這是預設值 |
| `2` | 僅允許本機傳到遠端 |
| `3` | 僅允許遠端傳到本機 |

💡 `--redirectfolders true` 只是啟用資料夾重新導向。由於 App 的沙箱限制，要分享哪些本機資料夾，仍然需要在 Windows App 介面裡手動選取。

#### RemoteApp 與 RDP 設定匯入

| 參數  | 說明  |
| --- | --- |
| `--remoteappprogram <string>` | 遠端應用程式路徑 |
| `--remoteappcmdline <string>` | 傳給遠端應用程式的參數 |
| `--remoteappworkingdir <string>` | 遠端應用程式的工作目錄 |
| `--rdpfilecontents <string>` | 直接使用 RDP 檔案內容建立設定 |

RemoteApp 參數需要搭配支援的遠端環境使用，並不是指定任意程式路徑，就能把一般遠端 PC 變成 RemoteApp 伺服器。

### 已儲存的裝置放在哪裡？

我原本以為 Windows App 裡面每一台已儲存的 PC，都會對應到某個資料夾下的一份 `.rdp` 檔。實際查看後才發現，這個版本主要把書籤資料存進 SQLite 資料庫：

```
~/Library/Containers/com.microsoft.rdc.macos/Data/Library/Application Support/com.microsoft.rdc.macos/com.microsoft.rdc.application-data.sqlite
```

另外還有 App Group 共用資料目錄：

```
~/Library/Group Containers/UBF8T346G9.com.microsoft.rdc/
```

這些是本機 11.4.1 的觀察結果，不是我會拿來當成穩定 API 的介面。要做整合，我會使用 `bookmark list`、`write` 與 `export`，讓 Windows App 自己負責資料儲存。

而且，**書籤資料與密碼儲存是兩件事**。不能只複製一份 SQLite 檔案，就假設帳號密碼、Keychain 存取權限與相關設定都一起搬過去了。

如果你真的需要 `.rdp` 檔，就透過前面介紹的 `bookmark export` 匯出即可。

### 從命令列開啟遠端桌面

這次查看的 `bookmark` 模組只有 `write`、`delete`、`list` 與 `export`，沒有 `connect` 或 `login` 子命令。

要開始連線，可以交給 macOS 的 `open` 命令啟動 Windows App。

#### 方法一：開啟 RDP 檔案

```bash
open -a "Windows App" ./server.rdp
```

#### 方法二：開啟 RDP URI

```bash
rdp_uri="$("$winapp" --script bookmark export "$bookmark_id" --uri)"
open -a "Windows App" "$rdp_uri"
```

第二種方法不需要產生中間檔案，很適合包成腳本。不過，正式腳本還需要檢查匯出是否成功，後面會說明原因。

> **開啟 URI 不等於從 Devices 啟動原本的書籤。** 我後續實際操作時，同一台主機從 Devices 直接雙擊可以登入，改用這段 URI 命令卻會要求輸入密碼。因此，這段命令可以用來啟動連線，但不能當成已驗證的免輸入密碼方案。

你也可以直接組一個簡單的 URI：

```bash
open -a "Windows App" \
  'rdp://full%20address=s:pc-a.example.com&username=s:alice&screen%20mode%20id=i:1'
```

這裡的格式有幾個地方需要留意：

-   欄位名稱中的空白必須編碼，例如 `full address` 變成 `full%20address`。
-   欄位之間使用 `&` 分隔。
-   值如果包含 `&`、`#` 或其他保留字元，也必須正確編碼。
-   整個 URI 要放在引號裡面，否則 Shell 會把 `&` 當成自己的語法。

RDP 檔案裡的 `full address:s:...`，放到 URI 後會變成 `full%20address=s:...`。這個地方是一個冒號換成等號，第一次自己組字串時很容易看漏。格式參考 [Remote Desktop URI scheme](https://learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/remote-desktop-uri#legacy-rdp-uri-scheme)。

如果設定很多，我會直接使用 `export --uri`，避免手動維護編碼規則。也不要把其他平台的 `ms-rd:` 範例直接套過來；URI 名稱相近，不代表每個用戶端支援相同的命令。

### 儲存密碼之後，為什麼 URI 還會要求輸入？

這次整合最容易誤判的地方，就是把「成功儲存帳密」當成「之後從任何入口連線，都會使用這組帳密」。

我原本串接的順序如下：

1.  準備目標主機的 RDP 設定與登入帳密。
2.  使用 `bookmark write` 寫入設定，並指定 `--username` 與 `--password`。
3.  使用 `bookmark export --uri` 匯出設定。
4.  透過 `open` 將 URI 交給 Windows App 開啟。

我有在本機驗證：透過 CLI 匯入包含帳號資訊的 RDP 設定時，Windows App 可以對應到既有的儲存認證。不過，這個測試只證明建立書籤時的認證關聯，沒有證明 URI 啟動時會使用同一筆認證。URI 傳遞的是連線設定，並不是把剛才的書籤 ID 當成「直接登入」命令送出去。

一開始我以為只要先儲存帳密，再匯出同一筆書籤就夠了。實際執行後卻一直要求輸入密碼。接著檢查 `prompt for credentials on client`，發現它早就已經是 `0`，並不是少設了這個開關。

於是改用同一台主機做對照：在 Devices 直接開啟可以登入，從 URI 開啟卻要密碼。這讓問題縮小到啟動路徑，沒有必要先去關閉 NLA 或修改遠端主機原則。後來又試了直接開啟 `.rdp` 檔案；雖然主機與帳號都和已儲存裝置相同，畫面仍出現 **Enter Your Credentials**。換成檔案並沒有解決這次的問題。

後續比較幾種啟動方式，結果如下：

| 啟動方式 | 本次環境的結果 |
| --- | --- |
| 在 Windows App 的 Devices 直接雙擊已儲存的裝置 | 可以登入，不必重新輸入密碼 |
| `bookmark export --uri` 後交給 `open` | 仍然要求輸入密碼 |
| 使用`open` 直接開啟測試機的 `.rdp` 檔 | 仍然要求輸入密碼 |
| 從 CLI 透過`System Events` 觸發已儲存裝置 | 可以登入，不必重新輸入密碼 |

我也重新檢查了對應書籤匯出的 URI，確認已有 `prompt for credentials on client=i:0`，而且書籤本身有儲存認證的關聯。因此，重複把這個值設成 `0`，並不能解決本次的問題。從這個比較可判斷，問題發生在 URI 啟動這條路徑；尚未確認的是 Windows App 內部為什麼沒有成功沿用認證。

目前能確認可用的方式，是從 **Devices 開啟已儲存的裝置**。本次查閱的[官方 CLI 文件](https://learn.microsoft.com/en-us/windows-app/cli-macos)與本機 `help` 都沒有直接連線至指定書籤的 `connect` 命令，也沒有保證 URI 會沿用該書籤認證的參數。後來我改從命令列觸發裝置卡片的 UI 動作，實際成功使用儲存認證登入，下一節就來說明這個方法。

如果想先透過介面設定，可以在 **Devices → 選擇 PC → Edit → Saved credential** 新增或選取認證。微軟在 [管理使用者帳戶文件](https://learn.microsoft.com/en-us/windows-app/user-account-settings-add-remove-manage#manage-credentials-for-devices-and-apps) 說明了這個流程。

我檢查過這次匯出的 RDP 內容，裡面有 `username`，但沒有跟著匯出密碼。因此，不能把一份匯出的 `.rdp` 當成攜帶完整帳密的登入檔案，也不要自行加上一個猜測出來的 `password:s:...` 欄位，就假設 macOS 的 Windows App 一定會讀取。

如果連 Devices 直接雙擊也會出現提示，才需要繼續檢查以下情況：

-   Keychain 尚未允許 Windows App 存取儲存的密碼。
-   密碼已變更，儲存認證還沒有更新。
-   RDP 設定中的帳號格式與儲存認證不一致。
-   遠端伺服器要求每次重新輸入密碼，或有額外的 MFA 要求。
-   真正出現的是 Gateway 認證或憑證提示，而不是目標 PC 的密碼提示。

`prompt for credentials on client:i:0` 可以表達不要在用戶端強制提示的設定，但它本身不會提供密碼，也不會取消伺服器端的認證政策。

### 從 CLI 啟動已儲存裝置：實測可行的方法

既然直接點選裝置可以登入，那麼從命令列做同一件事呢？macOS 的 `System Events` 可以透過 Accessibility 操作應用程式介面，不一定需要 App 提供專用的 AppleScript 命令。這是 Apple 文件介紹的 [UI scripting](https://developer.apple.com/library/archive/documentation/LanguagesUtilities/Conceptual/MacAutomationScriptingGuide/AutomatetheUserInterface.html) 機制。

我檢查 Windows App 的裝置卡片後，發現它提供 `AXPress` 動作。只要找到顯示名稱完全符合的卡片，就可以透過 JavaScript for Automation（JXA）觸發：

```javascript
// deviceCard 是已經找到的裝置卡片；這一行不是完整腳本。
deviceCard.actions.byName('AXPress').perform();
```

這個做法成功的地方，是讓 Windows App 開啟原本已經設定好的裝置，使用它關聯的 Saved credential。腳本不需要讀取 Keychain 密碼，也不用模擬鍵盤把密碼打進視窗。

我把完整程式整理在 [windows-app-connect.js](#%E5%BB%BA%E7%AB%8B-scriptswindows-app-connect.js)。在專案目錄下執行，參數填入 Windows App 裡的**裝置顯示名稱**：

```bash
osascript -l JavaScript scripts/windows-app-connect.js '開發主機 A'
```

腳本會開啟 Windows App、回到 **Window → Connection Center**、選擇 **Devices**，再觸發名稱完全符合的唯一裝置。找不到或找到多個相符元件時，就會停止，不會猜測要連哪一台。

這次我用同一台測試機比較，直接開啟 `.rdp` 會顯示 **Enter Your Credentials**，透過 `AXPress` 則直接進入 Windows 桌面。關閉工作階段視窗後，再執行完整腳本重新連線，也同樣成功；兩次都沒有輸入密碼。這裡有實際查看遠端桌面，並非只看到命令回傳 `0` 就當作登入成功。

這個方法仍有幾個使用條件：

-   裝置已經設定有效的 Saved credential，而且從 Devices 直接開啟時能正常登入。
-   執行腳本的 Terminal 或其他宿主程式，必須取得 macOS 的「輔助使用」及需要的「自動化」權限。
-   目前腳本以 Windows App 11.4.1 的英文介面與網格檢視為測試環境；目標卡片必須可見，必要時先清空搜尋與篩選。
-   需要可操作的 macOS 圖形桌面；沒有驗證鎖定畫面、純 SSH 或 CI 環境。

💡 腳本顯示「已送出連線請求」，只代表成功觸發裝置。密碼失效、VPN 未連線或出現其他提示時，仍需要在 Windows App 確認結果。完整研究記錄請參考[這份筆記（Markdown 下載）](/attachments/2026/10/windows-app-cli/windows-app-cli-passwordless-research.zh-tw.md)。

### 憑證提示與密碼提示要分開處理

如果出現的是「無法驗證憑證」之類的訊息，調整帳號密碼通常不會解決問題。這是用戶端在確認遠端主機的身分。

RDP 有一個和伺服器認證有關的設定：

```ini
authentication level:i:0
```

依據 [微軟的 RDP 屬性文件](https://learn.microsoft.com/en-us/azure/virtual-desktop/rdp-properties#authentication-level)，各數值的意義如下：

| 數值  | 伺服器認證失敗時的處理方式 |
| --- | --- |
| `0` | 不顯示警告，繼續連線 |
| `1` | 不建立連線 |
| `2` | 顯示警告，由使用者決定是否繼續 |
| `3` | 未指定認證要求 |

這個設定影響的是伺服器身分檢查。設成 `0` 不代表帳號密碼可以省略，也不等於停用所有可能出現的 TLS 或 Gateway 錯誤。

> 我這次有確認 RDP 匯入與匯出會保留 `authentication level`，但沒有逐一測試 Windows App 11.4.1 對所有憑證錯誤的處理行為。因此，不能把這個參數當成「所有憑證提示一定消失」的保證。

如果你要持續使用同一台主機，可以先確認它的憑證，再在憑證提示的 **Show Certificate** 裡設定信任；相關信任設定也可以在 macOS 的 Keychain Access 管理。參考 [Apple 的憑證信任設定說明](https://support.apple.com/guide/keychain-access/change-the-trust-settings-of-a-certificate-kyca11871/mac)。

正式使用的環境，則可以讓主機使用受用戶端信任、名稱也正確的憑證。這樣後續連線不需要每次依賴使用者判斷警告。

### 最後選擇：JXA 搭配 Makefile，直接開啟已儲存裝置

實際確認 `AXPress` 可以沿用儲存認證之後，我決定把日常連線流程縮減成：

```
make → osascript → System Events → 已儲存裝置 → Windows App 使用 Saved credential
```

帳號密碼與連線選項留在 Windows App 管理。Makefile 只記錄要開啟哪個裝置，JXA 只負責觸發該裝置。官方 CLI 仍然適合批次建立、修改與匯出設定，但日常連線不必每次重新寫入帳密。

#### 先設定 Windows App 的儲存認證

1.  在 Windows App 的 **Devices** 找到要連線的 PC，選擇 **Edit**。
2.  在 **Saved credential** 新增或選擇正確的帳號密碼，儲存設定。
3.  先直接開啟該裝置，確認不需要重新輸入密碼，就能進入 Windows 桌面。
4.  記下裝置的完整顯示名稱，並確認名稱唯一。

如果直接開啟裝置就會詢問密碼，應先解決認證設定或遠端原則問題。JXA 不會替你補上缺少的密碼。

#### 準備目錄

日常連線只需要以下兩個檔案：

```
rdp-workspace/
├── Makefile
└── scripts/
    └── windows-app-connect.js
```

連線設定與帳密事先儲存在 Windows App，日常只要透過這兩個檔案啟動指定裝置即可。

#### 建立 scripts/windows-app-connect.js

完整腳本如下。這是 macOS 的 JavaScript for Automation，不是 Node.js 程式，請使用 `osascript` 執行。

```javascript
// 用法：osascript -l JavaScript scripts/windows-app-connect.js '已儲存裝置名稱'
// 使用 Windows App 的儲存認證；不讀取、傳遞或輸入密碼。
// 已驗證 Windows App 11.4.1 的英文介面、Devices 網格檢視。

function read(element, property) {
    try { return element[property](); } catch (_) { return null; }
}

function findElements(root, predicate, depth) {
    if (depth > 12) return [];
    let matches = predicate(root) ? [root] : [];
    const children = read(root, 'uiElements') || [];
    for (const child of children) {
        matches = matches.concat(findElements(child, predicate, depth + 1));
    }
    return matches;
}

function run(argv) {
    if (argv.length !== 1 || !argv[0].trim()) {
        throw new Error('用法：osascript -l JavaScript scripts/windows-app-connect.js "已儲存裝置名稱"');
    }
    const deviceName = argv[0];
    const app = Application('Windows App');
    app.activate();
    const process = Application('System Events').processes.byName('Windows App');
    const deadline = Date.now() + 10000;
    while (!process.exists() && Date.now() < deadline) delay(0.2);
    if (!process.exists()) throw new Error('Windows App 未能啟動。');
    process.frontmost = true;

    // 先回到本機 Connection Center，避免把操作送到遠端桌面。
    try {
        process.menuBars[0].menuBarItems.byName('Window').menus[0]
            .menuItems.byName('Connection Center').click();
    } catch (_) {
        throw new Error('無法開啟 Connection Center。請確認 Windows App 使用英文介面，並允許執行腳本的程式使用 macOS「輔助使用」與「自動化」。');
    }

    let center;
    while (Date.now() < deadline) {
        center = process.windows().find(w => read(w, 'name') === 'Windows App');
        if (center) break;
        delay(0.2);
    }
    if (!center) throw new Error('找不到 Windows App 的 Connection Center 視窗。');

    const devices = findElements(center, el =>
        read(el, 'role') === 'AXButton' && read(el, 'description') === 'Devices', 0);
    if (devices.length !== 1) throw new Error('找不到 Devices 頁面，請確認介面版本與語言。');
    devices[0].actions.byName('AXPress').perform();
    delay(0.3);

    // 只操作可見且名稱完全符合的裝置，名稱重複時不猜測要連哪一台。
    const matches = findElements(center, el => {
        if (read(el, 'description') !== deviceName) return false;
        return (read(el, 'actions') || []).some(a => read(a, 'name') === 'AXPress');
    }, 0);
    if (matches.length !== 1) {
        throw new Error('找到 ' + matches.length + ' 個符合名稱的可見裝置。請清空搜尋與篩選、使用網格檢視，並確認裝置名稱唯一。');
    }
    matches[0].actions.byName('AXPress').perform();
    return '已透過已儲存裝置送出連線請求：' + deviceName + '。請在 Windows App 確認登入結果。';
}
```

可以先直接測試：

```bash
osascript -l JavaScript scripts/windows-app-connect.js '開發主機 A'
```

腳本比對的是裝置的**顯示名稱**，不是主機 IP、`.rdp` 檔名或書籤 ID。它不會自動捲動載入尚未顯示的卡片，請讓目標裝置出現在 Devices 的網格裡。

#### 建立 Makefile

以下用兩個示意裝置，分別對應 `rdp-site-a` 與 `rdp-site-b`：

```makefile
RDP_SCRIPT := $(CURDIR)/scripts/windows-app-connect.js
SITE_A_RDP_DEVICE ?= 開發主機 A
SITE_B_RDP_DEVICE ?= 開發主機 B
export SITE_A_RDP_DEVICE SITE_B_RDP_DEVICE

.PHONY: rdp-site-a rdp-site-b

rdp-site-a:
	@osascript -l JavaScript "$(RDP_SCRIPT)" "$$SITE_A_RDP_DEVICE"

rdp-site-b:
	@osascript -l JavaScript "$(RDP_SCRIPT)" "$$SITE_B_RDP_DEVICE"
```

Makefile 命令前面要使用 **Tab**。裝置名稱透過環境變數傳給 Shell，並以雙引號保留為單一參數；`$$` 是讓 Make 將 `$` 留給 Shell 使用。

平常執行：

```bash
make rdp-site-a
make rdp-site-b
```

需要更換裝置名稱時，也可以覆寫變數：

```bash
make rdp-site-a SITE_A_RDP_DEVICE='另一台開發主機'
```

這裡的變數是裝置名稱，不是帳號密碼。如果你已經有管理 VPN 的 Makefile，可以把這兩個 target 加進去。RDP target 只負責開啟已儲存裝置，執行前仍要連上正確的 VPN。

💡 我這次還遇到一個很容易忽略的細節：有一筆既有裝置名稱包含不可見的 `U+200B` 零寬字元。畫面看起來相同，不代表字串完全一樣。使用完整名稱比對時，要保留實際名稱；若在 Windows App 重新命名，也要同步修改 Makefile。

### 這次踩雷後留下的幾個提醒

#### 儲存成功與登入成功是兩個驗收條件

我最初確認了 `bookmark write` 能儲存設定與帳密，也確認匯出的 URI 包含正確的連線資訊，但實際開啟 URI 仍然詢問密碼。不能因為書籤建立成功、設定匯出正確，就把整條流程標成「自動登入完成」。

#### 模擬測試沒有覆蓋到 Windows App 真正的登入行為

如果測試只用模擬回應，檢查命令參數、呼叫順序與錯誤處理，就沒有真的連線到遠端主機，也沒有觀察密碼視窗。這類測試能確認腳本的程式邏輯，但無法證明免輸入密碼。

這次真正有決定性的測試，是用同一台主機比較「直接開啟裝置」、「開啟 URI」、「開啟 RDP 檔」與「JXA 觸發裝置」，最後還要看到實際 Windows 桌面。

#### CLI 回傳 0，也不一定代表命令成功

本機版本在匯出不存在的書籤時，會在 stdout 顯示書籤不存在，但 exit code 仍為 `0`。所以自動化設定管理時，要確認輸出內容，不能只看程序結束碼。

同樣地，JXA 成功觸發 `AXPress`，也只代表送出了啟動動作。這就是最終腳本刻意回報「已送出連線請求」，而不是直接宣告登入成功的原因。

### 使用 gateway 管理 Remote Desktop Gateway

如果你的環境需要經過 RD Gateway，可以先建立 Gateway，再讓書籤參照它。

```bash
"$winapp" --script gateway help
"$winapp" --script gateway write help

gateway_id="$(uuidgen)"

"$winapp" --script gateway write "$gateway_id" \
  --hostname 'rdgateway.example.com' \
  --friendlyname '公司 RD Gateway'

"$winapp" --script bookmark write "$bookmark_id" \
  --gateway "$gateway_id" \
  --bypassgateway false
```

Gateway 的 `write` 支援 `--hostname`、`--username`、`--password` 與 `--friendlyname`。如果 Gateway 與目標 Windows PC 使用不同帳號，要分別設定，不要只更新 PC 的密碼。

日常管理命令如下：

```bash
# 列出 Gateway
"$winapp" --script gateway list

# 修改既有 Gateway
"$winapp" --script gateway write "$gateway_id" \
  --friendlyname '主要 RD Gateway'

# 刪除指定 Gateway
"$winapp" --script gateway delete "$gateway_id"
```

這個模組也使用固定 ID 識別資料。如果你把它寫進長期使用的腳本，一樣要保留 ID。

### 使用 feed 管理 RDS 資源訂閱

`feed` 使用 URL 識別資源訂閱，和 `bookmark`、`gateway` 使用的 UUID 不同。

```bash
"$winapp" --script feed help
"$winapp" --script feed write help

feed_url='https://rdweb.example.com/RDWeb/Feed/webfeed.aspx'

# 建立訂閱設定，預設先建立可由 UI 重新整理的項目
"$winapp" --script feed write "$feed_url"

# 列出資源訂閱
"$winapp" --script feed list
```

若要在寫入時嘗試訂閱，需要加上 `--subscribe`，並提供該資源使用的帳號密碼：

```bash
read -rs 'feed_password?請輸入 RDS 訂閱密碼：'
printf '\n'

"$winapp" --script feed write "$feed_url" \
  --username 'CONTOSO\alice' \
  --password "$feed_password" \
  --subscribe

unset feed_password
```

依本機 `help` 說明，如果沒有指定帳號，或帳號無法完成訂閱，可能只建立一個 **stub feed**。因此，「清單裡看得到訂閱項目」不能直接當成認證成功的證據。

刪除時也是帶入 URL：

```bash
"$winapp" --script feed delete "$feed_url"
```

這裡還有一個很容易誤用的參數：`--accept-certificates`。它屬於 **`feed write` 訂閱流程**，用來接受該流程遇到的認證憑證。它不是 `bookmark write` 的全域憑證略過開關，不能搬過去解決任意遠端 PC 的憑證警告。

### 使用 defaults 調整用戶端行為

最後一個模組的名稱叫做 `defaults`：

```bash
"$winapp" --script defaults
```

這個命令會列出可調整的設定，但真正的讀寫工作是交給 macOS 內建的 `defaults` 命令處理。

例如，讀取或設定 UI 記錄層級：

```bash
defaults read com.microsoft.rdc.macos ClientSettings.UILogLevel

defaults write com.microsoft.rdc.macos \
  ClientSettings.UILogLevel -int 4
```

本機 `help` 列出的記錄層級為 `0` 不記錄、`1` 錯誤、`2` 警告、`3` 資訊、`4` 詳細。UI 與 Core 分別使用以下兩個設定名稱：

```
ClientSettings.UILogLevel
ClientSettings.CoreLogLevel
```

完成問題排查後，可以回復到本機 `help` 列出的預設錯誤層級：

```bash
defaults write com.microsoft.rdc.macos ClientSettings.UILogLevel -int 1
defaults write com.microsoft.rdc.macos ClientSettings.CoreLogLevel -int 1
```

其他設定整理如下：

| 設定名稱 | 用途  |
| --- | --- |
| `ClientSettings.DisableCameraRedirection` | 全域停用攝影機重新導向 |
| `ClientSettings.DisableMicrophoneRedirection` | 全域停用麥克風重新導向 |
| `ClientSettings.DisableClipboardRedirection` | 全域停用剪貼簿重新導向 |
| `ClientSettings.DisableSmartcardRedirection` | 全域停用智慧卡重新導向 |
| `ClientSettings.DisableFolderRedirection` | 全域停用資料夾重新導向 |
| `ClientSettings.DisablePrinterRedirection` | 全域停用印表機重新導向 |
| `ClientSettings.WorkspaceAutoRefreshInterval` | 資源訂閱自動重新整理間隔，單位為秒 |
| `ClientSettings.DisableOnPremWebSocketGateway` | 停用內部環境的 WebSocket Gateway 功能 |
| `ClientSettings.EnableAvdUdpSideTransport` | AVD 連線的 UDP 傳輸選項 |
| `EnableRemoteAppLocalMove` | RemoteApp 視窗的本機移動行為選項 |

重新整理間隔的本機說明是預設 `21600` 秒，也就是六小時，範圍介於 `1800` 到 `86400` 秒之間。

注意 `Disable...` 的命名方向：設成 `true` 是停用，不是啟用。舉例來說：

```bash
# 全域停用剪貼簿重新導向
defaults write com.microsoft.rdc.macos \
  ClientSettings.DisableClipboardRedirection -bool true

# 取消這個全域停用設定
defaults write com.microsoft.rdc.macos \
  ClientSettings.DisableClipboardRedirection -bool false
```

取消全域停用後，仍然要看個別連線與遠端政策是否允許。若是設定沒有生效，本機 `help` 也提到可檢查 `defaults -currentHost` 的使用方式，不要直接假設所有設定都只存在同一個偏好設定範圍。

此外，`defaults read` 找不到某個 Key，不代表那個功能不存在，也可能只是使用者尚未寫入覆寫值。讀取偏好設定與查詢程式執行時的有效設定，並不完全相同。

### 幾個實際排查問題的方向

#### 帳密明明存過，升級之後卻一直要求重新輸入

微軟有記錄過從 Microsoft Remote Desktop 升級到 Windows App 後，舊有 Keychain 項目未允許新 App 存取的問題。

可以開啟 **Keychain Access**，搜尋 `com.microsoft.rdc.macos`，選取對應密碼項目，查看 **Get Info → Access Control**，確認 Windows App 是否具有存取權。完整處理步驟請參考 [Windows App 已知問題](https://learn.microsoft.com/en-us/windows-app/troubleshoot-known-issues-limitations)。

#### 連線設定寫好了，卻連不到主機

先確認 VPN、DNS 與路由。CLI 可以成功建立書籤，並不表示它建立書籤的時候就已經連到遠端驗證過。

例如，你可以在**確認主機位址正確後**檢查 RDP 連接埠是否可達：

```bash
nc -vz pc-a.example.com 3389
```

這只能測試 TCP 連線，不能驗證 RDP 帳密、NLA、憑證或 Gateway 設定。若環境要求經過 RD Gateway，也不應把「無法直接連到 PC 的 3389」當成必然故障。

#### 誤把傳回碼當成登入結果

這個流程至少有三層結果需要區分：

| 層次  | 能證明的事情 |
| --- | --- |
| `bookmark write` 成功 | Windows App 接受並儲存了設定 |
| JXA 成功觸發`AXPress` | 已經對指定的裝置卡片送出啟動動作 |
| 遠端桌面真正出現 | 已經進入遠端工作階段，仍應確認是否還停在登入畫面 |

如果未來要做監控或完整端到端測試，需要再觀察實際工作階段狀態，不能只用 Make 的結束碼代替。

### 結語

本文介紹的 `bookmark`、`feed`、`gateway` 與 `defaults` 參數，是依照本機 Windows App 11.4.1 的說明整理。實際執行測試的部分，包括書籤建立與刪除、RDP 匯入匯出、換行與反斜線帳號，以及匯入時對既有儲存認證的關聯。

實際連線測試確認，從 Devices 開啟已儲存裝置可以直接登入，使用 URI 與直接開啟測試機 `.rdp` 卻仍會要求密碼。本文保留 URI 說明，作為 CLI 功能與踩雷經驗的一部分。

另外實測 JXA 透過 `System Events` 觸發已儲存裝置，在測試機連線與斷線重連兩次，都確認進入 Windows 桌面且無須輸入密碼。這不代表已驗證所有遠端主機、檢視模式或無圖形介面的環境。Gateway 與 RDS feed 的範例也沒有連到實際伺服器測試。

最終 Makefile 直接呼叫 JXA，另以命令展開及參數檢查確認裝置名稱的傳遞方式。真正完成登入驗證的是前面那台已儲存認證的測試機；其他主機仍需在自己的網路與 VPN 環境下確認，不能把 Make 的命令檢查當作登入驗證。

如果你要在自己的環境採用，可以先用一台已知能透過 Windows App 手動連線的主機測試，再把相同設定搬進這個流程。這樣遇到問題時，比較容易判斷是原本的 RDP 環境問題，還是自動化整合的問題。

### 相關連結

-   [Windows App for macOS 命令列介面](https://learn.microsoft.com/en-us/windows-app/cli-macos)
-   [使用 Windows App 連接裝置與應用程式](https://learn.microsoft.com/en-us/windows-app/get-started-connect-devices-desktops-apps)
-   [管理 Windows App 使用者帳戶與儲存認證](https://learn.microsoft.com/en-us/windows-app/user-account-settings-add-remove-manage)
-   [Remote Desktop URI scheme](https://learn.microsoft.com/en-us/windows-server/remote/remote-desktop-services/remote-desktop-uri)
-   [支援的 RDP 屬性](https://learn.microsoft.com/en-us/azure/virtual-desktop/rdp-properties)
-   [Windows App 已知問題與限制](https://learn.microsoft.com/en-us/windows-app/troubleshoot-known-issues-limitations)
-   [在 Mac 的 Keychain Access 調整憑證信任](https://support.apple.com/guide/keychain-access/change-the-trust-settings-of-a-certificate-kyca11871/mac)
