Technical Architecture

NanoClaw 架构分析

用大白话讲清楚这个项目是怎么设计的、怎么运行的、为什么这么做。

一句话总结

NanoClaw 是一个个人 AI 助手系统:你在 WhatsApp 里给它发消息,它用 Claude 在 Docker 容器里帮你干活,然后把结果发回来。

1. 整体架构:三层结构

整个系统可以分成三层,就像三明治一样:

用户层(WhatsApp)你发消息的地方
宿主层(Node.js 进程)消息调度中心
容器层(Docker + Claude Agent)AI 真正干活的地方

用户层

就是 WhatsApp。你在聊天里 @Andy(或者私聊直接说),消息就会被系统接收。底层用的是 Baileys 这个开源库,模拟 WhatsApp Web 协议来收发消息。

宿主层

一个 Node.js 进程,是整个系统的"大脑"。它负责:

容器层

AI 实际运行的地方。每次需要处理消息时,系统会启动一个 Docker 容器,在里面运行 Claude Agent SDK。容器是一次性的——用完就销毁,下次再起一个新的。容器里有:

2. 核心模块:谁干什么

整个项目大约 2000 行核心代码,模块划分很清晰:

模块文件一句话说明
主控src/index.ts系统的总指挥,把所有模块串起来
消息通道src/channels/whatsapp.ts连接 WhatsApp,收发消息
消息路由src/router.ts把消息格式化成 XML 给 AI 看,把 AI 回复清理干净发给用户
容器执行src/container-runner.ts启动 Docker 容器,传入数据,接收结果
容器运行时src/container-runtime.tsDocker 命令的薄封装(方便切换到 Apple Container)
队列管理src/group-queue.ts控制同时最多跑几个容器(默认 5 个)
定时任务src/task-scheduler.ts每分钟检查有没有到期的定时任务
进程通信src/ipc.ts容器内的 AI 想发消息或建任务,通过文件传给宿主
数据库src/db.tsSQLite 操作:存消息、存任务、存会话
配置src/config.ts触发词、超时时间、路径等配置项
环境变量src/env.ts从 .env 文件读密钥,不放进 process.env(安全考虑)
路径安全src/mount-security.ts校验容器要挂载的目录是不是安全的
文件夹校验src/group-folder.ts防止目录穿越攻击
日志src/logger.ts结构化日志(用 Pino)
类型定义src/types.tsTypeScript 接口定义

容器内部也有代码

模块文件说明
Agent 执行器container/agent-runner/src/index.ts读取输入 → 调用 Claude → 流式输出结果
MCP 服务器container/agent-runner/src/ipc-mcp-stdio.ts给 AI 提供工具:发消息、建定时任务、管理群组

3. 消息处理流程:一条消息的旅程

当你在 WhatsApp 群里发了一条 @Andy 今天天气怎么样,会发生什么?

你发消息: "@Andy 今天天气怎么样" WhatsApp (Baileys) 收到消息 存入 SQLite 数据库 (messages 表) 消息循环(每2秒轮询一次)发现新消息 检查:这个群注册了吗?消息匹配触发词吗? ✓ 群已注册,触发词 @Andy 匹配 获取这个群最近的聊天记录,格式化成 XML 加入群队列(GroupQueue) 检查:同时运行的容器数 < 5? 启动 Docker 容器 - 挂载群文件夹(读写) - 挂载全局记忆(只读) - 通过 stdin 传入消息和密钥 容器内:Claude Agent SDK 处理消息 - 读取 CLAUDE.md(群记忆) - 调用 Claude API - 可能使用工具(上网搜索、浏览器等) AI 生成回复,通过 stdout 流式输出 宿主进程接收回复,通过 WhatsApp 发回给你 更新游标(标记这条消息已处理) 更新会话 ID(下次可以续接对话) 容器关闭,释放并发槽位 队列里有等待的群?启动下一个 你收到回复: "今天北京晴,最高温度..."

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---

为什么用容器?

  1. 安全隔离:AI 只能访问你明确允许的文件夹,不能碰 .ssh.aws 等敏感目录
  2. 环境一致:每次都是干净的环境,不会有残留状态
  3. 进程隔离:容器崩了不影响宿主进程
  4. 权限控制:以非 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全新上下文执行"查一下今天的天气"

执行流程

定时器每 60 秒检查一次 有到期任务? → 加入群队列 启动容器 → 执行任务 → 发送结果 计算下次执行时间 → 更新数据库 记录执行日志(耗时、状态、结果)

8. IPC 通信:容器和宿主怎么说话

容器里的 AI 想发消息或建任务,不能直接调用宿主的代码(进程隔离嘛)。所以用了基于文件的 IPC

容器里的 AI 想发消息 MCP 工具写入 /workspace/ipc/messages/msg-xxx.json 宿主的 IPC 监听器发现新文件 读取 JSON → 验证权限 → 通过 WhatsApp 发送 删除已处理的文件

支持的 IPC 操作:

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 负责并发控制:

收到消息 → 加入群队列 当前运行数 < 5? 立即执行 放入等待队列 执行完毕 有空位了? 释放槽位 ←──────── 取出执行

每个群同一时间只有一个容器在运行。如果一个群有新消息进来但容器还在跑,消息会排队等容器空闲后再处理。

11. 会话管理:AI 记得上次聊了什么

每个群有一个 session_id,对应 Claude Agent SDK 的一个会话。

这意味着 AI 能记得你们之前聊了什么,不用每次从头开始。

12. 技术栈总结

层面技术为什么选它
语言TypeScript (ES2022)类型安全,Node.js 原生支持
运行时Node.js 20+异步 I/O 天然适合消息处理
WhatsAppBaileys最成熟的开源 WhatsApp Web 库
数据库SQLite (better-sqlite3)单文件、零配置、够用
容器Docker安全隔离、环境一致
AIClaude Agent SDKAnthropic 官方 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 缓存过于激进,有时需要完全清理重建