NanoClaw 是一个个人 AI 助手系统:你在 WhatsApp 里给它发消息,它用 Claude 在 Docker 容器里帮你干活,然后把结果发回来。
1. 整体架构:三层结构
整个系统可以分成三层,就像三明治一样:
用户层
就是 WhatsApp。你在聊天里 @Andy(或者私聊直接说),消息就会被系统接收。底层用的是 Baileys 这个开源库,模拟 WhatsApp Web 协议来收发消息。
宿主层
一个 Node.js 进程,是整个系统的"大脑"。它负责:
- 接收 WhatsApp 消息,存到数据库
- 每 2 秒检查一次有没有新消息需要处理
- 决定要不要启动 AI(比如消息里有没有 @Andy)
- 启动 Docker 容器让 AI 干活
- 把 AI 的回复发回 WhatsApp
- 管理定时任务(比如每天早上 9 点提醒你)
容器层
AI 实际运行的地方。每次需要处理消息时,系统会启动一个 Docker 容器,在里面运行 Claude Agent SDK。容器是一次性的——用完就销毁,下次再起一个新的。容器里有:
- Claude Agent SDK(调用 Claude API)
- Chromium 浏览器(可以帮你上网查东西)
- MCP 服务器(让 AI 能发消息、设定时任务)
2. 核心模块:谁干什么
整个项目大约 2000 行核心代码,模块划分很清晰:
| 模块 | 文件 | 一句话说明 |
|---|---|---|
| 主控 | src/index.ts | 系统的总指挥,把所有模块串起来 |
| 消息通道 | src/channels/whatsapp.ts | 连接 WhatsApp,收发消息 |
| 消息路由 | src/router.ts | 把消息格式化成 XML 给 AI 看,把 AI 回复清理干净发给用户 |
| 容器执行 | src/container-runner.ts | 启动 Docker 容器,传入数据,接收结果 |
| 容器运行时 | src/container-runtime.ts | Docker 命令的薄封装(方便切换到 Apple Container) |
| 队列管理 | src/group-queue.ts | 控制同时最多跑几个容器(默认 5 个) |
| 定时任务 | src/task-scheduler.ts | 每分钟检查有没有到期的定时任务 |
| 进程通信 | src/ipc.ts | 容器内的 AI 想发消息或建任务,通过文件传给宿主 |
| 数据库 | src/db.ts | SQLite 操作:存消息、存任务、存会话 |
| 配置 | src/config.ts | 触发词、超时时间、路径等配置项 |
| 环境变量 | src/env.ts | 从 .env 文件读密钥,不放进 process.env(安全考虑) |
| 路径安全 | src/mount-security.ts | 校验容器要挂载的目录是不是安全的 |
| 文件夹校验 | src/group-folder.ts | 防止目录穿越攻击 |
| 日志 | src/logger.ts | 结构化日志(用 Pino) |
| 类型定义 | src/types.ts | TypeScript 接口定义 |
容器内部也有代码
| 模块 | 文件 | 说明 |
|---|---|---|
| Agent 执行器 | container/agent-runner/src/index.ts | 读取输入 → 调用 Claude → 流式输出结果 |
| MCP 服务器 | container/agent-runner/src/ipc-mcp-stdio.ts | 给 AI 提供工具:发消息、建定时任务、管理群组 |
3. 消息处理流程:一条消息的旅程
当你在 WhatsApp 群里发了一条 @Andy 今天天气怎么样,会发生什么?
4. 数据存储:一个 SQLite 搞定
所有数据都存在一个 SQLite 文件里(store/messages.db),没有 Redis、没有 PostgreSQL,一个文件就够了。
核心表
chats(聊天)
├── jid → WhatsApp 群/人的唯一标识
├── name → 显示名称
├── channel → 来源通道(whatsapp)
└── is_group → 是群还是私聊
messages(消息)
├── id → 消息ID
├── chat_jid → 属于哪个聊天
├── sender → 谁发的
├── content → 消息内容
├── timestamp → 发送时间
├── is_from_me → 是不是自己发的
└── is_bot_message → 是不是 AI 发的
registered_groups(已注册群组)
├── jid → 群标识
├── name → 群名
├── folder → 对应的文件夹名
├── trigger_pattern → 触发正则(如 ^@Andy\b)
├── requires_trigger → 是否需要触发词
└── container_config → 容器配置(额外挂载等)
scheduled_tasks(定时任务)
├── id → 任务ID
├── group_folder → 属于哪个群
├── prompt → 要执行的指令
├── schedule_type → cron / interval / once
├── schedule_value → 具体的时间表达式
├── next_run → 下次执行时间
└── status → active / paused / completed
sessions(会话)
├── group_folder → 群文件夹
└── session_id → Claude Agent SDK 会话ID
5. 容器架构:安全沙箱
这是整个系统最核心的安全设计:AI 在容器里跑,不能直接碰宿主机。
容器内部的文件结构
/workspace/
├── project/ 只读 → 宿主机的项目目录(仅 main 群可见)
├── group/ 读写 → 这个群自己的文件夹
├── global/ 只读 → 全局共享记忆(非 main 群可见)
├── extra/ 按配置 → 额外挂载的目录
└── ipc/ 读写 → 和宿主通信的文件夹
├── input/ → 宿主传给容器的消息
├── messages/ → 容器要发送的消息
└── tasks/ → 容器要创建的任务
输入输出协议
输入:通过 stdin 传入 JSON
{
"prompt": "<messages>...</messages>",
"sessionId": "session-xxx",
"groupFolder": "main",
"chatJid": "123456@g.us",
"isMain": true,
"assistantName": "Andy",
"secrets": {
"ANTHROPIC_API_KEY": "sk-ant-..."
}
}
输出:通过 stdout 流式输出,用特殊标记包裹
---NANOCLAW_OUTPUT_START---
{"status":"success","result":"今天天气晴朗...","newSessionId":"session-yyy"}
---NANOCLAW_OUTPUT_END---
为什么用容器?
- 安全隔离:AI 只能访问你明确允许的文件夹,不能碰
.ssh、.aws等敏感目录 - 环境一致:每次都是干净的环境,不会有残留状态
- 进程隔离:容器崩了不影响宿主进程
- 权限控制:以非 root 用户(uid 1000)运行
6. 群组与权限:Main 群是管理员
系统里有一个特殊的群叫 main(你和自己的私聊),它相当于管理员账号。
| 能力 | Main 群 | 普通群 |
|---|---|---|
| 看自己的聊天记录 | ✓ | ✓ |
| 看别的群的记录 | ✗ | ✗ |
| 写自己的 CLAUDE.md | ✓ | ✓ |
| 写全局 CLAUDE.md | ✓ | ✗ |
| 给自己群发消息 | ✓ | ✓ |
| 给别的群发消息 | ✓ | ✗ |
| 注册/删除群 | ✓ | ✗ |
| 给别的群设定时任务 | ✓ | ✗ |
| 挂载宿主项目目录 | ✓ (只读) | ✗ |
7. 定时任务:AI 的闹钟
你可以让 AI 定时做事情。比如:
"@Andy 每天早上 9 点给我发天气预报"
AI 会调用 MCP 工具创建一个定时任务,存到数据库里。
任务类型
| 类型 | 说明 | 例子 |
|---|---|---|
cron | 按 cron 表达式重复 | 0 9 * * *(每天9点) |
interval | 按固定间隔重复 | 3600000(每小时) |
once | 只执行一次 | 2026-03-01T09:00:00Z |
执行模式
| 模式 | 说明 | 适合场景 |
|---|---|---|
group | 带着聊天历史执行 | "帮我跟进上次说的那件事" |
isolated | 全新上下文执行 | "查一下今天的天气" |
执行流程
8. IPC 通信:容器和宿主怎么说话
容器里的 AI 想发消息或建任务,不能直接调用宿主的代码(进程隔离嘛)。所以用了基于文件的 IPC:
支持的 IPC 操作:
message— 发送消息schedule_task— 创建定时任务pause_task/resume_task/cancel_task— 管理任务register_group— 注册新群(仅 main)refresh_groups— 刷新群列表(仅 main)
9. 记忆系统:CLAUDE.md 文件
AI 的"记忆"就是 CLAUDE.md 文件。Claude Agent SDK 启动时会自动读取工作目录下的 CLAUDE.md。
groups/
├── global/
│ └── CLAUDE.md ← 全局记忆,所有群共享(只有 main 能写)
├── main/
│ └── CLAUDE.md ← 管理员群的记忆
└── 家庭群/
└── CLAUDE.md ← 家庭群的专属记忆
每个群的 AI 只能看到自己的 CLAUDE.md 和全局的 CLAUDE.md,看不到别的群的。AI 在对话中可以修改自己群的 CLAUDE.md 来"记住"事情。
10. 并发控制:不让服务器爆掉
如果 5 个群同时 @Andy,系统不会同时启动 5 个容器让服务器爆掉。GroupQueue 负责并发控制:
每个群同一时间只有一个容器在运行。如果一个群有新消息进来但容器还在跑,消息会排队等容器空闲后再处理。
11. 会话管理:AI 记得上次聊了什么
每个群有一个 session_id,对应 Claude Agent SDK 的一个会话。
- 首次对话:没有 session_id,创建新会话
- 后续对话:传入上次的 session_id,AI 能续接上下文
- 会话存储:在
data/sessions/{群名}/.claude/下,由 SDK 管理 - 自动压缩:对话太长时,SDK 会自动压缩旧内容
这意味着 AI 能记得你们之前聊了什么,不用每次从头开始。
12. 技术栈总结
| 层面 | 技术 | 为什么选它 |
|---|---|---|
| 语言 | TypeScript (ES2022) | 类型安全,Node.js 原生支持 |
| 运行时 | Node.js 20+ | 异步 I/O 天然适合消息处理 |
| Baileys | 最成熟的开源 WhatsApp Web 库 | |
| 数据库 | SQLite (better-sqlite3) | 单文件、零配置、够用 |
| 容器 | Docker | 安全隔离、环境一致 |
| AI | Claude Agent SDK | Anthropic 官方 Agent 框架 |
| 浏览器 | Chromium + agent-browser | 容器内的网页自动化 |
| 日志 | Pino | 高性能结构化日志 |
| 校验 | Zod | 运行时数据校验 |
| 调度 | cron-parser | 解析 cron 表达式 |
| 模块格式 | ESM | 现代 JS 模块标准 |
13. 设计哲学:为什么这么做
1. 简单优先
整个核心大约 2000 行代码,一个人就能完全理解。
没有微服务、没有消息队列、没有 Redis。一个 Node.js 进程 + 一个 SQLite 文件,能跑就行。
2. 安全靠隔离
不在应用层做安全检查,靠操作系统级别的容器隔离。
AI 跑在 Docker 里,只能看到你挂载给它的目录。就算 AI 被提示注入攻击了,也碰不到你的 .ssh 密钥。
3. 用 Skill 扩展,不加功能
核心代码只接受 bug 修复和简化。新功能用 Skill 来加。
想加 Telegram 支持?不是往核心代码里塞,而是写一个 Skill(/add-telegram),用户自己选择要不要装。
4. 文件即协议
宿主和容器之间用文件通信,不用网络。
IPC 用的是文件系统:容器写文件,宿主读文件。简单、可靠、不需要额外的网络配置。
5. 为一个人设计
这不是框架,不是平台,是你自己的工具。
你 fork 下来,按自己的需求改。不需要考虑"其他用户"的兼容性。
14. 目录结构全景
nanoclaw/
│
├── src/ # 宿主进程源代码(约 2000 行)
│ ├── index.ts # 主控:启动、消息循环、Agent 调用
│ ├── channels/whatsapp.ts # WhatsApp 连接和消息收发
│ ├── router.ts # 消息格式化和路由
│ ├── container-runner.ts # 容器启动和 I/O 管理
│ ├── container-runtime.ts # Docker 命令封装
│ ├── group-queue.ts # 并发队列控制
│ ├── task-scheduler.ts # 定时任务调度
│ ├── ipc.ts # 文件 IPC 处理
│ ├── db.ts # SQLite 数据库操作
│ ├── config.ts # 配置常量
│ ├── env.ts # 环境变量读取
│ ├── mount-security.ts # 挂载安全校验
│ ├── group-folder.ts # 路径安全校验
│ ├── logger.ts # 日志
│ └── types.ts # 类型定义
│
├── container/ # 容器镜像相关
│ ├── Dockerfile # 镜像定义(Node.js + Chromium)
│ ├── build.sh # 构建脚本
│ ├── agent-runner/ # 容器内运行的代码
│ │ └── src/
│ │ ├── index.ts # Agent 执行入口
│ │ └── ipc-mcp-stdio.ts# MCP 工具服务器
│ └── skills/ # 容器内可用的 Skill
│
├── groups/ # 群组文件夹
│ ├── global/CLAUDE.md # 全局记忆
│ └── main/CLAUDE.md # 管理员群记忆
│
├── .claude/skills/ # Claude Code 自定义 Skill
│ ├── setup/ # 初始安装
│ ├── customize/ # 自定义扩展
│ ├── debug/ # 调试排障
│ ├── update/ # 更新升级
│ ├── add-telegram/ # 添加 Telegram
│ ├── add-gmail/ # 添加 Gmail
│ └── ...
│
├── setup/ # 安装引导脚本
├── docs/ # 技术文档
├── store/ # 运行时数据(SQLite、WhatsApp 认证)
├── data/ # 应用状态(会话、IPC)
├── logs/ # 服务日志
├── launchd/ # macOS 服务配置
└── scripts/ # 工具脚本
15. 已知问题和局限
| 问题 | 说明 |
|---|---|
| 会话分支过期 | Agent Teams 生成的子 Agent 可能导致 session JSONL 分支冲突 |
| 超时同时触发 | IDLE_TIMEOUT 和 CONTAINER_TIMEOUT 相同时,可能强制杀进程 |
| 游标提前推进 | 消息游标在 Agent 运行前就推进了,超时会导致消息丢失 |
| Apple Container 网络 | macOS 上需要手动配置 IP 转发和 NAT |
| 构建缓存 | Docker buildkit 缓存过于激进,有时需要完全清理重建 |