89fe54cc07
* 新增 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>
284 lines
17 KiB
Markdown
284 lines
17 KiB
Markdown
# 多租户改造 · 第 0 阶段计划
|
||
|
||
> 目标:在写第一行代码前,对齐设计、产出可评审的文档。这一阶段不动代码,时间盒 **两周封顶**。
|
||
|
||
---
|
||
|
||
## 产出物清单(8 份文档 + 1 份代码盘点)
|
||
|
||
| 产出物 | 形式 | 谁批 | 作用 |
|
||
|---|---|---|---|
|
||
| [ADR-001 数据隔离模型](./adr-001-data-isolation.zh-CN.md) | ADR(决策记录) | CTO / 架构 | 锁定行级 / schema / 库级;含 LangGraph checkpoint 表的注入路径 |
|
||
| [ADR-002 沙箱隔离模型](./adr-002-sandbox-isolation.zh-CN.md) | ADR + 威胁模型 | 安全 + 架构 | 锁定 K8s / Firecracker / VM |
|
||
| [ADR-003 LLM Key 与计费模型](./adr-003-llm-key-billing.zh-CN.md) | ADR | 产品 + CTO | 锁定 BYO / 平台付费 / 混合;含悲观预扣 + 内部 LLM 计费 |
|
||
| [ADR-004 租户 ↔ 用户层级](./adr-004-tenant-rbac.zh-CN.md) | ADR | 产品 | 锁定二级 RBAC + JWT/cache 一致性策略 |
|
||
| [ADR-005 存储拓扑](./adr-005-storage-topology.zh-CN.md) | ADR + ObjectStorage 接口签名 | 架构 + SRE | 锁定 DB / 对象存储 / 临时区分层 |
|
||
| [ADR-006 运行时与渠道租户化](./adr-006-runtime-channel-tenancy.zh-CN.md) | ADR | 后端 lead + 渠道 owner | 锁定 checkpointer / MCP cache / skills loader / 内部 LLM 计费 / IM 渠道 ↔ 租户 |
|
||
| [ADR-007 路由与前端租户化](./adr-007-routing-frontend.zh-CN.md) | ADR | 前端 lead + 后端 lead | 锁定 URL 形态 / cookie / 自签 JWT 扩字段 / SDK 切换 |
|
||
| [adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) | 审计报告 | 架构 | 7 份 ADR 对照现状代码的差异清单(已据其修订 ADR-001/006/007) |
|
||
| [adr-spike-langgraph-postgres](./adr-spike-langgraph-postgres.zh-CN.md) | spike 报告 | 架构 + 后端 lead | 验证 `langgraph-checkpoint-postgres==3.0.5` 的注入能力,结论改写 ADR-001 §4.1 / ADR-006 §2.1 |
|
||
| Tenant 数据模型设计 | DB schema 草稿 + ER 图 | 后端 lead | 第 1 阶段直接落地用 |
|
||
| 多租户改造代码盘点 | 表格 / spreadsheet | 后端 lead | 估工 + 拆 PR 用 |
|
||
|
||
每份 ADR 用统一结构:**目标客户画像 → 评估维度 → 选项对比 → 选 X 的理由 → 推翻条件**。
|
||
|
||
---
|
||
|
||
## 1. 四个决策怎么定(决策框架)
|
||
|
||
> 下面只是决策框架的 1 页概览。每份 ADR 的完整正文(背景 / 备选方案 / 落地影响 / 风险 / 推翻条件 / 默认假设)已分别成独立文档:
|
||
> - [ADR-001 数据隔离模型](./adr-001-data-isolation.zh-CN.md)
|
||
> - [ADR-002 沙箱隔离模型](./adr-002-sandbox-isolation.zh-CN.md)
|
||
> - [ADR-003 LLM Key 与计费模型](./adr-003-llm-key-billing.zh-CN.md)
|
||
> - [ADR-004 租户 ↔ 用户层级](./adr-004-tenant-rbac.zh-CN.md)
|
||
> - [ADR-005 存储拓扑与持久化策略](./adr-005-storage-topology.zh-CN.md)
|
||
> - [ADR-006 运行时与渠道租户化](./adr-006-runtime-channel-tenancy.zh-CN.md)
|
||
> - [ADR-007 路由与前端租户化](./adr-007-routing-frontend.zh-CN.md)
|
||
|
||
### ADR-001 数据隔离
|
||
|
||
| 维度 | 行级 (tenant_id WHERE) | per-tenant schema | per-tenant DB |
|
||
|---|---|---|---|
|
||
| 实现成本 | 低 | 中 | 高 |
|
||
| 跨租户 bug 爆炸半径 | 高 | 中 | 极低 |
|
||
| 备份/恢复粒度 | 全量 | 按 schema | 按 DB |
|
||
| 合规友好度(SOC2/HIPAA) | 一般 | 好 | 最好 |
|
||
| 跨租户分析查询 | 容易 | 中 | 难 |
|
||
| 适用客户规模 | <10k 租户 | 10k–100 大客户 | <100 大客户 |
|
||
|
||
**90% 团队选 行级 + Postgres RLS(双保险)**。理由:DeerFlow 现在的仓储层已经是 `user_id` 过滤模式,RLS 加上去几乎是平行扩展。
|
||
|
||
**推翻条件**:拿到金融/医疗类客户、客户合同里写明"物理数据隔离"——直接跳到 per-tenant DB。
|
||
|
||
### ADR-002 沙箱隔离
|
||
|
||
威胁模型表(每行一个攻击场景):
|
||
|
||
| 攻击 | 共享 Docker | per-tenant Namespace | per-tenant VM |
|
||
|---|---|---|---|
|
||
| 容器逃逸 | 全员沦陷 | 单租户沦陷 | 单租户沦陷 |
|
||
| 侧信道(CPU 缓存等) | 可行 | 可行 | 难 |
|
||
| 出网到云 metadata | 可行(必须默认禁) | 可行(必须默认禁) | 可行(必须默认禁) |
|
||
| 资源耗尽(fork bomb) | 影响他人 | 仅影响自己 | 仅影响自己 |
|
||
| 提权 | 看 K8s 配置 | 看 K8s 配置 | VM 边界更强 |
|
||
|
||
**推荐:K8s namespace + gVisor/Kata runtime + NetworkPolicy 默认禁出网**。Firecracker 是更强的方案但运维成本翻倍,留给"premium 租户专属"档位。
|
||
|
||
### ADR-003 Key & 计费
|
||
|
||
三种模式选一种主线:
|
||
|
||
- **BYO key**:客户自己带 OpenAI/Anthropic key。优点:你不背模型成本、不背滥用;缺点:客户体验差,需要 secret vault。
|
||
- **平台付费**:你统一付,按 token 加价转售给客户。优点:体验顺;缺点:你要做精细的 quota+成本归账,否则会被刷爆。
|
||
- **混合**(推荐起步):默认平台 key + 限额;高级套餐切 BYO key 不限额。
|
||
|
||
**写 ADR 时要把 "成本归因路径" 画清楚**:哪个表记 `tenant_id × model × token`,谁算月度账单,怎么和 Stripe 对账。
|
||
|
||
### ADR-004 租户内层级
|
||
|
||
最常见的两种:
|
||
|
||
- **扁平**:tenant 直接装 user,所有 user 等价。简单,适合自助型 SaaS。
|
||
- **二级 RBAC**:tenant 有 owner/admin/member,admin 能管 member 的 skill 安装权限和 quota 分配。适合企业销售。
|
||
|
||
如果要 SSO(SAML/OIDC),那默认要二级 RBAC——因为客户 IT 部门要能管理"哪些员工进哪些 workspace"。
|
||
|
||
### ADR-005 存储拓扑
|
||
|
||
详见独立文档:[ADR-005 · 存储拓扑与持久化策略](./adr-005-storage-topology.zh-CN.md)。
|
||
|
||
### ADR-006 运行时与渠道租户化
|
||
|
||
详见独立文档:[ADR-006 · 运行时与渠道层的租户化](./adr-006-runtime-channel-tenancy.zh-CN.md)。
|
||
|
||
要点:
|
||
|
||
- **LangGraph checkpointer**:保留原表结构、**不挂 RLS、不 ALTER 表**——`langgraph-checkpoint-postgres==3.0.5` 不存在 `connection_factory`(spike 验证),改用入口路由(`threads.py` + `thread_runs.py`)强校验 + `threads_meta` `UNIQUE(tenant_id, thread_id)` 兜底。详见 ADR-001 §4.1.1。
|
||
- **MCP 工具缓存**:模块级单例 → per-tenant LRU;OAuth token 当前**进程内存无持久化**,要直接做"持久化 + 加密 + 失败回退"三步并发到 `tenant_secrets`。
|
||
- **Skills loader**:拆 platform 共享只读 + tenant 私有;按 `tenant_skill_state.enabled` 过滤工具。
|
||
- **Sandbox provider**:实例单例,`acquire(thread_id)` 内按 tenant 路由到 K8s namespace;prewarm 池按 plan 大小。
|
||
- **内部 LLM 计费**:Memory / Title / Summarization 调用都算 tenant 用量,分 `usage_category` 报表展示。
|
||
- **IM 渠道**:每个 binding 加 `tenant_id`,webhook handler 强制注入 tenant ContextVar;ghost user 7 天未链接自动停。
|
||
|
||
### ADR-007 路由与前端租户化
|
||
|
||
详见独立文档:[ADR-007 · URL 路由与前端租户化](./adr-007-routing-frontend.zh-CN.md)。
|
||
|
||
要点:
|
||
|
||
- **URL 形态**:`/{slug}/...`,子域名留给 v2 自定义域名。
|
||
- **Cookie**:`Path=/` + JWT 内 `tid`;切换 tenant 重签 JWT + 硬刷新。
|
||
- **前端**:`app/(tenant)/[slug]/layout.tsx` 注入 `TenantProvider`;SDK 实例单例但调用读 `useTenant()`;切换时 `cancelAllStreams + window.location.assign`。
|
||
- **Auth**:扩现有 `app/gateway/auth/jwt.py` `TokenPayload` 加 `{tid, role}` 字段(沿用已有 `ver` 失效机制),不引入 Better Auth;登录后跳 picker / 直进 / onboarding。
|
||
|
||
要点:
|
||
|
||
- **现状问题**:自定义 skill / agent / memory / 上传 / 产物全部在容器本地文件系统。多副本不一致、容器重建丢数据、无备份。
|
||
- **决策**:三层拓扑——**结构化进 Postgres**(agent SOUL/config、memory facts、skill enabled、tenant secrets)+ **大对象进对象存储**(上传、产物、技能包)+ **临时区**(沙箱 workspace,不持久化)。
|
||
- **接口**:`ObjectStorage` Protocol,实现 `LocalObjectStorage`(dev)/ `S3ObjectStorage`(prod)/ `MinIOObjectStorage`(自部署)。
|
||
- **关键约束**:单 bucket + `tenants/{tid}/` prefix;presigned URL 短 TTL;HTML/SVG 强制 `Content-Disposition: attachment`(保留当前 XSS 防护)。
|
||
- **拒绝方案**:① 全部进 PVC(小文件读写差、备份难);② 全部进 S3(强一致差、列表慢、无事务)。
|
||
|
||
---
|
||
|
||
## 2. Tenant 数据模型草稿
|
||
|
||
第 0 阶段就把这张表画出来,第 1 阶段直接落地:
|
||
|
||
```sql
|
||
-- 新表
|
||
tenants (
|
||
id UUID PK,
|
||
slug VARCHAR(64) UNIQUE, -- URL 用,/t/{slug}/...
|
||
display_name VARCHAR(128),
|
||
plan VARCHAR(32), -- free / pro / enterprise
|
||
status VARCHAR(16), -- active / suspended / deleted
|
||
created_at, updated_at
|
||
)
|
||
|
||
tenant_memberships (
|
||
tenant_id UUID FK,
|
||
user_id UUID FK,
|
||
role VARCHAR(16), -- owner / admin / member
|
||
invited_by UUID,
|
||
joined_at,
|
||
PRIMARY KEY (tenant_id, user_id)
|
||
)
|
||
|
||
tenant_secrets (
|
||
tenant_id UUID FK,
|
||
key VARCHAR(64), -- e.g. OPENAI_API_KEY
|
||
encrypted_value BYTEA, -- KMS 加密
|
||
rotated_at,
|
||
PRIMARY KEY (tenant_id, key)
|
||
)
|
||
|
||
tenant_quotas (
|
||
tenant_id UUID FK,
|
||
metric VARCHAR(32), -- tokens_monthly / runs_concurrent / sandbox_cpu_seconds
|
||
hard_limit BIGINT,
|
||
soft_limit BIGINT,
|
||
PRIMARY KEY (tenant_id, metric)
|
||
)
|
||
|
||
tenant_usage_daily (
|
||
tenant_id UUID,
|
||
date DATE,
|
||
metric VARCHAR(32),
|
||
value BIGINT,
|
||
PRIMARY KEY (tenant_id, date, metric)
|
||
)
|
||
|
||
invitations (
|
||
id UUID PK,
|
||
tenant_id UUID FK,
|
||
email VARCHAR(320),
|
||
role VARCHAR(16),
|
||
token VARCHAR(64) UNIQUE,
|
||
expires_at,
|
||
used_at NULL
|
||
)
|
||
|
||
-- 已有表加列
|
||
users + default_tenant_id UUID
|
||
threads_meta + tenant_id UUID + INDEX (tenant_id, user_id, updated_at)
|
||
runs + tenant_id UUID + INDEX (tenant_id, created_at)
|
||
run_events + tenant_id UUID
|
||
feedback + tenant_id UUID
|
||
|
||
-- 未来 Tier 2 还要加:
|
||
skills_state (tenant_id, skill_name, enabled, source)
|
||
agent_configs (tenant_id, user_id, agent_name, soul_md, config_yaml)
|
||
mcp_configs (tenant_id, server_name, transport, url, encrypted_config)
|
||
```
|
||
|
||
画完后让 DBA / 后端 lead 评审两件事:**索引覆盖**(每个查询 path 是否走索引)和 **RLS policy 草稿**(每张带 tenant_id 的表写一条 policy)。
|
||
|
||
---
|
||
|
||
## 3. 多租户改造代码盘点
|
||
|
||
第 0 阶段最容易被忽略的是**先量一下工作量**。在 spreadsheet 里把"目前涉及 user_id / 全局状态"的代码点全部列出来:
|
||
|
||
```bash
|
||
grep -rn "user_id\|get_effective_user_id\|DEFAULT_USER_ID" backend/packages/harness/deerflow/ backend/app/
|
||
grep -rn "users/\|/.deer-flow/" backend/ scripts/
|
||
grep -rn "extensions_config\|skills/public\|skills/custom" backend/
|
||
```
|
||
|
||
把命中点分成五类:
|
||
|
||
| 类别 | 改造动作 | 估计点位 |
|
||
|---|---|---|
|
||
| **DB 仓储**(`persistence/*/sql.py`) | 增加 tenant_id 解析与 WHERE | ~10–15 处 |
|
||
| **文件系统路径**(`ThreadDataMiddleware`、memory storage、agents 存储) | 路径加 tenant 维度 | ~5–8 处 |
|
||
| **配置/Secret 读取**(`models/factory.py`、MCP client、community tools) | 改成 tenant 上下文取 key | ~8–12 处 |
|
||
| **路由 handler**(`app/gateway/routers/*.py`) | 加 `@require_permission` + tenant 上下文 | ~14 个 router 文件 |
|
||
| **全局单例**(沙箱 provider、MCP cache、skills loader) | 缓存 key 加 tenant 维度 | ~5 处 |
|
||
|
||
每条点位估一个 S/M/L 工作量。这张表是后面拆 PR、估工期、估钱的依据。
|
||
|
||
---
|
||
|
||
## 3.5 底座先行(Phase-0 之前 / 并行的基础设施)
|
||
|
||
> 来源:[adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) cross-cutting risk #2 — ADR-001 RLS、ADR-003 secret vault、ADR-005 三层存储、ADR-006 OAuth 持久化共用同一组缺失底座。这组**必须先于任何业务改造落地**,否则各 ADR 互为前置条件死锁。
|
||
|
||
| 底座 | 缺失现状 | 为何阻塞 ADR | Phase-0 内必须产出 |
|
||
|---|---|---|---|
|
||
| **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 + 警告日志),生产实现可推迟 |
|
||
|
||
**时间盒**:3 项底座**与 ADR 评审并行做**,加 1 周到 Phase-0(总计 3 周封顶)。完成验证标准是这 3 件事**至少有可 CI 验证的最小骨架**——不要求 100% 实现,但接口 + 1 个测试用例必须跑通。
|
||
|
||
> 这部分原稿没列。审计后补的。如果跳过这步直接做业务改造,ADR-001/003/005/006 实现时会发现互相依赖、谁都跑不起来。
|
||
|
||
---
|
||
|
||
## 4. 第 0 阶段的"完成定义" (DoD)
|
||
|
||
走完这阶段,团队应该能回答:
|
||
|
||
- [ ] 数据存哪、用什么数据库、怎么隔离 → ADR-001 给出
|
||
- [ ] 客户的 bash/工具跑在哪、能访问什么、爆炸半径多大 → ADR-002 给出
|
||
- [ ] 客户的 LLM 调用钱谁出、怎么算 → ADR-003 给出
|
||
- [ ] 客户内部能不能自己加员工、怎么加 → ADR-004 给出
|
||
- [ ] Skill / agent / 上传 / 产物 / memory 各自存哪、丢失怎么办 → ADR-005 给出
|
||
- [ ] LangGraph / MCP / 内部 LLM / IM 渠道这些"夹层"怎么按租户隔离 → ADR-006 给出
|
||
- [ ] 浏览器地址栏长什么样、Cookie 怎么 scope、租户切换怎么走 → ADR-007 给出
|
||
- [ ] **3 项底座**(Postgres 测试夹具 / ObjectStorage Protocol / KMS 抽象)有可 CI 验证的最小骨架 → §3.5 给出
|
||
- [ ] 第一阶段 PR 怎么拆、估几人周 → 代码盘点给出
|
||
- [ ] 第一个内测客户长什么样、什么时候能上 → 项目经理排期
|
||
|
||
---
|
||
|
||
## 5. 时间盒与节奏
|
||
|
||
第 0 阶段 **三周封顶**(原稿两周,加 §3.5 底座先行的 1 周),再长就是过度设计。
|
||
|
||
- **第 1 周**:写 ADR-001 ~ 007 草稿,团队读、challenge、收敛
|
||
- **第 2 周**:定 schema、做代码盘点、估工、定第一阶段范围与 design partner 客户;同时启动 §3.5 底座 spike(Postgres testcontainers / ObjectStorage Protocol / KMS 抽象)
|
||
- **第 3 周**:底座骨架 PR 合入 + ADR 据实测结果定稿(这一周已经在审计 + spike 中部分提前消耗,参见 [adr-vs-code-audit](./adr-vs-code-audit.zh-CN.md) 与 [adr-spike-langgraph-postgres](./adr-spike-langgraph-postgres.zh-CN.md))
|
||
|
||
如果三周后还有 ADR 定不下来,**绝大多数情况是因为缺一个真实客户做参照**——这时候应该先去签一个 design partner(哪怕免费),用他们的合同和合规要求来反推决策。
|
||
|
||
---
|
||
|
||
## 6. 默认假设(如无特殊情况按此推进)
|
||
|
||
为避免决策瘫痪,先写下一个"默认值",所有 ADR 在没有相反证据前按这个走:
|
||
|
||
| 决策 | 默认值 | 选它的理由 |
|
||
|---|---|---|
|
||
| **数据库** | 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 和企业销售留口 |
|
||
| 存储拓扑 | Postgres(结构化)+ S3 兼容对象存储(大对象)+ emptyDir(临时) | 沙箱 pod 真正无状态;备份/灾备/横向扩展直接通 |
|
||
| 运行时夹层 | per-tenant MCP cache + skills 拆双路 + 内部 LLM 计费分类 + IM binding 加 tenant | 关上"非仓储非沙箱"那一组进程级单例的隔离漏洞 |
|
||
| 前端路由 | `/{slug}/...` 路径 + JWT 内 tid + 硬刷新切换 | UX 简单,与 ADR-001/004 cookie 模型契合 |
|
||
|
||
> 这是"中等强度方案",覆盖 90% B2B SaaS。如果客户画像偏极端(大企业 / 强合规 / 自助小客户),再调整。
|