From 28553aba152e7841f4daad3f1fc1dfe114655431 Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 4 Sep 2026 13:47:46 +0800 Subject: [PATCH 1/3] fix(avatar): allow knowledge uploads up to 20MB --- digital-avatar-app/backend/routers/knowledge.py | 4 ++-- digital-avatar-app/nginx.conf | 4 ++++ 2 files changed, 6 insertions(+), 2 deletions(-) diff --git a/digital-avatar-app/backend/routers/knowledge.py b/digital-avatar-app/backend/routers/knowledge.py index 2b2a4fa..23de111 100644 --- a/digital-avatar-app/backend/routers/knowledge.py +++ b/digital-avatar-app/backend/routers/knowledge.py @@ -17,7 +17,7 @@ UPLOAD_DIR = os.path.abspath(os.getenv("UPLOAD_DIR", os.path.join(BASE_DIR, "upl os.makedirs(UPLOAD_DIR, exist_ok=True) ALLOWED_EXT = {".md", ".txt", ".pdf", ".doc", ".docx", ".xlsx"} -MAX_UPLOAD_BYTES = 10 * 1024 * 1024 +MAX_UPLOAD_BYTES = 20 * 1024 * 1024 class QAIn(BaseModel): @@ -82,7 +82,7 @@ async def upload_doc(avatar_id: str, file: UploadFile = File(...), authorization path = os.path.join(avatar_dir, stored) content = await file.read() if len(content) > MAX_UPLOAD_BYTES: - return fail("文件不能超过 10MB", code=400) + return fail("文件不能超过 20MB", code=400) with open(path, "wb") as f: f.write(content) doc = KnowledgeDoc( diff --git a/digital-avatar-app/nginx.conf b/digital-avatar-app/nginx.conf index 2919b85..bf31dc3 100644 --- a/digital-avatar-app/nginx.conf +++ b/digital-avatar-app/nginx.conf @@ -23,6 +23,10 @@ http { root /usr/share/nginx/html; index index.html; + # Keep the application gateway aligned with the production edge gateway. + # Without this Nginx rejects ordinary PDF uploads with HTTP 413 before + # FastAPI can return its user-facing file-size validation message. + client_max_body_size 20m; # SPA 兜底(hash 路由下深链接也可正常加载) location / { -- 2.54.0 From 6b7201e8904fc30bd863663375d430098cbc0fd8 Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 4 Sep 2026 13:56:29 +0800 Subject: [PATCH 2/3] fix(avatar): align knowledge upload limit with production --- .../backend/routers/knowledge.py | 23 +++++++++++++------ .../backend/tests/test_knowledge_storage.py | 23 +++++++++++++++++++ .../docs/H5_PRODUCTION_DEPLOYMENT.md | 2 +- digital-avatar-app/nginx.conf | 2 +- 4 files changed, 41 insertions(+), 9 deletions(-) diff --git a/digital-avatar-app/backend/routers/knowledge.py b/digital-avatar-app/backend/routers/knowledge.py index 23de111..199aa08 100644 --- a/digital-avatar-app/backend/routers/knowledge.py +++ b/digital-avatar-app/backend/routers/knowledge.py @@ -17,7 +17,8 @@ UPLOAD_DIR = os.path.abspath(os.getenv("UPLOAD_DIR", os.path.join(BASE_DIR, "upl os.makedirs(UPLOAD_DIR, exist_ok=True) ALLOWED_EXT = {".md", ".txt", ".pdf", ".doc", ".docx", ".xlsx"} -MAX_UPLOAD_BYTES = 20 * 1024 * 1024 +MAX_UPLOAD_BYTES = 100 * 1024 * 1024 +UPLOAD_CHUNK_BYTES = 1024 * 1024 class QAIn(BaseModel): @@ -80,17 +81,25 @@ async def upload_doc(avatar_id: str, file: UploadFile = File(...), authorization os.makedirs(avatar_dir, exist_ok=True) stored = f"{uuid.uuid4().hex}{ext}" path = os.path.join(avatar_dir, stored) - content = await file.read() - if len(content) > MAX_UPLOAD_BYTES: - return fail("文件不能超过 20MB", code=400) - with open(path, "wb") as f: - f.write(content) + file_size = 0 + try: + # Stream large files to disk so a 100MB upload does not occupy 100MB RAM. + with open(path, "wb") as f: + while chunk := await file.read(UPLOAD_CHUNK_BYTES): + file_size += len(chunk) + if file_size > MAX_UPLOAD_BYTES: + raise ValueError("文件不能超过 100MB") + f.write(chunk) + except ValueError as exc: + if os.path.exists(path): + os.remove(path) + return fail(str(exc), code=400) doc = KnowledgeDoc( id=uuid.uuid4().hex, avatar_id=avatar_id, filename=file.filename, file_type=ext.lstrip("."), - file_size=len(content), + file_size=file_size, file_url=f"/api/files/{avatar_id}/{stored}", status="parsing", ) diff --git a/digital-avatar-app/backend/tests/test_knowledge_storage.py b/digital-avatar-app/backend/tests/test_knowledge_storage.py index 50c86a3..b13c1a3 100644 --- a/digital-avatar-app/backend/tests/test_knowledge_storage.py +++ b/digital-avatar-app/backend/tests/test_knowledge_storage.py @@ -64,6 +64,29 @@ def test_upload_returns_before_background_vectorization( db.close() +def test_upload_rejects_oversize_file_before_queuing_indexing( + tmp_path: Path, + authorization_context, +): + context = authorization_context + with ( + patch("routers.knowledge.UPLOAD_DIR", str(tmp_path)), + patch("routers.knowledge.MAX_UPLOAD_BYTES", 4), + patch("routers.knowledge.knowledge_vectorizer.enqueue") as enqueue, + ): + response = client.post( + f"/api/avatar/{context['avatar'].id}/knowledge/docs", + headers=context["owner_headers"], + files={"file": ("oversize.md", b"12345", "text/markdown")}, + ) + + payload = response.json() + assert payload["code"] == 400 + assert payload["message"] == "文件不能超过 100MB" + enqueue.assert_not_called() + assert not list((tmp_path / context["avatar"].id).glob("*")) + + def test_background_vectorizer_commits_ready_document_and_chunks_together( tmp_path: Path, authorization_context, diff --git a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md index 1699f2c..1166429 100644 --- a/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md +++ b/digital-avatar-app/docs/H5_PRODUCTION_DEPLOYMENT.md @@ -117,7 +117,7 @@ location /api/ { proxy_set_header X-Forwarded-Proto $scheme; proxy_buffering off; proxy_read_timeout 300s; - client_max_body_size 20m; + client_max_body_size 100m; } ``` diff --git a/digital-avatar-app/nginx.conf b/digital-avatar-app/nginx.conf index bf31dc3..a854b95 100644 --- a/digital-avatar-app/nginx.conf +++ b/digital-avatar-app/nginx.conf @@ -26,7 +26,7 @@ http { # Keep the application gateway aligned with the production edge gateway. # Without this Nginx rejects ordinary PDF uploads with HTTP 413 before # FastAPI can return its user-facing file-size validation message. - client_max_body_size 20m; + client_max_body_size 100m; # SPA 兜底(hash 路由下深链接也可正常加载) location / { -- 2.54.0 From 08c58fe0e612d1b9e5d221c1969471d048221f39 Mon Sep 17 00:00:00 2001 From: stefanfeng Date: Fri, 4 Sep 2026 15:18:39 +0800 Subject: [PATCH 3/3] fix(avatar): cap knowledge files at 50MB --- digital-avatar-app/backend/routers/knowledge.py | 4 ++-- digital-avatar-app/backend/tests/test_knowledge_storage.py | 2 +- 2 files changed, 3 insertions(+), 3 deletions(-) diff --git a/digital-avatar-app/backend/routers/knowledge.py b/digital-avatar-app/backend/routers/knowledge.py index 199aa08..85e7b37 100644 --- a/digital-avatar-app/backend/routers/knowledge.py +++ b/digital-avatar-app/backend/routers/knowledge.py @@ -17,7 +17,7 @@ UPLOAD_DIR = os.path.abspath(os.getenv("UPLOAD_DIR", os.path.join(BASE_DIR, "upl os.makedirs(UPLOAD_DIR, exist_ok=True) ALLOWED_EXT = {".md", ".txt", ".pdf", ".doc", ".docx", ".xlsx"} -MAX_UPLOAD_BYTES = 100 * 1024 * 1024 +MAX_UPLOAD_BYTES = 50 * 1024 * 1024 UPLOAD_CHUNK_BYTES = 1024 * 1024 @@ -88,7 +88,7 @@ async def upload_doc(avatar_id: str, file: UploadFile = File(...), authorization while chunk := await file.read(UPLOAD_CHUNK_BYTES): file_size += len(chunk) if file_size > MAX_UPLOAD_BYTES: - raise ValueError("文件不能超过 100MB") + raise ValueError("文件不能超过 50MB") f.write(chunk) except ValueError as exc: if os.path.exists(path): diff --git a/digital-avatar-app/backend/tests/test_knowledge_storage.py b/digital-avatar-app/backend/tests/test_knowledge_storage.py index b13c1a3..f22aeb5 100644 --- a/digital-avatar-app/backend/tests/test_knowledge_storage.py +++ b/digital-avatar-app/backend/tests/test_knowledge_storage.py @@ -82,7 +82,7 @@ def test_upload_rejects_oversize_file_before_queuing_indexing( payload = response.json() assert payload["code"] == 400 - assert payload["message"] == "文件不能超过 100MB" + assert payload["message"] == "文件不能超过 50MB" enqueue.assert_not_called() assert not list((tmp_path / context["avatar"].id).glob("*")) -- 2.54.0