# CDK Quota and Task API v1.5.0

v1.5.0 是注册机项目的统一接入契约。它保留 v1.4 的动态渠道、项目级 HMAC、绑定和注册租约接口，同时增加“项目登录会话”。项目用户只需要在本地项目界面输入 CDK；项目后端使用自己加密保存的项目客户端密钥完成签名，运行时使用不透明会话，不必把 CDK 反复传给 worker。

在线入口：

- OpenAPI：`GET /api/v1/openapi.yaml`
- 最新对接文档：`GET /api/v1/integration-guide/latest`
- 固定版本文档：`GET /api/v1/integration-guide/v1.5.0`
- 项目契约：`GET /api/v1/projects/{project_key}/integration`
- 能力发现：`GET /api/v1/projects/{project_key}/capabilities`

## 0. 机器能力发现（必读）

项目接入不得通过抓取 WebUI、复制云端脚本或比较 OpenAPI `paths` 字符串来判断版本。完成一次只读的 CDK 状态检查后，项目后端必须调用：

```text
GET /api/v1/projects/{project_key}/capabilities
Authorization: Bearer <CDK>
```

该接口与项目契约使用同一份云端定义，返回 `revision=project-capabilities-v1`。客户端只依据 `project_access_allowed`、`feature_flags`、`required_endpoint_ids`、`authentication` 和 `endpoints[].path_template` 选择调用链；未知 `revision` 必须停止并提示升级，不能回退到本地模式或伪造授权。

关键结构（示例不含任何真实凭证）：

```json
{
  "ok": true,
  "api_version": "1.5.0",
  "contract_revision": "project-contract-v1.5",
  "project": {
    "project_key": "registration_core",
    "integration_mode": "registration"
  },
  "capabilities": {
    "revision": "project-capabilities-v1",
    "project_key": "registration_core",
    "project_access_allowed": true,
    "feature_flags": {
      "registration_login": true,
      "registration_sessions": true,
      "registration_leases": true,
      "persistent_identity": true,
      "replaceable_entitlement": true
    },
    "required_endpoint_ids": [
      "project_contract",
      "cdk_status",
      "project_capabilities",
      "registration_client_preflight",
      "registration_login",
      "registration_session_status",
      "registration_lease_reserve",
      "registration_lease_commit",
      "registration_lease_release"
    ],
    "path_rules": {
      "project_key_placeholder": "{project_key}",
      "must_not_parse_openapi_paths": true,
      "unknown_revision": "fail_closed"
    }
  }
}
```

`GET /api/v1/cdk/status?project_key=...` 也会返回同一份 `capabilities`。不带 `project_key` 的状态查询只返回通用发现提示，`cdk.project_access_allowed` 为 `null`，不代表已获得任何项目授权。

v1.4 客户端可以继续调用原有 `activate`、`authorize`、`workspace` 和 `leases` 路径，不需要立即升级。新项目和需要“账号保留、卡到期退出、换卡恢复”的项目应使用本版本。

## 1. 统一业务模型

每个注册机项目都有独立的 `project_key`、项目客户端密钥、注册账号、绑定、会话和任务数据。数据不能跨项目读取或复用。

CDK 可见项目由云端项目契约决定：普通项目卡只可见所属项目；明确配置 `registration_scope=global` 的管理员卡和平台总管理员卡可见全部启用的 registration 项目，但每个项目仍生成独立账号、绑定和会话。

账号的持久身份为 `persistent_identity = project_key + username + client_user_ref`。换卡只替换绑定，不改变该持久身份对应的账号和配置。

```text
本地用户登录 + 输入 CDK
    -> 项目后端以项目客户端密钥签名 login
    -> 云端创建/恢复账号与 CDK 绑定，并在此刻开始时效
    -> 云端仅返回一次不透明 registration_access_token
    -> 本地加密保存会话，定时 heartbeat
    -> 实际注册前申请 lease
    -> 成功 commit；失败/取消/超时 release
```

### 时效卡的定义

1. 新开时效 CDK 的 `activated_at` 为 `null`，查询状态、项目契约和预检不会开始计时。
2. 第一次成功 `registration/login`、旧 `registration/activate` 或真实 v1.4 注册租约认证才开始计时。
3. 到期后，云端把 CDK、绑定和访问会话标为过期；本地项目必须立即退出业务授权，但不得删除本地用户、注册账号、模板、配置、历史任务或已注册账号。
4. 使用新 CDK 对同一个 `username + client_user_ref` 再次 `registration/login`，云端会保留原 `account_id`、建立新绑定、将旧绑定标记为 `replaced`，并返回新会话。
5. 会话不是租约。会话只表示项目登录授权；租约只表示一次真实注册单元的额度与并发占位。

## 2. 凭证边界

| 凭证 | 保存位置 | 能做什么 | 不能做什么 |
|---|---|---|---|
| `super_admin_api_key` | 仅管理端加密配置 | 开卡、续期、换卡、禁用、审计、项目客户端密钥管理 | 注册业务登录、注册租约 |
| 项目 CDK | 用户输入后，本地加密保存 | 该项目登录、额度、并发 | 跨项目、全局管理接口 |
| `admin_cdk` + `registration_scope=global` | 受控本地加密配置 | 所有启用的 registration 项目登录 | 开卡、账本、审计、队列管理 |
| 平台总管理员 CDK | 受控本地加密配置 | 所有启用的 registration 项目登录 | `super_admin_api_key` 管理接口 |
| 项目客户端密钥 | 仅项目后端加密配置 | HMAC 证明调用来自对应项目 | 用户登录、跨项目调用 |
| `registration_access_token` | 仅项目后端加密配置或内存 | 当前账号的会话、注册租约 | 管理接口、其他账号或项目 |

不得把以上任意明文写入浏览器前端状态、普通设置、日志、任务详情、审计 payload、Git、导出文件或错误信息。

## 3. 项目固定配置

每个项目固定保存以下非用户输入配置：

```text
CDK_API_BASE_URL=https://<cdk-host>
PROJECT_KEY=<项目固定标识>
REGISTRATION_CLIENT_KEY=<当前项目客户端密钥，仅加密保存>
```

项目示例：

```text
GPTFREE 注册机：project_key=registration_core
生图注册机：project_key=image_generation_registration
```

普通 CDK 与项目客户端密钥必须一一按项目隔离。GPTFREE 的普通 CDK 或客户端密钥提交到生图项目时必须返回 `403`，不能自动映射或回退到另一项目。

## 4. HMAC 签名

所有 `/registration/*` 请求都使用项目客户端密钥签名。签名不是用户输入，也不是 CDK。

请求头：

```text
X-Registration-Client-Key: <项目客户端密钥>
X-Registration-Timestamp: <Unix 秒>
X-Registration-Signature: <HMAC-SHA256 十六进制>
```

签名原文：

```text
timestamp.METHOD.path.sha256(canonical_json_body)
```

规范 JSON：UTF-8、键名排序、无空格。没有业务字段的请求也必须发送 `{}` 并对 `{}` 签名。`path` 只包含 URL 路径，不带域名、查询串或 fragment。

```python
import hashlib
import hmac
import json
import time

def registration_headers(client_key: str, method: str, path: str, body: dict):
    canonical = json.dumps(body or {}, ensure_ascii=False, sort_keys=True, separators=(",", ":"))
    digest = hashlib.sha256(canonical.encode("utf-8")).hexdigest()
    timestamp = str(int(time.time()))
    text = f"{timestamp}.{method.upper()}.{path}.{digest}"
    signature = hmac.new(client_key.encode("utf-8"), text.encode("utf-8"), hashlib.sha256).hexdigest()
    return {
        "X-Registration-Client-Key": client_key,
        "X-Registration-Timestamp": timestamp,
        "X-Registration-Signature": signature,
    }
```

## 5. 首次登录、续期和换卡

### 5.1 查询但不启用

```text
GET /api/v1/cdk/status?project_key={project_key}
Authorization: Bearer <CDK>

GET /api/v1/projects/{project_key}/integration
Authorization: Bearer <CDK>
```

两条接口只能用于预检和显示；不会开始时效。项目应读取 `status`、`registration_scope`、`is_platform_master`、`expires_at`、`remaining_quota`、`max_concurrency` 和 `available_slots`。

带 `project_key` 查询状态时，中心返回最终项目访问判定。平台总管理员 CDK 或 `registration_scope=global` 的 `admin_cdk` 必须得到：

```json
{
  "cdk": {
    "project_key": "registration_core",
    "card_project_key": "platform_admin",
    "requested_project_key": "registration_core",
    "effective_project_key": "registration_core",
    "project_access_allowed": true,
    "role": "admin_cdk",
    "registration_scope": "global",
    "quota_mode": "unlimited",
    "is_platform_master": true
  }
}
```

`project_key` 和 `effective_project_key` 表示本次请求的有效项目；`card_project_key` 只表示卡片签发来源。客户端不得根据 CDK 前缀、长度或 `card_project_key` 判断权限，只能使用中心返回的 `project_access_allowed` 以及项目契约中的 `authentication.project_access.allowed`。

项目契约同步返回：

```json
{
  "authentication": {
    "project_access": {
      "allowed": true,
      "requested_project_key": "registration_core",
      "card_project_key": "platform_admin",
      "cross_project_access": true
    },
    "cdk_role": "admin_cdk",
    "registration_scope": "global",
    "is_platform_master": true
  }
}
```

### 5.2 项目客户端密钥预检

项目客户端密钥不是用户 CDK，也不是 `super_admin_api_key`。它是每个 registration 项目独立的一条 HMAC 密钥，由管理员接口创建或轮换，明文只在成功响应中显示一次；项目服务必须把它放在后端加密配置或部署密钥中，不能让用户在 CDK 输入框中填写，也不能下发到浏览器。

项目首次读取 `GET /api/v1/projects/{project_key}/capabilities` 或项目契约时，响应中的 `authentication.registration.client_key` 会同时给出不含密钥内容的中央配置状态：

| `provisioning_status` | 含义 | 下一步 |
|---|---|---|
| `not_provisioned` | 中央服务还没有该项目的有效客户端密钥 | 使用 `super_admin_api_key` 创建项目客户端密钥 |
| `provisioned` | 中央服务已有有效客户端密钥 | 将同一条明文安全注入项目后端，再执行预检 |
| `not_required` | 当前项目不是 registration 项目 | 不需要项目客户端 HMAC |

`server_ready` 只表示中央服务已经完成配置，不代表调用项目已经注入环境变量。`next_action` 只允许为 `create_project_client_key`、`inject_project_client_key` 或 `not_required`。项目后端应先读取这三个字段再显示唯一的修复提示，不要盲目重试，也不要把它误报为 CDK 过期。

在调用统一登录前，项目后端必须先调用以下只读接口：

```text
POST /api/v1/projects/{project_key}/registration/client-preflight
Authorization: Bearer <项目 CDK 或平台总管理员 CDK>
X-Registration-Client-Key: <当前项目客户端密钥>
X-Registration-Timestamp: <Unix seconds>
X-Registration-Signature: <HMAC-SHA256 hex>
Content-Type: application/json

{}
```

签名路径只包含 URL path，空请求体按 `{}` 计算。该预检只验证项目客户端密钥、CDK 项目权限、状态和有效期，不创建账号、绑定、会话，不开始 CDK 时效，不预留额度，也不申请注册租约。平台总管理员 CDK 仍然必须使用目标项目自己的客户端密钥；`baidu599` 这类平台总管理员 CDK 不能替代项目客户端密钥。

成功响应只返回安全状态字段：

```json
{
  "ok": true,
  "project": {"project_key": "image_generation_registration", "integration_mode": "registration"},
  "client_auth": {"status": "ok", "scope": "project", "project_key": "image_generation_registration", "fingerprint": "1a2b3c4d5e6f7890"},
  "cdk": {
    "status": "active",
    "role": "admin_cdk",
    "registration_scope": "global",
    "is_platform_master": true,
    "expires_at": null,
    "activated_at": null,
    "project_access_allowed": true
  },
  "lifecycle": {"starts_validity": false, "binding_created": false, "quota_reserved": 0}
}
```

若预检返回 `registration_client_auth_required`，表示目标项目客户端密钥没有注入或没有发送；这不是 CDK 过期。若返回 `registration_client_key_scope_denied`，表示拿了其他项目的客户端密钥，必须换成当前 `project_key` 的密钥。只有预检成功后才能继续 `registration/login`。

### 5.3 统一登录

```text
POST /api/v1/projects/{project_key}/registration/login
```

请求体：

```json
{
  "cdk": "<用户本次输入的 CDK>",
  "username": "local-user",
  "client_user_ref": "stable-project-user-001",
  "device_summary": "optional-safe-device-label"
}
```

规则：

- `client_user_ref` 必须是本地用户的稳定 ID，不能每次启动随机生成。
- 首次成功登录开始时效并创建远端 `account_id` 和 `binding_id`。
- 同一有效绑定重复登录不会重复开始时效或创建第二个账号。
- 旧卡已经到期时，同一用户名和 `client_user_ref` 使用新卡登录会自动换绑，保留账号和配置。
- 旧卡仍有效时，提交另一张卡会返回 `registration_user_bound`；项目不能偷偷替换有效权益。

成功响应关键字段：

```json
{
  "ok": true,
  "account": {
    "account_id": "regacct_xxx",
    "project_key": "registration_core",
    "login_allowed": true,
    "config_version": 1
  },
  "binding": {
    "binding_id": "regbind_xxx",
    "cdk_role": "customer_cdk",
    "registration_scope": "project",
    "is_platform_master": false,
    "expires_at": "..."
  },
  "registration_access_token": "regsess_live_...",
  "show_once": true,
  "session": {
    "active": true,
    "project_key": "registration_core",
    "binding_id": "regbind_xxx",
    "expires_at": "..."
  }
}
```

`registration_access_token` 只在这一次响应中返回。项目后端必须立刻加密保存；浏览器前端只接收账号状态和到期时间，不能接收 token。

### 5.3 运行会话

```text
POST /api/v1/projects/{project_key}/registration/session/status
POST /api/v1/projects/{project_key}/registration/session/heartbeat
POST /api/v1/projects/{project_key}/registration/session/logout
Authorization: Bearer <registration_access_token>
```

三条接口同时需要项目 HMAC。`status` 可用于显示到期和换卡入口；`heartbeat` 只在授权仍有效时返回成功；`logout` 可幂等清理本地登录状态。

项目应在用户登录后立即调用一次 `session/status`，运行中每 60 秒调用一次 `session/heartbeat`。收到以下任意错误时：

```text
registration_cdk_expired
registration_session_closed
registration_session_invalid
registration_project_access_denied
registration_scope_denied
```

必须停止新任务、释放未完成租约、清除本地会话并跳到“输入新 CDK”页面。账号和配置仍保留。

## 6. 实际注册额度与租约

v1.4 的租约接口保持不变，v1.5 推荐使用访问会话作为 Bearer：

```text
POST /api/v1/projects/{project_key}/registration/leases
GET  /api/v1/projects/{project_key}/registration/leases/{lease_id}
POST /api/v1/projects/{project_key}/registration/leases/{lease_id}/heartbeat
POST /api/v1/projects/{project_key}/registration/leases/{lease_id}/commit
POST /api/v1/projects/{project_key}/registration/leases/{lease_id}/release
Authorization: Bearer <registration_access_token>
```

旧客户端继续使用 `Authorization: Bearer <CDK>` 也兼容。新项目不应让 worker 保存或重复传递 CDK。

申请请求：

```json
{
  "binding_id": "regbind_xxx",
  "client_user_ref": "stable-project-user-001",
  "task_source": "manual",
  "client_run_id": "stable-local-run-id",
  "units": 1,
  "lease_ttl_seconds": 120,
  "metadata": {
    "template_version": "v3",
    "task_source": "manual"
  }
}
```

租约流程：

1. 真实注册单元开始前才申请租约；登录、预检、队列展示不能申请。
2. `Idempotency-Key` 必须是本地稳定任务 ID 加尝试次数；相同键不能重复占位。
3. worker 启动前再次读取或 heartbeat 租约；不是 `reserved`/`running` 立即停止。
4. 仅在本地确认真实注册成功、账号已持久化后调用 `commit`。
5. 失败、取消、异常、网络超时、卡到期和进程重启都调用 `release`；若没有机会调用，云端租约超时自动释放。
6. 普通 CDK 仅在 `commit` 时扣额度。管理员 CDK 和平台总管理员 CDK 不扣普通额度，但所有租约都记录审计。

访问会话只能操作自己的 `binding_id` 和租约，即使底层是全局管理员 CDK，也不能通过会话读取或操作另一账号的租约。

## 7. 本地存储与任务快照

本地项目至少分开保存：

```text
local_users
registration_accounts
cdk_bindings
encrypted_project_credentials
encrypted_access_sessions
registration_tasks
```

每个任务创建时写入不可变快照：

```text
template_version
template_snapshot
task_source
cdk_binding_id
cdk_config_version
registration_lease_id
client_run_id
```

页面随后修改模板、代理、邮箱、Remail 或线程数不能改变已经创建的任务。CDK 授权只限制账号登录、额度和最大并发，不能覆盖项目自己的注册模板、代理池、邮箱逻辑或计价逻辑。

实际并发应为：

```text
min(项目基础并发, 自动补号线程配置, CDK max_concurrency, 当前可用租约槽位)
```

## 8. 稳定错误与处理

| 错误码 | 本地处理 |
|---|---|
| `registration_cdk_expired` | 退出业务授权，保留账号，提示换卡登录 |
| `registration_session_closed` / `registration_session_invalid` | 清除会话；若 CDK 仍有效可重新 login，否则提示换卡 |
| `registration_project_access_denied` / `registration_scope_denied` | 拒绝，不回退到其他项目或其他 CDK |
| `registration_client_key_scope_denied` | 项目配置错误，停止服务端调用并通知管理员修复项目密钥 |
| `registration_signature_invalid` / `registration_timestamp_invalid` | 本地签名或时钟错误，禁止伪造成功 |
| `quota_exhausted` | 暂停新增注册单元，保留未执行缺口 |
| `concurrency_full` | 排队并按 `retry_after_seconds` 重试，不重复申请或扣费 |
| `registration_lease_expired` | 停止对应 worker，重新按新幂等键申请租约 |

网络错误和 5xx 只可按 `retryable` 重试；不得把网络失败、签名失败、未配置客户端密钥或无 CDK 当作授权成功。

## 9. 新项目接入清单

1. 管理端创建 registration 项目，并为该项目创建项目客户端密钥。
2. 本地项目仅在后端加密保存 `CDK_API_BASE_URL`、`PROJECT_KEY`、项目客户端密钥、当前 CDK 和访问会话。
3. 用户输入 CDK 后调用 `registration/login`；将账号、绑定、会话保存到本地。
4. 所有用户登录、手动注册、自动补号、重试、worker claim、队列恢复都先检查会话；没有有效会话绝不执行注册。
5. 每个真实注册单元通过租约 `reserve -> heartbeat -> commit/release` 闭环。
6. 到期、禁用、撤销时停止业务，保留账号与数据；新 CDK 使用同一稳定本地用户 ID 重新 login。
7. 使用本地 mock 覆盖：首次登录、重复登录、到期、禁用、换卡、项目隔离、平台总管理员跨项目、租约成功、失败释放、幂等回放和服务重启恢复。

## 10. v1.4 到 v1.5 迁移

现有 v1.4 项目无需停机重写：

1. 保留已有 HMAC 实现和原 `activate`/`authorize`/`workspace` 逻辑。
2. 新增 `registration/login` 作为唯一用户输入 CDK 的入口。
3. 将 worker 的 Bearer 从明文 CDK 切换为 `registration_access_token`。
4. 在本地登录守卫、任务创建、worker 启动和心跳处调用 `session/status` 或 `session/heartbeat`。
5. 保留旧租约路径和任务快照；只把认证来源从 CDK 迁移到会话。
6. 新旧接口可并行运行，确认会话模式稳定后再停止本地旧授权缓存。

旧显式换卡接口 `POST /api/v1/projects/{project_key}/registration/rebind` 仍保留，用于现有 v1.4 项目；v1.5 推荐由同一个 `registration/login` 处理到期后的换卡。
