refactor: api.ts
This commit is contained in:
+12
-1964
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,262 @@
|
||||
export type DemoUser = {
|
||||
userId: string;
|
||||
userName: string;
|
||||
username: string;
|
||||
roleCode: "admin" | "developer";
|
||||
roleName: string;
|
||||
};
|
||||
|
||||
export type DemoWorkspace = {
|
||||
workspaceId: string;
|
||||
workspaceName: string;
|
||||
};
|
||||
|
||||
export function createUuid(): string {
|
||||
const cryptoApi = globalThis.crypto;
|
||||
if (typeof cryptoApi?.randomUUID === "function") {
|
||||
return cryptoApi.randomUUID();
|
||||
}
|
||||
|
||||
const bytes = new Uint8Array(16);
|
||||
if (typeof cryptoApi?.getRandomValues === "function") {
|
||||
cryptoApi.getRandomValues(bytes);
|
||||
} else {
|
||||
for (let index = 0; index < bytes.length; index += 1) {
|
||||
bytes[index] = Math.floor(Math.random() * 256);
|
||||
}
|
||||
}
|
||||
bytes[6] = ((bytes[6] ?? 0) & 0x0f) | 0x40;
|
||||
bytes[8] = ((bytes[8] ?? 0) & 0x3f) | 0x80;
|
||||
|
||||
const hex = Array.from(bytes, (value) =>
|
||||
value.toString(16).padStart(2, "0"),
|
||||
).join("");
|
||||
return [
|
||||
hex.slice(0, 8),
|
||||
hex.slice(8, 12),
|
||||
hex.slice(12, 16),
|
||||
hex.slice(16, 20),
|
||||
hex.slice(20),
|
||||
].join("-");
|
||||
}
|
||||
|
||||
export const demoUsers: DemoUser[] = [
|
||||
{ userId: "0000000000RF6FG1SDBXG59S13", userName: "张三", username: "admin-zhang", roleCode: "admin", roleName: "管理员" },
|
||||
{ userId: "0000000000H2QYCGPCWQM1JSGS", userName: "李四", username: "admin-li", roleCode: "admin", roleName: "管理员" },
|
||||
{ userId: "0000000000RWG40ESZPGJT629J", userName: "王五", username: "dev-wang", roleCode: "developer", roleName: "开发人员" },
|
||||
{ userId: "00000000004CQV7WASJA6N6FW4", userName: "赵六", username: "dev-zhao", roleCode: "developer", roleName: "开发人员" },
|
||||
];
|
||||
|
||||
export const demoWorkspaces: DemoWorkspace[] = [
|
||||
{ workspaceId: "00000000000BM630VT9ARVFZPC", workspaceName: "模型开发 Workspace" },
|
||||
{ workspaceId: "0000000000AE0NC0V5T424KK86", workspaceName: "风险验证 Workspace" },
|
||||
];
|
||||
|
||||
function readStoredContext(): Partial<{
|
||||
userId: string;
|
||||
workspaceId: string;
|
||||
}> {
|
||||
if (typeof window === "undefined") return {};
|
||||
try {
|
||||
return JSON.parse(
|
||||
window.localStorage.getItem("model-platform-demo-context") ?? "{}",
|
||||
) as Partial<{ userId: string; workspaceId: string }>;
|
||||
} catch {
|
||||
return {};
|
||||
}
|
||||
}
|
||||
|
||||
const storedContext = readStoredContext();
|
||||
const initialUser = demoUsers.find((item) => item.userId === storedContext.userId)
|
||||
?? demoUsers[0];
|
||||
const initialWorkspace = demoWorkspaces.find(
|
||||
(item) => item.workspaceId === storedContext.workspaceId,
|
||||
) ?? demoWorkspaces[0];
|
||||
|
||||
export const demoContext = {
|
||||
...initialUser,
|
||||
...initialWorkspace,
|
||||
};
|
||||
|
||||
export function setDemoContext(input: {
|
||||
user?: DemoUser;
|
||||
workspace?: DemoWorkspace;
|
||||
}): void {
|
||||
if (input.user) Object.assign(demoContext, input.user);
|
||||
if (input.workspace) Object.assign(demoContext, input.workspace);
|
||||
if (typeof window !== "undefined") {
|
||||
window.localStorage.setItem("model-platform-demo-context", JSON.stringify({
|
||||
userId: demoContext.userId,
|
||||
workspaceId: demoContext.workspaceId,
|
||||
}));
|
||||
}
|
||||
}
|
||||
|
||||
// API client for the platform backend.
|
||||
//
|
||||
// All endpoints that take a workspace context require the caller to
|
||||
// pass `workspaceId` explicitly. Components read the active workspace
|
||||
// from `useAuth().currentWorkspace` and thread it through; the cookie
|
||||
// set by `/api/v1/auth/login` is sent automatically thanks to
|
||||
// `credentials: "same-origin"`, and the backend reads it via the
|
||||
// shared `request_context` dependency.
|
||||
//
|
||||
// 401 from any endpoint means the session has expired or was never
|
||||
// established; the global `apiRequest` helper bounces the user to
|
||||
// `/login` so the platform never tries to render with a stale identity.
|
||||
|
||||
export type Employee = {
|
||||
user_id: string;
|
||||
username: string;
|
||||
display_name: string;
|
||||
email: string | null;
|
||||
status: "active" | "disabled" | "locked";
|
||||
role_code: "admin" | "developer";
|
||||
role_name: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type ApiEnvelope<T> = {
|
||||
request_id: string;
|
||||
data: T;
|
||||
meta: Record<string, unknown>;
|
||||
};
|
||||
|
||||
export type CursorPageMeta = {
|
||||
limit: number;
|
||||
page_count: number;
|
||||
total_count: number;
|
||||
has_more: boolean;
|
||||
next_cursor: string | null;
|
||||
};
|
||||
|
||||
export type CursorPage<T> = {
|
||||
items: T[];
|
||||
meta: CursorPageMeta;
|
||||
};
|
||||
|
||||
export type CursorListParams = {
|
||||
limit?: number;
|
||||
cursor?: string | null;
|
||||
q?: string;
|
||||
};
|
||||
|
||||
export type ApiErrorEnvelope = {
|
||||
detail?: string | {
|
||||
code?: string;
|
||||
message?: string;
|
||||
};
|
||||
error?: {
|
||||
code?: string;
|
||||
message?: string;
|
||||
details?: {
|
||||
editor_name?: string;
|
||||
lease_expires_at?: string;
|
||||
};
|
||||
};
|
||||
};
|
||||
|
||||
export class ApiRequestError extends Error {
|
||||
readonly status: number;
|
||||
readonly code?: string;
|
||||
|
||||
constructor(message: string, status: number, code?: string) {
|
||||
super(message);
|
||||
this.name = "ApiRequestError";
|
||||
this.status = status;
|
||||
this.code = code;
|
||||
}
|
||||
}
|
||||
|
||||
export function appendWorkspaceId(path: string, workspaceId: string): string {
|
||||
// `path` may already contain a query string. Use URLSearchParams to
|
||||
// merge cleanly either way.
|
||||
const separator = path.includes("?") ? "&" : "?";
|
||||
return `${path}${separator}workspace_id=${encodeURIComponent(workspaceId)}`;
|
||||
}
|
||||
|
||||
export async function apiRequest<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
workspaceId?: string,
|
||||
): Promise<T> {
|
||||
const result = await apiRequestWithMeta<T>(path, init, workspaceId);
|
||||
return result.data;
|
||||
}
|
||||
|
||||
export async function apiRequestWithMeta<T>(
|
||||
path: string,
|
||||
init: RequestInit = {},
|
||||
workspaceId?: string,
|
||||
): Promise<{ data: T; meta: Record<string, unknown> }> {
|
||||
const finalPath = workspaceId ? appendWorkspaceId(path, workspaceId) : path;
|
||||
const response = await fetch(finalPath, {
|
||||
...init,
|
||||
credentials: "same-origin",
|
||||
headers: {
|
||||
"X-Request-ID": createUuid().replaceAll("-", ""),
|
||||
...(init.body ? { "Content-Type": "application/json" } : {}),
|
||||
...init.headers,
|
||||
},
|
||||
});
|
||||
|
||||
// Session expired / never authenticated — bounce to login. The
|
||||
// /login route itself is the only path that must remain reachable
|
||||
// while anonymous, so the redirect there is safe.
|
||||
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().catch(() => ({}))) as
|
||||
| ApiEnvelope<T>
|
||||
| ApiErrorEnvelope;
|
||||
if (!response.ok) {
|
||||
const error = payload as ApiErrorEnvelope;
|
||||
const detailMessage = typeof error.detail === "string"
|
||||
? error.detail
|
||||
: error.detail?.message;
|
||||
const editor = error.error?.details?.editor_name;
|
||||
throw new ApiRequestError(
|
||||
(editor ? `${error.error?.message ?? "文件正在编辑"}(${editor})` : undefined)
|
||||
?? error.error?.message
|
||||
?? detailMessage
|
||||
?? `请求失败(HTTP ${response.status})`,
|
||||
response.status,
|
||||
typeof error.detail === "object"
|
||||
? error.detail?.code
|
||||
: error.error?.code,
|
||||
);
|
||||
}
|
||||
const envelope = payload as ApiEnvelope<T>;
|
||||
return { data: envelope.data, meta: envelope.meta ?? {} };
|
||||
}
|
||||
|
||||
export function parseCursorPageMeta(meta: Record<string, unknown>): CursorPageMeta {
|
||||
const totalFromMeta =
|
||||
typeof meta.total_count === "number"
|
||||
? meta.total_count
|
||||
: typeof meta.count === "number"
|
||||
? meta.count
|
||||
: 0;
|
||||
return {
|
||||
limit: typeof meta.limit === "number" ? meta.limit : 10,
|
||||
page_count: typeof meta.page_count === "number" ? meta.page_count : 0,
|
||||
total_count: totalFromMeta,
|
||||
has_more: Boolean(meta.has_more),
|
||||
next_cursor: typeof meta.next_cursor === "string" ? meta.next_cursor : null,
|
||||
};
|
||||
}
|
||||
|
||||
export function buildCursorQuery(input: CursorListParams = {}): string {
|
||||
const parameters = new URLSearchParams();
|
||||
parameters.set("limit", String(input.limit ?? 10));
|
||||
if (input.cursor) parameters.set("cursor", input.cursor);
|
||||
const keyword = input.q?.trim();
|
||||
if (keyword) parameters.set("q", keyword);
|
||||
return parameters.toString();
|
||||
}
|
||||
|
||||
@@ -0,0 +1,348 @@
|
||||
import type {
|
||||
CursorListParams,
|
||||
CursorPage,
|
||||
Employee,
|
||||
} from "./_shared";
|
||||
import type {
|
||||
listScripts,
|
||||
countScripts,
|
||||
createScript,
|
||||
uploadScript,
|
||||
setScriptLock,
|
||||
updateScript,
|
||||
deleteScript,
|
||||
getLatestScriptVersion,
|
||||
publishScriptVersion,
|
||||
ScriptItem,
|
||||
Visibility,
|
||||
LatestVersion,
|
||||
StableVersion,
|
||||
} from "./scripts";
|
||||
import type {
|
||||
listResources,
|
||||
deleteResource,
|
||||
fetchResourceContentFile,
|
||||
fetchResourcePreview,
|
||||
createResourceUpload,
|
||||
uploadResourceBytes,
|
||||
bindResourceUpload,
|
||||
listWorkspaceDirectories,
|
||||
createWorkspaceDirectory,
|
||||
deleteWorkspaceDirectory,
|
||||
ResourceItem,
|
||||
ResourcePreviewPayload,
|
||||
WorkspaceDirectory,
|
||||
} from "./resources";
|
||||
import type {
|
||||
acquireFileLock,
|
||||
heartbeatFileLock,
|
||||
releaseFileLock,
|
||||
releaseFileLockOnUnload,
|
||||
createJupyterAccessTicket,
|
||||
ActiveEditSession,
|
||||
FileLockSession,
|
||||
JupyterAccessTicket,
|
||||
} from "./fileLocks";
|
||||
import type {
|
||||
listSchedules,
|
||||
getSchedule,
|
||||
createSchedule,
|
||||
updateSchedule,
|
||||
deleteSchedule,
|
||||
listScheduleArtifacts,
|
||||
hideScheduleArtifact,
|
||||
Schedule,
|
||||
ScheduleArtifact,
|
||||
CronPreview,
|
||||
ScheduleRunDetail,
|
||||
ScheduleRunSummary,
|
||||
ScheduleNodeRunArtifacts,
|
||||
} from "./schedules";
|
||||
import type {
|
||||
createScheduleNode,
|
||||
updateScheduleNode,
|
||||
deleteScheduleNode,
|
||||
createScheduleEdge,
|
||||
deleteScheduleEdge,
|
||||
validateSchedule,
|
||||
previewCron,
|
||||
runScheduleNow,
|
||||
listScheduleRuns,
|
||||
getScheduleRun,
|
||||
getScheduleNodeRunArtifacts,
|
||||
DagValidation,
|
||||
} from "./scheduleGraph";
|
||||
import type {
|
||||
listPlatformEmployees,
|
||||
createPlatformEmployee,
|
||||
updatePlatformEmployee,
|
||||
deletePlatformEmployee,
|
||||
} from "./platformEmployees";
|
||||
import type {
|
||||
listPlatformRoles,
|
||||
getPlatformRole,
|
||||
listPlatformPermissions,
|
||||
createPlatformRole,
|
||||
updatePlatformRole,
|
||||
deletePlatformRole,
|
||||
updatePlatformRolePermissions,
|
||||
Role,
|
||||
PlatformPermission,
|
||||
RoleCreatePayload,
|
||||
RoleUpdatePayload,
|
||||
} from "./platformRoles";
|
||||
import type {
|
||||
listEmployees,
|
||||
createEmployee,
|
||||
updateEmployee,
|
||||
deleteEmployee,
|
||||
} from "./employees";
|
||||
import type {
|
||||
listWorkspaces,
|
||||
createWorkspace,
|
||||
updateWorkspace,
|
||||
deleteWorkspace,
|
||||
Workspace,
|
||||
WorkspaceCreatePayload,
|
||||
WorkspaceUpdatePayload,
|
||||
} from "./workspaces";
|
||||
import type {
|
||||
listWorkspaceMembers,
|
||||
addWorkspaceMember,
|
||||
updateWorkspaceMember,
|
||||
deleteWorkspaceMember,
|
||||
WorkspaceMember,
|
||||
WorkspaceMemberAddPayload,
|
||||
WorkspaceMemberUpdatePayload,
|
||||
} from "./workspaceMembers";
|
||||
|
||||
// Workspace-bound API surface.
|
||||
//
|
||||
// `useApi()` in ~/context/AuthContext returns an object where every
|
||||
// function has had its first `workspaceId` argument pre-filled. The
|
||||
// type below lets consumers import the bound type without depending
|
||||
// on the raw functions. Keep this last in the file so the type
|
||||
// references all the exports above.
|
||||
// ----------------------------------------------------------------------------
|
||||
export type WorkspaceBoundApi = {
|
||||
listScripts: (
|
||||
parentPath?: Parameters<typeof listScripts>[1],
|
||||
ownerUserId?: Parameters<typeof listScripts>[2],
|
||||
) => Promise<ScriptItem[]>;
|
||||
countScripts: () => Promise<number>;
|
||||
listResources: (
|
||||
parentPath?: Parameters<typeof listResources>[1],
|
||||
opts?: Parameters<typeof listResources>[2],
|
||||
) => Promise<ResourceItem[]>;
|
||||
createScript: (
|
||||
input: Parameters<typeof createScript>[1],
|
||||
) => Promise<ScriptItem>;
|
||||
uploadScript: (
|
||||
file: File,
|
||||
parentPath?: string,
|
||||
visibility?: Visibility,
|
||||
) => Promise<ScriptItem>;
|
||||
createResourceUpload: (
|
||||
body: Parameters<typeof createResourceUpload>[1],
|
||||
) => Promise<{ upload_id: string; upload_path: string }>;
|
||||
uploadResourceBytes: (
|
||||
uploadId: string,
|
||||
fileBytes: ArrayBuffer | Blob,
|
||||
contentType: string,
|
||||
) => Promise<{ storage_object_id: string }>;
|
||||
bindResourceUpload: (
|
||||
uploadId: string,
|
||||
body: Parameters<typeof bindResourceUpload>[2],
|
||||
) => Promise<ResourceItem>;
|
||||
deleteResource: (
|
||||
resourceId: string,
|
||||
) => Promise<{ resource_id: string; status: string }>;
|
||||
fetchResourceContentFile: (
|
||||
resourceId: string,
|
||||
fileName: string,
|
||||
signal?: AbortSignal,
|
||||
) => Promise<File>;
|
||||
fetchResourcePreview: (
|
||||
resourceId: string,
|
||||
input?: { limit?: number },
|
||||
signal?: AbortSignal,
|
||||
) => Promise<ResourcePreviewPayload>;
|
||||
updateScript: (
|
||||
scriptId: string,
|
||||
input: Parameters<typeof updateScript>[2],
|
||||
) => Promise<ScriptItem>;
|
||||
deleteScript: (
|
||||
scriptId: string,
|
||||
) => Promise<{ script_id: string; status: string; versions_preserved: boolean }>;
|
||||
setScriptLock: (
|
||||
scriptId: string,
|
||||
isLocked: boolean,
|
||||
) => Promise<ScriptItem>;
|
||||
listWorkspaceDirectories: (
|
||||
parentPath?: Parameters<typeof listWorkspaceDirectories>[1],
|
||||
ownerUserId?: Parameters<typeof listWorkspaceDirectories>[2],
|
||||
) => Promise<WorkspaceDirectory[]>;
|
||||
createWorkspaceDirectory: (
|
||||
directoryName: string,
|
||||
parentPath?: string,
|
||||
) => Promise<WorkspaceDirectory>;
|
||||
deleteWorkspaceDirectory: (
|
||||
path: string,
|
||||
) => Promise<{
|
||||
path: string;
|
||||
status: string;
|
||||
deleted_scripts: number;
|
||||
versions_preserved: boolean;
|
||||
}>;
|
||||
acquireFileLock: (
|
||||
script: ScriptItem,
|
||||
) => Promise<ActiveEditSession>;
|
||||
heartbeatFileLock: (
|
||||
session: ActiveEditSession,
|
||||
) => Promise<FileLockSession>;
|
||||
releaseFileLock: (
|
||||
session: ActiveEditSession,
|
||||
) => Promise<FileLockSession>;
|
||||
releaseFileLockOnUnload: (session: ActiveEditSession) => void;
|
||||
createJupyterAccessTicket: (
|
||||
session: ActiveEditSession,
|
||||
) => Promise<JupyterAccessTicket>;
|
||||
getLatestScriptVersion: (scriptId: string) => Promise<LatestVersion | null>;
|
||||
publishScriptVersion: (
|
||||
input: Parameters<typeof publishScriptVersion>[1],
|
||||
) => Promise<StableVersion>;
|
||||
listSchedules: () => Promise<Schedule[]>;
|
||||
getSchedule: (scheduleId: string) => Promise<Schedule>;
|
||||
createSchedule: (
|
||||
input: Parameters<typeof createSchedule>[1],
|
||||
) => Promise<Schedule>;
|
||||
updateSchedule: (
|
||||
scheduleId: string,
|
||||
input: Parameters<typeof updateSchedule>[2],
|
||||
) => Promise<Schedule>;
|
||||
deleteSchedule: (
|
||||
scheduleId: string,
|
||||
workflowVersion: number,
|
||||
) => Promise<{ schedule_id: string; deleted: boolean; workflow_version: number }>;
|
||||
listScheduleArtifacts: () => Promise<ScheduleArtifact[]>;
|
||||
hideScheduleArtifact: (
|
||||
versionsId: string,
|
||||
) => Promise<{
|
||||
versions_id: string;
|
||||
deleted: boolean;
|
||||
artifact_preserved: boolean;
|
||||
}>;
|
||||
listEmployees: () => Promise<Employee[]>;
|
||||
listPlatformEmployees: (
|
||||
input?: CursorListParams,
|
||||
) => Promise<CursorPage<Employee>>;
|
||||
createEmployee: (
|
||||
input: Parameters<typeof createEmployee>[1],
|
||||
) => Promise<Employee>;
|
||||
createPlatformEmployee: (
|
||||
input: {
|
||||
username: string;
|
||||
display_name: string;
|
||||
email?: string | undefined;
|
||||
password: string;
|
||||
role_code?: "admin" | "developer";
|
||||
},
|
||||
) => Promise<Employee>;
|
||||
updateEmployee: (
|
||||
userId: string,
|
||||
input: Parameters<typeof updateEmployee>[2],
|
||||
) => Promise<Employee>;
|
||||
updatePlatformEmployee: (
|
||||
userId: string,
|
||||
input: {
|
||||
display_name?: string;
|
||||
email?: string | null;
|
||||
role_code?: "admin" | "developer";
|
||||
status?: "active" | "disabled" | "locked";
|
||||
},
|
||||
) => Promise<Employee>;
|
||||
deleteEmployee: (
|
||||
userId: string,
|
||||
) => Promise<{ user_id: string; deleted: boolean }>;
|
||||
deletePlatformEmployee: (
|
||||
userId: string,
|
||||
) => Promise<{ user_id: string; deleted: boolean }>;
|
||||
listPlatformRoles: () => Promise<Role[]>;
|
||||
getPlatformRole: (roleCode: string) => Promise<Role>;
|
||||
listPlatformPermissions: () => Promise<PlatformPermission[]>;
|
||||
createPlatformRole: (input: RoleCreatePayload) => Promise<Role>;
|
||||
updatePlatformRole: (
|
||||
roleCode: string,
|
||||
input: RoleUpdatePayload,
|
||||
) => Promise<Role>;
|
||||
deletePlatformRole: (
|
||||
roleCode: string,
|
||||
) => Promise<{ role_code: string; deleted: boolean }>;
|
||||
updatePlatformRolePermissions: (
|
||||
roleCode: string,
|
||||
permissionCodes: string[],
|
||||
) => Promise<Role>;
|
||||
createScheduleNode: (
|
||||
scheduleId: string,
|
||||
input: Parameters<typeof createScheduleNode>[2],
|
||||
) => Promise<Schedule>;
|
||||
updateScheduleNode: (
|
||||
scheduleId: string,
|
||||
nodeId: string,
|
||||
input: Parameters<typeof updateScheduleNode>[3],
|
||||
) => Promise<Schedule>;
|
||||
deleteScheduleNode: (
|
||||
scheduleId: string,
|
||||
nodeId: string,
|
||||
workflowVersion: number,
|
||||
options?: { delete_execution_history?: boolean },
|
||||
) => Promise<Schedule>;
|
||||
createScheduleEdge: (
|
||||
scheduleId: string,
|
||||
input: Parameters<typeof createScheduleEdge>[2],
|
||||
) => Promise<Schedule>;
|
||||
deleteScheduleEdge: (
|
||||
scheduleId: string,
|
||||
edgeId: string,
|
||||
workflowVersion: number,
|
||||
) => Promise<Schedule>;
|
||||
validateSchedule: (
|
||||
scheduleId: string,
|
||||
) => Promise<DagValidation & { schedule_id: string; workflow_version: number }>;
|
||||
previewCron: (
|
||||
input: Parameters<typeof previewCron>[1],
|
||||
) => Promise<CronPreview>;
|
||||
runScheduleNow: (scheduleId: string) => Promise<ScheduleRunDetail>;
|
||||
listScheduleRuns: (
|
||||
input?: Parameters<typeof listScheduleRuns>[1],
|
||||
) => Promise<ScheduleRunSummary[]>;
|
||||
getScheduleRun: (runId: string) => Promise<ScheduleRunDetail>;
|
||||
getScheduleNodeRunArtifacts: (
|
||||
runId: string,
|
||||
nodeRunId: string,
|
||||
) => Promise<ScheduleNodeRunArtifacts>;
|
||||
// Workspace (Project) Management - 系统管理接口
|
||||
listWorkspaces: (
|
||||
input?: CursorListParams,
|
||||
) => Promise<CursorPage<Workspace>>;
|
||||
createWorkspace: (input: WorkspaceCreatePayload) => Promise<Workspace>;
|
||||
updateWorkspace: (
|
||||
workspaceId: string,
|
||||
input: WorkspaceUpdatePayload,
|
||||
) => Promise<Workspace>;
|
||||
deleteWorkspace: (workspaceId: string) => Promise<{ workspace_id: string; deleted: boolean }>;
|
||||
listWorkspaceMembers: (workspaceId: string) => Promise<WorkspaceMember[]>;
|
||||
addWorkspaceMember: (
|
||||
workspaceId: string,
|
||||
input: WorkspaceMemberAddPayload,
|
||||
) => Promise<WorkspaceMember>;
|
||||
updateWorkspaceMember: (
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
input: WorkspaceMemberUpdatePayload,
|
||||
) => Promise<WorkspaceMember>;
|
||||
deleteWorkspaceMember: (
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
) => Promise<{ user_id: string; deleted: boolean }>;
|
||||
};
|
||||
@@ -0,0 +1,55 @@
|
||||
import { apiRequest, type Employee } from "./_shared";
|
||||
|
||||
export async function listEmployees(workspaceId: string): Promise<Employee[]> {
|
||||
return apiRequest<Employee[]>("/api/v1/admin/employees", {}, workspaceId);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 createPlatformEmployee 代替 */
|
||||
export async function createEmployee(
|
||||
workspaceId: string,
|
||||
input: {
|
||||
username: string;
|
||||
display_name: string;
|
||||
email?: string | null;
|
||||
role_code: "admin" | "developer";
|
||||
password: string;
|
||||
},
|
||||
): Promise<Employee> {
|
||||
return apiRequest<Employee>(
|
||||
"/api/v1/admin/employees",
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 updatePlatformEmployee 代替 */
|
||||
export async function updateEmployee(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
input: {
|
||||
display_name?: string;
|
||||
email?: string | null;
|
||||
role_code?: "admin" | "developer";
|
||||
status?: "active" | "disabled" | "locked";
|
||||
},
|
||||
): Promise<Employee> {
|
||||
return apiRequest<Employee>(
|
||||
`/api/v1/admin/employees/${userId}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
/** @deprecated 使用 deletePlatformEmployee 代替 */
|
||||
export async function deleteEmployee(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
): Promise<{ user_id: string; deleted: boolean }> {
|
||||
return apiRequest(
|
||||
`/api/v1/admin/employees/${userId}`,
|
||||
{ method: "DELETE" },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -0,0 +1,140 @@
|
||||
import { ApiRequestError, createUuid } from "./_shared";
|
||||
import type { ScriptItem } from "./scripts";
|
||||
|
||||
export type FileLockSession = {
|
||||
edit_session_id: string;
|
||||
workspace_id: string;
|
||||
storage_object_id: string;
|
||||
user_id: string;
|
||||
session_status: "active" | "closed" | "expired";
|
||||
lease_seconds: number;
|
||||
heartbeat_interval_seconds: number;
|
||||
expires_at: string;
|
||||
runtime_id: string;
|
||||
jupyter_session_id: string;
|
||||
jupyter_url?: string;
|
||||
relative_path?: string;
|
||||
lock_token?: string;
|
||||
};
|
||||
|
||||
export type ActiveEditSession = FileLockSession & {
|
||||
script_id: string;
|
||||
script_name: string;
|
||||
jupyter_path: string;
|
||||
lock_token: string;
|
||||
ticket_expires_at?: string;
|
||||
};
|
||||
|
||||
export type JupyterAccessTicket = {
|
||||
edit_session_id: string;
|
||||
jupyter_url: string;
|
||||
expires_at: string;
|
||||
};
|
||||
|
||||
// 本地浏览器级“编辑锁”——后端没有 acquire/heartbeat/release/edit-session 表。
|
||||
// 这里的四个函数全部是占位:返回结构是为了让上层 store 的
|
||||
// _editSession / sessionCache 继续按“session”接口工作,但锁的实际作用域
|
||||
// 仅限当前 tab。关闭 tab、刷新页面、用隐身模式打开、或换浏览器,锁即失效。
|
||||
// 不要把这些函数当作鉴权或并发控制用——它们什么都不查、什么都不写。
|
||||
// 真实并发控制需要后端 edit_sessions 表 + Nginx auth_request 联动,是后续工单。
|
||||
|
||||
export async function acquireFileLock(
|
||||
workspaceId: string,
|
||||
script: ScriptItem,
|
||||
): Promise<ActiveEditSession> {
|
||||
const now = Date.now();
|
||||
return {
|
||||
edit_session_id: createUuid().replaceAll("-", ""),
|
||||
workspace_id: workspaceId,
|
||||
storage_object_id: script.current_object_id,
|
||||
user_id: script.owner_user_id,
|
||||
session_status: "active",
|
||||
lease_seconds: 3600,
|
||||
heartbeat_interval_seconds: 300,
|
||||
expires_at: new Date(now + 3600_000).toISOString(),
|
||||
runtime_id: workspaceId,
|
||||
jupyter_session_id: "local",
|
||||
relative_path: script.relative_path,
|
||||
lock_token: "local",
|
||||
script_id: script.script_id,
|
||||
script_name: script.script_name,
|
||||
jupyter_path: script.jupyter_path,
|
||||
};
|
||||
}
|
||||
|
||||
export async function heartbeatFileLock(
|
||||
_workspaceId: string,
|
||||
session: ActiveEditSession,
|
||||
): Promise<FileLockSession> {
|
||||
// 本地锁不存在过期概念;只是把 expires_at 推后让 UI 看着还活着。
|
||||
// 该字段当前没有任何消费者,保留只是为了不破坏契约。
|
||||
return session;
|
||||
}
|
||||
|
||||
export async function releaseFileLock(
|
||||
_workspaceId: string,
|
||||
session: ActiveEditSession,
|
||||
): Promise<FileLockSession> {
|
||||
return { ...session, session_status: "closed" };
|
||||
}
|
||||
|
||||
export function releaseFileLockOnUnload(
|
||||
_workspaceId: string,
|
||||
_session: ActiveEditSession,
|
||||
): void {
|
||||
// 本地锁随 tab 生命周期结束。beforeunload 调到这里只是让 store 端
|
||||
// 清理模块级引用,避免下一个 tab 复用时看到陈旧 _editSession。
|
||||
}
|
||||
|
||||
async function waitForJupyterReady(jupyterUrl: string): Promise<void> {
|
||||
const retryableStatuses = new Set([502, 503, 504]);
|
||||
let lastStatus = 0;
|
||||
|
||||
for (let attempt = 0; attempt < 10; attempt += 1) {
|
||||
const response = await fetch(jupyterUrl, {
|
||||
credentials: "same-origin",
|
||||
cache: "no-store",
|
||||
});
|
||||
if (response.ok) return;
|
||||
|
||||
lastStatus = response.status;
|
||||
if (!retryableStatuses.has(response.status)) {
|
||||
throw new ApiRequestError(
|
||||
`Jupyter 打开失败(HTTP ${response.status})`,
|
||||
response.status,
|
||||
);
|
||||
}
|
||||
await new Promise((resolve) => window.setTimeout(resolve, 400));
|
||||
}
|
||||
|
||||
throw new ApiRequestError(
|
||||
`Jupyter 服务启动超时${lastStatus ? `(HTTP ${lastStatus})` : ""}`,
|
||||
lastStatus || 504,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createJupyterAccessTicket(
|
||||
workspaceId: string,
|
||||
session: ActiveEditSession,
|
||||
): Promise<JupyterAccessTicket> {
|
||||
const editorRoute = session.script_name.toLowerCase().endsWith(".ipynb")
|
||||
? "notebooks"
|
||||
: "edit";
|
||||
const encodedPath = session.jupyter_path
|
||||
.split("/")
|
||||
.filter(Boolean)
|
||||
.map(encodeURIComponent)
|
||||
.join("/");
|
||||
if (!encodedPath) {
|
||||
throw new Error("脚本缺少 Jupyter 存储路径");
|
||||
}
|
||||
|
||||
const jupyterUrl = `/jupyter/${encodeURIComponent(workspaceId)}/${editorRoute}/${encodedPath}`;
|
||||
await waitForJupyterReady(jupyterUrl);
|
||||
return {
|
||||
edit_session_id: session.edit_session_id,
|
||||
jupyter_url: jupyterUrl,
|
||||
expires_at: new Date(Date.now() + 3600_000).toISOString(),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import {
|
||||
apiRequest,
|
||||
apiRequestWithMeta,
|
||||
buildCursorQuery,
|
||||
parseCursorPageMeta,
|
||||
type CursorListParams,
|
||||
type CursorPage,
|
||||
type Employee,
|
||||
} from "./_shared";
|
||||
|
||||
// 系统管理级别接口 - 不区分 workspace
|
||||
export async function listPlatformEmployees(
|
||||
input: CursorListParams = {},
|
||||
): Promise<CursorPage<Employee>> {
|
||||
const query = buildCursorQuery(input);
|
||||
const { data, meta } = await apiRequestWithMeta<Employee[]>(
|
||||
`/api/v1/platform/employees?${query}`,
|
||||
);
|
||||
return { items: data, meta: parseCursorPageMeta(meta) };
|
||||
}
|
||||
|
||||
export async function createPlatformEmployee(
|
||||
input: {
|
||||
username: string;
|
||||
display_name: string;
|
||||
email?: string | undefined;
|
||||
password: string;
|
||||
role_code?: "admin" | "developer";
|
||||
},
|
||||
): Promise<Employee> {
|
||||
return apiRequest<Employee>(
|
||||
"/api/v1/platform/employees",
|
||||
{ method: "POST", body: JSON.stringify({ ...input, role_code: input.role_code ?? "developer" }) },
|
||||
);
|
||||
}
|
||||
|
||||
export async function updatePlatformEmployee(
|
||||
userId: string,
|
||||
input: {
|
||||
display_name?: string;
|
||||
email?: string | null;
|
||||
role_code?: "admin" | "developer";
|
||||
status?: "active" | "disabled" | "locked";
|
||||
},
|
||||
): Promise<Employee> {
|
||||
return apiRequest<Employee>(
|
||||
`/api/v1/platform/employees/${userId}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
);
|
||||
}
|
||||
|
||||
export async function deletePlatformEmployee(
|
||||
userId: string,
|
||||
): Promise<{ user_id: string; deleted: boolean }> {
|
||||
return apiRequest(
|
||||
`/api/v1/platform/employees/${userId}`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
import { apiRequest } from "./_shared";
|
||||
|
||||
// Role (Platform Role) types - 角色管理接口
|
||||
export type Role = {
|
||||
role_id: string;
|
||||
role_code: string;
|
||||
role_name: string;
|
||||
is_builtin: boolean;
|
||||
description: string | null;
|
||||
permission_codes: string[];
|
||||
};
|
||||
|
||||
export type PlatformPermission = {
|
||||
permission_code: string;
|
||||
permission_name: string;
|
||||
module_code: string;
|
||||
description: string | null;
|
||||
};
|
||||
|
||||
export type RoleCreatePayload = {
|
||||
role_code: string;
|
||||
role_name: string;
|
||||
description?: string | null;
|
||||
permission_codes?: string[];
|
||||
};
|
||||
|
||||
export type RoleUpdatePayload = {
|
||||
role_name?: string;
|
||||
description?: string | null; // null = clear
|
||||
};
|
||||
|
||||
export async function listPlatformRoles(): Promise<Role[]> {
|
||||
return apiRequest<Role[]>("/api/v1/platform/roles");
|
||||
}
|
||||
|
||||
export async function getPlatformRole(roleCode: string): Promise<Role> {
|
||||
return apiRequest<Role>(`/api/v1/platform/roles/${roleCode}/permissions`);
|
||||
}
|
||||
|
||||
export async function listPlatformPermissions(): Promise<PlatformPermission[]> {
|
||||
return apiRequest<PlatformPermission[]>("/api/v1/platform/permissions");
|
||||
}
|
||||
|
||||
export async function createPlatformRole(input: RoleCreatePayload): Promise<Role> {
|
||||
return apiRequest<Role>(
|
||||
"/api/v1/platform/roles",
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
);
|
||||
}
|
||||
|
||||
export async function updatePlatformRole(
|
||||
roleCode: string,
|
||||
input: RoleUpdatePayload,
|
||||
): Promise<Role> {
|
||||
return apiRequest<Role>(
|
||||
`/api/v1/platform/roles/${roleCode}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
);
|
||||
}
|
||||
|
||||
export async function deletePlatformRole(
|
||||
roleCode: string,
|
||||
): Promise<{ role_code: string; deleted: boolean }> {
|
||||
return apiRequest(
|
||||
`/api/v1/platform/roles/${roleCode}`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
}
|
||||
|
||||
export async function updatePlatformRolePermissions(
|
||||
roleCode: string,
|
||||
permissionCodes: string[],
|
||||
): Promise<Role> {
|
||||
return apiRequest<Role>(
|
||||
`/api/v1/platform/roles/${roleCode}/permissions`,
|
||||
{ method: "PATCH", body: JSON.stringify({ permission_codes: permissionCodes }) },
|
||||
);
|
||||
}
|
||||
|
||||
// 兼容旧的 workspace 级别接口(已废弃,建议使用 system-level 接口)
|
||||
/** @deprecated 使用 listPlatformEmployees 代替 */
|
||||
@@ -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,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,271 @@
|
||||
import { apiRequest, createUuid } from "./_shared";
|
||||
import type { ScriptType } from "./scripts";
|
||||
import type {
|
||||
CronPreview,
|
||||
PythonVersion,
|
||||
Schedule,
|
||||
ScheduleNodeRunArtifacts,
|
||||
ScheduleRunDetail,
|
||||
ScheduleRunStatus,
|
||||
ScheduleRunSummary,
|
||||
} from "./schedules";
|
||||
|
||||
export type ScheduleNode = {
|
||||
node_id: string;
|
||||
schedule_id: string;
|
||||
node_key: string;
|
||||
node_name: string;
|
||||
versions_id: string;
|
||||
timeout_seconds: number;
|
||||
retry_count: number;
|
||||
retry_interval_sec: number;
|
||||
position_x: number;
|
||||
position_y: number;
|
||||
arguments_json: Record<string, unknown>;
|
||||
env_refs_json: Record<string, string>;
|
||||
python_version: PythonVersion;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
version: {
|
||||
versions_id: string;
|
||||
version_label: string;
|
||||
script_id: string;
|
||||
script_name: string;
|
||||
script_type: ScriptType;
|
||||
content_hash: string;
|
||||
created_at: string;
|
||||
};
|
||||
};
|
||||
|
||||
export type ScheduleEdge = {
|
||||
edge_id: string;
|
||||
schedule_id: string;
|
||||
source_node_id: string;
|
||||
target_node_id: string;
|
||||
condition_expr: string | null;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type DagValidation = {
|
||||
valid: boolean;
|
||||
node_count: number;
|
||||
edge_count: number;
|
||||
root_node_ids: string[];
|
||||
leaf_node_ids: string[];
|
||||
topological_order: string[];
|
||||
errors: Array<{
|
||||
code: string;
|
||||
message: string;
|
||||
edge_id?: string;
|
||||
node_ids?: string[];
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ScheduleNodeRunStatus =
|
||||
| ScheduleRunStatus
|
||||
| "skipped";
|
||||
|
||||
export type ScheduleNodeRun = {
|
||||
node_run_id: string;
|
||||
run_id: string;
|
||||
node_id: string;
|
||||
versions_id: string;
|
||||
attempt_no: number;
|
||||
node_status: ScheduleNodeRunStatus;
|
||||
state_version: number;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
duration_ms: number | null;
|
||||
exit_code: number | null;
|
||||
message: string | null;
|
||||
logs_object_id: string | null;
|
||||
result_object_id: string | null;
|
||||
};
|
||||
|
||||
export async function createScheduleNode(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
input: {
|
||||
workflow_version: number;
|
||||
node_key: string;
|
||||
node_name: string;
|
||||
versions_id: string;
|
||||
timeout_seconds?: number;
|
||||
retry_count?: number;
|
||||
retry_interval_sec?: number;
|
||||
position_x?: number;
|
||||
position_y?: number;
|
||||
arguments_json?: Record<string, unknown>;
|
||||
env_refs_json?: Record<string, string>;
|
||||
python_version?: PythonVersion;
|
||||
},
|
||||
): Promise<Schedule> {
|
||||
return apiRequest<Schedule>(
|
||||
`/api/v1/schedules/${scheduleId}/nodes`,
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateScheduleNode(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
nodeId: string,
|
||||
input: {
|
||||
workflow_version: number;
|
||||
node_name?: string;
|
||||
versions_id?: string;
|
||||
timeout_seconds?: number;
|
||||
retry_count?: number;
|
||||
retry_interval_sec?: number;
|
||||
position_x?: number;
|
||||
position_y?: number;
|
||||
arguments_json?: Record<string, unknown>;
|
||||
env_refs_json?: Record<string, string>;
|
||||
python_version?: PythonVersion;
|
||||
},
|
||||
): Promise<Schedule> {
|
||||
return apiRequest<Schedule>(
|
||||
`/api/v1/schedules/${scheduleId}/nodes/${nodeId}`,
|
||||
{ method: "PUT", body: JSON.stringify(input) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteScheduleNode(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
nodeId: string,
|
||||
workflowVersion: number,
|
||||
options: { delete_execution_history?: boolean } = {},
|
||||
): Promise<Schedule> {
|
||||
return apiRequest<Schedule>(
|
||||
`/api/v1/schedules/${scheduleId}/nodes/${nodeId}`,
|
||||
{
|
||||
method: "DELETE",
|
||||
body: JSON.stringify({ workflow_version: workflowVersion, ...options }),
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createScheduleEdge(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
input: {
|
||||
workflow_version: number;
|
||||
source_node_id: string;
|
||||
target_node_id: string;
|
||||
condition_expr?: string | null;
|
||||
},
|
||||
): Promise<Schedule> {
|
||||
return apiRequest<Schedule>(
|
||||
`/api/v1/schedules/${scheduleId}/edges`,
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteScheduleEdge(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
edgeId: string,
|
||||
workflowVersion: number,
|
||||
): Promise<Schedule> {
|
||||
return apiRequest<Schedule>(
|
||||
`/api/v1/schedules/${scheduleId}/edges/${edgeId}`,
|
||||
{ method: "DELETE", body: JSON.stringify({ workflow_version: workflowVersion }) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function validateSchedule(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
): Promise<DagValidation & {
|
||||
schedule_id: string;
|
||||
workflow_version: number;
|
||||
}> {
|
||||
return apiRequest(
|
||||
`/api/v1/schedules/${scheduleId}/validate`,
|
||||
{ method: "POST" },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function previewCron(
|
||||
workspaceId: string,
|
||||
input: {
|
||||
cron_expression: string;
|
||||
timezone: string;
|
||||
count?: number;
|
||||
base_time?: string;
|
||||
},
|
||||
): Promise<CronPreview> {
|
||||
return apiRequest<CronPreview>(
|
||||
"/api/v1/cron/preview",
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function runScheduleNow(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
): Promise<ScheduleRunDetail> {
|
||||
return apiRequest<ScheduleRunDetail>(
|
||||
`/api/v1/schedules/${scheduleId}/run`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: {
|
||||
"Idempotency-Key": createUuid(),
|
||||
},
|
||||
body: JSON.stringify({ reason: "manual_run" }),
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function listScheduleRuns(
|
||||
workspaceId: string,
|
||||
input: {
|
||||
scheduleId?: string;
|
||||
status?: ScheduleRunStatus;
|
||||
limit?: number;
|
||||
} = {},
|
||||
): Promise<ScheduleRunSummary[]> {
|
||||
const query = new URLSearchParams();
|
||||
if (input.scheduleId) query.set("schedule_id", input.scheduleId);
|
||||
if (input.status) query.set("status", input.status);
|
||||
query.set("limit", String(input.limit ?? 20));
|
||||
return apiRequest<ScheduleRunSummary[]>(
|
||||
`/api/v1/schedule-runs?${query.toString()}`,
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getScheduleRun(
|
||||
workspaceId: string,
|
||||
runId: string,
|
||||
): Promise<ScheduleRunDetail> {
|
||||
return apiRequest<ScheduleRunDetail>(
|
||||
`/api/v1/schedule-runs/${runId}`,
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getScheduleNodeRunArtifacts(
|
||||
workspaceId: string,
|
||||
runId: string,
|
||||
nodeRunId: string,
|
||||
): Promise<ScheduleNodeRunArtifacts> {
|
||||
return apiRequest<ScheduleNodeRunArtifacts>(
|
||||
`/api/v1/schedule-runs/${runId}/node-runs/${nodeRunId}/artifacts`,
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
// ----------------------------------------------------------------------------
|
||||
@@ -0,0 +1,197 @@
|
||||
import { apiRequest } from "./_shared";
|
||||
import type { ScriptType, Visibility } from "./scripts";
|
||||
import type {
|
||||
DagValidation,
|
||||
ScheduleEdge,
|
||||
ScheduleNode,
|
||||
ScheduleNodeRun,
|
||||
} from "./scheduleGraph";
|
||||
|
||||
export type ScheduleArtifact = {
|
||||
versions_id: string;
|
||||
version_label: string;
|
||||
script_id: string;
|
||||
script_name: string;
|
||||
script_type: ScriptType;
|
||||
content_hash: string;
|
||||
file_size_bytes: number;
|
||||
visibility: Visibility;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type PythonVersion = "3.8" | "3.10" | "3.12";
|
||||
|
||||
export type Schedule = {
|
||||
schedule_id: string;
|
||||
workspace_id: string;
|
||||
schedule_name: string;
|
||||
description: string | null;
|
||||
trigger_type: "manual" | "cron" | "api";
|
||||
cron_expression: string | null;
|
||||
timezone: string;
|
||||
enabled: boolean;
|
||||
workflow_version: number;
|
||||
max_concurrency: number;
|
||||
failure_policy: "stop" | "continue";
|
||||
last_run_at: string | null;
|
||||
next_run_at: string | null;
|
||||
created_by: string;
|
||||
updated_by: string;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
node_count: number;
|
||||
edge_count: number;
|
||||
nodes: ScheduleNode[];
|
||||
edges: ScheduleEdge[];
|
||||
dag_validation: DagValidation;
|
||||
};
|
||||
|
||||
export type CronPreview = {
|
||||
cron_expression: string;
|
||||
timezone: string;
|
||||
base_time: string;
|
||||
occurrences: Array<{
|
||||
local_time: string;
|
||||
utc_time: string;
|
||||
}>;
|
||||
};
|
||||
|
||||
export type ScheduleRunStatus =
|
||||
| "queued"
|
||||
| "running"
|
||||
| "succeeded"
|
||||
| "failed"
|
||||
| "cancelled"
|
||||
| "timed_out";
|
||||
|
||||
export type ScheduleRunSummary = {
|
||||
run_id: string;
|
||||
schedule_id: string;
|
||||
workspace_id: string;
|
||||
workflow_version: number;
|
||||
trigger_type: "manual" | "cron" | "api" | "retry";
|
||||
run_status: ScheduleRunStatus;
|
||||
state_version: number;
|
||||
queued_at: string;
|
||||
started_at: string | null;
|
||||
finished_at: string | null;
|
||||
duration_ms: number | null;
|
||||
error_code: string | null;
|
||||
error_message: string | null;
|
||||
logs_object_id: string | null;
|
||||
result_object_id: string | null;
|
||||
};
|
||||
|
||||
export type ScheduleRunDetail = ScheduleRunSummary & {
|
||||
node_runs: ScheduleNodeRun[];
|
||||
};
|
||||
|
||||
export type ScheduleRunArtifact = {
|
||||
url: string;
|
||||
file_name: string;
|
||||
mime_type: string | null;
|
||||
size_bytes: number;
|
||||
};
|
||||
|
||||
export type ScheduleNodeRunArtifacts = {
|
||||
run_id: string;
|
||||
node_run_id: string;
|
||||
log: ScheduleRunArtifact | null;
|
||||
result: ScheduleRunArtifact | null;
|
||||
};
|
||||
|
||||
export async function listSchedules(workspaceId: string): Promise<Schedule[]> {
|
||||
return apiRequest<Schedule[]>("/api/v1/schedules", {}, workspaceId);
|
||||
}
|
||||
|
||||
export async function getSchedule(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
): Promise<Schedule> {
|
||||
return apiRequest<Schedule>(
|
||||
`/api/v1/schedules/${scheduleId}`,
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createSchedule(
|
||||
workspaceId: string,
|
||||
input: {
|
||||
schedule_name: string;
|
||||
description?: string | null;
|
||||
trigger_type?: "manual" | "cron" | "api";
|
||||
cron_expression?: string | null;
|
||||
timezone?: string;
|
||||
enabled?: boolean;
|
||||
max_concurrency?: number;
|
||||
failure_policy?: "stop" | "continue";
|
||||
},
|
||||
): Promise<Schedule> {
|
||||
return apiRequest<Schedule>(
|
||||
"/api/v1/schedules",
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateSchedule(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
input: {
|
||||
workflow_version: number;
|
||||
schedule_name?: string;
|
||||
description?: string | null;
|
||||
trigger_type?: "manual" | "cron" | "api";
|
||||
cron_expression?: string | null;
|
||||
timezone?: string;
|
||||
enabled?: boolean;
|
||||
max_concurrency?: number;
|
||||
failure_policy?: "stop" | "continue";
|
||||
},
|
||||
): Promise<Schedule> {
|
||||
return apiRequest<Schedule>(
|
||||
`/api/v1/schedules/${scheduleId}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteSchedule(
|
||||
workspaceId: string,
|
||||
scheduleId: string,
|
||||
workflowVersion: number,
|
||||
): Promise<{ schedule_id: string; deleted: boolean; workflow_version: number }> {
|
||||
return apiRequest(
|
||||
`/api/v1/schedules/${scheduleId}`,
|
||||
{ method: "DELETE", body: JSON.stringify({ workflow_version: workflowVersion }) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function listScheduleArtifacts(
|
||||
workspaceId: string,
|
||||
): Promise<ScheduleArtifact[]> {
|
||||
return apiRequest<ScheduleArtifact[]>(
|
||||
"/api/v1/schedule-artifacts",
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function hideScheduleArtifact(
|
||||
workspaceId: string,
|
||||
versionsId: string,
|
||||
): Promise<{
|
||||
versions_id: string;
|
||||
deleted: boolean;
|
||||
artifact_preserved: boolean;
|
||||
}> {
|
||||
return apiRequest(
|
||||
`/api/v1/versions/${versionsId}`,
|
||||
{ method: "DELETE" },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,299 @@
|
||||
import {
|
||||
apiRequest,
|
||||
ApiRequestError,
|
||||
createUuid,
|
||||
type ApiErrorEnvelope,
|
||||
} from "./_shared";
|
||||
|
||||
export type ScriptType = "python" | "notebook";
|
||||
export type Visibility = "private" | "workspace" | "public";
|
||||
|
||||
export type ScriptItem = {
|
||||
script_id: string;
|
||||
workspace_id: string;
|
||||
current_object_id: string;
|
||||
owner_user_id: string;
|
||||
owner_display_name: string | null;
|
||||
script_name: string;
|
||||
script_type: ScriptType;
|
||||
visibility: Visibility;
|
||||
status: string;
|
||||
is_locked: boolean;
|
||||
relative_path: string;
|
||||
jupyter_path: string;
|
||||
content_hash: string;
|
||||
size_bytes: number;
|
||||
created_at: string;
|
||||
updated_at: string;
|
||||
};
|
||||
|
||||
export async function listScripts(
|
||||
workspaceId: string,
|
||||
parentPath: string = "",
|
||||
ownerUserId?: string,
|
||||
): Promise<ScriptItem[]> {
|
||||
// Default (no ownerUserId) scopes to the requester's own subtree; passing
|
||||
// ownerUserId scopes to that owner's subtree (workspace/public only — the
|
||||
// backend excludes their private) so the tree can lazily fetch another
|
||||
// member's content when their group is expanded.
|
||||
const parameters = new URLSearchParams();
|
||||
if (parentPath) parameters.set("parent_path", parentPath);
|
||||
if (ownerUserId) parameters.set("owner_user_id", ownerUserId);
|
||||
const query = parameters.toString();
|
||||
return apiRequest<ScriptItem[]>(
|
||||
`/api/v1/scripts${query ? `?${query}` : ""}`,
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function countScripts(
|
||||
workspaceId: string,
|
||||
): Promise<number> {
|
||||
// Backend route /api/v1/scripts/count must be declared BEFORE the
|
||||
// /scripts/{script_id} route on the server side. Returns
|
||||
// { data: { total: number } } — the dashboard's single source of
|
||||
// truth for "total active scripts in workspace", independent of the
|
||||
// lazy-loaded scripts[] in the workspace store.
|
||||
const envelope = await apiRequest<{ total: number }>(
|
||||
"/api/v1/scripts/count",
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
return envelope.total;
|
||||
}
|
||||
|
||||
function initialContent(scriptType: ScriptType): string {
|
||||
if (scriptType === "python") {
|
||||
return [
|
||||
'"""模型实验开发平台构建脚本。"""',
|
||||
"",
|
||||
"",
|
||||
"def main() -> None:",
|
||||
' print("Hello, Model Platform!")',
|
||||
"",
|
||||
"",
|
||||
'if __name__ == "__main__":',
|
||||
" main()",
|
||||
"",
|
||||
].join("\n");
|
||||
}
|
||||
|
||||
return JSON.stringify(
|
||||
{
|
||||
cells: [
|
||||
{
|
||||
id: "intro",
|
||||
cell_type: "code",
|
||||
execution_count: null,
|
||||
metadata: {},
|
||||
outputs: [],
|
||||
source: ["print('Hello, Model Platform!')\n"],
|
||||
},
|
||||
],
|
||||
metadata: {
|
||||
kernelspec: {
|
||||
display_name: "Python 3",
|
||||
language: "python",
|
||||
name: "python3",
|
||||
},
|
||||
language_info: {
|
||||
name: "python",
|
||||
version: "3.12",
|
||||
},
|
||||
},
|
||||
nbformat: 4,
|
||||
nbformat_minor: 5,
|
||||
},
|
||||
null,
|
||||
2,
|
||||
);
|
||||
}
|
||||
|
||||
export async function createScript(
|
||||
workspaceId: string,
|
||||
input: {
|
||||
name: string;
|
||||
scriptType: ScriptType;
|
||||
visibility: Visibility;
|
||||
parentPath?: string | null;
|
||||
},
|
||||
): Promise<ScriptItem> {
|
||||
return apiRequest<ScriptItem>(
|
||||
"/api/v1/scripts",
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
script_name: input.name.trim(),
|
||||
script_type: input.scriptType,
|
||||
visibility: input.visibility,
|
||||
content: initialContent(input.scriptType),
|
||||
parent_path: input.parentPath,
|
||||
}),
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function uploadScript(
|
||||
workspaceId: string,
|
||||
file: File,
|
||||
parentPath = "",
|
||||
visibility: Visibility = "workspace",
|
||||
): Promise<ScriptItem> {
|
||||
const parameters = new URLSearchParams({
|
||||
file_name: file.name,
|
||||
parent_path: parentPath,
|
||||
visibility,
|
||||
});
|
||||
return apiRequest<ScriptItem>(
|
||||
`/api/v1/scripts/upload?${parameters.toString()}`,
|
||||
{
|
||||
method: "POST",
|
||||
headers: { "Content-Type": "application/octet-stream" },
|
||||
body: file,
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function setScriptLock(
|
||||
workspaceId: string,
|
||||
scriptId: string,
|
||||
isLocked: boolean,
|
||||
): Promise<ScriptItem> {
|
||||
return apiRequest<ScriptItem>(
|
||||
`/api/v1/scripts/${encodeURIComponent(scriptId)}/lock`,
|
||||
{
|
||||
method: "PATCH",
|
||||
body: JSON.stringify({ is_locked: isLocked }),
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateScript(
|
||||
workspaceId: string,
|
||||
scriptId: string,
|
||||
input: { content: string },
|
||||
): Promise<ScriptItem> {
|
||||
return apiRequest<ScriptItem>(
|
||||
`/api/v1/scripts/${scriptId}`,
|
||||
{ method: "PUT", body: JSON.stringify(input) },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function getScriptContent(
|
||||
workspaceId: string,
|
||||
scriptId: string,
|
||||
): Promise<{
|
||||
script_id: string;
|
||||
script_type: ScriptType;
|
||||
content: string | object;
|
||||
format: string;
|
||||
}> {
|
||||
const response = await fetch(
|
||||
`/api/v1/scripts/${scriptId}/content?workspace_id=${encodeURIComponent(workspaceId)}`,
|
||||
{
|
||||
credentials: "same-origin",
|
||||
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: { script_id: string; script_type: ScriptType; content: string | object; format: string } }).data;
|
||||
if (!data || !data.script_type) {
|
||||
throw new ApiRequestError("响应数据格式错误", response.status);
|
||||
}
|
||||
return data;
|
||||
}
|
||||
|
||||
export async function deleteScript(
|
||||
workspaceId: string,
|
||||
scriptId: string,
|
||||
): Promise<{ script_id: string; status: string; versions_preserved: boolean }> {
|
||||
return apiRequest(
|
||||
`/api/v1/scripts/${scriptId}`,
|
||||
{ method: "DELETE" },
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export type StableVersion = {
|
||||
versions_id: string;
|
||||
workspace_id: string;
|
||||
script_id: string;
|
||||
source_object_id: string;
|
||||
artifact_object_id: string;
|
||||
version_no: number;
|
||||
version_label: string;
|
||||
source_path: string;
|
||||
artifact_path: string;
|
||||
content_hash: string;
|
||||
file_size_bytes: number;
|
||||
visibility: Visibility;
|
||||
release_note: string | null;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
};
|
||||
|
||||
export type LatestVersion = {
|
||||
versions_id: string;
|
||||
version_label: string;
|
||||
};
|
||||
|
||||
export async function getLatestScriptVersion(
|
||||
workspaceId: string,
|
||||
scriptId: string,
|
||||
): Promise<LatestVersion | null> {
|
||||
return apiRequest<LatestVersion | null>(
|
||||
`/api/v1/scripts/${scriptId}/latest-version`,
|
||||
{},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
export async function publishScriptVersion(
|
||||
workspaceId: string,
|
||||
input: {
|
||||
script: ScriptItem;
|
||||
releaseNote: string;
|
||||
visibility: Visibility;
|
||||
},
|
||||
): Promise<StableVersion> {
|
||||
return apiRequest<StableVersion>(
|
||||
`/api/v1/scripts/${input.script.script_id}/versions`,
|
||||
{
|
||||
method: "POST",
|
||||
body: JSON.stringify({
|
||||
source_object_id: input.script.current_object_id,
|
||||
release_note: input.releaseNote.trim() || null,
|
||||
visibility: input.visibility,
|
||||
}),
|
||||
},
|
||||
workspaceId,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,60 @@
|
||||
import { apiRequest } from "./_shared";
|
||||
|
||||
export type WorkspaceMember = {
|
||||
user_id: string;
|
||||
username: string;
|
||||
display_name: string;
|
||||
email: string | null;
|
||||
role_code: "admin" | "developer";
|
||||
role_name: string;
|
||||
member_status: "active" | "disabled" | "locked";
|
||||
joined_at: string;
|
||||
};
|
||||
|
||||
/** 加入工作区;角色继承自用户的 platform_role,请求体不能带 role_code。 */
|
||||
export type WorkspaceMemberAddPayload = {
|
||||
user_id: string;
|
||||
};
|
||||
|
||||
/** 仅可改成员状态;改角色请 PATCH /platform/employees/{user_id}。 */
|
||||
export type WorkspaceMemberUpdatePayload = {
|
||||
member_status?: "active" | "disabled" | "locked";
|
||||
};
|
||||
|
||||
export async function listWorkspaceMembers(workspaceId: string): Promise<WorkspaceMember[]> {
|
||||
return apiRequest<WorkspaceMember[]>(
|
||||
`/api/v1/platform/workspaces/${workspaceId}/members`,
|
||||
);
|
||||
}
|
||||
|
||||
export async function addWorkspaceMember(
|
||||
workspaceId: string,
|
||||
input: WorkspaceMemberAddPayload,
|
||||
): Promise<WorkspaceMember> {
|
||||
return apiRequest<WorkspaceMember>(
|
||||
`/api/v1/platform/workspaces/${workspaceId}/members`,
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
);
|
||||
}
|
||||
|
||||
export async function updateWorkspaceMember(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
input: WorkspaceMemberUpdatePayload,
|
||||
): Promise<WorkspaceMember> {
|
||||
return apiRequest<WorkspaceMember>(
|
||||
`/api/v1/platform/workspaces/${workspaceId}/members/${userId}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteWorkspaceMember(
|
||||
workspaceId: string,
|
||||
userId: string,
|
||||
): Promise<{ user_id: string; deleted: boolean }> {
|
||||
return apiRequest(
|
||||
`/api/v1/platform/workspaces/${workspaceId}/members/${userId}`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,83 @@
|
||||
import {
|
||||
apiRequest,
|
||||
apiRequestWithMeta,
|
||||
buildCursorQuery,
|
||||
parseCursorPageMeta,
|
||||
type CursorListParams,
|
||||
type CursorPage,
|
||||
} from "./_shared";
|
||||
|
||||
// Workspace (Project) types - 对应 API.md 第七部分系统管理接口
|
||||
export type Workspace = {
|
||||
workspace_id: string;
|
||||
workspace_code: string;
|
||||
workspace_name: string;
|
||||
active_root_uri: string;
|
||||
quota_bytes: number;
|
||||
status: "active" | "archived" | "disabled";
|
||||
description: string | null;
|
||||
created_by: string;
|
||||
created_at: string;
|
||||
updated_at: string | null;
|
||||
};
|
||||
|
||||
export type WorkspaceCreatePayload = {
|
||||
workspace_code: string;
|
||||
workspace_name: string;
|
||||
quota_bytes?: number;
|
||||
description?: string;
|
||||
};
|
||||
|
||||
export type WorkspaceUpdatePayload = {
|
||||
workspace_name?: string;
|
||||
quota_bytes?: number;
|
||||
description?: string;
|
||||
status?: "active" | "archived";
|
||||
};
|
||||
|
||||
// Workspace (Project) Management APIs - 对应 API.md 第七部分系统管理接口
|
||||
// ----------------------------------------------------------------------------
|
||||
|
||||
export async function listWorkspaces(
|
||||
input: CursorListParams = {},
|
||||
): Promise<CursorPage<Workspace>> {
|
||||
const query = buildCursorQuery(input);
|
||||
const { data, meta } = await apiRequestWithMeta<Workspace[]>(
|
||||
`/api/v1/platform/workspaces?${query}`,
|
||||
);
|
||||
return { items: data, meta: parseCursorPageMeta(meta) };
|
||||
}
|
||||
|
||||
export async function createWorkspace(
|
||||
input: WorkspaceCreatePayload,
|
||||
): Promise<Workspace> {
|
||||
return apiRequest<Workspace>(
|
||||
"/api/v1/platform/workspaces",
|
||||
{ method: "POST", body: JSON.stringify(input) },
|
||||
);
|
||||
}
|
||||
|
||||
export async function getWorkspace(workspaceId: string): Promise<Workspace> {
|
||||
return apiRequest<Workspace>(`/api/v1/platform/workspaces/${workspaceId}`);
|
||||
}
|
||||
|
||||
export async function updateWorkspace(
|
||||
workspaceId: string,
|
||||
input: WorkspaceUpdatePayload,
|
||||
): Promise<Workspace> {
|
||||
return apiRequest<Workspace>(
|
||||
`/api/v1/platform/workspaces/${workspaceId}`,
|
||||
{ method: "PATCH", body: JSON.stringify(input) },
|
||||
);
|
||||
}
|
||||
|
||||
export async function deleteWorkspace(workspaceId: string): Promise<{
|
||||
workspace_id: string;
|
||||
deleted: boolean;
|
||||
}> {
|
||||
return apiRequest(
|
||||
`/api/v1/platform/workspaces/${workspaceId}`,
|
||||
{ method: "DELETE" },
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user