概览
源:kira-cdn/src/index.ts + routes/。base path cdn-api.kira.art(内部 kira-cdn.internal:8888)。
所有 /v1/* 端点走 eitherAuth()(Supabase JWT 或 X-Internal-Key),少数标 internal-only 的只接受 X-Internal-Key。授权规则见 认证模型。
| Method | Path | Auth | 用途 |
|---|---|---|---|
| POST | /v1/uploads | either | reserve assetId + presigned R2 PUT URL(不写 DB) |
| POST | /v1/uploads/sign-pending | internal | 给已 PUT 但未 register 的对象签 GET URL |
| POST | /v1/assets | either | register asset:HEAD R2 + Sharp meta + CSAM + INSERT |
| DELETE | /v1/assets | either | 统一删除入口(uuid + legacy path 混合,按 id 格式分流) |
| POST | /v1/sign/image | either | 批量 ref → signed URL + imgproxy thumb/preview + meta |
| POST | /v1/sign/video | either | 批量 ref → signed URL + 关联 thumb + meta |
| POST | /v1/fit | either | 尺寸/字节/格式受限的 imgproxy URL(喂外部 AI provider) |
| POST | /v1/copy | either | publish:跨 scope 物理复制 asset |
| POST | /v1/watermark | either | image 水印 URL(.wm.jpg) |
| POST | /v1/watermark-video | either | video 水印 URL(.wm.mp4) |
| DELETE | /v1/scope | either | directory cascade 删除(by owner ± scopeKind ± scopeId) |
| POST | /v1/media-tasks | internal | upsert task |
| POST | /v1/media-tasks/video | either | 浏览器视频上传 init + 触发 Trigger.dev 任务 |
| GET | /v1/media-tasks/:taskId | either | 轮询 task 状态 |
| DELETE | /v1/media-tasks/:taskId | either | 取消 task + 级联删 output.assetIds |
| DELETE | /v1/media-tasks | internal | 按 ownerId 批量删 task row(不级联 asset) |
| GET | /health | none | 健康检查 |
上传 / 注册
POST /v1/uploads
生成 assetId + presigned R2 PUT URL,不写 DB。caller PUT 完后调/v1/assets 注册。
Body(UploadRequest):
/v1/assets 回传同样的 (scopeKind, scopeId, kind, mime, assetId, ownerId),cdn 才能重算同一 storageKey。
POST /v1/uploads/sign-pending
Auth:internal-only(X-Internal-Key)。JWT user 不需要这个端点 —— register 完走 /v1/sign 即可。
给”已 PUT 但还没 register”的对象签 GET URL。worker 新协议用:client 拿了 assetId 直传 R2 后,worker 处理时 video asset 还没 register(register 要等 thumb 就位才能带 thumbAssetId 一起 INSERT),期间 /v1/sign 因 DB 无行拿不到 URL。本端点用 (ownerId, scopeKind, scopeId, assetId, mime) 重算 storageKey,HEAD R2 验对象在,直接签 GET URL,绕过 DB lookup。
Body(SignPendingRequest):
POST /v1/assets
caller PUT R2 完成后注册 row。流程:Image meta(Sharp)
mime 以
image/ 开头时,cdn fetch 对象 + Sharp 算 width/height/blurhash/format,覆盖 caller 传值;检测出的 format 覆盖 mime(如 .jpg 实际是 PNG → image/png)。video/audio 的 meta(width/height/durationMs)由 caller 传。CreateAssetRequest):
meta 字段被 spread 回顶层 flat,兼容老 API)。
CSAM 命中:
签名
POST /v1/sign/image
批量 image ref → signed URL + imgproxy 变体 + meta(整响应 Redis 缓存 24h)。Body{ refs: string[] (≤200), legacyBucket? }。
Response 是 { [ref]: ImageSignedRef }:
{ ready: false, error: "not_found" }。非 image mime → 返 base 不带 thumbUrl/previewUrl。
POST /v1/sign/video
批量 video ref → signed URL + 关联 thumb。Body 同上。Response{ [ref]: VideoSignedRef }:
meta.thumbAssetId,DB 查 thumb asset → 经 imgproxy 出 thumbUrl。判据是”有没有 thumbAssetId”(不 gate kind,兼容历史 insert_video)。legacy supabase passthrough video 按命名约定 video_*.mp4 ↔ vc_*.jpg 兜底解 thumb。thumb 解析失败只 warn,主签照返。
image 与 video 用独立 Redis namespace(
sign vs sign-video),响应 shape 不同,绝不共用 key。POST /v1/fit
出一个尺寸/字节/格式受限的 imgproxy 签名 URL,用于把用户原图降到外部 AI provider 的输入限制内。cdn 是薄签名器,caller 按各 provider 的 InputSpec 算好参数传进来。 Body(FitRequest):
{ url }。ref 解不到 / legacy 源已删 → 404。变体 URL 经资源三元组 + transform 参数缓存(同图同变换命中同一 URL,CF edge 缓住)。
Publish(copy)
POST /v1/copy
把 source asset 物理复制为另一个 scope(通常是 feed)的独立资源。 两条路径,新 row 一律落 R2:- uuid + R2 源 → R2 server-side
CopyObject(零字节进 cdn) - uuid + Supabase 源 / legacy path 源 → stream-through(下载 → cdn 内存 → R2 PUT)
CopyRequest):
meta spread 回顶层)。失败:source 不存在 404;jwt 跨 owner 403;字节传输失败 502/500。
水印
POST /v1/watermark
image 水印。同目录{srcKey}.wm.jpg,HEAD 命中直接签 URL,miss 则 Sharp composite + putObject。Body { ref, legacyBucket? }。Response { url }。非 image mime → 400。Redis 整响应缓存 24h(key wm:{backend}:{bucket}:{key})。
POST /v1/watermark-video
video 水印 + Kira 品牌 ending 拼接。同目录{srcKey}.wm.mp4,HEAD 命中直接签 URL,miss 则 ffmpeg(容器内 Bun.spawn)composite + putObject。Body { ref, legacyBucket? }。Response { url }。非 video mime → 400。Redis 缓存 24h(key wm-video:{backend}:{bucket}:{key})。
水印只在下载场景调用,不进 sign 响应路径(避免对每个 asset 都签无用 URL)。物理水印文件不靠 lifecycle,由 owen 自写 cron 按热度清。删 source 时同前缀的
.wm.jpg/.wm.mp4 一并级联清。删除
DELETE /v1/assets
统一删除入口,按 id 格式分流。Body{ ids: string[] (1..1000), legacyBucket? }:
- UUID → cdn DB row + R2 batch remove(硬删)
- 含
/的 path → Supabase batch remove(legacyBucket默认agent_message)
- 物理删成功才删 DB row(removeMany 不 catch,R2 失败抛出),绝不造不可达孤儿
- 同目录水印变体
.wm.jpg/.wm.mp4一并删(no-op 兜不存在) - video row 的
meta.thumbAssetId指向的 thumb 级联删(thumb 是 cdn 内部关系,caller 不一定 enumerate) - 清对应 Redis cache(sign / sign-video / wm / wm-video)
- JWT 模式 legacy path 越权:必须
startsWith{sub}//feed/{sub}//avatars/{sub}/,否则 403
{ deletedNew, deletedLegacy }。
DELETE /v1/scope
directory cascade 删除,由 DB 行驱动(不 R2 ListObjects)。Body(DeleteScopeRequest):
legacyPrefixes 走 Supabase list + remove(best-effort);(3) 按 (backend,bucket) 分组删物理(含水印变体,removeMany 不 catch);(4) 物理全成功才删 pg row + 清 Redis。Response { r2Deleted, legacyDeleted, dbDeleted }。
scope cascade 比 caller enumerate refs 更可靠 —— 不依赖 caller 把
messages.parts / version JSONB 都扫齐。Media Tasks
POST /v1/media-tasks
internal-only。worker upsert task(pending → processing → completed)。同 task_id 重写只更新 metadata,保留首次的 owner_id / media_type / created_at。Body { taskId, ownerId, mediaType, metadata? }。Response:upserted row。
POST /v1/media-tasks/video
浏览器视频上传 init(替代旧 kira-be/video/initialize)。流程:HEAD R2 验对象在(不存在 → 409)→ INSERT pending task → 经 Trigger.dev tasks.trigger("video-upload-kira", …) 触发 kira-video-worker(run 打 tag videoTask:<taskId>,便于 kira-be 取消)→ 返 taskId。Body:
{ taskId, status: "pending" }。
GET /v1/media-tasks/:taskId
轮询。task 未入表(刚发完 event 还没 upsert)返{ taskId, status: "pending" }(不是 404)。JWT 模式只返本人 task(他人 → 404)。taskId 非 uuid → 400。
DELETE /v1/media-tasks/:taskId
取消 task + 级联删metadata.output.assetIds 引用的资产(deleteAssetsByIds)。幂等:已删/不存在返 { deleted: false, assetsDeleted: 0 }。JWT 模式越权 → 404。Response { deleted: true, assetsDeleted }。
DELETE /v1/media-tasks
internal-only。账号注销级联:按ownerId 批量删 task row,不级联 asset(asset 清理走 DELETE /v1/scope by ownerId)。Body { ownerId }。Response { deleted: <count> }。
健康检查
GET / 返 { "service": "kira-cdn", "version": "0.0.1" }。/health 在 OTel 入站 span 后跳过请求日志,避免噪声。
Caller 集成模式
- 上传(浏览器)
- 发布到 feed
- 签名(任意 caller)
- 删除
- 视频 + worker