Improve Jupyter workspace bootstrap

This commit is contained in:
wuyang6
2026-05-12 22:33:15 +08:00
parent 295d5d1ea1
commit 86f49134a4
5 changed files with 858 additions and 44 deletions
+163 -19
View File
@@ -8,6 +8,7 @@ import {
} from "@/lib/claw-auth";
export const runtime = "nodejs";
const CLAW_API_URL = process.env.CLAW_API_URL ?? "http://127.0.0.1:8765";
export async function GET(req: Request) {
const account = await getCurrentAccount();
@@ -19,15 +20,26 @@ export async function GET(req: Request) {
const sessionId = normalizeSessionId(url.searchParams.get("session_id"));
const onlineDocs = await readOnlineDocMap(account.id);
if (!requestedPath && sessionId) {
const remoteFiles = await listRemoteSessionFiles(
account.id,
sessionId,
onlineDocs,
);
return Response.json({
session_id: sessionId,
input: await listSessionFiles(account.id, sessionId, "input", onlineDocs),
output: await listSessionFiles(
account.id,
sessionId,
"output",
onlineDocs,
),
input: [
...(await listSessionFiles(account.id, sessionId, "input", onlineDocs)),
...remoteFiles.input,
],
output: [
...(await listSessionFiles(
account.id,
sessionId,
"output",
onlineDocs,
)),
...remoteFiles.output,
],
});
}
if (!requestedPath) {
@@ -35,23 +47,38 @@ export async function GET(req: Request) {
if (!latestSessionId) {
return Response.json({ session_id: null, input: [], output: [] });
}
const remoteFiles = await listRemoteSessionFiles(
account.id,
latestSessionId,
onlineDocs,
);
return Response.json({
session_id: latestSessionId,
input: await listSessionFiles(
account.id,
latestSessionId,
"input",
onlineDocs,
),
output: await listSessionFiles(
account.id,
latestSessionId,
"output",
onlineDocs,
),
input: [
...(await listSessionFiles(
account.id,
latestSessionId,
"input",
onlineDocs,
)),
...remoteFiles.input,
],
output: [
...(await listSessionFiles(
account.id,
latestSessionId,
"output",
onlineDocs,
)),
...remoteFiles.output,
],
});
}
if (requestedPath.startsWith("jupyter://")) {
return downloadRemoteFile(account.id, requestedPath);
}
const accountRoot = path.resolve(accountBaseRoot(account.id));
const filePath = path.resolve(requestedPath);
if (!filePath.startsWith(`${accountRoot}${path.sep}`)) {
@@ -75,6 +102,123 @@ export async function GET(req: Request) {
}
}
async function listRemoteSessionFiles(
accountId: string,
sessionId: string,
onlineDocs: Record<string, OnlineDocRecord>,
) {
try {
const target = new URL(`${CLAW_API_URL}/api/jupyter/files`);
target.searchParams.set("account_id", accountId);
target.searchParams.set("session_id", sessionId);
const response = await fetch(target, { cache: "no-store" });
if (!response.ok) return { input: [], output: [] };
const payload = (await response.json()) as {
input?: RemoteSessionFile[];
output?: RemoteSessionFile[];
};
return {
input: mapRemoteFiles(payload.input, sessionId, "input", onlineDocs),
output: mapRemoteFiles(payload.output, sessionId, "output", onlineDocs),
};
} catch {
return { input: [], output: [] };
}
}
type RemoteSessionFile = {
name?: unknown;
path?: unknown;
kind?: unknown;
size?: unknown;
modified_at?: unknown;
};
function mapRemoteFiles(
files: RemoteSessionFile[] | undefined,
sessionId: string,
kind: "input" | "output",
onlineDocs: Record<string, OnlineDocRecord>,
) {
if (!Array.isArray(files)) return [];
return files
.map((file) => {
const remotePath =
typeof file.path === "string" && file.path.startsWith("/")
? file.path
: null;
if (!remotePath) return null;
const name =
typeof file.name === "string" && file.name.trim()
? file.name.trim()
: path.basename(remotePath);
const uri = toJupyterFileUri(sessionId, remotePath);
const onlineDoc = onlineDocs[uri];
return {
name,
path: uri,
kind,
source: "jupyter",
size: typeof file.size === "number" ? file.size : 0,
modified_at:
typeof file.modified_at === "string"
? file.modified_at
: new Date().toISOString(),
download_url: `/api/claw/files?path=${encodeURIComponent(uri)}`,
online_doc_url: cleanOnlineDocUrl(onlineDoc?.url) ?? null,
online_doc_title: onlineDoc?.title ?? null,
online_doc_kind: onlineDoc?.kind ?? null,
online_doc_updated_at: onlineDoc?.updated_at ?? null,
};
})
.filter((file): file is NonNullable<typeof file> => Boolean(file));
}
async function downloadRemoteFile(accountId: string, uri: string) {
const parsed = parseJupyterFileUri(uri);
if (!parsed) {
return Response.json({ error: "无效的远端文件路径" }, { status: 400 });
}
const target = new URL(`${CLAW_API_URL}/api/jupyter/file`);
target.searchParams.set("account_id", accountId);
target.searchParams.set("session_id", parsed.sessionId);
target.searchParams.set("path", parsed.remotePath);
const response = await fetch(target, { cache: "no-store" });
if (!response.ok) {
const payload = await response.text();
return new Response(payload, {
status: response.status,
headers: {
"content-type":
response.headers.get("content-type") ?? "application/json",
},
});
}
return new Response(response.body, {
status: response.status,
headers: {
"content-type":
response.headers.get("content-type") ?? "application/octet-stream",
"content-disposition":
response.headers.get("content-disposition") ??
`attachment; filename="${encodeURIComponent(path.basename(parsed.remotePath))}"`,
},
});
}
function toJupyterFileUri(sessionId: string, remotePath: string) {
return `jupyter://${sessionId}${remotePath}`;
}
function parseJupyterFileUri(uri: string) {
const match = uri.match(/^jupyter:\/\/([^/]+)(\/.*)$/u);
if (!match) return null;
return {
sessionId: match[1],
remotePath: match[2],
};
}
async function listSessionFiles(
accountId: string,
sessionId: string,