---
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-hans/docs/ai-agent-windows-sqlserver-pitfalls-cn/"
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-hans/docs/ai-agent-guardrails-three-layer-governance-cn/)）。如果能为正在考虑同类自动化的读者省下几个小时，那就再好不过了。

> 本文中的命令示例，已将连接目标、认证信息全部替换为占位符（`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` 钉死（pin）。再用 `-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 分钟以上的处理，很容易触发智能体的执行时限。
- **对策**: 长时间处理要**在后台启动，通过哨兵文件或结束通知来捕获完成**，不长时间占用 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' は…認識されていません`（“未识别 '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 时的坑

这是把 schema DDL 不经 sqlcmd、而是用 `System.Data.SqlClient`（.NET 标准的 SQL Server 连接库。可以不借助 sqlcmd 或 SSMS、直接从应用发出 DDL/DML，但反过来也不具备 sqlcmd 专属功能＝`GO` 分隔等），以集成认证直接灌入时，连发的 3 个。

### ⑦ `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-hans/docs/ai-agent-guardrails-three-layer-governance-cn/)来谈。

 BESTNET 合同会社提供运用 AI 智能体的基础设施、数据库迁移作业自动化支援。欢迎从“如何把自家环境的接缝标准化”开始咨询。
