From bff7b7f9a6f88d8ef32d637b24dc31c0d8812a00 Mon Sep 17 00:00:00 2001 From: "tao.chen" <93983997+taochen-ct@users.noreply.github.com> Date: Tue, 11 Aug 2026 21:53:52 +0800 Subject: [PATCH] fix: bugs --- backend/src/backend/scripts.py | 69 +++++++++++-------- .../components/platform/ScriptExplorer.tsx | 49 +++++++++++-- 2 files changed, 82 insertions(+), 36 deletions(-) diff --git a/backend/src/backend/scripts.py b/backend/src/backend/scripts.py index 0192473..6213d88 100644 --- a/backend/src/backend/scripts.py +++ b/backend/src/backend/scripts.py @@ -27,7 +27,6 @@ from fastapi import ( ) from loguru import logger from sqlalchemy import func, select -from sqlalchemy.exc import IntegrityError from sqlalchemy.ext.asyncio import AsyncSession from backend.dependencies import ( @@ -591,6 +590,7 @@ async def create_workspace_directory( child_path = f"{parent}/{name}" if parent else name relative_path = user_relative_path(context, child_path) scoped_prefix = user_relative_path(context) + path_hash = hashlib.sha256(relative_path.encode("utf-8")).digest() # Validate parent exists: there must be at least one StorageObject whose # relative_path is exactly the parent directory (the directory row itself) # OR lives somewhere below the parent (any file/dir nested under it). @@ -609,42 +609,53 @@ async def create_workspace_directory( status.HTTP_404_NOT_FOUND, "parent directory not found", ) - # S3 has no real directory objects — the prefix is implicitly - # created when a file is uploaded. Conflict detection is best-effort. + # Conflict check uses the unique index on (workspace_id, storage_backend, path_hash). + # A soft-deleted row at the same path can be revived; an available row is a conflict. existing = await session.scalar( - select(StorageObjects.storage_object_id).where( + select(StorageObjects) + .where( StorageObjects.workspace_id == context.workspace.workspace_id, - StorageObjects.relative_path == relative_path, - StorageObjects.object_status == "available", + StorageObjects.storage_backend == "rustfs", + StorageObjects.path_hash == path_hash, ) + .with_for_update() ) - if existing is not None: + if existing is not None and existing.object_status == "available": raise HTTPException( status.HTTP_409_CONFLICT, "a file or directory with the same path already exists", ) - directory = StorageObjects( - storage_object_id=new_ulid(), - workspace_id=context.workspace.workspace_id, - object_type="directory", - usage_type="working_copy", - storage_backend="rustfs", - storage_uri=f"inline://directory/{relative_path}", - relative_path=relative_path, - path_hash=hashlib.sha256(relative_path.encode("utf-8")).digest(), - object_status="available", - size_bytes=0, - visibility="private", - created_by=context.user.user_id, - ) - session.add(directory) - try: - await session.flush() - except IntegrityError as exc: - raise HTTPException( - status.HTTP_409_CONFLICT, - "directory already exists", - ) from exc + + if existing is None: + directory = StorageObjects( + storage_object_id=new_ulid(), + workspace_id=context.workspace.workspace_id, + storage_backend="rustfs", + object_type="directory", + usage_type="working_copy", + storage_uri=f"inline://directory/{relative_path}", + relative_path=relative_path, + path_hash=path_hash, + object_status="available", + size_bytes=0, + visibility="private", + created_by=context.user.user_id, + ) + session.add(directory) + else: + # Revive the soft-deleted row, keeping its original storage_object_id. + directory = existing + directory.object_status = "available" + directory.is_deleted = 0 + directory.deleted_at = None + directory.object_type = "directory" + directory.usage_type = "working_copy" + directory.storage_uri = f"inline://directory/{relative_path}" + directory.size_bytes = 0 + directory.visibility = "private" + directory.created_by = context.user.user_id + + await session.flush() return { "request_id": context.request_id, "data": { diff --git a/frontend/app/components/platform/ScriptExplorer.tsx b/frontend/app/components/platform/ScriptExplorer.tsx index 694e4af..fec1771 100644 --- a/frontend/app/components/platform/ScriptExplorer.tsx +++ b/frontend/app/components/platform/ScriptExplorer.tsx @@ -1,8 +1,8 @@ -import { type ChangeEvent, type MouseEvent as ReactMouseEvent, type RefObject } from "react"; +import { type ChangeEvent, type MouseEvent as ReactMouseEvent, type RefObject, useEffect, useMemo, useState } from "react"; import Icon from "../common/Icon"; import { WorkspaceTreeGroup } from "~/features/platform/WorkspaceTree"; -import type { ScriptItem, WorkspaceDirectory } from "~/services/api"; -import type { AuthUser } from "~/context/AuthContext"; +import type { ScriptItem, WorkspaceDirectory, WorkspaceMember } from "~/services/api"; +import { useApi, useAuth, type AuthUser } from "~/context/AuthContext"; type ScriptExplorerProps = { scripts: ScriptItem[]; @@ -50,7 +50,38 @@ export function ScriptExplorer({ uploadInputRef, onHandleUpload, }: ScriptExplorerProps) { - const memberScriptGroups = (() => { + const api = useApi(); + const { currentWorkspace } = useAuth(); + const [members, setMembers] = useState([]); + + useEffect(() => { + if (!currentWorkspace) { + setMembers([]); + return; + } + let cancelled = false; + api + .listWorkspaceMembers(currentWorkspace.workspace_id) + .then((list) => { + if (!cancelled) setMembers(list); + }) + .catch(() => { + if (!cancelled) setMembers([]); + }); + return () => { + cancelled = true; + }; + }, [api, currentWorkspace]); + + const displayNameByUserId = useMemo(() => { + const map = new Map(); + for (const m of members) { + map.set(m.user_id, m.display_name || m.username || m.user_id); + } + return map; + }, [members]); + + const memberScriptGroups = useMemo(() => { const visibleScripts = user?.is_system_admin === true ? filteredScripts @@ -74,13 +105,17 @@ export function ScriptExplorer({ directories: WorkspaceDirectory[]; }[] = []; for (const [ownerUserId, groupScripts] of byOwner.entries()) { + const displayName = + displayNameByUserId.get(ownerUserId) ?? + (ownerUserId === user?.user_id ? user?.display_name : null) ?? + `${ownerUserId.slice(-6)}…`; const groupUser = ownerUserId === user?.user_id ? user : ({ user_id: ownerUserId, username: ownerUserId, - display_name: ownerUserId, + display_name: displayName, email: null, status: "unknown", role_code: null, @@ -100,7 +135,7 @@ export function ScriptExplorer({ }); return groups; - })(); + }, [filteredScripts, user, displayNameByUserId]); return (