重构模型平台前后端并移除Redis依赖

This commit is contained in:
Winnie
2026-07-30 19:03:00 +08:00
commit c74b2abb48
172 changed files with 40825 additions and 0 deletions
+853
View File
@@ -0,0 +1,853 @@
export type DemoUser = {
userId: string;
userName: string;
username: string;
roleCode: "admin" | "developer";
roleName: string;
};
export type DemoWorkspace = {
workspaceId: string;
workspaceName: string;
};
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,
}));
}
}
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;
};
export type ScriptItem = {
script_id: string;
workspace_id: string;
current_object_id: string;
owner_user_id: string;
script_name: string;
script_type: ScriptType;
visibility: Visibility;
status: string;
relative_path: string;
content_hash: string;
size_bytes: number;
created_at: string;
updated_at: string;
};
export type WorkspaceDirectory = {
path: string;
name: string;
parent_path: string;
};
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;
}
}
async function apiRequest<T>(
path: string,
init: RequestInit = {},
): Promise<T> {
const response = await fetch(path, {
...init,
credentials: "same-origin",
headers: {
"X-User-ID": demoContext.userId,
"X-Workspace-ID": demoContext.workspaceId,
"X-Request-ID": crypto.randomUUID().replaceAll("-", ""),
...(init.body ? { "Content-Type": "application/json" } : {}),
...init.headers,
},
});
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(): Promise<ScriptItem[]> {
return apiRequest<ScriptItem[]>("/api/v1/scripts");
}
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: [
{
cell_type: "markdown",
metadata: {},
source: ["# 新建模型实验\\n", "在这里开始数据探索与模型构建。"],
},
{
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(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,
}),
});
}
export async function uploadScript(
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,
},
);
}
export async function deleteScript(
scriptId: string,
): Promise<{ script_id: string; status: string; versions_preserved: boolean }> {
return apiRequest(`/api/v1/scripts/${scriptId}`, { method: "DELETE" });
}
export async function listWorkspaceDirectories(): Promise<
WorkspaceDirectory[]
> {
const data = await apiRequest<{ directories: WorkspaceDirectory[] }>(
"/api/v1/workspace-tree",
);
return data.directories;
}
export async function createWorkspaceDirectory(
directoryName: string,
parentPath = "",
): Promise<WorkspaceDirectory> {
return apiRequest<WorkspaceDirectory>("/api/v1/workspace-directories", {
method: "POST",
body: JSON.stringify({
directory_name: directoryName,
parent_path: parentPath,
}),
});
}
export async function deleteWorkspaceDirectory(
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",
});
}
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;
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;
};
export async function acquireFileLock(
script: ScriptItem,
): Promise<ActiveEditSession> {
const session = await apiRequest<FileLockSession>(
`/api/v1/files/${script.current_object_id}/lock`,
{ method: "POST" },
);
if (!session.lock_token) {
throw new Error("加锁成功响应缺少 lock_token");
}
return {
...session,
script_id: script.script_id,
script_name: script.script_name,
lock_token: session.lock_token,
};
}
export async function heartbeatFileLock(
session: ActiveEditSession,
): Promise<FileLockSession> {
return apiRequest<FileLockSession>(
`/api/v1/file-locks/${session.edit_session_id}/heartbeat`,
{
method: "POST",
body: JSON.stringify({ lock_token: session.lock_token }),
},
);
}
export async function releaseFileLock(
session: ActiveEditSession,
): Promise<FileLockSession> {
return apiRequest<FileLockSession>(
`/api/v1/file-locks/${session.edit_session_id}`,
{
method: "DELETE",
body: JSON.stringify({ lock_token: session.lock_token }),
},
);
}
export function releaseFileLockOnUnload(session: ActiveEditSession): void {
void fetch(`/api/v1/file-locks/${session.edit_session_id}`, {
method: "DELETE",
credentials: "same-origin",
keepalive: true,
headers: {
"Content-Type": "application/json",
"X-User-ID": demoContext.userId,
"X-Workspace-ID": demoContext.workspaceId,
"X-Request-ID": crypto.randomUUID().replaceAll("-", ""),
},
body: JSON.stringify({ lock_token: session.lock_token }),
});
}
export async function createJupyterAccessTicket(
session: ActiveEditSession,
): Promise<JupyterAccessTicket> {
return apiRequest<JupyterAccessTicket>("/api/v1/jupyter/access-tickets", {
method: "POST",
body: JSON.stringify({
edit_session_id: session.edit_session_id,
lock_token: session.lock_token,
}),
});
}
export async function listScriptVersions(
scriptId: string,
): Promise<StableVersion[]> {
return apiRequest<StableVersion[]>(`/api/v1/scripts/${scriptId}/versions`);
}
export async function publishScriptVersion(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,
}),
},
);
}
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 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>;
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 async function listSchedules(): Promise<Schedule[]> {
return apiRequest<Schedule[]>("/api/v1/schedules");
}
export async function getSchedule(scheduleId: string): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}`);
}
export async function createSchedule(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),
});
}
export async function updateSchedule(
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),
});
}
export async function deleteSchedule(
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 }),
});
}
export async function listScheduleArtifacts(): Promise<ScheduleArtifact[]> {
return apiRequest<ScheduleArtifact[]>("/api/v1/schedule-artifacts");
}
export async function hideScheduleArtifact(
versionsId: string,
): Promise<{
versions_id: string;
deleted: boolean;
artifact_preserved: boolean;
}> {
return apiRequest(`/api/v1/versions/${versionsId}`, {
method: "DELETE",
});
}
export async function listEmployees(): Promise<Employee[]> {
return apiRequest<Employee[]>("/api/v1/admin/employees");
}
export async function createEmployee(input: {
username: string;
display_name: string;
email?: string | null;
role_code: "admin" | "developer";
}): Promise<Employee> {
return apiRequest<Employee>("/api/v1/admin/employees", {
method: "POST",
body: JSON.stringify(input),
});
}
export async function updateEmployee(
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),
});
}
export async function deleteEmployee(
userId: string,
): Promise<{ user_id: string; deleted: boolean }> {
return apiRequest(`/api/v1/admin/employees/${userId}`, {
method: "DELETE",
});
}
export async function createScheduleNode(
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>;
},
): Promise<Schedule> {
return apiRequest<Schedule>(`/api/v1/schedules/${scheduleId}/nodes`, {
method: "POST",
body: JSON.stringify(input),
});
}
export async function updateScheduleNode(
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>;
},
): Promise<Schedule> {
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/nodes/${nodeId}`,
{
method: "PUT",
body: JSON.stringify(input),
},
);
}
export async function deleteScheduleNode(
scheduleId: string,
nodeId: string,
workflowVersion: number,
): Promise<Schedule> {
return apiRequest<Schedule>(
`/api/v1/schedules/${scheduleId}/nodes/${nodeId}`,
{
method: "DELETE",
body: JSON.stringify({ workflow_version: workflowVersion }),
},
);
}
export async function createScheduleEdge(
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),
});
}
export async function deleteScheduleEdge(
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 }),
},
);
}
export async function validateSchedule(
scheduleId: string,
): Promise<DagValidation & {
schedule_id: string;
workflow_version: number;
}> {
return apiRequest(`/api/v1/schedules/${scheduleId}/validate`, {
method: "POST",
});
}
export async function previewCron(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),
});
}
export async function runScheduleNow(
scheduleId: string,
): Promise<ScheduleRunDetail> {
return apiRequest<ScheduleRunDetail>(
`/api/v1/schedules/${scheduleId}/run`,
{
method: "POST",
headers: {
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({ reason: "manual_run" }),
},
);
}
export async function listScheduleRuns(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()}`,
);
}
export async function getScheduleRun(
runId: string,
): Promise<ScheduleRunDetail> {
return apiRequest<ScheduleRunDetail>(`/api/v1/schedule-runs/${runId}`);
}