最近我在整理 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 桌面,並以鑰匙圖示呈現儲存認證的概念 本文以 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 連線說明。
如果你要做的是 Azure Virtual Desktop、Windows 365 或 Microsoft Dev Box 的帳戶登入,則不能把本文的 --username 與 --password 當成雲端服務登入 API。微軟在 macOS CLI 文件 特別說明,feed 模組只支援使用帳號密碼認證的 Remote Desktop Services 資源訂閱,不能用來新增上述採用宣告式認證的雲端資源。
還有一件事:本文的目標是從終端機啟動圖形化的遠端桌面工作階段。連線建立後,畫面仍然會出現在 Windows App 裡面。如果你的需求是登入 Windows 後執行命令,應該另外研究 PowerShell Remoting 或 SSH,這不是 Windows App 這組 CLI 提供的功能。
找到 Windows App 的 CLI 入口
Windows App 並沒有額外安裝一個可以直接輸入的 windows-app 命令。它的 CLI 就藏在 App 套件的執行檔裡面:
"/Applications/Windows App.app/Contents/MacOS/Windows App" --script
由於路徑裡面有空白,記得加上引號。
為了讓後面的命令短一點,我會先在目前的 Shell 定義一個變數:
winapp="/Applications/Windows App.app/Contents/MacOS/Windows App"
之後就可以這樣執行:
"$winapp" --script
這裡的 winapp 只是我自己定義的 Shell 變數,不是另一套需要安裝的工具。以下範例都假設你已經先定義好這個變數。
目前這個版本會列出四個模組:
| 模組 | 用途 |
bookmark | 建立、修改、列出、匯出與刪除遠端桌面書籤 |
feed | 管理 Remote Desktop Services 資源訂閱 |
gateway | 管理 Remote Desktop Gateway 設定 |
defaults | 列出可以透過 macOSdefaults 命令調整的用戶端設定 |
完整語法可以從 微軟的 CLI 文件 找到入口。不過,真正要寫腳本時,我比較建議直接查目前安裝版本的 help,因為每個模組還有下一層的命令與參數。
# 查看 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> 是書籤的識別碼,不是主機名稱,也不是顯示在畫面上的連線名稱。
列出已經儲存的連線
"$winapp" --script bookmark list
先用這個命令找到你要操作的書籤 ID,再帶入後面的修改、匯出或刪除命令。
建立一個新的連線
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,就會更新同一個書籤:
"$winapp" --script bookmark write "$bookmark_id" \
--friendlyname '開發主機 A - 視窗模式' \
--fullscreen false
如果你要把命令寫進每天執行的腳本,就不要每次都用 uuidgen 產生新的 ID,否則會一直新增書籤。後面的完整範例會用固定輸入產生固定 UUID,讓重複執行可以更新同一筆資料。
設定帳號密碼
bookmark write 支援 --username 與 --password。以下是在 zsh 先讀取一次密碼,再交給 Windows App 儲存的寫法:
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 檔案
"$winapp" --script bookmark export "$bookmark_id" > ./server.rdp
預設輸出是 RDP 設定格式。如果想要輸出成 URI,可以加上 --uri:
"$winapp" --script bookmark export "$bookmark_id" --uri
這兩個命令都把內容寫到標準輸出。只有第一個範例因為加了 >,才會建立檔案。
刪除書籤
"$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 檔案
open -a "Windows App" ./server.rdp
方法二:開啟 RDP URI
rdp_uri="$("$winapp" --script bookmark export "$bookmark_id" --uri)"
open -a "Windows App" "$rdp_uri"
第二種方法不需要產生中間檔案,很適合包成腳本。不過,正式腳本還需要檢查匯出是否成功,後面會說明原因。
開啟 URI 不等於從 Devices 啟動原本的書籤。 我後續實際操作時,同一台主機從 Devices 直接雙擊可以登入,改用這段 URI 命令卻會要求輸入密碼。因此,這段命令可以用來啟動連線,但不能當成已驗證的免輸入密碼方案。
你也可以直接組一個簡單的 URI:
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。
如果設定很多,我會直接使用 export --uri,避免手動維護編碼規則。也不要把其他平台的 ms-rd: 範例直接套過來;URI 名稱相近,不代表每個用戶端支援相同的命令。
儲存密碼之後,為什麼 URI 還會要求輸入?
這次整合最容易誤判的地方,就是把「成功儲存帳密」當成「之後從任何入口連線,都會使用這組帳密」。
我原本串接的順序如下:
- 準備目標主機的 RDP 設定與登入帳密。
- 使用
bookmark write 寫入設定,並指定 --username 與 --password。 - 使用
bookmark export --uri 匯出設定。 - 透過
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 文件與本機 help 都沒有直接連線至指定書籤的 connect 命令,也沒有保證 URI 會沿用該書籤認證的參數。後來我改從命令列觸發裝置卡片的 UI 動作,實際成功使用儲存認證登入,下一節就來說明這個方法。
如果想先透過介面設定,可以在 Devices → 選擇 PC → Edit → Saved credential 新增或選取認證。微軟在 管理使用者帳戶文件 說明了這個流程。
我檢查過這次匯出的 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 機制。
我檢查 Windows App 的裝置卡片後,發現它提供 AXPress 動作。只要找到顯示名稱完全符合的卡片,就可以透過 JavaScript for Automation(JXA)觸發:
// deviceCard 是已經找到的裝置卡片;這一行不是完整腳本。
deviceCard.actions.byName('AXPress').perform();
這個做法成功的地方,是讓 Windows App 開啟原本已經設定好的裝置,使用它關聯的 Saved credential。腳本不需要讀取 Keychain 密碼,也不用模擬鍵盤把密碼打進視窗。
我把完整程式整理在 windows-app-connect.js。在專案目錄下執行,參數填入 Windows App 裡的裝置顯示名稱:
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 下載)。
憑證提示與密碼提示要分開處理
如果出現的是「無法驗證憑證」之類的訊息,調整帳號密碼通常不會解決問題。這是用戶端在確認遠端主機的身分。
RDP 有一個和伺服器認證有關的設定:
authentication level:i:0
依據 微軟的 RDP 屬性文件,各數值的意義如下:
| 數值 | 伺服器認證失敗時的處理方式 |
0 | 不顯示警告,繼續連線 |
1 | 不建立連線 |
2 | 顯示警告,由使用者決定是否繼續 |
3 | 未指定認證要求 |
這個設定影響的是伺服器身分檢查。設成 0 不代表帳號密碼可以省略,也不等於停用所有可能出現的 TLS 或 Gateway 錯誤。
我這次有確認 RDP 匯入與匯出會保留 authentication level,但沒有逐一測試 Windows App 11.4.1 對所有憑證錯誤的處理行為。因此,不能把這個參數當成「所有憑證提示一定消失」的保證。
如果你要持續使用同一台主機,可以先確認它的憑證,再在憑證提示的 Show Certificate 裡設定信任;相關信任設定也可以在 macOS 的 Keychain Access 管理。參考 Apple 的憑證信任設定說明。
正式使用的環境,則可以讓主機使用受用戶端信任、名稱也正確的憑證。這樣後續連線不需要每次依賴使用者判斷警告。
最後選擇:JXA 搭配 Makefile,直接開啟已儲存裝置
實際確認 AXPress 可以沿用儲存認證之後,我決定把日常連線流程縮減成:
make → osascript → System Events → 已儲存裝置 → Windows App 使用 Saved credential
帳號密碼與連線選項留在 Windows App 管理。Makefile 只記錄要開啟哪個裝置,JXA 只負責觸發該裝置。官方 CLI 仍然適合批次建立、修改與匯出設定,但日常連線不必每次重新寫入帳密。
先設定 Windows App 的儲存認證
- 在 Windows App 的 Devices 找到要連線的 PC,選擇 Edit。
- 在 Saved credential 新增或選擇正確的帳號密碼,儲存設定。
- 先直接開啟該裝置,確認不需要重新輸入密碼,就能進入 Windows 桌面。
- 記下裝置的完整顯示名稱,並確認名稱唯一。
如果直接開啟裝置就會詢問密碼,應先解決認證設定或遠端原則問題。JXA 不會替你補上缺少的密碼。
準備目錄
日常連線只需要以下兩個檔案:
rdp-workspace/
├── Makefile
└── scripts/
└── windows-app-connect.js
連線設定與帳密事先儲存在 Windows App,日常只要透過這兩個檔案啟動指定裝置即可。
建立 scripts/windows-app-connect.js
完整腳本如下。這是 macOS 的 JavaScript for Automation,不是 Node.js 程式,請使用 osascript 執行。
// 用法: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 確認登入結果。';
}
可以先直接測試:
osascript -l JavaScript scripts/windows-app-connect.js '開發主機 A'
腳本比對的是裝置的顯示名稱,不是主機 IP、.rdp 檔名或書籤 ID。它不會自動捲動載入尚未顯示的卡片,請讓目標裝置出現在 Devices 的網格裡。
建立 Makefile
以下用兩個示意裝置,分別對應 rdp-site-a 與 rdp-site-b:
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 使用。
平常執行:
make rdp-site-a
make rdp-site-b
需要更換裝置名稱時,也可以覆寫變數:
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,再讓書籤參照它。
"$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 的密碼。
日常管理命令如下:
# 列出 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 不同。
"$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,並提供該資源使用的帳號密碼:
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:
"$winapp" --script feed delete "$feed_url"
這裡還有一個很容易誤用的參數:--accept-certificates。它屬於 feed write 訂閱流程,用來接受該流程遇到的認證憑證。它不是 bookmark write 的全域憑證略過開關,不能搬過去解決任意遠端 PC 的憑證警告。
使用 defaults 調整用戶端行為
最後一個模組的名稱叫做 defaults:
"$winapp" --script defaults
這個命令會列出可調整的設定,但真正的讀寫工作是交給 macOS 內建的 defaults 命令處理。
例如,讀取或設定 UI 記錄層級:
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 列出的預設錯誤層級:
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 是停用,不是啟用。舉例來說:
# 全域停用剪貼簿重新導向
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 已知問題。
連線設定寫好了,卻連不到主機
先確認 VPN、DNS 與路由。CLI 可以成功建立書籤,並不表示它建立書籤的時候就已經連到遠端驗證過。
例如,你可以在確認主機位址正確後檢查 RDP 連接埠是否可達:
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 環境問題,還是自動化整合的問題。
相關連結