Structure the tech specs directory (#836)

Tech spec some subdirectories for different languages
This commit is contained in:
cybermaggedon 2026-04-21 16:06:41 +01:00 committed by GitHub
parent 48da6c5f8b
commit e7efb673ef
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
423 changed files with 0 additions and 0 deletions

View file

@ -1,992 +0,0 @@
---
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 规范提供指导。