# 构建与运行时 NanoClaw 运行的是分拆式技术栈:宿主机使用 Node + pnpm,agent 容器使用 Bun。它们之间仅通过每个 session(会话)的两个 SQLite 文件来通信——它们之间没有共享模块,这也是它们能干净地使用不同运行时的原因。 ## 为什么分拆 - **宿主机保持使用 Node**,因为 Baileys(WhatsApp)依赖 `libsignal-node` 原生绑定和久经考验的 WebSocket/HTTP 栈。Bun 的 Node-API 兼容性已经有所改善,但这不是我们想冒险的地方。 - **容器使用 Bun**,因为 `bun:sqlite` 是内置的(无需每次镜像重建都编译 `better-sqlite3` 原生模块),源码直接运行(无需在镜像构建或 session 唤醒时做 tsc 构建步骤),而且 `bun install` 比 `npm 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-code`、`agent-browser`、`vercel`)。这些是 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` 与锁文件之间的任何偏差都会导致构建失败。 ## 供应链 - **宿主机 + 全局 CLI**(pnpm):`minimumReleaseAge: 4320`(新版本需要等待 3 天),`onlyBuiltDependencies` 白名单用于 postinstall 脚本。见 `pnpm-workspace.yaml` 和 `docs/SECURITY.md`。 - **Agent-runner**(Bun):无发布年龄策略——Bun 目前没有等效机制。防御措施是 `bun.lock` 锁定版本加上通过 Dockerfile ARG 固定版本的 CLI/Bun 本身。当升级 `@anthropic-ai/claude-agent-sdk` 或任何运行时依赖时,请查看 npm 上的发布日期并有意识地升级,而不是通过 `bun update`。 ## 镜像构建范围 `container/Dockerfile` 是基于 `node:22-slim` 的单阶段构建: - **固定的 ARG**——`BUN_VERSION`、`CLAUDE_CODE_VERSION`、`AGENT_BROWSER_VERSION`、`VERCEL_VERSION`。在 PR 中有意识地升级。 - **CJK 字体**——`ARG INSTALL_CJK_FONTS=false`。`container/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` 未变更时的重建速度很快。需要 BuildKit(Docker 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'`。绕过 tini(Docker 默认的 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-lockfile` 在 `container/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 test` 在 `container/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-code`、`vercel` 以及 agent 调用的任何未来 Node CLI 都应在 Dockerfile 的 pnpm 全局安装块下固定版本。`bun install -g` 会绕过 pnpm 供应链策略。 ## 迁移历史 这一结构替换了统一的 npm-on-Node 技术栈(宿主机和容器均为 Node)。pnpm 迁移首先落地(PR #1771),使宿主机纳入供应链策略,然后容器迁移到 Bun 以消除原生模块编译和每次唤醒的 tsc 步骤。选择分拆而非完全转用 Bun,是因为 Baileys 的原生依赖是宿主机上的主要风险面——容器没有这样的依赖,因此可以在不承担风险的情况下受益于 Bun。