* 新增 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>
17 KiB
多租户改造 · 第 0 阶段计划
目标:在写第一行代码前,对齐设计、产出可评审的文档。这一阶段不动代码,时间盒 两周封顶。
产出物清单(8 份文档 + 1 份代码盘点)
| 产出物 | 形式 | 谁批 | 作用 |
|---|---|---|---|
| ADR-001 数据隔离模型 | ADR(决策记录) | CTO / 架构 | 锁定行级 / schema / 库级;含 LangGraph checkpoint 表的注入路径 |
| ADR-002 沙箱隔离模型 | ADR + 威胁模型 | 安全 + 架构 | 锁定 K8s / Firecracker / VM |
| ADR-003 LLM Key 与计费模型 | ADR | 产品 + CTO | 锁定 BYO / 平台付费 / 混合;含悲观预扣 + 内部 LLM 计费 |
| ADR-004 租户 ↔ 用户层级 | ADR | 产品 | 锁定二级 RBAC + JWT/cache 一致性策略 |
| ADR-005 存储拓扑 | ADR + ObjectStorage 接口签名 | 架构 + SRE | 锁定 DB / 对象存储 / 临时区分层 |
| ADR-006 运行时与渠道租户化 | ADR | 后端 lead + 渠道 owner | 锁定 checkpointer / MCP cache / skills loader / 内部 LLM 计费 / IM 渠道 ↔ 租户 |
| ADR-007 路由与前端租户化 | ADR | 前端 lead + 后端 lead | 锁定 URL 形态 / cookie / 自签 JWT 扩字段 / SDK 切换 |
| adr-vs-code-audit | 审计报告 | 架构 | 7 份 ADR 对照现状代码的差异清单(已据其修订 ADR-001/006/007) |
| adr-spike-langgraph-postgres | 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 数据隔离
| 维度 | 行级 (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-006 运行时与渠道租户化
详见独立文档:ADR-006 · 运行时与渠道层的租户化。
要点:
- LangGraph checkpointer:保留原表结构、不挂 RLS、不 ALTER 表——
langgraph-checkpoint-postgres==3.0.5不存在connection_factory(spike 验证),改用入口路由(threads.py+thread_runs.py)强校验 +threads_metaUNIQUE(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 路由与前端租户化。
要点:
- 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.pyTokenPayload加{tid, role}字段(沿用已有ver失效机制),不引入 Better Auth;登录后跳 picker / 直进 / onboarding。
要点:
- 现状问题:自定义 skill / agent / memory / 上传 / 产物全部在容器本地文件系统。多副本不一致、容器重建丢数据、无备份。
- 决策:三层拓扑——结构化进 Postgres(agent SOUL/config、memory facts、skill enabled、tenant secrets)+ 大对象进对象存储(上传、产物、技能包)+ 临时区(沙箱 workspace,不持久化)。
- 接口:
ObjectStorageProtocol,实现LocalObjectStorage(dev)/S3ObjectStorage(prod)/MinIOObjectStorage(自部署)。 - 关键约束:单 bucket +
tenants/{tid}/prefix;presigned URL 短 TTL;HTML/SVG 强制Content-Disposition: attachment(保留当前 XSS 防护)。 - 拒绝方案:① 全部进 PVC(小文件读写差、备份难);② 全部进 S3(强一致差、列表慢、无事务)。
2. Tenant 数据模型草稿
第 0 阶段就把这张表画出来,第 1 阶段直接落地:
-- 新表
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 / 全局状态"的代码点全部列出来:
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 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-spike-langgraph-postgres)
如果三周后还有 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。如果客户画像偏极端(大企业 / 强合规 / 自助小客户),再调整。