你想让"每次会话结束时自动把对话存档"这件事完全自动化,于是配了一个钩子(hook)——在特定事件上自动执行脚本。配好之后,脚本要么拿不到输入,要么跑着跑着被莫名杀掉,钩子形同虚设。
现象
主要三种:
- 拿不到输入。 钩子按约定会把事件信息通过标准输入(stdin)喂给脚本,但脚本读到的 stdin 是空的,或者一直卡在读取上不返回。
- 被超时杀掉。 某些事件的默认超时只有 30 秒,脚本稍微多干点活(遍历、上传、压缩)就被终止。
- 事件未必触发。 你依赖的那个事件在某些路径下不一定会被触发,钩子于是静默跳过了。
根因:Windows 上的标准输入管道不可靠
在 Unix 系系统上,钩子通过管道把数据喂给 stdin 是很稳的做法。但到了 Windows,管道行为经常出问题:
- stdin 为空 / 冻结。 子进程拿到的管道可能是空句柄,或者因为父进程没有正确关闭写端而永久阻塞——脚本看起来"卡死",其实是在等一个永远不会来的 EOF。
- 默认超时偏短。 30 秒对"快速判断"够用,对"读文件、算哈希、写存档"这类真正的存档动作太紧,于是长动作被杀。
- 事件触发不稳定。 不同事件在异常退出、强制关闭等路径下的触发保证程度不同,不能假定某一个事件一定会来。
- 路径转义。 命令里用到 Windows 反斜杠时,还要考虑 shell 层的转义,容易在到达脚本前就被吃掉一层。
解决:按"最坏输入"设计钩子
-
不要依赖标准输入。 把脚本改成"自己能找到现场信息",最通用的兜底是取最新修改时间的那个会话文件:
# 不读 stdin,直接定位最近改动的会话文件 $dir = "C:\path\to\sessions" $latest = Get-ChildItem $dir -Filter *.jsonl | Sort-Object LastWriteTime -Descending | Select-Object -First 1这样即使 stdin 是空的、冻结的,脚本也能干活。
-
显式设置更大的超时。 给钩子配置足够长的超时,别用默认的 30 秒:
{ "hooks": { "SessionEnd": [ { "hooks": [ { "type": "command", "command": "powershell -File C:\\path\\archive.ps1", "timeout": 300 } ] } ] } } -
双事件兜底。 用一个触发最可靠的事件作主保障,另一个事件作补充。两个都写幂等逻辑(已经存档过就跳过),这样"多触发一次"也不会出问题。宁可多存一次,不可漏存。
-
用参数数组绕过 shell 转义。 把命令和参数拆成数组形式传入,避免把整条命令拼成一个字符串再让 shell 解析——转义层数越少,Windows 反斜杠坑就越少。
延伸与预防
钩子的正确设计心态是:假设最坏输入。 不能假设"它一定能拿到现场信息",因为管道、超时、触发时机全都不在你控制之内。三条通用原则:
- 信息自取:能自己从磁盘/环境找到的,就不要指望从参数或 stdin 拿。
- 动作幂等:钩子可能被重复触发,也可能延迟触发,逻辑要能承受。
- 宁可多跑:自动化里"多执行一次的代价"通常远小于"漏执行一次的代价",取舍时偏向前者。
调试阶段可以在脚本开头把拿到的参数、环境、stdin 长度都记进一个日志文件,跑几次就知道这个平台上到底能拿到什么。
一句话教训
自动化钩子要在"最坏输入"下设计,不能假设它一定能拿到现场信息。