feat(data-process): 增加 Office 原文件预览接口

This commit is contained in:
caoxiaozhu
2026-07-27 16:12:50 +08:00
parent 4623e3fa1c
commit 9025437a37
3 changed files with 482 additions and 5 deletions

View File

@@ -53,6 +53,11 @@ from app.modules.data_process.document_chunking import (
merge_short_chunks,
)
from app.modules.data_process.generation import generate_model_records
from app.modules.data_process.office_preview import (
MAX_XLSX_PREVIEW_ROWS,
build_docx_preview,
build_xlsx_preview,
)
from app.modules.data_process.storage import (
LocalDataProcessStorage,
StagedSourceObject,
@@ -111,6 +116,12 @@ LEGACY_OFFICE_CONVERSIONS = {
".xls": ".xlsx",
".ppt": ".pptx",
}
RAW_INLINE_PREVIEW_MEDIA_TYPES = {
"pdf": "application/pdf",
"docx": "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
"xlsx": "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
"pptx": "application/vnd.openxmlformats-officedocument.presentationml.presentation",
}
def ok(data: Any = None, message: str = "ok") -> dict[str, Any]:
@@ -897,8 +908,13 @@ def source_file_raw(
) -> StreamingResponse:
with api_errors():
source = store.get_source_file(task_id, file_id, include_content=False)
if str(source.get("file_format") or "").lower() != "pdf":
raise fail(415, "raw inline preview is only available for PDF source files")
file_format = str(source.get("file_format") or "").lower()
media_type = RAW_INLINE_PREVIEW_MEDIA_TYPES.get(file_format)
if media_type is None:
raise fail(
415,
"raw inline preview is only available for PDF and modern Office source files",
)
storage_object_id = str(source.get("storage_object_id") or "")
actual_size = storage.file_size(
storage_object_id,
@@ -906,14 +922,15 @@ def source_file_raw(
expected_source_file_id=file_id,
)
if actual_size is None:
raise fail(410, "the original PDF is unavailable for this legacy source file")
raise fail(410, "the original file is unavailable for this legacy source file")
expected_size = int(source.get("size_bytes") or 0)
if actual_size != expected_size:
raise ValueError("source object size does not match metadata")
selected_range = _source_byte_range(range_header, actual_size)
start, end = selected_range or (0, actual_size - 1)
length = end - start + 1
name = _safe_file_name(str(source.get("name") or "source.pdf"), "source.pdf")
default_name = f"source.{file_format}"
name = _safe_file_name(str(source.get("name") or default_name), default_name)
headers = {
"Accept-Ranges": "bytes",
"Cache-Control": "private, no-store",
@@ -938,11 +955,61 @@ def source_file_raw(
return StreamingResponse(
body,
status_code=206 if selected_range is not None else 200,
media_type="application/pdf",
media_type=media_type,
headers=headers,
)
@router.get("/{task_id}/source-files/{file_id}/office-preview")
def source_file_office_preview(
task_id: str,
file_id: str,
sheet_index: int = Query(default=0, ge=0),
offset: int = Query(default=0, ge=0),
limit: int = Query(default=100, ge=1, le=MAX_XLSX_PREVIEW_ROWS),
store: DataProcessStore = Depends(get_data_process_store),
storage: LocalDataProcessStorage = Depends(get_data_process_storage),
) -> dict[str, Any]:
"""返回 Word 版式块或 Excel 工作表网格,不把二进制内容下发给组件解析。"""
with api_errors():
source = store.get_source_file(task_id, file_id, include_content=False)
file_format = str(source.get("file_format") or "").lower()
if file_format not in {"docx", "xlsx"}:
raise fail(415, "Office preview is only available for DOCX and XLSX source files")
storage_object_id = str(source.get("storage_object_id") or "")
actual_size = storage.file_size(
storage_object_id,
expected_task_id=task_id,
expected_source_file_id=file_id,
)
if actual_size is None:
raise fail(410, "the original Office file is unavailable for this legacy source file")
expected_size = int(source.get("size_bytes") or 0)
if actual_size != expected_size:
raise ValueError("source object size does not match metadata")
raw = b"".join(
storage.iter_bytes(
storage_object_id,
expected_task_id=task_id,
expected_source_file_id=file_id,
expected_size=actual_size,
)
)
preview = (
build_docx_preview(raw)
if file_format == "docx"
else build_xlsx_preview(
raw,
sheet_index=sheet_index,
offset=offset,
limit=limit,
)
)
preview["file_name"] = str(source.get("name") or f"source.{file_format}")
return ok(preview)
@router.get("/{task_id}/source-files/{file_id}/pdf-pages")
def source_file_pdf_pages(
task_id: str,