在 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 钉死(pin)。再用 -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 分钟以上的处理,很容易触发智能体的执行时限。
  • 对策: 长时间处理要在后台启动,通过哨兵文件或结束通知来捕获完成,不长时间占用 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' は…認識されていません(“未识别 '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 时的坑 #

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

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 智能体的基础设施・数据库迁移作业自动化支援。欢迎从“如何把自家环境的接缝标准化”开始咨询。
Updated on 2026年6月27日

What are your feelings

  • Happy
  • 常规
  • Sad

©2020 BESTNET.LLC . All Rights Reserved.