Files
ZY-Agent/docs/multi-tenant-redesign/02-rollout/headless-api-track.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

566 lines
28 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 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 页面。
> **商业形态**:与 [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。
---
## 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` 客户端可直接接 |
| StreamingSSE | `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_<long-lived API key>`
- Pattern B`Authorization: Bearer eyJ<short-lived JWT>`
---
## 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 ExchangePattern 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 B7-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 内的 rolemember / 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 skipbearer 路径不要 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. 核心设计:两种身份模式
### 模式 Aservice 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 = <SA.id>
threads_meta.workspace_id = <SA.workspace_id>
```
memory 也是 service account 共享的(`workspaces/{wid}/service_accounts/{sa_id}/memory.json`)。
### 模式 Bexternal_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 = <SA.workspace_id>
threads_meta.service_account_id = <SA.id>
threads_meta.external_user_id = <external_users.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 BBrowser 直连 + 短期 JWT 交换)
### 为什么需要
业务系统自研的 web 页面如果走 Pattern ABackend 代理),他们 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_idDeerFlow 内部 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 JWTtyp=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_idcollapsed 模式)
- 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 完整 flowauthorize / 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: <client-generated-uuid>
SELECT idempotency_records WHERE (api_key_id, key=...) AND created_at > NOW() - 24h
命中 → 直接返回上次 response200 + body 原样)
未命中 → 处理请求 → 写 idempotency_records + 返回
```
只在写操作(POST / PUT / PATCH / DELETE)支持;GET 不需要。
---
## 6. 与 Stage 1 的整合
把 headless API MVP 包并入 Stage 1**时间盒 8-13 周**Postgres 切换已前移到 Stage 0,原"6-10 周 + 4-5 周 headless = 10-15"减去 Postgres 的 ~2 周)。
### 修订后 Stage 1 必做项
| 改动 | 类型 | 估工 |
|---|---|---|
| Quota 系统 + TokenUsage 持久化 | 原 Stage 1Postgres 已就绪) | 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 管理 UIworkspace settings 内)** | **新增**(最低限:CLI 也行)| SCLI/ MUI|
| **`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 不含 PostgresM+M+M=3M+ 新增 Pattern AM+M+XS+M+S+S+S+S=4M+ 新增 Pattern BS+S+M=2M= 约 9M ≈ 8-13 周。Pattern B 依赖 Pattern A 完成,建议放 Stage 1 末。
### 修订后 Stage 1 PR 顺序
**轨道一:付费 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. `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 是主线)
> **口径**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 |
| KMS | AWS / 阿里云 KMSStage 2 后落) | 客户自带 KMSHashiCorp 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` 也加上不阻塞 |