docs(multi-tenant): 加入汇总索引、跨文档一致性修订、Postgres 切换前移到 Stage 0

* 新增 README.zh-CN.md 汇总索引:ADR 状态表 + Stage 0-4 业务目标 / 技术路径 /
  验证方式 + Stage↔ADR 对照矩阵 + 不可逆决策一览 + 用语映射 + FAQ + 阅读路径
* 新增 workspace-schema-design.zh-CN.md(Stage 0 schema 锁定版)一并入库
* 7 份 ADR 顶部加"代码命名"映射行(tenant_id ↔ workspace_id)
* ADR-002 §1 加分期落地提示,明确 K8s 推迟到 Stage 3
* ADR-005 §5"第 1/2 阶段"补出与 rollout Stage 2/3 的映射
* ADR-007 §4 加 /api/v1/ 反向链接;§8 加 tid → wid 字段名映射
* headless-api §0/§7 把"SaaS + on-prem 双主线"改为"SaaS 主线、schema 兼容 on-prem"
* phased-rollout 去除重复的"Go/No-Go 进入 Stage 2"段

Postgres 切换从 Stage 1 提前到 Stage 0:Stage 0 已要 ALTER 4 张表加
workspace_id,先 SQLite 再 PG 是纯返工;Stage 0 没有生产数据,迁移阻力最小。
同步调整 phased-rollout / headless-api / phase-0-plan / workspace-schema-design /
README 中的时间盒(Stage 0: 3-4→4-5 周;Stage 1: 10-15→8-13 周)、不可逆决策
清单、PR 顺序、轨道前置依赖。

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
1445043649
2026-05-10 21:39:58 +08:00
parent 9ff790554d
commit 89fe54cc07
12 changed files with 712 additions and 69 deletions
@@ -7,6 +7,7 @@
| 决策者 | CTO + 架构 + 后端 lead |
| 关联 ADR | ADR-004 租户层级、ADR-005 存储拓扑、ADR-006 运行时与渠道 |
| 关联 spike / 审计 | [adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) · [langgraph-postgres spike](./adr-spike-langgraph-postgres.zh-CN.md) |
| 代码命名 | 本 ADR 写 `tenant_id`,落代码统一读作 `workspace_id`(详 [workspace-schema-design §1](./workspace-schema-design.zh-CN.md#1-命名约定--workspace-vs-tenant) |
---
@@ -7,6 +7,7 @@
| 决策者 | 安全 + 架构 + SRE |
| 关联 ADR | ADR-001 数据隔离、ADR-005 存储拓扑 |
| 关联审计 | [adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) — 注意:现有 `AioSandboxProvider` 出网/资源/cosign 缺位;K8sSandboxProvider 几乎从零开工(实际工作量大于本 ADR §5 估算) |
| 代码命名 | 本 ADR 写 `tenant_id` / `tenant-{tenant_id}` namespace,落代码统一读作 `workspace_id` / `ws-{workspace_id}`(详 [workspace-schema-design §1](./workspace-schema-design.zh-CN.md#1-命名约定--workspace-vs-tenant) |
---
@@ -101,6 +102,13 @@ spec:
## 1. 背景
> **分期落地提示**:本 ADR 描述的 K8s + gVisor + NetworkPolicy 全套架构是**目标态**。按 [phased-rollout-by-scale](../02-rollout/phased-rollout-by-scale.zh-CN.md) 实际落地节奏:
> - **Stage 1**:仅做 §3 威胁模型的"出网默认禁 + cgroup CPU/memory 限额",落到现有 `AioSandboxProvider` 上(轻量补丁版)。**不上 K8s**。
> - **Stage 3**:才换 `K8sSandboxProvider`,引入 namespace + gVisor + NetworkPolicy + Pod Security Standard 全套(§5 全文落地)。
> - **Stage 4 / premium**:按合同切 Kata-Firecracker 或独立 nodepool。
>
> 读 §2~§9 时把它当作"Stage 3 完成态"Stage 1 落地时只摘 §3 出网/资源那两行就好。
沙箱是多租户里**爆炸半径最大**的组件:客户的 agent 可以跑任意 bash 命令、读写文件、调用 MCP 工具。如果隔离不够强,一个客户能:
- **读到其他客户的数据**(容器逃逸 / 共享卷误用)
@@ -7,6 +7,7 @@
| 决策者 | 产品 + CTO + 财务 |
| 关联 ADR | ADR-001 数据隔离、ADR-005 存储拓扑、ADR-006 运行时与渠道 |
| 关联审计 | [adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) |
| 代码命名 | 本 ADR 写 `tenant_*` 表 / `tenant_id` 列,落代码统一读作 `workspace_*` / `workspace_id`(详 [workspace-schema-design §1](./workspace-schema-design.zh-CN.md#1-命名约定--workspace-vs-tenant) |
---
@@ -7,6 +7,7 @@
| 决策者 | 产品 + 后端 lead |
| 关联 ADR | ADR-001 数据隔离、ADR-003 LLM Key 与计费 |
| 关联审计 | [adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) — 现状:`users.token_version` + JWT `ver` claim 已存在;`@require_permission` 装饰器存在但 `owner_check` 是 bool 需扩为 enum`MembershipCache` 30s LRU 全新建 |
| 代码命名 | 本 ADR 写 `tenant_id` / `tenant_memberships`,落代码读作 `workspace_id` / `workspace_memberships`(详 [workspace-schema-design §1](./workspace-schema-design.zh-CN.md#1-命名约定--workspace-vs-tenant) |
---
@@ -7,6 +7,7 @@
| 决策者 | 架构 + 后端 lead + SRE |
| 关联 ADR | ADR-001 数据隔离、ADR-002 沙箱隔离、ADR-006 运行时与渠道 |
| 关联审计 | [adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) — 现状:`ObjectStorage` Protocol、7 张新表、KMS 抽象**全部不存在**,本 ADR 描述的是从 0 起的设计;phase-0 §3.5 已加"底座先行"骨架要求 |
| 代码命名 | 本 ADR 写 `tenants/{tid}/...` prefix 与 `tenant_*` 表名,落代码统一读作 `workspaces/{wid}/...``workspace_*`(详 [workspace-schema-design §1](./workspace-schema-design.zh-CN.md#1-命名约定--workspace-vs-tenant) |
---
@@ -289,9 +290,11 @@ storage:
## 5. 落地改造点(实施清单)
按优先级排序,每条对应第 1 阶段或第 2 阶段的一个 PR
按优先级排序,每条对应一个 PR
### 第 1 阶段(必须)
> **分期映射**(与 [phased-rollout-by-scale](../02-rollout/phased-rollout-by-scale.zh-CN.md) 对齐):本 §5 的"第 1 阶段"≈ rollout 的 **Stage 2**KMS + ObjectStorage + RLS 同期落地);"第 2 阶段"≈ rollout 的 **Stage 3**。Stage 0 / Stage 1 仅复用现有本地文件系统,不动 storage 拓扑。
### 第 1 阶段(必须;对应 rollout Stage 2
1. **抽象 `ObjectStorage` 接口 + `LocalObjectStorage` 实现** — 走通端到端,开发/测试用本地目录跑,不阻塞迁移
2. **memory.json 迁库**
@@ -339,7 +342,7 @@ storage:
**估工**:原 ADR-005 列了 1 条 bullet 偏乐观;这块包含 c/d/e/f 四子项,**整体约 M 偏 L**(一周量级),不是 S。
### 第 1 阶段 / 第 2 阶段交界
### 第 1 阶段 / 第 2 阶段交界rollout Stage 2 末 / Stage 3 头)
5. **`S3ObjectStorage` 实现** — 用 aioboto3,覆盖 protocol 全部方法
6. **上传文件改 presigned 直传**
@@ -353,7 +356,7 @@ storage:
- `POST /api/skills/install`:把 .skill 上传到 S3(带 SHA256 metadata),不再解压到本地全局目录
- 启动时按 tenant skill list 从 S3 拉 + 校验 + 解压到 LRU 缓存
### 第 2 阶段(建议)
### 第 2 阶段(建议;对应 rollout Stage 3
9. **退订 GC**tenant 标记 deleted 后,30 天定时任务跑 `delete_prefix(f"tenants/{tid}/")` + DB cascade delete
10. **跨区域复制 / CDN**:按客户分布加 region replica 或 CloudFront / OSS 加速域名
@@ -7,6 +7,7 @@
| 决策者 | 后端 lead + 架构 + 渠道 owner |
| 关联 ADR | ADR-001 数据隔离、ADR-003 LLM Key 与计费、ADR-005 存储拓扑 |
| 关联 spike / 审计 | [adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) · [langgraph-postgres spike](./adr-spike-langgraph-postgres.zh-CN.md) |
| 代码命名 | 本 ADR 写 `tenant_id` / `TenantMCPCache` / `tenant-{tenant_id}` namespace,落代码统一读作 `workspace_id` / `WorkspaceMCPCache` / `ws-{workspace_id}`(详 [workspace-schema-design §1](./workspace-schema-design.zh-CN.md#1-命名约定--workspace-vs-tenant) |
---
@@ -7,6 +7,7 @@
| 决策者 | 前端 lead + 后端 lead + 产品 |
| 关联 ADR | ADR-001 数据隔离、ADR-004 RBAC、ADR-006 运行时与渠道 |
| 关联审计 | [adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) |
| 代码命名 | 本 ADR 写 `tenant_id` / JWT `tid`,落代码统一读作 `workspace_id` / JWT `wid`(详 [workspace-schema-design §1, §4](./workspace-schema-design.zh-CN.md) |
---
@@ -107,12 +108,15 @@ ADR-001 ~ 006 锁定了数据/沙箱/Key/RBAC/存储/运行时——但客户最
API 路径**不带 slug**
```
/api/... # 业务 APItenant 由 JWT 决定
/api/langgraph/threads/{tid}/runs/stream # LangGraph 兼容
/api/v1/... # 业务 APItenant 由 JWT / API key 决定;Stage 1 起强制带版本
/api/... # 旧路径,Stage 1 起转发到 /api/v1Stage 3 sunset
/api/langgraph/threads/{tid}/runs/stream # LangGraph 兼容(不带版本,跟随上游 SDK 约定)
```
理由:API 是 SDK 调用的,不需要人类可读 URL;slug 只在浏览器导航/分享时有意义。
**`/api/v1/` 引入时机与设计**:详见 [headless-api-track §4](../02-rollout/headless-api-track.zh-CN.md#4-核心设计api-版本化) —— Stage 1 切换 mount prefix、保留旧路径转发并加 `X-API-Deprecated` header。
---
## 5. 后端:Path slug 与 JWT 的交叉校验
@@ -277,14 +281,16 @@ frontend/src/app/
## 8. Auth 改造(基于现有自签 JWT)
> 现状:`app/gateway/auth/jwt.py:14-19` `TokenPayload` 当前是 `{sub, exp, iat, ver}`;前端 cookie name 是 `access_token``users.token_version` 已存在,bump 该列即让所有旧 JWT 失效。
>
> **代码字段名以 [workspace-schema-design §4](./workspace-schema-design.zh-CN.md#4-jwt-tokenpayload--一次到位的字段集) 为准**:本 ADR 写 `tid`、落代码写 `wid`(同义)。Stage 0 PR2 已锁定 `wid` 命名 + Stage 0 一次性加齐 `wid` + `role` 两字段,避免 Stage 2 再 bump `token_version` 导致全用户重登。
多租户化改造:
1. **扩 `TokenPayload`**
1. **扩 `TokenPayload`**(字段名以 workspace-schema-design §4 为准)
```python
class TokenPayload(BaseModel):
sub: str # user_id
tid: str # tenant_id(新增)
tid: str # tenant_id(新增;落代码读作 wid / workspace_id
role: str # owner | admin | member(新增)
exp: int
iat: int
@@ -226,7 +226,7 @@ grep -rn "extensions_config\|skills/public\|skills/custom" backend/
| 底座 | 缺失现状 | 为何阻塞 ADR | Phase-0 内必须产出 |
|---|---|---|---|
| **Postgres 测试夹具**testcontainers + RLS smoke 测试基础设施) | 仓库当前以 SQLite 为默认后端,`tests/` 下无 Postgres fixtureSQLite 不支持 RLS | ADR-001 / 004 / 005 的所有租户隔离测试都要 Postgres | testcontainers 集成 + 至少 1 个 RLS 冒烟测试模板 + CI 跑通 |
| **Postgres 切换 + testcontainers 夹具**(生产 + 测试基础设施一并落) | 仓库当前以 SQLite 为默认后端,`tests/` 下无 Postgres fixtureSQLite 不支持 RLS | ADR-001 / 004 / 005 的所有租户隔离测试都要 PostgresStage 0 ALTER 4 张表如果在 SQLite 上做完再切 PG 是纯返工 | **Stage 0 直接切 Postgres 为生产默认**(Stage 0 没有生产数据,迁移阻力最小)+ testcontainers 集成 + 至少 1 个 RLS 冒烟测试模板policy Stage 2 才启用,但夹具 Stage 0 就位)+ CI 跑通 |
| **ObjectStorage Protocol + 实现** | `backend/packages/harness/deerflow/` 内 grep 不到 `ObjectStorage` 类;当前 memory/uploads/artifacts 全走文件系统 | ADR-005 §2 的三层拓扑、ADR-006 §2.2 的 OAuth 持久化都依赖它 | Protocol 接口 + LocalObjectStorage 骨架(可不实现 S3,留接口) |
| **KMS / Secret Vault 抽象** | 当前没有 secret 加密层;`mcp/oauth.py` 的 token 是明文进程内存 | ADR-003 §4.6 BYO key、ADR-006 §2.2 MCP OAuth、ADR-007 channel binding token 共用 | 抽象接口(envelope encryption pattern+ 本地 dev 实现(明文 fallback + 警告日志),生产实现可推迟 |
@@ -271,7 +271,8 @@ grep -rn "extensions_config\|skills/public\|skills/custom" backend/
| 决策 | 默认值 | 选它的理由 |
|---|---|---|
| 数据隔离 | 行级 + Postgres RLS(仅 DeerFlow 自有表)+ LangGraph 表应用层强校验 | 改造成本低;LangGraph 表无 RLS hookspike 已验证),应用层兜底 |
| **数据库** | Stage 0 起直接切 Postgres 为生产默认;SQLite 仅保留为可选 dev 兜底 | Stage 0 没有生产数据,迁移阻力最小;省 Stage 1 重 ALTER 一遍的返工 |
| 数据隔离 | 行级 + Postgres RLS(仅 DeerFlow 自有表,policy Stage 2 启用)+ LangGraph 表应用层强校验 | 改造成本低;LangGraph 表无 RLS hookspike 已验证),应用层兜底 |
| 沙箱隔离 | K8s namespace + gVisor + NetworkPolicy 默认禁出网 | 强度足够 + 运维可控 |
| LLM Key | 混合:默认平台 key + 限额,premium 切 BYO;悲观预扣防超额 | 体验与成本兼顾 |
| 租户层级 | 二级 RBACowner/admin/member+ JWT/cache 双层 | 为 SSO 和企业销售留口 |
@@ -0,0 +1,306 @@
# Workspace Schema 设计 · Stage 0 锁定版
> 写于 2026-05-10。Stage 0 PR1 动手前的 schema 锁定文档。
>
> **数据库基线**Stage 0 的 PR1 必须**先**完成 [phased-rollout Stage 0](../02-rollout/phased-rollout-by-scale.zh-CN.md#stage-0--workspace-模型立起来--postgres-切换--auth-收紧) 的 PR1-PR2Postgres 接入 + 默认切换),再起 schema PR。下面所有 ALTER 都直接在 Postgres 上跑,**不再走 SQLite → Postgres 二次迁移**。SQLite 仅保留为可选 dev 兜底。
>
> **范围**:仅 Stage 0 必须落地的 schema —— `workspaces` / `workspace_memberships` 两张新表,`users` / `threads_meta` / `runs` / `feedback` 的 ALTER`service_accounts` / `api_keys` / `external_users` 的预建(Stage 0 末,schema only),以及 JWT `TokenPayload` 一次到位的字段集。
>
> **不在范围**:仓储实现细节、ContextVar、路径迁移、路由校验、Stage 1+ 才加的列(`plan` / `allowed_origins` / `custom_domain` 等)。
>
> **配套阅读**
> - [02-rollout/phased-rollout-by-scale.zh-CN.md](../02-rollout/phased-rollout-by-scale.zh-CN.md) Stage 0 必做项
> - [02-rollout/stage-0-code-map.zh-CN.md](../02-rollout/stage-0-code-map.zh-CN.md) 现状代码锚点
> - [adr-001-data-isolation.zh-CN.md](./adr-001-data-isolation.zh-CN.md) §4.1 表结构改造
> - [adr-004-tenant-rbac.zh-CN.md](./adr-004-tenant-rbac.zh-CN.md) §5.1 / §5.2 RBAC + JWT
> - [adr-007-routing-frontend.zh-CN.md](./adr-007-routing-frontend.zh-CN.md) §4 slug 规范、§8 JWT 改造
> - [02-rollout/headless-api-track.zh-CN.md](../02-rollout/headless-api-track.zh-CN.md) §2 service account / API key
---
## 1. 命名约定 · workspace vs tenant
| 维度 | 选择 |
|---|---|
| 数据库列名 | `workspace_id` |
| Python 标识符 | `workspace_id` / `WorkspaceRow` / `_current_workspace` |
| JWT claim | `wid`(紧凑) |
| 用户可见用语 | "Workspace"(团队 workspace 也叫 workspace,不分"个人空间" |
ADR-001 / 004 / 007 原稿写 `tenant_id`——这些 ADR**不重命名**(成本不抵收益),落代码时统一读作 `workspace_id`。本文档与 02-rollout 系列保持 `workspace` 用语一致。
> 推翻条件:拿到强企业客户后真的出现"租户内多 workspace"的层级(tenant > workspace > user),那时再分裂概念。Stage 0/1/2 不预留这层。
---
## 2. 新增表
### 2.1 `workspaces`
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
| `id` | `String(36)` | PK | UUID v4 字符串。与 `users.id` 类型对齐,跨 DB 可移植(SQLite/Postgres 都用 36 字符 CHAR |
| `name` | `String(64)` | NOT NULL | 显示名(用户首次注册时默认 `<email 前缀>'s Workspace` |
| `slug` | `String(32)` | UNIQUE NOT NULL | URL 标识,正则 `^[a-z0-9](-?[a-z0-9])*$`3-32 字符;DB 存小写 |
| `status` | `String(16)` | NOT NULL default `'active'` | `active` / `suspended` / `deleted`platform admin 暂停/删 workspace |
| `owner_id` | `String(36)` | NOT NULL FK `users.id` | 冗余字段,便于查询;与 `workspace_memberships.role='owner'` 严格一致(事务保证) |
| `created_at` | `DateTime(timezone=True)` | NOT NULL | UTC |
| `updated_at` | `DateTime(timezone=True)` | NOT NULL | UTC,写入自动更新 |
**索引**
- PK: `id`
- UNIQUE: `slug`
- 不加 `(status)` 索引——Stage 0 用户量小,全表扫够用;Stage 1+ 视情况补
**slug 黑名单**(应用层校验,不写进 DB constraint):
```
admin, api, auth, login, signup, accept-invite, pricing, docs, status,
platform, system, health, static, public, favicon.ico, robots.txt,
sitemap.xml, _next, .well-known, settings, billing, onboarding, select-workspace
```
> ADR-007 §4 列了一份基础黑名单;本表是落代码版(含 Next.js 保留路径)。
**Stage 0 不加的列**(决策记录):
| 列 | 推迟到 | 理由 |
|---|---|---|
| `plan` | Stage 1(与 `workspace_quotas.plan` 一起加) | Stage 0 没有付费分层 |
| `allowed_origins` | Stage 1 末(headless API Pattern B | 用到时加 ARRAY/JSON 列代价低 |
| `custom_domain` | Stage 4enterprise | 长尾需求,列加在哪一层都行 |
| `billing_email` | Stage 1Stripe 对接) | 一并加 |
| `settings_json` | 永不加 | 扩展点用专门的 `workspace_settings` 表 + 枚举 key,比 JSON dump 易迁移 |
### 2.2 `workspace_memberships`
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
| `workspace_id` | `String(36)` | PK / FK `workspaces.id` ON DELETE CASCADE | |
| `user_id` | `String(36)` | PK / FK `users.id` ON DELETE CASCADE | |
| `role` | `String(16)` | NOT NULL | Stage 0 只允许 `owner`Stage 2 起 `owner`/`admin`/`member` |
| `invited_by` | `String(36)` | NULL FK `users.id` ON DELETE SET NULL | Stage 0 暂不写入;Stage 2 invitation 流程才用 |
| `joined_at` | `DateTime(timezone=True)` | NOT NULL | UTC |
**为什么 `role` 用 `String(16)` 不用 DB enum**Postgres enum ALTER 加值需要 `ALTER TYPE ... ADD VALUE`,且不可删;string + 应用层校验 = 后续随便加 `viewer` / `auditor` 等角色不动 DB schema。
**索引**
- PK: `(workspace_id, user_id)` 复合主键
- `idx_workspace_memberships_user`: `(user_id, workspace_id)` —— 倒查索引,用于 `/auth/me` 列出当前 user 所有 workspace
- `idx_one_owner_per_workspace`: UNIQUE on `(workspace_id)` WHERE `role = 'owner'` —— partial unique index
**partial unique 兼容性**
- SQLite **支持** `CREATE UNIQUE INDEX ... WHERE ...`(参 `users.idx_users_oauth_identity` 现有用法)
- Postgres 同样支持
- SQLAlchemy 通过 `Index(..., unique=True, sqlite_where=text(...), postgresql_where=text(...))` 表达;这条索引**保留两套 where 条件等价**
### 2.3 `service_accounts`Stage 0 末加,schema only
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
| `id` | `String(36)` | PK | |
| `workspace_id` | `String(36)` | NOT NULL FK `workspaces.id` ON DELETE CASCADE | 必属于一个 workspace |
| `name` | `String(64)` | NOT NULL | 业务系统起的标识名 |
| `role` | `String(16)` | NOT NULL default `'member'` | SA 在 workspace 内的 role |
| `identity_mode` | `String(16)` | NOT NULL default `'collapsed'` | `collapsed` / `external_passthrough` / `both` |
| `status` | `String(16)` | NOT NULL default `'active'` | `active` / `suspended` / `revoked` |
| `created_by` | `String(36)` | NOT NULL FK `users.id` ON DELETE RESTRICT | 必须是 workspace owner/admin |
| `created_at` | `DateTime(tz)` | NOT NULL | |
| `updated_at` | `DateTime(tz)` | NOT NULL | |
索引:`idx_service_accounts_workspace`: `(workspace_id, status)`
### 2.4 `api_keys`Stage 0 末加,schema only
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
| `id` | `String(36)` | PK | |
| `service_account_id` | `String(36)` | NOT NULL FK `service_accounts.id` ON DELETE CASCADE | |
| `key_prefix` | `String(16)` | UNIQUE NOT NULL | 前 16 字符明文(`dfk_live_...`),UI 展示用 |
| `key_hash` | `String(128)` | NOT NULL | 完整 key 的 sha256 hex64 字符)+ 余量 |
| `name` | `String(64)` | NOT NULL | "生产环境 key" |
| `scopes` | `String(1024)` | NOT NULL default `''` | 逗号分隔字符串。Postgres 已是 Stage 0 默认,但保持 `String` 以兼容 SQLite dev 兜底;如果未来确认完全弃用 SQLite,可平滑迁 `text[]` |
| `rate_limit_rpm` | `Integer` | NULL | NULL = 用 workspace plan 默认 |
| `expires_at` | `DateTime(tz)` | NULL | |
| `last_used_at` | `DateTime(tz)` | NULL | |
| `revoked_at` | `DateTime(tz)` | NULL | 软删除标记 |
| `created_at` | `DateTime(tz)` | NOT NULL | |
索引:
- `idx_api_keys_sa`: `(service_account_id)`
- `idx_api_keys_active`: `(key_prefix)` WHERE `revoked_at IS NULL`partial
### 2.5 `external_users`Stage 0 末加,schema only
| 列 | 类型 | 约束 | 说明 |
|---|---|---|---|
| `id` | `String(36)` | PK | ghost user idDeerFlow 内部) |
| `workspace_id` | `String(36)` | NOT NULL FK `workspaces.id` ON DELETE CASCADE | |
| `service_account_id` | `String(36)` | NOT NULL FK `service_accounts.id` ON DELETE CASCADE | |
| `external_id` | `String(128)` | NOT NULL | 业务系统传入 ID,原样存 |
| `display_name` | `String(128)` | NULL | |
| `metadata_json` | `JSON` | NOT NULL default `{}` | 业务字段 |
| `created_at` | `DateTime(tz)` | NOT NULL | |
| `last_seen_at` | `DateTime(tz)` | NULL | |
索引:UNIQUE `(service_account_id, external_id)` —— 同一 SA 下 external_id 唯一
---
## 3. ALTER 现有表
### 3.1 `users`
```python
# 新增列
default_workspace_id: Mapped[str | None] = mapped_column(
String(36),
ForeignKey("workspaces.id", ondelete="SET NULL"),
nullable=True,
comment="登录后默认进入的 workspaceNULL 时强制走 pickeruser 多 workspace 场景)"
)
```
**为什么不加 `current_workspace_id`**:每次登录时从 `default``/select-workspace` 决定,写入 JWT 的 `wid` claimDB 不存"当前激活"状态,避免多设备冲突。
`system_role` 字段保留——它是**平台级** role(`platform_admin` / `user`),与 workspace role 正交(参 ADR-004 §6)。
### 3.2 `threads_meta`
```python
# 新增列(Stage 0 PR3 先 nullable,回填后 ALTER 改 NOT NULL
workspace_id: Mapped[str | None] = mapped_column(
String(36),
ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=True, # PR3 中段;回填脚本跑完改 NOT NULL
comment="所属 workspace;与 (thread_id) 复合 UNIQUE 防跨 workspace 复用同 ID"
)
# 新增索引(在 __table_args__ 里)
Index("idx_threads_meta_workspace_thread", "workspace_id", "thread_id", unique=True),
Index("idx_threads_meta_workspace_user_updated", "workspace_id", "user_id", "updated_at"),
```
**关键索引说明**
- `(workspace_id, thread_id)` UNIQUE 是 ADR-001 §4.1.1 修订版的"应用层强约束 + DB 兜底"防线
- `(workspace_id, user_id, updated_at)` 覆盖前端 thread list 默认查询模式
- 现有 `user_id` 上的非复合索引可以**保留**(删了某些后台 cleanup 脚本会变慢;不阻塞主路径)
### 3.3 `runs`
```python
workspace_id: Mapped[str | None] = mapped_column(
String(36),
ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=True, # PR3 中段
)
Index("idx_runs_workspace_created", "workspace_id", "created_at"),
```
### 3.4 `feedback`
```python
workspace_id: Mapped[str | None] = mapped_column(
String(36),
ForeignKey("workspaces.id", ondelete="CASCADE"),
nullable=True,
)
Index("idx_feedback_workspace_run", "workspace_id", "run_id"),
```
### 3.5 `run_events`(待确认)
stage-0-code-map §2 标注 `run_events` 是否 DB 持久化"unverified"。PR3 第一步先 grep 确认;若是 DB 表就比照 `runs``workspace_id`,若是内存队列则跳过。
---
## 4. JWT TokenPayload · 一次到位的字段集
> Stage 0 落 `wid` + `role`**Stage 2 不再 bump**。理由:每次扩字段都要 bump `token_version` 让所有用户重登,churn 体验差;一次加齐两次的份。
```python
# backend/app/gateway/auth/jwt.py
class TokenPayload(BaseModel):
sub: str # user_id(沿用)
wid: str # workspace_idStage 0 新增)
role: str # owner / admin / memberStage 0 新增;1 人 workspace 默认 'owner'
exp: datetime # 沿用
iat: datetime | None = None # 沿用
ver: int = 0 # token_version(沿用;任何 membership 变更 bump
```
**Stage 0 实际填充值**
- `wid` = 用户 `default_workspace_id`(注册时自动建的 1 人 workspace
- `role` = 总是 `'owner'`Stage 0 还没有团队 workspace
**Stage 2 启用时**
- 加入团队 workspace 后 → `role` 真正区分 admin/member
- bump `token_version` 让旧 JWT(仍写 `'owner'`)过期重发
**兼容性**
- 旧 4 字段 token`{sub, exp, iat, ver}`)解码失败时**强制走 `/select-workspace`** 重发新 JWT(参 ADR-007 §11)。Stage 0 部署后 7 天(默认 token TTL)内所有老 token 自然轮替完。
---
## 5. 不可逆决策清单
| 决策 | 选择 | 反悔代价 |
|---|---|---|
| id 类型 | `String(36)` (UUID v4 字符串) | 改 native UUID → 全表 schema rewrite + 所有 FK 重建 + Python `str``UUID` 边界改造 |
| 命名(workspace_id | `workspace_id` | 改 `tenant_id` → 全代码改名 + 所有 ADR 文档同步 |
| slug 字符集 | `^[a-z0-9](-?[a-z0-9])*$` 3-32 | 改 → 老 URL 全失效(v1 还没暴露 slug 路由前改是免费的) |
| memberships PK | `(workspace_id, user_id)` 复合 | 改 surrogate id → migration 脚本要写 dedup 逻辑 |
| TokenPayload 字段 | `sub/wid/role/exp/iat/ver` | 加新字段 → 必 bump `token_version`,全用户重登(Stage 0 一次性加 `wid` + `role`,省一次) |
| `users.default_workspace_id` 而非 `current_workspace_id` | 默认 + JWT 决定当前 | 改成 `current_*` → 多设备语义混乱 |
| FK 删除策略(workspace 删 → memberships/threads CASCADE| CASCADE | 改 RESTRICT → 平台 admin 删 workspace 时手动级联,运营负担大 |
> Stage 0 PR1 合入前,上面这 7 项**全部**要在团队 review 中拍板;任何一项改主意都要 revert PR1 重写。
---
## 6. PR 拆分(Stage 0 内的 5 个 schema PR
> **前置 PR**:本表 PR1 之前必须先完成 [phased-rollout Stage 0 PR1-2](../02-rollout/phased-rollout-by-scale.zh-CN.md#stage-0--workspace-模型立起来--postgres-切换--auth-收紧)Postgres 接入 + testcontainers + 默认 backend 切换。本文档下面的 PR1 = phased-rollout 的 PR3,本文档 PR5 = phased-rollout 的 PR8。
>
> 在 Postgres 已就绪的基础上,schema 改动按下面 5 个 PR 推:
| PR | 范围 | 落本文档的哪些章节 | 状态 |
|---|---|---|---|
| **PR1** | `workspaces` + `workspace_memberships` 表 + 仓储 + 单测 | §2.1 + §2.2 | 设计完,可写 |
| **PR2** | 注册/initialize 流程改造(自动建 1 人 workspace+ JWT 扩 `wid`/`role` + AuthMiddleware ContextVar 注入 | §3.1 (`default_workspace_id`) + §4 | 依赖 PR1 |
| **PR3** | 现有 4 表 ALTER 加 `workspace_id`(直接 Postgres,先 nullable+ 数据回填脚本(legacy_workspace)→ ALTER 改 NOT NULL | §3.2-§3.5 | 依赖 PR1+PR2 |
| **PR4** | 路由层 `(workspace_id, thread_id)` 校验 + `Paths` 切 workspace 维度 + 文件系统迁移脚本 | 不在本文档(仓储/路径设计) | 依赖 PR3 |
| **PR5** | `service_accounts` / `api_keys` / `external_users` schema(不接路径) | §2.3-§2.5 | 与 PR4 并行 |
每个 PR 必须独立可上线、可回滚。PR1 单独合入后系统行为不变(新表无人写入)。
---
## 7. 测试要点(PR1 范围)
仿现有 `tests/test_*.py` 模式:
- `test_workspace_repo.py`
- create / get / update / delete workspace
- slug 唯一性约束
- slug 黑名单校验(应用层)
- status 状态机(active → suspended → deleted
- `test_workspace_membership_repo.py`
- create membership
- 同一 workspace 不能有 2 个 ownerpartial unique index
- CASCADE 删(删 workspace 后 memberships 消失)
- `list_workspaces_by_user(user_id)` 返回正确顺序
- 不写:路由测试(PR4 才有路由)、JWT 测试(PR2 才扩字段)
---
## 8. 推翻条件
整份 schema 设计要重排只在两种情况:
1. **拿到强企业客户必须自定义角色**role 列从 String(16) + 应用层校验 → 完整 RBAC engine`roles` / `permissions` / `role_permissions` 表)。schema **加表**,不改现有列,影响小。
2. **决定改用 native UUID 类型**Postgres 切换时一并):String(36) → `UUID`。需要全表 ALTER + Python 边界改造。建议**不**做,String(36) 在 Postgres 上落地为 `CHAR(36)`,性能差异 < 5%,可接受。
> 上面 §5 七项不可逆决策不在"推翻条件"覆盖范围——那些一旦发布到生产就只能往前走。
@@ -3,7 +3,7 @@
> 写于 2026-05-09。承接 [phased-rollout-by-scale.zh-CN.md](./phased-rollout-by-scale.zh-CN.md)。
>
> **触发**:现已有 1-2 个明确的业务系统集成需求,1-3 个月内要 demo / 调通。集成形态包括 IM channels(已支持)+ 业务系统自研 web 页面。
> **商业形态**SaaS + on-prem 双主线
> **商业形态**与 [phased-rollout-by-scale §0](./phased-rollout-by-scale.zh-CN.md) 一致——**中心化 SaaS 主线**schema / auth / quota 设计对 on-prem 友好(`workspaces.id` 映射到 self-host 安装),但 on-prem 不作为产品主线,仅按客户合同启用
> **身份模式**service account 折叠 + external_user_id 透传,两种都支持,按 endpoint 选。
> **集成 pattern****Pattern A(业务系统 backend 代理)+ Pattern B(浏览器直连 + 短期 JWT**。不做嵌入式 widget。
@@ -433,14 +433,13 @@ SELECT idempotency_records WHERE (api_key_id, key=...) AND created_at > NOW() -
## 6. 与 Stage 1 的整合
把 headless API MVP 包并入 Stage 1**时间盒从 6-10 周延到 8-13 周**。
把 headless API MVP 包并入 Stage 1**时间盒 8-13 周**Postgres 切换已前移到 Stage 0,原"6-10 周 + 4-5 周 headless = 10-15"减去 Postgres 的 ~2 周)
### 修订后 Stage 1 必做项
| 改动 | 类型 | 估工 |
|---|---|---|
| Postgres 切换 | 原 Stage 1 | M |
| Quota 系统 + TokenUsage 持久化 | 原 Stage 1 | M+ |
| Quota 系统 + TokenUsage 持久化 | 原 Stage 1Postgres 已就绪) | M+ |
| Stripe 基础订阅 | 原 Stage 1 | M |
| AioSandbox 出网/资源收紧 | 原 Stage 1 | M |
| **API Key + Service Account 数据模型 + 仓储** | **新增(Pattern A** | M |
@@ -456,20 +455,19 @@ SELECT idempotency_records WHERE (api_key_id, key=...) AND created_at > NOW() -
| **`workspaces.allowed_origins` + CORS 中间件** | **新增(Pattern B** | M |
| **Idempotency keys** | **可选**(推荐) | S |
合计原 Stage 1M+M++M+M=4M+ 新增 Pattern AM+M+XS+M+S+S+S+S=4M+ 新增 Pattern BS+S+M=2M= 约 10M-15 周。Pattern B 依赖 Pattern A 完成,建议放 Stage 1 末。
合计原 Stage 1 不含 PostgresM+M+M=3M+ 新增 Pattern AM+M+XS+M+S+S+S+S=4M+ 新增 Pattern BS+S+M=2M= 约 9M ≈ 8-13 周。Pattern B 依赖 Pattern A 完成,建议放 Stage 1 末。
### 修订后 Stage 1 PR 顺序
**轨道一:付费 SaaS 基础**与下面并行
1. Postgres 切换(dev → 灰度 → 全切)
2. `workspace_quotas` / `workspace_usage_daily` 表 + 仓储
3. `TokenUsageMiddleware` 升级为持久化
4. `QuotaMiddleware` 加入中间件链
5. Stripe webhook + 订阅状态同步
6. AioSandbox 收紧
7. 基础监控
**轨道一:付费 SaaS 基础**Postgres 已在 Stage 0 切完,本轨道直接从 quota 起
1. `workspace_quotas` / `workspace_usage_daily` 表 + 仓储
2. `TokenUsageMiddleware` 升级为持久化
3. `QuotaMiddleware` 加入中间件链
4. Stripe webhook + 订阅状态同步
5. AioSandbox 收紧
6. 基础监控
**轨道二:Headless API Pattern A**(与轨道一并行;步骤 1 必须先完成轨道一的 1
**轨道二:Headless API Pattern A**(与轨道一并行;无前置依赖
1. `service_accounts` + `api_keys` + `external_users` 仓储(**先于业务路径**
2. `APIKeyAuthBackend` + `AuthMiddleware` 双路径(cookie + bearer
3. CSRF middleware skip on bearer
@@ -491,9 +489,11 @@ SELECT idempotency_records WHERE (api_key_id, key=...) AND created_at > NOW() -
---
## 7. SaaS vs on-prem 差异
## 7. SaaS vs on-prem 差异SaaS 是主线)
| 能力 | SaaS 形态 | on-prem 形态 |
> **口径**phased-rollout §0 已明确"中心化 SaaS 主线,不做 self-host 主线"。本节列出**如果**未来按客户合同启用 on-prem 时的差异点——目的是让 Stage 0/1 的 schema 与 auth 设计**不阻塞** on-prem,而不是把 on-prem 当作并行产品线投入资源。
| 能力 | SaaS 形态 | on-prem 形态(按合同启用) |
|---|---|---|
| API key 管理 | workspace settings UI + CLI | CLI 必须;UI 可选;env var 注入预置 key 也合理 |
| 配额 / 计费 | 按 plan,Stripe 同步 | 配额作为容量管理(防内部失控),不接 Stripe |
@@ -16,8 +16,8 @@
| Stage | 触发条件(业务事实) | 主旋律 | 时间盒 |
|---|---|---|---|
| **0** | 现在 → 第一个付费客户准备 | workspace 模型立起来;现有 auth 收紧;不做真隔离 | 34 周 |
| **1** | 第一批付费客户(1050 付费 / 5002000 free + **1-2 业务系统集成(含自研 web 页面)** | **Postgres + Quota + Headless APIPattern A backend 代理 + Pattern B browser 直连)必落**workspace 全链路 + 入口强校验;AioSandbox 收紧 | 1015 周 |
| **0** | 现在 → 第一个付费客户准备 | workspace 模型立起来;**Postgres 切换**现有 auth 收紧;不做真隔离 | 45 周 |
| **1** | 第一批付费客户(1050 付费 / 5002000 free + **1-2 业务系统集成(含自研 web 页面)** | **Quota + Headless APIPattern A backend 代理 + Pattern B browser 直连)必落**workspace 全链路 + 入口强校验;AioSandbox 收紧 | 813 周 |
| **2** | 增长期(100500 付费 / 5k20k 用户) | DeerFlow 表 RLS、KMS、ObjectStorage S3、内部 LLM 计费分类、付费分层、Webhook outbound | 1016 周 |
| **3** | 成熟期(1k+ 付费 / 50k+ 用户)**或** 出现安全/成本事故 | K8s sandbox + namespace、BYO key(付费档福利)、audit DB 拆分、prewarm 池 | 1626 周 |
| **4** | 单客户合同驱动(合规 / 企业销售) | SSO、custom domain、per-tenant DB(仅强合规) | 按需,单客户 4–8 周 |
@@ -28,29 +28,30 @@
---
## Stage 0 — workspace 模型立起来 + auth 收紧
## Stage 0 — workspace 模型立起来 + Postgres 切换 + auth 收紧
**触发**:你现在所在的位置——刚把 ADR 收敛完,准备开第一个付费客户。
**退出**:能给一个外部用户开账号,他登进来看到自己的 workspace、能创建 thread、隔离干净。
**时间盒**34 周
**退出**:能给一个外部用户开账号,他登进来看到自己的 workspace、能创建 thread、隔离干净;生产已跑在 Postgres 上
**时间盒**45 周(原 34 周;Postgres 切换 + testcontainers + 部署/onboarding 调整加 1 周)
### 必做
| 改动 | 说明 |
|---|---|
| **Postgres 切换**dev + 生产)| **不可逆决策**——Stage 0 没有生产数据,迁移阻力最小;现在切完省掉 Stage 1 重 ALTER 一遍的返工。`init_engine_from_config` 已支持双驱动,docker-compose 加 PG service、`make setup`/`make doctor`/CI 切默认。**这是 §3.5 底座先行的成果落地**,不是单独 spike。 |
| **Postgres testcontainers + RLS 测试夹具骨架** | phase-0 §3.5 底座之一;CI 跑通至少 1 个 RLS 冒烟测试模板(Stage 0 还没用 RLS,但夹具就位) |
| **`workspaces` 表 + 自动建 1 人 workspace** | 每个新注册用户自动获得 1 个 workspace;用户 = workspace owner。这是后面所有租户改造的底座。 |
| **`workspace_id` 列加到现有 SQLite 表** | `threads_meta` / `runs` / `feedback` / `users``workspace_id`。**不可逆决策**——SQLite 上加列后再迁 Postgres 比直接在 Postgres 上加痛苦得多。 |
| **`workspace_id` 列加到现有表**(直接在 Postgres 上加)| `threads_meta` / `runs` / `feedback` / `users``workspace_id`。**不可逆决策**——直接在 Postgres 上 ALTER 一次,不再走 SQLite → Postgres 二次迁移。 |
| **`workspace_memberships` 表** | 即使个人用户也是"1 个 owner 成员",团队功能未启用但模型先就位。`role` 字段先只有 `owner`。 |
| **`service_accounts` / `api_keys` / `external_users` schema** | Stage 1 才接路径,但 schema 在 Stage 0 末加上不阻塞——避免 Stage 1 临时改表。详见 [headless-api-track §2](./headless-api-track.zh-CN.md)。 |
| **JWT 扩 `wid` 字段** | 沿用现有 `app/gateway/auth/jwt.py` `TokenPayload`(参 ADR-007 §8 修订版),加 `wid`workspace_id),不引入 Better Auth。 |
| **入口路由 `(workspace_id, thread_id)` 校验** | `threads.py` + `thread_runs.py` 入口处必校验(参 ADR-001 §4.1.2)。SQLite 阶段就上,避免 Stage 1 临时补。 |
| **入口路由 `(workspace_id, thread_id)` 校验** | `threads.py` + `thread_runs.py` 入口处必校验(参 ADR-001 §4.1.2)。Stage 0 就上,避免 Stage 1 临时补。 |
| **CLI / admin UI 的"workspace 管理"基础** | platform admin 能看 workspace 列表、暂停/删除某个 workspace(防止滥用第一时间反应)。 |
| **现有 auth 完善** | setup flow 能创建第一个 admin、邀请用户走基本流程(不必 invitation token,可手动建账号)。`token_version` 已存在,复用。 |
### 不做(推迟到 Stage 1+
- ❌ Postgres 切换 — Stage 1
- ❌ RLS — Stage 2
- ❌ RLS policy 启用 — Stage 2(夹具 Stage 0 就位,但 policy 不上)
- ❌ Quota 系统 — Stage 1(早一点也行,但有了第一个付费客户再做反应快)
- ❌ K8s sandbox — Stage 3
- ❌ KMS / ObjectStorage S3 — Stage 2
@@ -59,17 +60,20 @@
### 关键 PR 顺序(避免半截不可运行)
1. `workspaces` + `workspace_memberships` 表 + 仓储
2. 注册流程改造(自动建 1 人 workspace+ JWT 扩 `wid`
3. 现有表 ALTER 加 `workspace_id` 列 + 数据回填脚本("legacy_workspace"
4. `threads.py` / `thread_runs.py` 入口校验 + 迁移所有现有 thread 到对应 workspace
5. CI boundary 测试:禁止任何路径绕过入口直连 LangGraph saver
1. **Postgres 接入 + testcontainers**docker-compose 加 PG、`make doctor` 兼容、CI 跑通;现有 SQLite 数据导入(如有 dev 数据)
2. **将默认 backend 切到 Postgres**`make setup` / `make dev` / `.env.example` 默认指向 PG;SQLite 保留为可选 dev 兜底
3. `workspaces` + `workspace_memberships` 表 + 仓储
4. 注册流程改造(自动建 1 人 workspace+ JWT 扩 `wid`
5. 现有表 ALTER 加 `workspace_id` 列(直接在 Postgres 上加,先 nullable+ 数据回填脚本("legacy_workspace")→ ALTER 改 NOT NULL
6. `threads.py` / `thread_runs.py` 入口校验 + 迁移所有现有 thread 到对应 workspace
7. CI boundary 测试:禁止任何路径绕过入口直连 LangGraph saver
8. `service_accounts` / `api_keys` / `external_users` schema onlyStage 0 末,为 Stage 1 准备)
### Go/No-Go 进入 Stage 1
- 第一个付费意向客户出现
- Stage 0 已部署到生产 ≥ 2 周,无 workspace 隔离 bug 报告
- 团队对 SQLite 性能瓶颈有共识(用户数破百时切 Postgres)
- 生产已稳定运行在 Postgres 上 ≥ 2 周,无 schema / 性能 regression
---
@@ -77,15 +81,14 @@
**触发**:Stage 0 跑稳 + 拿到第一批付费用户(10–50 付费 / 5002000 free+ 1-2 个业务系统集成需求。
**退出**:① 能放心让媒体/产品社区曝光,不会被白嫖跑偏;② 业务系统能用 API key 调通核心 endpointgo-live。
**时间盒**1015 周(原稿 610 周;headless API Pattern A 加 2-3 周 + Pattern B 加 2 周)
**时间盒**813 周(原 10-15 周;Postgres 切换已在 Stage 0 完成,省 2 周)
> Stage 1 是**双轨并行**:付费 SaaScookie auth + Stripe + quota)和 Headless APIbearer auth + service account + `/api/v1/`)。两者共用 workspace + auth + quota 基座。详细 headless API 设计见 [headless-api-track.zh-CN.md](./headless-api-track.zh-CN.md)。
> Stage 1 是**双轨并行**:付费 SaaScookie auth + Stripe + quota)和 Headless APIbearer auth + service account + `/api/v1/`)。两者共用 workspace + auth + quota 基座Stage 0 已落地)。详细 headless API 设计见 [headless-api-track.zh-CN.md](./headless-api-track.zh-CN.md)。
### 必做(付费 SaaS 轨道)
| 改动 | 说明 | 关联 ADR |
|---|---|---|
| **Postgres 切换** | 老数据 `pg_loader` 导入;`workspace_id` 已就位(Stage 0 加过)。**不可逆**。 | ADR-001 §4.4 |
| **Quota 系统 v1**(强制) | `workspace_quotas` + `workspace_usage_daily` 表;`QuotaMiddleware` 在 lead_agent 链最前;硬限到达拒调用。**Freemium 不上 quota = 信用卡递给攻击者**。 | ADR-003 §4.4 |
| **`TokenUsageMiddleware` 持久化** | 当前只 log(参 audit ADR-003);要写入 `workspace_usage_daily(workspace_id, date, model, tokens_in, tokens_out)`,按 SA / external_user 维度同时支持。 | ADR-003 §4.3 |
| **悲观预扣**(轻量版) | 按 `model_max_input_tokens` 估上限;幽灵 token 防御。 | ADR-003 §4.4.1 |
@@ -130,16 +133,15 @@
### 关键 PR 顺序(双轨)
**轨道 A:付费 SaaS**
1. Postgres 切换(dev 双驱动 → 生产灰度 → 全切)
2. `workspace_quotas` / `workspace_usage_daily` 表 + 仓储
3. `TokenUsageMiddleware` 升级为持久化(参 audit + ADR-003 §4.3
4. `QuotaMiddleware` 加入中间件链
5. Stripe webhook + 订阅状态同步到 `workspace_quotas.plan`
6. AioSandbox egress 白名单 + 资源限额
7. 监控/告警接入
**轨道 A:付费 SaaS**Postgres 已在 Stage 0 切完,本轨道直接从 quota 起)
1. `workspace_quotas` / `workspace_usage_daily` 表 + 仓储
2. `TokenUsageMiddleware` 升级为持久化(参 audit + ADR-003 §4.3
3. `QuotaMiddleware` 加入中间件链
4. Stripe webhook + 订阅状态同步到 `workspace_quotas.plan`
5. AioSandbox egress 白名单 + 资源限额
6. 监控/告警接入
**轨道 BHeadless API Pattern A**(与 A 并行;步骤 1-2 必须先完成轨道 A 的 1
**轨道 BHeadless API Pattern A**(与 A 并行;无前置依赖
1. `service_accounts` / `api_keys` / `external_users` 仓储(schema 已在 Stage 0 加上)
2. `APIKeyAuthBackend` + `AuthMiddleware` 双路径(cookie + bearer
3. CSRF middleware skip on bearer
@@ -166,12 +168,6 @@
- 文件存储或 secret 管理出现一次手忙脚乱(备份遗漏 / key 误提交等)
- 业务系统开始要求 webhook 推送(不再满足于轮询)
### Go/No-Go 进入 Stage 2
- 月活付费用户 ≥ 50 **或** 月活免费用户 ≥ 1000
- 出现一次"差点超额"事件(quota 在悲观预扣下还是漏了一次)
- 文件存储或 secret 管理出现一次手忙脚乱(备份遗漏 / key 误提交等)
---
## Stage 2 — 增长期,安全与隔离深化
@@ -284,10 +280,10 @@
| 决策 | 在哪 Stage 做 | 做错了的代价 |
|---|---|---|
| **`workspace_id` 列加到所有业务表** | Stage 0 | 漏了某张表 → Stage 1 还在补;SQLite 加列后再迁 Postgres 痛苦 |
| **Postgres 切换**dev + 生产)| Stage 0 | Stage 0 选这个时机:没有生产数据,迁移阻力最小;切完不回头 |
| **`workspace_id` 列加到所有业务表**(直接 Postgres| Stage 0 | 漏了某张表 → Stage 1 还在补;不再走 SQLite → Postgres 二次迁移 |
| **`workspaces` 表设计**slug、plan、status 字段) | Stage 0 | 后期改 schema 要写迁移;用户 URL 全变 |
| **JWT payload 字段** | Stage 0 + Stage 2 | 加字段时旧 cookie 失效;一次性想清楚 `wid/role/plan` 都加上,少一次 churn |
| **Postgres 切换** | Stage 1 | 老数据迁移;切完不回头 |
| **JWT payload 字段** | Stage 0 一次加齐 `wid+role` | 加字段时旧 cookie 失效;workspace-schema-design §4 锁定一次到位,避免 Stage 2 再 bump |
| **ObjectStorage prefix 形态**`workspaces/{wid}/...` | Stage 2 | 改了所有用户产物 URL 失效 |
| **K8s namespace 命名规则**`ws-{wid}` 还是 `tenant-{wid}` | Stage 3 | 改了所有 NetworkPolicy / RBAC |
@@ -313,15 +309,15 @@
| Stage | 触发 | 时间盒 | 累计 |
|---|---|---|---|
| 0 | 现在 | 34 周 | 1 个月 |
| 1 | 首批付费 + 业务系统集成(含自研 web 直连) | 1015 周(headless API Pattern A+B 并行 4-5 周) | 4-5 个月 |
| 2 | 增长期 | 1016 周 | 8-9 个月 |
| 3 | 成熟期 | 1626 周 | 14-16 个月 |
| 0 | 现在 | 45 周(含 Postgres 切换 + testcontainers | 1.01.3 个月 |
| 1 | 首批付费 + 业务系统集成(含自研 web 直连) | 813 周(headless API Pattern A+B 并行 4-5 周Postgres 切换已前移到 Stage 0 | 34 个月 |
| 2 | 增长期 | 1016 周 | 78 个月 |
| 3 | 成熟期 | 1626 周 | 1314 个月 |
| 4 | 企业客户 | 单客户 4–8 周 | + 按需 |
**全功能落地**~14-16 个月(Stage 03 累计),不含 Stage 4 enterprise 特性。
**最小可付费 + 业务系统集成**(Stage 0 + 1):~4-5 个月。
**风险可控的增长**Stage 0 + 1 + 2):~8-9 个月。
**全功能落地**~13-14 个月(Stage 03 累计),不含 Stage 4 enterprise 特性。
**最小可付费 + 业务系统集成**(Stage 0 + 1):~3-4 个月。
**风险可控的增长**Stage 0 + 1 + 2):~7-8 个月。
---
+319
View File
@@ -0,0 +1,319 @@
# 多租户改造 · 总览与汇总索引
> 写于 2026-05-10。把 7 份 ADR + 2 份 spike/审计 + 4 份 rollout / schema 文档,按"ADR 状态 + 5 阶段(Stage 0–4)的业务目标 / 技术路径 / 验证方式"重新串一遍,让团队从任何角度切入都能找到对应位置。
>
> **范围**:仅汇总与导航,不引入新决策。具体决策正文在各自的 ADR / rollout 文档里。
>
> **目标客户画像**(来自 [phased-rollout-by-scale §0](./02-rollout/phased-rollout-by-scale.zh-CN.md)):以个人用户为主、少量小团队;**统一只有 workspace 概念**(个人 = 1 人 workspace);**中心化 SaaS 主线**schema 兼容 on-prem,按合同启用);**Freemium**DeerFlow 付 LLM 账单)。
---
## 0. 文档总图
```
docs/multi-tenant-redesign/
├── README.zh-CN.md ← 本文档(入口)
├── 00-current-state/
│ └── architecture-overview.zh-CN.md 现状架构鸟瞰
├── 01-redesign/ 决策(ADR)+ 锁定文档
│ ├── adr-001-data-isolation 数据隔离(行级 + RLS)
│ ├── adr-002-sandbox-isolation 沙箱隔离(K8s + gVisor
│ ├── adr-003-llm-key-billing LLM Key 与计费(混合 BYO
│ ├── adr-004-tenant-rbac 租户 RBACowner/admin/member
│ ├── adr-005-storage-topology 存储拓扑(DB + S3 + emptyDir
│ ├── adr-006-runtime-channel-tenancy 运行时夹层 + IM 渠道
│ ├── adr-007-routing-frontend URL / Cookie / 前端
│ ├── adr-spike-langgraph-postgres spikeLangGraph PG 注入能力
│ ├── adr-vs-code-audit 审计:ADR vs 现状代码
│ ├── multi-tenant-phase-0-plan Phase-0 时间盒 / 产出物
│ └── workspace-schema-design **Stage 0 schema 锁定版**(不可逆决策点)
└── 02-rollout/ 落地路线 + 集成轨道
├── phased-rollout-by-scale **Stage 04 主线** 路线图
├── stage-0-code-map Stage 0 现状代码地图(行号锚点)
└── headless-api-track 业务系统集成轨道(Pattern A / B
```
---
## 1. ADR 与配套文档状态表
| # | 文档 | 状态 | 最近修订 | 核心决策摘要 |
|---|---|---|---|---|
| **ADR-001** | [数据隔离](./01-redesign/adr-001-data-isolation.zh-CN.md) | 草稿 · 据 spike 修订 | 2026-05-09 | 行级 `workspace_id` + Postgres RLS(仅 DeerFlow 自有表);LangGraph 表走应用层强校验 + `UNIQUE(wid, thread_id)` 兜底 |
| **ADR-002** | [沙箱隔离](./01-redesign/adr-002-sandbox-isolation.zh-CN.md) | 草稿 | 2026-05-08 | K8s namespace + gVisor + NetworkPolicy 默认禁出网;premium 切 Kata-Firecracker。**Stage 1 仅做 AioSandbox 加固,K8s 推迟到 Stage 3** |
| **ADR-003** | [LLM Key & 计费](./01-redesign/adr-003-llm-key-billing.zh-CN.md) | 草稿 · 据审计修订 §4.2/§4.3/§4.4.2 | 2026-05-09 | 混合 BYOFree/Pro 用平台 key + quotaEnterprise BYO;悲观预扣防"幽灵 token"Memory/Title/Summarization 内部 LLM 全计入 workspace |
| **ADR-004** | [租户 RBAC](./01-redesign/adr-004-tenant-rbac.zh-CN.md) | 草稿 | 2026-05-08 | 二级 RBACowner/admin/member);JWT 带 role + 30s LRU cache + 敏感操作 `strict=True` 必查 DB`token_version` bump 触发失效 |
| **ADR-005** | [存储拓扑](./01-redesign/adr-005-storage-topology.zh-CN.md) | 草稿 | 2026-05-08 | 三层:Postgres(结构化)+ S3(大对象 + presigned+ emptyDir(临时);沙箱 pod 真正无状态 |
| **ADR-006** | [运行时夹层 + 渠道](./01-redesign/adr-006-runtime-channel-tenancy.zh-CN.md) | 草稿 · 据 spike + 审计修订 | 2026-05-09 | LangGraph 表走应用层校验;MCP cache per-workspaceOAuth token 入 KMS DBIM 渠道加 `channel_bindings(workspace_id)` |
| **ADR-007** | [URL / 前端](./01-redesign/adr-007-routing-frontend.zh-CN.md) | 草稿 · Better Auth 假设作废 | 2026-05-09 | path-based slug `/{slug}/...`;扩现有自签 JWT 加 `wid/role`,不引入 Better Auth;切换 workspace 硬刷新 |
| spike | [LangGraph PG 注入](./01-redesign/adr-spike-langgraph-postgres.zh-CN.md) | 已结论 | 2026-05-09 | `langgraph-checkpoint-postgres==3.0.5` **不存在 connection_factory**;改走应用层强校验 + 自有表 RLS 的两层模型 |
| 审计 | [ADR vs 代码](./01-redesign/adr-vs-code-audit.zh-CN.md) | 已结论 | 2026-05-09 | 代码库 0 处 `tenant`Better Auth 不存在;ObjectStorage / KMS / Postgres 测试夹具全缺;底座先行 §3.5 |
| 锁定 | [workspace-schema-design](./01-redesign/workspace-schema-design.zh-CN.md) | **Stage 0 锁定版** | 2026-05-10 | `workspace_id` 命名 + 7 项不可逆决策;Stage 0 PR1 动手前必读 |
| 计划 | [phase-0-plan](./01-redesign/multi-tenant-phase-0-plan.zh-CN.md) | 计划 | 2026-05-09 | Phase-0 时间盒 3 周;含底座先行(§3.5) |
| 路线 | [phased-rollout-by-scale](./02-rollout/phased-rollout-by-scale.zh-CN.md) | **当前主线路线图** | 2026-05-09 | Stage 04 + 触发/退出/时间盒/Go-No-Go |
| 锚点 | [stage-0-code-map](./02-rollout/stage-0-code-map.zh-CN.md) | Stage 0 用 | 2026-05-09 | 当前代码文件:行号锚点 + Stage 0 改动落点 |
| 集成 | [headless-api-track](./02-rollout/headless-api-track.zh-CN.md) | Stage 1 内并行轨道 | 2026-05-10 | API key + service account + Pattern A/B(不做嵌入式 widget |
---
## 2. Stage 04 速览矩阵
| Stage | 触发 | 退出 | 时间盒 | 主要 ADR 章节 |
|---|---|---|---|---|
| **0** | 现在 / 准备开第一个付费客户 | 外部用户能登入、看到自己 workspace、隔离干净;**生产已跑在 Postgres 上** | 45 周(含 Postgres 切换 + testcontainers| ADR-001 §4.1.2 / ADR-001 §4.4Postgres 切换)/ ADR-004 §1-§3owner-only 简化)/ ADR-007 §1-§5 / workspace-schema-design 全篇 |
| **1** | 首批付费 (1050 / 5002k free) + 1-2 业务系统集成 | 可放心曝光 + 业务系统 go-live | 813 周(双轨并行;Postgres 已在 Stage 0 切完)| ADR-001 §4.1(应用层校验完整)+ ADR-002 §3 轻量 + ADR-003 §4.3-§4.4 + ADR-007 §6-§8 + headless-api 全篇 |
| **2** | 100500 付费 / 5k20k 用户 | 架构能撑用户 ×10 | 1016 周 | ADR-001 §4.2DeerFlow 表 RLS+ ADR-003 §4.1-§4.2KMS+ ADR-004 §5(完整 RBAC+ ADR-005 §1-§5 + ADR-006 §2.2/§2.5/§2.6 |
| **3** | 1k+ 付费 / 50k+ 用户 **或** 安全/成本事故 | 撑到 enterprise 销售前夕 | 1626 周 | ADR-002 §1-§5K8s 完整)+ ADR-003 §4.6BYO+ ADR-006 §2.4prewarm 池)+ ADR-007 §9custom domain 预留) |
| **4** | 单 enterprise 合同(合规 / SSO / 自定义域名) | 长期持续 | 单客户 4–8 周 | ADR-001 §6per-tenant DB+ ADR-002 §5gVisor+ ADR-007 §8 SSO 段 + §9 |
---
## 3. 各阶段详细:业务目标 / 技术路径 / 验证方式
### Stage 0 · workspace 模型立起来 + Postgres 切换 + auth 收紧
**业务目标**
- 把 workspace 概念落到 schema 层、auth 层、入口路由层,给"开第一个付费客户"准备好底座
- **生产 backend 切到 Postgres**——Stage 0 没有生产数据,迁移阻力最小;省 Stage 1 重 ALTER 一遍的返工
- 不追求真隔离(无 RLS、无 K8s、无 KMS),追求**模型立得住**——后续每个 Stage 加东西都不需要重写 Stage 0 的产物
**技术路径**(按 PR 拆分)
1. **Postgres 接入 + testcontainers**docker-compose 加 PG service、`make doctor` / CI 兼容;testcontainers 集成到 backend 测试套;现有 SQLite dev 数据导入(如有)
2. **将默认 backend 切到 Postgres**`make setup` / `make dev` / `.env.example` 默认指向 PG;SQLite 保留为可选 dev 兜底
3. 新建 `workspaces` + `workspace_memberships` 表 + 仓储([workspace-schema-design §2.1-§2.2](./01-redesign/workspace-schema-design.zh-CN.md)
4. 注册 / `/auth/initialize` 改造:每个新用户自动建 1 人 workspaceowner=自己);JWT 扩 `wid` + `role`owner-only 简化)
5. 现有 4 张表 ALTER 加 `workspace_id` 列(直接在 Postgres 上加,先 nullable+ 数据回填脚本("legacy_workspace")→ 改 NOT NULL
6. 入口路由 `(workspace_id, thread_id)` 强校验 — `threads.py` + `thread_runs.py`ADR-001 §4.1.2
7. CI boundary 测试:禁止任何路径绕过入口直连 LangGraph saverADR-006 §2.1
8. Stage 0 末追加:`service_accounts` / `api_keys` / `external_users` schema only(不接路径,为 Stage 1 准备)
**验证方式**
- 单测:`test_workspace_repo.py` / `test_workspace_membership_repo.py`partial unique、CASCADE、slug 黑名单)
- 集成:注册新用户 → DB 中可见 1 个 workspace + 1 条 owner membership + JWT cookie 含 `wid`
- 回归:Stage 0 部署到生产 ≥ 2 周,无 workspace 隔离 bug 报告;Postgres 上稳定运行 ≥ 2 周,无 schema / 性能 regression
- CI boundary`test_langgraph_access_boundary.py` 静态扫描不能命中绕过路径
- testcontainers 夹具:CI 跑通至少 1 个 Postgres 集成测试模板(policy Stage 2 才启用,但夹具 Stage 0 就位)
---
### Stage 1 · 第一批付费客户 + 业务系统集成(双轨并行)
**业务目标**
- **付费 SaaS 轨道**:上线 quota + 计费 + 基础沙箱收紧,让"开放注册"不会被滥用刷爆 LLM 账单
- **Headless API 轨道**:让 1-2 个业务系统能用 API key 调通核心 endpoint go-live,含浏览器直连场景(streaming 友好)
**技术路径**
**轨道 A · 付费 SaaS**Postgres 已在 Stage 0 切完,本轨道直接从 quota 起):
1. `workspace_quotas` / `workspace_usage_daily` 表 + 仓储(ADR-003 §4.3
2. `TokenUsageMiddleware` 升级为持久化 + 4 类 `usage_category`ADR-003 §4.4 + ADR-006 §2.5
3. `QuotaMiddleware` 加入中间件链 + 悲观预扣防幽灵 tokenADR-003 §4.4.1
4. Stripe webhook + 订阅状态同步到 `workspace_quotas.plan`
5. AioSandbox egress 白名单 + cgroup CPU/memory 限额(ADR-002 §3 轻量版,**不上 K8s**
6. 基础监控:per-workspace token 用量、quota 命中率、异常告警
**轨道 B · Headless API Pattern A**(与 A 并行;无前置依赖):
1-10. service_accounts / api_keys / external_users 仓储 → APIKeyAuthBackend 双路径 → CSRF skip on bearer → `/api/v1/` 切换 → external_user_id 透传 → identity_mode 三态 → `@require_permission` 升级 → 基础 rate limit → API key 管理 CLI/UI → idempotency keys(详 [headless-api §6 Stage 1 PR 顺序](./02-rollout/headless-api-track.zh-CN.md#6-与-stage-1-的整合)
**轨道 C · Headless API Pattern B**(依赖轨道 B 的 1-7Stage 1 末 1-2 周):
1-5. `workspaces.allowed_origins` 列 → `WorkspaceAwareCORSMiddleware``POST /api/v1/auth/exchange-token``ServiceTokenAuthBackend` → SSE 跨域 streaming 验证 + 业务方接入示例
**验证方式**
- 单测:quota 中间件硬限/软限/预扣/释放;APIKey 哈希存储;ServiceTokenPayload 签发与验证
- 集成:`test_billing_stream_abort.py`(客户端断流仍记 token);2 个 workspace 互调 thread 必 404
- 端到端:业务系统 demo 应用用 API key 跑通 thread 创建 / SSE / external_user_id 透传
- 业务事实:1-2 个业务系统集成 go-live 并稳定运行 ≥ 1 个月;月活付费 ≥ 50 或免费 ≥ 1k;出现一次"差点超额"事件证明 quota gate 在工作
---
### Stage 2 · 增长期,安全与隔离深化
**业务目标**
- 把 Stage 1 的"应用层兜底"升级为"DB 层兜底"——RLS、KMS、ObjectStorage 三大底座落地
- 团队 workspace 真正可用(invitation + 完整 RBAC
- 内部 LLM 计费透明化、per-user skill 覆盖支持小团队个性化
**技术路径**PR 顺序)
1. testcontainers + Postgres CI 跑通(phase-0 §3.5 底座之一)
2. ObjectStorage Protocol + LocalObjectStorage + 端到端打通(ADR-005 §4-§5
3. KMS 抽象 + AWS/阿里云 KMS 实现 + 已有 secret 灰度迁移(ADR-003 §4.1
4. **DeerFlow 自有表启用 RLS**testcontainers 验证后上生产;ADR-001 §4.2
5. S3ObjectStorage 实现 + 上传/产物迁 S3
6. 多档付费(Free/Pro/Team+ invitation 流程 + role 扩到 owner/admin/member
7. `WorkspaceMCPCache` + OAuth token 落 KMS 加密 DBADR-006 §2.2
8. per-user skill 覆盖(`user_skill_overrides`+ per-user skill configKMS 加密;ADR-005 §5.4 扩展)
9. 内部 LLM 计费分类(Memory/Title/Summarization usage_categoryADR-006 §2.5
10. Webhook outboundStage 1 推迟过来;headless-api §1+ 分维度 rate limit(引入 Redis
11. 基础 audit log(写业务 DBStage 3 才拆)
**验证方式**
- RLS 冒烟:testcontainers 起 Postgres,跨 workspace SELECT 必返空;跨 workspace UPDATE 必拒绝
- KMSsecret 写入后 DB 列只见密文;rotate 流程不破坏旧密文解密
- 计费分项:UI 报表能区分 main / memory / title / summarizationBYO key 走自己额度
- 业务事实:跨 workspace 数据访问尝试 0 次(哪怕日志里);客户开始问 BYO key
---
### Stage 3 · 成熟期,K8s 隔离 + BYO
**业务目标**
- 沙箱从"AioSandbox 加固"升级到"K8s + namespace + NetworkPolicy",杜绝单进程资源争用
- BYO LLM key 作为付费档福利交付
- 拆分 audit DB、加跨 region 备份,为 enterprise 销售铺路
**技术路径**
1. K8s 集群部署 + per-workspace namespace + ResourceQuota / LimitRange + NetworkPolicy 默认禁出网(ADR-002 §5
2. `K8sSandboxProvider` 全新建 + Cosign 镜像签名 + Pod Security Standard restrictedADR-002 §5.1-§5.5
3. per-workspace prewarm 池 controller,按 plan 大小(ADR-006 §2.4
4. BYO LLM key 路径:`workspace_secrets` 解密 → `create_chat_model()` 优先用 tenant keyADR-003 §4.2 + §4.6
5. presigned URL 全量启用 + S3 lifecycle(按 plan 设保留期)
6. audit DB 拆分(独立连接池或独立实例;ADR-006 §2.4 脚注)
7. 跨 region 备份 / DRRTO ≤ 4h
8. 完整监控栈:Grafana + Prometheus + APM + per-workspace SLA
**验证方式**
- 安全:渗透测试容器逃逸场景(gVisor 启用前后对比);NetworkPolicy 阻断 169.254.169.254 / 内网 IP
- 性能:Pod 冷启动 P50 < 2s / P99 < 5sprewarm 命中率 > 80%
- 业务:BYO 客户 ≥ 5 个;K8s 切换零数据丢失;audit DB 写入与业务 DB 解耦验证
---
### Stage 4 · 企业化,按需开启
**业务目标**
- 单客户合同驱动,不为"万一"提前投资
- SSO / 自定义域名 / per-tenant DB / gVisor / 合规审计——按客户付费决定做哪几样
**技术路径**(按合同选做)
- **SSOSAML/OIDC**:复用现有 oauth_provider 字段;新建 `workspace_sso_configs`IdP 用户/组映射到 workspace_membershipsADR-007 §8 SSO 段)
- **自定义域名**`workspaces.custom_domain` 列 + ACME 动态签证 + nginx vhost 路由(ADR-007 §9
- **物理数据隔离**:仅该客户切 per-tenant DBADR-001 §6 推翻条件)
- **私有部署**:打 enterprise tier docker 镜像 + 部署文档;放弃中心化运维优势
- **gVisor / Kata 切换**K8s 切对应 RuntimeClassADR-002 §5
- **合规审计报告(SOC2/ISO**:强化 audit log 留存 + 评估机构对接
**验证方式**
- 合同里写明的 SLA / 合规条款逐条验收
- 安全审计 / 渗透测试报告(如客户要求)
- SSO IdP 端到端登录测试
---
## 4. Stage ↔ ADR 章节细粒度对照
| Stage | ADR-001 | ADR-002 | ADR-003 | ADR-004 | ADR-005 | ADR-006 | ADR-007 | headless-api |
|---|---|---|---|---|---|---|---|---|
| 0 | §4.1.2 入口校验 + §4.4 Postgres 切换 | — | — | §1-§3 owner-only | — | — | §1-§5(无 slug 路由可暂缓) | §2 schema only |
| 1 | §4.1 应用层校验完整版(**不含 RLS**Postgres 已 Stage 0 切完)| §3 轻量(egress + cgroup | §4.3-§4.4 quota + 持久化 + 预扣 | — | — | §2.5 usage_category | §6-§8 slug + JWT 扩字段 | §2-§5 全(Pattern A+ §3.5 全(Pattern B|
| 2 | §4.2 DeerFlow 表 RLS | — | §4.1-§4.2 KMS + create_chat_model 改造 | §5 完整 RBAC + invitation | §1-§5 ObjectStorage 完整 | §2.2 / §2.5 / §2.6 | — | §1 Webhook outbound |
| 3 | — | §1-§5 K8s 完整 | §4.6 BYO | — | §5 第 2 阶段(presigned + lifecycle| §2.4 prewarm 池 | §9 custom domain 预留 | — |
| 4 | §6 per-tenant DB | §5 gVisor / Kata | — | §5.7 SSO/SCIM | — | — | §8 SSO 段 + §9 落地 | — |
---
## 5. 不可逆决策一览(按 Stage 集中)
| Stage | 决策 | 文档锚点 | 反悔代价 |
|---|---|---|---|
| 0 | **Postgres 切换**dev + 生产)| [phased-rollout Stage 0](./02-rollout/phased-rollout-by-scale.zh-CN.md#stage-0--workspace-模型立起来--postgres-切换--auth-收紧) + [phase-0-plan §3.5](./01-redesign/multi-tenant-phase-0-plan.zh-CN.md#35-底座先行phase-0-之前--并行的基础设施) | Stage 0 选这个时机:没有生产数据,迁移阻力最小;切完不回头 |
| 0 | `workspace_id` 列加到所有业务表(直接 Postgres) | [workspace-schema-design §3](./01-redesign/workspace-schema-design.zh-CN.md#3-alter-现有表) | 漏一张 → Stage 1 补;不再走 SQLite → Postgres 二次迁移 |
| 0 | `workspaces` 表字段(id 类型 / slug 字符集 / owner_id 冗余) | [workspace-schema-design §5](./01-redesign/workspace-schema-design.zh-CN.md#5-不可逆决策清单) | 7 项已锁定;任何一项改主意 → revert PR1 重写 |
| 0 | JWT TokenPayload 字段集 `{sub, wid, role, exp, iat, ver}` | [workspace-schema-design §4](./01-redesign/workspace-schema-design.zh-CN.md#4-jwt-tokenpayload--一次到位的字段集) | 加新字段 → bump `token_version` 全用户重登;Stage 0 一次性加齐省一次 churn |
| 0 | 文件系统路径形态 `workspaces/{wid}/threads/{tid}/...` | [stage-0-code-map §4](./02-rollout/stage-0-code-map.zh-CN.md#4-threaddatamiddleware--路径系统) | 改了所有用户产物 URL 失效 |
| 0 | API Key 格式(`dfk_live_*` / `dfk_test_*`+ `service_accounts` 不跨 workspace | [headless-api §8](./02-rollout/headless-api-track.zh-CN.md#8-不可逆决策动手前想清楚) | 业务系统接入后改格式所有 key 失效 |
| 1 | `/api/v1/` mount prefix + deprecation 时间 | [headless-api §4](./02-rollout/headless-api-track.zh-CN.md#4-核心设计api-版本化) | 业务系统接了之后改 prefix 全部联调 |
| 1 | identity_mode 三态语义(collapsed / external / both | [headless-api §3](./02-rollout/headless-api-track.zh-CN.md#3-核心设计两种身份模式) | 改语义所有业务系统集成重测 |
| 1 | memory 隔离粒度(SA 共享 vs per external_user | 同上 | 客户用上后迁移 memory 数据极麻烦 |
| 2 | ObjectStorage prefix 形态 `workspaces/{wid}/...` | [ADR-005 §2.1](./01-redesign/adr-005-storage-topology.zh-CN.md#21-各类数据的归属) | 改了所有用户产物 URL 失效 |
| 3 | K8s namespace 命名规则(`ws-{wid}` vs `tenant-{wid}` | [ADR-002 §5.2](./01-redesign/adr-002-sandbox-isolation.zh-CN.md#52-k8s-资源每租户-namespace-一份)(落代码读 `ws-{wid}` | 改了所有 NetworkPolicy / RBAC / 监控 dashboard |
---
## 6. 用语映射速查
> ADR 与 02-rollout 系列因写作时序不同,存在两组用语并行。这里给一张一次性映射表,避免读不同文档时反复对照。
| ADR 用语 | 落代码 / 02-rollout 用语 | 备注 |
|---|---|---|
| `tenant_id`(数据库列、Python 变量) | `workspace_id` | workspace-schema-design §1 锁定 |
| `tenants` 表 | `workspaces` 表 | 同上 |
| `tenant_memberships` | `workspace_memberships` | 同上 |
| `tenant_secrets` | `workspace_secrets` | 同上 |
| `tenant_quotas` / `tenant_usage_daily` | `workspace_quotas` / `workspace_usage_daily` | 同上 |
| `tenant_skill_state` / `tenant_mcp_configs` | `workspace_skill_state` / `workspace_mcp_configs` | 同上 |
| JWT `tid` claim | JWT `wid` claim | workspace-schema-design §4 锁定 |
| K8s `tenant-{tenant_id}` namespace | `ws-{workspace_id}` | ADR-002 §5.2 → 落代码读 |
| ObjectStorage `tenants/{tid}/...` prefix | `workspaces/{wid}/...` | ADR-005 §2.1 → 落代码读 |
| `TenantMCPCache` 类名 | `WorkspaceMCPCache` | ADR-006 §2.2 → 落代码读 |
| ADR 用语 "v1 / v2 路线图" | rollout Stage 04 | "v1" ≈ Stage 0+1"v2" ≈ Stage 2+;具体见每条决策的 Stage 标注 |
| ADR-005 §5 "第 1 阶段 / 第 2 阶段" | rollout Stage 2 / Stage 3 | ADR-005 §5 已加映射注 |
| ADR-006 §3 "约 14 人周" | 对应 rollout Stage 2 必做项里的 §2.2/§2.5/§2.6 | — |
---
## 7. 阅读路径建议
**第一次进项目(30 min**
1. 本 README
2. [00-current-state/architecture-overview](./00-current-state/architecture-overview.zh-CN.md) — 现状是什么样的
3. [phased-rollout-by-scale](./02-rollout/phased-rollout-by-scale.zh-CN.md) §0 + §总览 + §Stage 0 — 现在在哪、下一步做什么
**准备动手做 Stage 0(半天)**:
1. [workspace-schema-design](./01-redesign/workspace-schema-design.zh-CN.md) **全文** — 不可逆决策、PR 拆分
2. [stage-0-code-map](./02-rollout/stage-0-code-map.zh-CN.md) **全文** — 当前代码锚点
3. [ADR-001 §4.1.2](./01-redesign/adr-001-data-isolation.zh-CN.md) + [ADR-007 §8](./01-redesign/adr-007-routing-frontend.zh-CN.md)
4. [ADR-006 §2.1](./01-redesign/adr-006-runtime-channel-tenancy.zh-CN.md) + [adr-spike-langgraph-postgres](./01-redesign/adr-spike-langgraph-postgres.zh-CN.md) — 为什么 LangGraph 表不挂 RLS
**准备动手做 Stage 1(一天)**:
1. [phased-rollout Stage 1](./02-rollout/phased-rollout-by-scale.zh-CN.md) — 双轨并行
2. [headless-api-track](./02-rollout/headless-api-track.zh-CN.md) **全文** — Pattern A/B 完整设计
3. [ADR-003 §4.3-§4.4](./01-redesign/adr-003-llm-key-billing.zh-CN.md) — quota + 悲观预扣
4. [ADR-002 §3](./01-redesign/adr-002-sandbox-isolation.zh-CN.md) — Stage 1 用 §3 轻量版(**不**读 §5 K8s 完整版)
**做安全/合规评审**
1. ADR-001 / ADR-002 / ADR-003 §4.6BYO/ ADR-004 §5.4strict 装饰器)
2. [adr-vs-code-audit](./01-redesign/adr-vs-code-audit.zh-CN.md) cross-cutting risks 全部
3. headless-api §3.5 + §8(短期 JWT TTL / Token revoke 策略)
**做架构评审**
1. ADR-001 全 + ADR-005 全 + ADR-006 全
2. spike + audit
3. workspace-schema-design §5 不可逆清单
---
## 8. 常见问题(FAQ
**QPostgres 切换在哪个 Stage**
A**Stage 0**。原稿放 Stage 1,但 Stage 0 已经要 ALTER 4 张表加 `workspace_id`,先 SQLite 加列再 Stage 1 重 ALTER 是纯返工;Stage 0 没有生产数据,迁移阻力最小,且 phase-0 §3.5 早已把 Postgres testcontainers 列为底座。详见 [phased-rollout Stage 0](./02-rollout/phased-rollout-by-scale.zh-CN.md#stage-0--workspace-模型立起来--postgres-切换--auth-收紧) + [phase-0-plan §3.5](./01-redesign/multi-tenant-phase-0-plan.zh-CN.md#35-底座先行phase-0-之前--并行的基础设施)。SQLite 仅保留为可选 dev 兜底。
**QADR 写 `tenant_id`,代码写 `workspace_id`,到底哪个是对的?**
A:代码以 `workspace_id` 为准([workspace-schema-design §1](./01-redesign/workspace-schema-design.zh-CN.md) 锁定)。ADR 不重命名是为了节省成本,每份 ADR 顶部已加交叉提示。
**QADR-002 写要上 K8s + gVisor,是不是 Stage 1 就要做?**
A:不是。Stage 1 只做 ADR-002 §3 的"AioSandbox 出网白名单 + cgroup 限额"。K8s + gVisor 的完整方案推迟到 Stage 3。ADR-002 §1 已加分期落地提示。
**QLangGraph 表为什么不挂 RLS**
Aspike 验证 `langgraph-checkpoint-postgres==3.0.5` 不存在 `connection_factory`,无法注入 `SET LOCAL app.tenant_id`。改走应用层强校验(`threads.py` + `thread_runs.py` 入口)+ `threads_meta` `UNIQUE(workspace_id, thread_id)` 兜底。详见 [spike 报告](./01-redesign/adr-spike-langgraph-postgres.zh-CN.md) §3.3 / [ADR-001 §4.1.1](./01-redesign/adr-001-data-isolation.zh-CN.md)。
**Q:是否引入 Better Auth**
A:不引入。审计发现前端实际不用 Better Auth;扩现有 `app/gateway/auth/jwt.py``wid` + `role` 字段比引入框架的破坏面小得多。详见 [ADR-007 §8](./01-redesign/adr-007-routing-frontend.zh-CN.md)。
**Q:业务系统集成走哪种 pattern?**
A:默认 Pattern A(业务 backend 代理,长期 API key);自研 web 页面 + streaming 延迟敏感的走 Pattern B(短期 JWT,浏览器直连)。**不做 Pattern C 嵌入式 widget**。详见 [headless-api §0 速览](./02-rollout/headless-api-track.zh-CN.md#集成-pattern-速览)。
**Qon-prem 怎么定位?**
ASaaS 是产品主线;Stage 0 的 schema 设计同时兼容 on-prem`workspaces.id` 1:1 对应 self-host 安装),按 enterprise 客户合同启用,不作为并行产品线投入。详见 [phased-rollout §0](./02-rollout/phased-rollout-by-scale.zh-CN.md) + [headless-api §7](./02-rollout/headless-api-track.zh-CN.md#7-saas-vs-on-prem-差异saas-是主线)。
---
## 9. 推翻条件(什么会让整个分期方案重排)
来自 [phased-rollout §推翻条件](./02-rollout/phased-rollout-by-scale.zh-CN.md#推翻条件)
- **目标客户画像突变**:拿到 enterprise 合同要求 SSO + 自定义域名 → Stage 4 部分提前到 Stage 2
- **出现安全事故**:跨 workspace 泄露 / sandbox 逃逸 → 跳过未启动 Stage,直接做 Stage 3 的 K8s + RLS
- **付费转化远不及预期**Stage 1 上线 6 个月付费 < 10 → 重评 freemium 模型,可能不需要走完 Stage 2/3
- **LLM 价格大跌 / 自部署模型成熟**:成本控制优先级下降 → quota 与 BYO 可简化
> 单条 ADR 的"推翻条件"在每份 ADR 末尾,触发时只重排该 ADR 涉及的 Stage。