feat: add evidence search and stabilize pdf previews

- add document evidence parsing/search/open routes, Hermes tool wiring, local index settings/status, and the document-evidence skill plus design notes
- fix PDF resource tabs by rendering PDFs inline with pdf.js canvases instead of iframe preview pages, release PDF documents on close, and document the fourth-PDF stall bug
- keep PDF preview at 2x rendering while removing the previous lazy-load/placeholder direction, and make dev:hot bind loopback defaults externally reachable

Verification:
- node --check rust/crates/mnote-web/browser/document-resource-tab-runtime.js
- node scripts/task-dev-hot-plan-test.js
- cargo test -p mnote-web --manifest-path rust/Cargo.toml pdf_preview_page_does_not_render_visible_toolbar
- cargo test -p mnote-web --manifest-path rust/Cargo.toml document_shell_returns_page_aggregate_snapshot
- cargo build -p mnote-web --manifest-path rust/Cargo.toml
- browser smoke: sequentially opened the four tea_seed_oil_cosmetic PDFs; fourth PDF rendered 15/15 canvases, iframeCount=0, browser errors=0
This commit is contained in:
lix-2026
2026-06-04 18:51:16 +08:00
parent 9f4b5c4d48
commit 627213dc01
51 changed files with 15960 additions and 1844 deletions
@@ -30,6 +30,14 @@ function documentUrl(root, relativePath) {
return url.toString();
}
function navigationUrl(root) {
const url = new URL(`${BASE_URL}/`);
url.searchParams.set("sourceKind", "local_folder");
url.searchParams.set("rootUri", fileUrl(root));
url.searchParams.set("treeView", "filetree");
return url.toString();
}
function writeWorkspaceManifest(root, ownerId) {
const metadataDir = path.join(root, ".mnote");
fs.mkdirSync(metadataDir, { recursive: true });
@@ -66,13 +74,31 @@ async function browserSearch(page, root, query) {
}, { rootUri: fileUrl(root), queryText: query });
}
async function browserRefreshLocalIndex(page, root) {
return page.evaluate(async ({ rootUri }) => {
const response = await fetch("/api/search/local-index/refresh", {
method: "POST",
headers: { "content-type": "application/json", accept: "application/json" },
body: JSON.stringify({
workspaceId: "local-ws:user_real:task452",
rootUri,
}),
});
const payload = await response.json().catch(() => null);
return {
status: response.status,
payload,
};
}, { rootUri: fileUrl(root) });
}
async function capturePanelDiagnostics(page) {
try {
return await page.evaluate(function() {
var bl = document.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]');
var tg = document.querySelector('[data-testid="wolai-page-settings-local-index-tags"]');
var st = document.querySelector('[data-testid="wolai-page-settings-local-index-status"]');
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
var popover = document.querySelector('[data-testid="mnote-local-index-settings-popover"]');
return {
backlinksHtml: bl ? bl.innerHTML : '(missing)',
tagsHtml: tg ? tg.innerHTML : '(missing)',
@@ -131,6 +157,9 @@ async function run() {
"utf8",
);
const refreshedBeforeSearch = await browserRefreshLocalIndex(page, root);
assert.equal(refreshedBeforeSearch.status, 200, `刷新本地索引应成功: ${JSON.stringify(refreshedBeforeSearch)}`);
debug.refreshedBeforeSearch = refreshedBeforeSearch.payload;
const first = await browserSearch(page, root, token);
assert.equal(first.status, 200, `初次搜索应成功: ${JSON.stringify(first)}`);
debug.first = first.payload;
@@ -140,6 +169,29 @@ async function run() {
`新建页面应立即可搜索: ${JSON.stringify(firstResults)}`,
);
await page.goto(navigationUrl(root), {
waitUntil: "domcontentloaded",
timeout: UI_TIMEOUT_MS,
});
await page.locator('[data-testid="mnote-local-index-settings-toggle"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-testid="mnote-local-index-settings-popover"]').waitFor({
state: "visible",
timeout: UI_TIMEOUT_MS,
});
debug.navigationIndexSettings = await page.evaluate(function() {
var popover = document.querySelector('[data-testid="mnote-local-index-settings-popover"]');
var ranges = document.querySelector('[data-testid="wolai-page-settings-local-index-ranges"]');
var pageSettingsIndexTab = document.querySelector('[data-page-settings-tab="index"]');
return {
path: window.location.pathname,
visible: Boolean(popover && !popover.hidden),
rangeInputs: ranges ? ranges.querySelectorAll('[data-local-index-range-input]').length : 0,
hasPageSettingsIndexTab: Boolean(pageSettingsIndexTab),
};
});
assert.equal(debug.navigationIndexSettings.visible, true, `导航页应能打开索引设置: ${JSON.stringify(debug.navigationIndexSettings)}`);
assert.equal(debug.navigationIndexSettings.hasPageSettingsIndexTab, false, `索引设置不应留在页面设置页签: ${JSON.stringify(debug.navigationIndexSettings)}`);
await page.goto(documentUrl(root, firstRelativePath), {
waitUntil: "domcontentloaded",
timeout: UI_TIMEOUT_MS,
@@ -162,8 +214,7 @@ async function run() {
if (msg.type() === 'error') { diagApiResponses._consoleErrors = (diagApiResponses._consoleErrors || []).concat([msg.text()]); }
});
await page.locator('[data-testid="wolai-page-settings-trigger"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-page-settings-tab="index"]').click({ timeout: UI_TIMEOUT_MS });
await page.locator('[data-testid="mnote-local-index-settings-toggle"]').click({ timeout: UI_TIMEOUT_MS });
// 等待本地索引面板渲染;超时时捕获 DOM 诊断再抛
try {
@@ -195,6 +246,9 @@ async function run() {
debug.diagApiResponses = diagApiResponses;
fs.renameSync(path.join(root, firstRelativePath), path.join(root, renamedRelativePath));
const refreshedAfterRename = await browserRefreshLocalIndex(page, root);
assert.equal(refreshedAfterRename.status, 200, `重命名后刷新本地索引应成功: ${JSON.stringify(refreshedAfterRename)}`);
debug.refreshedAfterRename = refreshedAfterRename.payload;
const second = await browserSearch(page, root, token);
assert.equal(second.status, 200, `重命名后搜索应成功: ${JSON.stringify(second)}`);
debug.second = second.payload;