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

28 KiB
Raw Blame History

Headless API · 业务系统集成轨道

写于 2026-05-09。承接 phased-rollout-by-scale.zh-CN.md

触发:现已有 1-2 个明确的业务系统集成需求,1-3 个月内要 demo / 调通。集成形态包括 IM channels(已支持)+ 业务系统自研 web 页面。 商业形态:与 phased-rollout-by-scale §0 一致——中心化 SaaS 主线schema / auth / quota 设计对 on-prem 友好(workspaces.id 映射到 self-host 安装),但 on-prem 不作为产品主线,仅按客户合同启用。 身份模式service account 折叠 + external_user_id 透传,两种都支持,按 endpoint 选。 集成 patternPattern 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 AAuthorization: Bearer dfk_live_<long-lived API key>
  • Pattern BAuthorization: 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

数据模型

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 装饰器升级

# 现有签名(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 颁发。

数据模型扩展

-- 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 设计

# 不复用现有 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 设计

# 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-tokensexternal_users.token_version bumpexchange-token 签发时把它写进 JWT,验证时比对)

第 3 条是 nice-to-have;MVP 不做,等业务方明确提需求再加。

Pattern B 不做的事(重要)

  • 让浏览器直接持有 API keydfk_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 全量改地址。

设计

# 现状
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. ServiceTokenAuthBackendAuthMiddleware 第三条路径)
  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 也合理
配额 / 计费 按 planStripe 同步 配额作为容量管理(防内部失控),不接 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 路由 路由层假设 cookie + JWT;本文档加了 bearer + API key 平行路径
adr-007 §8 Auth TokenPayload 扩 tid/role 已对齐;本文档新增 API key 路径不复用 JWT
adr-004 RBAC service_accounts.role 复用 owner/admin/member;不另建 role 体系
adr-003 §4.4 quota quota 写入按 SA 归属时只看 workspace_id;按 external_user 归属时多一维
phased-rollout-by-scale Stage 1 本文档 §6 给出 Stage 1 修订后必做项与 PR 顺序
stage-0-code-map Stage 0 schema 时把 service_accounts 也加上不阻塞