Files
nanoclaw/docs/answers/06-data-model.md

9.1 KiB
Raw Permalink Blame History

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 多对多 wiringagent↔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_modeengage_patternsender_scopeignored_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_rolesagent_group_id IS NULLgrantRole() 中强制
  • 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.dbdata/v2-sessions/<session_id>/ 下,用 CREATE TABLE IF NOT EXISTS 做前向兼容

Q17: DB 迁移怎么组织?我要加一张表或一个字段该改哪些文件?

答案

迁移文件组织

每个迁移是 src/db/migrations/ 下的一个文件,导出 Migration 对象:

// 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 编号空着——早前重编号的。

注册机制(runMigrationsindex.ts:40-77

  1. 如果不存在则创建 schema_version 表。唯一性在 name,不在 version
  2. SELECT name FROM schema_versionSet<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

    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 行级更新。幂等守护模式(迁移 012line 29-32ALTER 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 schemaINBOUND_SCHEMAOUTBOUND_SCHEMA)用 CREATE TABLE IF NOT EXISTS,新列通过惰性迁移 helpermigrateDeliveredTable() 等)落地,不跟踪 schema_version
  • Container 端也有自己的迁移container/agent-runner/src/db/connection.ts:86-110 有惰性 session DB 迁移(添加 on_wake 列、delivered 表列等)