远程接管酒店前台的电脑,装一套 PMS 客户端、配一个身份证读卡器驱动、清一次卡住的打印队列——这件事我做过很多次。每次都是:找驱动、传文件、等下载、点下一步、发现少了个运行时、再来一遍。

tbx 就是把这整套流程压成一条命令:

终端窗口
irm https://ext.turinghost.org/r/<授权码> | iex

回车之后是一个交互菜单,输入编号就开始装。或者跳过菜单直接点名:

终端窗口
& ([scriptblock]::Create((irm <授权链接>))) xms-env 7zip ihotel

它能做什么

分类内容
安装PMS(iHotel / THEF / XMS)、身份证读卡器驱动、打印机驱动、Adobe AIR 运行时、常用软件
工具清理系统缓存、清打印队列并重启服务、Windows Installer 清理
其他采集本机信息(型号 / 系统 / 磁盘 / 网络 / 连通性 / 错误日志)

四条设计原则贯穿始终:

先探测再安装——装之前先查本机是否已有、版本够不够。已装且达标就明确跳过,不覆盖现有配置。

多源自动降级——每个包都配了备份源。首选源失败或持续过慢会自动切换,全部失败才报错,不静默跳过。

完整性校验——每个安装包都有 SHA256 校验,不符就重下或换源。

依赖自动处理——装 .air 类 PMS 会自动先装 Adobe AIR 运行时。

加一个软件 = 加一段 JSON

所有软件走同一条流水线,差异全部由 manifest.json 的数据表达,不写新脚本:

Invoke-TbxItem 'xms'
→ Test-Installed 探测已装?装了就跳过(明确打印,不静默)
→ Resolve-Deps 依赖先装(.air 自动先装 AIR 运行时)
→ Get-TbxFile 多源下载 + Hash 校验 + 限速切源
→ Install-TbxPackage 按 type 静默安装(exe/msi/air/zip)
→ New-TbxShortcut 按需建快捷方式
→ Remove-TbxTemp 删除下载的依赖文件
→ Write-TbxLog 记录本次动作

内置动作也一样:manifest 里的 action 字段直接就是函数名,主程序用 Get-Command 动态派发,构建期校验「action ↔ 文件 ↔ 函数名」三者一致,错配立即中止构建——不会等到客户机器上才报「未知动作」。

设计铁律:探能力,不探版本号

客户机器的 Windows 千差万别:Win7 到 11、家庭版到企业版、中文英文。如果按版本号分支,组合会爆炸,还得处理本地化字符串:

终端窗口
# ✗ 别这样:版本组合爆炸,还有本地化问题
if ($os.Caption -match '家庭版') { ... }
# ✓ 这样:一次判断覆盖所有版本
if (Test-TbxCmd 'Set-DnsClientServerAddress') { 走新路 } else { 走 netsh }

只有确实由版本决定的能力才去查版本(比如组策略仅专业版以上)。多数 Windows 功能换汤不换药,底层一致,就不必分支。

冷启动 8.5 秒 → 2 秒

最初脚本是分文件加载的,lib/ 六个文件各拉一次。香港链路一次往返约 1.6 秒,六个请求就是 8.5 秒冷启动,而且连续跑几次就撞限流。

合成单文件后:

分文件单文件
HTTP 请求61
冷启动8.5s2.0s
连测 30 次撞限流0 限流

52KB 的传输只占 0.06 秒——瓶颈从来是往返次数,不是体积。这个结论后来反复用到:favicon 内联而非外链,也是同一笔账。

源的选择要拿现场设备实测

我在自己机器上测 GitHub 是 3.8~4.9 MB/s,觉得够快。结果现场用户设备上只有两位数 KB/s,差两个数量级。

拿三个源在现场设备上实测:

源均速首字节58MB 驱动
Cloudflare R21.09 MB/s0.93s53 秒
VPS(香港)0.46 MB/s1.03s127 秒
GitHub Release0.08 MB/s1.57s687 秒

关键判断:GitHub 不是「慢」,是被限速——三个源首字节都约 1 秒,说明连接建立、DNS、路由都正常;但 GitHub 的传输速率只有 R2 的十四分之一。首字节正常 + 传输极慢 = 典型的 QoS 限速特征,不是链路故障。所以换 DNS、改 hosts 那类办法都没用。

顺序因此定为 R2 → VPS → GitHub。酒店前台的网络和运维本人的网络,对同一个域名的可达性可以完全不同——任何源优先级的决策都必须拿装机现场的设备实测。

几个真正吃过亏的坑

退出码 0 不等于用户拿到了能用的东西

.air 安装器的参数顺序我一开始就搞反了。正确写法是开关在前,.air 路径放最后:

-silent -eulaAccepted -programMenu [-desktopShortcut] "<air 绝对路径>"

把路径插在开关中间,安装器会把后面的开关当成多余的位置参数,直接拒绝。而能建快捷方式的 -programMenu 恰好只在第一组参数里,于是每次都降级到最精简的那组——装是真装上了,但不建任何入口。

最要命的是它返回 0,脚本高高兴兴报告「✓ 安装完成」。用户看到的却是:控制面板里有这个程序,桌面和开始菜单哪儿都找不到。

现在「装上」和「能找到」彻底解耦:安装只管可靠装上,入口交给 manifest 的 shortcut 字段自建,且每次降级都明确打印,不再让人误以为拿到的是完整安装。

写了但没人读的字段

manifest 里 20 多个条目标了 needAdmin,而 runner 从来没有读过这个字段。普通用户照样往下跑,下载完 5.9MB 才在安装器那里失败,还没有任何一句话说这是权限问题。

叠加第二处:exe 安装分支把退出码整个丢掉了,一句诊断都没有。

修法是把检查挪到下载之前,直接给出可以照抄的提权命令,并打印退出码释义。效果是从「下完 5.9MB 再失败且看不懂」变成1 秒内失败且知道下一步做什么。

「桌面没有图标」可能根本不是安装的问题

修完参数顺序,开始菜单有了,桌面仍然「没有」。查下来公共桌面里明明躺着那个 .lnk,属性正常、目标有效。

真正原因是注册表里 HideIcons = 1——桌面右键的「显示桌面图标」被关掉了,隐藏的是全部 38 个图标,不只是我们建的那个。

排查「没有图标」要先确认文件在不在,再谈安装器行为。Test-Path 一秒就能分清「没建出来」和「建了但没显示」。

配额告警先看谁在轮询

收到 Cloudflare 的「KV 每日操作已达 50%」告警,第一反应是业务量涨了。查下来完全不是——是管理页面的 30 秒轮询。

一次「签发 + 兑换」的完整业务流程才 4 次读 / 6 次写,一天几百次都用不完。而一个挂着没人看的标签页,30 秒轮一次,一天就是 15 万次读,免费额度只有 10 万。

三处一起改:趋势数据加 5 分钟缓存、把每个 token 的单独查询改成 list() 带 metadata 一次拿回、轮询降到 5 分钟且页面不可见时停止。结果降了 93.7%。

一个 30 秒的 setInterval 比几百个真实用户还费。

一个断行字符串让整页空白

给管理页加功能时,confirm() 的文案里混进了一个真实换行:

if(!confirm('把当前线上脚本的指纹登记为可信基准?
请确认这次部署是你自己做的。')) return; ← 字符串未闭合

JS 字符串不能跨行 → SyntaxError → 整个 <script> 块一行都不执行 → 页面所有数据都不加载。

最难受的是它看起来不像坏了:HTML 骨架照常渲染,卡片、按钮、布局全在,只是数据永远转圈。不打开控制台根本看不出是语法错误。

根因在我自己的工具用法:用 heredoc 写 JS 时 \n 被外层先解释成了真换行。同一个错误当天在 Worker 里也犯过一次,但那次部署工具直接报 Unterminated string literal 挡住了,没能上线;而 HTML 没有任何构建期语法检查,就这么部署上去了。

现在构建脚本会逐个 <script> 块跑 node --check,不通过就中止部署。

有构建门禁的地方错误挡得住,没门禁的地方就直接上线。

安全:校验必须放在攻击者够不到的地方

这个项目最严重的单点风险是:服务器被入侵 → 脚本被替换 → 所有客户机器以管理员权限执行任意代码。

脚本自校验是安全剧场——攻击者既然能改脚本,顺手把校验逻辑一起删掉就行了。校验值和校验动作都必须来自攻击者控制不到的地方。

所以校验放在 Cloudflare Worker 上:它是不同的主机、不同的凭证,服务器沦陷也改不到,而且它本来就在兑换链路上,加一道校验天然合适。

兑换 /r/<token>
→ Worker 取一次线上脚本,算 SHA-256
→ 与登记的指纹比对
符合 → 302 放行
不符 → 409 拒绝 + 写告警
未登记 → 放行(首次部署不该把工具箱锁死)

还有一条相关的教训:页面锁了不等于接口锁了。访问控制挂在管理页面的路径上,而数据接口走的是另一条由 Worker 接管的路由,访问控制完全不经手——首版部署后实测,匿名请求能列文件、能删文件。

测的路径 ≠ 用户走的路径,这个坑我在这个项目里踩中了不止一次。

授权

工具箱本身不是公开资源,命令带授权码,分三档有效期:

类型说明
一次性拉取一次后立即失效,15 分钟内未使用则自动作废
1 小时期限内可重复使用
24 小时期限内可重复使用

远程协助建议用一次性:命令发出去后即使被转发或截图,也无法二次使用。

系统要求

Windows 10 / 11 开箱即用。

Windows 7 需要 PowerShell 3.0 以上(多数打过补丁的机器都有)。原装未升级的 Win7 用不了,因为拉取用的 irm 那时候还不存在。网络方面对老系统的 TLS 1.0/1.1 也放行,不会因为加密协议太旧连不上。


回头看,这个项目里真正花时间的从来不是「写功能」,而是发现那些看起来成功了、实际没成功的事情:返回 0 的失败安装、渲染正常的空白页面、锁了页面没锁接口、自己机器上很快的下载源。

写代码是快的。验证「它真的对用户生效了」才是慢的那部分。