import { apiRequest, ApiRequestError, createUuid, type ApiErrorEnvelope, } from "./_shared"; export type WorkspaceDirectory = { path: string; name: string; parent_path: string; owner_user_id: string; has_children?: boolean; }; export type ResourceItem = { resource_id: string; workspace_id: string; storage_object_id: string; owner_user_id: string; owner_display_name?: string | null; resource_name: string; description: string | null; visibility: "private" | "workspace" | "public"; status: string; created_at: string; updated_at: string; file: { file_name: string; file_extension: string | null; mime_type: string | null; size_bytes: number; content_hash: string | null; object_status: string; }; jupyter_accessible_path: string; absolute_path: string; }; export async function listResources( workspaceId: string, parentPath: string = "", opts?: { visibility?: string; keyword?: string; ownerUserId?: string }, ): Promise { // Default (no ownerUserId) scopes to the requester's own object_key // subtree; passing ownerUserId scopes to that owner's subtree so the tree // can lazily fetch another member's data resources on group expand. const parameters = new URLSearchParams(); if (parentPath) parameters.set("parent_path", parentPath); if (opts?.visibility) parameters.set("visibility", opts.visibility); if (opts?.keyword) parameters.set("keyword", opts.keyword); if (opts?.ownerUserId) parameters.set("owner_user_id", opts.ownerUserId); const query = parameters.toString(); // apiRequest already unwraps the envelope's `data` field, so we // request `ResourceItem[]` directly here (matching listScripts). return apiRequest( `/api/v1/data-resources${query ? `?${query}` : ""}`, {}, workspaceId, ); } export async function deleteResource( workspaceId: string, resourceId: string, ): Promise<{ resource_id: string; status: string }> { return apiRequest<{ resource_id: string; status: string }>( `/api/v1/data-resources/${encodeURIComponent(resourceId)}`, { method: "DELETE" }, workspaceId, ); } /** 同源流式下载数据资源字节,供 Excel 等预览器使用。 */ export async function fetchResourceContentFile( workspaceId: string, resourceId: string, fileName: string, signal?: AbortSignal, ): Promise { const response = await fetch( `/api/v1/data-resources/${encodeURIComponent(resourceId)}/content?workspace_id=${encodeURIComponent(workspaceId)}`, { credentials: "same-origin", signal, headers: { "X-Request-ID": createUuid().replaceAll("-", ""), }, }, ); if (response.status === 401 && typeof window !== "undefined") { const here = window.location.pathname; if (here !== "/login") { window.location.assign("/login"); } throw new ApiRequestError("未登录或登录已过期", 401); } if (!response.ok) { let message = `请求失败(HTTP ${response.status})`; try { const payload = (await response.json()) as ApiErrorEnvelope; const detailMessage = typeof payload.detail === "string" ? payload.detail : payload.detail?.message; if (detailMessage) message = detailMessage; } catch { /* ignore non-JSON error bodies */ } throw new ApiRequestError(message, response.status); } const blob = await response.blob(); return new File([blob], fileName, { type: blob.type || "application/octet-stream", }); } export type ResourcePreviewPayload = { kind: "table"; columns: string[]; rows: string[][]; row_count: number; truncated: boolean; delimiter: string; }; /** 表格类数据资源抽样预览(csv / tsv)。 */ export async function fetchResourcePreview( workspaceId: string, resourceId: string, input: { limit?: number } = {}, signal?: AbortSignal, ): Promise { const parameters = new URLSearchParams(); parameters.set("workspace_id", workspaceId); if (input.limit != null) parameters.set("limit", String(input.limit)); const response = await fetch( `/api/v1/data-resources/${encodeURIComponent(resourceId)}/preview?${parameters.toString()}`, { credentials: "same-origin", signal, headers: { "X-Request-ID": createUuid().replaceAll("-", ""), }, }, ); if (response.status === 401 && typeof window !== "undefined") { const here = window.location.pathname; if (here !== "/login") { window.location.assign("/login"); } throw new ApiRequestError("未登录或登录已过期", 401); } const payload = await response.json(); if (!response.ok) { const error = payload as ApiErrorEnvelope; const detailMessage = typeof error.detail === "string" ? error.detail : error.detail?.message; throw new ApiRequestError( detailMessage ?? `请求失败(HTTP ${response.status})`, response.status, ); } const data = (payload as { data: ResourcePreviewPayload }).data; if (!data || data.kind !== "table" || !Array.isArray(data.columns)) { throw new ApiRequestError("响应数据格式错误", response.status); } return data; } export async function createResourceUpload( workspaceId: string, body: { file_name: string; content_type: string; expected_size_bytes: number; expected_hash: string | null; target_path?: string; }, ): Promise<{ upload_id: string; upload_path: string }> { return apiRequest<{ upload_id: string; upload_path: string }>( "/api/v1/data-resources/uploads", { method: "POST", body: JSON.stringify(body), headers: { "Idempotency-Key": createUuid().replaceAll("-", "") }, }, workspaceId, ); } export async function uploadResourceBytes( workspaceId: string, uploadId: string, fileBytes: ArrayBuffer | Blob, contentType: string, ): Promise<{ storage_object_id: string }> { return apiRequest<{ storage_object_id: string }>( `/api/v1/data-resources/uploads/${encodeURIComponent(uploadId)}`, { method: "PUT", headers: { "Content-Type": contentType }, body: fileBytes, }, workspaceId, ); } export async function bindResourceUpload( workspaceId: string, uploadId: string, body: { resource_name: string; description: string; visibility: "private" | "workspace" | "public"; }, ): Promise { return apiRequest( `/api/v1/data-resources/uploads/${encodeURIComponent(uploadId)}/bind`, { method: "POST", body: JSON.stringify(body), }, workspaceId, ); } export async function listWorkspaceDirectories( workspaceId: string, parentPath: string = "", ownerUserId?: string, ): Promise { // Default (no ownerUserId) scopes to the requester's own subtree; passing // ownerUserId scopes to that owner so the tree can lazily render their // directory structure on expand. Directories are structural rows; file // visibility is still enforced by the scripts/data-resources endpoints. const parameters = new URLSearchParams(); if (parentPath) parameters.set("parent_path", parentPath); if (ownerUserId) parameters.set("owner_user_id", ownerUserId); const query = parameters.toString(); const data = await apiRequest<{ directories: WorkspaceDirectory[] }>( `/api/v1/workspace-directories${query ? `?${query}` : ""}`, {}, workspaceId, ); return data.directories; } export async function createWorkspaceDirectory( workspaceId: string, directoryName: string, parentPath = "", ): Promise { return apiRequest( "/api/v1/workspace-directories", { method: "POST", body: JSON.stringify({ directory_name: directoryName, parent_path: parentPath, }), }, workspaceId, ); } export async function deleteWorkspaceDirectory( workspaceId: string, path: string, ): Promise<{ path: string; status: string; deleted_scripts: number; versions_preserved: boolean; }> { const parameters = new URLSearchParams({ path }); return apiRequest( `/api/v1/workspace-directories?${parameters.toString()}`, { method: "DELETE" }, workspaceId, ); }