feat: land page aggregate and phase7 document ai mainline

- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
This commit is contained in:
lix-2026
2026-04-23 07:38:34 +08:00
parent 8353aea2f9
commit 41e958769e
93 changed files with 8778 additions and 2222 deletions
@@ -1,380 +0,0 @@
"use client";
import { useEffect, useMemo, useRef, useState } from "react";
import { getMnoteRuntimeConfig } from "@/lib/runtime-config";
import { ensureMnoteWebAuthCookie } from "@/lib/mnote-web-auth";
const TREE_SHELL_CHANNEL = "mnote-tree-shell-v1";
const TREE_SHELL_PATH = "/tree";
export type MnoteTreeShellMode = "page" | "picker" | "filetree";
type TreeShellMessage =
| {
channel?: string;
type?: string;
documentId?: string;
assetId?: string;
rowId?: string;
rowKind?: string;
x?: number;
y?: number;
target?: { documentId?: string };
payload?: {
documentId?: string;
assetId?: string;
rowId?: string;
rowKind?: string;
x?: number;
y?: number;
};
}
| null
| undefined;
type MnoteWebTreeShellProps = {
workspaceId: string | null;
activeDocumentId?: string;
actorId?: string | null;
mode?: MnoteTreeShellMode;
allowRootPick?: boolean;
excludeIds?: string[];
reloadToken?: number;
onNavigate: (documentId: string) => void;
onPick?: (documentId: string | null) => void;
onOpenAsset?: (assetId: string) => void;
onOpenContextMenu?: (args: { documentId: string; x: number; y: number }) => void;
onOpenFileTreeContextMenu?: (args: {
documentId?: string;
assetId?: string;
rowId?: string;
rowKind?: string;
x: number;
y: number;
}) => void;
onRefresh: () => Promise<void>;
fallback: React.ReactNode;
};
function buildShellUrl(
baseUrl: string,
workspaceId: string | null,
activeDocumentId: string | undefined,
actorId: string | null | undefined,
mode: MnoteTreeShellMode,
allowRootPick: boolean,
excludeIds: string[],
refreshKey: number,
reloadToken: number,
) {
const url = new URL(TREE_SHELL_PATH, `${baseUrl}/`);
if (workspaceId) {
url.searchParams.set("workspaceId", workspaceId);
}
if (activeDocumentId) {
url.searchParams.set("activeDocumentId", activeDocumentId);
}
if (actorId && actorId.trim()) {
url.searchParams.set("actorId", actorId.trim());
}
url.searchParams.set("mode", mode);
if (mode === "picker") {
url.searchParams.set("allowRootPick", allowRootPick ? "1" : "0");
if (excludeIds.length > 0) {
url.searchParams.set("excludeIds", excludeIds.join(","));
}
}
url.searchParams.set("host", "wolai-frontend");
url.searchParams.set("channel", TREE_SHELL_CHANNEL);
url.searchParams.set("v", `${refreshKey}-${reloadToken}`);
return url.toString();
}
function extractDocumentId(message: Exclude<TreeShellMessage, null | undefined>) {
const direct = typeof message.documentId === "string" ? message.documentId.trim() : "";
if (direct) return direct;
const fromTarget =
message.target && typeof message.target.documentId === "string"
? message.target.documentId.trim()
: "";
if (fromTarget) return fromTarget;
const fromPayload =
message.payload && typeof message.payload.documentId === "string"
? message.payload.documentId.trim()
: "";
return fromPayload;
}
function extractAssetId(message: Exclude<TreeShellMessage, null | undefined>) {
const direct = typeof message.assetId === "string" ? message.assetId.trim() : "";
if (direct) return direct;
const fromPayload =
message.payload && typeof message.payload.assetId === "string"
? message.payload.assetId.trim()
: "";
return fromPayload;
}
function extractCoordinate(
message: Exclude<TreeShellMessage, null | undefined>,
axis: "x" | "y",
) {
const direct = typeof message[axis] === "number" ? message[axis] : null;
if (typeof direct === "number" && Number.isFinite(direct)) return direct;
const fromPayload =
message.payload && typeof message.payload[axis] === "number"
? message.payload[axis]
: null;
if (typeof fromPayload === "number" && Number.isFinite(fromPayload)) return fromPayload;
return null;
}
function extractTextField(
message: Exclude<TreeShellMessage, null | undefined>,
field: "rowId" | "rowKind",
) {
const direct = typeof message[field] === "string" ? message[field].trim() : "";
if (direct) return direct;
const fromPayload =
message.payload && typeof message.payload[field] === "string"
? message.payload[field].trim()
: "";
return fromPayload;
}
export function MnoteWebTreeShell({
workspaceId,
activeDocumentId,
actorId,
mode = "page",
allowRootPick = false,
excludeIds = [],
reloadToken = 0,
onNavigate,
onPick,
onOpenAsset,
onOpenContextMenu,
onOpenFileTreeContextMenu,
onRefresh,
fallback,
}: MnoteWebTreeShellProps) {
const runtime = useMemo(() => getMnoteRuntimeConfig(), []);
const baseUrl = (runtime.mnoteWebBaseUrl ?? "").trim().replace(/\/+$/, "");
const treeShellEnabled = runtime.mnoteWebTreeShellEnabled === true;
const iframeRef = useRef<HTMLIFrameElement | null>(null);
const readyTimerRef = useRef<number | null>(null);
const [refreshKey, setRefreshKey] = useState(0);
const [failedShellUrl, setFailedShellUrl] = useState<string | null>(null);
const [authCookieReady, setAuthCookieReady] = useState(false);
const shellUrl = useMemo(() => {
if (!baseUrl || !treeShellEnabled) return null;
return buildShellUrl(
baseUrl,
workspaceId,
activeDocumentId,
actorId,
mode,
allowRootPick,
excludeIds,
refreshKey,
reloadToken,
);
}, [
activeDocumentId,
actorId,
allowRootPick,
baseUrl,
excludeIds,
mode,
reloadToken,
refreshKey,
treeShellEnabled,
workspaceId,
]);
useEffect(() => {
if (!shellUrl) {
setAuthCookieReady(false);
return;
}
let cancelled = false;
setAuthCookieReady(false);
setFailedShellUrl(null);
void (async () => {
try {
await ensureMnoteWebAuthCookie();
if (!cancelled) {
setAuthCookieReady(true);
}
} catch {
if (!cancelled) {
setFailedShellUrl(shellUrl);
}
}
})();
return () => {
cancelled = true;
};
}, [shellUrl]);
useEffect(() => {
if (!shellUrl || !authCookieReady) return;
if (readyTimerRef.current) {
window.clearTimeout(readyTimerRef.current);
}
// 说明:当前 shell 仍处于渐进接入期,若路由不存在或页面未按协议 ready,
// 前端应自动回退到旧 React 树,而不是让用户看到空白 iframe。
readyTimerRef.current = window.setTimeout(() => {
setFailedShellUrl(shellUrl);
}, 2500);
return () => {
if (readyTimerRef.current) {
window.clearTimeout(readyTimerRef.current);
readyTimerRef.current = null;
}
};
}, [authCookieReady, shellUrl]);
useEffect(() => {
if (!baseUrl || !treeShellEnabled) return;
const expectedOrigin = (() => {
try {
return new URL(baseUrl).origin;
} catch {
return "";
}
})();
const onMessage = (event: MessageEvent<TreeShellMessage>) => {
if (!expectedOrigin || event.origin !== expectedOrigin) return;
if (event.source !== iframeRef.current?.contentWindow) return;
const message = event.data;
if (!message || typeof message !== "object") return;
if (message.channel !== TREE_SHELL_CHANNEL) return;
const type = typeof message.type === "string" ? message.type.trim() : "";
if (!type) return;
if (type === "tree.ready" || type === "ready") {
if (readyTimerRef.current) {
window.clearTimeout(readyTimerRef.current);
readyTimerRef.current = null;
}
return;
}
if (type === "tree.navigate" || type === "navigate") {
const documentId = extractDocumentId(message);
if (documentId) {
onNavigate(documentId);
}
return;
}
if (type === "tree.pick" || type === "picker.pick") {
onPick?.(extractDocumentId(message) || null);
return;
}
if (type === "tree.pick.root" || type === "picker.pick.root") {
onPick?.(null);
return;
}
if (type === "tree.asset.open" || type === "filetree.asset.open") {
const assetId = extractAssetId(message);
if (assetId) {
onOpenAsset?.(assetId);
}
return;
}
if (type === "tree.context-menu" || type === "tree.page.context-menu") {
const documentId = extractDocumentId(message);
const x = extractCoordinate(message, "x");
const y = extractCoordinate(message, "y");
if (!documentId || x === null || y === null) {
return;
}
const iframeRect = iframeRef.current?.getBoundingClientRect();
if (!iframeRect) {
return;
}
onOpenContextMenu?.({
documentId,
x: iframeRect.left + x,
y: iframeRect.top + y,
});
return;
}
if (type === "tree.filetree.context-menu") {
const documentId = extractDocumentId(message) || undefined;
const assetId = extractAssetId(message) || undefined;
const rowId = extractTextField(message, "rowId") || undefined;
const rowKind = extractTextField(message, "rowKind") || undefined;
const x = extractCoordinate(message, "x");
const y = extractCoordinate(message, "y");
if (x === null || y === null) {
return;
}
const iframeRect = iframeRef.current?.getBoundingClientRect();
if (!iframeRect) {
return;
}
onOpenFileTreeContextMenu?.({
documentId,
assetId,
rowId,
rowKind,
x: iframeRect.left + x,
y: iframeRect.top + y,
});
return;
}
if (
type === "tree.node.created" ||
type === "tree.node.renamed" ||
type === "tree.subtree.moved" ||
type === "tree.refresh" ||
type === "refresh"
) {
void onRefresh().finally(() => {
setRefreshKey((value) => value + 1);
});
}
};
window.addEventListener("message", onMessage);
return () => window.removeEventListener("message", onMessage);
}, [baseUrl, onNavigate, onOpenAsset, onOpenContextMenu, onOpenFileTreeContextMenu, onPick, onRefresh, treeShellEnabled]);
if (!shellUrl || !authCookieReady || failedShellUrl === shellUrl) {
return <>{fallback}</>;
}
return (
<div className="h-full w-full min-w-0 overflow-hidden bg-white">
<iframe
ref={iframeRef}
title="mnote-web tree shell"
src={shellUrl}
className="h-full w-full border-0 bg-white"
loading="lazy"
onError={() => {
setFailedShellUrl(shellUrl);
}}
/>
</div>
);
}