Files
ZY-Agent/docs/multi-tenant-redesign/01-redesign/multi-tenant-phase-0-plan.zh-CN.md
T
1445043649 89fe54cc07 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>
2026-05-10 21:39:58 +08:00

17 KiB
Raw Blame History

多租户改造 · 第 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 租户 10k100 大客户 <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。
  • 二级 RBACtenant 有 owner/admin/memberadmin 能管 member 的 skill 安装权限和 quota 分配。适合企业销售。

如果要 SSOSAML/OIDC),那默认要二级 RBAC——因为客户 IT 部门要能管理"哪些员工进哪些 workspace"。

ADR-005 存储拓扑

详见独立文档:ADR-005 · 存储拓扑与持久化策略

ADR-006 运行时与渠道租户化

详见独立文档:ADR-006 · 运行时与渠道层的租户化

要点:

  • LangGraph checkpointer:保留原表结构、不挂 RLS、不 ALTER 表——langgraph-checkpoint-postgres==3.0.5 不存在 connection_factoryspike 验证),改用入口路由(threads.py + thread_runs.py)强校验 + threads_meta UNIQUE(tenant_id, thread_id) 兜底。详见 ADR-001 §4.1.1。
  • MCP 工具缓存:模块级单例 → per-tenant LRUOAuth token 当前进程内存无持久化,要直接做"持久化 + 加密 + 失败回退"三步并发到 tenant_secrets
  • Skills loader:拆 platform 共享只读 + tenant 私有;按 tenant_skill_state.enabled 过滤工具。
  • Sandbox provider:实例单例,acquire(thread_id) 内按 tenant 路由到 K8s namespaceprewarm 池按 plan 大小。
  • 内部 LLM 计费Memory / Title / Summarization 调用都算 tenant 用量,分 usage_category 报表展示。
  • IM 渠道:每个 binding 加 tenant_idwebhook handler 强制注入 tenant ContextVarghost user 7 天未链接自动停。

ADR-007 路由与前端租户化

详见独立文档:ADR-007 · URL 路由与前端租户化

要点:

  • URL 形态/{slug}/...,子域名留给 v2 自定义域名。
  • CookiePath=/ + JWT 内 tid;切换 tenant 重签 JWT + 硬刷新。
  • 前端app/(tenant)/[slug]/layout.tsx 注入 TenantProviderSDK 实例单例但调用读 useTenant();切换时 cancelAllStreams + window.location.assign
  • Auth:扩现有 app/gateway/auth/jwt.py TokenPayload{tid, role} 字段(沿用已有 ver 失效机制),不引入 Better Auth;登录后跳 picker / 直进 / onboarding。

要点:

  • 现状问题:自定义 skill / agent / memory / 上传 / 产物全部在容器本地文件系统。多副本不一致、容器重建丢数据、无备份。
  • 决策:三层拓扑——结构化进 Postgresagent SOUL/config、memory facts、skill enabled、tenant secrets+ 大对象进对象存储(上传、产物、技能包)+ 临时区(沙箱 workspace,不持久化)。
  • 接口ObjectStorage Protocol,实现 LocalObjectStoragedev/ S3ObjectStorageprod/ MinIOObjectStorage(自部署)。
  • 关键约束:单 bucket + tenants/{tid}/ prefixpresigned URL 短 TTLHTML/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 ~1015 处
文件系统路径ThreadDataMiddleware、memory storage、agents 存储) 路径加 tenant 维度 ~58 处
配置/Secret 读取models/factory.py、MCP client、community tools 改成 tenant 上下文取 key ~812 处
路由 handlerapp/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 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 + 警告日志),生产实现可推迟

时间盒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 底座 spikePostgres testcontainers / ObjectStorage Protocol / KMS 抽象)
  • 第 3 周:底座骨架 PR 合入 + ADR 据实测结果定稿(这一周已经在审计 + spike 中部分提前消耗,参见 adr-vs-code-auditadr-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 hookspike 已验证),应用层兜底
沙箱隔离 K8s namespace + gVisor + NetworkPolicy 默认禁出网 强度足够 + 运维可控
LLM Key 混合:默认平台 key + 限额,premium 切 BYO;悲观预扣防超额 体验与成本兼顾
租户层级 二级 RBACowner/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。如果客户画像偏极端(大企业 / 强合规 / 自助小客户),再调整。