1690 lines
46 KiB
TypeScript
1690 lines
46 KiB
TypeScript
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<T> = {
|
||
request_id: string;
|
||
data: T;
|
||
meta: Record<string, unknown>;
|
||
};
|
||
|
||
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<T>(
|
||
path: string,
|
||
init: RequestInit = {},
|
||
workspaceId?: string,
|
||
): Promise<T> {
|
||
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,
|
||
);
|
||
}
|
||
return (payload as ApiEnvelope<T>).data;
|
||
}
|
||
|
||
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 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,
|
||
);
|
||
}
|
||
|
||
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 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 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,
|
||
);
|
||
}
|
||
|
||
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<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(),
|
||
};
|
||
}
|
||
|
||
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,
|
||
);
|
||
}
|
||
|
||
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<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 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<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,
|
||
);
|
||
}
|
||
|
||
// 系统管理级别接口 - 不区分 workspace
|
||
export async function listPlatformEmployees(): Promise<Employee[]> {
|
||
return apiRequest<Employee[]>("/api/v1/platform/employees");
|
||
}
|
||
|
||
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" },
|
||
);
|
||
}
|
||
|
||
// 兼容旧的 workspace 级别接口(已废弃,建议使用 system-level 接口)
|
||
/** @deprecated 使用 listPlatformEmployees 代替 */
|
||
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,
|
||
);
|
||
}
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// Workspace (Project) Management APIs - 对应 API.md 第七部分系统管理接口
|
||
// ----------------------------------------------------------------------------
|
||
|
||
export async function listWorkspaces(): Promise<Workspace[]> {
|
||
return apiRequest<Workspace[]>("/api/v1/platform/workspaces");
|
||
}
|
||
|
||
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" },
|
||
);
|
||
}
|
||
|
||
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" },
|
||
);
|
||
}
|
||
|
||
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,
|
||
);
|
||
}
|
||
|
||
// ----------------------------------------------------------------------------
|
||
// 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 }>;
|
||
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: () => Promise<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 }>;
|
||
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: () => Promise<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 }>;
|
||
};
|