Fix tiptap selection sync and toolbar event loop
This commit is contained in:
@@ -6,7 +6,8 @@ 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 type { PageSubtreeProjection } from "@/lib/documents/page-subtree";
|
||||
import { normalizeDocumentContentResponse } from "@/lib/documents/page-subtree-response";
|
||||
import { executeRustBridgeQueryTransport, resolveRustBridgeQueryPlan } from "@/lib/documents/rust-runtime";
|
||||
|
||||
interface DocumentPageProps {
|
||||
@@ -48,15 +49,18 @@ type DocumentContentPayload = {
|
||||
content?: unknown;
|
||||
revision?: number | null;
|
||||
conflict_detection_key?: string | null;
|
||||
page_subtree?: PageSubtreeProjection | null;
|
||||
};
|
||||
|
||||
async function fetchDocumentContentOnServer(input: {
|
||||
documentId: string;
|
||||
workspaceId: string;
|
||||
title?: string | null;
|
||||
}): Promise<{
|
||||
content: unknown;
|
||||
revision: number | null;
|
||||
conflictDetectionKey: string | null;
|
||||
pageSubtree: PageSubtreeProjection | null;
|
||||
}> {
|
||||
try {
|
||||
const headerList = await headers();
|
||||
@@ -101,23 +105,19 @@ async function fetchDocumentContentOnServer(input: {
|
||||
client,
|
||||
plan,
|
||||
});
|
||||
const normalized = normalizeDocumentContentResponse({
|
||||
documentId: input.documentId,
|
||||
title: input.title ?? null,
|
||||
payload: result,
|
||||
});
|
||||
|
||||
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`,
|
||||
};
|
||||
return normalized;
|
||||
} catch {
|
||||
return {
|
||||
content: null,
|
||||
revision: null,
|
||||
conflictDetectionKey: null,
|
||||
pageSubtree: null,
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -169,11 +169,7 @@ export default async function DocumentPage({ params, searchParams }: DocumentPag
|
||||
const initialDocumentContent = await fetchDocumentContentOnServer({
|
||||
documentId: doc.id,
|
||||
workspaceId: doc.workspace_id,
|
||||
});
|
||||
const initialPageSubtree = buildPageSubtreeProjection({
|
||||
documentId: doc.id,
|
||||
title: doc.title ?? "无标题",
|
||||
content: initialDocumentContent.content,
|
||||
});
|
||||
|
||||
return (
|
||||
@@ -187,7 +183,7 @@ export default async function DocumentPage({ params, searchParams }: DocumentPag
|
||||
initialContent={initialDocumentContent.content}
|
||||
initialContentRevision={initialDocumentContent.revision}
|
||||
initialConflictDetectionKey={initialDocumentContent.conflictDetectionKey}
|
||||
initialPageSubtree={initialPageSubtree}
|
||||
initialPageSubtree={initialDocumentContent.pageSubtree}
|
||||
initialOptions={initialOptions}
|
||||
initialStats={initialStats}
|
||||
openTableId={openTableId}
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
buildDocumentQueryEnvelope,
|
||||
documentBridgeErrorResponse,
|
||||
} from "@/lib/documents/bridge";
|
||||
import type { PageSubtreeProjection } from "@/lib/documents/page-subtree";
|
||||
import { normalizeDocumentContentResponse } from "@/lib/documents/page-subtree-response";
|
||||
import {
|
||||
executeRustBridgeQueryTransport,
|
||||
resolveRustBridgeQueryPlan,
|
||||
@@ -35,6 +37,8 @@ export async function GET(request: Request) {
|
||||
content?: unknown;
|
||||
revision?: number | null;
|
||||
conflict_detection_key?: string | null;
|
||||
title?: string | null;
|
||||
page_subtree?: PageSubtreeProjection | null;
|
||||
} | null>({
|
||||
client,
|
||||
plan,
|
||||
@@ -54,16 +58,17 @@ export async function GET(request: Request) {
|
||||
);
|
||||
}
|
||||
|
||||
const normalized = normalizeDocumentContentResponse({
|
||||
documentId,
|
||||
title: typeof result.title === "string" && result.title.trim() ? result.title : null,
|
||||
payload: result,
|
||||
});
|
||||
|
||||
return NextResponse.json({
|
||||
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
|
||||
: `${documentId}:0`,
|
||||
content: normalized.content,
|
||||
revision: normalized.revision,
|
||||
conflictDetectionKey: normalized.conflictDetectionKey,
|
||||
pageSubtree: normalized.pageSubtree,
|
||||
meta: {
|
||||
requestId: bridgeContext.requestId,
|
||||
traceId: bridgeContext.traceId,
|
||||
|
||||
@@ -20,7 +20,7 @@ import { cn } from "@/lib/utils";
|
||||
import { Button } from "@/components/ui/button";
|
||||
import { DocumentToc } from "@/components/editor/document-toc";
|
||||
import { DocumentReadView } from "@/components/editor/document-read-view";
|
||||
import { buildPageSubtreeProjection, extractPageBlocks, type PageSubtreeProjection } from "@/lib/documents/page-subtree";
|
||||
import { extractPageBlocks, type PageSubtreeProjection } from "@/lib/documents/page-subtree";
|
||||
import {
|
||||
deleteDocumentCommand,
|
||||
embedDocumentCommand,
|
||||
@@ -143,6 +143,11 @@ export function DocumentContent({
|
||||
const openMoveEmbedPicker = useMoveEmbedPickerStore((s) => s.openPicker);
|
||||
const [pageTitle, setPageTitle] = useState(title ?? "无标题");
|
||||
const [content, setContent] = useState<unknown>(initialContent);
|
||||
const [serverContentSnapshot, setServerContentSnapshot] = useState<unknown>(initialContent);
|
||||
const [serverPageSubtreeSnapshot, setServerPageSubtreeSnapshot] = useState<PageSubtreeProjection | null>(
|
||||
initialPageSubtree,
|
||||
);
|
||||
const [serverPageSubtreeTitle, setServerPageSubtreeTitle] = useState<string>(title ?? "无标题");
|
||||
const [contentRevision, setContentRevision] = useState<number | null>(initialContentRevision);
|
||||
const [conflictDetectionKey, setConflictDetectionKey] = useState<string | null>(initialConflictDetectionKey);
|
||||
const [contentLoading, setContentLoading] = useState(() => initialContent == null);
|
||||
@@ -259,6 +264,14 @@ export function DocumentContent({
|
||||
setPageTitle(title ?? "无标题");
|
||||
}, [title]);
|
||||
|
||||
useEffect(() => {
|
||||
setServerPageSubtreeTitle(title ?? "无标题");
|
||||
}, [title]);
|
||||
|
||||
useEffect(() => {
|
||||
setServerPageSubtreeSnapshot(initialPageSubtree);
|
||||
}, [initialPageSubtree]);
|
||||
|
||||
useEffect(() => {
|
||||
setOptions(initialOptions ?? defaultOptions);
|
||||
}, [initialOptions]);
|
||||
@@ -275,6 +288,10 @@ export function DocumentContent({
|
||||
setConflictDetectionKey(initialConflictDetectionKey);
|
||||
}, [initialConflictDetectionKey]);
|
||||
|
||||
useEffect(() => {
|
||||
setServerContentSnapshot(initialContent);
|
||||
}, [initialContent]);
|
||||
|
||||
useEffect(() => {
|
||||
const nextBlocks = extractPageBlocks(content);
|
||||
latestBlocksRef.current = nextBlocks.length > 0 ? (nextBlocks as Json) : null;
|
||||
@@ -354,9 +371,11 @@ export function DocumentContent({
|
||||
content?: unknown;
|
||||
revision?: number | null;
|
||||
conflictDetectionKey?: string | null;
|
||||
pageSubtree?: PageSubtreeProjection | null;
|
||||
};
|
||||
if (canceled) return;
|
||||
setContent(payload.content ?? null);
|
||||
setServerContentSnapshot(payload.content ?? null);
|
||||
setContentRevision(
|
||||
typeof payload.revision === "number" && Number.isInteger(payload.revision)
|
||||
? payload.revision
|
||||
@@ -367,6 +386,13 @@ export function DocumentContent({
|
||||
? payload.conflictDetectionKey
|
||||
: `${documentId}:0`,
|
||||
);
|
||||
setServerPageSubtreeSnapshot(payload.pageSubtree ?? null);
|
||||
setServerPageSubtreeTitle(
|
||||
typeof payload.pageSubtree?.rootNode.metadata.title === "string" &&
|
||||
payload.pageSubtree.rootNode.metadata.title.trim()
|
||||
? payload.pageSubtree.rootNode.metadata.title
|
||||
: title ?? "无标题",
|
||||
);
|
||||
} catch (error) {
|
||||
if (canceled) return;
|
||||
if ((error as { name?: string })?.name === "AbortError") return;
|
||||
@@ -393,7 +419,7 @@ export function DocumentContent({
|
||||
contentLoadingTimerRef.current = null;
|
||||
}
|
||||
};
|
||||
}, [documentId, initialContent, contentReloadKey, workspaceId]);
|
||||
}, [documentId, initialContent, contentReloadKey, title, workspaceId]);
|
||||
|
||||
const persistTitle = useCallback(
|
||||
async (nextTitle: string) => {
|
||||
@@ -730,18 +756,24 @@ export function DocumentContent({
|
||||
options.hideChildPages && "wolai-hide-child-pages",
|
||||
);
|
||||
const pageSubtree = useMemo(() => {
|
||||
if (initialPageSubtree && content === initialContent && pageTitle === (title ?? "无标题")) {
|
||||
return initialPageSubtree;
|
||||
const hasServerPageSubtree = Boolean(serverPageSubtreeSnapshot);
|
||||
const titleUnchanged = pageTitle === serverPageSubtreeTitle;
|
||||
const contentUnchanged = content === serverContentSnapshot;
|
||||
|
||||
if (hasServerPageSubtree && titleUnchanged && contentUnchanged) {
|
||||
return serverPageSubtreeSnapshot;
|
||||
}
|
||||
return buildPageSubtreeProjection({
|
||||
documentId,
|
||||
title: pageTitle,
|
||||
content,
|
||||
});
|
||||
}, [content, documentId, initialContent, initialPageSubtree, pageTitle, title]);
|
||||
return null;
|
||||
}, [
|
||||
content,
|
||||
pageTitle,
|
||||
serverContentSnapshot,
|
||||
serverPageSubtreeSnapshot,
|
||||
serverPageSubtreeTitle,
|
||||
]);
|
||||
const readViewTocEntries = useMemo(
|
||||
() =>
|
||||
pageSubtree.outline
|
||||
(pageSubtree?.outline ?? [])
|
||||
.filter((entry) => typeof entry.anchorBlockId === "string" && entry.anchorBlockId.trim())
|
||||
.map(({ anchorBlockId, level, numbering, title: entryTitle }) => ({
|
||||
id: anchorBlockId as string,
|
||||
|
||||
@@ -5,7 +5,6 @@ import type { CSSProperties, ReactNode } from "react";
|
||||
import { cn } from "@/lib/utils";
|
||||
import type { TocEntry } from "@/components/editor/document-toc";
|
||||
import {
|
||||
buildPageSubtreeProjection,
|
||||
clampHeadingLevel,
|
||||
extractPageBlocks,
|
||||
getInlineText,
|
||||
@@ -181,11 +180,26 @@ const buildHeadingNumberingMapFromOutline = (outline: PageOutlineEntry[]): Map<s
|
||||
export const extractReadViewBlocks = (content: unknown): PageSubtreeBlock[] => extractPageBlocks(content);
|
||||
|
||||
export const buildReadViewTocEntries = (blocks: PageSubtreeBlock[]): TocEntry[] => {
|
||||
return buildPageSubtreeProjection({
|
||||
documentId: "preview",
|
||||
title: "预览",
|
||||
content: blocks,
|
||||
}).outline.map(({ id, level, numbering, title }) => ({
|
||||
const counters = [0, 0, 0, 0, 0];
|
||||
return blocks.flatMap((block) => {
|
||||
if (block.type !== "heading") {
|
||||
return [];
|
||||
}
|
||||
const level = clampHeadingLevel(block.props?.level);
|
||||
counters[level - 1] += 1;
|
||||
for (let index = level; index < counters.length; index += 1) {
|
||||
counters[index] = 0;
|
||||
}
|
||||
return [{
|
||||
id: String(block.id ?? `preview-heading-${counters.join("-")}`),
|
||||
level,
|
||||
numbering: counters
|
||||
.slice(0, level)
|
||||
.filter((value) => value > 0)
|
||||
.join("."),
|
||||
title: getInlineText(block.content) || "未命名标题",
|
||||
}];
|
||||
}).map(({ id, level, numbering, title }) => ({
|
||||
id,
|
||||
level,
|
||||
numbering,
|
||||
@@ -583,22 +597,15 @@ function DocumentReadStructurePanel({ pageSubtree }: { pageSubtree: PageSubtreeP
|
||||
|
||||
export function DocumentReadView({ content, documentId, options, pageSubtree, className }: DocumentReadViewProps) {
|
||||
const blocks = extractReadViewBlocks(content);
|
||||
const resolvedPageSubtree =
|
||||
pageSubtree ??
|
||||
buildPageSubtreeProjection({
|
||||
documentId,
|
||||
title: null,
|
||||
content,
|
||||
});
|
||||
const headingNumberingById =
|
||||
resolvedPageSubtree.outline.length > 0
|
||||
? buildHeadingNumberingMapFromOutline(resolvedPageSubtree.outline)
|
||||
(pageSubtree?.outline.length ?? 0) > 0
|
||||
? buildHeadingNumberingMapFromOutline(pageSubtree?.outline ?? [])
|
||||
: buildHeadingNumberingMap(blocks);
|
||||
|
||||
if (blocks.length === 0) {
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{options.showStructure ? <DocumentReadStructurePanel pageSubtree={resolvedPageSubtree} /> : null}
|
||||
{options.showStructure && pageSubtree ? <DocumentReadStructurePanel pageSubtree={pageSubtree} /> : null}
|
||||
<div className="flex min-h-[40vh] items-center justify-center rounded-2xl border border-dashed border-[#e4e4e7] bg-[#fafafa] px-6 py-10 text-sm text-[#71717a]">
|
||||
页面暂无正文内容
|
||||
</div>
|
||||
@@ -608,7 +615,7 @@ export function DocumentReadView({ content, documentId, options, pageSubtree, cl
|
||||
|
||||
return (
|
||||
<div className={cn("space-y-4", className)}>
|
||||
{options.showStructure ? <DocumentReadStructurePanel pageSubtree={resolvedPageSubtree} /> : null}
|
||||
{options.showStructure && pageSubtree ? <DocumentReadStructurePanel pageSubtree={pageSubtree} /> : null}
|
||||
{renderBlocks(blocks, options, documentId, headingNumberingById, 0)}
|
||||
</div>
|
||||
);
|
||||
|
||||
@@ -0,0 +1,78 @@
|
||||
import { describe, expect, it } from "vitest";
|
||||
import { normalizeDocumentContentResponse } from "@/lib/documents/page-subtree-response";
|
||||
import type { PageSubtreeProjection } from "@/lib/documents/page-subtree";
|
||||
|
||||
describe("page subtree response helpers", () => {
|
||||
it("保留服务端返回的 pageSubtree", () => {
|
||||
const serverPageSubtree = {
|
||||
projectionId: "server_projection",
|
||||
projection: "page_tree",
|
||||
rootNodeId: "doc_1",
|
||||
rootNode: {
|
||||
id: "doc_1",
|
||||
parentNodeId: null,
|
||||
nodeType: "page",
|
||||
blockId: null,
|
||||
anchorBlockId: null,
|
||||
depth: 0,
|
||||
metadata: {
|
||||
title: "服务端标题",
|
||||
textSnippet: null,
|
||||
blockType: null,
|
||||
headingLevel: null,
|
||||
numbering: null,
|
||||
childCount: 0,
|
||||
order: 0,
|
||||
path: ["doc_1"],
|
||||
},
|
||||
},
|
||||
subtree: {
|
||||
rootNodeId: "doc_1",
|
||||
nodes: [],
|
||||
},
|
||||
outline: [],
|
||||
evidence: [],
|
||||
stats: {
|
||||
blockCount: 0,
|
||||
headingCount: 0,
|
||||
evidenceCount: 0,
|
||||
maxDepth: 0,
|
||||
},
|
||||
} satisfies PageSubtreeProjection;
|
||||
|
||||
const normalized = normalizeDocumentContentResponse({
|
||||
documentId: "doc_1",
|
||||
payload: {
|
||||
content: null,
|
||||
revision: 1,
|
||||
conflictDetectionKey: "doc_1:1",
|
||||
pageSubtree: serverPageSubtree,
|
||||
},
|
||||
});
|
||||
|
||||
expect(normalized.pageSubtree).toBe(serverPageSubtree);
|
||||
});
|
||||
|
||||
it("在服务端缺失 pageSubtree 时保留空值", () => {
|
||||
const normalized = normalizeDocumentContentResponse({
|
||||
documentId: "doc_1",
|
||||
title: "测试页面",
|
||||
payload: {
|
||||
content: [
|
||||
{
|
||||
id: "heading_1",
|
||||
type: "heading",
|
||||
props: { level: 1 },
|
||||
content: [{ type: "text", text: "章节一" }],
|
||||
},
|
||||
],
|
||||
revision: 3,
|
||||
conflictDetectionKey: "doc_1:3",
|
||||
},
|
||||
});
|
||||
|
||||
expect(normalized.revision).toBe(3);
|
||||
expect(normalized.conflictDetectionKey).toBe("doc_1:3");
|
||||
expect(normalized.pageSubtree).toBeNull();
|
||||
});
|
||||
});
|
||||
@@ -0,0 +1,48 @@
|
||||
import {
|
||||
type PageSubtreeProjection,
|
||||
} from "@/lib/documents/page-subtree";
|
||||
|
||||
export type DocumentContentResponseLike = {
|
||||
content?: unknown;
|
||||
revision?: number | null;
|
||||
conflict_detection_key?: string | null;
|
||||
conflictDetectionKey?: string | null;
|
||||
page_subtree?: PageSubtreeProjection | null;
|
||||
pageSubtree?: PageSubtreeProjection | null;
|
||||
title?: string | null;
|
||||
};
|
||||
|
||||
export type NormalizedDocumentContentResponse = {
|
||||
content: unknown;
|
||||
revision: number;
|
||||
conflictDetectionKey: string;
|
||||
pageSubtree: PageSubtreeProjection | null;
|
||||
};
|
||||
|
||||
export function normalizeDocumentContentResponse(input: {
|
||||
documentId: string;
|
||||
title?: string | null;
|
||||
payload?: DocumentContentResponseLike | null;
|
||||
}): NormalizedDocumentContentResponse {
|
||||
const payload = input.payload ?? null;
|
||||
const content = payload?.content ?? null;
|
||||
const revision =
|
||||
typeof payload?.revision === "number" && Number.isInteger(payload.revision)
|
||||
? payload.revision
|
||||
: 0;
|
||||
const conflictDetectionKey = (
|
||||
typeof payload?.conflictDetectionKey === "string" && payload.conflictDetectionKey.trim()
|
||||
? payload.conflictDetectionKey
|
||||
: typeof payload?.conflict_detection_key === "string" && payload.conflict_detection_key.trim()
|
||||
? payload.conflict_detection_key
|
||||
: `${input.documentId}:0`
|
||||
);
|
||||
const pageSubtree = payload?.pageSubtree ?? payload?.page_subtree ?? null;
|
||||
|
||||
return {
|
||||
content,
|
||||
revision,
|
||||
conflictDetectionKey,
|
||||
pageSubtree,
|
||||
};
|
||||
}
|
||||
@@ -73,6 +73,36 @@ const parseRuntimeBoolean = (value: unknown): boolean | undefined => {
|
||||
return undefined;
|
||||
};
|
||||
|
||||
function getServerNodeBuiltin<T>(moduleName: string): T | null {
|
||||
if (typeof window !== "undefined") {
|
||||
return null;
|
||||
}
|
||||
|
||||
const processWithBuiltin = process as NodeJS.Process & {
|
||||
getBuiltinModule?: (id: string) => unknown;
|
||||
};
|
||||
if (typeof processWithBuiltin.getBuiltinModule === "function") {
|
||||
const builtIn = processWithBuiltin.getBuiltinModule(moduleName);
|
||||
if (builtIn) {
|
||||
return builtIn as T;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
// 说明:某些 Node/打包环境仍保留 CommonJS require,这里只作为兜底。
|
||||
const runtimeRequire = new Function(
|
||||
'return typeof require === "function" ? require : null',
|
||||
)() as NodeJS.Require | null;
|
||||
if (runtimeRequire) {
|
||||
return runtimeRequire(moduleName) as T;
|
||||
}
|
||||
} catch {
|
||||
return null;
|
||||
}
|
||||
|
||||
return null;
|
||||
}
|
||||
|
||||
const readFromEnv = (): MnoteRuntimeConfig => ({
|
||||
useConvex: process.env.USE_CONVEX === "1" || process.env.NEXT_PUBLIC_USE_CONVEX === "1",
|
||||
supabaseUrl: process.env.NEXT_PUBLIC_SUPABASE_URL,
|
||||
@@ -111,12 +141,14 @@ const readFromPublicJson = (): Partial<MnoteRuntimeConfig> => {
|
||||
// 桌面端运行时 process.cwd() 会被 Electron 切到 desktop-next 根目录。
|
||||
// 网页端运行时 process.cwd() 通常为 wolai-frontend。
|
||||
|
||||
// 说明:这里不能直接写 `require("fs")` / `require("path")`,
|
||||
// 否则客户端 bundle 在解析该模块时会把它们也当成浏览器依赖,触发持续重编译或空白页。
|
||||
// 仅在服务端运行时通过惰性 require 读取本地 public/mnote-env.json。
|
||||
const runtimeRequire = new Function("return require")() as NodeJS.Require;
|
||||
const fs = runtimeRequire("node:fs") as typeof import("fs");
|
||||
const path = runtimeRequire("node:path") as typeof import("path");
|
||||
// 说明:这里不能直接静态引入 `node:fs` / `node:path`,
|
||||
// 否则客户端 bundle 在解析该模块时会把它们也当成浏览器依赖。
|
||||
// Node 22 优先走 `process.getBuiltinModule`,其余环境再兜底到 runtime require。
|
||||
const fs = getServerNodeBuiltin<typeof import("fs")>("node:fs");
|
||||
const path = getServerNodeBuiltin<typeof import("path")>("node:path");
|
||||
if (!fs || !path) {
|
||||
return {};
|
||||
}
|
||||
|
||||
// 说明:Next standalone 产物的 server.js 会执行 `process.chdir(__dirname)`,
|
||||
// 导致 process.cwd() 变成 `.next/standalone`,此时 public/mnote-env.json 位于上层目录。
|
||||
@@ -224,18 +256,28 @@ export const getMnoteRuntimeConfig = (): MnoteRuntimeConfig => {
|
||||
return normalizeRuntimeConfig(window.__MNOTE_RUNTIME_CONFIG__ ?? readFromEnv());
|
||||
}
|
||||
const isDesktop = process.env.MNOTE_DESKTOP === "1";
|
||||
const publicRuntime = readFromPublicJson();
|
||||
const envRuntime = readFromEnv();
|
||||
// 说明:桌面端需要优先使用 public/mnote-env.json 来覆盖 build 时注入的 NEXT_PUBLIC_*。
|
||||
// Web 端开发时则应优先使用环境变量(例如本机 http://127.0.0.1:18000),避免被
|
||||
// public/mnote-env.json 中的远程/自签地址覆盖导致浏览器登录请求失败。
|
||||
const merged: MnoteRuntimeConfig = isDesktop
|
||||
? {
|
||||
...readFromEnv(),
|
||||
...readFromPublicJson(),
|
||||
...envRuntime,
|
||||
...publicRuntime,
|
||||
isDesktop,
|
||||
}
|
||||
: {
|
||||
...readFromPublicJson(),
|
||||
...readFromEnv(),
|
||||
...publicRuntime,
|
||||
...envRuntime,
|
||||
// 说明:Rust Web 的 tree shell 属于运行期开关,必须允许 public/mnote-env.json
|
||||
// 在网页端覆盖环境变量;否则开发机上的旧 NEXT_PUBLIC_* 会把显式开关吃掉。
|
||||
...(publicRuntime.mnoteWebBaseUrl !== undefined
|
||||
? { mnoteWebBaseUrl: publicRuntime.mnoteWebBaseUrl }
|
||||
: {}),
|
||||
...(publicRuntime.mnoteWebTreeShellEnabled !== undefined
|
||||
? { mnoteWebTreeShellEnabled: publicRuntime.mnoteWebTreeShellEnabled }
|
||||
: {}),
|
||||
isDesktop,
|
||||
};
|
||||
return normalizeRuntimeConfig(merged);
|
||||
|
||||
Reference in New Issue
Block a user