Files
nanoclaw/docs/zh/build-and-runtime.md
2026-05-12 13:14:17 +00:00

6.4 KiB
Raw Permalink Blame History

构建与运行时

NanoClaw 运行的是分拆式技术栈:宿主机使用 Node + pnpmagent 容器使用 Bun。它们之间仅通过每个 session会话的两个 SQLite 文件来通信——它们之间没有共享模块,这也是它们能干净地使用不同运行时的原因。

为什么分拆

  • 宿主机保持使用 Node,因为 BaileysWhatsApp依赖 libsignal-node 原生绑定和久经考验的 WebSocket/HTTP 栈。Bun 的 Node-API 兼容性已经有所改善,但这不是我们想冒险的地方。
  • 容器使用 Bun,因为 bun:sqlite 是内置的(无需每次镜像重建都编译 better-sqlite3 原生模块),源码直接运行(无需在镜像构建或 session 唤醒时做 tsc 构建步骤),而且 bun installnpm install 快大约 5-10 倍。

宿主机和容器各有自己的包树:

/                             pnpm + Node 22
  pnpm-lock.yaml              宿主机依赖频道、Chat SDK、Baileys、better-sqlite3 等)
  pnpm-workspace.yaml         minimumReleaseAge + onlyBuiltDependencies 策略

/container/agent-runner/      Bun 1.3+
  bun.lock                     agent-runner 运行时依赖Claude Agent SDK、MCP SDK、zod 等)
  package.json                 @types/bun、typescript 开发依赖用于类型检查

容器镜像内部也有 pnpm + Node用于全局 CLI@anthropic-ai/claude-codeagent-browservercel)。这些是 agent 在运行时调用的 Node 二进制文件,不是库依赖。将它们保持在 pnpm 上可以保留 CLI 版本的供应链策略。

锁文件

锁文件 管理器 依赖变更后重新生成
宿主机 pnpm-lock.yaml pnpm 10 pnpm install
Agent-runner container/agent-runner/bun.lock Bun 1.3+ cd container/agent-runner && bun install

两者都已提交。CI 和 Dockerfile 运行 --frozen-lockfile 变体——package.json 与锁文件之间的任何偏差都会导致构建失败。

供应链

  • 宿主机 + 全局 CLIpnpmminimumReleaseAge: 4320(新版本需要等待 3 天),onlyBuiltDependencies 白名单用于 postinstall 脚本。见 pnpm-workspace.yamldocs/SECURITY.md
  • Agent-runnerBun无发布年龄策略——Bun 目前没有等效机制。防御措施是 bun.lock 锁定版本加上通过 Dockerfile ARG 固定版本的 CLI/Bun 本身。当升级 @anthropic-ai/claude-agent-sdk 或任何运行时依赖时,请查看 npm 上的发布日期并有意识地升级,而不是通过 bun update

镜像构建范围

container/Dockerfile 是基于 node:22-slim 的单阶段构建:

  • 固定的 ARG——BUN_VERSIONCLAUDE_CODE_VERSIONAGENT_BROWSER_VERSIONVERCEL_VERSION。在 PR 中有意识地升级。
  • CJK 字体——ARG INSTALL_CJK_FONTS=falsecontainer/build.sh.env 读取 INSTALL_CJK_FONTS 并传递。默认构建节省约 200MB当用户处理中文/日文/韩文内容时选择加入。
  • BuildKit 缓存挂载——/var/cache/apt/var/lib/apt/root/.bun/install/cache/root/.cache/pnpm。当 package.json/bun.lock 未变更时的重建速度很快。需要 BuildKitDocker 23+、Apple Container 兼容下默认启用)。
  • tini 作为 init——回收 Chromium 僵尸进程,转发信号以便在 SIGTERM 时完成正在进行的 outbound.db 写入。
  • entrypoint.sh(已提取)——exec bun run /app/src/index.ts 在 tini 下运行。可读且可 diff。
  • 无编译后的 /app/dist——Bun 直接运行 TS。宿主机在 session 启动时还会将最新源码挂载到 /app/src 上,因此宿主机的编辑无需重建镜像即可生效。

Session 唤醒(两条路径)

  1. 基础镜像 ENTRYPOINT——用于 stdin 管道测试调用,如 container/build.sh 中的示例:tini --> entrypoint.sh 捕获 stdin 到 /tmp/input.json,然后 exec bun run src/index.ts
  2. 宿主机生成的 session——src/container-runner.ts 第 301 行附近使用 --entrypoint bash-c 'exec bun run /app/src/index.ts'。绕过 tiniDocker 默认的 PID 1 处理生效。stdin 未使用;所有 IO 通过挂载的 session DB 流动。

两条路径最终都由 Bun 运行同一个源码文件 /app/src/index.ts

CI 流程

.github/workflows/ci.yml 安装 Node带 pnpm 缓存)和 Bun然后按顺序运行

  1. pnpm install --frozen-lockfile(宿主机)
  2. bun install --frozen-lockfilecontainer/agent-runner/ 中(容器)
  3. pnpm run format:check
  4. pnpm exec tsc --noEmit(宿主机类型检查)
  5. pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit(容器类型检查)
  6. pnpm exec vitest run(宿主机测试)
  7. bun testcontainer/agent-runner/ 中(容器测试)

任何失败都会导致 PR 失败。

关键不变条件

  • Session DB 必须使用 journal_mode=DELETE WAL 的 -shm 内存映射不会跨宿主机和客户机之间的 VirtioFS 传递。参见 container/agent-runner/src/db/connection.ts 顶部的文档注释和 src/session-manager.ts
  • 容器中的命名 SQL 参数要求在 JS 对象键中使用前缀。 bun:sqlite 不会像宿主机上的 better-sqlite3 那样自动去除 @/$/:。在 SQL 和键中都使用 $name.run({ $id: msg.id })。位置 ? 参数正常工作。
  • Agent-runner 测试在 bun:test 下运行,而非 vitest。 vitest.config.ts 排除了 container/agent-runner/ 树,因为 vitest 在 Node 上运行,无法加载 bun:sqlite
  • 容器镜像中没有 tsc 构建步骤。 重新添加一个会重新引入我们已移除的每个 session 唤醒约 200-500ms 的成本。
  • 全局容器 CLI 保持使用 pnpm而非 Bun。 agent-browser@anthropic-ai/claude-codevercel 以及 agent 调用的任何未来 Node CLI 都应在 Dockerfile 的 pnpm 全局安装块下固定版本。bun install -g 会绕过 pnpm 供应链策略。

迁移历史

这一结构替换了统一的 npm-on-Node 技术栈(宿主机和容器均为 Node。pnpm 迁移首先落地PR #1771使宿主机纳入供应链策略然后容器迁移到 Bun 以消除原生模块编译和每次唤醒的 tsc 步骤。选择分拆而非完全转用 Bun是因为 Baileys 的原生依赖是宿主机上的主要风险面——容器没有这样的依赖,因此可以在不承担风险的情况下受益于 Bun。