# 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` | 平台用户身份(`:`) | 根实体 | | 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//` 下,用 `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` —— **去重 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` 表列等)