Files
nanoclaw/docs/zh/db.md
2026-05-12 13:14:17 +00:00

120 lines
8.5 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# NanoClaw 数据库架构 — 概述
数据模型的概览三个数据库database它们如何协同工作以及跨数据库保持不变的那些约束。表级 schema 请参阅下面的链接。
- **[db-central.md](db-central.md)** — `data/v2.db` 中的每一张表身份标识、连接关系、审批、Chat SDK 状态)以及迁移系统。
- **[db-session.md](db-session.md)** — 每个 session会话`inbound.db` + `outbound.db` 对、seq 奇偶规则以及 session 目录布局。
相关文档:[architecture.md](architecture.md)(高层设计);[api-details.md](api-details.md)(出入站消息内容格式);[isolation-model.md](isolation-model.md)(频道到 agent 的连接模式)。
---
## 1. 三个数据库
NanoClaw 使用**三种 SQLite 数据库**,全部位于宿主机文件系统上:
| DB | 位置 | 写入者 | 读取者 | 用途 |
|----|----------|--------|---------|---------|
| **中央库Central** | `data/v2.db` | 宿主机 | 宿主机 | 身份、权限、路由、连接——管理平面 |
| **Session 入站库** | `data/v2-sessions/<agent_group_id>/<session_id>/inbound.db` | 宿主机 | 宿主机(同步)、容器(只读) | 宿主机 → 容器消息 + 路由投影 |
| **Session 出站库** | `data/v2-sessions/<agent_group_id>/<session_id>/outbound.db` | 容器 | 宿主机(轮询)、容器 | 容器 → 宿主机消息 + 处理状态 |
**单一写入者规则。** 每个 SQLite 文件有且仅有一个写入者。宿主机写入中央库和每个 `inbound.db`;容器只写入自己的 `outbound.db`。这消除了跨越 Docker/Apple Container 挂载边界的写入竞争——SQLite 锁在跨挂载场景下不可靠。
**一切皆为消息。** 宿主机和容器之间没有 IPC、stdin 管道或文件监视器。两个 session DB 是唯一的 IO 接口。心跳heartbeat是对 `.heartbeat` 的文件 `touch(2)`,而非数据库写入。
**日志模式。** Session DB 使用 `journal_mode = DELETE`(而非 WAL。跨挂载的 WAL 可见性是个 bug 温床DELETE 模式加上 open-write-close 会强制刷新页面缓存,使对端能看到变更。
---
## 2. 数据库映射
```
data/
v2.db ← 中央库(宿主机 ↔ 宿主机)
v2-sessions/
<agent_group_id>/
.claude-shared/ ← agent group 共享的 Claude 状态
agent-runner-src/ ← 每个 agent group 的 agent-runner 覆盖层
<session_id>/
inbound.db ← 宿主机写入,容器读取
outbound.db ← 容器写入,宿主机读取
.heartbeat ← 容器触碰的 mtime
inbox/<message_id>/ ← 已解码的用户附件
outbox/<message_id>/ ← agent 生成的附件
```
路径辅助函数:`sessionDir()``inboundDbPath()``outboundDbPath()``heartbeatPath()`——均在 `src/session-manager.ts` 中。
---
## 3. 中央库 vs. session数据存放规则
| 数据类型 | 存放位置 | 原因 |
|--------------|-------|-----|
| 身份、角色、成员关系 | 中央库 | 稳定、跨 session、极少写入 |
| 频道连接、路由规则 | 中央库 | 管理平面 |
| 目的地 ACL | 中央库(+ 每个 session 的投影) | 中央为真源session 本地快速查找 |
| Session 注册表id、状态 | 中央库 | 宿主机编排生命周期 |
| 审批与待处理问题 | 中央库 | 能经受容器重启、管理员可见 |
| 丢弃消息审计 | 中央库 | 全局运维视图 |
| 入站消息、重试状态 | session `inbound.db` | 每个 session 的工作负载;宿主机为唯一写入者 |
| 出站消息、agent 状态 | session `outbound.db` | 容器为唯一写入者;宿主机轮询 |
| 投递结果 | session `inbound.db``delivered` | 宿主机在成功时写入;容器读取以定位编辑目标 |
| 处理状态 | session `outbound.db``processing_ack` | 容器不能写入 `inbound.db` |
启发式原则:如果一个值是消息、路由投影或运行时确认,则放入 per-session 的 DB。其他一切都在中央库。
---
## 4. 跨挂载可见性
Session DB 被 bind mount 到容器中。在修改 DB 代码之前,需要了解几条规则:
- **`journal_mode = DELETE`,而非 WAL。** WAL 文件不能可靠地跨越挂载边界容器可能读到过时的页面。DELETE 模式强制每个写入者刷新主文件。
- **宿主机侧 open-write-close。** 宿主机对 `inbound.db` 的写入是打开连接、写入、然后关闭。保持句柄打开会使缓存的页面对容器不可见。
- **容器读取为只读。** 容器以 `readonly: true` 打开 `inbound.db`,且从不写入——所有容器→宿主机的状态通过 `outbound.db` 传递(见 [db-session.md](db-session.md#52-processing_ack))。
- **心跳是文件 touch。** `.heartbeat` 的 mtime 是存活信号,而非 DB 列。每次心跳做 DB 写入会串行化在其他写入者后面。
这些规则在 `src/session-manager.ts``container/agent-runner/src/db/` 中通过约定强制执行。如果你要修改 DB 的打开方式,请先重新阅读这些代码。
---
## 5. 设计模式一览
1. **两库 session 拆分。** `inbound.db``outbound.db` 各有一个写入者,一个数据流向——无跨挂载锁竞争。
2. **Seq 奇偶规则。** 偶数 = 宿主机,奇数 = 容器。两张表之间的不相交命名空间让 agent 可以仅凭 `seq` 引用任何消息。详情见 [db-session.md §3](db-session.md#3-sequence-numbering-invariant)。
3. **投影模式。** `agent_destinations``session_routing` 在容器唤醒时从中央库投影到每个 session 的 `inbound.db` 中——容器获得快速、本地的读取路径,无需跨挂载查询。
4. **通过反向通道确认。** 容器从不写入 `inbound.db`。状态同步通过 `outbound.db` 中的 `processing_ack` 实现,由宿主机轮询并协调。
5. **带外心跳。**`.heartbeat` 的文件 `touch`,而非 DB 写入,因此存活检查不串行在其他写入者后面。
6. **惰性 session-DB 迁移。** 中央库使用编号迁移per-session 的 DB 使用 `IF NOT EXISTS` + 针对旧 session 目录的临时 `ALTER TABLE` 辅助函数。
7. **ACL = 行的存在。** `agent_destinations` 的成员关系本身就是权限——没有单独的 `permissions` 表。
---
## 6. 读取者与写入者 — 一览
| 表 | DB | 写入者 | 读取者 |
|-------|----|-----------|-----------|
| `agent_groups` | central | `src/db/agent-groups.ts` | session 解析器、投递、路由 |
| `messaging_groups` | central | `src/db/messaging-groups.ts`、频道设置 | 路由、投递、session 解析器 |
| `messaging_group_agents` | central | `src/db/messaging-groups.ts` | 路由 |
| `users` | central | `src/db/users.ts`、认证流程 | 权限检查 |
| `user_roles` | central | `src/db/user-roles.ts` | `src/access.ts`、所有权限关卡 |
| `agent_group_members` | central | `src/db/agent-group-members.ts` | 成员资格检查 |
| `user_dms` | central | `src/user-dm.ts``ensureUserDm` | 审批 + 配对投递 |
| `sessions` | central | `src/db/sessions.ts``src/session-manager.ts` | 投递、sweep、容器运行器 |
| `pending_questions` | central | `src/db/sessions.ts`(通过 `ask_user_question` | 容器响应匹配器 |
| `agent_destinations` | central | `src/db/agent-destinations.ts`、迁移 004 回填 | `writeDestinations()`、投递 ACL |
| `pending_approvals` | central | `src/db/sessions.ts``src/onecli-approvals.ts` | 管理员卡片投递、sweep |
| `unregistered_senders` | central | `src/db/dropped-messages.ts` | 运维工具 |
| `chat_sdk_*` | central | `src/state-sqlite.ts` | Chat SDK 桥接 |
| `schema_version` | central | `src/db/migrations/index.ts` | 迁移运行器 |
| `messages_in` | inbound | `src/db/session-db.ts` | `container/agent-runner/src/db/messages-in.ts` |
| `delivered` | inbound | `src/db/session-db.ts``markDelivered` | 容器编辑/反应定位 |
| `destinations` | inbound | `writeDestinations()``src/session-manager.ts` | 容器路由 / ACL |
| `session_routing` | inbound | `writeSessionRouting()``src/session-manager.ts` | 容器 `send_message` 默认值 |
| `messages_out` | outbound | `container/agent-runner/src/db/messages-out.ts` | `src/delivery.ts` 轮询循环 |
| `processing_ack` | outbound | `container/agent-runner/src/db/messages-in.ts` | `src/host-sweep.ts``syncProcessingAcks` |
| `session_state` | outbound | `container/agent-runner/src/db/session-state.ts` | 容器启动时 |