IntelliPicHub HTTP API
面向第三方 / cron 任务的实时参考文档。后端每个挂了
@RequireScope的 endpoint 都应该在本文里有对应章节。新增带 scope 的 endpoint 时, 在合适章节追加 request 形状、response 形状和一段 curl 示例;新增或 重命名 scope 时,同步更新下面的 Scopes 表。
Base URL https://<host>/api · 本地开发: http://localhost:8123/api
版本状态:V1 — 覆盖图片采集。后续版本会陆续加入图片读取 API、搜索、 通知。Scope 字符串一旦发布永久不变,详见下方稳定性保证章节。
目录
认证
进入 API 有两条路径,endpoint 不关心请求走的是哪条 —— 只要解析后的 用户拥有所需权限即可。
| 路径 | 来源 | 有效期 | 适用场景 |
|---|---|---|---|
| Supabase JWT | supabase.auth 浏览器会话 | 1 小时,可刷新 | 浏览器 UI、所有交互式场景 |
| API 密钥 | POST /auth/api-keys(本文档) | 直到撤销 / 过期 | Cron、Workers、服务器到服务器调用 |
两者用同一个 header:
Authorization: Bearer <token>服务端的 ApiKeyAuthFilter 先跑,识别 iph_live_ 前缀的 token; 其它一律交给 JwtAuthFilter。所以同一个 endpoint 同时给浏览器和 机器调用都没问题。
API 密钥代理用户身份
密钥的有效权限 = (持有人的角色权限) ∩ (密钥被授予的 scope)。 普通用户的 picture:upload 密钥只能上传到该用户拥有 PICTURE_UPLOAD 权限的 space;即使 scope 字符串匹配也无法访问 管理员 endpoint。admin:* 这个 scope 只有管理员能授予。
Token 格式
iph_live_<32 个随机字符>32 字符随机串从一个 56 符号字母表中抽取,显式排除了 0、O、 1、l、I(避免复制粘贴误读产生另一个有效 token)。约 186 bits 熵。
服务端只存 SHA-256(token),没有恢复流程。丢了就吊销 + 重建。完整明文仅在 POST /auth/api-keys 的响应里返回一次, 之后 API 永远只返回 13 字符的展示前缀(如 iph_live_a8K9x)。
Scopes
| Scope | 谁可以授予 | 涵盖范围 |
|---|---|---|
picture:upload | 任何用户 | R2 两阶段上传流程 + 管理员批量 URL 采集 |
admin:* | 仅管理员 | 资源前缀通配符 —— 所有管理员 endpoint(回收站清理、批量采集诊断、用户封禁等)。不是全局通配符,不覆盖 picture:upload。 |
运行时通配规则:授予 a:b:* 匹配所有 a:b:<任意值>;授予 * 匹 配一切。如果将来确实需要超级密钥,授予 *(目前未在公开目录中, 请谨慎控制)。
POST /auth/api-keys 创建时可授予的 scope 集合会按调用方角色过 滤。调 GET /auth/api-keys/available-scopes 可查到当前用户具体 能申请哪些 scope。
响应封装
每个 JSON endpoint 都用如下结构包裹返回值:
{
"code": 0,
"data": { /* endpoint 自己的数据 */ },
"message": "ok"
}code: 0 = 成功;非零 code = 业务错误,data 为 null。详见 下方错误码。SSE 端点(/picture/upload/batch/selected)是例 外,直接流式返回事件,不裹封装。
错误码
| Code | 含义 | 典型触发场景 |
|---|---|---|
0 | 成功 | — |
40000 | 请求参数错误 | 字段缺失 / 格式错误,body 校验未通过 |
40100 | 未登录 | 密钥缺失 / 错误 / 已撤销 / 已过期。这四种情况返回完全相同的错误,调用方无法通过响应区分(防探测)。 |
40101 | 权限不足 / scope 不匹配 | 已认证,但密钥不带 endpoint 所需的 scope。例如:"API key missing required scope: picture:upload" |
40300 | 用户层权限拒绝 | 用户角色 / space 权限校验失败(如调用方对目标 space 没有 PICTURE_UPLOAD 权限) |
42301 | 嵌入向量未就绪 | 图片刚上传,/similar/list 还没生成嵌入。前端会回退到基于标签的推荐 |
50000 | 服务端异常 | Bug 或下游故障 |
HTTP 状态码标准:200 表示成功,4xx/5xx 对应错误类。永远要看 body 里的 code —— HTTP 200 + code != 0 仍然是业务错误。
API 密钥管理
下面这五个端点都在 /auth/api-keys 路径下,只要登录用户即可调用 (Supabase JWT 可以;带相应 scope 的 API 密钥理论上也可以,但 V1 没为"自管理"开放 scope,所以请用 JWT)。
POST /auth/api-keys — 创建
请求体:
{
"name": "ingest-worker-2026-05",
"scopes": ["picture:upload"],
"description": "Cloudflare Worker cron, Wikimedia CC0 ingest",
"expiresInDays": 365
}| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
name | string | 是 | 1–255 字符,UI 列表展示用 |
scopes | string[] | 是 | 每项必须在目录中 且 调用方有权授予 |
description | string | 否 | 自由备注 |
expiresInDays | int | 否 | 不传 / 0 表示永不过期 |
成功响应(code: 0):
{
"data": {
"plaintext": "iph_live_...",
"key": {
"id": "1234567890",
"name": "ingest-worker-2026-05",
"prefix": "iph_live_a8K9",
"scopes": ["picture:upload"],
"expiresAt": "2027-05-04T00:00:00Z",
"revokedAt": null,
"lastUsedAt": null,
"lastUsedIp": null,
"totalRequests": 0,
"createTime": "2026-05-04T13:02:11Z",
"description": "Cloudflare Worker cron, Wikimedia CC0 ingest"
}
}
}plaintext 只返回这一次,立刻保存到安全位置。
curl 示例:
curl -X POST https://<host>/api/auth/api-keys \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{"name":"ingest-worker-2026-05","scopes":["picture:upload"]}'GET /auth/api-keys — 列表
列出当前调用方自己的密钥(包括已撤销的,带 revokedAt)。分页。
GET /auth/api-keys?current=1&pageSize=20响应结构:标准 MyBatis-Plus Page<ApiKeyVO> —— 包含 records[]、 total、current、size。keyHash 永远不返回。
POST /auth/api-keys/{id}/revoke — 撤销
幂等操作。第二次调用一个已撤销的密钥仍然返回 true —— 服务端 故意不区分"刚刚撤销"和"早就撤销了",防止好奇的调用方用响 应差异探测状态。已撤销的密钥下一次使用时被拒绝(返回 40100)。
curl -X POST https://<host>/api/auth/api-keys/1234567890/revoke \
-H "Authorization: Bearer <jwt>"POST /auth/api-keys/update — 更新元数据
只能改 name 和 description。scopes 故意不可变 —— 如果 需要不同的 scope 集合,创建新密钥并撤销旧的。这是为了防止权限 慢慢扩张("permission creep")。
{
"id": "1234567890",
"name": "ingest-worker-rotated",
"description": "rotated 2026-05-04, original token compromised"
}GET /auth/api-keys/available-scopes — 可授予的 scope 目录
返回当前调用方有权授予的 scope 列表。前端用它渲染创建密钥对话框 里的复选框。脚本里也可以用它在运行时发现新增的 scope —— 这个目 录会随版本逐步扩充。
{
"data": [
{
"value": "picture:upload",
"label": "Upload pictures",
"description": "Upload originals via the R2 two-stage flow + admin batch URL ingestion.",
"requiredRole": "user"
}
]
}图片上传(picture:upload)
下面三个 endpoint 都需要带 picture:upload 的 API 密钥,且 解析出来的用户对目标 space 拥有 PICTURE_UPLOAD 权限(目标是公开 gallery 时不需要 space 权限)。密钥层 → 用户层 → space 层,三道 权限闸门叠加生效。
POST /picture/upload/r2/check — 阶段一
客户端本地算 sha256,服务端去重检测,然后要么返回已存在的 blobId(跳过上传),要么签发一个 R2 presigned PUT URL。
请求体:
{
"sha256": "<原始文件的 64 字符 hex 哈希>",
"size": 524288,
"ext": "jpg",
"contentType": "image/jpeg",
"spaceId": null
}去重命中(已有 blob)的响应:
{
"data": {
"duplicate": true,
"blobId": "987",
"stagingKey": null,
"putUrl": null,
"thumb": { "stagingKey": "...", "putUrl": "..." },
"preview": { "stagingKey": "...", "putUrl": "..." }
}
}新文件(需要上传)的响应:
{
"data": {
"duplicate": false,
"blobId": null,
"stagingKey": "staging/<uuid>.jpg",
"putUrl": "https://<r2-host>/staging/<uuid>.jpg?X-Amz-Signature=...",
"thumb": { "stagingKey": "...", "putUrl": "..." },
"preview": { "stagingKey": "...", "putUrl": "..." }
}
}之后客户端用 HTTP PUT 把字节直接传到 presigned URL,然后调用 finalize。
POST /picture/upload/r2/finalize — 阶段二
把 staging 对象提升为 permanent,创建 picture 表行,bump blob 的 ref_count,返回 picture VO。事务性。
新文件的请求体:
{
"sha256": "...",
"stagingKey": "<阶段一返回的>",
"size": 524288,
"format": "JPEG",
"ext": "jpg",
"thumbKey": "<阶段一返回的>",
"previewKey": "<阶段一返回的>",
"embeddingKey": null,
"spaceId": null,
"name": "Mt. Fuji at sunrise",
"introduction": "Wikimedia Commons, CC0",
"category": "landscape"
}如果是去重命中,可以省略 stagingKey / format / thumbKey 等字段 (服务端会从已有 blob 中读)。如果是把已有 picture 克隆到新 space, 传 pictureId 代替这些字段。
POST /picture/upload/batch/selected — 管理员 URL 批量采集
服务端拉取器:给一组公开 URL,服务端逐个下载、去重、上传到 R2、 持久化为公开 gallery 图片。只有管理员可调用。响应是 SSE 流 (每个 URL 一个事件 + 一个最终汇总事件),不用 JSON 封装格式。
请求体:
{
"urlList": [
"https://upload.wikimedia.org/wikipedia/commons/.../foo.jpg",
"https://upload.wikimedia.org/wikipedia/commons/.../bar.jpg"
],
"namePrefix": "wikimedia-",
"tags": ["nature", "cc0"]
}单批次最多 50 个 URL。
每个 URL 处理完发一个事件:
data: {"index":0,"url":"...","status":"success","message":"...","done":false}最终汇总事件:
data: {"done":true,"total":50,"successCount":48}Worker / cron 的 curl 示例:
curl -N -X POST https://<host>/api/picture/upload/batch/selected \
-H "Authorization: Bearer iph_live_..." \
-H "Content-Type: application/json" \
-d '{"urlList":["https://...jpg"],"namePrefix":"cc0-","tags":["cc0"]}'无人值守的批量采集场景优先用这个 endpoint —— 不需要自己处理 R2 presigned PUT。
稳定性保证
- Scope 字符串永久不变。 一旦发布,就永远不重命名。如果某个 scope 要废弃,会从公开目录里移除(无法再被授予),但
ApiScopes.hasScope会继续认它,以免持有该 scope 的旧密钥失效。 - Token 格式
iph_live_*稳定。 未来可能引入额外前缀(如iph_test_*用于 staging 环境),但iph_live_*永远代表生产 级密钥。 /api/...路径下的 endpoint 遵循 semver。 已发布 endpoint 的破坏性变更会提前公告,并在路径中升一个主版本号。新增字段 (可选)、新增 endpoint、新增 scope 这种纯加法变更随时可能发生。- 错误码值稳定。
40100永远是"未登录",40101永远是"权 限不足 / scope 不匹配"。
路线图
数据库 schema 和 filter chain 在设计时就为下面的功能预留了接缝, 未来落地时全部是纯加法,无需迁移:
- 每密钥限速 ——
rate_limit_rpm列 + Bucket4j filter。 - IP 白名单 ——
ip_allowlist CIDR[]列 + filter 检查。 - 审计日志 —— 新增
api_key_audit表 + 异步监听器。 - 密钥轮换 ——
rotated_from_key_id列 + 轮换 endpoint。 - publishable / restricted 密钥类型 —— 已在现有 schema 的
key_type列预留。 - OAuth 风格的第三方应用授权 —— 独立流程、独立实体。
如果你正在对接本 API 并希望上面某项尽早实现,欢迎在 GitHub 上提 issue,带上具体使用场景。
