6.4 KiB
6.4 KiB
构建与运行时
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 唤醒(两条路径)
- 基础镜像 ENTRYPOINT——用于 stdin 管道测试调用,如
container/build.sh中的示例:tini --> entrypoint.sh捕获 stdin 到/tmp/input.json,然后exec bun run src/index.ts。 - 宿主机生成的 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,然后按顺序运行:
pnpm install --frozen-lockfile(宿主机)bun install --frozen-lockfile在container/agent-runner/中(容器)pnpm run format:checkpnpm exec tsc --noEmit(宿主机类型检查)pnpm exec tsc -p container/agent-runner/tsconfig.json --noEmit(容器类型检查)pnpm exec vitest run(宿主机测试)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。