第一次合并develop-fech

This commit is contained in:
Winnie
2026-08-03 18:27:53 +08:00
parent d7bd88335c
commit feae98cc24
4 changed files with 88 additions and 60 deletions
+72 -57
View File
@@ -453,76 +453,83 @@ export type StableVersion = {
created_at: string;
};
// Note: the file-lock and jupyter-ticket endpoints are not yet
// implemented in the backend (see the cookie+JWT auth refactor plan).
// They are retained here so the editor UI keeps its existing call
// sites, but they will return 404 until the backend ships the
// corresponding routes.
// The current backend authorizes Jupyter through the session cookie and
// deliberately has no persisted file-lock or access-ticket endpoints.
// Keep the editor's session-shaped UI contract locally while opening the
// existing authenticated Jupyter proxy directly.
export async function acquireFileLock(
workspaceId: string,
script: ScriptItem,
): Promise<ActiveEditSession> {
const session = await apiRequest<FileLockSession>(
`/api/v1/files/${script.current_object_id}/lock`,
{ method: "POST" },
workspaceId,
);
if (!session.lock_token) {
throw new Error("加锁成功响应缺少 lock_token");
}
const now = Date.now();
return {
...session,
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: "unlocked-session",
relative_path: script.relative_path,
lock_token: "unlocked-session",
script_id: script.script_id,
script_name: script.script_name,
jupyter_path: session.relative_path ?? script.jupyter_path,
lock_token: session.lock_token,
jupyter_path: script.jupyter_path,
};
}
export async function heartbeatFileLock(
workspaceId: string,
_workspaceId: string,
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 }),
},
workspaceId,
);
return {
...session,
expires_at: new Date(Date.now() + 3600_000).toISOString(),
};
}
export async function releaseFileLock(
workspaceId: string,
_workspaceId: string,
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 }),
},
workspaceId,
);
return { ...session, session_status: "closed" };
}
export function releaseFileLockOnUnload(
workspaceId: string,
session: ActiveEditSession,
_workspaceId: string,
_session: ActiveEditSession,
): void {
void fetch(
`/api/v1/file-locks/${session.edit_session_id}?workspace_id=${
encodeURIComponent(workspaceId)
}`,
{
method: "DELETE",
// No backend lock is created in compatibility mode.
}
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",
keepalive: true,
headers: { "Content-Type": "application/json" },
body: JSON.stringify({ lock_token: session.lock_token }),
},
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,
);
}
@@ -530,17 +537,25 @@ export async function createJupyterAccessTicket(
workspaceId: string,
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,
}),
},
workspaceId,
);
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 async function listScriptVersions(