2026-04-16 22:01:51 +08:00
|
|
|
import { headers } from "next/headers";
|
2025-11-23 10:55:04 +08:00
|
|
|
import { notFound, redirect } from "next/navigation";
|
|
|
|
|
import { DocumentShell } from "@/components/editor/document-shell";
|
2026-04-14 13:22:29 +08:00
|
|
|
import type { PageFont, PageLayoutDensity, PageOptionsState, DocumentStats } from "@/types/page-options";
|
2026-01-17 10:12:53 +08:00
|
|
|
import { isConvexEnabled } from "@/lib/convex/enabled";
|
2026-04-14 13:22:29 +08:00
|
|
|
import { fetchDocumentMetaViaBridge } from "@/lib/documents/bridge-server";
|
2026-04-16 22:01:51 +08:00
|
|
|
import { getAuthedConvexClient } from "@/lib/convex/route";
|
|
|
|
|
import { buildDocumentBridgeContext, buildDocumentQueryEnvelope } from "@/lib/documents/bridge";
|
|
|
|
|
import { buildPageSubtreeProjection } from "@/lib/documents/page-subtree";
|
|
|
|
|
import { executeRustBridgeQueryTransport, resolveRustBridgeQueryPlan } from "@/lib/documents/rust-runtime";
|
2025-11-23 10:55:04 +08:00
|
|
|
|
|
|
|
|
interface DocumentPageProps {
|
|
|
|
|
params: Promise<{ id: string }>;
|
2026-04-14 13:22:29 +08:00
|
|
|
searchParams?: Promise<Record<string, string | string[] | undefined>>;
|
2025-11-23 10:55:04 +08:00
|
|
|
}
|
|
|
|
|
|
2026-04-14 13:22:29 +08:00
|
|
|
type DocumentMetaPayload = {
|
|
|
|
|
id: string;
|
|
|
|
|
workspace_id: string;
|
|
|
|
|
title: string | null;
|
|
|
|
|
updated_at: string | null;
|
|
|
|
|
can_edit?: boolean | null;
|
|
|
|
|
disable_download?: boolean | null;
|
|
|
|
|
disable_copy?: boolean | null;
|
|
|
|
|
wide_layout?: boolean | null;
|
|
|
|
|
use_small_text?: boolean | null;
|
|
|
|
|
show_heading_numbers?: boolean | null;
|
|
|
|
|
show_toc?: boolean | null;
|
|
|
|
|
show_structure?: boolean | null;
|
|
|
|
|
protect_editing?: boolean | null;
|
|
|
|
|
show_word_count?: boolean | null;
|
|
|
|
|
collapse_backlinks?: boolean | null;
|
|
|
|
|
page_font?: PageFont | null;
|
|
|
|
|
layout_density?: PageLayoutDensity | null;
|
|
|
|
|
hide_child_pages?: boolean | null;
|
|
|
|
|
show_block_ref_count?: boolean | null;
|
|
|
|
|
embed_default_block_id?: string | null;
|
|
|
|
|
word_count?: number | null;
|
|
|
|
|
character_count?: number | null;
|
|
|
|
|
block_count?: number | null;
|
|
|
|
|
todo_total?: number | null;
|
|
|
|
|
todo_total_count?: number | null;
|
|
|
|
|
todo_done?: number | null;
|
|
|
|
|
todo_done_count?: number | null;
|
|
|
|
|
};
|
|
|
|
|
|
2026-04-16 22:01:51 +08:00
|
|
|
type DocumentContentPayload = {
|
|
|
|
|
content?: unknown;
|
|
|
|
|
revision?: number | null;
|
|
|
|
|
conflict_detection_key?: string | null;
|
|
|
|
|
};
|
|
|
|
|
|
|
|
|
|
async function fetchDocumentContentOnServer(input: {
|
|
|
|
|
documentId: string;
|
|
|
|
|
workspaceId: string;
|
|
|
|
|
}): Promise<{
|
|
|
|
|
content: unknown;
|
|
|
|
|
revision: number | null;
|
|
|
|
|
conflictDetectionKey: string | null;
|
|
|
|
|
}> {
|
|
|
|
|
try {
|
|
|
|
|
const headerList = await headers();
|
|
|
|
|
const requestHeaders = new Headers();
|
|
|
|
|
[
|
|
|
|
|
"cookie",
|
|
|
|
|
"authorization",
|
|
|
|
|
"x-request-id",
|
|
|
|
|
"x-trace-id",
|
|
|
|
|
"x-session-id",
|
|
|
|
|
"x-source-channel",
|
|
|
|
|
"x-source-client",
|
|
|
|
|
"user-agent",
|
|
|
|
|
].forEach((name) => {
|
|
|
|
|
const value = headerList.get(name);
|
|
|
|
|
if (value) {
|
|
|
|
|
requestHeaders.set(name, value);
|
|
|
|
|
}
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
const request = new Request("http://mnote.local/documents/content", {
|
|
|
|
|
method: "GET",
|
|
|
|
|
headers: requestHeaders,
|
|
|
|
|
});
|
|
|
|
|
const { client } = await getAuthedConvexClient();
|
|
|
|
|
const bridgeContext = await buildDocumentBridgeContext({
|
|
|
|
|
request,
|
|
|
|
|
workspaceId: input.workspaceId,
|
|
|
|
|
});
|
|
|
|
|
const envelope = buildDocumentQueryEnvelope({
|
|
|
|
|
name: "documents.content.get",
|
|
|
|
|
payload: {
|
|
|
|
|
documentId: input.documentId,
|
|
|
|
|
workspaceId: input.workspaceId,
|
|
|
|
|
},
|
|
|
|
|
});
|
|
|
|
|
const plan = await resolveRustBridgeQueryPlan({
|
|
|
|
|
context: bridgeContext,
|
|
|
|
|
envelope,
|
|
|
|
|
});
|
|
|
|
|
const result = await executeRustBridgeQueryTransport<DocumentContentPayload | null>({
|
|
|
|
|
client,
|
|
|
|
|
plan,
|
|
|
|
|
});
|
|
|
|
|
|
|
|
|
|
return {
|
|
|
|
|
content: result?.content ?? null,
|
|
|
|
|
revision:
|
|
|
|
|
typeof result?.revision === "number" && Number.isInteger(result.revision)
|
|
|
|
|
? result.revision
|
|
|
|
|
: 0,
|
|
|
|
|
conflictDetectionKey:
|
|
|
|
|
typeof result?.conflict_detection_key === "string" && result.conflict_detection_key.trim()
|
|
|
|
|
? result.conflict_detection_key
|
|
|
|
|
: `${input.documentId}:0`,
|
|
|
|
|
};
|
|
|
|
|
} catch {
|
|
|
|
|
return {
|
|
|
|
|
content: null,
|
|
|
|
|
revision: null,
|
|
|
|
|
conflictDetectionKey: null,
|
|
|
|
|
};
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-10 10:35:21 +08:00
|
|
|
export default async function DocumentPage({ params, searchParams }: DocumentPageProps) {
|
2025-11-23 10:55:04 +08:00
|
|
|
const { id } = await params;
|
2026-01-10 10:35:21 +08:00
|
|
|
const resolvedSearch = (await searchParams) ?? {};
|
|
|
|
|
const openTableIdRaw = resolvedSearch?.openTableId;
|
|
|
|
|
const openTableId = typeof openTableIdRaw === "string" ? openTableIdRaw : null;
|
2026-01-17 10:12:53 +08:00
|
|
|
|
|
|
|
|
if (isConvexEnabled()) {
|
2026-04-14 13:22:29 +08:00
|
|
|
const workspaceIdRaw = resolvedSearch?.workspaceId;
|
|
|
|
|
const workspaceId = typeof workspaceIdRaw === "string" ? workspaceIdRaw : null;
|
|
|
|
|
const result = await fetchDocumentMetaViaBridge<DocumentMetaPayload>({
|
|
|
|
|
documentId: id,
|
|
|
|
|
workspaceId,
|
|
|
|
|
});
|
|
|
|
|
const doc = result?.doc;
|
2026-01-17 10:12:53 +08:00
|
|
|
if (!doc) {
|
|
|
|
|
notFound();
|
|
|
|
|
}
|
2026-04-14 13:22:29 +08:00
|
|
|
const readOnly = doc.can_edit === false;
|
|
|
|
|
const disableDownload = Boolean(doc.disable_download);
|
|
|
|
|
const disableCopy = Boolean(doc.disable_copy);
|
2026-01-17 10:12:53 +08:00
|
|
|
|
|
|
|
|
const initialOptions: PageOptionsState = {
|
|
|
|
|
wideLayout: doc.wide_layout ?? false,
|
|
|
|
|
smallText: doc.use_small_text ?? false,
|
|
|
|
|
showHeadingNumbers: doc.show_heading_numbers ?? true,
|
|
|
|
|
showToc: doc.show_toc ?? false,
|
|
|
|
|
showStructure: doc.show_structure ?? false,
|
|
|
|
|
protectEditing: doc.protect_editing ?? false,
|
|
|
|
|
showWordCount: doc.show_word_count ?? true,
|
2026-04-14 13:22:29 +08:00
|
|
|
collapseBacklinks: doc.collapse_backlinks ?? false,
|
|
|
|
|
pageFont: doc.page_font ?? "default",
|
|
|
|
|
layoutDensity: doc.layout_density ?? "normal",
|
|
|
|
|
hideChildPages: doc.hide_child_pages ?? false,
|
|
|
|
|
showBlockRefCount: doc.show_block_ref_count ?? false,
|
|
|
|
|
embedDefaultBlockId: doc.embed_default_block_id ?? null,
|
2026-01-17 10:12:53 +08:00
|
|
|
};
|
|
|
|
|
|
|
|
|
|
const initialStats: DocumentStats = {
|
|
|
|
|
wordCount: doc.word_count ?? 0,
|
|
|
|
|
characterCount: doc.character_count ?? 0,
|
|
|
|
|
blockCount: doc.block_count ?? 0,
|
2026-04-14 13:22:29 +08:00
|
|
|
todoTotal: doc.todo_total ?? doc.todo_total_count ?? 0,
|
|
|
|
|
todoDone: doc.todo_done ?? doc.todo_done_count ?? 0,
|
2026-01-17 10:12:53 +08:00
|
|
|
};
|
2026-04-16 22:01:51 +08:00
|
|
|
const initialDocumentContent = await fetchDocumentContentOnServer({
|
|
|
|
|
documentId: doc.id,
|
|
|
|
|
workspaceId: doc.workspace_id,
|
|
|
|
|
});
|
|
|
|
|
const initialPageSubtree = buildPageSubtreeProjection({
|
|
|
|
|
documentId: doc.id,
|
|
|
|
|
title: doc.title ?? "无标题",
|
|
|
|
|
content: initialDocumentContent.content,
|
|
|
|
|
});
|
2026-01-17 10:12:53 +08:00
|
|
|
|
|
|
|
|
return (
|
|
|
|
|
<div className="flex h-screen flex-col">
|
|
|
|
|
<div className="min-h-0 flex-1">
|
|
|
|
|
<DocumentShell
|
|
|
|
|
documentId={doc.id}
|
|
|
|
|
workspaceId={doc.workspace_id}
|
|
|
|
|
title={doc.title ?? "无标题"}
|
|
|
|
|
updatedAt={doc.updated_at}
|
2026-04-16 22:01:51 +08:00
|
|
|
initialContent={initialDocumentContent.content}
|
|
|
|
|
initialContentRevision={initialDocumentContent.revision}
|
|
|
|
|
initialConflictDetectionKey={initialDocumentContent.conflictDetectionKey}
|
|
|
|
|
initialPageSubtree={initialPageSubtree}
|
2026-01-17 10:12:53 +08:00
|
|
|
initialOptions={initialOptions}
|
|
|
|
|
initialStats={initialStats}
|
|
|
|
|
openTableId={openTableId}
|
2026-01-22 18:53:20 +08:00
|
|
|
readOnly={readOnly}
|
2026-01-24 12:32:51 +08:00
|
|
|
disableDownload={disableDownload}
|
|
|
|
|
disableCopy={disableCopy}
|
2026-01-17 10:12:53 +08:00
|
|
|
/>
|
|
|
|
|
</div>
|
|
|
|
|
</div>
|
|
|
|
|
);
|
|
|
|
|
}
|
|
|
|
|
|
2026-01-18 19:01:31 +08:00
|
|
|
redirect("/auth");
|
2025-11-23 10:55:04 +08:00
|
|
|
}
|