---
title: "在 plink、PowerShell 5.1、SQL Server 2022 上踩到的 10 個坑——把 Windows 實機作業交給 AI 代理 (SSH/字元編碼/T-SQL)"
description: 以 AI 代理（Claude Code）在實機運行 Windows/SQL Server 自動化所踩的 10 個坑。從 plink 主機金鑰釘選、CP932/BOM、cmd 引號、RemoteSigned，到 System.Data.SqlClient 的 GO 分割、ARITHABORT、sys.partitions，以症狀→原因→對策逐一解說。
url: "https://bestnetllc.co.jp/zh-hant/docs/ai-agent-windows-sqlserver-pitfalls-tw/"
updated: "2026-10-19"
category: Tech Blog
---

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

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 護欄篇」](https://bestnetllc.co.jp/zh-hant/docs/ai-agent-guardrails-three-layer-governance-tw/)）。若能為正在考慮同類自動化的人省下幾個小時，便是萬幸。

> 本篇的指令範例，已將連線目標與認證資訊全部替換為佔位符（`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> (...)  — 每種金鑰類型輸出一行

# 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:workdone.txt"
:: 以另一條命令確認完成:  if exist C:workdone.txt type C:workdone.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。

```
# 以帶 BOM 的 UTF-8 寫出（將現有無 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:worktask.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*GOs*$` 切割，會連 `/* … */` 註解中以縮排寫的 `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. 連線以 `plink`／`pscp` ＋ 主機金鑰釘選 ＋ `-batch` 達成免手動
2. 複雜的處理別寫成行內，傳送 `.ps1` 再以 `-File` 執行（在本次作業的 `RemoteSigned` 環境下成立。視組織原則而定需確認）
3. 字元編碼以「輸出＝chcp 65001／輸入（指令稿）＝UTF-8 BOM」切分

另外，在排除這些坑的過程中，也有 AI 代理端的安全機制兩次攔下 AI 自身操作的一幕。一件是試圖削弱執行原則，一件是試圖把認證資訊寫入檔案。這與其說是「坑」，不如說是**治理的議題**，因此會在[番外篇之二「AI 護欄篇」](https://bestnetllc.co.jp/zh-hant/docs/ai-agent-guardrails-three-layer-governance-tw/)處理。

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