Files
mnote/wolai-frontend/src/app/api/tree/commands/route.ts
T
lix-2026 e564dfde02 feat: continue tree rust family cutover
- add rust renderer/state-family scaffolds and inline compat host thinning for page tree, file tree, and picker
- route tree/filetree preflight, file projection, resource artifact, and stream delta contracts through rust plans
- preserve canonical move-order validation, file-tree search projection, and related frontend/runtime regression coverage
2026-04-26 19:35:52 +08:00

764 lines
22 KiB
TypeScript

import { randomUUID } from "node:crypto";
import { NextResponse } from "next/server";
import type { Json } from "@/types/supabase";
import { api } from "@/lib/convex/api";
import { isConvexEnabled } from "@/lib/convex/enabled";
import { getAuthedConvexClient } from "@/lib/convex/route";
import { composeContentWithBlocks, extractBlocksFromContent } from "@/lib/document-content";
import {
assertDocumentId,
assertTitle,
buildDocumentBridgeContext,
buildDocumentCommandEnvelope,
documentBridgeErrorResponse,
} from "@/lib/documents/bridge";
import {
recordBridgeCommandFailureArtifacts,
} from "@/lib/documents/bridge-log";
import {
copyMindmapFilesIfExists,
ensureDocumentScaffold,
} from "@/lib/documents/page-lifecycle-side-effects";
import { buildDocumentSavePayload } from "@/lib/documents/save-contract";
import { loadSidebarDataFromConvex } from "@/lib/server/sidebar-data";
import {
executeRustBridgeMutationTransport,
recordRustBridgeCommandArtifacts,
resolveRustBridgeCommandPlan,
} from "@/lib/documents/rust-runtime";
type TreeCommandAction =
| "create"
| "rename"
| "move"
| "archive"
| "restore"
| "purge"
| "embed"
| "copy";
type TreeCopyItem = {
documentId: string;
recursive: boolean;
};
type TreeCommandPayload = {
action?: TreeCommandAction;
workspaceId?: string | null;
documentId?: string | null;
parentId?: string | null;
targetParentId?: string | null;
title?: string | null;
accessScope?: "private" | "shared" | "public" | null;
content?: unknown;
sortOrder?: number | null;
sourceId?: string | null;
targetId?: string | null;
items?: TreeCopyItem[] | null;
};
type TreeDeltaDocument = {
id: string;
workspace_id: string;
title: string | null;
parent_id: string | null;
sort_order: number | null;
is_starred: boolean | null;
access_scope: "private" | "shared" | "public";
is_template: boolean;
created_at: string;
updated_at: string | null;
};
type TreeMutationResult<TResult> = {
context: Awaited<ReturnType<typeof buildDocumentBridgeContext>>;
envelope: ReturnType<typeof buildDocumentCommandEnvelope>;
plan: Awaited<ReturnType<typeof resolveRustBridgeCommandPlan>>;
result: TResult;
};
function trimOrNull(value: unknown) {
if (typeof value !== "string") return null;
const trimmed = value.trim();
return trimmed.length > 0 ? trimmed : null;
}
function normalizeTitle(value: unknown) {
const title = typeof value === "string" ? value.trim() : "";
return title || "无标题";
}
function normalizeSortOrder(value: unknown) {
return typeof value === "number" && Number.isFinite(value) ? Math.max(0, Math.floor(value)) : 0;
}
function isRecord(value: unknown): value is Record<string, unknown> {
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
}
async function recordTreeCommandSuccess(args: {
context: Awaited<ReturnType<typeof buildDocumentBridgeContext>>;
envelope: ReturnType<typeof buildDocumentCommandEnvelope>;
client: Awaited<ReturnType<typeof getAuthedConvexClient>>["client"];
plan: Awaited<ReturnType<typeof resolveRustBridgeCommandPlan>>;
result: unknown;
}) {
try {
await recordRustBridgeCommandArtifacts({
context: args.context,
envelope: args.envelope,
client: args.client,
plan: args.plan,
result: args.result,
});
} catch (error) {
console.warn("[tree.commands] Rust bridge success artifacts skipped:", error);
}
}
async function loadTreeCommandSidebarSnapshot(args: {
client: Awaited<ReturnType<typeof getAuthedConvexClient>>["client"];
auth: Awaited<ReturnType<typeof getAuthedConvexClient>>["auth"];
workspaceId: string | null;
}) {
if (!args.workspaceId) {
return null;
}
try {
const result = await loadSidebarDataFromConvex({
client: args.client,
auth: {
userId: args.auth.userId,
email: args.auth.email,
name: args.auth.name,
},
fallbackName: args.auth.email ?? args.auth.name ?? "我的空间",
requestedWorkspaceId: args.workspaceId,
});
return result.sidebarInitialData ?? null;
} catch (error) {
console.warn("[tree.commands] sidebar snapshot skipped:", error);
return null;
}
}
function buildTreeMovePreflightDataFromSidebarSnapshot(
sidebarSnapshot: unknown,
): Record<string, unknown> | null {
if (!isRecord(sidebarSnapshot) || !Array.isArray(sidebarSnapshot.documents)) {
return null;
}
return {
documents: sidebarSnapshot.documents,
};
}
async function resolveTreeMutationResult<TResult>(args: {
request: Request;
workspaceId: string | null;
commandName: string;
payload: unknown;
preflightData?: Record<string, unknown> | null;
pageId?: string | null;
client: Awaited<ReturnType<typeof getAuthedConvexClient>>["client"];
}) {
const context = await buildDocumentBridgeContext({
request: args.request,
workspaceId: args.workspaceId,
});
const envelope = buildDocumentCommandEnvelope({
name: args.commandName,
payload: args.payload,
context,
preflightData: args.preflightData ?? null,
target: {
workspaceId: args.workspaceId,
pageId: args.pageId ?? undefined,
},
reason: `tree-route ${args.commandName}`,
refs: ["next-tree-route"],
});
const plan = await resolveRustBridgeCommandPlan({
context,
envelope,
});
try {
const result = await executeRustBridgeMutationTransport<TResult>({
client: args.client,
plan,
});
return {
context,
envelope,
plan,
result,
};
} catch (error) {
await recordBridgeCommandFailureArtifacts({
context,
envelope,
client: args.client,
error,
});
throw error;
}
}
export async function POST(request: Request) {
if (!isConvexEnabled()) {
return NextResponse.json({ error: "当前仅支持 Convex 模式" }, { status: 501 });
}
try {
const payload = (await request.json()) as TreeCommandPayload;
switch (payload.action) {
case "create":
return await handleCreate(request, payload);
case "move":
return await handleMove(request, payload);
case "rename":
return await handleRename(request, payload);
case "archive":
return await handleArchive(request, payload);
case "restore":
return await handleRestore(request, payload);
case "purge":
return await handlePurge(request, payload);
case "embed":
return await handleEmbed(request, payload);
case "copy":
return await handleCopy(request, payload);
default:
return NextResponse.json({ error: "不支持的 tree action" }, { status: 400 });
}
} catch (error) {
return documentBridgeErrorResponse(error);
}
}
async function handleCreate(request: Request, payload: TreeCommandPayload) {
const { auth, client } = await getAuthedConvexClient();
const parentId = trimOrNull(payload.parentId);
const workspaceBootstrap = await client.mutation(api.workspaces.ensureDefaultWorkspace, {
fallbackName: auth.email ?? auth.name ?? "我的空间",
workspaceIdIfCreate: randomUUID(),
});
let workspaceId: string | null = trimOrNull(payload.workspaceId);
let accessScope: "private" | "shared" | "public" = "private";
if (parentId) {
const parentDoc = await client.query(api.documents.getMeta, { id: parentId });
if (!parentDoc) {
return NextResponse.json({ error: "父页面不存在或无权限" }, { status: 404 });
}
workspaceId = trimOrNull(parentDoc.workspace_id);
accessScope = (parentDoc.access_scope ?? "private") as typeof accessScope;
} else if (!workspaceId) {
workspaceId = trimOrNull(workspaceBootstrap.activeWorkspaceId);
}
if (!workspaceId) {
return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 });
}
const documentId = trimOrNull(payload.documentId) ?? randomUUID();
const title = normalizeTitle(payload.title);
const mutation = await resolveTreeMutationResult<{
id: string;
document?: TreeDeltaDocument | null;
title: string | null;
parent_id: string | null;
sort_order: number | null;
workspace_id: string;
access_scope: "private" | "shared" | "public";
is_template: boolean;
created_at: string;
updated_at: string;
}>({
request,
workspaceId,
commandName: "tree.node.create",
payload: {
documentId,
workspaceId,
parentId,
title,
accessScope: trimOrNull(payload.accessScope) ?? accessScope,
content: Array.isArray(payload.content) ? payload.content : [],
},
pageId: documentId,
client,
});
const { context, envelope, result } = mutation;
await ensureDocumentScaffold(result.id, result.title ?? title);
await recordTreeCommandSuccess({
context,
envelope,
client,
plan: mutation.plan,
result,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "create",
workspaceId,
documentId: result.id,
parentId: result.parent_id ?? parentId,
title: result.title ?? title,
sortOrder: result.sort_order ?? null,
updatedAt: result.updated_at ?? null,
execution: result,
},
});
}
async function handleMove(request: Request, payload: TreeCommandPayload) {
const { auth, client } = await getAuthedConvexClient();
const documentId = assertDocumentId(payload.documentId ?? null);
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 });
}
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
const parentId = trimOrNull(payload.parentId);
const sortOrder = normalizeSortOrder(payload.sortOrder);
const sidebarSnapshot = await loadTreeCommandSidebarSnapshot({
client,
auth,
workspaceId,
});
const movePreflightData = buildTreeMovePreflightDataFromSidebarSnapshot(sidebarSnapshot);
const mutation = await resolveTreeMutationResult<{
ok?: boolean;
parent_id?: string | null;
sort_order?: number | null;
workspace_id?: string | null;
updated_at?: string | null;
}>({
request,
workspaceId,
commandName: "tree.subtree.move",
payload: {
documentId,
parentId,
sortOrder,
},
preflightData: movePreflightData,
pageId: documentId,
client,
});
const { context, envelope, result } = mutation;
await recordTreeCommandSuccess({
context,
envelope,
client,
plan: mutation.plan,
result,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "move",
workspaceId: trimOrNull(result?.workspace_id) ?? workspaceId,
documentId,
parentId: trimOrNull(result?.parent_id) ?? parentId,
sortOrder:
typeof result?.sort_order === "number" && Number.isFinite(result.sort_order)
? result.sort_order
: sortOrder,
updatedAt: trimOrNull(result?.updated_at) ?? null,
execution: {
...(result ?? null),
parent_id: trimOrNull(result?.parent_id) ?? parentId,
sort_order:
typeof result?.sort_order === "number" && Number.isFinite(result.sort_order)
? result.sort_order
: sortOrder,
workspace_id: trimOrNull(result?.workspace_id) ?? workspaceId,
},
},
});
}
async function handleRename(request: Request, payload: TreeCommandPayload) {
const { client } = await getAuthedConvexClient();
const documentId = assertDocumentId(payload.documentId ?? null);
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 });
}
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
const title = assertTitle(payload.title ?? null);
const mutation = await resolveTreeMutationResult<{
ok?: boolean;
document?: TreeDeltaDocument | null;
updated_at?: string | null;
}>({
request,
workspaceId,
commandName: "tree.node.rename",
payload: {
documentId,
workspaceId,
title,
},
pageId: documentId,
client,
});
const { context, envelope, result } = mutation;
await recordTreeCommandSuccess({
context,
envelope,
client,
plan: mutation.plan,
result,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "rename",
workspaceId,
documentId,
title,
updatedAt: result?.updated_at ?? null,
execution: result ?? null,
},
});
}
async function handleArchive(request: Request, payload: TreeCommandPayload) {
const { client } = await getAuthedConvexClient();
const documentId = assertDocumentId(payload.documentId ?? null);
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 });
}
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
const mutation = await resolveTreeMutationResult<{
ok?: boolean;
document?: TreeDeltaDocument | null;
updated_at?: string | null;
}>({
request,
workspaceId,
commandName: "tree.node.archive",
payload: {
documentId,
workspaceId,
},
pageId: documentId,
client,
});
const { context, envelope, result } = mutation;
await recordTreeCommandSuccess({
context,
envelope,
client,
plan: mutation.plan,
result,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "archive",
workspaceId,
documentId,
updatedAt: result?.updated_at ?? null,
execution: result ?? null,
},
});
}
async function handleRestore(request: Request, payload: TreeCommandPayload) {
const { client } = await getAuthedConvexClient();
const documentId = assertDocumentId(payload.documentId ?? null);
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 });
}
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
const mutation = await resolveTreeMutationResult<{
ok?: boolean;
document?: TreeDeltaDocument | null;
updated_at?: string | null;
}>({
request,
workspaceId,
commandName: "tree.node.restore",
payload: {
documentId,
workspaceId,
},
pageId: documentId,
client,
});
const { context, envelope, result } = mutation;
await recordTreeCommandSuccess({
context,
envelope,
client,
plan: mutation.plan,
result,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "restore",
workspaceId,
documentId,
updatedAt: result?.updated_at ?? null,
execution: result ?? null,
},
});
}
async function handlePurge(request: Request, payload: TreeCommandPayload) {
const { client } = await getAuthedConvexClient();
const documentId = assertDocumentId(payload.documentId ?? null);
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
if (!sourceDoc) {
return NextResponse.json({ error: "页面不存在或无权访问" }, { status: 404 });
}
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
const mutation = await resolveTreeMutationResult<{
ok?: boolean;
purged?: boolean;
purged_at?: string | null;
}>({
request,
workspaceId,
commandName: "tree.node.purge",
payload: {
documentId,
},
pageId: documentId,
client,
});
const { context, envelope, result } = mutation;
await recordTreeCommandSuccess({
context,
envelope,
client,
plan: mutation.plan,
result,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "purge",
workspaceId,
documentId,
purged: result?.purged ?? true,
updatedAt: result?.purged_at ?? null,
execution: result ?? null,
},
});
}
async function handleEmbed(request: Request, payload: TreeCommandPayload) {
const { client } = await getAuthedConvexClient();
const sourceId = assertDocumentId(payload.sourceId ?? null);
const targetId = assertDocumentId(payload.targetId ?? null);
const sourceDoc = await client.query(api.documents.getMeta, { id: sourceId });
if (!sourceDoc) {
return NextResponse.json({ error: "原始页面不存在或无权限" }, { status: 404 });
}
const targetContent = await client.query(api.documents.getContent, { id: targetId });
if (!targetContent) {
return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 });
}
const targetMeta = await client.query(api.documents.getMeta, { id: targetId });
const currentBlocks = extractBlocksFromContent(targetContent.content);
const anchorId = trimOrNull(
(targetMeta as { embed_default_block_id?: string | null } | null)?.embed_default_block_id,
);
const anchorIndex = anchorId
? currentBlocks.findIndex(
(block) =>
typeof block === "object" &&
block !== null &&
String((block as { id?: string }).id ?? "") === anchorId,
)
: -1;
const insertIndex = anchorIndex >= 0 ? anchorIndex + 1 : currentBlocks.length;
const nextBlocks: Json[] = [
...currentBlocks.slice(0, insertIndex),
{
id: randomUUID(),
type: "pageReference",
props: {
pageId: sourceId,
title: sourceDoc.title ?? "无标题",
},
},
...currentBlocks.slice(insertIndex),
];
const nextContent = composeContentWithBlocks(targetContent.content, nextBlocks);
const workspaceId =
trimOrNull(sourceDoc.workspace_id) ??
trimOrNull((targetMeta as { workspace_id?: string | null } | null)?.workspace_id);
const mutation = await resolveTreeMutationResult<{
revision?: number | null;
conflict_detection_key?: string | null;
}>({
request,
workspaceId,
commandName: "tree.node.embed",
payload: {
...buildDocumentSavePayload({
documentId: targetId,
workspaceId,
revision:
typeof targetContent.revision === "number" && Number.isInteger(targetContent.revision)
? targetContent.revision
: null,
content: nextContent,
conflictDetectionKey:
typeof targetContent.conflict_detection_key === "string"
? targetContent.conflict_detection_key
: null,
blockCount: nextBlocks.length,
}),
sourceDocumentId: sourceId,
targetDocumentId: targetId,
anchorBlockId: anchorId,
},
pageId: targetId,
client,
});
const { context, envelope, result } = mutation;
await recordTreeCommandSuccess({
context,
envelope,
client,
plan: mutation.plan,
result,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "embed",
workspaceId,
documentId: targetId,
sourceDocumentId: sourceId,
targetDocumentId: targetId,
execution: result ?? null,
},
});
}
async function handleCopy(request: Request, payload: TreeCommandPayload) {
const { client } = await getAuthedConvexClient();
const normalizedItems = (payload.items ?? [])
.filter((item) => item?.documentId)
.map((item) => ({
documentId: assertDocumentId(item.documentId),
recursive: Boolean(item.recursive),
}));
if (normalizedItems.length === 0) {
return NextResponse.json({ error: "items 为空" }, { status: 400 });
}
const targetParentId = trimOrNull(payload.targetParentId);
let workspaceId: string | null = null;
if (targetParentId) {
const targetDoc = await client.query(api.documents.getMeta, { id: targetParentId });
if (!targetDoc) {
return NextResponse.json({ error: "目标页面不存在或无权限" }, { status: 404 });
}
workspaceId = trimOrNull(targetDoc.workspace_id);
} else {
const firstDoc = await client.query(api.documents.getMeta, {
id: normalizedItems[0]?.documentId ?? "",
});
if (!firstDoc) {
return NextResponse.json({ error: "源页面不存在或无权限" }, { status: 404 });
}
workspaceId = trimOrNull(firstDoc.workspace_id);
}
if (!workspaceId) {
return NextResponse.json({ error: "缺少目标工作空间" }, { status: 400 });
}
const mutation = await resolveTreeMutationResult<{
items: Array<{
oldId: string;
newId: string;
title?: string | null;
document?: TreeDeltaDocument | null;
}>;
}>({
request,
workspaceId,
commandName: "tree.subtree.copy",
payload: {
workspaceId,
targetParentId,
items: normalizedItems,
},
pageId: targetParentId,
client,
});
const { context, envelope, result } = mutation;
await Promise.all(
(result.items ?? []).map(async (item) => {
await ensureDocumentScaffold(item.newId, item.title ?? "无标题");
await copyMindmapFilesIfExists(item.oldId, item.newId);
}),
);
await recordTreeCommandSuccess({
context,
envelope,
client,
plan: mutation.plan,
result,
});
return NextResponse.json({
requestId: context.requestId,
traceId: context.traceId,
result: {
action: "copy",
workspaceId,
targetParentId,
items: (result.items ?? []).map((item) => ({
oldId: item.oldId,
newId: item.newId,
})),
execution: result ?? null,
},
});
}
export const runtime = "nodejs";