289 lines
8.1 KiB
TypeScript
289 lines
8.1 KiB
TypeScript
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<ResourceItem[]> {
|
||
// 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<T> already unwraps the envelope's `data` field, so we
|
||
// request `ResourceItem[]` directly here (matching listScripts).
|
||
return apiRequest<ResourceItem[]>(
|
||
`/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<File> {
|
||
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<ResourcePreviewPayload> {
|
||
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<ResourceItem> {
|
||
return apiRequest<ResourceItem>(
|
||
`/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<WorkspaceDirectory[]> {
|
||
// 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<WorkspaceDirectory> {
|
||
return apiRequest<WorkspaceDirectory>(
|
||
"/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,
|
||
);
|
||
}
|