4-26 树rust-2
This commit is contained in:
@@ -1,8 +1,10 @@
|
||||
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,
|
||||
@@ -10,21 +12,49 @@ import {
|
||||
buildDocumentCommandEnvelope,
|
||||
documentBridgeErrorResponse,
|
||||
} from "@/lib/documents/bridge";
|
||||
import { ensureDocumentScaffold } from "@/lib/documents/page-lifecycle-side-effects";
|
||||
import {
|
||||
recordBridgeCommandArtifacts,
|
||||
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,
|
||||
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?: "create" | "move" | "rename";
|
||||
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;
|
||||
};
|
||||
|
||||
function trimOrNull(value: unknown) {
|
||||
@@ -42,6 +72,150 @@ 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);
|
||||
}
|
||||
|
||||
function attachStreamDelta(commandPayload: unknown, streamDelta?: Record<string, unknown> | null) {
|
||||
if (!streamDelta) {
|
||||
return commandPayload;
|
||||
}
|
||||
if (isRecord(commandPayload)) {
|
||||
return {
|
||||
...commandPayload,
|
||||
streamDelta,
|
||||
};
|
||||
}
|
||||
return {
|
||||
payload: commandPayload,
|
||||
streamDelta,
|
||||
};
|
||||
}
|
||||
|
||||
async function recordTreeCommandSuccess(args: {
|
||||
context: Awaited<ReturnType<typeof buildDocumentBridgeContext>>;
|
||||
envelope: ReturnType<typeof buildDocumentCommandEnvelope>;
|
||||
client: Awaited<ReturnType<typeof getAuthedConvexClient>>["client"];
|
||||
commandPayload?: unknown;
|
||||
}) {
|
||||
try {
|
||||
await recordBridgeCommandArtifacts({
|
||||
context: args.context,
|
||||
envelope: args.envelope,
|
||||
client: args.client,
|
||||
commandPayload: args.commandPayload,
|
||||
});
|
||||
} catch (error) {
|
||||
console.warn("[tree.commands] 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 buildTreeCommandSnapshotDelta(
|
||||
sidebarSnapshot: unknown,
|
||||
): Record<string, unknown> | null {
|
||||
if (!isRecord(sidebarSnapshot)) {
|
||||
return null;
|
||||
}
|
||||
if (Array.isArray(sidebarSnapshot.documents)) {
|
||||
return {
|
||||
op: "replace_documents",
|
||||
documents: sidebarSnapshot.documents,
|
||||
};
|
||||
}
|
||||
return {
|
||||
op: "replace_sidebar",
|
||||
sidebar: sidebarSnapshot,
|
||||
};
|
||||
}
|
||||
|
||||
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,
|
||||
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 });
|
||||
@@ -51,11 +225,21 @@ export async function POST(request: Request) {
|
||||
const payload = (await request.json()) as TreeCommandPayload;
|
||||
switch (payload.action) {
|
||||
case "create":
|
||||
return handleCreate(request, payload);
|
||||
return await handleCreate(request, payload);
|
||||
case "move":
|
||||
return handleMove(request, payload);
|
||||
return await handleMove(request, payload);
|
||||
case "rename":
|
||||
return handleRename(request, payload);
|
||||
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 });
|
||||
}
|
||||
@@ -91,33 +275,7 @@ async function handleCreate(request: Request, payload: TreeCommandPayload) {
|
||||
|
||||
const documentId = trimOrNull(payload.documentId) ?? randomUUID();
|
||||
const title = normalizeTitle(payload.title);
|
||||
const context = await buildDocumentBridgeContext({
|
||||
request,
|
||||
workspaceId,
|
||||
});
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "tree.node.create",
|
||||
payload: {
|
||||
documentId,
|
||||
workspaceId,
|
||||
parentId,
|
||||
title,
|
||||
accessScope: trimOrNull(payload.accessScope) ?? accessScope,
|
||||
content: Array.isArray(payload.content) ? payload.content : [],
|
||||
},
|
||||
context,
|
||||
target: {
|
||||
workspaceId,
|
||||
pageId: documentId,
|
||||
},
|
||||
reason: "tree-route create",
|
||||
refs: ["next-tree-route"],
|
||||
});
|
||||
const plan = await resolveRustBridgeCommandPlan({
|
||||
context,
|
||||
envelope,
|
||||
});
|
||||
const result = await executeRustBridgeMutationTransport<{
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
id: string;
|
||||
title: string | null;
|
||||
parent_id: string | null;
|
||||
@@ -128,11 +286,42 @@ async function handleCreate(request: Request, payload: TreeCommandPayload) {
|
||||
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,
|
||||
plan,
|
||||
});
|
||||
|
||||
await ensureDocumentScaffold(result.id, result.title ?? title);
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "upsert_document",
|
||||
document: {
|
||||
id: result.id,
|
||||
workspace_id: result.workspace_id,
|
||||
title: result.title ?? title,
|
||||
parent_id: result.parent_id ?? parentId,
|
||||
sort_order: result.sort_order ?? 0,
|
||||
access_scope: result.access_scope,
|
||||
is_starred: false,
|
||||
is_template: result.is_template,
|
||||
created_at: result.created_at,
|
||||
updated_at: result.updated_at,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
requestId: context.requestId,
|
||||
@@ -151,7 +340,7 @@ async function handleCreate(request: Request, payload: TreeCommandPayload) {
|
||||
}
|
||||
|
||||
async function handleMove(request: Request, payload: TreeCommandPayload) {
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const { auth, client } = await getAuthedConvexClient();
|
||||
const documentId = assertDocumentId(payload.documentId ?? null);
|
||||
const sourceDoc = await client.query(api.documents.getMeta, { id: documentId });
|
||||
if (!sourceDoc) {
|
||||
@@ -161,32 +350,44 @@ async function handleMove(request: Request, payload: TreeCommandPayload) {
|
||||
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
|
||||
const parentId = trimOrNull(payload.parentId);
|
||||
const sortOrder = normalizeSortOrder(payload.sortOrder);
|
||||
const context = await buildDocumentBridgeContext({
|
||||
request,
|
||||
const sidebarSnapshot = await loadTreeCommandSidebarSnapshot({
|
||||
client,
|
||||
auth,
|
||||
workspaceId,
|
||||
});
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "tree.subtree.move",
|
||||
const movePreflightData = buildTreeMovePreflightDataFromSidebarSnapshot(sidebarSnapshot);
|
||||
const { context, envelope, result } = 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,
|
||||
},
|
||||
context,
|
||||
target: {
|
||||
workspaceId,
|
||||
pageId: documentId,
|
||||
},
|
||||
reason: "tree-route move",
|
||||
refs: ["next-tree-route"],
|
||||
preflightData: movePreflightData,
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
const plan = await resolveRustBridgeCommandPlan({
|
||||
const postMutationSidebarSnapshot = await loadTreeCommandSidebarSnapshot({
|
||||
client,
|
||||
auth,
|
||||
workspaceId,
|
||||
});
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
});
|
||||
const result = await executeRustBridgeMutationTransport<{ ok?: boolean; updated_at?: string | null }>({
|
||||
client,
|
||||
plan,
|
||||
commandPayload: attachStreamDelta(
|
||||
envelope.payload,
|
||||
buildTreeCommandSnapshotDelta(postMutationSidebarSnapshot),
|
||||
),
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -194,12 +395,23 @@ async function handleMove(request: Request, payload: TreeCommandPayload) {
|
||||
traceId: context.traceId,
|
||||
result: {
|
||||
action: "move",
|
||||
workspaceId,
|
||||
workspaceId: trimOrNull(result?.workspace_id) ?? workspaceId,
|
||||
documentId,
|
||||
parentId,
|
||||
sortOrder,
|
||||
updatedAt: result?.updated_at ?? null,
|
||||
execution: result ?? null,
|
||||
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,
|
||||
},
|
||||
},
|
||||
});
|
||||
}
|
||||
@@ -214,32 +426,33 @@ async function handleRename(request: Request, payload: TreeCommandPayload) {
|
||||
|
||||
const workspaceId = trimOrNull(payload.workspaceId) ?? trimOrNull(sourceDoc.workspace_id);
|
||||
const title = assertTitle(payload.title ?? null);
|
||||
const context = await buildDocumentBridgeContext({
|
||||
const { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
ok?: boolean;
|
||||
updated_at?: string | null;
|
||||
}>({
|
||||
request,
|
||||
workspaceId,
|
||||
});
|
||||
const envelope = buildDocumentCommandEnvelope({
|
||||
name: "tree.node.rename",
|
||||
commandName: "tree.node.rename",
|
||||
payload: {
|
||||
documentId,
|
||||
workspaceId,
|
||||
title,
|
||||
},
|
||||
context,
|
||||
target: {
|
||||
workspaceId,
|
||||
pageId: documentId,
|
||||
},
|
||||
reason: "tree-route rename",
|
||||
refs: ["next-tree-route"],
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
const plan = await resolveRustBridgeCommandPlan({
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
});
|
||||
const result = await executeRustBridgeMutationTransport<{ ok?: boolean; updated_at?: string | null }>({
|
||||
client,
|
||||
plan,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "upsert_document",
|
||||
document: {
|
||||
id: documentId,
|
||||
title,
|
||||
updated_at: result?.updated_at ?? null,
|
||||
},
|
||||
}),
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
@@ -256,4 +469,338 @@ async function handleRename(request: Request, payload: TreeCommandPayload) {
|
||||
});
|
||||
}
|
||||
|
||||
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 { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
ok?: boolean;
|
||||
updated_at?: string | null;
|
||||
}>({
|
||||
request,
|
||||
workspaceId,
|
||||
commandName: "tree.node.archive",
|
||||
payload: {
|
||||
documentId,
|
||||
workspaceId,
|
||||
},
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "remove_document",
|
||||
documentId,
|
||||
}),
|
||||
});
|
||||
|
||||
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 { 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 { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
ok?: boolean;
|
||||
updated_at?: string | null;
|
||||
}>({
|
||||
request,
|
||||
workspaceId,
|
||||
commandName: "tree.node.restore",
|
||||
payload: {
|
||||
documentId,
|
||||
workspaceId,
|
||||
},
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
const sidebarSnapshot = await loadTreeCommandSidebarSnapshot({
|
||||
client,
|
||||
auth,
|
||||
workspaceId,
|
||||
});
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(
|
||||
envelope.payload,
|
||||
buildTreeCommandSnapshotDelta(sidebarSnapshot),
|
||||
),
|
||||
});
|
||||
|
||||
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 { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
ok?: boolean;
|
||||
purged?: boolean;
|
||||
purged_at?: string | null;
|
||||
}>({
|
||||
request,
|
||||
workspaceId,
|
||||
commandName: "tree.node.purge",
|
||||
payload: {
|
||||
documentId,
|
||||
},
|
||||
pageId: documentId,
|
||||
client,
|
||||
});
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "remove_document",
|
||||
documentId,
|
||||
}),
|
||||
});
|
||||
|
||||
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 { context, envelope, result } = 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,
|
||||
});
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(envelope.payload, {
|
||||
op: "noop",
|
||||
}),
|
||||
});
|
||||
|
||||
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 { auth, 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 { context, envelope, result } = await resolveTreeMutationResult<{
|
||||
items: Array<{
|
||||
oldId: string;
|
||||
newId: string;
|
||||
title?: string | null;
|
||||
}>;
|
||||
}>({
|
||||
request,
|
||||
workspaceId,
|
||||
commandName: "tree.subtree.copy",
|
||||
payload: {
|
||||
workspaceId,
|
||||
targetParentId,
|
||||
items: normalizedItems,
|
||||
},
|
||||
pageId: targetParentId,
|
||||
client,
|
||||
});
|
||||
|
||||
await Promise.all(
|
||||
(result.items ?? []).map(async (item) => {
|
||||
await ensureDocumentScaffold(item.newId, item.title ?? "无标题");
|
||||
await copyMindmapFilesIfExists(item.oldId, item.newId);
|
||||
}),
|
||||
);
|
||||
const sidebarSnapshot = await loadTreeCommandSidebarSnapshot({
|
||||
client,
|
||||
auth,
|
||||
workspaceId,
|
||||
});
|
||||
await recordTreeCommandSuccess({
|
||||
context,
|
||||
envelope,
|
||||
client,
|
||||
commandPayload: attachStreamDelta(
|
||||
envelope.payload,
|
||||
buildTreeCommandSnapshotDelta(sidebarSnapshot),
|
||||
),
|
||||
});
|
||||
|
||||
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";
|
||||
|
||||
Reference in New Issue
Block a user