# 进度与预览

[HTML](https://pdfcraft.ai/zh-CN/docs/api/preview/) · [Site index](https://pdfcraft.ai/llms.txt)

查询处理进度，读取文本和图片预览。


只需要最终结果时，轮询 `GET /transforms/{uuid}` 即可。展示处理进度或预览内容时，使用 observation 接口。

## Observation 响应

`GET /transforms/{uuid}/observation?preview_limit=100`

```json
{
  "status": "running",
  "progress": {
    "queued": false,
    "stage": "converting",
    "completed_words": 1250,
    "total_words": 8000
  },
  "preview": {
    "cursor": "opaque-cursor",
    "updated": [
      {
        "file": "converting/body",
        "url": "https://storage.example/body.jsonl?signed",
        "range": {"start": 0, "end": 240}
      }
    ]
  }
}
```

示例地址与偏移仅说明响应结构；实际读取时使用任务返回的完整 URL 和范围。

`progress` 只可能在任务运行时返回。`queued` 为 true 表示仍在排队，此时无法计算进度百分比。执行阶段包括 `preparing`、`extracting`、`converting`、`packaging`、`uploading`。

页数由 `completed_pages` 和 `total_pages` 表示，原文处理量由 `completed_words` 和 `total_words` 表示。这些字段可能缺失，不能按零处理。只有总量大于零时，才能计算百分比。

进度对应当前操作，切换阶段后总量可能变化。这些数字不用于表示费用。

处理量可能包含回退处理的内容，不能据此判断翻译是否完整。任务完成以 `status: "succeeded"` 为准。

## 读取更新并推进 cursor

每个任务需要分别保存 `preview.cursor`。下次请求 observation 时，将它通过 `URLSearchParams` 编码，作为 `cursor` 参数传入，不要修改其内容。

响应始终包含 `preview.cursor`；没有新内容时，可能不返回 `updated`。

1. 请求 `updated` 中的每个 URL，无需传入 API Key，也不要缓存响应。
2. 有 `range` 时发送 `Range: bytes=start-(end-1)`。偏移按 UTF-8 字节计算，包含 start、不包含 end。
3. 要求响应为 `206`，完整读取字节后解码 UTF-8，再解析完整 JSONL 记录。元数据更新没有 range，读取为完整 JSON。
4. 所有更新都读取并处理成功后，再保存新的 cursor。任何一步失败都保留旧值，稍后重试。重试可能读到相同内容，可按任务、内容流和字节范围去重，避免重复添加。

以下函数读取指定字节范围内的 JSONL 记录：

```javascript
async function readPreviewRange(update) {
  const { start, end } = update.range;
  const response = await fetch(update.url, {
    headers: { Range: `bytes=${start}-${end - 1}` },
    cache: "no-store",
  });
  if (response.status !== 206) throw new Error(`Preview HTTP ${response.status}`);
  const bytes = await response.arrayBuffer();
  if (bytes.byteLength !== end - start) throw new Error("Incomplete preview range");
  const text = new TextDecoder("utf-8", { fatal: true }).decode(bytes);
  if (!text.endsWith("\n")) throw new Error("Incomplete JSONL record");
  return text.slice(0, -1).split("\n").map(line => JSON.parse(line));
}
```

轮询、元数据读取和状态更新需由调用方处理。预览请求不携带 API Key；展示文档内容时需防止脚本注入。

## 预览范围

`preview_limit` 控制每个 JSONL 流返回的记录数，默认为 500，传入更大的值也按 500 处理。首次请求返回最近的记录；如果读取速度跟不上处理速度，可能会跳过较早的内容。

因此，不能通过 cursor 遍历整份文档。完整内容请从转换结果中下载。

任务结束时，请处理完最后一批更新再停止查询。成功后，响应的 `download_urls` 中会包含 `{get_url, head_url}`，下载时使用默认文件名。如需指定名称，请另行请求 `/download-urls?filename=...`。

失败的任务也可能留下部分预览，但内容并不完整。

## 内容流与资源

| 内容流 | 内容 |
| --- | --- |
| `extraction/metadata`、`converting/metadata` | 完整 `{metadata, original?}` JSON；替换旧视图，不作增量合并 |
| `extraction/body`、`extraction/footnotes` | PDF 提取记录，包含 `content` 树和可选原文区域 |
| PDF 的 `converting/body`、`converting/footnotes` | 自然段记录，含 `text`、可选 `original` 和原文 `regions` |
| EPUB 的 `converting/body` | 纯文本片段，含 `original`、`translated` 和可选 `epubcfi` |

PDF 提取预览可能与最终章节结构不同。EPUB 没有 extraction 或独立脚注流。记录可能带可选的 `emitted_at`；应兼容没有该字段的旧记录，并忽略未知的新增字段。

预览内容中的图片和公式用资源哈希标识。将哈希填入 `GET /transforms/{uuid}/preview-assets/{extraction|converting}/{hash}.{png|pdf}`，即可获取文件地址。

读取文件时无需传入 API Key。链接过期后，重新调用该接口获取即可。
