把开源 Agent 打包成"解压双击即用"的 Windows 便携包:一条命令的完整实现

先说结论:把一个 Node 写的开源 Agent 做成「拷到 U 盘、插到一台从没装过 Node 的电脑、解压双击就能跑」的便携包,难点不在改代码,而在你愿不愿意把"在我机器上能跑"这句话,一个字一个字拆掉,再到一台干净机器上真跑一遍。

这篇我把从动手到验收的完整链路写下来,包括我替你踩平的几个坑。技术栈以 Windows x64 为目标,思路通用。

一、为什么"源码能跑"远远不够

开发机上一切顺手:装好包管理器、装好依赖、node app.mjs gateway 起服务。但交付场景完全是另一回事------目标机可能没有 Node,没有包管理器,甚至下载源也不通。你让对方"先装个 Node 22 以上",很多单子就卡在这第一步。

所以我先定死一条验收线,后面所有决定都围着它转:

一台从没装过 Node 的 Windows,解压、双击、能起来;换台机器,配置和会话还在。

达不到这条,做得再花哨都是自嗨。为了过线,我把交付物拆成焊死在一起的四块:

  1. runtime/ ------ 内置绿色 Node 运行时,目标机零安装;
  2. app/ ------ 程序构建产物 + 裁剪后的生产依赖;
  3. ruyi_workspace/ ------ 自包含状态目录,配置/密钥/会话全在包内;
  4. RuyiClaw.cmd ------ 一键启动器。

二、一条命令产出整个包

我没做成"点七八个脚本",而是封装成一条 pnpm run pack 背后跑一个 scripts/pack.mjs。交付流程必须能一键复现,否则每次打包靠记忆,迟早出错。

2.1 内置绿色 Node

直接从 nodejs.org 取 win-x64 的 .zip (不是 .msi------msi 是要安装的,违背便携初衷),解压出 node.exe 放进 runtime/。脚本里用 fetch 下载、缓存,再解压:

js 复制代码
const zipName = `node-v${version}-win-x64.zip`;
await downloadFile(`https://nodejs.org/dist/v${version}/${zipName}`, zipPath);
// 解压见第三节的 tar 坑

我固定到了 LTS 版本(22.22.3),因为入口脚本要求 Node ≥ 22.19,固定版本可复现。

2.2 裁剪生产依赖:npm pack 还是 pnpm deploy?

这是最容易翻车的地方。项目是 pnpm 工作区,还带一堆内置扩展。社区第一反应都是 pnpm deploy --prod。我一开始也想这么干,但停下来想清楚:交付包真正要的性质是「确定性」------同样的输入永远得到同样的产物,而且最好跟用户真实安装时拿到的一模一样。

我的选择是 npm pack,理由很实在:

  • npm pack 严格按 package.jsonfiles 白名单打包,还会带上 npm-shrinkwrap.json
  • 这恰恰是用户 npm i -g <pkg> 时拿到的那份东西;
  • 把这个 tgz 解开当作 app/,再在里面 npm install --omit=dev,靠 shrinkwrap 把依赖版本钉死------这次装 297 个包,下次还是这 297 个。
js 复制代码
// 1) 产出与发布一致的 tarball
runStep("npm", ["pack", "--ignore-scripts", "--pack-destination", cacheDir]);
// 2) 解开为 app/,再用 shrinkwrap 钉死生产依赖
runStep("npm", ["install", "--omit=dev", "--no-audit", "--no-fund"], appDir);

pnpm deploy 当然也能用,但它的产物完整性要你自己反复验证,而 shrinkwrap 这条路本身就是发布路径,我更信它。

选型的本质不是比谁更专业,是比谁更贴合你真正要的那个性质。 我要确定性 + 和真实安装一致,npm pack 直接命中。

三、我替你踩过的坑:tar 不是只有一个 tar

第一次跑卡在解压,报错 tar: Cannot connect to D: resolve failed。这是 GNU tar 把 D:\... 里的盘符冒号当成了「远程主机:路径」,想去连一台叫 D 的机器。

换写法后,解压 Node 的 zip 又报 tar: This does not look like a tar archive------因为当前环境里 tar 解析成了 GNU tar,而 GNU tar 根本不处理 zip

解法分两层:

  • 给 tar 的路径一律用「相对文件名 + 设好 cwd」,绝不把带盘符冒号的绝对路径塞给它;
  • 在 Windows 上直接固定调用 C:\Windows\System32\tar.exe,它是 bsdtar(libarchive),既能读写 zip 又能读 tgz,还认盘符。
js 复制代码
const TAR_BIN = process.platform === "win32"
  ? path.join(process.env.SystemRoot || "C:\\Windows", "System32", "tar.exe")
  : "tar";

这个坑的价值在于提醒你:便携包脚本的运行环境本身就是不确定的 ,同一个 tar 在 PowerShell、cmd、不同 shell 里可能是三个不同程序。做交付件,就得把这种隐性假设一个个挖出来钉死。

四、状态自包含,这块我盯得最紧

启动器 RuyiClaw.cmd 每一行都在为"便携"服务:

bat 复制代码
set "PATH=%~dp0runtime;%PATH%"
set "OPENCLAW_STATE_DIR=%~dp0ruyi_workspace"

%~dp0 是脚本自己所在目录,所以无论拖到 U 盘还是别人桌面,路径都现算,没有任何硬编码绝对路径。首次运行从模板生成配置、复制出一个空的密钥模板,提示你填上自己的 key 再运行。我一个字节的 key 都没往包里塞,留的是空模板让你填------既是合规,也是对客户负责。

我没只靠"设了环境变量应该就行"收尾,而是做了个最硬的验证:把宿主机原本的状态目录整个改名挪走,用包内 Node、状态目录指向包里启动服务。结果服务在 127.0.0.1:18789 正常起来,而宿主机那个目录没有被重新创建,所有 sqlite、身份、会话全写进了包内。验完把目录改回去。承诺变成亲眼看过的事实,这才算数。

五、三层验收,必须真机验

我的验收分三层,每层对应客户会不会骂街:

  1. 能起来 :把系统 Node 从 PATH 摘干净,只留包内 runtime,node app/app.mjs --version 打出版本,服务绑上端口。证明目标机真不需要预装 Node。
  2. 状态写在包内:跑完去翻状态目录,配置/数据库/身份都在里面,宿主主目录纹丝不动。
  3. 换机器会话还在:状态全在包里,整包复制过去会话自然跟着走。

前两层我能在自己机器上验到死,第三层的物理动作(拿 U 盘插另一台机)是交付现场的事------脚本保证机制成立,最后那一插得你自己做。诚实地区分哪步我验了、哪步要你验,比拍胸脯重要。

六、合规这条线:改品牌不抹来源

底子是 MIT 许可的开源项目。我换上自己的交付名可以,但 LICENSE 和第三方声明原样打进包里,一个字不删。靠人家的开源底子吃饭,署名就得留着。

七、你能照搬的步骤

  1. 把"干净机器解压双击就能跑"写成你自己的验收标准;
  2. 把打包封成一条命令,别留一堆零散脚本;
  3. 挨个挖隐性假设:Node 哪来、依赖怎么锁、状态写哪、key 谁填、tar 是哪个 tar;
  4. 拿一台没装过 Node 的机器真跑一遍。

把这套跑通,你交付的就不再是"源码",而是一个产品。

相关推荐
没事别瞎琢磨3 小时前
十一、审计与 Run Session——每一步操作都被记录
人工智能·node.js
没事别瞎琢磨3 小时前
十六、AgentSandbox——把所有模块串起来的编排类
人工智能·node.js
没事别瞎琢磨3 小时前
十二、网络代理与白名单规则引擎
人工智能·node.js
没事别瞎琢磨3 小时前
十四、Git Worktree 隔离执行
人工智能·node.js
没事别瞎琢磨4 小时前
十、统一 Runner 入口——能力检测与模式回退
人工智能·node.js
没事别瞎琢磨5 小时前
八、环境隔离——构建安全的子进程环境
人工智能·node.js
没事别瞎琢磨6 小时前
六、输出捕获与截断
人工智能·node.js
没事别瞎琢磨6 小时前
七、敏感路径预检——Protected Paths
人工智能·node.js
没事别瞎琢磨6 小时前
五、进程执行——spawn、超时与进程树清理
人工智能·node.js