跳到主要内容

数据截至 (上游 commit 53ea1e8ba6fd)

NanoClaw — 架构与原理

30 秒导读: NanoClaw 把「个人 AI 助理」做成一个常驻的 Node 主机进程 + 一堆按需拉起的 Docker 容器。你在 Telegram/Slack/iMessage 里 @ 它,主机把消息写进一个 SQLite 文件,容器里的 Claude Code 读到、干活、把回复写进另一个 SQLite 文件,主机再发回聊天软件。它最值得学的不是「怎么调模型」,而是「怎么让主机和沙箱之间只靠两个文件通信,还不出错」。


1. 这是什么(零基础也能懂)

一句话定义

NanoClaw 是一个跑在你自己机器上的聊天式 AI 助理:你在日常用的聊天软件里跟它说话,它在隔离的 Linux 容器里替你干活(读文件、跑命令、上网、发消息)。

解决什么问题、给谁用

设想这个场景:你想让 AI 助理帮你管日程、盯 GitHub、每周五整理一次周报,而且希望直接在微信/Telegram 里跟它说,而不是打开某个网页。问题来了——你敢把「能跑任意 shell 命令」的 AI 直接放在自己电脑上吗?

NanoClaw 的答案是:能跑,但只在容器里跑,而且只能看见你明确挂进去的目录

它面向的是「一个人 + 自己的机器」,不是团队 SaaS:

你是谁你会怎么用它
想要私人助理的个人开发者clone 下来跑 bash nanoclaw.sh,配一个 IM 渠道,开始 @ 它
关心安全的自建党看得懂全部代码(主机侧 ~2.4 万行 TS,容器侧 ~5700 行),容器 + 凭据网关双重边界
想自己改的人项目明确说「定制 = 改代码」,没有配置文件迷宫,让 Claude Code 直接改你的 fork

它能做什么

  • 多渠道接入 —— WhatsApp、Telegram、Discord、Slack、iMessage、Teams、邮件等,由 /add-<channel> 技能按需装进来(主干自带任何渠道适配器)。
  • 每个 agent 一套工作区 —— 自己的 CLAUDE.md、自己的记忆目录、自己的容器、自己的挂载白名单。
  • 定时任务 —— cron 式重复任务,还能挂一个「预检脚本」:脚本说没事做就不唤醒 agent(省 token)。
  • 容器隔离 —— agent 跑在 Docker 里,Bash 工具的杀伤半径被限制在容器内。
  • 凭据不落地到 agent —— API key 由 OneCLI Agent Vault 在请求发出时注入,容器里拿不到明文。
  • agent 改自己 —— agent 可以申请「给我装个 apt 包 / 接一个 MCP server」,走人工审批后主机重建镜像、重启容器。

用起来什么样

装:

git clone https://github.com/nanocoai/nanoclaw.git nanoclaw-v2
cd nanoclaw-v2
bash nanoclaw.sh

用(在你配好的聊天窗口里,默认触发词 @Andy):

@Andy 每个工作日早上 9 点给我发一份销售管线概览(可以读我的 Obsidian 目录)
@Andy 暂停周一那个简报任务

运维(在主机终端里,ncl 是它的管理 CLI):

ncl groups list # 有哪些 agent 工作区
ncl wirings list # 哪个聊天群接到了哪个 agent
ncl tasks list # 定时任务
ncl sessions list # 当前会话(只读)

一句话直觉

把它想成一个「消息中转站 + 一次性工位」:

  • 聊天软件是大厅,主机进程是前台
  • 每个会话是一间独立工位(一个容器),下班就拆(容器 --rm)。
  • 前台和工位之间没有对讲机,只有一个收件筐和一个发件筐——两个 SQLite 文件。前台只往收件筐放,工位只往发件筐放,谁也不碰对方的筐。

这段只是直觉比喻,不是术语。 后面各章一律用「主机进程 / 容器 / inbound.db / outbound.db」这套名字。这个「两个筐」的设计是整个项目的技术核心,第 2 章专讲。


2. 顶层全景(它大概怎么转)

2.1 一张图看清结构

怎么读这张图:从上往下是四层——平台、常驻的单一 Node 主机进程、两个 SQLite 文件、按需拉起的容器;圈号 ①~⑤ 对应下面 §2.2 表里的部件;中间那一层的两个 .db 文件是唯一的跨进程边界

┌─ 平台(Telegram / Slack / iMessage / …)
│ ↓ 平台把消息交进来 ↑ 主机把回复发回去

├─ 主机进程:单个常驻 Node 进程,代码在 src/
│ ① 渠道适配器:把某个平台的收发翻译成统一的入站事件
│ ② 路由 router:查中央库 data/v2.db(用户 / 群 / agent / 接线),定 agent 与会话
│ ③ 会话管理 + 容器运行器:消息写进 inbound.db,再 docker run 拉起该会话的容器
│ ④ 投递 delivery:轮询 outbound.db,把 agent 的输出交给适配器发回平台
│ 另有 60 秒一次的巡检 host-sweep,兜底所有异常路径(见 §2.3 第 7 步)

├─ 跨进程边界:会话目录里的两个 SQLite 文件
│ 没有 socket、没有管道、没有文件监听,两边都是轮询
│ inbound.db 主机写、容器读 —— 主机 → 容器 的唯一通道
│ outbound.db 容器写、主机读 —— 容器 → 主机 的唯一通道

└─ 容器:每会话一个,退出即删(--rm)
⑤ agent-runner(Bun + Claude Agent SDK):轮询 inbound.db 干活,结果写 outbound.db

2.2 部件一句话职责

部件干什么在哪个文件
入口起 DB、跑迁移、拉起适配器、开三个轮询src/index.ts
渠道适配器把某个 IM 平台的收发抽象成统一接口src/channels/adapter.ts(接口)、src/channels/channel-registry.ts(注册表)
路由 router决定这条消息该给哪个 agent、落到哪个会话src/router.ts
会话管理建会话目录、开关两个 session DB、写消息行src/session-manager.ts
容器运行器docker run 参数、算挂载、拉起/杀掉容器src/container-runner.ts
投递 delivery轮询 outbound.db,把 agent 的输出发回平台src/delivery.ts
巡检 host-sweep60 秒一次:同步状态、判卡死、重试、重复任务重排src/host-sweep.ts
agent-runner容器内的轮询循环:读消息 → 调 provider → 写回复container/agent-runner/src/poll-loop.ts
guard所有特权动作(建 agent、装包、跨 agent 发消息)的唯一决策点src/guard/guard.ts
ncl CLI管理中央库的命令行,主机走 Unix socket、容器走 session DBsrc/cli/dispatch.tscontainer/agent-runner/src/cli/ncl.ts

2.3 主线走一遍(高层,不进代码)

  1. 平台来消息。 适配器把它交给 routeInbound,主机顺手盖上「是哪个适配器实例收到的」这个戳。
  2. 路由决定归属。 查中央库:这个聊天窗口接了哪些 agent?这条消息够不够触发(被 @ 了?匹配正则?)?发消息的人有没有权限?
  3. 落到会话。 每个 (agent, 聊天群, 线程) 组合对应一个会话;会话在磁盘上是一个目录,里面有 inbound.dboutbound.db。主机把消息写进 inbound.db
  4. 唤醒容器。 如果这个会话没有容器在跑,docker run 拉一个,把会话目录挂进去。
  5. 容器干活。 容器里的 agent-runner 每 0.5~1 秒轮询一次 inbound.db,读到新消息就格式化成 prompt 交给 Claude Agent SDK;要发消息就写进 outbound.db
  6. 主机投递。 主机每 1 秒轮询运行中会话的 outbound.db,把新行通过适配器发回平台,并在 inbound.db 的 delivered 表里记一笔「这条发过了」。
  7. 巡检兜底。 每 60 秒一次的 sweep 负责所有异常路径:容器崩了、消息卡住了、到点的定时任务该唤醒了、重复任务该重排了。

注意第 3 步和第 6 步之间没有任何「通知」机制——没有信号、没有管道、没有文件监听。两边都是轮询 SQLite。这个看起来「土」的选择,恰恰是它能在 Docker Desktop 的 virtiofs 挂载上稳定工作的原因,第 2 章会讲清楚为什么。


3. 阅读地图

建议顺序(每章都能独立读,但按序读收益最大):

顺序章节讲什么你会带走什么
101-message-lifecycle.md一条消息的一生,主线端到端整个系统怎么串起来
202-two-db-ipc.md两个 SQLite 当 IPC 的全部工程细节本项目最值钱的一章:跨挂载文件通信的坑与解法
303-container-isolation.md容器怎么起来、挂了什么、凭据怎么隔离一套可抄的「给 AI 开沙箱」清单
404-agent-runner.md容器内的轮询循环与投递门流式输出怎么做到「不漏发、不重发」
505-entities-and-guard.md实体模型、触发策略、特权动作闸门多 agent × 多渠道的建模方式
606-extensibility.md模块注册表、skill 安装、定时任务「主干不带功能、按需装」的实现手法
707-essence-and-limits.md精华 / 局限 / 对比 / 总代码地图什么该抄、什么别抄

只想看一章? 想学跨进程通信 → 第 2 章;想学 AI 沙箱 → 第 3 章;想学 agent 输出的可靠投递 → 第 4 章。


4. 规模速览(用来校准预期)

指标数值怎么来的
主机侧源码(不含测试)约 23,600 行 TypeScriptsrc/**/*.ts 排除 *.test.ts
主机侧测试约 20,900 行src/**/*.test.ts
容器侧 agent-runner(不含测试)约 5,700 行container/agent-runner/src/**/*.ts
测试文件数126 个src + container 下的 *.test.ts
运行时依赖8 个package.json dependencies

测试量几乎和主机源码等量——这是个「注释比代码长、测试比代码多」的项目。读它的时候,注释本身就是设计文档:很多关键决策(为什么用 DELETE journal、为什么开一次关一次、为什么 seq 分奇偶)只写在源码注释里。


5. 代码地图(导航索引)

主题文件路径符号名
进程入口与启动顺序src/index.tsmain
优雅关停src/index.tsshutdown
入站路由主函数src/router.tsrouteInbound
会话解析与创建src/session-manager.tsresolveSession
容器唤醒src/container-runner.tswakeContainer
出站投递轮询src/delivery.tsdeliverSessionMessages / drainSession
60 秒巡检src/host-sweep.tssweep / sweepSession
容器内主循环container/agent-runner/src/poll-loop.tsrunPollLoop
中央库 schema(参考副本)src/db/schema.tsSCHEMA
会话双库 schemasrc/mailbox/sqlite/schema.tsINBOUND_SCHEMA / OUTBOUND_SCHEMA
特权动作决策src/guard/guard.tsguard