appQQQ commited on
Commit
a0cc0b1
·
verified ·
1 Parent(s): f9e7866

chore: upload app/api/documents.py

Browse files
Files changed (1) hide show
  1. app/api/documents.py +105 -0
app/api/documents.py ADDED
@@ -0,0 +1,105 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ """文档管理 API. 阶段 2: 完整 upload / list / delete / get / chunks."""
2
+ from __future__ import annotations
3
+
4
+ import logging
5
+
6
+ from fastapi import APIRouter, File, HTTPException, Query, UploadFile
7
+
8
+ from app.core.errors import AppError, DocumentNotFoundError
9
+ from app.models import db
10
+ from app.models.schemas import DocumentChunk, IngestResult
11
+ from app.services.ingestion import (
12
+ delete_document,
13
+ get_document,
14
+ ingest_bytes,
15
+ list_documents,
16
+ )
17
+
18
+ logger = logging.getLogger(__name__)
19
+ router = APIRouter(prefix="/documents", tags=["documents"])
20
+
21
+
22
+ @router.get("")
23
+ async def list_docs(
24
+ limit: int = Query(default=100, ge=1, le=500),
25
+ offset: int = Query(default=0, ge=0),
26
+ ) -> dict:
27
+ docs = list_documents(limit=limit)
28
+ return {
29
+ "documents": docs[offset : offset + limit],
30
+ "total": len(docs),
31
+ }
32
+
33
+
34
+ @router.get("/{doc_id}")
35
+ async def get_doc(doc_id: str) -> dict:
36
+ doc = get_document(doc_id)
37
+ if doc is None:
38
+ raise DocumentNotFoundError(f"Document {doc_id} not found", code="doc_not_found")
39
+ return doc
40
+
41
+
42
+ @router.get("/{doc_id}/chunks")
43
+ async def list_chunks(
44
+ doc_id: str,
45
+ limit: int = Query(default=200, ge=1, le=1000),
46
+ offset: int = Query(default=0, ge=0),
47
+ ) -> dict:
48
+ """获取文档的 chunks 列表, 用于前端预览拆分结果.
49
+
50
+ 返回顺序: 按 chunk_index ASC. 包含 text / token_count / page_no / heading / context_prefix.
51
+ """
52
+ doc = get_document(doc_id)
53
+ if doc is None:
54
+ raise DocumentNotFoundError(f"Document {doc_id} not found", code="doc_not_found")
55
+ rows = db.chunk_get_by_doc(doc_id, limit=limit + offset)
56
+ # 截取 offset
57
+ rows = rows[offset : offset + limit]
58
+ return {
59
+ "doc_id": doc_id,
60
+ "chunks": [DocumentChunk(**r).model_dump() for r in rows],
61
+ "total": doc.get("chunk_count", len(rows)),
62
+ "returned": len(rows),
63
+ }
64
+
65
+
66
+ @router.post("/upload", response_model=IngestResult)
67
+ async def upload(file: UploadFile = File(...)) -> IngestResult:
68
+ """上传并摄入文档. 支持 PDF / DOCX / 图片.
69
+
70
+ 流程: 读字节 -> 摄入编排器 (内部 SHA256 查重 + 解析 + 分块 + 向量化 + 入库)
71
+ """
72
+ if file.filename is None:
73
+ raise HTTPException(status_code=400, detail="filename missing")
74
+
75
+ # 限制大小 (50MB; HF Space free 16GB RAM 下够用)
76
+ MAX_BYTES = 50 * 1024 * 1024
77
+ content = await file.read(MAX_BYTES + 1)
78
+ if len(content) > MAX_BYTES:
79
+ raise HTTPException(
80
+ status_code=413,
81
+ detail=f"File too large (> {MAX_BYTES // 1024 // 1024}MB). "
82
+ "Consider chunked upload via S3/R2 (TODO 阶段 5).",
83
+ )
84
+
85
+ try:
86
+ result = await ingest_bytes(content=content, filename=file.filename)
87
+ except AppError:
88
+ # 业务异常 (e.g. IngestionFailedError) 直接抛, 走全局 AppError handler
89
+ # 保留 .code / .message / .detail (含 last_traceback 等诊断信息)
90
+ logger.exception("Upload failed (AppError)")
91
+ raise
92
+ except Exception as e: # noqa: BLE001
93
+ # 兜底: 未知异常, 包成 HTTP 502
94
+ logger.exception("Upload failed (unexpected)")
95
+ raise HTTPException(status_code=502, detail=f"Ingest failed: {e}") from e
96
+
97
+ return result
98
+
99
+
100
+ @router.delete("/{doc_id}")
101
+ async def delete_doc(doc_id: str) -> dict:
102
+ ok = await delete_document(doc_id)
103
+ if not ok:
104
+ raise DocumentNotFoundError(f"Document {doc_id} not found", code="doc_not_found")
105
+ return {"doc_id": doc_id, "deleted": True}