refactor: api.ts

This commit is contained in:
tao.chen
2026-09-01 11:10:39 +08:00
parent 4bfd4ac4c2
commit 0deb2d205b
13 changed files with 2157 additions and 1964 deletions
File diff suppressed because it is too large Load Diff
+262
View File
@@ -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();
}
+348
View File
@@ -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 }>;
};
+55
View File
@@ -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,
);
}
// ----------------------------------------------------------------------------
+140
View File
@@ -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 代替 */
+289
View File
@@ -0,0 +1,289 @@
import {
apiRequest,
ApiRequestError,
createUuid,
type ApiErrorEnvelope,
} from "./_shared";
export type WorkspaceDirectory = {
path: string;
name: string;
parent_path: string;
owner_user_id: string;
has_children?: boolean;
};
export type ResourceItem = {
resource_id: string;
workspace_id: string;
storage_object_id: string;
owner_user_id: string;
owner_display_name?: string | null;
resource_name: string;
description: string | null;
visibility: "private" | "workspace" | "public";
status: string;
created_at: string;
updated_at: string;
file: {
file_name: string;
file_extension: string | null;
mime_type: string | null;
size_bytes: number;
content_hash: string | null;
object_status: string;
};
jupyter_accessible_path: string;
absolute_path: string;
};
export async function listResources(
workspaceId: string,
parentPath: string = "",
opts?: { visibility?: string; keyword?: string; ownerUserId?: string },
): Promise<ResourceItem[]> {
// Default (no ownerUserId) scopes to the requester's own object_key
// subtree; passing ownerUserId scopes to that owner's subtree so the tree
// can lazily fetch another member's data resources on group expand.
const parameters = new URLSearchParams();
if (parentPath) parameters.set("parent_path", parentPath);
if (opts?.visibility) parameters.set("visibility", opts.visibility);
if (opts?.keyword) parameters.set("keyword", opts.keyword);
if (opts?.ownerUserId) parameters.set("owner_user_id", opts.ownerUserId);
const query = parameters.toString();
// apiRequest<T> already unwraps the envelope's `data` field, so we
// request `ResourceItem[]` directly here (matching listScripts).
return apiRequest<ResourceItem[]>(
`/api/v1/data-resources${query ? `?${query}` : ""}`,
{},
workspaceId,
);
}
export async function deleteResource(
workspaceId: string,
resourceId: string,
): Promise<{ resource_id: string; status: string }> {
return apiRequest<{ resource_id: string; status: string }>(
`/api/v1/data-resources/${encodeURIComponent(resourceId)}`,
{ method: "DELETE" },
workspaceId,
);
}
/** 同源流式下载数据资源字节,供 Excel 等预览器使用。 */
export async function fetchResourceContentFile(
workspaceId: string,
resourceId: string,
fileName: string,
signal?: AbortSignal,
): Promise<File> {
const response = await fetch(
`/api/v1/data-resources/${encodeURIComponent(resourceId)}/content?workspace_id=${encodeURIComponent(workspaceId)}`,
{
credentials: "same-origin",
signal,
headers: {
"X-Request-ID": createUuid().replaceAll("-", ""),
},
},
);
if (response.status === 401 && typeof window !== "undefined") {
const here = window.location.pathname;
if (here !== "/login") {
window.location.assign("/login");
}
throw new ApiRequestError("未登录或登录已过期", 401);
}
if (!response.ok) {
let message = `请求失败(HTTP ${response.status}`;
try {
const payload = (await response.json()) as ApiErrorEnvelope;
const detailMessage =
typeof payload.detail === "string"
? payload.detail
: payload.detail?.message;
if (detailMessage) message = detailMessage;
} catch {
/* ignore non-JSON error bodies */
}
throw new ApiRequestError(message, response.status);
}
const blob = await response.blob();
return new File([blob], fileName, {
type: blob.type || "application/octet-stream",
});
}
export type ResourcePreviewPayload = {
kind: "table";
columns: string[];
rows: string[][];
row_count: number;
truncated: boolean;
delimiter: string;
};
/** 表格类数据资源抽样预览(csv / tsv)。 */
export async function fetchResourcePreview(
workspaceId: string,
resourceId: string,
input: { limit?: number } = {},
signal?: AbortSignal,
): Promise<ResourcePreviewPayload> {
const parameters = new URLSearchParams();
parameters.set("workspace_id", workspaceId);
if (input.limit != null) parameters.set("limit", String(input.limit));
const response = await fetch(
`/api/v1/data-resources/${encodeURIComponent(resourceId)}/preview?${parameters.toString()}`,
{
credentials: "same-origin",
signal,
headers: {
"X-Request-ID": createUuid().replaceAll("-", ""),
},
},
);
if (response.status === 401 && typeof window !== "undefined") {
const here = window.location.pathname;
if (here !== "/login") {
window.location.assign("/login");
}
throw new ApiRequestError("未登录或登录已过期", 401);
}
const payload = await response.json();
if (!response.ok) {
const error = payload as ApiErrorEnvelope;
const detailMessage =
typeof error.detail === "string" ? error.detail : error.detail?.message;
throw new ApiRequestError(
detailMessage ?? `请求失败(HTTP ${response.status}`,
response.status,
);
}
const data = (payload as { data: ResourcePreviewPayload }).data;
if (!data || data.kind !== "table" || !Array.isArray(data.columns)) {
throw new ApiRequestError("响应数据格式错误", response.status);
}
return data;
}
export async function createResourceUpload(
workspaceId: string,
body: {
file_name: string;
content_type: string;
expected_size_bytes: number;
expected_hash: string | null;
target_path?: string;
},
): Promise<{ upload_id: string; upload_path: string }> {
return apiRequest<{ upload_id: string; upload_path: string }>(
"/api/v1/data-resources/uploads",
{
method: "POST",
body: JSON.stringify(body),
headers: { "Idempotency-Key": createUuid().replaceAll("-", "") },
},
workspaceId,
);
}
export async function uploadResourceBytes(
workspaceId: string,
uploadId: string,
fileBytes: ArrayBuffer | Blob,
contentType: string,
): Promise<{ storage_object_id: string }> {
return apiRequest<{ storage_object_id: string }>(
`/api/v1/data-resources/uploads/${encodeURIComponent(uploadId)}`,
{
method: "PUT",
headers: { "Content-Type": contentType },
body: fileBytes,
},
workspaceId,
);
}
export async function bindResourceUpload(
workspaceId: string,
uploadId: string,
body: {
resource_name: string;
description: string;
visibility: "private" | "workspace" | "public";
},
): Promise<ResourceItem> {
return apiRequest<ResourceItem>(
`/api/v1/data-resources/uploads/${encodeURIComponent(uploadId)}/bind`,
{
method: "POST",
body: JSON.stringify(body),
},
workspaceId,
);
}
export async function listWorkspaceDirectories(
workspaceId: string,
parentPath: string = "",
ownerUserId?: string,
): Promise<WorkspaceDirectory[]> {
// Default (no ownerUserId) scopes to the requester's own subtree; passing
// ownerUserId scopes to that owner so the tree can lazily render their
// directory structure on expand. Directories are structural rows; file
// visibility is still enforced by the scripts/data-resources endpoints.
const parameters = new URLSearchParams();
if (parentPath) parameters.set("parent_path", parentPath);
if (ownerUserId) parameters.set("owner_user_id", ownerUserId);
const query = parameters.toString();
const data = await apiRequest<{ directories: WorkspaceDirectory[] }>(
`/api/v1/workspace-directories${query ? `?${query}` : ""}`,
{},
workspaceId,
);
return data.directories;
}
export async function createWorkspaceDirectory(
workspaceId: string,
directoryName: string,
parentPath = "",
): Promise<WorkspaceDirectory> {
return apiRequest<WorkspaceDirectory>(
"/api/v1/workspace-directories",
{
method: "POST",
body: JSON.stringify({
directory_name: directoryName,
parent_path: parentPath,
}),
},
workspaceId,
);
}
export async function deleteWorkspaceDirectory(
workspaceId: string,
path: string,
): Promise<{
path: string;
status: string;
deleted_scripts: number;
versions_preserved: boolean;
}> {
const parameters = new URLSearchParams({ path });
return apiRequest(
`/api/v1/workspace-directories?${parameters.toString()}`,
{ method: "DELETE" },
workspaceId,
);
}
+271
View File
@@ -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,
);
}
// ----------------------------------------------------------------------------
+197
View File
@@ -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,
);
}
+299
View File
@@ -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" },
);
}
+83
View File
@@ -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" },
);
}