From 637981b060efb816880eaf221191dc1141246e25 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Wed, 25 Mar 2026 13:39:24 +0800 Subject: [PATCH] =?UTF-8?q?fix(viewer):=20=E6=9B=B4=E6=96=B0=E4=B8=8B?= =?UTF-8?q?=E8=BD=BD=E6=96=87=E4=BB=B6=E9=80=BB=E8=BE=91=E4=BB=A5=E5=A4=84?= =?UTF-8?q?=E7=90=86=E5=A4=A7=E6=96=87=E4=BB=B6=E5=B9=B6=E8=BF=94=E5=9B=9E?= =?UTF-8?q?=E6=AD=A3=E7=A1=AE=E7=9A=84=E5=93=8D=E5=BA=94?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/package/yuxi/config/static/models.py | 2 +- .../services/viewer_filesystem_service.py | 22 ++++++++++++++----- .../test/api/test_viewer_filesystem_router.py | 21 ++++++++++++++++++ 3 files changed, 39 insertions(+), 6 deletions(-) diff --git a/backend/package/yuxi/config/static/models.py b/backend/package/yuxi/config/static/models.py index 83cc2707..77495b65 100644 --- a/backend/package/yuxi/config/static/models.py +++ b/backend/package/yuxi/config/static/models.py @@ -117,7 +117,7 @@ DEFAULT_CHAT_MODEL_PROVIDERS: dict[str, ChatModelProvider] = { ), "minimax": ChatModelProvider( name="MiniMax", - url="https://platform.minimaxi.com/document/introduction", + url="https://platform.minimaxi.com/docs/guides/models-intro", base_url="https://api.minimaxi.com/v1", default="MiniMax-M2.7", env="MINIMAX_API_KEY", diff --git a/backend/package/yuxi/services/viewer_filesystem_service.py b/backend/package/yuxi/services/viewer_filesystem_service.py index 3439df38..748f4572 100644 --- a/backend/package/yuxi/services/viewer_filesystem_service.py +++ b/backend/package/yuxi/services/viewer_filesystem_service.py @@ -7,9 +7,9 @@ from pathlib import PurePosixPath from urllib.parse import quote from fastapi import HTTPException -from fastapi.responses import StreamingResponse +from fastapi.responses import FileResponse, StreamingResponse from sqlalchemy.ext.asyncio import AsyncSession -from yuxi.agents.backends.sandbox import SKILLS_PATH, USER_DATA_PATH +from yuxi.agents.backends.sandbox import SKILLS_PATH, USER_DATA_PATH, resolve_virtual_path from yuxi.agents.backends.skills_backend import SelectedSkillsReadonlyBackend from yuxi.agents.middlewares.skills_middleware import normalize_selected_skills from yuxi.services.filesystem_service import _resolve_filesystem_state @@ -199,7 +199,7 @@ async def download_viewer_file( db: AsyncSession, ) -> StreamingResponse: normalized_path = _normalize_path(path) - sandbox_backend, skills_backend, _selected_skills = await _resolve_viewer_state( + _sandbox_backend, skills_backend, _selected_skills = await _resolve_viewer_state( thread_id=thread_id, agent_id=agent_id, agent_config_id=agent_config_id, @@ -209,8 +209,20 @@ async def download_viewer_file( try: if _is_user_data_path(normalized_path): - responses = await asyncio.to_thread(sandbox_backend.download_files, [normalized_path]) - elif _is_skills_path(normalized_path): + actual_path = resolve_virtual_path(thread_id, normalized_path) + if not actual_path.exists(): + raise HTTPException(status_code=404, detail="文件不存在") + if not actual_path.is_file(): + raise HTTPException(status_code=400, detail="当前路径是目录") + + file_name = actual_path.name or "download" + media_type = mimetypes.guess_type(file_name)[0] or "application/octet-stream" + headers = { + "Content-Disposition": f"attachment; filename*=UTF-8''{quote(file_name)}", + } + return FileResponse(path=actual_path, media_type=media_type, headers=headers) + + if _is_skills_path(normalized_path): responses = await asyncio.to_thread(skills_backend.download_files, [_strip_skills_prefix(normalized_path)]) else: raise HTTPException( diff --git a/backend/test/api/test_viewer_filesystem_router.py b/backend/test/api/test_viewer_filesystem_router.py index ffa875ad..31a82cd0 100644 --- a/backend/test/api/test_viewer_filesystem_router.py +++ b/backend/test/api/test_viewer_filesystem_router.py @@ -6,6 +6,8 @@ import uuid import pytest +from yuxi.agents.backends.sandbox import ensure_thread_dirs, sandbox_workspace_dir, virtual_path_for_thread_file + pytestmark = [pytest.mark.asyncio, pytest.mark.integration] @@ -107,3 +109,22 @@ async def test_viewer_download_returns_attachment_response(test_client, standard assert "attachment;" in content_disposition assert "download_demo" in content_disposition assert "download-me" in response.text + +async def test_viewer_download_returns_full_file_for_large_user_data_content(test_client, standard_user): + headers = standard_user["headers"] + thread_id = await _create_thread_for_user(test_client, headers) + large_content = "0123456789abcdef" * 4096 + + ensure_thread_dirs(thread_id) + actual_path = sandbox_workspace_dir(thread_id) / "large_download.txt" + actual_path.write_text(large_content, encoding="utf-8") + file_path = virtual_path_for_thread_file(thread_id, actual_path) + + response = await test_client.get( + "/api/viewer/filesystem/download", + params={"thread_id": thread_id, "path": file_path}, + headers=headers, + ) + assert response.status_code == 200, response.text + assert response.content == large_content.encode("utf-8") +