From 3f1ec778a480b5fd62d58c2925513a401c6a6099 Mon Sep 17 00:00:00 2001 From: Wenjie Zhang Date: Tue, 31 Mar 2026 19:41:08 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=96=B0=E5=A2=9E=E5=AF=B9=E4=BA=8E=20?= =?UTF-8?q?workspace=20=E7=9A=84=E5=88=A0=E9=99=A4=E6=8E=A5=E5=8F=A3?= =?UTF-8?q?=E7=9A=84=E5=AE=9E=E7=8E=B0?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - 为前端工作台文件树补齐文件删除能力:`/api/viewer/filesystem/file` - 新增删除接口,`AgentPanel` 文件节点新增删除按钮与确认交互,删除后会同步刷新树与预览状态 --- .../services/viewer_filesystem_service.py | 39 ++++++++++ backend/server/routers/filesystem_router.py | 20 +++++ .../api/test_viewer_filesystem_router.py | 28 +++++++ docs/develop-guides/roadmap.md | 3 +- web/src/apis/viewer_filesystem.js | 7 +- web/src/components/AgentPanel.vue | 77 ++++++++++++++++++- 6 files changed, 171 insertions(+), 3 deletions(-) diff --git a/backend/package/yuxi/services/viewer_filesystem_service.py b/backend/package/yuxi/services/viewer_filesystem_service.py index 2a6d466c..a935b185 100644 --- a/backend/package/yuxi/services/viewer_filesystem_service.py +++ b/backend/package/yuxi/services/viewer_filesystem_service.py @@ -507,3 +507,42 @@ async def download_viewer_file( "Content-Disposition": f"attachment; filename*=UTF-8''{quote(file_name)}", } return StreamingResponse(stream, media_type=media_type, headers=headers) + + +async def delete_viewer_file( + *, + thread_id: str, + path: str, + agent_id: str | None, + agent_config_id: int | None, + current_user: User, + db: AsyncSession, +) -> dict: + if not thread_id: + raise HTTPException(status_code=422, detail="thread_id 不能为空") + + normalized_path = _normalize_path(path) + await _resolve_viewer_state( + thread_id=thread_id, + agent_id=agent_id, + agent_config_id=agent_config_id, + current_user=current_user, + db=db, + ) + + if not _is_user_data_path(normalized_path): + raise HTTPException(status_code=400, detail="当前路径不支持删除") + + try: + actual_path = resolve_virtual_path(thread_id, normalized_path) + if not actual_path.exists(): + raise HTTPException(status_code=404, detail="文件不存在") + if actual_path.is_dir(): + raise HTTPException(status_code=400, detail="当前路径是目录") + await asyncio.to_thread(actual_path.unlink) + except PermissionError as e: + raise HTTPException(status_code=400, detail=str(e)) from e + except ValueError as e: + raise HTTPException(status_code=422, detail=str(e)) from e + + return {"success": True, "path": normalized_path} diff --git a/backend/server/routers/filesystem_router.py b/backend/server/routers/filesystem_router.py index 72e28aee..f99458a9 100644 --- a/backend/server/routers/filesystem_router.py +++ b/backend/server/routers/filesystem_router.py @@ -11,6 +11,7 @@ from sqlalchemy.ext.asyncio import AsyncSession from server.utils.auth_middleware import get_db, get_required_user from yuxi.services.viewer_filesystem_service import ( + delete_viewer_file, download_viewer_file, list_viewer_filesystem_tree, read_viewer_file_content, @@ -58,6 +59,25 @@ async def get_viewer_file( ) +@filesystem_router.delete("/file", response_model=dict) +async def delete_viewer_file_route( + thread_id: str = Query(..., description="线程 ID"), + path: str = Query(..., description="文件路径"), + agent_id: str | None = Query(None, description="智能体 ID"), + agent_config_id: int | None = Query(None, description="智能体配置 ID"), + current_user: User = Depends(get_required_user), + db: AsyncSession = Depends(get_db), +): + return await delete_viewer_file( + thread_id=thread_id, + path=path, + agent_id=agent_id, + agent_config_id=agent_config_id, + current_user=current_user, + db=db, + ) + + @filesystem_router.get("/download") async def download_viewer( thread_id: str = Query(..., description="线程 ID"), diff --git a/backend/test/integration/api/test_viewer_filesystem_router.py b/backend/test/integration/api/test_viewer_filesystem_router.py index 80775ef0..e3b79747 100644 --- a/backend/test/integration/api/test_viewer_filesystem_router.py +++ b/backend/test/integration/api/test_viewer_filesystem_router.py @@ -334,6 +334,34 @@ async def test_viewer_download_returns_full_file_for_large_user_data_content(tes assert response.content == large_content.encode("utf-8") +async def test_viewer_delete_removes_user_data_file(test_client, standard_user): + headers = standard_user["headers"] + thread_id = await _create_thread_for_user(test_client, headers) + + ensure_thread_dirs(thread_id) + actual_path = sandbox_workspace_dir(thread_id) / "delete_me.txt" + actual_path.write_text("delete me", encoding="utf-8") + file_path = virtual_path_for_thread_file(thread_id, actual_path) + + delete_response = await test_client.delete( + "/api/viewer/filesystem/file", + params={"thread_id": thread_id, "path": file_path}, + headers=headers, + ) + assert delete_response.status_code == 200, delete_response.text + assert delete_response.json()["success"] is True + assert not actual_path.exists() + + tree_response = await test_client.get( + "/api/viewer/filesystem/tree", + params={"thread_id": thread_id, "path": "/home/gem/user-data/workspace"}, + headers=headers, + ) + assert tree_response.status_code == 200, tree_response.text + paths = {entry.get("path") for entry in tree_response.json().get("entries", [])} + assert file_path not in paths + + async def test_viewer_tree_root_hides_kbs_namespace_when_no_database_is_visible(test_client, standard_user): headers = standard_user["headers"] thread_id = await _create_thread_for_user(test_client, headers) diff --git a/docs/develop-guides/roadmap.md b/docs/develop-guides/roadmap.md index 1403e334..e2443909 100644 --- a/docs/develop-guides/roadmap.md +++ b/docs/develop-guides/roadmap.md @@ -57,7 +57,8 @@ ### 修复 -- 调整前端工作台文件预览交互:恢复默认侧边/弹窗预览,并新增显式“全屏预览”入口;全屏模式下由预览内容直接覆盖整页,仅保留右上角悬浮关闭按钮 +- 为前端工作台文件树补齐文件删除能力:`/api/viewer/filesystem/file` 新增删除接口,`AgentPanel` 文件节点新增删除按钮与确认交互,删除后会同步刷新树与预览状态 +- 调整前端工作台文件预览交互:恢复默认侧边/弹窗预览,并新增显式“全屏预览”入口;全屏模式下由预览内容直接覆盖整页,仅保留右上角悬浮关闭按钮; - 统一 Agent Panel 文件预览与消息区交付物预览组件:两处改为复用同一套 `AgentFilePreview` 预览实现,并为交付物预览补齐与工作台一致的“全屏预览”入口 - 兼容旧版已安装的内置 `reporter` 技能记录:`update_builtin_skill` 现在会识别由 `system` 或 `builtin-system` 管理的历史记录,避免更新时误报“技能 `reporter` 不是内置 skill” - 调整沙盒 user-data 目录隔离策略:`workspace` 改为共享目录 `saves/threads/shared/workspace`,`uploads/outputs` 继续保持 thread 级隔离;同时更新 thread artifact 权限校验、viewer 文件系统列举逻辑,以及对应的 router/E2E 测试 diff --git a/web/src/apis/viewer_filesystem.js b/web/src/apis/viewer_filesystem.js index 1f7ec21f..c99f58af 100644 --- a/web/src/apis/viewer_filesystem.js +++ b/web/src/apis/viewer_filesystem.js @@ -1,4 +1,4 @@ -import { apiGet } from './base' +import { apiDelete, apiGet } from './base' const buildQuery = (params) => { const query = new URLSearchParams() @@ -38,3 +38,8 @@ export const downloadViewerFile = (threadId, path, agentId = null, agentConfigId const query = buildViewerQuery(threadId, path, agentId, agentConfigId) return apiGet(`/api/viewer/filesystem/download?${query}`, {}, true, 'blob') } + +export const deleteViewerFile = (threadId, path, agentId = null, agentConfigId = null) => { + const query = buildViewerQuery(threadId, path, agentId, agentConfigId) + return apiDelete(`/api/viewer/filesystem/file?${query}`) +} diff --git a/web/src/components/AgentPanel.vue b/web/src/components/AgentPanel.vue index 00d0ca0e..70aea378 100644 --- a/web/src/components/AgentPanel.vue +++ b/web/src/components/AgentPanel.vue @@ -98,6 +98,14 @@ > + @@ -153,7 +161,15 @@