feat(data-process): 增加 Office 原文件预览接口
This commit is contained in:
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user