在 plink・PowerShell 5.1・SQL Server 2022 上踩到的 10 個坑——把 Windows 實機作業交給 AI 代理 (SSH/字元編碼/T-SQL)

在 plink・PowerShell 5.1・SQL Server 2022 上踩到的 10 個坑——把 Windows 實機作業交給 AI 代理 (SSH/字元編碼/T-SQL)

9 min read

BESTNET TECH BLOG

在 plink・PowerShell 5.1・SQL Server 2022 上踩到的 10 個坑

把 Windows 實機作業交給 AI 代理 ― 在 SSH 非互動化、字元編碼、T-SQL 的「接縫」處踩坑的實戰記

作者: Hideyuki Chinda / BESTNET LLC2026-06-13系列 技術番外篇

本系列正篇報告了:將 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 並不理解 GOGO 是 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 點。

  1. 連線以 plinkpscp + 主機金鑰釘選 + -batch 達成免手動
  2. 複雜的處理別寫成行內,傳送 .ps1 再以 -File 執行(在本次作業的 RemoteSigned 環境下成立。視組織原則而定需確認)
  3. 字元編碼以「輸出=chcp 65001/輸入(指令稿)=UTF-8 BOM」切分

另外,在排除這些坑的過程中,也有 AI 代理端的安全機制兩次攔下 AI 自身操作的一幕。一件是試圖削弱執行原則,一件是試圖把認證資訊寫入檔案。這與其說是「坑」,不如說是治理的議題,因此會在番外篇之二「AI 護欄篇」處理。

BESTNET 合同公司提供運用 AI 代理進行基礎架構、DB 遷移作業自動化的支援。歡迎從「如何將自家環境的接縫標準化」開始與我們諮詢。
Updated on 2026年6月27日

What are your feelings

  • Happy
  • Normal
  • Sad

©2020 BESTNET.LLC . All Rights Reserved.