9.8 KiB
NanoClaw 源码学习路线图
前置知识
阅读源码前,确保理解以下核心概念:
- 两层运行时:Host 进程用 Node/pnpm,Container 进程用 Bun。两者不共享代码,不共享模块。
- 唯一的 IO 界面:Host 和 Container 之间没有 IPC、没有 stdin pipe。两个 SQLite 文件是唯一的通讯界面:
inbound.db— Host 写,Container 读outbound.db— Container 写,Host 读
- seq 奇偶规则:Host 使用偶数 seq,Container 使用奇数 seq,避免双方同时写同一个 DB 时的冲突。
- 实体模型:
users → messaging_groups → agent_groups → sessions,中间通过messaging_group_agents多对多连接。 - 会话不等于代理组:一个 agent group 可以有多个 session(比如不同线程),每个 session 有自己独立的
inbound.db+outbound.db。
建议:先快速浏览
docs/architecture.md和docs/architecture-diagram.md,建立一个心智模型再开始。
第一层:全局鸟瞰(先读文档)
| 顺序 | 文件 | 解决什么问题 |
|---|---|---|
| 1 | docs/architecture.md |
整个系统的架构全貌,消息怎么流转 |
| 2 | docs/architecture-diagram.md |
消息从进到出的可视化链路 |
| 3 | docs/db.md |
三层 DB 模型(central + inbound + outbound) |
| 4 | docs/db-central.md |
中央库 data/v2.db 每一张表的职责和字段 |
| 5 | docs/db-session.md |
会话库的 schema + seq 奇偶分配机制 + cross-mount 不变式 |
| 6 | docs/build-and-runtime.md |
为什么 Host 用 Node,Container 用 Bun?两套锁文件互不干扰 |
第二层:主循环 — 消息从进到出的主干链路
这是整个系统的脊椎。按调用顺序读,每个文件不必求甚解,但要理解数据流向。
| 顺序 | 文件 | 在链路中的角色 |
|---|---|---|
| 7 | src/index.ts |
程序入口:初始化 DB、运行迁移、启动所有 channel adapter、启动 poll sweep、启动 CLI socket server |
| 8 | src/router.ts |
入站路由:收到消息 → 解析 messaging group → 解析 agent group → 权限门检查 → 创建/查找 session → 写 inbound.db → 唤醒容器 |
| 9 | src/session-manager.ts |
会话生命周期:管理 session 目录、打开/关闭 DB、心跳检测、跨 mount 的 journal_mode=DELETE 不变式 |
| 10 | src/delivery.ts |
出站投递:轮询 outbound.db → 通过 channel adapter 发送消息 → 处理系统动作(审批、调度等) |
| 11 | src/host-sweep.ts |
后台清扫:60 秒周期 — processing_ack 同步、僵死容器检测、到期消息唤醒、定时任务触发 |
到这里你应该能回答:一条用户消息从发出来到 agent 回复,经历了哪些步骤。
第三层:容器侧 — Agent 在里面干什么
理解 Host 怎么把消息交给容器之后,钻进去看容器内部。
| 顺序 | 文件 | 在链路中的角色 |
|---|---|---|
| 12 | container/agent-runner/src/index.ts |
容器入口:加载 /workspace/agent/container.json、构建 MCP server 配置、创建 provider、进入 poll loop |
| 13 | container/agent-runner/src/poll-loop.ts |
核心大循环:读 inbound.db → 调 provider(Claude/OpenCode)→ 格式化输出 → 写 outbound.db |
| 14 | container/agent-runner/src/providers/factory.ts |
Provider 工厂:根据 container.json 中的 agent_provider 字段选择 Claude 或 OpenCode |
| 15 | container/agent-runner/src/providers/claude.ts |
Claude Agent SDK 的具体对接实现 |
| 16 | container/agent-runner/src/formatter.ts |
出站消息格式化:按目标 channel 类型做内容适配 |
| 17 | container/agent-runner/src/mcp-tools/index.ts |
MCP 工具注册入口:agent 能调用的所有工具列表 |
第四层:带着问题深入子系统
以下问题按系统层次组织,每个问题后面标注了需要读的关键文件。带着问题去读比按文件线性读高效得多。
全局架构
Q1: 一条用户消息从 Slack 发出,到 agent 回复出现在聊天框里,完整路径是什么?
追踪:
src/router.ts→src/session-manager.ts→container/agent-runner/src/poll-loop.ts→src/delivery.ts。动手画一张时序图,标注每个环节读/写了哪个数据库。
Q2: 为什么 Host 用 Node,Container 用 Bun?两套运行时之间的"协议"是什么?
docs/build-and-runtime.md+src/container-runner.ts
Q3: inbound.db 和 outbound.db 为什么各只能有一个 writer?journal_mode=DELETE 为什么是必须的?
docs/db-session.md+src/session-manager.ts中的注释块
路由与会话
Q4: 一个 messaging group(比如 Slack 频道)怎么决定路由到哪个 agent group?没匹配上怎么办?
src/router.ts的routeInbound()+src/db/messaging-groups.ts
Q5: Session 什么时候创建?agent-shared、shared、per-thread 三种隔离模式的区别是什么?
docs/isolation-model.md+src/session-manager.ts
Q6: 容器 idle 后被 kill,用户再发消息时怎么被唤醒?on_wake 消息为什么不会被旧容器偷走?
src/container-runner.ts的killContainer()+src/container-restart.ts
权限与安全
Q7: 用户身份怎么确定?owner / admin / member 三级的权限检查在哪张表、哪段代码里完成?
src/modules/permissions/access.ts+src/modules/permissions/db/user-roles.ts
Q8: 陌生人在群里 @bot,系统怎么决定是忽略、审批、还是直接响应?
src/modules/permissions/sender-approval.ts+src/router.ts中的 access gate 回调
Q9: Agent 在容器里能用 ncl 命令吗?能查其他 agent group 的数据吗?cli_scope 在哪里被检查?
src/cli/dispatch.ts+src/command-gate.ts+src/db/migrations/015-cli-scope.ts
容器生命周期
Q10: 启动 agent 容器时 mount 了哪些东西?/workspace、session DB、CLAUDE.md、skills 分别从哪里来?
src/container-runner.ts的 mount 参数 +src/group-init.ts
Q11: Agent 的 system prompt 是怎么拼出来的?CLAUDE.md + 全局指令 + container config 各自贡献了什么?
src/claude-md-compose.ts+container/agent-runner/src/index.ts
Q12: 容器心跳怎么检测?进程活着但 poll loop 卡死了,host 怎么发现?
src/host-sweep.ts+src/session-manager.ts中.heartbeat文件的逻辑
出站投递与系统动作
Q13: Agent 回复消息后,delivery.ts 怎么知道用哪个 channel adapter 发送?重试和失败怎么处理?
src/delivery.ts+src/channels/adapter.ts
Q14: Agent 发起 install_packages 或 add_mcp_server,从发出请求到容器重建完成,完整的审批-执行链路是什么?
container/agent-runner/src/mcp-tools/self-mod.ts→src/modules/self-mod/request.ts→src/modules/approvals/primitive.ts→src/modules/self-mod/apply.ts
Q15: 定时任务(cron)怎么实现?Agent 在容器里能创建吗?触发时谁写消息进 inbound.db?
src/modules/scheduling/recurrence.ts+src/modules/scheduling/db.ts+src/host-sweep.ts中 recurrence 处理
数据模型
Q16: 中央库 data/v2.db 有哪些表?它们之间的外键关系是怎样的?
src/db/schema.ts+docs/db-central.md。建议自己画一张 ER 图。
Q17: DB 迁移怎么组织?我要加一张表或一个字段该改哪些文件?
src/db/migrations/index.ts+ 任意一个src/db/migrations/0XX-*.ts
Provider 与 MCP
Q18: Claude Agent SDK、OpenCode、Ollama 三个 provider 怎么抽象成统一接口?切换 provider 改什么?
container/agent-runner/src/providers/factory.ts+container/agent-runner/src/providers/types.ts
Q19: 容器里的 MCP server 怎么启动?内置工具(core、agents、self-mod 等)和外部 MCP server 有什么不同?
container/agent-runner/src/mcp-tools/server.ts+container/agent-runner/src/index.ts中的 MCP 构建逻辑
Channel 适配器
Q20: 如果要加一个新的 channel(比如钉钉),需要实现什么接口、改哪些文件?
src/channels/adapter.ts(接口定义)+src/channels/channel-registry.ts(注册表)+src/channels/chat-sdk-bridge.ts(如果用 Chat SDK 模式)
Q21: Chat SDK bridge 是什么?为什么 Discord/Slack/Telegram 等共用它?
src/channels/chat-sdk-bridge.ts
建议的阅读策略
- 先跑通主干(第一层→第二层:Q1),不求甚解,但要能画出消息的完整流转路径
- 再读容器侧(第三层),理解 agent 内部怎么调 Claude、怎么写回结果
- 挑一个子系统深入(第四层),根据你最关心的方向:
- 关注多租户/权限 → Q7, Q8, Q9
- 关注容器/部署 → Q6, Q10, Q11, Q12
- 关注扩展新 provider/channel → Q18, Q19, Q20, Q21
- 关注运维/稳定性 → Q2, Q3, Q5, Q14, Q15
- 最后回到文档,通读
docs/api-details.md和docs/agent-runner-details.md
关键文件速查表
| 想了解... | 先读这个文件 |
|---|---|
| 整体启动流程 | src/index.ts |
| 消息怎么进来 | src/router.ts |
| 消息怎么出去 | src/delivery.ts |
| Session 怎么管理 | src/session-manager.ts |
| 容器怎么启动/杀死 | src/container-runner.ts |
| Agent 的主循环 | container/agent-runner/src/poll-loop.ts |
| Agent 能调用哪些工具 | container/agent-runner/src/mcp-tools/index.ts |
| 权限检查入口 | src/modules/permissions/access.ts |
| ncl CLI 怎么工作 | src/cli/dispatch.ts |
| DB 有哪些表 | src/db/schema.ts |
| 审批流程 | src/modules/approvals/primitive.ts |
| 自我修改如何实现 | src/modules/self-mod/apply.ts |