跳转至

内容上传 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 的字段添加。

const form = new FormData();
for (const [key, value] of Object.entries(fields)) {
  form.append(key, value);
}
form.append('file', fileBlob);
await fetch(url, { method: 'POST', body: form });

过期时间

预签名 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)— 全部确认成功:

{
  "itemId": "019e4596-2b4a-...",
  "status": "ready"
}

响应(200)— 部分文件失败:

{
  "itemId": "019e4596-2b4a-...",
  "status": "partial",
  "failed": [1]
}

failed 数组包含无法验证的附件索引。客户端可以重新上传这些文件并再次调用 confirm。

错误:

状态码 原因
404 Item 不存在
403 不是 item 的所有者
409 Item 不是 pending 状态

GET /api/raw-items

列出当前用户的 items,支持分页。按创建时间倒序(最新的在前)。响应不包含 contentuserId 字段。

查询参数:

参数 说明 默认值
status 按状态过滤:pendingreadypartial 全部
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": "2026-05-31T08:00:00Z",
  "RAW": 3,
  "INGESTED": 1,
  "READY": 12,
  "FAILED": 2
}
字段 说明
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