收口 MNote P0 P1 P2 审查尾项

- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目
- 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线
- 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径

验证:
- cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1
- cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1
- git diff --check
- git diff --cached --check
- codegraph index . --force && codegraph status .
- codegraph sync . && codegraph status .
This commit is contained in:
lix-2026
2026-06-01 09:29:12 +08:00
parent 49a0545148
commit 1882db7681
143 changed files with 29810 additions and 3228 deletions
@@ -78,6 +78,32 @@ async function insertMindmapThroughSlash(page) {
});
}
async function assertMindmapStyleDrawerClosedByDefault(page) {
await page.locator('[data-testid="mindmap-rust-shell"]').first().waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
const state = await page.evaluate(() => {
const shell = document.querySelector('[data-testid="mindmap-rust-shell"]');
const sidebar = document.querySelector('[data-testid="mindmap-schema-sidebar"]');
const drawer = document.querySelector('[data-testid="mindmap-schema-sidebar-drawer"]');
return {
panelOpen: shell instanceof HTMLElement ? shell.getAttribute("data-sidebar-panel-open") || "" : "",
activePanel: shell instanceof HTMLElement ? shell.getAttribute("data-sidebar-active-panel") || "" : "",
sidebarPresent: sidebar instanceof HTMLElement,
sidebarPanelOpen: sidebar instanceof HTMLElement ? sidebar.getAttribute("data-panel-open") || "" : "",
drawerVisible: drawer instanceof HTMLElement && drawer.getClientRects().length > 0,
bodyText: document.body?.innerText || "",
};
});
assert.equal(state.panelOpen, "false", `节点样式抽屉不应默认打开: ${JSON.stringify(state)}`);
if (state.sidebarPanelOpen) {
assert.equal(state.sidebarPanelOpen, "false", `sidebar panel 状态应与 shell 保持默认收起: ${JSON.stringify(state)}`);
}
assert.equal(state.drawerVisible, false, `节点样式抽屉不应默认渲染遮挡画布: ${JSON.stringify(state)}`);
return state;
}
async function assertSlashMenuAnchorsAfterMindmap(page) {
const editor = page.locator('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror[contenteditable="true"]').first();
await editor.click({ timeout: UI_TIMEOUT_MS });
@@ -122,6 +148,134 @@ async function assertSlashMenuAnchorsAfterMindmap(page) {
return state;
}
async function resizeMindmapThroughCornerHandle(page) {
await page.locator('[data-testid="mindmap-resize-handle-nw"]').first().waitFor({
state: "attached",
timeout: UI_TIMEOUT_MS,
});
const before = await page.evaluate(() => {
const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]');
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
const handles = Array.from(document.querySelectorAll('[data-mnote-mindmap-resize-handle]'))
.map((handle) => handle.getAttribute("data-mnote-mindmap-resize-handle") || "")
.sort();
const rect = placeholder?.getBoundingClientRect();
const editorRect = editor?.getBoundingClientRect();
return {
handles,
rect: rect ? { width: Math.round(rect.width), height: Math.round(rect.height) } : null,
centerDelta: rect && editorRect ? Math.round((rect.left + rect.width / 2) - (editorRect.left + editorRect.width / 2)) : null,
cssMaxWidth: root instanceof HTMLElement ? getComputedStyle(root).getPropertyValue("--mnote-mindmap-block-max-width").trim() : "",
htmlCssMaxWidth: getComputedStyle(document.documentElement).getPropertyValue("--mnote-mindmap-block-max-width").trim(),
};
});
assert.deepEqual(before.handles, ["ne", "nw", "se", "sw"], `mindmap 应渲染四角 resize handle: ${JSON.stringify(before)}`);
assert(before.rect, `mindmap resize 前应能读取占位块尺寸: ${JSON.stringify(before)}`);
assert(Math.abs(before.centerDelta) <= 2, `初始 mindmap 应以正文列中心对齐: ${JSON.stringify(before)}`);
await page.waitForFunction(() => {
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
return Object.values(registry).some((bridge) => typeof bridge?.instance?.resize === "function");
}, null, { timeout: UI_TIMEOUT_MS });
await page.evaluate(() => {
window.__MNOTE_TEST_MINDMAP_RESIZE_COUNT = 0;
const registry = window.__MNOTE_LEPTOS_MINDMAP_BRIDGES__ || {};
Object.values(registry).forEach((bridge) => {
const instance = bridge?.instance;
if (!instance || typeof instance.resize !== "function" || instance.resize.__mnoteResizeCounterPatched) return;
const original = instance.resize.bind(instance);
const patched = function patchedMindmapResize() {
window.__MNOTE_TEST_MINDMAP_RESIZE_COUNT = Number(window.__MNOTE_TEST_MINDMAP_RESIZE_COUNT || 0) + 1;
return original();
};
patched.__mnoteResizeCounterPatched = true;
instance.resize = patched;
});
});
const handleBox = await page.locator('[data-testid="mindmap-resize-handle-nw"]').first().boundingBox();
assert(handleBox, "左上角 resize handle 应有可交互位置");
await page.mouse.move(handleBox.x + handleBox.width / 2, handleBox.y + handleBox.height / 2);
await page.mouse.down();
await page.mouse.move(handleBox.x + 160, handleBox.y + 110, { steps: 8 });
await page.mouse.up();
await page.waitForFunction(
({ previousWidth, previousHeight }) => {
const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]');
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
if (!(placeholder instanceof HTMLElement) || !(scene instanceof HTMLElement)) return false;
const width = Number(placeholder.dataset.mnoteMindmapWidth || 0);
const height = Number(placeholder.dataset.mnoteMindmapHeight || 0);
return width >= 320
&& height >= 240
&& width < previousWidth
&& height < previousHeight
&& scene.dataset.mnoteMindmapHeight === String(height);
},
{ previousWidth: before.rect.width, previousHeight: before.rect.height },
{ timeout: UI_TIMEOUT_MS },
);
const resized = await page.evaluate(() => {
const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]');
const scene = document.querySelector('[data-testid="leptos-mindmap-island"]');
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
const rect = placeholder?.getBoundingClientRect();
const editorRect = editor?.getBoundingClientRect();
return {
width: placeholder instanceof HTMLElement ? Number(placeholder.dataset.mnoteMindmapWidth || 0) : 0,
height: placeholder instanceof HTMLElement ? Number(placeholder.dataset.mnoteMindmapHeight || 0) : 0,
sceneHeight: scene instanceof HTMLElement ? Number(scene.dataset.mnoteMindmapHeight || 0) : 0,
rect: rect ? { width: Math.round(rect.width), height: Math.round(rect.height) } : null,
centerDelta: rect && editorRect ? Math.round((rect.left + rect.width / 2) - (editorRect.left + editorRect.width / 2)) : null,
cssMaxWidth: getComputedStyle(document.documentElement).getPropertyValue("--mnote-mindmap-block-max-width").trim(),
resizeCallCount: Number(window.__MNOTE_TEST_MINDMAP_RESIZE_COUNT || 0),
};
});
assert(Math.abs(resized.centerDelta) <= 2, `resize 后 mindmap 仍应以正文列中心对齐: ${JSON.stringify(resized)}`);
assert(resized.resizeCallCount <= 3, `拖动过程中不应连续触发 simple-mind-map resize 重绘: ${JSON.stringify(resized)}`);
const afterGlobalWidthPreference = await page.evaluate(() => {
const setMindmapWidthMax = (value) => {
document.documentElement.style.setProperty("--mnote-mindmap-block-max-width", value);
document.querySelector('.document-shell')?.style.setProperty("--mnote-mindmap-block-max-width", value);
document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]')?.style.setProperty("--mnote-mindmap-block-max-width", value);
window.dispatchEvent(new CustomEvent("mnote:page-width-preference-changed", { detail: { type: "mindmap" } }));
};
setMindmapWidthMax("720px");
return true;
});
assert.equal(afterGlobalWidthPreference, true);
await page.waitForFunction(() => {
const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]');
const rect = placeholder?.getBoundingClientRect();
return placeholder instanceof HTMLElement
&& !placeholder.dataset.mnoteMindmapWidth
&& rect
&& Math.round(rect.width) >= 900
&& Math.round(rect.width) <= 920;
}, null, { timeout: UI_TIMEOUT_MS });
const globalWidthState = await page.evaluate(() => {
const placeholder = document.querySelector('[data-testid="mnote-mindmap-placeholder"]');
const editor = document.querySelector('.document-pane[data-pane-role="primary"] .editor-surface .ProseMirror');
const rect = placeholder?.getBoundingClientRect();
const editorRect = editor?.getBoundingClientRect();
return {
widthAttr: placeholder instanceof HTMLElement ? placeholder.dataset.mnoteMindmapWidth || "" : "",
heightAttr: placeholder instanceof HTMLElement ? placeholder.dataset.mnoteMindmapHeight || "" : "",
rect: rect ? { width: Math.round(rect.width), height: Math.round(rect.height) } : null,
centerDelta: rect && editorRect ? Math.round((rect.left + rect.width / 2) - (editorRect.left + editorRect.width / 2)) : null,
cssMaxWidth: getComputedStyle(document.documentElement).getPropertyValue("--mnote-mindmap-block-max-width").trim(),
};
});
assert.equal(globalWidthState.widthAttr, "", `全局 Mindmap 宽度设置后应清除本块手动宽度: ${JSON.stringify(globalWidthState)}`);
assert(Math.abs(globalWidthState.centerDelta) <= 2, `全局 Mindmap 宽度设置后仍应居中: ${JSON.stringify(globalWidthState)}`);
return { ...resized, globalWidthState };
}
async function readState(page) {
return page.evaluate(() => {
const editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
@@ -184,7 +338,9 @@ async function waitForMindmapId(page) {
await page.waitForFunction(
() => {
const root = document.querySelector('[data-testid="mnote-mindmap-editor-root"]');
return root instanceof HTMLElement && /^思维导图\d{6}\.json$/.test(root.dataset.mnoteMindmapId || "");
const mindmapId = root instanceof HTMLElement ? String(root.dataset.mnoteMindmapId || "") : "";
return /^思维导图\d{6}\.json$/.test(mindmapId)
|| /^mindmap[-_][^/\\]+(?:\.json)?$/.test(mindmapId);
},
null,
{ timeout: UI_TIMEOUT_MS },
@@ -193,6 +349,11 @@ async function waitForMindmapId(page) {
return state.mindmapId;
}
function localMindmapFileName(mindmapId) {
const value = String(mindmapId || "").trim();
return value.toLowerCase().endsWith(".json") ? value : `${value}.json`;
}
function rowMatchesMindmap(row, mindmapId) {
const assetId = String(row && row.assetId || "");
const title = String(row && row.title || "");
@@ -312,6 +473,7 @@ async function main() {
markdownMissingMindmapReferenceAfterInsert: false,
refreshSkippedBecauseMarkdownNotSaved: false,
commandResponseSummary: null,
resizeAfterInsert: null,
};
try {
@@ -319,13 +481,16 @@ async function main() {
result.screenshots.push(await screenshot(page, "01-open-clean-page"));
await insertMindmapThroughSlash(page);
result.mindmapId = await waitForMindmapId(page);
const mindmapFileName = localMindmapFileName(result.mindmapId);
result.screenshots.push(await screenshot(page, "02-after-insert-mindmap"));
result.defaultStyleDrawer = await assertMindmapStyleDrawerClosedByDefault(page);
result.resizeAfterInsert = await resizeMindmapThroughCornerHandle(page);
await page.waitForFunction(
({ expected }) => {
const tree = document.getElementById("sidebar-file-tree-root");
return (tree?.textContent || "").includes(expected);
},
{ expected: result.mindmapId },
{ expected: mindmapFileName },
{ timeout: UI_TIMEOUT_MS },
);
result.saveAfterInsert = await waitForStableEditorSave(page, networkRecords, "after-insert");
@@ -333,12 +498,12 @@ async function main() {
result.diskAfterInsert = fs.readdirSync(pageDir).sort();
assert(result.diskAfterInsert.includes("CleanPage.md"), `页面 Markdown 应存在: ${result.diskAfterInsert.join(",")}`);
assert(result.diskAfterInsert.includes(result.mindmapId), `mindmap 应直接出现在页面文件夹下: ${result.diskAfterInsert.join(",")}`);
assert(!fs.existsSync(path.join(root, result.mindmapId)), "root 同级不应残留 mindmap 文件");
assert(!fs.existsSync(path.join(pageDir, "assets", result.mindmapId)), "assets 下不应残留 mindmap 文件");
assert(result.diskAfterInsert.includes(mindmapFileName), `mindmap 应直接出现在页面文件夹下: ${result.diskAfterInsert.join(",")}`);
assert(!fs.existsSync(path.join(root, mindmapFileName)), "root 同级不应残留 mindmap 文件");
assert(!fs.existsSync(path.join(pageDir, "assets", mindmapFileName)), "assets 下不应残留 mindmap 文件");
result.markdown = fs.readFileSync(markdownPath, "utf8");
result.markdownMissingMindmapReferenceAfterInsert = !result.markdown.includes(`](${result.mindmapId})`);
result.markdownMissingMindmapReferenceAfterInsert = !result.markdown.includes(`](${mindmapFileName})`);
result.samples = await sampleFileTree(page, result.mindmapId, 4_000);
result.commandResponseSummary = await postMindmapCommand(page, documentId, result.mindmapId);
@@ -355,7 +520,7 @@ async function main() {
timeout: UI_TIMEOUT_MS,
});
const afterRefresh = await readState(page);
assert.equal(afterRefresh.mindmapId, result.mindmapId, `刷新后 mindmapId 应保持不变: ${JSON.stringify(afterRefresh)}`);
assert.equal(localMindmapFileName(afterRefresh.mindmapId), mindmapFileName, `刷新后 mindmapId 应保持不变: ${JSON.stringify(afterRefresh)}`);
assert(
afterRefresh.mindmapRows.filter((row) => rowMatchesMindmap(row, result.mindmapId)).length === 1,
`刷新后文件树应只有本轮 mindmap 一行: ${JSON.stringify(afterRefresh.mindmapRows)}`,