内容上传 API¶
内容上传管线允许用户上传文本和附件的组合包。文件通过预签名 URL 直传 S3 — 后端不代理文件数据。
所有接口需要 JWT 认证。
上传流程¶
sequenceDiagram
participant C as 客户端
participant B as 后端
participant S3 as S3
participant CF as CloudFront
C->>B: POST /api/raw-items/upload
Note right of B: 创建 files 记录 (pending)
B-->>C: itemId + presigned URLs
loop 每个附件
C->>S3: POST multipart 表单
S3-->>C: 204 + ETag + SHA256
end
C->>B: POST /api/raw-items/id/confirm
B->>S3: HeadObject
S3-->>B: size, etag, sha256
Note right of B: 更新 files 和 item (ready)
B-->>C: status ready
C->>B: GET /api/files/fileId/download
B-->>C: 307 重定向
C->>CF: GET 签名 URL
CF->>S3: 获取对象 (OAC)
S3-->>CF: 文件数据
CF-->>C: 文件数据
接口¶
POST /api/raw-items/upload¶
请求上传。后端创建数据库记录并返回每个附件的预签名上传 URL。
请求:
{
"type": "PORTAL_UPLOAD_BUNDLE",
"mainText": "今天的笔记,附带一些文件",
"attachments": [
{
"filename": "photo.jpg",
"contentType": "image/jpeg",
"sizeBytes": 2048000
},
{
"filename": "report.pdf",
"contentType": "application/pdf",
"sizeBytes": 512000
},
{
"type": "link",
"url": "https://example.com/interesting-article"
}
]
}
响应(201):
{
"itemId": "019e4596-2b4a-7c03-9b7f-7c6939c9564f",
"attachments": [
{
"index": 0,
"url": "https://inklet-dev.s3.us-east-1.amazonaws.com",
"fields": {
"key": "user/1a16f2b9-.../2026/05/019e4596-2b44-....jpg",
"Content-Type": "image/jpeg",
"x-amz-checksum-algorithm": "SHA256",
"x-amz-credential": "AKIA.../20260520/us-east-1/s3/aws4_request",
"x-amz-algorithm": "AWS4-HMAC-SHA256",
"x-amz-date": "20260520T130000Z",
"policy": "eyJleHBpcm...",
"x-amz-signature": "a1b2c3d4..."
}
},
{
"index": 1,
"url": "https://inklet-dev.s3.us-east-1.amazonaws.com",
"fields": { "..." }
}
],
"expiresAt": "2026-05-20T13:47:09Z"
}
上传到 S3
客户端需要构造一个 multipart/form-data POST 请求发送到 url。先将所有 fields 作为表单字段添加,最后将文件作为名为 file 的字段添加。
过期时间
预签名 URL 在 15 分钟后过期(由 expiresAt 指示)。请在此时间之前完成所有文件上传。
错误:
| 状态码 | 原因 |
|---|---|
| 400 | 缺少 type、不支持的类型、或无效的 content type |
| 413 | 文件超过 10 MB 大小限制 |
允许的 content type(文件附件):
image/png, image/jpeg, image/gif, image/webp, image/svg+xml, application/pdf, text/plain, text/markdown, application/json
链接附件
type: "link" 类型的附件不需要 S3 上传。URL 直接存储在 content JSON 中。Summary worker 处理时通过 Jina Reader 抓取链接内容,将原始 markdown 存入 content.attach[].raw_content。LLM 摘要结果存入 summary_content.attachments[].description(不含 raw_content)。
POST /api/raw-items/{id}/confirm¶
确认所有文件已上传完成。后端通过 HeadObject 逐个验证文件是否存在于 S3,获取 SHA256 校验和,并更新数据库。
响应(200)— 全部确认成功:
响应(200)— 部分文件失败:
failed 数组包含无法验证的附件索引。客户端可以重新上传这些文件并再次调用 confirm。
错误:
| 状态码 | 原因 |
|---|---|
| 404 | Item 不存在 |
| 403 | 不是 item 的所有者 |
| 409 | Item 不是 pending 状态 |
GET /api/raw-items¶
列出当前用户的 items,支持分页。按创建时间倒序(最新的在前)。响应不包含 content 和 userId 字段。
查询参数:
| 参数 | 说明 | 默认值 |
|---|---|---|
status |
按状态过滤:pending、ready、partial |
全部 |
page |
页码(从 1 开始) | 1 |
limit |
每页数量(最大 50) | 20 |
响应(200):
{
"items": [
{
"id": "019e4596-2b4a-...",
"type": "PORTAL_UPLOAD_BUNDLE",
"format": "v0",
"status": "ready",
"processStatus": "RAW",
"createdAt": "2026-05-20T13:32:09Z",
"updatedAt": "2026-05-20T13:32:15Z"
}
],
"total": 42,
"page": 1,
"limit": 20
}
GET /api/raw-items/{id}¶
获取单个 item。如果 item 属于其他用户则返回 403。
GET /api/raw-items/stats¶
当前用户最近 7 天的处理统计 —— 按 processStatus 统计 raw item 数量。
响应(200):
| 字段 | 说明 |
|---|---|
since |
7 天窗口的起点(RFC3339) |
RAW |
尚未拆解的 item 数 |
INGESTED |
已拆解为 content block、处理中的 item 数 |
READY |
所有 block 已摘要完成的 item 数 |
FAILED |
至少有一个 content block 失败的 item 数 |
GET /api/files/{fileId}/download¶
返回 307 重定向到 CloudFront 签名 URL(15 分钟过期)。客户端跟随重定向即可下载文件。
只有文件所有者可以访问。文件必须处于 ready 状态。
错误:
| 状态码 | 原因 |
|---|---|
| 404 | 文件不存在 |
| 403 | 不是文件所有者 |
| 409 | 文件未就绪 |
S3 对象路径格式¶
user/{userId}/{年}/{月}/{fileId}.{扩展名}
示例:
user/1a16f2b9-3b9d-4e57-9272-6a03f0f2274e/2026/05/019e4596-2b44-7c2b-8e36-d367ecab52b7.txt
Content JSON 结构¶
PORTAL_UPLOAD_BUNDLE 类型 item 的 content 字段(format v0):
{
"type": "PORTAL_UPLOAD_BUNDLE",
"format": "v0",
"upload_at": "2026-05-20T13:32:09Z",
"main_text": "用户的文本内容",
"attach": [
{
"type": "image",
"mime": "image/jpeg",
"file_id": "019e4596-2b44-7c2b-8e36-d367ecab52b7"
},
{
"type": "doc",
"mime": "application/pdf",
"file_id": "019e4596-3c55-7d3c-9f47-e478fdb63c8a"
},
{
"type": "link",
"url": "https://example.com/article",
"raw_content": "# Article Title\n\n通过 Jina Reader 抓取的完整 markdown 内容..."
}
]
}
附件类型:
| 类型 | 字段 | 说明 |
|---|---|---|
image |
mime, file_id |
上传到 S3 的图片文件 |
doc |
mime, file_id |
上传到 S3 的文档文件 |
link |
url, raw_content |
URL 链接。raw_content 由 summary worker 通过 Jina Reader 抓取后写入 |
文件生命周期¶
pending → ready (正常流程:上传 + confirm)
pending → failed (清理:1 小时内未上传)
ready → deleted (用户删除)
deleted → 彻底删除 (清理:S3 对象在 7 天后移除)
处理状态¶
Item 的上传状态变为 ready 后,processStatus 字段跟踪内容处理管线:
| processStatus | 说明 |
|---|---|
RAW |
刚上传,尚未拆解 |
INGESTED |
已拆解为 content block,download/summary 处理中 |
READY |
所有 content block 已摘要完成 |
FAILED |
至少有一个 content block 失败 |
流转为 RAW → INGESTED → READY。任一 content block 失败(download 或 summary),item 即被置为 FAILED。