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:
@@ -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/... # 业务 API(tenant 由 JWT 决定)
|
||||
/api/langgraph/threads/{tid}/runs/stream # LangGraph 兼容
|
||||
/api/v1/... # 业务 API(tenant 由 JWT / API key 决定;Stage 1 起强制带版本)
|
||||
/api/... # 旧路径,Stage 1 起转发到 /api/v1,Stage 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 fixture;SQLite 不支持 RLS | ADR-001 / 004 / 005 的所有租户隔离测试都要 Postgres | testcontainers 集成 + 至少 1 个 RLS 冒烟测试模板 + CI 跑通 |
|
||||
| **Postgres 切换 + testcontainers 夹具**(生产 + 测试基础设施一并落) | 仓库当前以 SQLite 为默认后端,`tests/` 下无 Postgres fixture;SQLite 不支持 RLS | ADR-001 / 004 / 005 的所有租户隔离测试都要 Postgres;Stage 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 hook(spike 已验证),应用层兜底 |
|
||||
| **数据库** | Stage 0 起直接切 Postgres 为生产默认;SQLite 仅保留为可选 dev 兜底 | Stage 0 没有生产数据,迁移阻力最小;省 Stage 1 重 ALTER 一遍的返工 |
|
||||
| 数据隔离 | 行级 + Postgres RLS(仅 DeerFlow 自有表,policy Stage 2 启用)+ LangGraph 表应用层强校验 | 改造成本低;LangGraph 表无 RLS hook(spike 已验证),应用层兜底 |
|
||||
| 沙箱隔离 | K8s namespace + gVisor + NetworkPolicy 默认禁出网 | 强度足够 + 运维可控 |
|
||||
| LLM Key | 混合:默认平台 key + 限额,premium 切 BYO;悲观预扣防超额 | 体验与成本兼顾 |
|
||||
| 租户层级 | 二级 RBAC(owner/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-PR2(Postgres 接入 + 默认切换),再起 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 4(enterprise) | 长尾需求,列加在哪一层都行 |
|
||||
| `billing_email` | Stage 1(Stripe 对接) | 一并加 |
|
||||
| `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 hex(64 字符)+ 余量 |
|
||||
| `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 id(DeerFlow 内部) |
|
||||
| `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="登录后默认进入的 workspace;NULL 时强制走 picker(user 多 workspace 场景)"
|
||||
)
|
||||
```
|
||||
|
||||
**为什么不加 `current_workspace_id`**:每次登录时从 `default` 或 `/select-workspace` 决定,写入 JWT 的 `wid` claim;DB 不存"当前激活"状态,避免多设备冲突。
|
||||
|
||||
`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_id(Stage 0 新增)
|
||||
role: str # owner / admin / member(Stage 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 个 owner(partial 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 1(Postgres 已就绪) | 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 1(M+M++M+M=4M)+ 新增 Pattern A(M+M+XS+M+S+S+S+S=4M)+ 新增 Pattern B(S+S+M=2M)= 约 10M-15 周。Pattern B 依赖 Pattern A 完成,建议放 Stage 1 末。
|
||||
合计原 Stage 1 不含 Postgres(M+M+M=3M)+ 新增 Pattern A(M+M+XS+M+S+S+S+S=4M)+ 新增 Pattern B(S+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 收紧;不做真隔离 | 3–4 周 |
|
||||
| **1** | 第一批付费客户(10–50 付费 / 500–2000 free) + **1-2 业务系统集成(含自研 web 页面)** | **Postgres + Quota + Headless API(Pattern A backend 代理 + Pattern B browser 直连)必落**;workspace 全链路 + 入口强校验;AioSandbox 收紧 | 10–15 周 |
|
||||
| **0** | 现在 → 第一个付费客户准备 | workspace 模型立起来;**Postgres 切换**;现有 auth 收紧;不做真隔离 | 4–5 周 |
|
||||
| **1** | 第一批付费客户(10–50 付费 / 500–2000 free) + **1-2 业务系统集成(含自研 web 页面)** | **Quota + Headless API(Pattern A backend 代理 + Pattern B browser 直连)必落**;workspace 全链路 + 入口强校验;AioSandbox 收紧 | 8–13 周 |
|
||||
| **2** | 增长期(100–500 付费 / 5k–20k 用户) | DeerFlow 表 RLS、KMS、ObjectStorage S3、内部 LLM 计费分类、付费分层、Webhook outbound | 10–16 周 |
|
||||
| **3** | 成熟期(1k+ 付费 / 50k+ 用户)**或** 出现安全/成本事故 | K8s sandbox + namespace、BYO key(付费档福利)、audit DB 拆分、prewarm 池 | 16–26 周 |
|
||||
| **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、隔离干净。
|
||||
**时间盒**:3–4 周
|
||||
**退出**:能给一个外部用户开账号,他登进来看到自己的 workspace、能创建 thread、隔离干净;生产已跑在 Postgres 上。
|
||||
**时间盒**:4–5 周(原 3–4 周;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 only(Stage 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 付费 / 500–2000 free)+ 1-2 个业务系统集成需求。
|
||||
**退出**:① 能放心让媒体/产品社区曝光,不会被白嫖跑偏;② 业务系统能用 API key 调通核心 endpoint,go-live。
|
||||
**时间盒**:10–15 周(原稿 6–10 周;headless API Pattern A 加 2-3 周 + Pattern B 加 2 周)
|
||||
**时间盒**:8–13 周(原 10-15 周;Postgres 切换已在 Stage 0 完成,省 2 周)
|
||||
|
||||
> Stage 1 是**双轨并行**:付费 SaaS(cookie auth + Stripe + quota)和 Headless API(bearer auth + service account + `/api/v1/`)。两者共用 workspace + auth + quota 基座。详细 headless API 设计见 [headless-api-track.zh-CN.md](./headless-api-track.zh-CN.md)。
|
||||
> Stage 1 是**双轨并行**:付费 SaaS(cookie auth + Stripe + quota)和 Headless API(bearer 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. 监控/告警接入
|
||||
|
||||
**轨道 B:Headless API Pattern A**(与 A 并行;步骤 1-2 必须先完成轨道 A 的 1)
|
||||
**轨道 B:Headless 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 | 现在 | 3–4 周 | 1 个月 |
|
||||
| 1 | 首批付费 + 业务系统集成(含自研 web 直连) | 10–15 周(headless API Pattern A+B 并行 4-5 周) | 4-5 个月 |
|
||||
| 2 | 增长期 | 10–16 周 | 8-9 个月 |
|
||||
| 3 | 成熟期 | 16–26 周 | 14-16 个月 |
|
||||
| 0 | 现在 | 4–5 周(含 Postgres 切换 + testcontainers) | 1.0–1.3 个月 |
|
||||
| 1 | 首批付费 + 业务系统集成(含自研 web 直连) | 8–13 周(headless API Pattern A+B 并行 4-5 周;Postgres 切换已前移到 Stage 0) | 3–4 个月 |
|
||||
| 2 | 增长期 | 10–16 周 | 7–8 个月 |
|
||||
| 3 | 成熟期 | 16–26 周 | 13–14 个月 |
|
||||
| 4 | 企业客户 | 单客户 4–8 周 | + 按需 |
|
||||
|
||||
**全功能落地**:~14-16 个月(Stage 0–3 累计),不含 Stage 4 enterprise 特性。
|
||||
**最小可付费 + 业务系统集成**(Stage 0 + 1):~4-5 个月。
|
||||
**风险可控的增长**(Stage 0 + 1 + 2):~8-9 个月。
|
||||
**全功能落地**:~13-14 个月(Stage 0–3 累计),不含 Stage 4 enterprise 特性。
|
||||
**最小可付费 + 业务系统集成**(Stage 0 + 1):~3-4 个月。
|
||||
**风险可控的增长**(Stage 0 + 1 + 2):~7-8 个月。
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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 租户 RBAC(owner/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 spike:LangGraph 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 0–4 主线** 路线图
|
||||
├── 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 | 混合 BYO:Free/Pro 用平台 key + quota;Enterprise BYO;悲观预扣防"幽灵 token";Memory/Title/Summarization 内部 LLM 全计入 workspace |
|
||||
| **ADR-004** | [租户 RBAC](./01-redesign/adr-004-tenant-rbac.zh-CN.md) | 草稿 | 2026-05-08 | 二级 RBAC(owner/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-workspace;OAuth token 入 KMS DB;IM 渠道加 `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 0–4 + 触发/退出/时间盒/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 0–4 速览矩阵
|
||||
|
||||
| Stage | 触发 | 退出 | 时间盒 | 主要 ADR 章节 |
|
||||
|---|---|---|---|---|
|
||||
| **0** | 现在 / 准备开第一个付费客户 | 外部用户能登入、看到自己 workspace、隔离干净;**生产已跑在 Postgres 上** | 4–5 周(含 Postgres 切换 + testcontainers)| ADR-001 §4.1.2 / ADR-001 §4.4(Postgres 切换)/ ADR-004 §1-§3(owner-only 简化)/ ADR-007 §1-§5 / workspace-schema-design 全篇 |
|
||||
| **1** | 首批付费 (10–50 / 500–2k free) + 1-2 业务系统集成 | 可放心曝光 + 业务系统 go-live | 8–13 周(双轨并行;Postgres 已在 Stage 0 切完)| ADR-001 §4.1(应用层校验完整)+ ADR-002 §3 轻量 + ADR-003 §4.3-§4.4 + ADR-007 §6-§8 + headless-api 全篇 |
|
||||
| **2** | 100–500 付费 / 5k–20k 用户 | 架构能撑用户 ×10 | 10–16 周 | ADR-001 §4.2(DeerFlow 表 RLS)+ ADR-003 §4.1-§4.2(KMS)+ ADR-004 §5(完整 RBAC)+ ADR-005 §1-§5 + ADR-006 §2.2/§2.5/§2.6 |
|
||||
| **3** | 1k+ 付费 / 50k+ 用户 **或** 安全/成本事故 | 撑到 enterprise 销售前夕 | 16–26 周 | ADR-002 §1-§5(K8s 完整)+ ADR-003 §4.6(BYO)+ ADR-006 §2.4(prewarm 池)+ ADR-007 §9(custom domain 预留) |
|
||||
| **4** | 单 enterprise 合同(合规 / SSO / 自定义域名) | 长期持续 | 单客户 4–8 周 | ADR-001 §6(per-tenant DB)+ ADR-002 §5(gVisor)+ 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 人 workspace(owner=自己);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 saver(ADR-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` 加入中间件链 + 悲观预扣防幽灵 token(ADR-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-7;Stage 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 加密 DB(ADR-006 §2.2)
|
||||
8. per-user skill 覆盖(`user_skill_overrides`)+ per-user skill config(KMS 加密;ADR-005 §5.4 扩展)
|
||||
9. 内部 LLM 计费分类(Memory/Title/Summarization usage_category;ADR-006 §2.5)
|
||||
10. Webhook outbound(Stage 1 推迟过来;headless-api §1)+ 分维度 rate limit(引入 Redis)
|
||||
11. 基础 audit log(写业务 DB,Stage 3 才拆)
|
||||
|
||||
**验证方式**
|
||||
- RLS 冒烟:testcontainers 起 Postgres,跨 workspace SELECT 必返空;跨 workspace UPDATE 必拒绝
|
||||
- KMS:secret 写入后 DB 列只见密文;rotate 流程不破坏旧密文解密
|
||||
- 计费分项:UI 报表能区分 main / memory / title / summarization;BYO 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 restricted(ADR-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 key(ADR-003 §4.2 + §4.6)
|
||||
5. presigned URL 全量启用 + S3 lifecycle(按 plan 设保留期)
|
||||
6. audit DB 拆分(独立连接池或独立实例;ADR-006 §2.4 脚注)
|
||||
7. 跨 region 备份 / DR(RTO ≤ 4h)
|
||||
8. 完整监控栈:Grafana + Prometheus + APM + per-workspace SLA
|
||||
|
||||
**验证方式**
|
||||
- 安全:渗透测试容器逃逸场景(gVisor 启用前后对比);NetworkPolicy 阻断 169.254.169.254 / 内网 IP
|
||||
- 性能:Pod 冷启动 P50 < 2s / P99 < 5s;prewarm 命中率 > 80%
|
||||
- 业务:BYO 客户 ≥ 5 个;K8s 切换零数据丢失;audit DB 写入与业务 DB 解耦验证
|
||||
|
||||
---
|
||||
|
||||
### Stage 4 · 企业化,按需开启
|
||||
|
||||
**业务目标**
|
||||
- 单客户合同驱动,不为"万一"提前投资
|
||||
- SSO / 自定义域名 / per-tenant DB / gVisor / 合规审计——按客户付费决定做哪几样
|
||||
|
||||
**技术路径**(按合同选做)
|
||||
- **SSO(SAML/OIDC)**:复用现有 oauth_provider 字段;新建 `workspace_sso_configs`;IdP 用户/组映射到 workspace_memberships(ADR-007 §8 SSO 段)
|
||||
- **自定义域名**:`workspaces.custom_domain` 列 + ACME 动态签证 + nginx vhost 路由(ADR-007 §9)
|
||||
- **物理数据隔离**:仅该客户切 per-tenant DB(ADR-001 §6 推翻条件)
|
||||
- **私有部署**:打 enterprise tier docker 镜像 + 部署文档;放弃中心化运维优势
|
||||
- **gVisor / Kata 切换**:K8s 切对应 RuntimeClass(ADR-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 0–4 | "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.6(BYO)/ ADR-004 §5.4(strict 装饰器)
|
||||
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)
|
||||
|
||||
**Q:Postgres 切换在哪个 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 兜底。
|
||||
|
||||
**Q:ADR 写 `tenant_id`,代码写 `workspace_id`,到底哪个是对的?**
|
||||
A:代码以 `workspace_id` 为准([workspace-schema-design §1](./01-redesign/workspace-schema-design.zh-CN.md) 锁定)。ADR 不重命名是为了节省成本,每份 ADR 顶部已加交叉提示。
|
||||
|
||||
**Q:ADR-002 写要上 K8s + gVisor,是不是 Stage 1 就要做?**
|
||||
A:不是。Stage 1 只做 ADR-002 §3 的"AioSandbox 出网白名单 + cgroup 限额"。K8s + gVisor 的完整方案推迟到 Stage 3。ADR-002 §1 已加分期落地提示。
|
||||
|
||||
**Q:LangGraph 表为什么不挂 RLS?**
|
||||
A:spike 验证 `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-速览)。
|
||||
|
||||
**Q:on-prem 怎么定位?**
|
||||
A:SaaS 是产品主线;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。
|
||||
Reference in New Issue
Block a user