BESTNET TECH BLOG
把 Windows 實機作業交給 AI 代理 ― 在 SSH 非互動化、字元編碼、T-SQL 的「接縫」處踩坑的實戰記
本系列正篇報告了:將 Oracle 19c 至 SQL Server 2022 的遷移前處理,在不向 AI 編碼代理(Claude Code)交付實際資料的情況下加以自動化,並完成在遷移目標實機上的套用與功能驗證。
本篇談的是其幕後。把「實機上的作業」交給 AI 代理時,會卡住的地方通常不是 AI 的聰明程度,而是 AI 與 Shell、OS 的字元編碼與工具之間的「接縫」。SSH 的非互動化、Windows 的亂碼、cmd 的引號處理、從程式碼投入 T-SQL——每一項單獨來看都不起眼,卻足以讓自動化停擺。在這些實戰中踩過的坑裡,本篇以症狀、原因、對策的形式留下偏技術的 10 個(其餘 2 個牽涉 AI 安全機制的治理議題,請見番外篇之二「AI 護欄篇」)。若能為正在考慮同類自動化的人省下幾個小時,便是萬幸。
本篇的指令範例,已將連線目標與認證資訊全部替換為佔位符(
USER@HOST、-pw '****'、C:\work)。
1. 連線篇 — 讓免手動的 SSH 成立 #
讓 AI 代理進行作業的前提,是人類一次都不必輸入密碼即可讓 SSH 指令通過。這裡馬上就有 3 個。
① OpenSSH 無法以非互動方式傳入密碼 #
- 症狀:
ssh user@host "コマンド"在等待密碼輸入處停住。無法從指令稿自動執行。 - 原因:OpenSSH 用戶端基於安全考量,只從終端(tty)讀取密碼。即使以管線傳入標準輸入也不會接收。Windows 也沒有內建
sshpass。 - 對策:使用 PuTTY 的
plink,以-pw傳入密碼。
plink -ssh -pw '****' USER@HOST "whoami"
就安全性而言,正式運用上請優先考慮公開金鑰驗證(金鑰組+
pageant或-i)。-pw以命令列傳入有可能殘留於行程清單或 Shell 歷史中,即便不得已使用,前提也是一次性+作業後輪替。
② plink 在首次的主機金鑰提示處卡住 #
- 症狀:即使下
echo y | plink ...也不前進。在逾時前毫無反應。 - 原因:plink 會從終端(tty)讀取首次連線時的金鑰快取確認「Store key in cache? (y/n)」。在如 AI 代理執行那樣沒有 tty 的環境(經由管線/行程啟動)中,接收不到以管線傳入的
y而卡住。在互動主控台上echo y | plink有時可行,但在自動執行時靠不住。 - 對策:先以
ssh-keyscan取得主機金鑰,把 SHA256 指紋以-hostkey釘選固定。以-batch停用提示本身。
# 1) ホスト鍵のフィンガープリントを取得
ssh-keyscan -T 6 HOST 2>/dev/null | ssh-keygen -lf -
# => 256 SHA256:<fingerprint> <host> (...) のように、鍵種ごとに1行ずつ出る
# 2) 取得値を -hostkey で固定し、-batch で非対話接続
# サーバが複数の鍵種(ED25519/RSA等)を返す環境では、ネゴシエートされる鍵に備え各 -hostkey を列挙する
plink -ssh -hostkey SHA256:<fingerprint> -batch -pw '****' USER@HOST "コマンド"
釘選在安全性上也是加分(對中間人攻擊而言,會彈掉預期以外的金鑰)。
③ 長時間指令要設計成「拋出後等待」 #
- 症狀:把 ISO 下載或產品安裝這類長時間處理同步執行時,會撞上工具端的逾時而斷線。
- 原因:持續占用 SSH 工作階段、阻塞 10 分鐘以上的處理,容易撞上代理的執行限制。
- 對策:長時間處理要在背景啟動,並以哨兵檔(sentinel file)或完成通知來接收完成。不長時間占用 SSH 工作階段。
:: 非同期起動し、完了時に終了コードをセンチネルへ(同一 cmd /c 内なので & は逐次評価される)
start "" /b cmd /c "installer.exe /quiet & echo DONE=%errorlevel%> C:\work\done.txt"
:: 別コマンドで完了確認: if exist C:\work\done.txt type C:\work\done.txt
2. 字元編碼篇 — CP932 與 BOM 的雙重布局 #
面對日文 Windows 時,亂碼不是「外觀的問題」,而會成為破壞處理的 bug。
④ 遠端輸出以 Shift-JIS 變成亂碼 #
- 症狀:指令結果像
�w�肳�ꂽ�p�X...那樣亂碼而讀不出來。 - 原因:日文 Windows 以 CP932(Shift-JIS)輸出。傳到以 UTF-8 為前提的終端便會變亂碼。
- 對策:在遠端側指令開頭加上
chcp 65001(切換為 UTF-8)。PowerShell 連輸出編碼也要明確指定。
chcp 65001>nul & 後続コマンド
[Console]::OutputEncoding = [Text.UTF8Encoding]::new()
⑤ PowerShell 5.1 會把無 BOM 的 .ps1 當成 CP932 讀取【最消耗時間的坑】 #
- 症狀:指令稿中的日文字面值(如
'すべて')變成'鬟溷刀'之類的別物,並以'~' 付近に不適切な構文があります讓 SQL 或處理失敗。 - 原因:PowerShell 5.1 會把無 BOM 的
.ps1解讀為系統 ANSI 字碼頁(日文環境即 CP932)。以 UTF-8 儲存的指令稿中的日文便會壞掉。只含 ASCII 的指令稿毫髮無傷,因此不易及早察覺,相當棘手。 - 對策:含日文的
.ps1要以 UTF-8(含 BOM) 儲存。PS5.1 會看 BOM 判定為 UTF-8。
# UTF-8 BOM付きで書き出す(既存のBOM無しUTF-8を変換する例)
$txt = [IO.File]::ReadAllText($src, [Text.UTF8Encoding]::new($false))
[IO.File]::WriteAllText($dst, $txt, [Text.UTF8Encoding]::new($true)) # $true = BOM付き
這兩個(④⑤)是不同層級的問題(終端輸出 vs 指令稿讀取),但症狀同為「亂碼」,因此容易混淆。記成輸出用 chcp、輸入(指令稿)用 BOM,切分開來會比較快。
3. 執行模式篇 — 從 cmd 引號地獄脫身 #
⑥ 經由 cmd 的行內 PowerShell 在 | " ; 處壞掉 #
- 症狀:流入
powershell -Command "Get-ChildItem | Select-Object Name"時出現'Select-Object' は…認識されていません。"的巢狀也會崩。 - 原因:因為是經 SSH→cmd→PowerShell 多段傳遞,
|會先被 cmd 解讀為管線。雙引號的巢狀也會在 cmd 那一段壞掉。流入標準輸入的方式(-Command -),在指令稿較長時也曾在中途被截斷。 - 對策:放棄行內,在本機寫好
.ps1→以pscp傳輸→以powershell -NoProfile -File執行。在本次作業的環境(Windows Server 2022 預設的RemoteSigned)下,本機未簽署的.ps1可直接執行。如此一來,引號問題與標準輸入截斷大半都能解決。
本次作業中這個模式很穩定。「複雜的遠端處理別硬要行內處理,做成檔案傳過去再以
-File叫起」。也不需要以Bypass削弱執行原則(那個操作反而被安全機制攔下了——請見番外篇之二)。不過前提需要留意。
RemoteSigned下傳輸的指令稿能否執行,會隨組織的群組原則或簽署原則(AllSigned/Constrained Language Mode/AppLocker・WDAC 等)而異。此外,「pscp 傳輸的檔案不會帶 MOTW(Mark of the Web)=簽署防護不生效」並非優點而是取捨,對來自第三方的指令稿反而是風險。請別忘了「因為是內部已掌握內容的工具才予以容許」這個前提。
pscp -hostkey SHA256:xxxx -batch -pw '****' .\task.ps1 USER@HOST:C:/work/task.ps1
plink -ssh -hostkey SHA256:xxxx -batch -pw '****' USER@HOST "powershell -NoProfile -File C:\work\task.ps1"
4. SQL 投入篇 — 從程式碼流入 T-SQL 時的坑 #
以下是把結構描述 DDL 不經 sqlcmd,改以 System.Data.SqlClient(.NET 標準的 SQL Server 連線函式庫。可不使用 sqlcmd 或 SSMS、從應用程式直接發出 DDL/DML,但反過來說不具備 sqlcmd 專屬功能=GO 分隔等),透過整合驗證直接流入時的三連發。
⑦ GO 分割會因註解內的 GO 而被切開 #
- 症狀:批次執行 DDL 時,部分批次以
コメントの終了マーク '*/' がありません或'=' 付近に不適切な構文失敗。 - 原因:
System.Data.SqlClient並不理解GO(GO是 sqlcmd/SSMS 的批次分隔,並非 T-SQL)。需要自行分割,但若天真地以^\s*GO\s*$切割,會連/* … */註解中以縮排寫的GO(例示程式碼等)也一併切開,導致註解被截斷。 - 對策:只切割行首(第 0 欄)的
GO。只要註解內的GO有縮排(這個程式碼庫即是如此),便能藉此排除。更嚴謹的做法,本來應是「先排除註解與字串字面值後再做GO分割」。
# 列0のGOのみで分割(インデントされたコメント内GOは無視)
$batches = [regex]::Split($script, '(?im)^GO[ \t]*;?[ \t]*\r?$')
⑧ 索引檢視表的建立因 ARITHABORT 而失敗 #
- 症狀:
CREATE UNIQUE CLUSTERED INDEX(建立索引檢視表)以…SET options have incorrect settings: 'ARITHABORT'失敗。 - 原因:
System.Data.SqlClient預設為ARITHABORT OFF。建立索引檢視表需要多個 SET 選項為 ON。 - 對策:連線後隨即投入必要的 SET,再流入 DDL。
SET ANSI_NULLS ON; SET ANSI_PADDING ON; SET ANSI_WARNINGS ON;
SET ARITHABORT ON; SET CONCAT_NULL_YIELDS_NULL ON;
SET QUOTED_IDENTIFIER ON; SET NUMERIC_ROUNDABORT OFF;
⑨ 取得列數時出現 列名 'rows' が無効です #
- 症狀:想以
SELECT SUM(p.rows) FROM sys.dm_db_partition_stats p …取得各表列數時,以列名 'rows' が無効です(Invalid column name 'rows')失敗。 - 原因:
sys.dm_db_partition_stats(DMV=動態管理檢視。回傳伺服器內部狀態的系統檢視)的列數欄是row_count,而非rows。具有rows欄的是sys.partitions那一邊。我搞混了。 - 對策:列數彙總要使用
sys.partitions(這是慣例做法)。
SELECT t.name, ISNULL(SUM(p.rows), 0) AS rows
FROM sys.tables t
LEFT JOIN sys.partitions p ON p.object_id = t.object_id AND p.index_id IN (0,1)
WHERE t.is_ms_shipped = 0
GROUP BY t.name ORDER BY t.name;
5. 收尾篇 — 雖不起眼卻會讓自動化停擺的坑 #
⑩ Get-ChildItem -Exclude 什麼都不回傳,導致收尾無法進行 #
- 症狀:即使執行
Get-ChildItem 'C:\work' -Exclude 'keep.sql' | Remove-Item -Force也一個檔案都沒刪掉(也不報錯)。 - 原因:指定目錄的
-Exclude,若不在路徑末尾加上\*或併用-Recurse,有時不會如預期列舉而回傳空(PowerShell 的已知毛病)。傳入Remove-Item的輸入為空,於是什麼都沒發生。 - 對策:別偷懶,明確列舉對象並以
Remove-Item -LiteralPath -Force處理。
foreach ($f in 'a.exe','b.iso','c.ps1') {
Remove-Item -LiteralPath (Join-Path 'C:\work' $f) -Force -ErrorAction SilentlyContinue
}
總結 — 坑出在「接縫」,而非「聰明程度」 #
並排來看,AI 代理本身出錯的地方幾乎沒有。卡住的幾乎全都是接縫。
- AI 與 Shell 的接縫(cmd 的引號解讀、主機金鑰提示的輸入路徑)
- Shell 與 OS 的接縫(CP932、BOM)
- 程式碼與工具的接縫(SqlClient 不認得
GO、DMV 的欄名、ARITHABORT 的預設值)
反過來說,越是把接縫標準化,自動化就越穩定(當然,也可能殘留別的接縫)。本次作業的實用解,歸結為以下 3 點。
- 連線以
plink/pscp+ 主機金鑰釘選 +-batch達成免手動 - 複雜的處理別寫成行內,傳送
.ps1再以-File執行(在本次作業的RemoteSigned環境下成立。視組織原則而定需確認) - 字元編碼以「輸出=chcp 65001/輸入(指令稿)=UTF-8 BOM」切分
另外,在排除這些坑的過程中,也有 AI 代理端的安全機制兩次攔下 AI 自身操作的一幕。一件是試圖削弱執行原則,一件是試圖把認證資訊寫入檔案。這與其說是「坑」,不如說是治理的議題,因此會在番外篇之二「AI 護欄篇」處理。