diff --git a/docs/multi-tenant-redesign/02-rollout/headless-api-track.zh-CN.md b/docs/multi-tenant-redesign/02-rollout/headless-api-track.zh-CN.md new file mode 100644 index 00000000..3759c6ec --- /dev/null +++ b/docs/multi-tenant-redesign/02-rollout/headless-api-track.zh-CN.md @@ -0,0 +1,565 @@ +# Headless API · 业务系统集成轨道 + +> 写于 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 双主线。 +> **身份模式**:service account 折叠 + external_user_id 透传,两种都支持,按 endpoint 选。 +> **集成 pattern**:**Pattern A(业务系统 backend 代理)+ Pattern B(浏览器直连 + 短期 JWT)**。不做嵌入式 widget。 + +--- + +## 0. 现状评估 + +DeerFlow 架构已经接近"可被业务系统调用的后端"——证据:`backend/app/channels/` 下的 IM 集成(Slack / 飞书 / 钉钉 / Telegram)就是这种用法的活体范例。它们通过 `langgraph-sdk` 调 Gateway HTTP API + 把响应转给 IM 平台,**根本不经过 `frontend/` 工程**。 + +### 已经具备(约 80%) + +| 能力 | 实现位置 | +|---|---| +| 完整 REST API | `backend/app/gateway/routers/`(threads / runs / messages / events / feedback / models / skills / mcp / memory / uploads / artifacts)| +| LangGraph SDK 兼容路径 | `/api/langgraph/*` —— 任何 `langgraph-sdk` 客户端可直接接 | +| Streaming(SSE) | `runs/stream` + `messages-tuple` delta + `values` + `custom` | +| 嵌入式 Python 客户端 | `packages/harness/deerflow/client.py` `DeerFlowClient` | +| 现成的"无 web 前端"调用证明 | `app/channels/manager.py` 完全不依赖 frontend | + +### Stage 0 完成后还差什么 + +Stage 0 把 workspace 概念立起来了,但 **auth 模式仍是 cookie + JWT + CSRF**——这是为 web 前端设计的,不适合 server-to-server,更不适合"业务系统自研 web 页面浏览器直连"的场景。要让业务系统调,必须补两条平行 auth 路径 + 一组管理能力,详见 §1。 + +### 集成 pattern 速览 + +| Pattern | 链路 | 适用 | 优先级 | +|---|---|---|---| +| **A. Backend 代理**(默认) | browser → 业务系统 backend → DeerFlow API key → DeerFlow | 业务系统已有 backend;不在乎多一跳 | Stage 1 必做 | +| **B. Browser 直连**(streaming 友好) | 业务系统 backend 颁短期 JWT → browser 直连 DeerFlow `/api/v1/*`(含 SSE) | chat / agent 类、对 token 流延迟敏感、自研 web 页面 | Stage 1 末必做 | +| ~~C. 嵌入式 widget / iframe~~ | DeerFlow 托管 chat widget URL,业务系统 embed | 集成方零前端开发 | **不做**(产品决定) | + +Pattern A 是"server-to-server",Pattern B 是"browser-to-server"。两者共用同一组 service account / API key 数据模型,**只是 auth 路径不同**: +- Pattern A:`Authorization: Bearer dfk_live_` +- Pattern B:`Authorization: Bearer eyJ` + +--- + +## 1. 改造清单(按 MVP 优先级) + +| # | 能力 | 是否 MVP | 缺它会怎么样 | 工作量 | +|---|---|---|---|---| +| 1 | **API Key 认证(Pattern A)** | **必须** | 业务系统 backend 没法调,跨域 + CSRF 灾难 | M | +| 2 | **Service Account 概念** | **必须** | API key 必须挂在某个"账户"上做归属、计费、quota | M | +| 3 | **CSRF bypass on bearer** | **必须** | bearer 路径走 CSRF middleware 直接 403 | XS | +| 4 | **External User ID 透传** | **必须** | 业务系统的"小明"在 DeerFlow 内不能体现,memory / 个性化失效 | M | +| 5 | **API 版本化 `/api/v1/`** | **必须**(早做便宜)| 演进时业务系统要全量改 | S(早做)/ L(晚做)| +| 6 | **Rate limit per API key** | **必须**(基础版) | 业务系统 bug 把 DeerFlow 打爆 | S(基础)/ M(分层)| +| 7 | **Token Exchange(Pattern B)** | **必须** | 浏览器只能走 backend 代理,自研 web 页面延迟差 | S | +| 8 | **CORS 中间件 + per-workspace allowed_origins** | **必须**(与 Pattern B 配套)| 浏览器请求被同源策略挡 | M | +| 9 | **短期 JWT 验证路径**(AuthMiddleware 第三条) | **必须**(与 Pattern B 配套)| 短期 JWT 没法验 | S | +| 10 | **Idempotency keys** | 推荐 | 业务系统重试时重复建 thread/run | S | +| 11 | **Webhook outbound** | 推迟 | 业务系统轮询事件,多调几次 SSE 而已 | M(推到 Stage 2)| + +**MVP 包**(前 9 项)= **4-5 周**;放进 Stage 1 并行做。Pattern B(7-9)依赖 1-3 完成,建议 Stage 1 末(最后 1-2 周)。 + +--- + +## 2. 核心设计:API Key + Service Account + +### 数据模型 + +```sql +service_accounts ( + id UUID PK, + workspace_id UUID FK NOT NULL, -- 必属于一个 workspace + name VARCHAR(64), -- "X 业务系统集成" + role VARCHAR(16), -- 在 workspace 内的 role:member / admin + identity_mode VARCHAR(16), -- collapsed | external_passthrough | both + status VARCHAR(16), -- active / suspended / revoked + created_by UUID, -- 哪个 user 创建的(必须是 workspace owner/admin) + created_at, updated_at +) + +api_keys ( + id UUID PK, + service_account_id UUID FK NOT NULL, + key_prefix VARCHAR(16) UNIQUE, -- 前 16 字符明文(dfk_live_abc123...)UI 可显示 + key_hash BYTEA NOT NULL, -- 完整 key 的 sha256,比对用 + name VARCHAR(64), -- "生产环境 key" / "灰度 key" + scopes TEXT[], -- ["threads:read", "runs:create", "uploads:write"...] + rate_limit_rpm INT NULL, -- 每分钟请求数;NULL=用 workspace plan 默认 + expires_at TIMESTAMP NULL, -- 可选过期时间 + last_used_at TIMESTAMP NULL, + revoked_at TIMESTAMP NULL, + created_at +) + +external_users ( -- ghost user,按需建(identity_mode=external_passthrough 时) + id UUID PK, + workspace_id UUID FK NOT NULL, + service_account_id UUID FK NOT NULL, + external_id VARCHAR(128) NOT NULL, -- 业务系统传过来的 ID,原样存 + display_name VARCHAR(128) NULL, + metadata JSONB, -- 可选业务字段 + created_at, last_active_at, + UNIQUE (workspace_id, service_account_id, external_id) +) +``` + +### Key 格式约定 + +``` +dfk_live_<24 字符随机> # 生产 key +dfk_test_<24 字符随机> # 测试 key +``` + +- 前缀 `dfk_live_` / `dfk_test_` 让一眼区分环境(防止把测试 key 投进生产) +- 写入 DB 时只存 `sha256(key)`,明文创建后只能在 UI 显示一次 +- `key_prefix` 列存前 16 字符(`dfk_live_abc12345`)—— UI 列表 + 审计日志可识别但不能用 + +### 认证流程 + +``` +请求 → AuthMiddleware → 检测 Authorization header + │ + ├── "Bearer dfk_..." + │ ↓ + │ APIKeyAuthBackend.authenticate + │ ↓ + │ SELECT api_keys WHERE key_hash = sha256(token) + │ ↓ + │ load service_account + workspace + │ ↓ + │ set_current_workspace(workspace_id) + │ set_current_service_account(account) + │ set_current_user(None) # 没有真人 user + │ ↓ + │ CSRFMiddleware skip(bearer 路径不要 CSRF) + │ ↓ + │ 如有 X-External-User-Id header 且 identity_mode 允许: + │ → upsert external_users → set_current_external_user + │ + └── "Cookie: access_token=..." → 走现有 web 前端流程 +``` + +### `@require_permission` 装饰器升级 + +```python +# 现有签名(cookie 模式) +@require_permission("threads", "read", owner_check=True) + +# 改为同时支持 service account 路径 +@require_permission( + resource="threads", + action="read", + scopes=["threads:read"], # API key 必须有此 scope + owner_check="workspace_or_user", # SA: 校验 workspace 归属;user: 校验 user_id +) +``` + +--- + +## 3. 核心设计:两种身份模式 + +### 模式 A:service account 折叠(identity_mode=`collapsed`) + +业务系统调一切操作都归到该 service account 名下。**适合工具类集成**(CRM 自动总结、安全面板分析)。 + +``` +POST /api/v1/threads +Authorization: Bearer dfk_live_xxx + +→ 创建 thread,所有归属都是 service_account_id + threads_meta.user_id = NULL + threads_meta.service_account_id = + threads_meta.workspace_id = +``` + +memory 也是 service account 共享的(`workspaces/{wid}/service_accounts/{sa_id}/memory.json`)。 + +### 模式 B:external_user_id 透传(identity_mode=`external_passthrough`) + +业务系统的"小明"在 DeerFlow 内独立。**适合 chatbot / 助手类集成**。 + +``` +POST /api/v1/threads +Authorization: Bearer dfk_live_xxx +X-External-User-Id: bizsys_user_42 # 业务系统的用户 ID + +→ AuthMiddleware: + 1. 解 API key → load SA + 2. 看 SA.identity_mode 允许 external_passthrough + 3. SELECT external_users WHERE (workspace_id, service_account_id, external_id="bizsys_user_42") + 4. 没找到 → 自动建 ghost external_user + 5. set_current_external_user(...) + +→ 创建 thread: + threads_meta.workspace_id = + threads_meta.service_account_id = + threads_meta.external_user_id = +``` + +memory 是 per external_user 的(`workspaces/{wid}/service_accounts/{sa_id}/external_users/{eu_id}/memory.json`)。 + +### 模式选择策略 + +`SA.identity_mode` 三态: +- `collapsed`:忽略所有 `X-External-User-Id` header +- `external_passthrough`:必须传 `X-External-User-Id`,缺失时 400 +- `both`:传了走透传、不传走折叠(最灵活但最复杂;建议默认不开放) + +业务系统接入时由 workspace owner 创建 SA 时选定。 + +### 这影响哪些 endpoints + +| endpoint | service account 折叠 | external_user_id 透传 | +|---|---|---| +| `POST /threads` | thread 归 SA | thread 归 external_user | +| `GET /threads` | 列出 SA 的所有 thread | 仅列出该 external_user 的 thread | +| memory 注入 | SA 共享 memory | 该 external_user 的 memory | +| `usage_daily` 写入 | `(workspace_id, SA_id, "main", ...)` | 同上 + `external_user_id` 维度 | +| feedback | feedback.user_id 留空,标 SA | feedback.external_user_id 标人 | + +--- + +## 3.5 核心设计:Pattern B(Browser 直连 + 短期 JWT 交换) + +### 为什么需要 + +业务系统自研的 web 页面如果走 Pattern A(Backend 代理),他们 backend 必须实现 SSE 流式转发——这是个不小的工程量,且每个 token 多一跳延迟。**Pattern B 把 streaming 直接交给浏览器**,业务系统 backend 只做一次性 token 颁发。 + +### 数据模型扩展 + +```sql +-- workspaces 表加列 +workspaces.allowed_origins TEXT[] -- ["https://app.partner.com", "https://staging.partner.com"] + +-- 不需要新表;短期 JWT 不持久化(足够短就不需要 revoke list) +``` + +### Token Exchange 流程 + +``` +[业务系统 backend] [DeerFlow] [浏览器] + │ │ │ + │ 1. 用户在业务系统登录 │ │ + │ ◄────────────────────────────────│────────────────────────────────│ + │ │ │ + │ 2. 业务 backend 鉴权后调 │ │ + │ POST /api/v1/auth/exchange-token │ + │ Authorization: Bearer dfk_live_xxx │ + │ { external_user_id: "ming_42", expires_in: 600 } │ + │ ────────────────────────────────►│ │ + │ │ │ + │ │ 3. 校验 API key + SA 状态 │ + │ │ upsert external_users 行 │ + │ │ 签短期 JWT │ + │ │ │ + │ { access_token: "eyJ...", │ │ + │ expires_at: "..." } │ │ + │ ◄────────────────────────────────│ │ + │ │ │ + │ 4. 把 access_token 发给浏览器 │ │ + │ ─────────────────────────────────────────────────────────────────►│ + │ │ │ + │ │ 5. browser 直连 DeerFlow │ + │ │ GET /api/v1/threads/.../events│ + │ │ Authorization: Bearer eyJ...│ + │ │ Origin: https://app.partner.com│ + │ │ ◄──────────────────────────────│ + │ │ │ + │ │ 6. CORS preflight 通过 │ + │ │ SSE stream 200 │ + │ │ ──────────────────────────────►│ +``` + +### 短期 JWT 设计 + +```python +# 不复用现有 cookie JWT 的 TokenPayload,新增 ServiceTokenPayload +class ServiceTokenPayload(BaseModel): + sub: str # external_user_id(DeerFlow 内部 id,不是业务方原始 id) + sa: str # service_account_id + wid: str # workspace_id + eid: str # external_id(业务方原始 id,传给 audit log) + scopes: list[str] # 从 api_key.scopes 继承(不能放大) + exp: int # 5-15 min(默认 10 min) + iat: int + iss: "deerflow" # 区分自签 vs 业务方签 + typ: "service" # 区分 cookie JWT(typ=user) +``` + +### Endpoint 规格 + +``` +POST /api/v1/auth/exchange-token +Authorization: Bearer dfk_live_xxx (API key) +Content-Type: application/json + +Request: +{ + "external_user_id": "ming_42", // 必填(identity_mode=external_passthrough) + "expires_in": 600, // 可选,默认 600s,最大 3600s + "scopes": ["threads:write", "runs:read"] // 可选;省略则继承 API key 全部 scopes +} + +Response 200: +{ + "access_token": "eyJ...", + "token_type": "Bearer", + "expires_at": "2026-05-09T15:20:00Z" +} + +Errors: +- 401 invalid API key +- 403 SA suspended / API key revoked +- 400 identity_mode 不允许 external_user_id(collapsed 模式) +- 400 scopes 超出 API key 授权 +``` + +### AuthMiddleware 第三条路径 + +``` +请求 → AuthMiddleware → 检测 Authorization + │ + ├── "Bearer dfk_..." → APIKeyAuthBackend (Pattern A) + ├── "Bearer eyJ...typ=service" → ServiceTokenAuthBackend (Pattern B) ← 新增 + └── "Cookie: access_token=..." → CookieAuthBackend (web 前端) +``` + +`ServiceTokenAuthBackend.authenticate`: +1. 验 JWT 签名(DeerFlow 自签私钥;HS256 即可,不需要 RSA) +2. 检 `iss=deerflow, typ=service`(防止把 cookie JWT 误用) +3. 校 `sa` service account 状态(可能在签发后被 suspend) +4. 校 scope 子集合法(不能超过 SA 当前 scopes) +5. `set_current_workspace(wid)` + `set_current_service_account(sa)` + `set_current_external_user(sub)` + +### CORS 设计 + +```python +# CORSMiddleware 在 AuthMiddleware 之前加载(FastAPI 中间件顺序) +class WorkspaceAwareCORSMiddleware: + async def dispatch(self, request, call_next): + origin = request.headers.get("origin") + if not origin: + return await call_next(request) # 非浏览器请求 + + # 解析当前 workspace(从 token 或 query string) + # CORS preflight (OPTIONS) 没有 token,要从其他维度推 + # 简化方案:所有 /api/v1/* 路径在 OPTIONS 时回 wildcard,但不带 credentials; + # 真请求时按 token 上的 wid 查 workspaces.allowed_origins 做精确匹配 + ... +``` + +**关键安全点**: +- `Access-Control-Allow-Credentials: false`(短期 JWT 不依赖 cookie,不需要 credentials;防止意外打开 cookie 跨域) +- `Access-Control-Allow-Origin` 精确匹配,不用通配 +- `Access-Control-Max-Age` 短一点(5 分钟),方便切换 origin 时不被缓存卡 + +### 与现有 CSRF 的关系 + +CSRFMiddleware 检测到 `Authorization: Bearer ...` 直接 skip——Pattern A 和 Pattern B 都走同一条 skip 逻辑。CSRF 仅对 cookie 路径生效。 + +### Token revoke 策略 + +短期 JWT 默认**不显式 revoke**(5-15 min 过期,自然失效)。但有三个例外: +1. SA `status=suspended` → ServiceTokenAuthBackend 在 step 3 检测时直接拒 +2. API key 被 `revoked_at` → 同上(短期 JWT 验证时关联回 `sa.api_key`) +3. 如果客户提需要立即 revoke 用户访问 → 业务系统调 DeerFlow `POST /api/v1/external-users/{id}/revoke-tokens` 把 `external_users.token_version` bump(`exchange-token` 签发时把它写进 JWT,验证时比对) + +> 第 3 条是 nice-to-have;MVP 不做,等业务方明确提需求再加。 + +### Pattern B 不做的事(重要) + +- **不**让浏览器直接持有 API key(`dfk_live_*`)——再短的 TTL 也不行;API key 必须留在业务方 backend +- **不**支持 OAuth 2.0 完整 flow(authorize / consent / refresh)——太重,业务方 backend 自己做完用户认证再换 token 即可 +- **不**做客户端 SDK——给一份 OpenAPI + 浏览器原生 fetch / EventSource 例子,业务方自己接 + +--- + +## 4. 核心设计:API 版本化 + +**早做的成本**:仅是 `app/gateway/routers/__init__.py` 里 mount prefix 改 `/api/v1`。 +**晚做的成本**:所有业务系统 client 全量改地址。 + +### 设计 + +```python +# 现状 +app.include_router(threads_router, prefix="/api/threads") + +# Stage 1 改造 +app.include_router(threads_router, prefix="/api/v1/threads") +# 同时保留 /api/threads → 转发到 v1(前端用),sunset 在 Stage 3 +``` + +**deprecation 策略**: +- `/api/*`(无版本)路径在 response 加 `X-API-Deprecated: 2027-01-01` header +- frontend 同步迁到 `/api/v1` +- LangGraph SDK 兼容路径 `/api/langgraph/` 不带版本(跟随上游 SDK 约定) + +**版本演进规则**: +- 加字段:v1 内做,不升 v2 +- 改语义、删字段、改默认值:v2 +- 整体 endpoints 重组:v2 + +--- + +## 5. 核心设计:Rate Limit + Idempotency + +### Rate Limit(基础版) + +``` +api_keys.rate_limit_rpm 设了值:用 key 自己的 +没设:用 workspace plan 默认(free=60, pro=600, team=3000,可配) + +实现:进程内 sliding window(依赖 Postgres 即可,不需要 Redis): + rate_limit_log(api_key_id, minute_bucket, count) 单独小表 +``` + +进阶(Stage 2):分维度(per endpoint、per LLM、per sandbox quota)的复合 rate limit;引入 Redis。 + +### Idempotency Keys + +``` +请求带 Idempotency-Key: + ↓ +SELECT idempotency_records WHERE (api_key_id, key=...) AND created_at > NOW() - 24h + ↓ +命中 → 直接返回上次 response(200 + body 原样) +未命中 → 处理请求 → 写 idempotency_records + 返回 +``` + +只在写操作(POST / PUT / PATCH / DELETE)支持;GET 不需要。 + +--- + +## 6. 与 Stage 1 的整合 + +把 headless API MVP 包并入 Stage 1,**时间盒从 6-10 周延到 8-13 周**。 + +### 修订后 Stage 1 必做项 + +| 改动 | 类型 | 估工 | +|---|---|---| +| Postgres 切换 | 原 Stage 1 | M | +| Quota 系统 + TokenUsage 持久化 | 原 Stage 1 | M+ | +| Stripe 基础订阅 | 原 Stage 1 | M | +| AioSandbox 出网/资源收紧 | 原 Stage 1 | M | +| **API Key + Service Account 数据模型 + 仓储** | **新增(Pattern A)** | M | +| **APIKeyAuthBackend + AuthMiddleware 双路径** | **新增(Pattern A)** | M | +| **CSRF skip on bearer** | **新增** | XS | +| **External User ID 透传 + ghost user** | **新增** | M | +| **`/api/v1/` 版本化** | **新增**(早做便宜)| S | +| **Per-API-key rate limit(基础版)** | **新增** | S | +| **`@require_permission` 装饰器升级支持 SA 路径** | **新增** | S | +| **API key 管理 UI(workspace settings 内)** | **新增**(最低限:CLI 也行)| S(CLI)/ M(UI)| +| **`POST /api/v1/auth/exchange-token` endpoint** | **新增(Pattern B)** | S | +| **`ServiceTokenAuthBackend`(短期 JWT 验证)** | **新增(Pattern B)** | S | +| **`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 PR 顺序 + +**轨道一:付费 SaaS 基础**(与下面并行) +1. Postgres 切换(dev → 灰度 → 全切) +2. `workspace_quotas` / `workspace_usage_daily` 表 + 仓储 +3. `TokenUsageMiddleware` 升级为持久化 +4. `QuotaMiddleware` 加入中间件链 +5. Stripe webhook + 订阅状态同步 +6. AioSandbox 收紧 +7. 基础监控 + +**轨道二:Headless API Pattern A**(与轨道一并行;步骤 1 必须先完成轨道一的 1) +1. `service_accounts` + `api_keys` + `external_users` 仓储(**先于业务路径**) +2. `APIKeyAuthBackend` + `AuthMiddleware` 双路径(cookie + bearer) +3. CSRF middleware skip on bearer +4. `/api/v1/` mount prefix 切换 + 旧路径兼容转发 +5. `external_user_id` 透传机制(依赖 1-4) +6. `service_accounts.identity_mode` 三态行为分支 +7. `@require_permission` 升级 + scope 校验 +8. 基础 rate limit +9. API key 管理 CLI + UI +10. Idempotency keys(可选) + +**轨道三:Headless API Pattern B**(依赖轨道二的 1-7 完成;建议 Stage 1 最后 1-2 周) +1. `workspaces.allowed_origins` 列 + workspace settings UI 的 origin 管理 +2. `WorkspaceAwareCORSMiddleware`(在 AuthMiddleware 之前) +3. `POST /api/v1/auth/exchange-token` endpoint + `ServiceTokenPayload` 设计 +4. `ServiceTokenAuthBackend`(AuthMiddleware 第三条路径) +5. SSE 在 CORS 跨域下的 streaming 验证(写一个 fixture web 页面测) +6. 业务方文档:从换 token 到浏览器直连的端到端示例(HTML + JS) + +--- + +## 7. SaaS vs on-prem 差异 + +| 能力 | SaaS 形态 | on-prem 形态 | +|---|---|---| +| API key 管理 | workspace settings UI + CLI | CLI 必须;UI 可选;env var 注入预置 key 也合理 | +| 配额 / 计费 | 按 plan,Stripe 同步 | 配额作为容量管理(防内部失控),不接 Stripe | +| KMS | AWS / 阿里云 KMS(Stage 2 后落) | 客户自带 KMS(HashiCorp Vault / 客户自有);env var 兜底(明文)| +| 监控 | 你们运维的 Grafana | 客户自有;DeerFlow 暴露 `/metrics` Prometheus 端点(Stage 2 加)| +| Webhook | 平台默认 | 客户内网回调;要求 url 可配置 | +| Rate limit | 平台分档强制 | 客户自定义;默认放宽 | +| 升级路径 | 你们灰度发布 | docker image tag + 升级文档;schema migration 跑通 | + +**重要**:Stage 0 的 schema 设计已经兼容两者(workspace 是 self-hosted 时也 1 对 1 对应一个安装)。**不需要为 on-prem 单独建分支**。 + +### on-prem 专属工作(Stage 1+ 一次性) + +- docker compose 模板(已部分有,加 service_accounts seed 流程) +- "首次安装预置 1 个 workspace + 1 个 admin + 1 个 platform key" 的 init script +- 升级脚本(schema migrations 自动跑) +- 部署文档(一份就够) + +工作量约 1 人周,可以在 Stage 1 末或 Stage 2 头做。 + +--- + +## 8. 不可逆决策(动手前想清楚) + +| 决策 | 难回头的原因 | +|---|---| +| API Key 格式(前缀 `dfk_` / 长度 / 加密路径)| 业务系统接入后改格式所有 key 全失效 | +| `service_accounts` 表是否能跨 workspace(暂定不允许) | 改了所有 quota / 计费归属逻辑 | +| `X-External-User-Id` header 名 | 业务系统集成后改 header 名要全部联调 | +| `identity_mode` 三态语义(collapsed / external / both) | 改语义要联调所有业务系统 | +| `/api/v1/` 是不是从 Stage 1 第 5 步开始;具体 deprecation 时间 | 业务系统接了之后再要求换 prefix 很不友好 | +| ghost user 自动创建策略(X-External-User-Id 缺失时拒绝还是 fallback collapsed) | 改了业务系统接入测试要重跑 | +| memory 隔离粒度(SA 共享 vs per external_user) | 一旦客户用上,迁移 memory 数据极麻烦 | +| **短期 JWT TTL 默认值**(5 / 10 / 15 min)和 max | 业务系统集成后调短会断当前会话;调长会留更大被偷窃的窗口 | +| **`ServiceTokenPayload` 字段集**(claim 名 / 是否带 `eid`) | 改 claim 名所有签名校验失败;早一点把审计需要的字段都设计进去 | +| **`workspaces.allowed_origins` 是 workspace 级还是 SA 级** | 暂定 workspace 级(更简单);改成 SA 级要拆数据 | + +--- + +## 9. 推荐推进路径 + +**第 0-1 周**: +- 找现有需要集成的业务系统聊一下,把"两种身份模式哪个适合 / 是否有 webhook 需求 / 是否有 rate limit 偏好"问清楚 +- 写一份 API key 创建 + 第一次 hello-world 调用的快速指南草稿(不写代码,纯设计验证) + +**第 2-3 周(Stage 0 末)**: +- Stage 0 收尾时把 `service_accounts` / `api_keys` / `external_users` schema 也加上(schema 加上,路径不接) + +**第 4-13 周(Stage 1)**: +- 按上面的 13 步 PR 顺序推进 +- 第 6-8 周可以拉一个业务系统做内测集成(用真实 API key 调真实 endpoint) +- 第 12-13 周收尾,给业务系统正式 go-live + +**与 Stage 1 业务客户上线的关系**: +- Stage 1 一头是付费 SaaS 客户、一头是业务系统集成客户。两条产品线**共用同一套** workspace + auth + quota,**只是认证模式不同**(cookie vs bearer) +- 因此架构上没有冲突,团队可以并行推 + +--- + +## 10. 与现有 ADR / rollout 的关系 + +| 引用 | 关系 | +|---|---| +| [adr-007 §6 路由](../01-redesign/adr-007-routing-frontend.zh-CN.md) | 路由层假设 cookie + JWT;本文档加了 bearer + API key 平行路径 | +| [adr-007 §8 Auth](../01-redesign/adr-007-routing-frontend.zh-CN.md) | TokenPayload 扩 `tid/role` 已对齐;本文档新增 API key 路径不复用 JWT | +| [adr-004 RBAC](../01-redesign/adr-004-tenant-rbac.zh-CN.md) | service_accounts.role 复用 owner/admin/member;不另建 role 体系 | +| [adr-003 §4.4 quota](../01-redesign/adr-003-llm-key-billing.zh-CN.md) | quota 写入按 SA 归属时只看 workspace_id;按 external_user 归属时多一维 | +| [phased-rollout-by-scale Stage 1](./phased-rollout-by-scale.zh-CN.md) | 本文档 §6 给出 Stage 1 修订后必做项与 PR 顺序 | +| [stage-0-code-map](./stage-0-code-map.zh-CN.md) | Stage 0 schema 时把 `service_accounts` 也加上不阻塞 | diff --git a/docs/multi-tenant-redesign/02-rollout/phased-rollout-by-scale.zh-CN.md b/docs/multi-tenant-redesign/02-rollout/phased-rollout-by-scale.zh-CN.md index de88d5cf..76b995f3 100644 --- a/docs/multi-tenant-redesign/02-rollout/phased-rollout-by-scale.zh-CN.md +++ b/docs/multi-tenant-redesign/02-rollout/phased-rollout-by-scale.zh-CN.md @@ -17,11 +17,13 @@ | Stage | 触发条件(业务事实) | 主旋律 | 时间盒 | |---|---|---|---| | **0** | 现在 → 第一个付费客户准备 | workspace 模型立起来;现有 auth 收紧;不做真隔离 | 3–4 周 | -| **1** | 第一批付费客户(10–50 付费 / 500–2000 free) | **Postgres + Quota 必落**;workspace 全链路 + 入口强校验;AioSandbox 收紧 | 6–10 周 | -| **2** | 增长期(100–500 付费 / 5k–20k 用户) | DeerFlow 表 RLS、KMS、ObjectStorage S3、内部 LLM 计费分类、付费分层 | 10–16 周 | +| **1** | 第一批付费客户(10–50 付费 / 500–2000 free) + **1-2 业务系统集成(含自研 web 页面)** | **Postgres + Quota + Headless API(Pattern A backend 代理 + Pattern B browser 直连)必落**;workspace 全链路 + 入口强校验;AioSandbox 收紧 | 10–15 周 | +| **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 周 | +> **并行轨道**:Stage 1 同时承载"第一批付费 SaaS 客户"和"业务系统集成"两条产品线,共用 workspace + auth 基座;详见 [headless-api-track.zh-CN.md](./headless-api-track.zh-CN.md)。On-prem 部署形态在 Stage 0 schema 设计层面就已兼容,Stage 1 末加部署文档即可。 + **核心原则**:每期只做下一档规模真正逼出来的事;做了就不回头的"不可逆决策"集中在 Stage 0/1,避免后期重写。 --- @@ -39,6 +41,7 @@ | **`workspaces` 表 + 自动建 1 人 workspace** | 每个新注册用户自动获得 1 个 workspace;用户 = workspace owner。这是后面所有租户改造的底座。 | | **`workspace_id` 列加到现有 SQLite 表** | `threads_meta` / `runs` / `feedback` / `users` 加 `workspace_id`。**不可逆决策**——SQLite 上加列后再迁 Postgres 比直接在 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 临时补。 | | **CLI / admin UI 的"workspace 管理"基础** | platform admin 能看 workspace 列表、暂停/删除某个 workspace(防止滥用第一时间反应)。 | @@ -70,37 +73,64 @@ --- -## Stage 1 — 第一批付费客户(freemium 真上线) +## Stage 1 — 第一批付费客户 + 业务系统集成 -**触发**:Stage 0 跑稳 + 拿到第一批付费用户(10–50 付费 / 500–2000 free)。 -**退出**:能放心让媒体/产品社区曝光,不会被白嫖跑偏。 -**时间盒**:6–10 周 +**触发**: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 周) -### 必做 +> 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)。 + +### 必做(付费 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)`。 | ADR-003 §4.3 | +| **`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 | | **Stripe 对接(基础订阅)** | 单档付费先;webhook 同步到 `workspace_quotas.plan` 字段。 | — | | **AioSandbox 出网收紧** | egress 白名单(默认禁出网,按需放行)+ cgroup CPU/memory 限额。**不上 K8s**——AioSandbox 加这两个补丁就能撑到 Stage 3。 | ADR-002(轻量版) | | **Sandbox 资源 quota** | 每 workspace 的"沙箱 CPU 秒/月"也进 quota(防止白嫖跑挖矿)。 | ADR-003 §4.3 | | **基础监控** | per-workspace token 用量曲线、quota 命中率、异常用量告警。 | — | +### 必做(Headless API 轨道 - Pattern A:业务 backend 代理) + +| 改动 | 说明 | 关联文档 | +|---|---|---| +| **API Key + Service Account 仓储** | `service_accounts` / `api_keys` 表(schema Stage 0 已加)+ 仓储 + 哈希存储。 | headless-api §2 | +| **APIKeyAuthBackend + AuthMiddleware 双路径** | bearer 走 SA 路径、cookie 走 user 路径;CSRF middleware 在 bearer 路径 skip。 | headless-api §2 | +| **External User ID 透传 + ghost user** | `external_users` 表(schema Stage 0 已加)+ `X-External-User-Id` header 解析;`identity_mode` 三态语义。 | headless-api §3 | +| **`/api/v1/` 版本化** | mount prefix 切换;旧 `/api/*` 兼容转发并加 deprecation header。**早做便宜**。 | headless-api §4 | +| **`@require_permission` 装饰器升级** | 同时支持 cookie user 路径和 SA + scope 校验;`owner_check` 扩为 enum(`workspace_or_user`)。 | headless-api §2 | +| **Per-API-key rate limit(基础版)** | sliding window,存 Postgres;分档默认配 free/pro/team。 | headless-api §5 | +| **API key 管理(CLI 优先 + UI 跟进)** | workspace owner / admin 创建 SA + key + 选 identity_mode;CLI 必有,UI 在前端 workspace settings 跟。 | headless-api §2 | +| **Idempotency keys**(推荐) | 业务系统重试不重复建 thread/run。 | headless-api §5 | + +### 必做(Headless API 轨道 - Pattern B:自研 web 浏览器直连) + +| 改动 | 说明 | 关联文档 | +|---|---|---| +| **`POST /api/v1/auth/exchange-token` endpoint** | 业务系统 backend 用 API key + `external_user_id` 换 5-15 min 短期 JWT。 | headless-api §3.5 | +| **`ServiceTokenAuthBackend`(AuthMiddleware 第三条路径)** | 验短期 JWT 签名 + `iss=deerflow,typ=service` + SA 当前状态 + scope 子集合法。 | headless-api §3.5 | +| **`workspaces.allowed_origins` 列 + WorkspaceAwareCORSMiddleware** | per-workspace 配置允许的 origin;浏览器请求过 CORS preflight。 | headless-api §3.5 | +| **SSE 在 CORS 跨域下的 streaming 验证** | 写一份业务方对接示例(HTML + 原生 EventSource) | headless-api §3.5 | + ### 不做(推迟到 Stage 2+) - ❌ RLS — Stage 2(应用层 + 入口校验先撑着) -- ❌ KMS — Stage 2(先用环境变量管理 key) +- ❌ KMS — Stage 2(先用环境变量管理 key + 平台 key 散列入 DB) - ❌ ObjectStorage S3 — Stage 2(先继续本地文件 + 备份脚本) - ❌ K8s sandbox — Stage 3 - ❌ BYO key — Stage 3(先全部用平台 key + quota) - ❌ 团队 invitation 流程 — Stage 2(除非有团队客户先到) - ❌ 多档付费 — Stage 2 +- ❌ Webhook outbound — Stage 2(先轮询) +- ❌ 分维度 / 分档 rate limit — Stage 2 -### 关键 PR 顺序 +### 关键 PR 顺序(双轨) +**轨道 A:付费 SaaS** 1. Postgres 切换(dev 双驱动 → 生产灰度 → 全切) 2. `workspace_quotas` / `workspace_usage_daily` 表 + 仓储 3. `TokenUsageMiddleware` 升级为持久化(参 audit + ADR-003 §4.3) @@ -109,6 +139,33 @@ 6. AioSandbox egress 白名单 + 资源限额 7. 监控/告警接入 +**轨道 B:Headless API Pattern A**(与 A 并行;步骤 1-2 必须先完成轨道 A 的 1) +1. `service_accounts` / `api_keys` / `external_users` 仓储(schema 已在 Stage 0 加上) +2. `APIKeyAuthBackend` + `AuthMiddleware` 双路径(cookie + bearer) +3. CSRF middleware skip on bearer +4. `/api/v1/` mount prefix 切换 + 旧路径兼容转发 +5. `external_user_id` 透传机制 +6. `service_accounts.identity_mode` 三态行为分支 +7. `@require_permission` 升级 + scope 校验(依赖轨道 A 的 quota 完成) +8. 基础 rate limit +9. API key 管理 CLI + UI +10. Idempotency keys(可选) + +**轨道 C:Headless API Pattern B**(依赖轨道 B 的 1-7 完成;建议 Stage 1 末 1-2 周) +1. `workspaces.allowed_origins` 列 + workspace settings UI 的 origin 管理 +2. `WorkspaceAwareCORSMiddleware`(在 AuthMiddleware 之前) +3. `POST /api/v1/auth/exchange-token` endpoint + `ServiceTokenPayload` 设计 +4. `ServiceTokenAuthBackend`(AuthMiddleware 第三条路径) +5. SSE 跨域 streaming 验证 + 业务方对接示例(HTML + JS) + +### Go/No-Go 进入 Stage 2 + +- 月活付费用户 ≥ 50 **或** 月活免费用户 ≥ 1000 +- 1-2 个业务系统集成完成 go-live 并稳定运行 ≥ 1 个月 +- 出现一次"差点超额"事件(quota 在悲观预扣下还是漏了一次) +- 文件存储或 secret 管理出现一次手忙脚乱(备份遗漏 / key 误提交等) +- 业务系统开始要求 webhook 推送(不再满足于轮询) + ### Go/No-Go 进入 Stage 2 - 月活付费用户 ≥ 50 **或** 月活免费用户 ≥ 1000 @@ -139,7 +196,9 @@ | **skill 上传权限收口** | workspace owner / admin 才能上传 skill 包;其他成员只能 enable/disable + 填自己的 config。 | ADR-004 §5.4 | | **内部 LLM 计费分类** | Memory/Title/Summarization 三类 LLM 调用都计入 workspace 用量,区分 `usage_category`。 | ADR-006 §2.5 | | **role 扩到 owner/admin/member** | 团队 workspace 出现 → RBAC 真正发挥作用;`@require_permission` 装饰器升级。 | ADR-004 §5.4 | -| **基础 audit log** | 写入业务 DB(暂不拆分),关键操作(quota 改、role 改、删 workspace)记录。 | — | +| **基础 audit log** | 写入业务 DB(暂不拆分),关键操作(quota 改、role 改、删 workspace、API key 创建/吊销)记录。 | — | +| **Webhook outbound** | `webhook_subscriptions` 表 + 重试机制;业务系统订阅 thread 完成 / run 失败 / quota 触底。Stage 1 推迟来的,此时业务系统已经开始要。 | headless-api §1 | +| **API key 分维度 rate limit** | per-endpoint / per-LLM / per-sandbox 复合限速;引入 Redis。 | headless-api §5 | ### 不做(推迟到 Stage 3+) @@ -255,14 +314,14 @@ | Stage | 触发 | 时间盒 | 累计 | |---|---|---|---| | 0 | 现在 | 3–4 周 | 1 个月 | -| 1 | 首批付费 | 6–10 周 | 4 个月 | -| 2 | 增长期 | 10–16 周 | 8 个月 | -| 3 | 成熟期 | 16–26 周 | 14 个月 | +| 1 | 首批付费 + 业务系统集成(含自研 web 直连) | 10–15 周(headless API Pattern A+B 并行 4-5 周) | 4-5 个月 | +| 2 | 增长期 | 10–16 周 | 8-9 个月 | +| 3 | 成熟期 | 16–26 周 | 14-16 个月 | | 4 | 企业客户 | 单客户 4–8 周 | + 按需 | -**全功能落地**:~14 个月(Stage 0–3 累计),不含 Stage 4 enterprise 特性。 -**最小可付费**(Stage 0 + 1):~4 个月。 -**风险可控的增长**(Stage 0 + 1 + 2):~8 个月。 +**全功能落地**:~14-16 个月(Stage 0–3 累计),不含 Stage 4 enterprise 特性。 +**最小可付费 + 业务系统集成**(Stage 0 + 1):~4-5 个月。 +**风险可控的增长**(Stage 0 + 1 + 2):~8-9 个月。 ---