新增 docs/multi-tenant-redesign/01-redesign/database-schema-as-built.zh-CN.md: 对照实现代码(persistence/*/model.py + base.py/engine.py + alembic 0001-0003) 生成的事实参考,补齐之前只有「锁定版」(动手前决策)而缺失的落地 schema 文档。 涵盖: - 持久化层总览:memory/sqlite/postgres 三后端、create_all vs Alembic、 Postgres 库自愈、SQLite WAL、JSON ensure_ascii=False、partial index 双 where 兼容 - LangGraph checkpointer/store 表不归 ORM 管的说明 - ER 图(mermaid)+ 10 张表全字段参考(users/workspaces/workspace_memberships/ threads_meta/runs/run_events/feedback/service_accounts/api_keys/external_users) - 外键与删除策略矩阵(CASCADE/RESTRICT/SET NULL) - 迁移历史 0001-0003(含 0002→0003 两段式上线:可空列→回填→锁 NOT NULL) - 与锁定版的差异(run_events 已确认为 DB 表、runs token 分项列、status 状态机等) README.zh-CN.md 的「文档总图」与「状态表」登记该文档,并注明 与锁定版冲突时以落地版为准。 Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
18 KiB
数据库设计 · 落地版(as-built)
写于 2026-06-27。对照实现代码生成,反映 Stage 0 PR1–PR8 合入后的真实 schema。
与 workspace-schema-design.zh-CN.md 的关系:那份是 Stage 0 动手前的锁定版(决策 + 不可逆点),本文是 落地后的事实参考。两者冲突时以代码与本文为准(锁定版里标 "待确认 / schema only" 的项,这里给出最终结果,例如
run_events已确认为 DB 表并带workspace_id)。真源:
backend/packages/harness/deerflow/persistence/
- 表定义:各子目录
*/model.py(如user/model.py、api_key/model.py)+models/run_event.py- 基类 / 引擎:
base.py/engine.py- 迁移:
migrations/versions/0001..0003
1. 持久化层总览
1.1 后端与建表
| 维度 | 说明 |
|---|---|
| 引擎 | 异步 SQLAlchemy(create_async_engine),见 engine.py |
| 后端三选一 | memory(不建引擎,仓储回退内存实现)/ sqlite(aiosqlite)/ postgres(asyncpg)。Stage 0 生产默认 postgres |
| 建表方式 | dev:启动时 Base.metadata.create_all() 自动建表(缺表即补,不改已存在的表)。生产:用 Alembic 迁移(migrations/versions/) |
| Postgres 自愈 | 目标库不存在时(报 does not exist),自动连到 postgres 维护库 CREATE DATABASE 后重建引擎重试(_auto_create_postgres_db) |
| SQLite 加固 | 每条连接启用 PRAGMA journal_mode=WAL + synchronous=NORMAL + foreign_keys=ON |
| JSON 序列化 | 自定义 json.dumps(..., ensure_ascii=False),中文不转义 |
| 连接池 | postgres:pool_size(默认 5)+ pool_pre_ping=True |
⚠️
create_all只新建缺失的表,不会 ALTER 已存在的表。给已有表加列/改约束必须走 Alembic 迁移;dev 下想偷懒可删库重建。
1.2 不归 ORM 管的表
LangGraph 的 checkpointer(checkpoints*)与 store(store / store_migrations)由 LangGraph 自己 setup() 建表,不在 Base.metadata 里(见 runtime/checkpointer/ 与 runtime/store/)。它们与本文的业务表共用同一个 Postgres 库,但生命周期、迁移各自独立。多租户隔离对这些表走"应用层强校验 + UNIQUE(workspace_id, thread_id) 兜底"(见 ADR-001 / spike-langgraph-postgres)。
1.3 通用约定
- 主键 id:业务实体用
String(36)(UUID v4 字符串),跨 SQLite/Postgres 可移植(Postgres 落CHAR(36),性能差异可忽略)。 - 时间:一律
DateTime(timezone=True),应用层写datetime.now(UTC);updated_at在写入时自动更新。 - 枚举:状态/角色用
String(16)+ 应用层校验,不用 DB enum(Postgres enum 加值要ALTER TYPE、不可删,扩展成本高)。 - JSON 列:用 SQLAlchemy 可移植
JSON类型(Postgres 落json),默认{}。 - partial unique / partial index:同时声明
sqlite_where+postgresql_where两套等价条件,双后端兼容。
2. 实体关系总览
erDiagram
users ||--o{ workspace_memberships : "成员"
workspaces ||--o{ workspace_memberships : "包含"
users ||--o| workspaces : "owner_id (RESTRICT)"
workspaces ||--o| users : "default_workspace_id (SET NULL)"
workspaces ||--o{ threads_meta : "wid (CASCADE)"
workspaces ||--o{ runs : "wid (CASCADE)"
workspaces ||--o{ run_events : "wid (CASCADE)"
workspaces ||--o{ feedback : "wid (CASCADE)"
workspaces ||--o{ service_accounts : "wid (CASCADE)"
service_accounts ||--o{ api_keys : "CASCADE"
service_accounts ||--o{ external_users : "CASCADE"
workspaces ||--o{ external_users : "wid (CASCADE)"
threads_meta ||--o{ runs : "thread_id (逻辑)"
runs ||--o{ run_events : "run_id (逻辑)"
runs ||--o{ feedback : "run_id (逻辑)"
两条主线:
- 租户骨架:
users↔workspaces(多对多经workspace_memberships)。workspace 是隔离粒度单位,每个用户注册自动建 1 人 workspace。 - 业务数据:
threads_meta→runs→run_events/feedback,全部挂workspace_id(行级隔离),workspace 删除时级联清空。 - Headless 接入(Stage 0 末预建 schema):
service_accounts→api_keys(鉴权凭证)+external_users(passthrough 终端身份)。
threads_meta.thread_id/runs.run_id与下游是逻辑关联(无 DB 外键,因 thread/run id 也被 LangGraph 表使用);workspace 外键才是物理约束。
3. 表参考
列约定:所有
created_at/updated_at为DateTime(tz)NOT NULL,下表不再逐行重复说明。
3.1 users — 用户账户(本地密码 + OAuth)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
String(36) | PK | UUID |
email |
String(320) | UNIQUE NOT NULL,索引 | 登录邮箱 |
password_hash |
String(128) | NULL | OAuth-only 用户为 NULL |
system_role |
String(16) | NOT NULL default "user" |
平台级角色 admin/user(与 workspace role 正交) |
oauth_provider |
String(32) | NULL | google/github… |
oauth_id |
String(128) | NULL | 提供商内用户 ID |
needs_setup |
Boolean | NOT NULL default False |
首次设置标记 |
token_version |
Integer | NOT NULL default 0 |
自增即吊销该用户所有旧 JWT |
default_workspace_id |
String(36) | NULL,FK workspaces.id SET NULL |
登录默认进入的 workspace;NULL 走 picker |
created_at |
DateTime(tz) | NOT NULL |
索引:idx_users_oauth_identity UNIQUE (oauth_provider, oauth_id),仅当两者均非 NULL(partial)。
3.2 workspaces — 工作空间(多租户隔离单位)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
String(36) | PK | UUID |
name |
String(64) | NOT NULL | 显示名 |
slug |
String(32) | UNIQUE NOT NULL | URL 标识 ^[a-z0-9](-?[a-z0-9])*$,3–32 字符 |
status |
String(16) | NOT NULL default "active" |
active/suspended/deleted |
owner_id |
String(36) | NOT NULL,FK users.id RESTRICT |
冗余 owner;删 owner 被阻拦 |
created_at / updated_at |
DateTime(tz) | NOT NULL |
slug 黑名单(
admin/api/auth/_next/… 见锁定版 §2.1)走应用层校验,不入 DB 约束。
3.3 workspace_memberships — 成员(RBAC)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
workspace_id |
String(36) | 复合 PK,FK workspaces.id CASCADE |
|
user_id |
String(36) | 复合 PK,FK users.id CASCADE |
|
role |
String(16) | NOT NULL | Stage 0 仅 owner;Stage 2 起 admin/member |
invited_by |
String(36) | NULL,FK users.id SET NULL |
Stage 2 invitation 才写 |
joined_at |
DateTime(tz) | NOT NULL |
索引:
- 复合 PK
(workspace_id, user_id) idx_workspace_memberships_user(user_id, workspace_id)— 倒查"某 user 的所有 workspace"idx_one_owner_per_workspaceUNIQUE(workspace_id)WHERErole='owner'(partial)— 每 workspace 恰好 1 owner
3.4 threads_meta — 会话元数据
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
thread_id |
String(64) | PK | LangGraph thread_id |
assistant_id |
String(128) | NULL,索引 | 自定义 agent 名;NULL=默认 lead agent |
user_id |
String(64) | NULL,索引 | 所有者;NULL=历史无主 |
workspace_id |
String(36) | NOT NULL(0003 后),FK workspaces.id CASCADE |
|
display_name |
String(256) | NULL | 自动标题或用户改名 |
status |
String(20) | NOT NULL default "idle" |
idle/busy |
metadata_json |
JSON | NOT NULL default {} |
|
created_at / updated_at |
DateTime(tz) | NOT NULL |
索引:
idx_threads_meta_workspace_user_updated(workspace_id, user_id, updated_at)— 前端 thread list 默认查询idx_threads_meta_workspace_threadUNIQUE(workspace_id, thread_id)(0003 加)— 防跨 workspace 复用同 thread_id
3.5 runs — 单次 agent 运行(含 token 指标)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
run_id |
String(64) | PK | |
thread_id |
String(64) | NOT NULL,索引 | 所属会话 |
assistant_id |
String(128) | NULL | |
user_id |
String(64) | NULL,索引 | |
workspace_id |
String(36) | NOT NULL(0003 后),FK workspaces.id CASCADE |
|
status |
String(20) | NOT NULL default "pending" |
pending/running/success/error/timeout/interrupted |
model_name |
String(128) | NULL | |
multitask_strategy |
String(20) | NOT NULL default "reject" |
reject/interrupt/rollback/enqueue |
metadata_json / kwargs_json |
JSON | NOT NULL default {} |
运行级元数据 / 提交参数 |
error |
Text | NULL | 失败错误文本 |
message_count |
Integer | NOT NULL default 0 |
|
first_human_message / last_ai_message |
Text | NULL | 文本预览 |
total_input_tokens / total_output_tokens / total_tokens |
Integer | NOT NULL default 0 |
累计 token |
llm_call_count |
Integer | NOT NULL default 0 |
|
lead_agent_tokens / subagent_tokens / middleware_tokens |
Integer | NOT NULL default 0 |
分项 token(主 agent / 子 agent / 中间件) |
follow_up_to_run_id |
String(64) | NULL | 续接的上一次运行(重新生成/继续) |
created_at / updated_at |
DateTime(tz) | NOT NULL |
索引:ix_runs_thread_status (thread_id, status)。
3.6 run_events — 运行事件流(回放 + 审计真源)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
Integer | PK autoincrement | |
thread_id |
String(64) | NOT NULL | |
run_id |
String(64) | NOT NULL,索引 | |
user_id |
String(64) | NULL,索引 | |
workspace_id |
String(36) | NOT NULL(0003 后),FK workspaces.id CASCADE |
|
event_type |
String(32) | NOT NULL | 子类型(ai_message_chunk/tool_call…) |
category |
String(16) | NOT NULL | message/trace/lifecycle |
content |
Text | NOT NULL default "" |
事件文本 |
event_metadata |
JSON | NOT NULL default {} |
|
seq |
Integer | NOT NULL | thread 内全局递增序号 |
created_at |
DateTime(tz) | NOT NULL |
索引:
uq_events_thread_seqUNIQUE(thread_id, seq)ix_events_thread_cat_seq(thread_id, category, seq)ix_events_run(thread_id, run_id, seq)
3.7 feedback — 运行反馈(赞/踩 + 评论)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
feedback_id |
String(64) | PK | |
run_id |
String(64) | NOT NULL,索引 | |
thread_id |
String(64) | NOT NULL,索引 | |
user_id |
String(64) | NULL,索引 | |
workspace_id |
String(36) | NOT NULL(0003 后),FK workspaces.id CASCADE |
|
message_id |
String(64) | NULL | NULL=针对整次运行而非单条消息 |
rating |
Integer | NOT NULL | +1 赞 / -1 踩 |
comment |
Text | NULL | |
created_at |
DateTime(tz) | NOT NULL |
索引:uq_feedback_thread_run_user UNIQUE (thread_id, run_id, user_id) — 一人对一次运行只一条反馈。
3.8 service_accounts — 服务账号(headless 非人身份)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
String(36) | PK | |
workspace_id |
String(36) | NOT NULL,FK workspaces.id CASCADE,索引 |
|
name |
String(64) | NOT NULL | |
role |
String(16) | NOT NULL default "member" |
|
identity_mode |
String(16) | NOT NULL default "collapsed" |
collapsed/external_passthrough/both |
status |
String(16) | NOT NULL default "active" |
active/suspended/deleted |
created_by |
String(36) | NOT NULL,FK users.id RESTRICT |
创建者(owner/admin) |
created_at / updated_at |
DateTime(tz) | NOT NULL |
索引:idx_service_accounts_workspace (workspace_id, status)。
3.9 api_keys — API Key(headless 凭证)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
String(36) | PK | |
service_account_id |
String(36) | NOT NULL,FK service_accounts.id CASCADE,索引 |
|
key_prefix |
String(16) | UNIQUE NOT NULL | 公开 prefix(dfk_live_…),可打日志 |
key_hash |
String(128) | NOT NULL | 完整 token 的 sha-256 hex |
name |
String(64) | NOT NULL | 标签(同 SA 内不强制唯一) |
scopes |
String(1024) | NOT NULL default "" |
逗号分隔(threads:read,threads:write) |
rate_limit_rpm |
Integer | NULL | NULL=走 SA 默认 |
expires_at / last_used_at / revoked_at |
DateTime(tz) | NULL | revoked_at 非空=软删,不删行 |
created_at |
DateTime(tz) | NOT NULL |
索引:
idx_api_keys_sa(service_account_id)idx_api_keys_active(key_prefix)WHERErevoked_at IS NULL(partial)— 鉴权热路径只扫活跃 key
3.10 external_users — 终端用户身份(passthrough)
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
id |
String(36) | PK | ghost user id |
workspace_id |
String(36) | NOT NULL,FK workspaces.id CASCADE |
冗余存,加速跨 SA 的 workspace 查询 |
service_account_id |
String(36) | NOT NULL,FK service_accounts.id CASCADE |
|
external_id |
String(128) | NOT NULL | 调用方传入的 X-External-User-Id,原样存 |
display_name |
String(128) | NULL | 仅 admin UI 展示 |
metadata_json |
JSON | NOT NULL default {} |
plan tier / region / tag |
created_at |
DateTime(tz) | NOT NULL | 首见时间 |
last_seen_at |
DateTime(tz) | NULL |
索引:uq_external_users_sa_external UNIQUE (service_account_id, external_id) — 同一 SA 下 external_id 唯一(鉴权中间件按此 upsert)。
4. 外键与删除策略一览
| 子表 | 外键列 | 指向 | ON DELETE | 含义 |
|---|---|---|---|---|
| users | default_workspace_id | workspaces.id | SET NULL | 默认 workspace 没了就清空,用户仍在 |
| workspaces | owner_id | users.id | RESTRICT | 不能直接删 owner,需先转移 |
| workspace_memberships | workspace_id | workspaces.id | CASCADE | 删 workspace → 成员清空 |
| workspace_memberships | user_id | users.id | CASCADE | 删 user → 其成员关系清空 |
| workspace_memberships | invited_by | users.id | SET NULL | 删邀请人,保留成员关系 |
| threads_meta / runs / run_events / feedback | workspace_id | workspaces.id | CASCADE | 删 workspace → 业务数据全清 |
| service_accounts | workspace_id | workspaces.id | CASCADE | 删 workspace → SA 清空 |
| service_accounts | created_by | users.id | RESTRICT | 不能删 SA 创建者 |
| api_keys | service_account_id | service_accounts.id | CASCADE | 删 SA → key 清空 |
| external_users | workspace_id | workspaces.id | CASCADE | |
| external_users | service_account_id | service_accounts.id | CASCADE |
心智模型:删 workspace = 整租户级联清空(业务数据 + SA + key + 外部身份);user 作为他人的 owner/creator 受 RESTRICT 保护,不能"误删带塌一片"。
5. 迁移历史(Alembic)
dev 用
create_all直接建到最新;生产/已有库用迁移逐步推进。migrations/versions/:
| 版本 | 依赖 | 变更 | 要点 |
|---|---|---|---|
0001 users_default_workspace |
— | users 加 default_workspace_id(nullable)+ FK→workspaces.id SET NULL |
无需回填(nullable) |
0002 business_tables_workspace |
0001 | threads_meta/runs/feedback/run_events 各加 workspace_id(nullable)+ FK CASCADE;threads_meta 加 (workspace_id,user_id,updated_at) 索引 |
先 nullable,留给 scripts/backfill_workspace_id.py 回填 |
0003 business_tables_workspace_not_null |
0002 | 4 张表 workspace_id 改 NOT NULL;threads_meta 加 (workspace_id,thread_id) UNIQUE |
升级前校验:任一表仍有 workspace_id IS NULL 则拒绝升级,逼先跑回填脚本 |
两段式上线(0002→0003)是为了零停机:先加可空列 → 后台回填 → 校验通过再锁 NOT NULL,避免大表 ALTER 长锁与脏数据静默写入。
6. 与设计锁定版的差异 / 落地补充
| 项 | 锁定版 | 落地实际 |
|---|---|---|
run_events.workspace_id |
"待确认是否 DB 表" | 已确认为 DB 表,比照 runs 加 workspace_id + CASCADE,并入 0002/0003 |
runs token 指标列 |
未在 schema 文档列出 | 实际有完整一组:total_*_tokens / llm_call_count / lead_agent_tokens / subagent_tokens / middleware_tokens |
service_accounts.status |
active/suspended/revoked |
落地为 active/suspended/deleted(与 workspace 状态机一致) |
api_keys.scopes 默认 |
'' |
一致(String(1024),逗号分隔) |
| 索引名 | 设计期未定名 | 见各表"索引"小节(如 ix_runs_thread_status、uq_events_thread_seq) |
不可逆决策(id 类型 / 命名 / slug / 复合 PK / TokenPayload / FK 删除策略)均按锁定版 §5 执行,未变。
7. 配套阅读
- workspace-schema-design.zh-CN.md — Stage 0 schema 锁定版(决策依据 + 不可逆点 + JWT TokenPayload)
- adr-001-data-isolation.zh-CN.md — 行级
workspace_id+ Postgres RLS + LangGraph 表两层模型 - adr-004-tenant-rbac.zh-CN.md — RBAC + JWT 设计
- adr-spike-langgraph-postgres.zh-CN.md — 为何 LangGraph 表不归 ORM 管
- 03-impl/STATUS.md +
03-impl/pr8-headless-api-schema.md— PR 级实现进度