docs: add detailed answers for 21 learning roadmap questions
This commit is contained in:
167
docs/answers/06-data-model.md
Normal file
167
docs/answers/06-data-model.md
Normal file
@@ -0,0 +1,167 @@
|
||||
# Q16-Q17: 数据模型
|
||||
|
||||
---
|
||||
|
||||
## Q16: 中央库 `data/v2.db` 有哪些表?它们之间的外键关系是怎样的?
|
||||
|
||||
### 答案
|
||||
|
||||
中央库有约 20 张表,横跨多次迁移。以下按逻辑分类:
|
||||
|
||||
### 核心实体表(迁移 001)
|
||||
|
||||
| # | 表 | 用途 | 关键 FK 关系 |
|
||||
|---|-----|------|-------------|
|
||||
| 1 | `agent_groups` | Agent 工作区(folder、skills、CLAUDE.md) | 根实体——无 FK 出 |
|
||||
| 2 | `messaging_groups` | 一个平台聊天/频道/DM | 根实体;`UNIQUE(channel_type, platform_id)` |
|
||||
| 3 | `messaging_group_agents` | 多对多 wiring:agent↔channel | `→ messaging_groups(id)`, `→ agent_groups(id)`;`UNIQUE(messaging_group_id, agent_group_id)` |
|
||||
| 4 | `users` | 平台用户身份(`<channel>:<handle>`) | 根实体 |
|
||||
| 5 | `user_roles` | 特权授予(owner/admin) | `→ users(id)` (x2: user + granted_by), `→ agent_groups(id)`;`PK (user_id, role, agent_group_id)` |
|
||||
| 6 | `agent_group_members` | 非特权访问门 | `→ users(id)` (x2), `→ agent_groups(id)`;`PK (user_id, agent_group_id)` |
|
||||
| 7 | `user_dms` | 冷 DM 缓存 | `→ users(id)`, `→ messaging_groups(id)`;`PK (user_id, channel_type)` |
|
||||
| 8 | `sessions` | Session 生命周期元数据 | `→ agent_groups(id)`, `→ messaging_groups(id)` |
|
||||
| 9 | `pending_questions` | 交互式问题状态 | `→ sessions(id)` |
|
||||
|
||||
### 后续迁移添加的表
|
||||
|
||||
| # | 表 | 迁移 | 说明 |
|
||||
|---|-----|------|------|
|
||||
| 10 | `chat_sdk_kv` | 002 | Chat SDK 不透明状态 |
|
||||
| 11 | `chat_sdk_subscriptions` | 002 | Chat SDK 线程订阅 |
|
||||
| 12 | `chat_sdk_locks` | 002 | Chat SDK 分布式锁 |
|
||||
| 13 | `chat_sdk_lists` | 002 | Chat SDK 列表状态 |
|
||||
| 14 | `pending_approvals` | module-003 | `→ sessions(id)`, `→ agent_groups(id)` |
|
||||
| 15 | `agent_destinations` | module-004 | `→ agent_groups(id)`;`PK (agent_group_id, local_name)` |
|
||||
| 16 | `unregistered_senders` | 008 | 审计跟踪;`PK (channel_type, platform_id)` |
|
||||
| 17 | `container_configs` | 014 | `→ agent_groups(id) ON DELETE CASCADE` |
|
||||
| 18 | `pending_sender_approvals` | 011 | `→ messaging_groups(id)`, `→ agent_groups(id)`;`UNIQUE(messaging_group_id, sender_identity)` |
|
||||
| 19 | `pending_channel_approvals` | 012 | `→ messaging_groups(id)`, `→ agent_groups(id)` |
|
||||
| 20 | `schema_version` | 内建 | 迁移分类账——无 FK |
|
||||
|
||||
### 已删除的表
|
||||
|
||||
- `pending_credentials` — 在迁移 009 中删除(已废弃)
|
||||
|
||||
### 列级变更
|
||||
|
||||
- `agent_groups.denied_at` — 迁移 012 添加
|
||||
- `messaging_group_agents` 的 4 列(`engage_mode`、`engage_pattern`、`sender_scope`、`ignored_message_policy`)— 迁移 010 添加,替换旧的 `trigger_rules` + `response_scope`
|
||||
- `container_configs.cli_scope` — 迁移 015 添加
|
||||
|
||||
### ER 关系图
|
||||
|
||||
```
|
||||
agent_groups ──────────────────────────────────────────────────────────────────────────┐
|
||||
│ │
|
||||
├── messaging_group_agents ──→ messaging_groups ◄── user_dms │
|
||||
│ (UNIQUE pair) │
|
||||
│ │
|
||||
├── sessions ──→ messaging_groups │
|
||||
│ └── pending_questions │
|
||||
│ │
|
||||
├── user_roles (privilege: owner/admin, scoped or global) ──→ users (user_id, granted_by)
|
||||
│ │
|
||||
├── agent_group_members (unprivileged access gate) ──→ users (user_id, added_by) │
|
||||
│ │
|
||||
├── agent_destinations (ACL + name→target routing map) │
|
||||
│ │
|
||||
├── container_configs (1:1, ON DELETE CASCADE) │
|
||||
│ │
|
||||
├── pending_approvals │
|
||||
├── pending_sender_approvals │
|
||||
└── pending_channel_approvals │
|
||||
|
||||
users ────────────────────────────────────────────────────────────────────────────────┐
|
||||
├── user_roles (user_id, granted_by 都自引用 users) │
|
||||
├── agent_group_members │
|
||||
└── user_dms │
|
||||
|
||||
schema_version, chat_sdk_*, unregistered_senders — 无 FK;独立叶子表
|
||||
```
|
||||
|
||||
### 关键不变式
|
||||
|
||||
- **Owner 必须全局:** `role='owner'` 意味着 `user_roles` 中 `agent_group_id IS NULL`。`grantRole()` 中强制
|
||||
- **Admin 隐含 membership:** agent group A 的 admin 自动是 member——不需要 `agent_group_members` 行
|
||||
- **`agent_destinations` 双重角色:** 既是路由表也是 ACL。无行 = 未授权发送。源是中央库,spawn 时投影写入容器内 `inbound.db`
|
||||
- **Chat SDK 表是不透明的**:NanoClaw 代码很少直接操作它们——由 `state-sqlite.ts` 持有
|
||||
- **Session DB 是分离的**:`inbound.db` / `outbound.db` 在 `data/v2-sessions/<session_id>/` 下,用 `CREATE TABLE IF NOT EXISTS` 做前向兼容
|
||||
|
||||
---
|
||||
|
||||
## Q17: DB 迁移怎么组织?我要加一张表或一个字段该改哪些文件?
|
||||
|
||||
### 答案
|
||||
|
||||
### 迁移文件组织
|
||||
|
||||
每个迁移是 `src/db/migrations/` 下的一个文件,导出 `Migration` 对象:
|
||||
|
||||
```typescript
|
||||
// src/db/migrations/index.ts:18-22
|
||||
interface Migration {
|
||||
version: number; // 在 barrel 数组中的排序提示
|
||||
name: string; // UNIQUE name — schema_version 中的去重 key
|
||||
up: (db: Database) => void; // 在事务中运行
|
||||
}
|
||||
```
|
||||
|
||||
### 迁移注册顺序(`migrations/index.ts:24-38`)
|
||||
|
||||
```
|
||||
001-initial.ts → 核心表
|
||||
002-chat-sdk-state.ts → Chat SDK 表
|
||||
module-approvals-pending-approvals.ts → pending_approvals
|
||||
module-agent-to-agent-destinations.ts → agent_destinations
|
||||
module-approvals-title-options.ts → ALTER pending_approvals
|
||||
008-dropped-messages.ts → unregistered_senders
|
||||
009-drop-pending-credentials.ts → DROP pending_credentials
|
||||
010-engage-modes.ts → ALTER messaging_group_agents
|
||||
011-pending-sender-approvals.ts
|
||||
012-channel-registration.ts
|
||||
013-approval-render-metadata.ts
|
||||
014-container-configs.ts
|
||||
015-cli-scope.ts
|
||||
```
|
||||
|
||||
005 和 006 编号空着——早前重编号的。
|
||||
|
||||
### 注册机制(`runMigrations`,`index.ts:40-77`)
|
||||
|
||||
1. 如果不存在则创建 `schema_version` 表。唯一性在 `name`,不在 `version`
|
||||
2. 读 `SELECT name FROM schema_version` 到 `Set<string>` —— **去重 key 是 `name`**,不是 `version`。这允许模块迁移使用任意版本号
|
||||
3. `migrations.filter(m => !applied.has(m.name))` 得到待执行列表
|
||||
4. 在 `db.transaction()` 中运行每个待执行迁移:
|
||||
- 调 `m.up(db)` 执行 DDL
|
||||
- 计算 `next = MAX(version) + 1`
|
||||
- 插入 `schema_version`
|
||||
|
||||
### 加一张表
|
||||
|
||||
1. 创建 `src/db/migrations/016-your-table.ts`:
|
||||
```typescript
|
||||
import type Database from 'better-sqlite3';
|
||||
import type { Migration } from './index.js';
|
||||
export const migration016: Migration = {
|
||||
version: 16,
|
||||
name: 'your-table', // 必须在所有迁移中全局唯一
|
||||
up(db: Database) {
|
||||
db.exec(`CREATE TABLE IF NOT EXISTS your_table (...)`);
|
||||
},
|
||||
};
|
||||
```
|
||||
|
||||
2. 在 `src/db/migrations/index.ts` 中:
|
||||
- 添加 `import { migration016 } from './016-your-table.js';`
|
||||
- 追加到 `migrations` 数组
|
||||
|
||||
### 加一个字段
|
||||
|
||||
同理,写一个 ALTER TABLE 的迁移。复杂变更(回填)做 JS 行级更新。**幂等守护模式**(迁移 012,line 29-32):ALTER TABLE ADD COLUMN 前先 `PRAGMA table_info()` 检查列是否已存在。
|
||||
|
||||
### 边界情况 / Gotchas
|
||||
|
||||
- **DROP TABLE 中的 FK 完整性:** 迁移事务中 DROP TABLE 会触发 SQLite FK 完整性检查。不能 toggle `PRAGMA foreign_keys`。优先 ALTER TABLE ADD COLUMN
|
||||
- **模块迁移用任意版本:** `module-` 前缀文件的 `name` 与文件名不同——`name` 保持稳定,防止重命名后重新运行
|
||||
- **Session DB 迁移是独立的:** Session DB schema(`INBOUND_SCHEMA`、`OUTBOUND_SCHEMA`)用 `CREATE TABLE IF NOT EXISTS`,新列通过惰性迁移 helper(`migrateDeliveredTable()` 等)落地,不跟踪 `schema_version`
|
||||
- **Container 端也有自己的迁移**:`container/agent-runner/src/db/connection.ts:86-110` 有惰性 session DB 迁移(添加 `on_wake` 列、`delivered` 表列等)
|
||||
Reference in New Issue
Block a user