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 ScriptType = "python" | "notebook"; export type Visibility = "private" | "workspace" | "public"; 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; }; // 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 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; }; 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"; }; /** 加入工作区;角色继承自用户的 platform_role,请求体不能带 role_code。 */ export type WorkspaceMemberAddPayload = { user_id: string; }; /** 仅可改成员状态;改角色请 PATCH /platform/employees/{user_id}。 */ export type WorkspaceMemberUpdatePayload = { member_status?: "active" | "disabled" | "locked"; }; 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 type WorkspaceDirectory = { path: string; name: string; parent_path: string; owner_user_id: string; has_children?: boolean; }; type ApiEnvelope = { request_id: string; data: T; meta: Record; }; 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; } } 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)}`; } async function apiRequest( path: string, init: RequestInit = {}, workspaceId?: string, ): Promise { 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 | 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, ); } return (payload as ApiEnvelope).data; } export async function listScripts( workspaceId: string, parentPath: string = "", ownerUserId?: string, ): Promise { // 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( `/api/v1/scripts${query ? `?${query}` : ""}`, {}, workspaceId, ); } export async function countScripts( workspaceId: string, ): Promise { // 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 { return apiRequest( "/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 { const parameters = new URLSearchParams({ file_name: file.name, parent_path: parentPath, visibility, }); return apiRequest( `/api/v1/scripts/upload?${parameters.toString()}`, { method: "POST", headers: { "Content-Type": "application/octet-stream" }, body: file, }, workspaceId, ); } 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, ); } 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 setScriptLock( workspaceId: string, scriptId: string, isLocked: boolean, ): Promise { return apiRequest( `/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 { return apiRequest( `/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 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, ); } 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; }; 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; }; // 本地浏览器级“编辑锁”——后端没有 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 { 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 { // 本地锁不存在过期概念;只是把 expires_at 推后让 UI 看着还活着。 // 该字段当前没有任何消费者,保留只是为了不破坏契约。 return session; } export async function releaseFileLock( _workspaceId: string, session: ActiveEditSession, ): Promise { return { ...session, session_status: "closed" }; } export function releaseFileLockOnUnload( _workspaceId: string, _session: ActiveEditSession, ): void { // 本地锁随 tab 生命周期结束。beforeunload 调到这里只是让 store 端 // 清理模块级引用,避免下一个 tab 复用时看到陈旧 _editSession。 } async function waitForJupyterReady(jupyterUrl: string): Promise { 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 { 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(), }; } export type LatestVersion = { versions_id: string; version_label: string; }; export async function getLatestScriptVersion( workspaceId: string, scriptId: string, ): Promise { return apiRequest( `/api/v1/scripts/${scriptId}/latest-version`, {}, workspaceId, ); } export async function publishScriptVersion( workspaceId: string, input: { script: ScriptItem; releaseNote: string; visibility: Visibility; }, ): Promise { return apiRequest( `/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, ); } 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 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; env_refs_json: Record; 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 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 ScheduleNodeRunStatus = | ScheduleRunStatus | "skipped"; 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 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 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 { return apiRequest("/api/v1/schedules", {}, workspaceId); } export async function getSchedule( workspaceId: string, scheduleId: string, ): Promise { return apiRequest( `/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 { return apiRequest( "/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 { return apiRequest( `/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 { return apiRequest( "/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, ); } // 系统管理级别接口 - 不区分 workspace export async function listPlatformEmployees(): Promise { return apiRequest("/api/v1/platform/employees"); } export async function createPlatformEmployee( input: { username: string; display_name: string; email?: string | undefined; password: string; role_code?: "admin" | "developer"; }, ): Promise { return apiRequest( "/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 { return apiRequest( `/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" }, ); } // 兼容旧的 workspace 级别接口(已废弃,建议使用 system-level 接口) /** @deprecated 使用 listPlatformEmployees 代替 */ export async function listEmployees(workspaceId: string): Promise { return apiRequest("/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 { return apiRequest( "/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 { return apiRequest( `/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, ); } // ---------------------------------------------------------------------------- // Workspace (Project) Management APIs - 对应 API.md 第七部分系统管理接口 // ---------------------------------------------------------------------------- export async function listWorkspaces(): Promise { return apiRequest("/api/v1/platform/workspaces"); } export async function createWorkspace( input: WorkspaceCreatePayload, ): Promise { return apiRequest( "/api/v1/platform/workspaces", { method: "POST", body: JSON.stringify(input) }, ); } export async function getWorkspace(workspaceId: string): Promise { return apiRequest(`/api/v1/platform/workspaces/${workspaceId}`); } export async function updateWorkspace( workspaceId: string, input: WorkspaceUpdatePayload, ): Promise { return apiRequest( `/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" }, ); } export async function listWorkspaceMembers(workspaceId: string): Promise { return apiRequest( `/api/v1/platform/workspaces/${workspaceId}/members`, ); } export async function addWorkspaceMember( workspaceId: string, input: WorkspaceMemberAddPayload, ): Promise { return apiRequest( `/api/v1/platform/workspaces/${workspaceId}/members`, { method: "POST", body: JSON.stringify(input) }, ); } export async function updateWorkspaceMember( workspaceId: string, userId: string, input: WorkspaceMemberUpdatePayload, ): Promise { return apiRequest( `/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" }, ); } 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; env_refs_json?: Record; python_version?: PythonVersion; }, ): Promise { return apiRequest( `/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; env_refs_json?: Record; python_version?: PythonVersion; }, ): Promise { return apiRequest( `/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 { return apiRequest( `/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 { return apiRequest( `/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 { return apiRequest( `/api/v1/schedules/${scheduleId}/edges/${edgeId}`, { method: "DELETE", body: JSON.stringify({ workflow_version: workflowVersion }) }, workspaceId, ); } export async function validateSchedule( workspaceId: string, scheduleId: string, ): Promise { 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 { return apiRequest( "/api/v1/cron/preview", { method: "POST", body: JSON.stringify(input) }, workspaceId, ); } export async function runScheduleNow( workspaceId: string, scheduleId: string, ): Promise { return apiRequest( `/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 { 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( `/api/v1/schedule-runs?${query.toString()}`, {}, workspaceId, ); } export async function getScheduleRun( workspaceId: string, runId: string, ): Promise { return apiRequest( `/api/v1/schedule-runs/${runId}`, {}, workspaceId, ); } export async function getScheduleNodeRunArtifacts( workspaceId: string, runId: string, nodeRunId: string, ): Promise { return apiRequest( `/api/v1/schedule-runs/${runId}/node-runs/${nodeRunId}/artifacts`, {}, workspaceId, ); } // ---------------------------------------------------------------------------- // 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[1], ownerUserId?: Parameters[2], ) => Promise; countScripts: () => Promise; listResources: ( parentPath?: Parameters[1], opts?: Parameters[2], ) => Promise; createScript: ( input: Parameters[1], ) => Promise; uploadScript: ( file: File, parentPath?: string, visibility?: Visibility, ) => Promise; createResourceUpload: ( body: Parameters[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[2], ) => Promise; deleteResource: ( resourceId: string, ) => Promise<{ resource_id: string; status: string }>; updateScript: ( scriptId: string, input: Parameters[2], ) => Promise; deleteScript: ( scriptId: string, ) => Promise<{ script_id: string; status: string; versions_preserved: boolean }>; setScriptLock: ( scriptId: string, isLocked: boolean, ) => Promise; listWorkspaceDirectories: ( parentPath?: Parameters[1], ownerUserId?: Parameters[2], ) => Promise; createWorkspaceDirectory: ( directoryName: string, parentPath?: string, ) => Promise; deleteWorkspaceDirectory: ( path: string, ) => Promise<{ path: string; status: string; deleted_scripts: number; versions_preserved: boolean; }>; acquireFileLock: ( script: ScriptItem, ) => Promise; heartbeatFileLock: ( session: ActiveEditSession, ) => Promise; releaseFileLock: ( session: ActiveEditSession, ) => Promise; releaseFileLockOnUnload: (session: ActiveEditSession) => void; createJupyterAccessTicket: ( session: ActiveEditSession, ) => Promise; getLatestScriptVersion: (scriptId: string) => Promise; publishScriptVersion: ( input: Parameters[1], ) => Promise; listSchedules: () => Promise; getSchedule: (scheduleId: string) => Promise; createSchedule: ( input: Parameters[1], ) => Promise; updateSchedule: ( scheduleId: string, input: Parameters[2], ) => Promise; deleteSchedule: ( scheduleId: string, workflowVersion: number, ) => Promise<{ schedule_id: string; deleted: boolean; workflow_version: number }>; listScheduleArtifacts: () => Promise; hideScheduleArtifact: ( versionsId: string, ) => Promise<{ versions_id: string; deleted: boolean; artifact_preserved: boolean; }>; listEmployees: () => Promise; listPlatformEmployees: () => Promise; createEmployee: ( input: Parameters[1], ) => Promise; createPlatformEmployee: ( input: { username: string; display_name: string; email?: string | undefined; password: string; role_code?: "admin" | "developer"; }, ) => Promise; updateEmployee: ( userId: string, input: Parameters[2], ) => Promise; updatePlatformEmployee: ( userId: string, input: { display_name?: string; email?: string | null; role_code?: "admin" | "developer"; status?: "active" | "disabled" | "locked"; }, ) => Promise; deleteEmployee: ( userId: string, ) => Promise<{ user_id: string; deleted: boolean }>; deletePlatformEmployee: ( userId: string, ) => Promise<{ user_id: string; deleted: boolean }>; createScheduleNode: ( scheduleId: string, input: Parameters[2], ) => Promise; updateScheduleNode: ( scheduleId: string, nodeId: string, input: Parameters[3], ) => Promise; deleteScheduleNode: ( scheduleId: string, nodeId: string, workflowVersion: number, options?: { delete_execution_history?: boolean }, ) => Promise; createScheduleEdge: ( scheduleId: string, input: Parameters[2], ) => Promise; deleteScheduleEdge: ( scheduleId: string, edgeId: string, workflowVersion: number, ) => Promise; validateSchedule: ( scheduleId: string, ) => Promise; previewCron: ( input: Parameters[1], ) => Promise; runScheduleNow: (scheduleId: string) => Promise; listScheduleRuns: ( input?: Parameters[1], ) => Promise; getScheduleRun: (runId: string) => Promise; getScheduleNodeRunArtifacts: ( runId: string, nodeRunId: string, ) => Promise; // Workspace (Project) Management - 系统管理接口 listWorkspaces: () => Promise; createWorkspace: (input: WorkspaceCreatePayload) => Promise; updateWorkspace: ( workspaceId: string, input: WorkspaceUpdatePayload, ) => Promise; deleteWorkspace: (workspaceId: string) => Promise<{ workspace_id: string; deleted: boolean }>; listWorkspaceMembers: (workspaceId: string) => Promise; addWorkspaceMember: ( workspaceId: string, input: WorkspaceMemberAddPayload, ) => Promise; updateWorkspaceMember: ( workspaceId: string, userId: string, input: WorkspaceMemberUpdatePayload, ) => Promise; deleteWorkspaceMember: ( workspaceId: string, userId: string, ) => Promise<{ user_id: string; deleted: boolean }>; };