trustgraph/docs/tech-specs/large-document-loading.zh-cn.md
Alex Jenkins f95fd4f052
Feat: TrustGraph i18n & Documentation Translation Updates (#781)
Native CLI i18n: The TrustGraph CLI has built-in translation support
that dynamically loads language strings. You can test and use
different languages by simply passing the --lang flag (e.g., --lang
es for Spanish, --lang ru for Russian) or by configuring your
environment's LANG variable.

Automated Docs Translations: This PR introduces autonomously
translated Markdown documentation into several target languages,
including Spanish, Swahili, Portuguese, Turkish, Hindi, Hebrew,
Arabic, Simplified Chinese, and Russian.
2026-04-14 12:07:58 +01:00

992 lines
32 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

---
layout: default
title: "大型文档加载技术规范"
parent: "Chinese (Beta)"
---
# 大型文档加载技术规范
> **Beta Translation:** This document was translated via Machine Learning and as such may not be 100% accurate. All non-English languages are currently classified as Beta.
## 概述
本规范解决了在将大型文档加载到 TrustGraph 时出现的可扩展性和用户体验问题。 当前架构将文档上传视为单个原子操作,这会在管道的多个阶段导致内存压力,并且不为用户提供任何反馈或恢复选项。
本次实施的目标是以下用例:
1. **大型 PDF 处理**: 在不耗尽内存的情况下,上传和处理数百兆字节的 PDF 文件。
2. **可恢复的上传**: 允许中断的上传从中断的地方继续,而不是重新开始。
3. **进度反馈**: 向用户提供有关上传和处理进度的实时可见性。
4. **内存高效处理**: 以流式方式处理文档,而无需将整个文件保存在内存中。
## 目标
**增量上传**: 支持通过 REST 和 WebSocket 进行分块文档上传。
**可恢复传输**: 启用从中断的上传中恢复。
**进度可见性**: 向客户端提供上传/处理进度反馈。
**内存效率**: 消除整个管道中的完整文档缓冲。
**向后兼容性**: 现有的小型文档工作流程保持不变。
**流式处理**: PDF 解码和文本分块操作在流上进行。
## 背景
### 当前架构
文档提交流程如下:
1. **客户端** 通过 REST (`POST /api/v1/librarian`) 或 WebSocket 提交文档。
2. **API 网关** 接收包含 base64 编码的文档内容的完整请求。
3. **LibrarianRequestor** 将请求转换为 Pulsar 消息。
4. **Librarian 服务** 接收消息,将文档解码到内存中。
5. **BlobStore** 将文档上传到 Garage/S3。
6. **Cassandra** 存储包含对象引用的元数据。
7. 为了处理:从 S3 中检索文档,解码,分块——所有都在内存中。
关键文件:
REST/WebSocket 入口:`trustgraph-flow/trustgraph/gateway/service.py`
Librarian 核心:`trustgraph-flow/trustgraph/librarian/librarian.py`
Blob 存储:`trustgraph-flow/trustgraph/librarian/blob_store.py`
Cassandra 表:`trustgraph-flow/trustgraph/tables/library.py`
API 模式:`trustgraph-base/trustgraph/schema/services/library.py`
### 当前限制
当前设计存在几个相互影响的内存和用户体验问题:
1. **原子上传操作**: 整个文档必须在一个请求中传输。
大型文档需要长时间运行的请求,并且在连接失败时没有进度指示和重试机制。
2. **API 设计**: 无论是 REST 还是 WebSocket API都期望在单个消息中接收完整的文档。
模式 (`LibrarianRequest`) 包含一个 `content` 字段,其中包含整个 base64 编码的文档。
3. **Librarian 内存**: librarian 服务在将文档上传到 S3 之前,将其完全解码到内存中。
对于一个 500MB 的 PDF 文件,这意味着需要在进程内存中保留 500MB+。
4. **PDF 解码器内存**: 当处理开始时PDF 解码器将整个 PDF 文件加载到内存中以提取文本。
PyPDF 和类似的库通常需要访问整个文档。
5. **分块器内存**: 文本分块器接收完整的提取文本,并在生成块时将其保存在内存中。
**内存影响示例** (500MB PDF)
网关:~700MB (base64 编码开销)
Librarian~500MB (解码后的字节)
PDF 解码器:~500MB + 提取缓冲区
分块器:提取的文本 (可变,可能高达 100MB+)
对于单个大型文档,峰值内存总和可能超过 2GB。
## 技术设计
### 设计原则
1. **API 接口**: 所有客户端交互都通过 librarian API 进行。客户端
没有直接访问或了解底层 S3/Garage 存储的权限。
2. **S3 多部分上传**: 使用标准的 S3 多部分上传。
这在所有兼容 S3 的系统中都得到广泛支持 (AWS S3, MinIO, Garage,
Ceph, DigitalOcean Spaces, Backblaze B2 等),确保可移植性。
3. **原子完成**: S3 多部分上传本质上是原子的 - 上传的
部分在调用 `CompleteMultipartUpload` 之前是不可见的。不需要临时
文件或重命名操作。
4. **可跟踪的状态**: 上传会话在 Cassandra 中跟踪,从而
提供对未完成上传的可见性,并启用恢复功能。
### 分块上传流程
```
Client Librarian API S3/Garage
│ │ │
│── begin-upload ───────────►│ │
│ (metadata, size) │── CreateMultipartUpload ────►│
│ │◄── s3_upload_id ─────────────│
│◄── upload_id ──────────────│ (store session in │
│ │ Cassandra) │
│ │ │
│── upload-chunk ───────────►│ │
│ (upload_id, index, data) │── UploadPart ───────────────►│
│ │◄── etag ─────────────────────│
│◄── ack + progress ─────────│ (store etag in session) │
│ ⋮ │ ⋮ │
│ (repeat for all chunks) │ │
│ │ │
│── complete-upload ────────►│ │
│ (upload_id) │── CompleteMultipartUpload ──►│
│ │ (parts coalesced by S3) │
│ │── store doc metadata ───────►│ Cassandra
│◄── document_id ────────────│ (delete session) │
```
客户端从不直接与 S3 交互。 库librarian在内部将我们的分块上传 API 转换为 S3 的多部分操作。
### Librarian API 操作
### 图书管理员API操作
#### `begin-upload`
初始化一个分块上传会话。
请求:
```json
{
"operation": "begin-upload",
"document-metadata": {
"id": "doc-123",
"kind": "application/pdf",
"title": "Large Document",
"user": "user-id",
"tags": ["tag1", "tag2"]
},
"total-size": 524288000,
"chunk-size": 5242880
}
```
响应:
```json
{
"upload-id": "upload-abc-123",
"chunk-size": 5242880,
"total-chunks": 100
}
```
馆员:
1. 生成一个唯一的 `upload_id``object_id` (用于对象存储的 UUID)
2. 调用 S3 `CreateMultipartUpload`,接收 `s3_upload_id`
3. 在 Cassandra 中创建会话记录
4.`upload_id` 返回给客户端
#### `upload-chunk`
上传单个分块。
请求:
```json
{
"operation": "upload-chunk",
"upload-id": "upload-abc-123",
"chunk-index": 0,
"content": "<base64-encoded-chunk>"
}
```
响应:
```json
{
"upload-id": "upload-abc-123",
"chunk-index": 0,
"chunks-received": 1,
"total-chunks": 100,
"bytes-received": 5242880,
"total-bytes": 524288000
}
```
馆员:
1. 通过 `upload_id` 查找会话。
2. 验证所有权(用户必须与会话创建者匹配)。
3. 使用块数据调用 S3 `UploadPart`,接收 `etag`
4. 使用块索引和 etag 更新会话记录。
5. 将进度返回给客户端。
失败的块可以重试 - 只需要再次发送相同的 `chunk-index`
#### `complete-upload`
完成上传并创建文档。
请求:
```json
{
"operation": "complete-upload",
"upload-id": "upload-abc-123"
}
```
响应:
```json
{
"document-id": "doc-123",
"object-id": "550e8400-e29b-41d4-a716-446655440000"
}
```
馆员:
1. 查找会话,验证是否接收到所有分块。
2. 使用 part etags 调用 S3 `CompleteMultipartUpload` (S3 内部合并分块,馆员无需消耗内存)。
3. 在 Cassandra 中创建文档记录,包含元数据和对象引用。
4. 删除上传会话记录。
5. 将文档 ID 返回给客户端。
#### `abort-upload`
取消正在进行的上传。
请求:
```json
{
"operation": "abort-upload",
"upload-id": "upload-abc-123"
}
```
馆员:
1. 调用 S3 `AbortMultipartUpload` 清理部分数据。
2. 从 Cassandra 中删除会话记录。
#### `get-upload-status`
查询上传状态(用于断点续传功能)。
请求:
```json
{
"operation": "get-upload-status",
"upload-id": "upload-abc-123"
}
```
响应:
```json
{
"upload-id": "upload-abc-123",
"state": "in-progress",
"chunks-received": [0, 1, 2, 5, 6],
"missing-chunks": [3, 4, 7, 8],
"total-chunks": 100,
"bytes-received": 36700160,
"total-bytes": 524288000
}
```
#### `list-uploads`
获取用户未完成上传的列表。
请求:
```json
{
"operation": "list-uploads"
}
```
响应:
```json
{
"uploads": [
{
"upload-id": "upload-abc-123",
"document-metadata": { "title": "Large Document", ... },
"progress": { "chunks-received": 43, "total-chunks": 100 },
"created-at": "2024-01-15T10:30:00Z"
}
]
}
```
### 上传会话存储
在 Cassandra 中跟踪正在进行的上传:
```sql
CREATE TABLE upload_session (
upload_id text PRIMARY KEY,
user text,
document_id text,
document_metadata text, -- JSON: title, kind, tags, comments, etc.
s3_upload_id text, -- internal, for S3 operations
object_id uuid, -- target blob ID
total_size bigint,
chunk_size int,
total_chunks int,
chunks_received map<int, text>, -- chunk_index → etag
created_at timestamp,
updated_at timestamp
) WITH default_time_to_live = 86400; -- 24 hour TTL
CREATE INDEX upload_session_user ON upload_session (user);
```
**TTL 行为:**
如果未完成,会话将在 24 小时后过期。
当 Cassandra TTL 过期时,会话记录将被删除。
孤立的 S3 分片由 S3 生命周期策略清理(在桶上配置)。
### 故障处理和原子性
**分块上传失败:**
客户端重试失败的分块(使用相同的 `upload_id``chunk-index`)。
S3 `UploadPart` 对于相同的分片编号是幂等的。
会话跟踪哪些分块已成功。
**客户端在上传过程中断连接:**
会话仍然存在于 Cassandra 中,记录了已接收的分块。
客户端可以调用 `get-upload-status` 以查看缺少的内容。
通过仅上传缺失的分块来恢复,然后调用 `complete-upload`
**完整上传失败:**
S3 `CompleteMultipartUpload` 是原子性的 - 要么完全成功,要么完全失败。
如果失败,分片仍然存在,客户端可以重试 `complete-upload`
永远不会出现部分文档。
**会话过期:**
Cassandra TTL 在 24 小时后删除会话记录。
S3 桶生命周期策略清理不完整的多部分上传。
不需要手动清理。
### S3 多部分原子性
S3 多部分上传提供了内置的原子性:
1. **分片不可见:** 上传的分片不能作为对象访问。
它们仅作为不完整的多部分上传的分片存在。
2. **原子完成:** `CompleteMultipartUpload` 要么成功(对象
以原子方式出现),要么失败(未创建对象)。 没有部分状态。
3. **无需重命名:** 最终对象键在
`CreateMultipartUpload` 时指定。 分片直接合并到该键。
4. **服务器端合并:** S3 内部合并分片。 库管理员
永远不会读取分片 - 无论文档大小如何,都没有内存开销。
### BlobStore 扩展
**文件:** `trustgraph-flow/trustgraph/librarian/blob_store.py`
添加多部分上传方法:
```python
class BlobStore:
# Existing methods...
def create_multipart_upload(self, object_id: UUID, kind: str) -> str:
"""Initialize multipart upload, return s3_upload_id."""
# minio client: create_multipart_upload()
def upload_part(
self, object_id: UUID, s3_upload_id: str,
part_number: int, data: bytes
) -> str:
"""Upload a single part, return etag."""
# minio client: upload_part()
# Note: S3 part numbers are 1-indexed
def complete_multipart_upload(
self, object_id: UUID, s3_upload_id: str,
parts: List[Tuple[int, str]] # [(part_number, etag), ...]
) -> None:
"""Finalize multipart upload."""
# minio client: complete_multipart_upload()
def abort_multipart_upload(
self, object_id: UUID, s3_upload_id: str
) -> None:
"""Cancel multipart upload, clean up parts."""
# minio client: abort_multipart_upload()
```
### 分块大小的考量
**S3 最小值**: 每个分块 5MB (除了最后一个分块)
**S3 最大值**: 每个上传 10,000 个分块
**实际默认值**: 5MB 分块
500MB 文档 = 100 个分块
5GB 文档 = 1,000 个分块
**进度粒度**: 较小的分块 = 更精细的进度更新
**网络效率**: 较大的分块 = 更少的网络请求
分块大小可以在一定范围内由客户端配置 (5MB - 100MB)。
### 文档处理:流式检索
上传流程旨在高效地将文档存储到存储系统中。处理流程旨在提取和分块文档,而无需将整个文档加载到内存中。
#### 设计原则:标识符,而非内容
目前,当触发处理时,文档内容通过 Pulsar 消息传递。这会将整个文档加载到内存中。 应该这样做:
Pulsar 消息仅携带 **文档标识符**
处理程序直接从 librarian 获取文档内容
获取过程是 **流式传输到临时文件**
文档特定的解析 (PDF、文本等) 使用文件,而不是内存缓冲区
这使得 librarian 不依赖于文档结构。 PDF 解析、文本
提取和其他特定于格式的逻辑保留在各自的解码器中。
#### 处理流程
```
Pulsar PDF Decoder Librarian S3
│ │ │ │
│── doc-id ───────────►│ │ │
│ (processing msg) │ │ │
│ │ │ │
│ │── stream-document ──────►│ │
│ │ (doc-id) │── GetObject ────►│
│ │ │ │
│ │◄── chunk ────────────────│◄── stream ───────│
│ │ (write to temp file) │ │
│ │◄── chunk ────────────────│◄── stream ───────│
│ │ (append to temp file) │ │
│ │ ⋮ │ ⋮ │
│ │◄── EOF ──────────────────│ │
│ │ │ │
│ │ ┌──────────────────────────┐ │
│ │ │ temp file on disk │ │
│ │ │ (memory stays bounded) │ │
│ │ └────────────┬─────────────┘ │
│ │ │ │
│ │ PDF library opens file │
│ │ extract page 1 text ──► chunker │
│ │ extract page 2 text ──► chunker │
│ │ ⋮ │
│ │ close file │
│ │ delete temp file │
```
#### 图书管理员流式 API
添加一个流式文档检索操作:
**`stream-document`**
请求:
```json
{
"operation": "stream-document",
"document-id": "doc-123"
}
```
响应:流式传输的二进制数据块(不是单个响应)。
对于 REST API这会返回一个带有 `Transfer-Encoding: chunked` 的流式响应。
对于内部服务之间的调用(处理器到图书管理员),可能如下:
通过预签名 URL 进行直接 S3 流式传输(如果内部网络允许)。
通过服务协议进行分块响应。
一个专用的流式传输端点。
关键要求:数据以块的形式流动,永远不会完全缓存在图书管理员中。
#### PDF 解码器更改
**当前实现**(占用大量内存):
```python
def decode_pdf(document_content: bytes) -> str:
reader = PdfReader(BytesIO(document_content)) # full doc in memory
text = ""
for page in reader.pages:
text += page.extract_text() # accumulating
return text # full text in memory
```
**新实现** (临时文件,增量式)
```python
def decode_pdf_streaming(doc_id: str, librarian_client) -> Iterator[str]:
"""Yield extracted text page by page."""
with tempfile.NamedTemporaryFile(delete=True, suffix='.pdf') as tmp:
# Stream document to temp file
for chunk in librarian_client.stream_document(doc_id):
tmp.write(chunk)
tmp.flush()
# Open PDF from file (not memory)
reader = PdfReader(tmp.name)
# Yield pages incrementally
for page in reader.pages:
yield page.extract_text()
# tmp file auto-deleted on context exit
```
内存配置:
临时文件在磁盘上:大小为 PDF 文件的大小(磁盘很便宜)
内存中:一次加载一页的文本
峰值内存:受限,与文档大小无关
#### 文本文档解码器更改
对于纯文本文档,更加简单 - 不需要临时文件:
```python
def decode_text_streaming(doc_id: str, librarian_client) -> Iterator[str]:
"""Yield text in chunks as it streams from storage."""
buffer = ""
for chunk in librarian_client.stream_document(doc_id):
buffer += chunk.decode('utf-8')
# Yield complete lines/paragraphs as they arrive
while '\n\n' in buffer:
paragraph, buffer = buffer.split('\n\n', 1)
yield paragraph + '\n\n'
# Yield remaining buffer
if buffer:
yield buffer
```
文本文件可以直接流式传输,无需临时文件,因为它们的结构是线性的。
#### 流式分块集成
分块器接收文本的迭代器(页面或段落),并逐步生成块:
```python
class StreamingChunker:
def __init__(self, chunk_size: int, overlap: int):
self.chunk_size = chunk_size
self.overlap = overlap
def process(self, text_stream: Iterator[str]) -> Iterator[str]:
"""Yield chunks as text arrives."""
buffer = ""
for text_segment in text_stream:
buffer += text_segment
while len(buffer) >= self.chunk_size:
chunk = buffer[:self.chunk_size]
yield chunk
# Keep overlap for context continuity
buffer = buffer[self.chunk_size - self.overlap:]
# Yield remaining buffer as final chunk
if buffer.strip():
yield buffer
```
#### 端到端处理流程
```python
async def process_document(doc_id: str, librarian_client, embedder):
"""Process document with bounded memory."""
# Get document metadata to determine type
metadata = await librarian_client.get_document_metadata(doc_id)
# Select decoder based on document type
if metadata.kind == 'application/pdf':
text_stream = decode_pdf_streaming(doc_id, librarian_client)
elif metadata.kind == 'text/plain':
text_stream = decode_text_streaming(doc_id, librarian_client)
else:
raise UnsupportedDocumentType(metadata.kind)
# Chunk incrementally
chunker = StreamingChunker(chunk_size=1000, overlap=100)
# Process each chunk as it's produced
for chunk in chunker.process(text_stream):
# Generate embeddings, store in vector DB, etc.
embedding = await embedder.embed(chunk)
await store_chunk(doc_id, chunk, embedding)
```
在任何时候,完整的文档或完整的提取文本都不会保存在内存中。
#### 临时文件注意事项
**位置:** 使用系统临时目录(`/tmp` 或等效目录)。对于
容器化部署,请确保临时目录有足够的空间,并且位于快速存储上(如果可能,不要使用网络挂载)。
**清理:** 使用上下文管理器(`with tempfile...`)以确保即使在出现异常时也能进行清理。
**并发处理:** 每个处理任务都有自己的临时文件。
并行文档处理之间不会发生冲突。
**磁盘空间:** 临时文件是短暂存在的(处理过程中的持续时间)。
对于一个 500MB 的 PDF 文件,在处理过程中需要 500MB 的临时空间。如果磁盘空间受限,可以在上传时强制执行大小限制。
### 统一处理接口:子文档
PDF 提取和文本文档处理需要输入到相同的下游流程(分块 → 嵌入 → 存储)。为了实现这一点,并使用一致的“按 ID 检索”接口,提取的文本块被存储回 librarian作为子文档。
#### 使用子文档的处理流程
```
PDF Document Text Document
│ │
▼ │
pdf-extractor │
│ │
│ (stream PDF from librarian) │
│ (extract page 1 text) │
│ (store as child doc → librarian) │
│ (extract page 2 text) │
│ (store as child doc → librarian) │
│ ⋮ │
▼ ▼
[child-doc-id, child-doc-id, ...] [doc-id]
│ │
└─────────────────────┬───────────────────────────────┘
chunker
│ (receives document ID)
│ (streams content from librarian)
│ (chunks incrementally)
[chunks → embedding → storage]
```
分块器具有一个统一的接口:
接收文档 ID通过 Pulsar
从 Librarian 流式传输内容
将其分块
它不知道也不关心该 ID 指的是:
用户上传的文本文档
从 PDF 页面提取的文本块
任何未来的文档类型
#### 子文档元数据
扩展文档模式以跟踪父/子关系:
```sql
-- Add columns to document table
ALTER TABLE document ADD parent_id text;
ALTER TABLE document ADD document_type text;
-- Index for finding children of a parent
CREATE INDEX document_parent ON document (parent_id);
```
**文档类型:**
| `document_type` | 描述 |
|-----------------|-------------|
| `source` | 用户上传的文档PDF、文本等 |
| `extracted` | 源自源文档例如PDF 页面文本) |
**元数据字段:**
| 字段 | 源文档 | 提取的子文档 |
|-------|-----------------|-----------------|
| `id` | 用户提供或生成 | 生成(例如,`{parent-id}-page-{n}` |
| `parent_id` | `NULL` | 父文档 ID |
| `document_type` | `source` | `extracted` |
| `kind` | `application/pdf`,等等 | `text/plain` |
| `title` | 用户提供 | 生成例如“Report.pdf 的第 3 页”) |
| `user` | 经过身份验证的用户 | 与父文档相同 |
#### 子文档的 Librarian API
**创建子文档**(内部,由 pdf-extractor 使用):
```json
{
"operation": "add-child-document",
"parent-id": "doc-123",
"document-metadata": {
"id": "doc-123-page-1",
"kind": "text/plain",
"title": "Page 1"
},
"content": "<base64-encoded-text>"
}
```
对于较小的提取文本(典型页面文本通常小于 100KB单次上传是可以接受的。对于非常大的文本提取可以使用分块上传。
**列出子文档(用于调试/管理):**
```json
{
"operation": "list-children",
"parent-id": "doc-123"
}
```
响应:
```json
{
"children": [
{ "id": "doc-123-page-1", "title": "Page 1", "kind": "text/plain" },
{ "id": "doc-123-page-2", "title": "Page 2", "kind": "text/plain" },
...
]
}
```
#### 用户可见的行为
**`list-documents` 默认行为:**
```sql
SELECT * FROM document WHERE user = ? AND parent_id IS NULL;
```
只有顶层(源)文档才会出现在用户的文档列表中。
子文档默认会被过滤掉。
**可选的包含子文档标志**(用于管理员/调试):
```json
{
"operation": "list-documents",
"include-children": true
}
```
#### 级联删除
当父级文档被删除时,所有子级必须被删除:
```python
def delete_document(doc_id: str):
# Find all children
children = query("SELECT id, object_id FROM document WHERE parent_id = ?", doc_id)
# Delete child blobs from S3
for child in children:
blob_store.delete(child.object_id)
# Delete child metadata from Cassandra
execute("DELETE FROM document WHERE parent_id = ?", doc_id)
# Delete parent blob and metadata
parent = get_document(doc_id)
blob_store.delete(parent.object_id)
execute("DELETE FROM document WHERE id = ? AND user = ?", doc_id, user)
```
#### 存储注意事项
提取的文本块会重复内容:
原始 PDF 文件存储在 Garage 中
每个页面的提取文本也存储在 Garage 中
这种权衡方案可以实现:
**统一的分块接口**: 分块器始终通过 ID 获取数据
**恢复/重试**: 可以在分块阶段重新启动,而无需重新提取 PDF
**调试**: 可以检查提取的文本
**职责分离**: PDF 提取器和分块器是独立的服务
对于一个 500MB 的 PDF 文件,其中包含 200 页,平均每页 5KB 的文本:
PDF 存储500MB
提取的文本存储:约 1MB
额外开销:可以忽略不计
#### PDF 提取器输出
PDF 提取器在处理文档后:
1. 从 librarian 流式传输 PDF 到临时文件
2. 逐页提取文本
3. 对于每一页,将提取的文本作为子文档通过 librarian 存储
4. 将子文档 ID 发送到分块器队列
```python
async def extract_pdf(doc_id: str, librarian_client, output_queue):
"""Extract PDF pages and store as child documents."""
with tempfile.NamedTemporaryFile(delete=True, suffix='.pdf') as tmp:
# Stream PDF to temp file
for chunk in librarian_client.stream_document(doc_id):
tmp.write(chunk)
tmp.flush()
# Extract pages
reader = PdfReader(tmp.name)
for page_num, page in enumerate(reader.pages, start=1):
text = page.extract_text()
# Store as child document
child_id = f"{doc_id}-page-{page_num}"
await librarian_client.add_child_document(
parent_id=doc_id,
document_id=child_id,
kind="text/plain",
title=f"Page {page_num}",
content=text.encode('utf-8')
)
# Send to chunker queue
await output_queue.send(child_id)
```
分块器接收这些子 ID并以与处理用户上传的文本文档完全相同的方式处理它们。
如何处理用户上传的文本文档。
### 客户端更新
#### Python SDK
Python SDK (`trustgraph-base/trustgraph/api/library.py`) 应该能够透明地处理分块上传。公共接口保持不变:
分块上传。公共接口保持不变:
```python
# Existing interface - no change for users
library.add_document(
id="doc-123",
title="Large Report",
kind="application/pdf",
content=large_pdf_bytes, # Can be hundreds of MB
tags=["reports"]
)
```
内部SDK 会检测文档大小并切换策略:
```python
class Library:
CHUNKED_UPLOAD_THRESHOLD = 2 * 1024 * 1024 # 2MB
def add_document(self, id, title, kind, content, tags=None, ...):
if len(content) < self.CHUNKED_UPLOAD_THRESHOLD:
# Small document: single operation (existing behavior)
return self._add_document_single(id, title, kind, content, tags)
else:
# Large document: chunked upload
return self._add_document_chunked(id, title, kind, content, tags)
def _add_document_chunked(self, id, title, kind, content, tags):
# 1. begin-upload
session = self._begin_upload(
document_metadata={...},
total_size=len(content),
chunk_size=5 * 1024 * 1024
)
# 2. upload-chunk for each chunk
for i, chunk in enumerate(self._chunk_bytes(content, session.chunk_size)):
self._upload_chunk(session.upload_id, i, chunk)
# 3. complete-upload
return self._complete_upload(session.upload_id)
```
**进度回调** (可选的增强功能)
```python
def add_document(self, ..., on_progress=None):
"""
on_progress: Optional callback(bytes_sent, total_bytes)
"""
```
这允许用户界面显示上传进度,而无需更改基本 API。
#### 命令行工具
**`tg-add-library-document`** 保持不变:
```bash
# Works transparently for any size - SDK handles chunking internally
tg-add-library-document --file large-report.pdf --title "Large Report"
```
可以添加可选的进度显示:
```bash
tg-add-library-document --file large-report.pdf --title "Large Report" --progress
# Output:
# Uploading: 45% (225MB / 500MB)
```
**已移除的旧工具:**
`tg-load-pdf` - 已过时,请使用 `tg-add-library-document`
`tg-load-text` - 已过时,请使用 `tg-add-library-document`
**管理员/调试命令**(可选,优先级较低):
```bash
# List incomplete uploads (admin troubleshooting)
tg-add-library-document --list-pending
# Resume specific upload (recovery scenario)
tg-add-library-document --resume upload-abc-123 --file large-report.pdf
```
这些可能只是现有命令上的标志,而不是单独的工具。
#### API 规范更新
OpenAPI 规范 (`specs/api/paths/librarian.yaml`) 需要更新以下内容:
**新操作:**
`begin-upload` - 初始化分块上传会话
`upload-chunk` - 上传单个分块
`complete-upload` - 完成上传
`abort-upload` - 取消上传
`get-upload-status` - 查询上传进度
`list-uploads` - 列出用户未完成的上传
`stream-document` - 流式文档检索
`add-child-document` - 存储提取的文本(内部)
`list-children` - 列出子文档(管理员)
**修改后的操作:**
`list-documents` - 添加 `include-children` 参数
**新的模式:**
`ChunkedUploadBeginRequest`
`ChunkedUploadBeginResponse`
`ChunkedUploadChunkRequest`
`ChunkedUploadChunkResponse`
`UploadSession`
`UploadProgress`
**WebSocket 规范更新** (`specs/websocket/`)
镜像 REST 操作以供 WebSocket 客户端使用,从而实现上传过程的实时
进度更新。
#### 用户体验注意事项
API 规范的更新可以实现前端改进:
**上传进度 UI**
显示已上传分块的进度条
剩余预估时间
暂停/恢复功能
**错误恢复:**
中断的上传可以选择“恢复上传”
重新连接时,显示挂起的上传列表
**大型文件处理:**
客户端文件大小检测
大型文件的自动分块上传
长时间上传时提供清晰的反馈
这些用户体验改进需要前端工作,并由更新后的 API 规范提供指导。