博客
30 秒完成连接:`npx tacticremote` 分发路径的构建之路
`npx tacticremote` 背后的工程故事,从 Bun 编译二进制和轻量 npm 启动器,到扫码自动保存和更快的首次引导。
继续查看该栏目下的产品与工程更新。
浏览此分类目标说起来简单,做起来出乎意料地难:新用户运行一条命令、扫一下二维码,30 秒内在手机上看到实时终端输出。
这篇文章讲的就是我们怎么做到的。
问题所在
npx tacticremote 出现之前,启动服务器是一个多步骤过程,而且因平台而异:
- macOS: 下载 DMG、挂载、将 App 移到 Applications、启动、和 Gatekeeper 周旋。
- Linux / CI: 克隆仓库、
npm install,再自己理清楚如何在那个环境里把服务器跑起来。
对于在远程 Linux 虚拟机或 Docker 容器里工作的开发者来说,这个过程相当痛苦。克隆仓库的方式需要 git、Node.js,以及能正常访问 npm registry 的网络环境。根本没有一条命令可以直接甩给别人说“跑这个就行”。
我们想要的是:npx tacticremote → 扫码 → 搞定。
为什么不直接 npm install?
服务端的运行时依赖并不算多,但 npm install 仍然有明显摩擦。当前运行时包里包括 ws、bonjour-service 和 qrcode-terminal。
npm 依赖解析本身就要时间,服务端在此之前一行日志都不会输出。在受限环境里,比如企业内网或容器化 CI,npm install 还可能因为代理设置、registry 镜像或权限问题直接失败。
更关键的是,CLI 路径本身应该足够轻。如果发布到 npm 的包仍然携带完整的运行时依赖树,首次体验依旧是“先下载一堆 JavaScript,再启动服务端”,这离我们想要的引导标准还差得很远。
所以发布到 npm 的那一层必须尽可能薄。
Bun 编译方案
我们用 Bun 把服务端编译成一个自包含的单体二进制。结果是一个大约 20 MB 的可执行文件,gzip 之前就已经能直接运行,里面包含:
- Bun 运行时
- 全部运行时依赖
- 完整的服务器源码
没有 node_modules,没有额外的 Bun 安装步骤,也没有启动时的依赖解析。拿到二进制就能运行。
GitHub Actions(.github/workflows/build-cli.yml)在每次发布时构建四个目标平台:
bun-darwin-arm64- Apple Silicon Macbun-darwin-x64- Intel Macbun-linux-x64- x86_64 Linuxbun-linux-arm64- ARM Linux(树莓派、AWS Graviton 等)
每个目标产物都会以 gzip 压缩包的形式上传到 GitHub Release。
轻量封装层
发布到 npmjs.com 的 npm 包不是服务器本体,而是一个 124 行的启动器脚本 bin/tacticremote.js。它实际做的事情如下:
- 用
process.platform和process.arch检测当前操作系统和 CPU 架构 - 根据平台解析出对应的二进制文件名
- 在
~/.tacticremote/bin中查找当前版本的缓存二进制 - 如果没有缓存,就从 GitHub Releases 下载 gzip 压缩的二进制,解压后写入磁盘并设置执行权限
- 启动该二进制,继承标准输入输出,并透传
SIGINT、SIGTERM等信号
首次运行会下载对应 release 资产并缓存到本地。此后再次运行时,会复用当前 npm 包版本对应的本地二进制。
这个模式并不新鲜。esbuild 是先驱,turbo 也在用,prisma 有类似做法。核心思路是:npm 是分发渠道,不是编译环境。用它分发一个下载器,而不是依赖树。
双 package.json 方案
我们维护两个独立的包配置文件:
package.json— 开发配置。包含所有 devDependencies(Jest、ESLint、Prettier)、测试脚本、lint 脚本,以及本地开发时使用的完整依赖列表。package.npm.json— 实际发布到 npm 的配置。只包含指向tacticremote.js的bin字段,以及去掉运行时依赖树的精简清单。
这种拆分让开发清单和发布清单各司其职,但也带来一个明确的维护成本:两份清单里重叠的 metadata 必须保持一致。
这也是当前流程里已知的脆弱点。仓库里还没有一条发布期校验来确保这次切换始终正确,所以 CI 自动化仍然是下一步最直接的改进。
30 秒引导流程
解决了分发问题之后,我们对连接流程的每一步进行了审查,逐一消除摩擦。
第 0–5 秒。 用户在终端运行 npx tacticremote。首次运行时,垫片发现没有缓存二进制并下载之。此后每次运行,缓存检查耗时以毫秒计。
第 5–10 秒。 服务器二进制启动。检测本机局域网 IP 地址,生成随机 API key,直接在终端用 Unicode 方块字符打印二维码。无需浏览器,无需导航到任何 Web UI。
第 10–15 秒。 用户在手机上打开 Tactic Remote。主屏显示“连接你的电脑”卡片,包含 npx tacticremote 命令和“扫描二维码”按钮。点击按钮打开相机。
第 15–25 秒。 用户扫描终端里的二维码。二维码编码了一个 JSON payload,包含服务器的 IP 地址、端口和 API key。应用解码 payload,自动保存服务器配置——无需“添加服务器”界面,无需手动输入 IP,无需复制粘贴 API key。
第 25–30 秒。 WebSocket 握手完成。终端输出开始流式传输到手机。连接指示灯变绿。
二维码自动保存这一步消除了最多的摩擦。此前的引导流程需要:点击“添加服务器”、输入 IP 地址、输入端口、输入 API key、点击保存、点击连接。五次点击、三次手动输入,每一步都可能出错。扫码将这一切压缩成了一个手势。
如果重来,我们会怎么做
几处已知的不足,尚未修复:
没有 SHA256 校验。 垫片中的二进制下载器通过 HTTPS 从 GitHub Releases 获取文件,HTTPS 提供了传输安全,但没有对照公开的校验和验证二进制。被篡改的 GitHub release 或 CDN 上的中间人攻击可能投递恶意二进制。我们计划内置硬编码公钥,并验证随每个 release 附带的签名。这是当前实现中优先级最高的安全漏洞。
手动 package.json 同步。 发布前的 package.npm.json → package.json 替换是手工操作。迟早会有人在一个文件里加了字段,另一个忘了改。CI 应该接管这个步骤。
没有 CDN 备用方案。 如果 GitHub Releases 不可用(这种情况确实发生过),二进制下载就会失败,没有任何备用方案。次级镜像——比如把二进制也发布为 npm 包资产——可以提高下载的可靠性。
大图景
这套分发管线围绕一个原则设计:用户不应该需要考虑基础设施。他们不需要知道什么是 Bun,不需要知道自己的 Linux 服务器是什么架构,不需要知道服务器用哪个端口。npx 命令负责运行时,垫片负责二进制,二维码负责连接配置。
服务器本身——架构、单依赖设计、安全模型——在姊妹文章《只有一个依赖:用 ws 包构建生产级服务器》中有详细介绍。这篇文章讲的是如何把那个服务器快速送到用户手中。
如果你还没试过这个 30 秒流程,去 /download 体验一下。
服务器架构详见《只有一个依赖:用 ws 包构建生产级服务器》。安装说明详见快速开始指南。