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钉死(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不理解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 护栏篇》来谈。