feat(kernel): complete tree-first graph tasks 074-080
This commit is contained in:
@@ -1,8 +1,13 @@
|
||||
import { headers } from "next/headers";
|
||||
import { notFound, redirect } from "next/navigation";
|
||||
import { DocumentShell } from "@/components/editor/document-shell";
|
||||
import type { PageFont, PageLayoutDensity, PageOptionsState, DocumentStats } from "@/types/page-options";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { fetchDocumentMetaViaBridge } from "@/lib/documents/bridge-server";
|
||||
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";
|
||||
|
||||
interface DocumentPageProps {
|
||||
params: Promise<{ id: string }>;
|
||||
@@ -39,6 +44,84 @@ type DocumentMetaPayload = {
|
||||
todo_done_count?: number | null;
|
||||
};
|
||||
|
||||
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,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
export default async function DocumentPage({ params, searchParams }: DocumentPageProps) {
|
||||
const { id } = await params;
|
||||
const resolvedSearch = (await searchParams) ?? {};
|
||||
@@ -83,6 +166,15 @@ export default async function DocumentPage({ params, searchParams }: DocumentPag
|
||||
todoTotal: doc.todo_total ?? doc.todo_total_count ?? 0,
|
||||
todoDone: doc.todo_done ?? doc.todo_done_count ?? 0,
|
||||
};
|
||||
const initialDocumentContent = await fetchDocumentContentOnServer({
|
||||
documentId: doc.id,
|
||||
workspaceId: doc.workspace_id,
|
||||
});
|
||||
const initialPageSubtree = buildPageSubtreeProjection({
|
||||
documentId: doc.id,
|
||||
title: doc.title ?? "无标题",
|
||||
content: initialDocumentContent.content,
|
||||
});
|
||||
|
||||
return (
|
||||
<div className="flex h-screen flex-col">
|
||||
@@ -92,9 +184,10 @@ export default async function DocumentPage({ params, searchParams }: DocumentPag
|
||||
workspaceId={doc.workspace_id}
|
||||
title={doc.title ?? "无标题"}
|
||||
updatedAt={doc.updated_at}
|
||||
initialContent={null}
|
||||
initialContentRevision={null}
|
||||
initialConflictDetectionKey={null}
|
||||
initialContent={initialDocumentContent.content}
|
||||
initialContentRevision={initialDocumentContent.revision}
|
||||
initialConflictDetectionKey={initialDocumentContent.conflictDetectionKey}
|
||||
initialPageSubtree={initialPageSubtree}
|
||||
initialOptions={initialOptions}
|
||||
initialStats={initialStats}
|
||||
openTableId={openTableId}
|
||||
|
||||
@@ -43,6 +43,10 @@ type RequestPayload = {
|
||||
mindmapId?: string;
|
||||
selectedUids?: string[];
|
||||
documentBlocks?: unknown;
|
||||
node?: unknown;
|
||||
subtree?: unknown;
|
||||
outline?: unknown;
|
||||
evidence?: unknown;
|
||||
};
|
||||
options?: {
|
||||
searxng?: boolean;
|
||||
@@ -85,6 +89,18 @@ const makeRunId = () => {
|
||||
return `run_${Date.now()}_${Math.random().toString(16).slice(2, 10)}`;
|
||||
};
|
||||
|
||||
const serializeContextSnapshot = (label: string, value: unknown, limit: number) => {
|
||||
if (value === undefined) {
|
||||
return null;
|
||||
}
|
||||
try {
|
||||
const text = JSON.stringify(value);
|
||||
return `${label}=${text.slice(0, limit)}`;
|
||||
} catch {
|
||||
return `${label}=provided`;
|
||||
}
|
||||
};
|
||||
|
||||
const clampSteps = (raw: unknown) => {
|
||||
const parsed = Number(raw ?? DEFAULT_AGENT_MAX_STEPS);
|
||||
if (!Number.isFinite(parsed)) return DEFAULT_AGENT_MAX_STEPS;
|
||||
@@ -157,12 +173,19 @@ const buildHermesInstructions = (
|
||||
lines.push(`selectedUids=${selectedUids.join(",")}`);
|
||||
}
|
||||
if (payload.context?.documentBlocks !== undefined) {
|
||||
try {
|
||||
const snapshot = JSON.stringify(payload.context.documentBlocks);
|
||||
lines.push(`documentBlocksSnapshot=${snapshot.slice(0, 4000)}`);
|
||||
} catch {
|
||||
lines.push("documentBlocksSnapshot=provided");
|
||||
}
|
||||
lines.push(serializeContextSnapshot("documentBlocksSnapshot", payload.context.documentBlocks, 4000) ?? "documentBlocksSnapshot=provided");
|
||||
}
|
||||
if (payload.context?.node !== undefined) {
|
||||
lines.push(serializeContextSnapshot("kernelNode", payload.context.node, 1800) ?? "kernelNode=provided");
|
||||
}
|
||||
if (payload.context?.subtree !== undefined) {
|
||||
lines.push(serializeContextSnapshot("kernelSubtree", payload.context.subtree, 5000) ?? "kernelSubtree=provided");
|
||||
}
|
||||
if (payload.context?.outline !== undefined) {
|
||||
lines.push(serializeContextSnapshot("kernelOutline", payload.context.outline, 2500) ?? "kernelOutline=provided");
|
||||
}
|
||||
if (payload.context?.evidence !== undefined) {
|
||||
lines.push(serializeContextSnapshot("kernelEvidence", payload.context.evidence, 2500) ?? "kernelEvidence=provided");
|
||||
}
|
||||
if (attachments.length > 0) {
|
||||
lines.push(
|
||||
|
||||
@@ -0,0 +1,30 @@
|
||||
"use client";
|
||||
|
||||
import { useRouter } from "next/navigation";
|
||||
import { StandaloneMindmapView } from "@/components/editor/blocks/MindmapBlock";
|
||||
import type { MindmapProjection } from "@/lib/mindmap/mindmap-projection";
|
||||
|
||||
type MindmapPageClientProps = {
|
||||
docId: string;
|
||||
mindmapId: string;
|
||||
initialProjection: MindmapProjection | null;
|
||||
};
|
||||
|
||||
export default function MindmapPageClient({
|
||||
docId,
|
||||
mindmapId,
|
||||
initialProjection,
|
||||
}: MindmapPageClientProps) {
|
||||
const router = useRouter();
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-white">
|
||||
<StandaloneMindmapView
|
||||
docId={docId}
|
||||
mindmapId={mindmapId}
|
||||
initialProjection={initialProjection}
|
||||
onExitFullscreen={() => router.push(`/documents/${docId}`)}
|
||||
/>
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1,47 +1,110 @@
|
||||
"use client";
|
||||
import { headers } from "next/headers";
|
||||
import { getAuthedConvexClient } from "@/lib/convex/route";
|
||||
import { isConvexEnabled } from "@/lib/convex/enabled";
|
||||
import { buildDocumentBridgeContext, buildDocumentQueryEnvelope } from "@/lib/documents/bridge";
|
||||
import { executeRustBridgeQueryTransport, resolveRustBridgeQueryPlan } from "@/lib/documents/rust-runtime";
|
||||
import {
|
||||
buildMindmapProjection,
|
||||
defaultMindmapData,
|
||||
type MindmapProjection,
|
||||
} from "@/lib/mindmap/mindmap-projection";
|
||||
import MindmapPageClient from "./mindmap-page-client";
|
||||
|
||||
import { useMemo } from "react";
|
||||
import { useParams, useRouter } from "next/navigation";
|
||||
import type { BlockNoteEditor } from "@blocknote/core";
|
||||
import type { CustomBlockSchema } from "@/components/editor/schema";
|
||||
import { MindmapBlockView, defaultMindmapData } from "@/components/editor/blocks/MindmapBlock";
|
||||
type MindmapRouteQueryResult = {
|
||||
data?: unknown;
|
||||
meta?: unknown;
|
||||
};
|
||||
|
||||
const editorStub = {
|
||||
updateBlock: () => {
|
||||
/* 独立全屏页中跳过 BlockNote 持久化(思维导图数据由自身 API 管理) */
|
||||
},
|
||||
} as unknown as BlockNoteEditor<CustomBlockSchema>;
|
||||
async function fetchMindmapProjectionOnServer(input: {
|
||||
docId: string;
|
||||
mindmapId: string;
|
||||
}): Promise<MindmapProjection | null> {
|
||||
if (!isConvexEnabled()) {
|
||||
return buildMindmapProjection({
|
||||
documentId: input.docId,
|
||||
mindmapId: input.mindmapId,
|
||||
data: defaultMindmapData,
|
||||
meta: null,
|
||||
});
|
||||
}
|
||||
|
||||
export default function MindmapFullscreenPage({
|
||||
}: Record<string, never>) {
|
||||
const router = useRouter();
|
||||
const params = useParams<{ docId?: string; mindmapId?: string }>();
|
||||
const docId = params?.docId ?? "";
|
||||
const mindmapId = params?.mindmapId ?? "";
|
||||
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 stubBlock = useMemo(
|
||||
() =>
|
||||
({
|
||||
id: mindmapId,
|
||||
type: "mindmap",
|
||||
props: {
|
||||
docId,
|
||||
data: defaultMindmapData,
|
||||
},
|
||||
content: [],
|
||||
children: [],
|
||||
}) as any,
|
||||
[docId, mindmapId],
|
||||
);
|
||||
const request = new Request("http://mnote.local/mindmap/projection", {
|
||||
method: "GET",
|
||||
headers: requestHeaders,
|
||||
});
|
||||
const { client } = await getAuthedConvexClient();
|
||||
const context = await buildDocumentBridgeContext({
|
||||
request,
|
||||
workspaceId: null,
|
||||
});
|
||||
const envelope = buildDocumentQueryEnvelope({
|
||||
name: "mindmaps.get",
|
||||
payload: {
|
||||
documentId: input.docId,
|
||||
mindmapId: input.mindmapId,
|
||||
workspaceId: null,
|
||||
},
|
||||
});
|
||||
const plan = await resolveRustBridgeQueryPlan({
|
||||
context,
|
||||
envelope,
|
||||
});
|
||||
const result = await executeRustBridgeQueryTransport<MindmapRouteQueryResult | null>({
|
||||
client,
|
||||
plan,
|
||||
});
|
||||
|
||||
return buildMindmapProjection({
|
||||
documentId: input.docId,
|
||||
mindmapId: input.mindmapId,
|
||||
data: result?.data ?? defaultMindmapData,
|
||||
meta: result?.meta ?? {
|
||||
requestId: context.requestId,
|
||||
traceId: context.traceId,
|
||||
workspaceId: context.workspaceId,
|
||||
documentId: input.docId,
|
||||
pageId: input.docId,
|
||||
mindmapId: input.mindmapId,
|
||||
attachmentId: input.mindmapId,
|
||||
},
|
||||
});
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
export default async function MindmapFullscreenPage({
|
||||
params,
|
||||
}: {
|
||||
params: Promise<{ docId: string; mindmapId: string }>;
|
||||
}) {
|
||||
const { docId, mindmapId } = await params;
|
||||
const initialProjection = await fetchMindmapProjectionOnServer({ docId, mindmapId });
|
||||
|
||||
return (
|
||||
<div className="fixed inset-0 bg-white">
|
||||
<MindmapBlockView
|
||||
block={stubBlock}
|
||||
editor={editorStub}
|
||||
fullscreen
|
||||
onExitFullscreen={() => router.push(`/documents/${docId}`)}
|
||||
/>
|
||||
</div>
|
||||
<MindmapPageClient
|
||||
docId={docId}
|
||||
mindmapId={mindmapId}
|
||||
initialProjection={initialProjection}
|
||||
/>
|
||||
);
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user