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>
566 lines
28 KiB
Markdown
566 lines
28 KiB
Markdown
# 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` 客户端可直接接 |
|
||
| 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_<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 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 = <SA.id>
|
||
threads_meta.workspace_id = <SA.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 = <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 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: <client-generated-uuid>
|
||
↓
|
||
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,**时间盒 8-13 周**(Postgres 切换已前移到 Stage 0,原"6-10 周 + 4-5 周 headless = 10-15"减去 Postgres 的 ~2 周)。
|
||
|
||
### 修订后 Stage 1 必做项
|
||
|
||
| 改动 | 类型 | 估工 |
|
||
|---|---|---|
|
||
| Quota 系统 + TokenUsage 持久化 | 原 Stage 1(Postgres 已就绪) | 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 不含 Postgres(M+M+M=3M)+ 新增 Pattern A(M+M+XS+M+S+S+S+S=4M)+ 新增 Pattern B(S+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 / 阿里云 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` 也加上不阻塞 |
|