168 lines
9.1 KiB
Markdown
168 lines
9.1 KiB
Markdown
# 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` 表列等)
|