Skip to content

Glymonir HTTP API

面向第三方 / cron 任务的实时参考文档。后端每个挂了 @RequireScope 的 endpoint 都应该在本文里有对应章节。新增带 scope 的 endpoint 时, 在合适章节追加 request 形状、response 形状和一段 curl 示例;新增或 重命名 scope 时,同步更新下面的 Scopes 表。

Base URL  https://<host>/api  ·  本地开发: http://localhost:8123/api

版本状态:V1 — 覆盖图片采集。后续版本会陆续加入图片读取 API、搜索、 通知。Scope 字符串一旦发布永久不变,详见下方稳定性保证章节。


用 CLI 快速上手

绝大多数"用脚本上传一张图"场景下,官方 Node CLI 是最快的路径 —— 一条命令搞定,不用自己写预处理:

bash
export GLYMONIR_API_KEY=gly_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

# 零安装:
npx glymonir-cli upload photo.jpg

# 上传到指定 library:
npx glymonir-cli upload --library 1234 photo.jpg

# 附带元数据 + 批量:
npx glymonir-cli upload \
  --name "Mt. Fuji at sunrise" --tags mountain,sunrise \
  *.jpg

CLI 本地完成网页端 imagePreprocess.ts 做的所有事(thumb + preview WebP、512×512 embedding JPEG、SHA-256 去重),然后调本文档下方 的 /picture/upload/r2/* endpoint。源码 + 文档在 tools/glymonir-cli/

要更细粒度的控制 / 不用 Node,直接看下面的 HTTP API。


目录


认证

进入 API 有两条路径,endpoint 不关心请求走的是哪条 —— 只要解析后的 用户拥有所需权限即可。

路径来源有效期适用场景
Supabase JWTsupabase.auth 浏览器会话1 小时,可刷新浏览器 UI、所有交互式场景
API 密钥POST /user/api-keys(本文档)直到撤销 / 过期Cron、Workers、服务器到服务器调用

两者用同一个 header:

Authorization: Bearer <token>

服务端的 ApiKeyAuthFilter 先跑,识别 gly_live_ 前缀的 token; 其它一律交给 JwtAuthFilter。所以同一个 endpoint 同时给浏览器和 机器调用都没问题。

API 密钥代理用户身份

密钥的有效权限 = (持有人的角色权限) ∩ (密钥被授予的 scope)。 即使 scope 字符串匹配,密钥也无法访问超出持有人本身角色 / library 权限的 endpoint。admin:*gallery:upload 这两个 scope 只有管理员能授予

Token 格式

gly_live_<32 个随机字符>

32 字符随机串从一个 56 符号字母表中抽取,显式排除了 0O1lI(避免复制粘贴误读产生另一个有效 token)。约 186 bits 熵。

服务端只存 SHA-256(token),没有恢复流程。丢了就吊销 + 重建。完整明文仅在 POST /user/api-keys 的响应里返回一次, 之后 API 永远只返回 13 字符的展示前缀(如 gly_live_a8K9x)。


Scopes

Scope谁可以授予涵盖范围
gallery:read任何登录用户(受发行准入限制)批量读取公开 gallery —— 列表 / 单图全字段(名称、描述、标签、颜色、尺寸、R2 URL)+ 1024 维 CLIP 向量。见下方 /api/gallery/*
gallery:upload仅管理员通过 R2 两阶段流程上传图到公开 gallery + 管理员批量 URL 采集。
admin:*仅管理员资源前缀通配符 —— 所有 admin:xxx endpoint(回收站清理、批量采集诊断、用户封禁等)。不是全局通配符,覆盖 gallery:uploadgallery:read

发行准入门槛:POST /user/api-keys 要求调用者当前订阅为 Pro 或角色为 admin。Free / Starter 用户被拒,返回 40101 + 升级提 示。Admin 角色自动绕开订阅档位检查("effective plan = UNLIMITED")。

通配规则:授予 a:b:* 匹配所有 a:b:<任意值>;授予 * 匹 配一切。如果将来确实需要超级密钥,授予 *(目前未在公开目录中, 请谨慎控制)。

遗留别名:2026-05-25 之前发行的密钥可能仍带 picture:upload 这个被弃用的 scope。服务端把 picture:upload 视作等价于 gallery:upload,老密钥继续可用;新密钥不能再申请 picture:upload

POST /user/api-keys 创建时可授予的 scope 集合会按 角色 + 订阅 档 过滤。调 GET /user/api-keys/available-scopes 可查到当前用户 具体能申请哪些 scope —— 返回空列表 = 当前账号无资格创建 API 密钥 (很可能是 Free / Starter 计划)。


响应封装

每个 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: gallery:read"
40300用户层权限拒绝用户角色 / library 权限校验失败(如调用方对目标 library 没有 PICTURE_UPLOAD 权限)
40400资源不存在资源不存在 / 不在公开 gallery / 未通过审核 —— API 三种情况合并为同一个 404,防止用来探测私有状态。
42301嵌入向量未就绪(相似搜索)图片刚上传,/similar/list 还没生成嵌入。前端会回退到基于标签的推荐
42500嵌入向量未就绪(gallery API)/api/gallery/picture/{id}/embedding —— 图片存在但 CLIP 向量还没算出来。等几分钟重试。
42900速率限制每密钥配额耗尽。响应附带 HTTP Retry-After header(单位:秒)。默认 Pro/user = 60 req/min 稳定速率 + 100 token burst;admin 无限
50000服务端异常Bug 或下游故障

HTTP 状态码标准:200 表示成功,4xx/5xx 对应错误类。永远要看 body 里的 code —— HTTP 200 + code != 0 仍然是业务错误。


API 密钥管理

下面这五个端点都在 /user/api-keys 路径下,只要登录用户即可调用 (Supabase JWT 可以;带相应 scope 的 API 密钥理论上也可以,但 V1 没为"自管理"开放 scope,所以请用 JWT)。

POST /user/api-keys — 创建

请求体:

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

成功响应(code: 0):

json
{
  "data": {
    "plaintext": "gly_live_...",
    "key": {
      "id": "1234567890",
      "name": "ingest-worker-2026-05",
      "prefix": "gly_live_a8K9",
      "scopes": ["gallery:read"],
      "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/user/api-keys \
  -H "Authorization: Bearer <jwt>" \
  -H "Content-Type: application/json" \
  -d '{"name":"ingest-worker-2026-05","scopes":["gallery:read"]}'

GET /user/api-keys — 列表

列出当前调用方自己的密钥(包括已撤销的,带 revokedAt)。分页。

GET /user/api-keys?current=1&pageSize=20

响应结构:标准 MyBatis-Plus Page<ApiKeyVO> —— 包含 records[]totalcurrentsizekeyHash 永远不返回

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

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

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

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

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

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

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

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

json
{
  "data": [
    {
      "value": "gallery:read",
      "label": "Read public gallery",
      "description": "Bulk-read public gallery pictures + metadata + 1024-d CLIP embedding vectors.",
      "requiredRole": "user"
    }
  ]
}

下面三个 endpoint 为面向程序的 gallery 消费方设计(Pro 用户 + 管 理员)。只返回通过审核(PASS)的公开 gallery 图;库 / 私有 library / REVIEWING / REJECTED 一律返回 40400,与调用方无关。

按时间倒序游标分页。游标稳定 —— 即使中间有新图入库,昨天拿到的 "第 2 页"依然是同一批结果。

参数:

名称类型默认说明
cursorstring不透明游标。首次请求不传;后续传上一次响应的 nextCursor
limitint50每页大小,clamp 到 [1, 200]

响应 data:

json
{
  "items": [
    {
      "id": "1234567890",
      "sha256": "abc...",
      "name": "Mt. Fuji at sunrise",
      "format": "jpg",
      "width": 4032, "height": 3024,
      "sizeBytes": 5242880,
      "originalUrl": "https://cdn.example.com/photos/...jpg",
      "thumbUrl": "https://cdn.example.com/cdn-cgi/image/.../...webp",
      "createTime": "2026-05-25T12:00:00Z"
    }
  ],
  "nextCursor": "MTcxNjY0MjQwMHwxMjM0NTY3ODkw",
  "limit": 50
}

nextCursor 缺省 = 已到末页。

完整元数据 + R2 URL。CLIP 向量拆到下一个 endpoint(4 KB,单独取 更划算)。

响应 data:

json
{
  "id": "1234567890",
  "sha256": "abc...",
  "name": "Mt. Fuji at sunrise",
  "introduction": "Captured 2024-09-15 from Hakone.",
  "tags": ["mountain","sunrise","japan"],
  "userId": "987",
  "createTime": "2026-05-25T12:00:00Z",
  "format": "jpg", "width": 4032, "height": 3024, "sizeBytes": 5242880,
  "picPalette":   "[{\"hex\":\"#a36b3f\",\"ratio\":0.42,\"lab\":[...]}]",
  "picMosaicLab": "[[L,a,b], [L,a,b], ... 25 entries]",
  "originalUrl": "https://cdn.example.com/photos/...jpg",
  "thumbUrl":    "https://cdn.example.com/cdn-cgi/image/.../...webp",
  "previewUrl":  "https://cdn.example.com/cdn-cgi/image/.../...webp"
}

40400(code 40400):id 不存在 / 图属于私有 library / 未通过审核 —— 合并为同一个 404,API 表面不可探测。

json
{
  "id": "1234567890",
  "embeddingModel": "jina-clip-v2",
  "dimensions": 1024,
  "embedding": [0.0123, -0.0456, ...],
  "computedAt": "2026-05-25T12:00:01Z"
}

状态:

  • code 0 —— 向量就绪,在 data.embedding
  • code 42500 —— 图存在但向量还没算,data.embeddingnull。 几分钟后重试。
  • code 40400 —— 同上 404 合并语义。

向量已 L2-normalised(norm=1.0),所以点积 = cosine 相似度。


下面三个 endpoint 都需要带 gallery:upload(仅管理员)的 API 密钥, 解析出来的用户对目标 library 拥有 PICTURE_UPLOAD 权限(目标是公开 gallery 时不需要 library 权限)。密钥层 → 用户层 → library 层,三道 权限闸门叠加生效。

上传契约(下面所有 endpoint 一视同仁,admin 不旁路):

规则取值作用范围
格式白名单image/jpeg / image/png / image/webp所有上传。GIF / TIFF / HEIC / SVG 一律拒绝。
单文件上限≤ 50 MB所有上传、所有目的地。
公开 gallery 下限≥ 1 MB不传 libraryId(或显式置 null)。质量底线,防止低分辨率小图污染浏览面板。
library 下限libraryId 指向某个 library。用户私有存储,50 KB 的图标也是合法内容。
管理员 URL 批量采集≥ 1 MB≤ 50 MB与 gallery 一致 —— /picture/upload/batch/selected 永远目标公开 gallery。

违反契约返回 code: 40000(PARAMS_ERROR),message 会指明 触发的具体规则("File size exceeds 50 MB cap...""Public gallery uploads must be at least 1 MB...")。常量由 PictureUploadSizeIT 锁死,不会被静悄悄改动。

三变体上传模型

每张图(无论是上传到公共 gallery 还是 library)在 R2 里会存最多 4 个对象。 客户端(浏览器 / CLI / 你的代码)需要在调 check 之前就把三个变体生成好。

对象Slot 名是否必须服务端期望的格式
原图original用户上传的原始格式(JPEG / PNG / WebP)
缩略图thumbWebP,最长边 1280px,质量 ~76
预览图previewWebP,最长边 3200px,质量 ~80
Embeddingembedding可选JPEG,精确 512×512,黑色 padding 保持原始宽高比,质量 ~85。喂给 Jina CLIP 做相似度搜索。生成不出来可以省,管理员后台有补传流程。

客户端对每个变体算 (sha256, 字节大小, content-type) 三元组,在 check 请求里一次性提交。服务端对每个还没存过的变体 slot 各 签发一个 presigned PUT URL(全部命中去重 = 所有 slot 返回 null, 客户端直接跳到 finalize)。

参考实现见 tools/glymonir-cli/src/preprocess.ts。 不想自己复现,用上面的 CLI 快速上手那一节里的 CLI,它会全帮你做好。

finalize 的美学元数据

除了那 4 个 R2 对象之外,picture 行还携带一些客户端通常在上传时 顺手算好的元数据。全部可选 —— 你不传,图也能存进去,只是 UI 质量会降级。

字段含义null 的后果
width, height原图像素尺寸强烈推荐 —— 列表布局靠宽高比排版
thumbhashbase64 LQIP 占位图,约 30 字节(用 thumbhash 库生成)瀑布流在 thumb 加载完之前显示纯灰色占位
aveColorPaletteJSON 字符串:Top-5 Lab K-means 调色板,[{hex,ratio,lab}, ...]列表卡片背景渐变退化为默认灰
aveMosaicLabJSON 字符串:5×5 Lab 空间网格,[[L,a,b], ... ×25]"photo-mosaic" 相似搜索无法索引这张图
exifJSON 字符串:EXIF blob(机身 / 镜头 / 光圈 / 快门 / ISO / GPS)详情页隐藏 EXIF 区块

CLI 用 sharp + thumbhash + 本地 Lab K-means 把这些全算了。 纯 HTTP 接入方算不出来,传 null 即可。

POST /picture/upload/r2/check — 阶段一

客户端本地算原图和 3 个变体的 sha256,服务端做去重检测,然后对每个 未存过的 slot 返回一个 presigned PUT URL。

请求体 —— 除 embedding 外都是必填:

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

  "thumb": {
    "sha256":      "<thumb WebP 字节的 sha256>",
    "size":        18432,
    "contentType": "image/webp"
  },
  "preview": {
    "sha256":      "<preview WebP 字节的 sha256>",
    "size":        184320,
    "contentType": "image/webp"
  },
  "embedding": {
    "sha256":      "<512×512 embedding JPEG 字节的 sha256>",
    "size":        32768,
    "contentType": "image/jpeg"
  },

  "libraryId": null
}
字段必填?说明
sha25616 进制小写 64 字符,用于 blob 去重
size原图字节数。服务端在签发 URL 前会按 1 MB / 50 MB 上下限校验
ext小写,不带点 —— jpg / png / webp
contentType必须是 image/jpeg / image/png / image/webp 之一
thumb{sha256, size, contentType}。变体不跨图去重(R2 key 由原图 sha256 推导)
previewthumb
embedding可选同上结构。生成不出来传 null 或省略
libraryId可选null(或省略)= 公共 gallery;整数 = 上传到指定 library

响应:

json
{
  "code": 0,
  "data": {
    "dedupe":           false,
    "blobId":           null,
    "original":         { "uploadUrl": "...", "requiredHeaders": {"Content-Type": "image/jpeg"}, "stagingKey": "staging/<uuid>.jpg" },
    "thumb":            { "uploadUrl": "...", "requiredHeaders": {"Content-Type": "image/webp"}, "stagingKey": "thumb/<sha>.webp" },
    "preview":          { "uploadUrl": "...", "requiredHeaders": {"Content-Type": "image/webp"}, "stagingKey": "preview/<sha>.webp" },
    "embedding":        { "uploadUrl": "...", "requiredHeaders": {"Content-Type": "image/jpeg"}, "stagingKey": "embedding/<sha>.jpg" },
    "expiresInSeconds": 900
  }
}
字段含义
dedupe原图 sha256 命中已有 blob 时为 true。即使命中,单个变体 slot 仍可能非 null(blob 缺该变体)
blobId命中去重时给出已存在的 picture_blob.id,新文件为 null。命中时把这个 id 透传到 finalize
original / thumb / preview / embedding该变体已有 → null(别 PUT);否则给出 presigned slot,见下表
expiresInSeconds本次响应里所有 uploadUrl 的 TTL,默认 900(15 分钟)

每个非 null slot 结构:

Slot 字段含义
uploadUrlR2 预签名 URL。直接 PUT 字节过去
requiredHeaders必须在 PUT 时原样回填的 header map,否则 R2 报 SignatureDoesNotMatch。一定包含对应的 Content-Type
stagingKeyR2 对象 key。原图是 staging/(finalize 时升级为永久);变体是最终 key —— PUT 两次幂等覆盖。透传到 finalize

阶段 1.5 —— PUT 到 presigned URL

check 响应里每个非 null slot,直接把字节 PUT 给 R2:

bash
curl -X PUT "<slot.uploadUrl>" \
  -H "Content-Type: <slot.requiredHeaders['Content-Type']>" \
  --data-binary @/path/to/variant.bin
  • PUT,raw body(是 multipart)
  • requiredHeaders 里每个 header 都要原样回填,缺一个或 Content-Type 不对 = R2 返回 403 SignatureDoesNotMatch
  • 成功 = HTTP 200,无 JSON body
  • 这一步直接打 R2,不带 Authorization: Bearer,不带 Glymonir API key —— 签名 URL 本身就是凭证
  • 多个 slot 可以并发上传
  • check 响应里 null 的 slot 直接跳过

POST /picture/upload/r2/finalize — 阶段二

把原图的 staging/* 对象提升到永久 photos/* key,创建 picture 行, bump blob 的 ref_count,返回 picture VO。事务性 —— 失败时回滚 picture 行,R2 staging 对象由 GC sweeper 后续清理。

请求体 —— 完整字段,按必填 → 可选排列:

json
{
  "sha256":     "<原图 sha256,跟 /check 里一致>",
  "stagingKey": "<check 响应里 original.stagingKey>",
  "size":       524288,
  "format":     "JPEG",
  "ext":        "jpg",
  "width":      4032,
  "height":     3024,
  "thumbKey":   "<check 响应里 thumb.stagingKey>",
  "previewKey": "<check 响应里 preview.stagingKey>",
  "embeddingKey":   "<check 响应里 embedding.stagingKey>",
  "thumbhash":      "<base64,约 28 字节>",
  "aveColorPalette":"<JSON 字符串>",
  "aveMosaicLab":   "<JSON 字符串>",
  "exif":           "<JSON 字符串>",
  "name":           "Mt. Fuji at sunrise",
  "introduction":   "Wikimedia Commons, CC0",
  "category":       "landscape",
  "tags":           ["nature", "mountain"],
  "libraryId":      null,
  "pictureId":      null
}
字段必填?说明
sha256原图 sha256,用于查询/创建 picture_blob
stagingKey条件必填check 响应里的 original.stagingKey去重命中时省略(blob 已有永久 key)
size原图字节数
format新文件必填JPEG / PNG / WEBP 之一(大写)。去重命中时省略
ext新文件必填小写不带点。去重命中时省略
width / height推荐原图像素宽高。即使语义上可选,也强烈推荐 —— 列表布局靠宽高比
thumbKey / previewKey新变体上传时必填透传 check 响应里的值。去重命中且服务端已有该变体 → 省略
embeddingKey可选check 有签发就透传;跳过 embedding 时传 null
thumbhash / aveColorPalette / aveMosaicLab / exif全部可选美学元数据。算不出来的字段传 null 或省略
name可选显示名。省略 → 默认用文件名(去扩展名)
introduction可选说明 / 描述
category可选自由字符串
tags可选string[]。公共 gallery 列表当前不展示 tags(走 concept browse);library 会用
libraryId可选null = 公共 gallery,要跟 check 时传的值一致
pictureId可选只在编辑已有图片(纯元数据 patch)时填。新增上传留 null

响应 —— BaseResponse<PictureVO>:

json
{
  "code": 0,
  "data": {
    "id":            2058948115995348994,
    "blobId":        12345,
    "imagePath":     "photos/74/74ec3715...c1cd6ebf.jpg",
    "url":           "https://img.glymonir.com/photos/74/74ec3715...c1cd6ebf.jpg",
    "thumbnailUrl":  "https://img.glymonir.com/thumb/74ec3715...c1cd6ebf.webp",
    "originalUrl":   "https://img.glymonir.com/photos/74/74ec3715...c1cd6ebf.jpg",
    "thumbhash":     "<base64 回显>",
    "exif":          "<JSON 字符串回显,可能为 null>",
    "name":          "Mt. Fuji at sunrise",
    "introduction":  "Wikimedia Commons, CC0",
    "tags":          ["nature", "mountain"],
    "picSize":       524288,
    "picWidth":      4032,
    "picHeight":     3024,
    "picScale":      1.333,
    "picFormat":     "JPEG",
    "picPalette":    "<JSON 字符串回显>",
    "picMosaicLab":  "<JSON 字符串回显>",
    "userId":        1001,
    "originUserId":  null,
    "spaceId":       null,
    "createTime":    "2026-05-26T05:48:00.000+00:00",
    "editTime":      "2026-05-26T05:48:00.000+00:00",
    "updateTime":    "2026-05-26T05:48:00.000+00:00",
    "likeCount":     0,
    "viewCount":     0,
    "downloadCount": 0,
    "visibility":    "PUBLIC",
    "reviewStatus":  1,
    "slug":          "mt-fuji-at-sunrise"
  },
  "message": ""
}

上传后客户端最常用的字段:id(以后操作这张图的句柄)、 url / thumbnailUrl(展示 URL)、reviewStatus(1 = 自动通过 / 0 = 待审核 / 2 = 拒绝)、slug(SEO 友好的 slug,用于规范 URL /picture/<slug>-<id>)。

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 gly_live_..." \
  -H "Content-Type: application/json" \
  -d '{"urlList":["https://...jpg"],"namePrefix":"cc0-","tags":["cc0"]}'

无人值守的批量采集场景优先用这个 endpoint —— 不需要自己处理 R2 presigned PUT。


稳定性保证

  • Scope 字符串永久不变。 一旦发布,就永远不重命名。如果某个 scope 要废弃,会从公开目录里移除(无法再被授予),但 ApiScopes.hasScope 会继续认它,以免持有该 scope 的旧密钥失效。
  • Token 格式 gly_live_* 稳定。 未来可能引入额外前缀(如 iph_test_* 用于 staging 环境),但 gly_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 许可证发布。