feat: 完善模型平台相关功能

This commit is contained in:
Winnie
2026-07-31 19:10:37 +08:00
parent 86988bdaef
commit 49ee2c0a4a
24 changed files with 529 additions and 125 deletions
+7 -2
View File
@@ -4,9 +4,14 @@ RUN corepack enable && corepack prepare pnpm@10.15.1 --activate
COPY frontend/package.json frontend/pnpm-lock.yaml ./
RUN pnpm install --frozen-lockfile
COPY frontend/ ./
RUN pnpm build
RUN pnpm typecheck && pnpm build
FROM nginx:alpine
COPY ./default.conf /etc/nginx/conf.d/default.conf
COPY ./default.conf /etc/nginx/conf.d/default.conf.template
COPY ./scripts/nginx-entrypoint.sh /usr/local/bin/model-platform-entrypoint.sh
RUN sed -i 's/\r$//' /usr/local/bin/model-platform-entrypoint.sh \
&& chmod +x /usr/local/bin/model-platform-entrypoint.sh
COPY --from=frontend-build /app/build/client /usr/share/nginx/html
EXPOSE 80
ENTRYPOINT ["/usr/local/bin/model-platform-entrypoint.sh"]
CMD ["nginx", "-g", "daemon off;"]
+1 -1
View File
@@ -1,4 +1,4 @@
import { FormEvent, useEffect, useState } from "react";
import { type FormEvent, useEffect, useState } from "react";
import {
ApiRequestError,
@@ -1,6 +1,6 @@
import {
type ChangeEvent,
FormEvent,
type FormEvent,
type MouseEvent as ReactMouseEvent,
useEffect,
useMemo,
@@ -1,8 +1,8 @@
import {
DragEvent,
FormEvent,
MouseEvent as ReactMouseEvent,
PointerEvent as ReactPointerEvent,
type DragEvent,
type FormEvent,
type MouseEvent as ReactMouseEvent,
type PointerEvent as ReactPointerEvent,
useEffect,
useMemo,
useRef,
+46 -45
View File
@@ -87,6 +87,7 @@ export type ScriptItem = {
visibility: Visibility;
status: string;
relative_path: string;
jupyter_path: string;
content_hash: string;
size_bytes: number;
created_at: string;
@@ -195,16 +196,18 @@ function initialContent(scriptType: ScriptType): string {
{
cells: [
{
id: "intro",
cell_type: "markdown",
metadata: {},
source: ["# 新建模型实验\\n", "在这里开始数据探索与模型构建。"],
source: ["# 新建模型实验\n", "在这里开始数据探索与模型构建。"],
},
{
id: "main",
cell_type: "code",
execution_count: null,
metadata: {},
outputs: [],
source: ["print('Hello, Model Platform!')\\n"],
source: ["print('Hello, Model Platform!')\n"],
},
],
metadata: {
@@ -325,6 +328,7 @@ export type FileLockSession = {
export type ActiveEditSession = FileLockSession & {
script_id: string;
script_name: string;
jupyter_path: string;
lock_token: string;
ticket_expires_at?: string;
};
@@ -356,70 +360,67 @@ export type StableVersion = {
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");
}
const now = Date.now();
return {
...session,
edit_session_id: crypto.randomUUID().replaceAll("-", ""),
workspace_id: script.workspace_id,
storage_object_id: script.current_object_id,
user_id: demoContext.userId,
session_status: "active",
lease_seconds: 3600,
heartbeat_interval_seconds: 300,
expires_at: new Date(now + 3600_000).toISOString(),
runtime_id: script.workspace_id,
jupyter_session_id: "demo-session",
relative_path: script.relative_path,
lock_token: "demo-unlocked-session",
script_id: script.script_id,
script_name: script.script_name,
lock_token: session.lock_token,
jupyter_path: script.jupyter_path,
};
}
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 }),
},
);
return {
...session,
expires_at: new Date(Date.now() + 3600_000).toISOString(),
};
}
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 }),
},
);
return { ...session, session_status: "closed" };
}
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 function releaseFileLockOnUnload(_session: ActiveEditSession): void {
// The current Backend deliberately has no persisted file-lock API.
}
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,
}),
});
const result = await apiRequest<{ expires_at: number }>(
"/api/v1/auth/demo-session",
{
method: "POST",
},
);
const editorRoute = session.script_name.toLowerCase().endsWith(".ipynb")
? "notebooks"
: "edit";
const encodedPath = session.jupyter_path
.split("/")
.filter(Boolean)
.map(encodeURIComponent)
.join("/");
return {
edit_session_id: session.edit_session_id,
jupyter_url: `/jupyter/${encodeURIComponent(session.workspace_id)}/${editorRoute}/${encodedPath}`,
expires_at: new Date(result.expires_at * 1000).toISOString(),
};
}
export async function listScriptVersions(
+2 -2
View File
@@ -8,11 +8,11 @@ export default defineConfig({
port: 5173,
proxy: {
"/api": {
target: process.env.VITE_GATEWAY_URL || "http://127.0.0.1:8081",
target: process.env.VITE_GATEWAY_URL || "http://127.0.0.1:8890",
changeOrigin: true,
},
"/jupyter": {
target: process.env.VITE_GATEWAY_URL || "http://127.0.0.1:8081",
target: process.env.VITE_GATEWAY_URL || "http://127.0.0.1:8890",
changeOrigin: true,
ws: true,
},