客户的机器不允许装软件。这个简单的限制决定了整个产品的前两年。
背景是这样的:我需要把 openclaw(一个开源 AI 助手框架)打成一个可以在 Windows 上开箱即用的产品。客户是完全不懂技术的计算机新手,他们不知道什么是"端口"、"环境变量"、"管理员权限",不看得懂英文错误码,害怕 SmartScreen 和 Defender 的弹窗,会在插着的状态下直接拔 U 盘。更关键的是,他们的机器常年被企业 IT 部门的安全策略锁定——既无法装 WSL2,也无法获得管理员权限运行任何安装程序。
这个约束排除了所有常见的分发方式。传统的 Windows 应用安装程序需要管理员权限或系统权限。虚拟机方案需要巨大的镜像和首次解压时间。云端方案对网络有依赖,在公司代理、DNS 污染的环境下会宕掉。唯一可行的路径是:把整个运行环境打进一个 USB 设备里,用户只需要插入、等待、使用。
选 USB 便携方案本身还不够,它带来了三个硬约束,每一个都对应方案里的一条技术线。
第一个约束:运行时必须自包含
USB 设备不能依赖宿主系统已经装了什么。一个 Windows 用户可能根本没有 Node.js、Python 或任何开发工具。Defender 可能会误删或隔离系统 DLL。PowerShell 版本可能太老。这意味着 openclaw 需要的所有东西都要打进去——Node 24、Python 3.11、ffmpeg、7zip、imagemagick、curl。
自包含运行时的代价是体积。便携 Node 约 70 MB,Python 约 120 MB,工具链 200 MB,openclaw 本体 1.5 GB,Python 依赖 1.8 GB,15-20 个内置 skill 各自的依赖又是一层。最后 U 盘占用达到约 3.8 GB。32 GB 的 U 盘只剩 28 GB 给用户数据——对消费级应用这不算离谱,但对工具链意味着没有多余空间放实验性模块。
被放弃的选项是"精简运行时"——比如只装 Node,让 Python 用户自己装。这个想法在企业环境里立刻失效。被放弃的另一个是"延迟加载"——首启时解压热缓存,冷依赖等用户第一次调用时再下载。这要求联网,违反了"完全离线可用"的设计目标。
自包含运行时的好处是,USB 从 A 机器拔出、插到 B 机器,配置完全一致。用户关机不退出、下次插进去期望一切还在——这个看似理所当然的用户心智其实很难满足,除非整个依赖树都在本地盘符上。
第二个约束:状态必须能在 U 盘与本地缓存之间同步
首启性能是第二个杀手约束。主流用户期望"从插入 U 盘到能用"不超过 10 秒。但 3.8 GB 的解压按 USB 2.0 速度需要几十秒。
解决方案是热/冷缓存分离。180 MB 的热缓存包含 Gateway 核心、基础 skill、一个默认的 AI provider——这部分在启动器的 5 秒内就解压完了,托盘就能亮起来,用户可以开始聊天。剩下的 1.3 GB 冷缓存(所有 IM 渠道插件、所有备选 provider、60+ 个 bundled skill)在后台悄悄解压,进度条显示在托盘气泡里,用户感受不到阻塞。
这个设计带来了状态一致性的问题。进程在解压中途可能崩溃、电源可能掉、用户可能直接拔 U 盘。启动器必须能识别"哪些文件已经写好、哪些只写了一半"。采用的方案是原子 rename——解压到临时目录,全部成功后一次性 rename 到目标位置,这样就天然避免了半写状态。
但这还不够。U 盘本身可能是 FAT32(有 4 GB 单文件限制),可能只有 USB 2.0 接口,可能被系统弄脏(比如在双系统里被 Linux 写过)。本地缓存目录可能不可写(企业 Defender 的 Controlled Folder Access)。启动器需要一个兜底方案——如果本地 %LOCALAPPDATA% 无法写入,就把缓存降级回 U 盘的 cache-fallback/ 目录,虽然性能差点但总能跑起来。
被放弃的方案是"只在 U 盘上缓存"——这样省了跨盘符的状态管理,但牺牲了启动速度(USB 2.0 全程卡)。被放弃的另一个是"每次启动都完整重解压"——这样没有任何状态一致性问题,但两分钟的首启时间不可接受。
状态同步的好处是,插到企业机器上,Defender 可能在后台扫描,杀软可能隔离了某个 .node 文件,重启一次启动器就能自动从备份重解压修复。缓存机制同时是防护层。
第三个约束:更新不能依赖安装程序
传统 Windows 应用的升级走安装程序,需要管理员权限。便携方案无法这样做。
变成了 OTA(Over-The-Air)——启动器后台每 4 小时检查一次更新,有新版本就下载到 updates/staging/ 目录。下载可能中断(网络抖动)、磁盘空间可能满、校验可能失败。下一次启动时,启动器用双槽 A/B 的策略:校验完整性后原子切换,current/ 变成 rollback/,staging/ 变成 current/。如果新版本连续崩溃 3 次,自动回滚到 rollback/ 的旧版。
被放弃的是"差分更新"——用 bsdiff 只传输变化部分。这能省流量,但增加了失败模式的复杂性:差分包损坏、老版本号错误、合并失败都无法自愈。Phase 1 就用整包替换,虽然 150-300 MB 的新版本下载可能要几分钟,但完全幂等、完全可回滚。
被放弃的另一个是"依赖云端激活服务"——每次更新都校验签名、校验设备有没有授权。这样 Phase 2 才行,当前还没有激活系统。Phase 1 的 OTA 使用本地签名校验,master public key 内置在启动器,本地一样能验——即使完全离线也能回滚。
OTA 机制的约束是,一旦发布错误版本,马上就会在数千台设备扩散。所以后台需要发布管理系统:单管理员账号、版本列表、三通道(canary / beta / stable)切换、一键回滚。
架构沿着这三条线演进
这三个约束会在接下来的两年里演化成整个架构的基石。
自包含运行时的约束延伸出来是 Launcher + Tauri 桌面软件的两层壳。Launcher 是纯 Rust 静态编译的 8 MB 小程序,处理 U 盘识别、缓存管理、五层状态机、68 项兜底自愈。Tauri 桌面软件是配置中心,用户改 AI provider key、配置 IM 渠道、管理 skill 都在这里。openclaw 原生 WebUI 保持零改动,只负责聊天。三层进程分离,各自独立,启动器可以在不启动 Tauri 的情况下让 Gateway 跑起来。
状态同步的约束逐步演化成缓存一致性机制。V8 启动快照能压缩 Node 初始化时间,从 2-4 秒压到 0.3-0.8 秒。并行 zstd 多线程解压能把冷缓存解压从 10 秒压到 3-4 秒。但更根本的是,缓存分层之后,系统具有"部分就绪"的能力——即使某个可选模块损坏,基础聊天功能照样可用。同样的理念也用在了设计中 skill 的三层存放上(bundled / 预装 / workspace)。
OTA 机制到 Phase 2 时扩展成激活系统。用户输入激活码,后台签发 license(30 天刷新一次),每台机器的 license 通过 AES-256-GCM 用 VolumeSerialNumber 派生密钥加密,丢失也无法读。这样 license 可以明文放在 U 盘 data/ 目录,不用担心被盗。激活系统同时引入了 policy.yml 的远程策略下发,设计上预留了强制切自建 AI 网关的能力。
如果单机便携方案验证成功、用户数量持续增长,自然会遇到容量瓶颈。到那时单机迟早要让位于某种集中部署方案——当时并没有具体方案。USB 启动器定下的三个约束——自包含、状态可靠、OTA 更新——从一开始就会成为产品基因,很难改掉。
所有这些约束换来的设计目标是什么。首启 5 秒托盘亮、10 秒全功能就绪、完全离线可用、热拔无损、自动自愈。用户只需要会"插 U 盘、等待、用"。从这个意义上,设计不是为了展示技术,而是为了消除用户需要知道的东西。
■