Files
superDreamFront/docs/sub2api_api.md
xuyong 35c0b7de16 integrate sub2api as upstream for auth/keys/usage via FastAPI BFF
Preserve local user table for superDream-specific features while syncing
user lifecycle, API key CRUD and usage queries through sub2api. Admin token
handles reads and user lifecycle; per-user tokens (Fernet-encrypted in DB)
handle key writes that admin endpoints do not expose.

Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
2026-04-17 21:23:08 +08:00

340 lines
12 KiB
Markdown
Raw Permalink 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.
# sub2api 接口对接规范
本文档记录 superDreamFront 与 sub2api 对接所使用的全部 HTTP 接口,以及 BFF 层superDreamFront FastAPI各服务的调用策略。
> 对应的 sub2api 源码位置:
> - 路由:`sub2api/backend/internal/server/routes/{auth,user,admin}.go`
> - 用户侧 handler`sub2api/backend/internal/handler/{auth_handler,api_key_handler,usage_handler,user_handler}.go`
> - 管理员 handler`sub2api/backend/internal/handler/admin/{user_handler,apikey_handler}.go`
> - DTO 类型:`sub2api/backend/internal/handler/dto/types.go`
---
## 1. 通用约定
### 1.1 Base URL
所有接口以 `/api/v1` 为前缀,完整地址由环境变量 `SD_SUB2API_BASE_URL` 决定,例如:
```
SD_SUB2API_BASE_URL = http://127.0.0.1:8080
→ 实际请求http://127.0.0.1:8080/api/v1/...
```
### 1.2 响应 envelope
所有响应统一包一层(来源 `internal/pkg/response/response.go`
```json
{
"code": 0, // 0 表示成功;非 0 为 HTTP 状态码
"message": "success",
"reason": "", // 仅错误时出现
"metadata": null, // 错误附加信息
"data": { ... } // 业务载荷
}
```
分页响应的 `data` 形状固定为:
```json
{
"items": [...],
"total": 1234,
"page": 1,
"page_size": 20,
"pages": 62
}
```
BFF 层建议在 `sub2api.client.request()` 中统一解 envelope
- `code == 0` → 返回 `data`
- `code != 0` → 抛自定义异常 `Sub2APIError(code, message, reason, metadata)`
### 1.3 认证方式
sub2api 有三类鉴权:
| 鉴权类型 | 携带方式 | 用途 |
|---|---|---|
| 公开 | 无 | 注册、登录、忘记密码、验证码 |
| 用户 JWT | `Authorization: Bearer <access_token>` | `/keys``/usage``/user/*` |
| 管理员 | `x-api-key: <admin_api_key>``Authorization: Bearer <admin_jwt>` | `/admin/*` |
> 管理员 API Key 在 sub2api 后台 `Admin 设置 → Admin API Key → 生成` 后拿到,写入 `SD_SUB2API_ADMIN_TOKEN`。
### 1.4 Turnstile
部分公开接口会校验 Cloudflare Turnstile。sub2api 管理员可在设置里关闭;本 BFF 暂不处理 Turnstile若 sub2api 启用会 400。
---
## 2. 认证(公开接口)
### 2.1 发送邮箱验证码
`POST /api/v1/auth/send-verify-code`
请求:
```json
{ "email": "user@example.com", "turnstile_token": "" }
```
响应:
```json
{ "message": "Verification code sent successfully", "countdown": 60 }
```
### 2.2 注册
`POST /api/v1/auth/register`
请求:
```json
{
"email": "user@example.com",
"password": "******", // min 6
"verify_code": "123456", // 若 sub2api 开启邮箱验证码注册
"turnstile_token": "",
"promo_code": "", // 可选注册优惠码
"invitation_code": "" // 可选邀请码
}
```
响应(同 `AuthResponse`
```json
{
"access_token": "...",
"refresh_token": "...",
"expires_in": 3600, // access 有效期(秒)
"token_type": "Bearer",
"user": { ...dto.User }
}
```
### 2.3 登录
`POST /api/v1/auth/login`
请求:
```json
{ "email": "user@example.com", "password": "******", "turnstile_token": "" }
```
若用户未开 2FA响应同注册。
若开启 2FA响应
```json
{ "requires_2fa": true, "temp_token": "...", "user_email_masked": "u**@e**.com" }
```
此时前端需再调 `POST /api/v1/auth/login/2fa`
```json
{ "temp_token": "...", "totp_code": "123456" }
```
### 2.4 刷新 Token
`POST /api/v1/auth/refresh`
```json
// 请求
{ "refresh_token": "..." }
// 响应
{ "access_token": "...", "refresh_token": "...", "expires_in": 3600, "token_type": "Bearer" }
```
注意sub2api 的 refresh_token **一次性使用、每次刷新会 rotate**BFF 取到新 refresh_token 要覆盖旧值。
### 2.5 登出
`POST /api/v1/auth/logout` body 可选 `{"refresh_token": "..."}`,服务端会撤销该 token。
### 2.6 忘记 / 重置密码
- `POST /api/v1/auth/forgot-password` `{"email", "turnstile_token"}`
- `POST /api/v1/auth/reset-password` `{"email", "token", "new_password"}`
### 2.7 当前用户
`GET /api/v1/auth/me` 需要用户 JWT响应 `dto.User + {run_mode}`
---
## 3. API Key用户 JWT
路由前缀 **`/api/v1/keys`**(注意不是 `/api-keys`handler 注释里旧路径是误导)。
### 3.1 列表
`GET /api/v1/keys?page=1&page_size=20&sort_by=created_at&sort_order=desc&search=&status=&group_id=`
响应 `data`(分页 envelope
```json
{
"items": [ { ...dto.APIKey } ],
"total": 5, "page": 1, "page_size": 20, "pages": 1
}
```
`dto.APIKey` 关键字段(完整字段见 `types.go`
```ts
{
id: number,
user_id: number,
key: string, // 完整 key注意sub2api 确实返回明文完整 key不做截断
name: string,
group_id: number | null,
status: "active" | "inactive",
ip_whitelist: string[], ip_blacklist: string[],
last_used_at: string | null,
quota: number, // USD0 = 无限
quota_used: number,
expires_at: string | null,
rate_limit_5h: number, rate_limit_1d: number, rate_limit_7d: number, // 0=不限
usage_5h: number, usage_1d: number, usage_7d: number,
reset_5h_at, reset_1d_at, reset_7d_at: string | null,
created_at, updated_at: string,
group?: dto.Group
}
```
### 3.2 单个查询
`GET /api/v1/keys/:id``dto.APIKey`
### 3.3 新建
`POST /api/v1/keys`
```json
{
"name": "my-key",
"group_id": 1, // 可选
"custom_key": null, // 可选自定义 key
"ip_whitelist": [], "ip_blacklist": [],
"quota": 0, // 0=无限
"expires_in_days": null, // null=不过期
"rate_limit_5h": 0, "rate_limit_1d": 0, "rate_limit_7d": 0
}
```
响应:`dto.APIKey`
### 3.4 更新
`PUT /api/v1/keys/:id`(字段同上,全部可选;特殊:`expires_at=""` 清除过期,`reset_quota=true` 重置已用,`reset_rate_limit_usage=true` 重置限速计数)
### 3.5 删除
`DELETE /api/v1/keys/:id``{ "message": "API key deleted successfully" }`
### 3.6 用户可用分组 / 专属倍率
- `GET /api/v1/groups/available``dto.Group[]`
- `GET /api/v1/groups/rates``{ [groupID: number]: number }`
---
## 4. 用量(用户 JWT
### 4.1 列表
`GET /api/v1/usage?page=&page_size=&api_key_id=&model=&request_type=&stream=&billing_type=&start_date=YYYY-MM-DD&end_date=YYYY-MM-DD&timezone=Asia/Shanghai`
响应 `items: dto.UsageLog[]`(分页)。
`dto.UsageLog` 关键字段:
```ts
{
id, user_id, api_key_id, account_id: number,
request_id, model: string,
input_tokens, output_tokens, cache_creation_tokens, cache_read_tokens,
input_cost, output_cost, cache_creation_cost, cache_read_cost, total_cost, actual_cost,
request_type: string, // "chat"/"responses"/"messages"/"image" 等
stream: boolean,
duration_ms, first_token_ms: number | null,
image_count: number,
created_at: string
}
```
### 4.2 详情
`GET /api/v1/usage/:id``dto.UsageLog`
### 4.3 统计
`GET /api/v1/usage/stats?period=today|week|month``start_date&end_date`
→ 返回 `service.UsageStats`(求和 + tokens/requests/cost 各纬度汇总;字段见 sub2api `UsageStats` 定义)。
### 4.4 Dashboard
- `GET /api/v1/usage/dashboard/stats`
- `GET /api/v1/usage/dashboard/trend?granularity=day&start_date&end_date`
- `GET /api/v1/usage/dashboard/models?start_date&end_date`
- `POST /api/v1/usage/dashboard/api-keys-usage` body `{ "api_key_ids": [1,2,3] }`
---
## 5. 管理员接口x-api-key
superDream BFF 仅用到以下 admin 接口:
### 5.1 用户
- `POST /api/v1/admin/users` — 注册同步
```json
{ "email", "password", "username", "notes": "from superDream",
"balance": 0, "concurrency": 0, "allowed_groups": [] }
```
→ `dto.AdminUser`
- `PUT /api/v1/admin/users/:id` — 改密/改状态同步(字段均为可选 PATCH 语义)
- `DELETE /api/v1/admin/users/:id` — 删除同步
- `GET /api/v1/admin/users?search=email@x.com` — 查找用户(拿 `id` 用于后续调用)
- `GET /api/v1/admin/users/:id` — 详情
### 5.2 查用户 API Keys / 用量
- `GET /api/v1/admin/users/:id/api-keys?page=&page_size=&sort_by=&sort_order=` → `dto.APIKey[]`(分页)
- `GET /api/v1/admin/users/:id/usage?period=today|week|month` → `UsageStats`
### 5.3 能力缺口
sub2api **admin 路径不提供** 代用户创建/删除/修改 Key 的接口(`registerAdminAPIKeyRoutes` 仅 `PUT /admin/api-keys/:id` 改分组)。
→ Key 的写操作必须使用用户 JWT。
---
## 6. BFF 对接策略
### 6.1 本地 `User` 表扩展字段
| 字段 | 类型 | 说明 |
|---|---|---|
| `sub2api_user_id` | `BIGINT` | 对应 sub2api 的 `dto.User.id` |
| `sub2api_refresh_token_enc` | `TEXT` | Fernet 加密后的 refresh_token |
| `sub2api_access_token` | `TEXT` | 当前有效的 access_token内存级缓存即可但写库便于重启复用 |
| `sub2api_access_expires_at` | `DATETIME` | access_token 过期时间UTC |
### 6.2 鉴权 → Token 获取顺序
每次 BFF 需要代理到 sub2api **用户接口** 时:
1. 如本地缓存 access_token 未过期(预留 60s 余量),直接用
2. 否则用 Fernet 解密 refresh_token调 `/auth/refresh` 换新rotate 后写回)
3. 若 refresh 也 401token 被撤销)→ 返回前端 `401 sub2api_reauth_required`,前端弹窗让用户重新输密码,后端重调 `/auth/login` 重新拿到 token
### 6.3 各业务同步矩阵
| 动作 | 本地 | sub2apiadmin token | sub2api用户 token |
|---|---|---|---|
| 注册 | ①写 userpending | ②`POST /admin/users` 取回 `id` | ③`POST /auth/login` 拿首对 tokens<br>④成功后 commit 本地;任一失败整体回滚 |
| 登录 | ①校验本地密码 → 签本地 JWT | — | ②同步调 `/auth/login` 刷新存储 refresh_token |
| 改密 | ①更新本地 hash | ②`PUT /admin/users/:id` | ③用新密码调 `/auth/login` 替换 token |
| 删用户 | ①级联删 | ②`DELETE /admin/users/:id` | — |
| `GET /keys` | — | `GET /admin/users/:id/api-keys` | — |
| `POST/PUT/DELETE /keys` | — | — | `/keys` 系列 |
| `GET /usage/*` | — | `GET /admin/users/:id/usage` | 兜底:若 admin 接口粒度不够,回退到用户 token 调 `/usage` |
| Dashboard 统计 | 本地聚合 sub2api 返回 | `/admin/users/:id/usage` | `/usage/dashboard/*` |
### 6.4 前端字段替换
替换 `frontend/src/services/` 下的 TS interface使其与 `dto.APIKey` / `dto.UsageLog` / `dto.User` 完全一致(字段名用 snake_case 保留,不转 camelCase避免映射层。具体列在实现阶段列出 diff。
### 6.5 错误映射
| sub2api 返回 | BFF 抛出 | 前端显示 |
|---|---|---|
| `code=400` | `HTTPException 400` + `reason` | 表单校验提示 |
| `code=401 INVALID_TOKEN/TOKEN_EXPIRED` | 触发 refresh 流;失败抛 `401 sub2api_reauth_required` | 弹框"请重新登录" |
| `code=403` | `403` | "没有权限" |
| `code=404` | `404` | "资源不存在" |
| `code>=500` | `502 upstream_error` + 日志 | "上游服务异常" |
| 网络异常 | `504 upstream_timeout` | "请求超时" |
---
## 7. 新增配置项
写入 `app/config/settings.py`
```python
sub2api_base_url: str = "http://127.0.0.1:8080"
sub2api_admin_token: str = "" # sub2api Admin API Key
sub2api_request_timeout: float = 10.0 # 秒
sub2api_turnstile_bypass: bool = True # 是否绕过 turnstile需 sub2api 端也关)
token_encryption_key: str = "" # Fernet key44 字节 urlsafe base64
```
生成 Fernet key
```bash
python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())"
```
`requirements.txt` 增加:`httpx>=0.27` `cryptography>=42`