refactor: api.ts

This commit is contained in:
tao.chen
2026-09-02 10:10:41 +08:00
committed by tao.chen
parent cc0e2eaec4
commit 9b95357b39
13 changed files with 2157 additions and 1964 deletions
+289
View File
@@ -0,0 +1,289 @@
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,
);
}