# 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 ` | `/keys`、`/usage`、`/user/*` | | 管理员 | `x-api-key: ` 或 `Authorization: Bearer ` | `/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, // USD,0 = 无限 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 也 401(token 被撤销)→ 返回前端 `401 sub2api_reauth_required`,前端弹窗让用户重新输密码,后端重调 `/auth/login` 重新拿到 token ### 6.3 各业务同步矩阵 | 动作 | 本地 | sub2api(admin token) | sub2api(用户 token) | |---|---|---|---| | 注册 | ①写 user(pending) | ②`POST /admin/users` 取回 `id` | ③`POST /auth/login` 拿首对 tokens
④成功后 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 key(44 字节 urlsafe base64) ``` 生成 Fernet key: ```bash python -c "from cryptography.fernet import Fernet; print(Fernet.generate_key().decode())" ``` `requirements.txt` 增加:`httpx>=0.27` `cryptography>=42`