Skip to content

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 JWTsupabase.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 符号字母表中抽取,显式排除了 0O1lI(避免复制粘贴误读产生另一个有效 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 都用如下结构包裹返回值:

json
{
  "code": 0,
  "data": { /* endpoint 自己的数据 */ },
  "message": "ok"
}

code: 0 = 成功;非零 code = 业务错误,datanull。详见 下方错误码。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 — 创建

请求体:

json
{
  "name": "ingest-worker-2026-05",
  "scopes": ["picture:upload"],
  "description": "Cloudflare Worker cron, Wikimedia CC0 ingest",
  "expiresInDays": 365
}
字段类型必填说明
namestring1–255 字符,UI 列表展示用
scopesstring[]每项必须在目录中 调用方有权授予
descriptionstring自由备注
expiresInDaysint不传 / 0 表示永不过期

成功响应(code: 0):

json
{
  "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 示例:

bash
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[]totalcurrentsizekeyHash 永远不返回

POST /auth/api-keys/{id}/revoke — 撤销

幂等操作。第二次调用一个已撤销的密钥仍然返回 true —— 服务端 故意不区分"刚刚撤销"和"早就撤销了",防止好奇的调用方用响 应差异探测状态。已撤销的密钥下一次使用时被拒绝(返回 40100)。

bash
curl -X POST https://<host>/api/auth/api-keys/1234567890/revoke \
  -H "Authorization: Bearer <jwt>"

POST /auth/api-keys/update — 更新元数据

只能改 namedescriptionscopes 故意不可变 —— 如果 需要不同的 scope 集合,创建新密钥并撤销旧的。这是为了防止权限 慢慢扩张("permission creep")。

json
{
  "id": "1234567890",
  "name": "ingest-worker-rotated",
  "description": "rotated 2026-05-04, original token compromised"
}

GET /auth/api-keys/available-scopes — 可授予的 scope 目录

返回当前调用方有权授予的 scope 列表。前端用它渲染创建密钥对话框 里的复选框。脚本里也可以用它在运行时发现新增的 scope —— 这个目 录会随版本逐步扩充。

json
{
  "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。

请求体:

json
{
  "sha256": "<原始文件的 64 字符 hex 哈希>",
  "size": 524288,
  "ext": "jpg",
  "contentType": "image/jpeg",
  "spaceId": null
}

去重命中(已有 blob)的响应:

json
{
  "data": {
    "duplicate": true,
    "blobId": "987",
    "stagingKey": null,
    "putUrl": null,
    "thumb": { "stagingKey": "...", "putUrl": "..." },
    "preview": { "stagingKey": "...", "putUrl": "..." }
  }
}

新文件(需要上传)的响应:

json
{
  "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。事务性。

新文件的请求体:

json
{
  "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 封装格式。

请求体:

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 示例:

bash
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,带上具体使用场景。

基于 MIT 许可证发布。