2026-04-26 04:29:23 +08:00
|
|
|
import { NextResponse } from "next/server";
|
|
|
|
|
import { isConvexEnabled } from "@/lib/convex/enabled";
|
2026-04-23 07:38:34 +08:00
|
|
|
import { getAuthedConvexClient } from "@/lib/convex/route";
|
2026-04-22 05:57:06 +08:00
|
|
|
import {
|
2026-04-26 04:29:23 +08:00
|
|
|
buildDocumentBridgeContextWithActor,
|
2026-04-23 07:38:34 +08:00
|
|
|
buildDocumentQueryEnvelope,
|
|
|
|
|
} from "@/lib/documents/bridge";
|
|
|
|
|
import {
|
|
|
|
|
executeRustBridgeQueryTransport,
|
|
|
|
|
resolveRustBridgeQueryPlan,
|
|
|
|
|
} from "@/lib/documents/rust-runtime";
|
2026-04-26 04:29:23 +08:00
|
|
|
import type { SidebarDatasetListQueryResult } from "@/lib/sidebar-data";
|
|
|
|
|
import { attachKernelFileTreeProjection, resolveKernelFileTreeProjection } from "@/lib/server/kernel-file-tree";
|
2026-04-24 06:10:18 +08:00
|
|
|
import {
|
2026-04-26 04:29:23 +08:00
|
|
|
streamTreeFrames,
|
|
|
|
|
type TreeStreamOverview,
|
|
|
|
|
type TreeStreamSnapshotPayload,
|
|
|
|
|
} from "@/lib/tree-stream/server";
|
2026-04-22 05:57:06 +08:00
|
|
|
|
|
|
|
|
export const dynamic = "force-dynamic";
|
2026-04-26 04:29:23 +08:00
|
|
|
export const runtime = "nodejs";
|
2026-04-22 05:57:06 +08:00
|
|
|
|
2026-04-26 04:29:23 +08:00
|
|
|
function readNumberParam(url: URL, name: string): number | null {
|
|
|
|
|
const raw = url.searchParams.get(name);
|
|
|
|
|
if (!raw?.trim()) {
|
|
|
|
|
return null;
|
|
|
|
|
}
|
|
|
|
|
const parsed = Number(raw);
|
|
|
|
|
return Number.isFinite(parsed) ? parsed : null;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
function encodeSseFrame(event: string, payload: unknown) {
|
|
|
|
|
return `event: ${event}\ndata: ${JSON.stringify(payload)}\n\n`;
|
2026-04-23 07:38:34 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-22 05:57:06 +08:00
|
|
|
export async function GET(request: Request) {
|
2026-04-26 04:29:23 +08:00
|
|
|
if (!isConvexEnabled()) {
|
|
|
|
|
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
|
|
|
|
|
}
|
2026-04-22 05:57:06 +08:00
|
|
|
|
2026-04-26 04:29:23 +08:00
|
|
|
const requestUrl = new URL(request.url);
|
|
|
|
|
const workspaceId = requestUrl.searchParams.get("workspaceId")?.trim();
|
|
|
|
|
if (!workspaceId) {
|
|
|
|
|
return NextResponse.json({ error: "缺少 workspaceId" }, { status: 400 });
|
|
|
|
|
}
|
2026-04-22 05:57:06 +08:00
|
|
|
|
2026-04-26 04:29:23 +08:00
|
|
|
const { auth, client } = await getAuthedConvexClient();
|
|
|
|
|
const actor = {
|
|
|
|
|
actorType: "user",
|
|
|
|
|
actorId: auth.userId,
|
|
|
|
|
sessionId: null,
|
|
|
|
|
};
|
|
|
|
|
const context = buildDocumentBridgeContextWithActor({
|
|
|
|
|
request,
|
|
|
|
|
actor,
|
|
|
|
|
workspaceId,
|
|
|
|
|
source: {
|
|
|
|
|
channel: "next_mnote_web_stream",
|
|
|
|
|
client: "wolai-frontend",
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-04-22 05:57:06 +08:00
|
|
|
|
2026-04-26 04:29:23 +08:00
|
|
|
const loadOverview = async (): Promise<TreeStreamOverview> => {
|
|
|
|
|
const envelope = buildDocumentQueryEnvelope({
|
2026-04-23 07:38:34 +08:00
|
|
|
name: "bridge.workspace.overview",
|
|
|
|
|
payload: {
|
|
|
|
|
workspaceId,
|
2026-04-26 04:29:23 +08:00
|
|
|
limit: 50,
|
|
|
|
|
cursor: null,
|
2026-04-23 07:38:34 +08:00
|
|
|
commandStatus: null,
|
|
|
|
|
eventStatus: null,
|
|
|
|
|
targetPageId: null,
|
|
|
|
|
targetBlockId: null,
|
|
|
|
|
aggregateType: null,
|
|
|
|
|
aggregateId: null,
|
|
|
|
|
},
|
|
|
|
|
});
|
2026-04-26 04:29:23 +08:00
|
|
|
const plan = await resolveRustBridgeQueryPlan({
|
2026-04-23 07:38:34 +08:00
|
|
|
context,
|
2026-04-26 04:29:23 +08:00
|
|
|
envelope,
|
2026-04-23 07:38:34 +08:00
|
|
|
});
|
2026-04-26 04:29:23 +08:00
|
|
|
return executeRustBridgeQueryTransport<TreeStreamOverview>({
|
2026-04-23 07:38:34 +08:00
|
|
|
client,
|
2026-04-26 04:29:23 +08:00
|
|
|
plan,
|
2026-04-23 07:38:34 +08:00
|
|
|
});
|
2026-04-26 04:29:23 +08:00
|
|
|
};
|
2026-04-23 07:38:34 +08:00
|
|
|
|
2026-04-26 04:29:23 +08:00
|
|
|
const loadSnapshot = async (): Promise<TreeStreamSnapshotPayload> => {
|
|
|
|
|
const envelope = buildDocumentQueryEnvelope({
|
|
|
|
|
name: "sidebar.dataset.list",
|
|
|
|
|
payload: {
|
|
|
|
|
workspaceId,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const plan = await resolveRustBridgeQueryPlan({
|
|
|
|
|
context,
|
|
|
|
|
envelope,
|
|
|
|
|
});
|
|
|
|
|
const dataset = await executeRustBridgeQueryTransport<SidebarDatasetListQueryResult>({
|
|
|
|
|
client,
|
|
|
|
|
plan,
|
|
|
|
|
});
|
|
|
|
|
const datasetWithFileTree = attachKernelFileTreeProjection({
|
|
|
|
|
dataset,
|
|
|
|
|
projection: await resolveKernelFileTreeProjection({
|
|
|
|
|
client,
|
|
|
|
|
request,
|
|
|
|
|
workspaceId,
|
|
|
|
|
actor,
|
|
|
|
|
dataset,
|
|
|
|
|
rootNodeId: requestUrl.searchParams.get("rootNodeId")?.trim() || null,
|
|
|
|
|
depth: readNumberParam(requestUrl, "depth"),
|
|
|
|
|
}),
|
|
|
|
|
});
|
|
|
|
|
return {
|
2026-04-23 07:38:34 +08:00
|
|
|
requestId: context.requestId,
|
|
|
|
|
traceId: context.traceId,
|
2026-04-26 04:29:23 +08:00
|
|
|
data: datasetWithFileTree,
|
2026-04-23 07:38:34 +08:00
|
|
|
snapshot: {
|
2026-04-26 04:29:23 +08:00
|
|
|
dataset: datasetWithFileTree,
|
2026-04-23 07:38:34 +08:00
|
|
|
},
|
|
|
|
|
};
|
2026-04-26 04:29:23 +08:00
|
|
|
};
|
2026-04-23 07:38:34 +08:00
|
|
|
|
2026-04-26 04:29:23 +08:00
|
|
|
const encoder = new TextEncoder();
|
|
|
|
|
const stream = new ReadableStream<Uint8Array>({
|
|
|
|
|
async start(controller) {
|
|
|
|
|
try {
|
|
|
|
|
for await (const frame of streamTreeFrames({
|
|
|
|
|
workspaceId,
|
|
|
|
|
rootNodeId: requestUrl.searchParams.get("rootNodeId"),
|
|
|
|
|
initialCursor: requestUrl.searchParams.get("cursor"),
|
|
|
|
|
pollMs: readNumberParam(requestUrl, "pollMs") ?? undefined,
|
|
|
|
|
maxPolls: readNumberParam(requestUrl, "maxPolls"),
|
|
|
|
|
loadOverview,
|
|
|
|
|
loadSnapshot,
|
|
|
|
|
})) {
|
|
|
|
|
if (request.signal.aborted) {
|
|
|
|
|
break;
|
|
|
|
|
}
|
|
|
|
|
controller.enqueue(encoder.encode(encodeSseFrame(frame.event, frame.payload)));
|
|
|
|
|
}
|
|
|
|
|
controller.close();
|
|
|
|
|
} catch (error) {
|
|
|
|
|
controller.error(error);
|
|
|
|
|
}
|
|
|
|
|
},
|
|
|
|
|
cancel() {
|
|
|
|
|
return undefined;
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return new NextResponse(stream, {
|
|
|
|
|
status: 200,
|
|
|
|
|
headers: {
|
|
|
|
|
"content-type": "text/event-stream; charset=utf-8",
|
|
|
|
|
"cache-control": "no-store",
|
|
|
|
|
connection: "keep-alive",
|
2026-04-29 12:24:44 +08:00
|
|
|
"x-upstream": "next-tree-stream-compat",
|
|
|
|
|
"x-mnote-web-owner": "mnote-web",
|
|
|
|
|
"x-mnote-tree-stream-owner": "rust-web",
|
|
|
|
|
"x-mnote-compat-boundary": "mnote-web-stream-alias",
|
2026-04-26 04:29:23 +08:00
|
|
|
},
|
|
|
|
|
});
|
2026-04-22 05:57:06 +08:00
|
|
|
}
|