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
@@ -200,17 +200,25 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
});
const applyPageOptionsToShell = (...args) => sidebarPageSettings.applyPageOptionsToShell(...args);
const closePageHistoryDrawer = (...args) => sidebarPageSettings.closePageHistoryDrawer(...args);
const closeAllSettingsPopovers = (...args) => sidebarPageSettings.closeAllSettingsPopovers(...args);
const closePageSettingsPopover = (...args) => sidebarPageSettings.closePageSettingsPopover(...args);
const closePageShareDialog = (...args) => sidebarPageSettings.closePageShareDialog(...args);
const currentPageOptions = (...args) => sidebarPageSettings.currentPageOptions(...args);
const ensureHistorySnapshotsSeeded = (...args) => sidebarPageSettings.ensureHistorySnapshotsSeeded(...args);
const isAnySettingsOpen = (...args) => sidebarPageSettings.isAnySettingsOpen(...args);
const isPageSettingsOpen = (...args) => sidebarPageSettings.isPageSettingsOpen(...args);
const openPageHistoryDrawer = (...args) => sidebarPageSettings.openPageHistoryDrawer(...args);
const openPageIndexSettingsPopover = (...args) => sidebarPageSettings.openPageIndexSettingsPopover(...args);
const openLocalOcrSettingsPopover = (...args) => sidebarPageSettings.openLocalOcrSettingsPopover(...args);
const openPageShareDialog = (...args) => sidebarPageSettings.openPageShareDialog(...args);
const pageOptionIsSupported = (...args) => sidebarPageSettings.pageOptionIsSupported(...args);
const addLocalIndexRange = (...args) => sidebarPageSettings.addLocalIndexRange(...args);
const persistLocalOcrAutoPreference = (...args) => sidebarPageSettings.persistLocalOcrAutoPreference(...args);
const persistLocalIndexSettings = (...args) => sidebarPageSettings.persistLocalIndexSettings(...args);
const persistPageOptionsPatch = (...args) => sidebarPageSettings.persistPageOptionsPatch(...args);
const persistPageWidthPreference = (...args) => sidebarPageSettings.persistPageWidthPreference(...args);
const refreshLocalIndex = (...args) => sidebarPageSettings.refreshLocalIndex(...args);
const removeLocalIndexRange = (...args) => sidebarPageSettings.removeLocalIndexRange(...args);
const recordPageHistorySnapshot = (...args) => sidebarPageSettings.recordPageHistorySnapshot(...args);
const renderPageSettingsPopover = (...args) => sidebarPageSettings.renderPageSettingsPopover(...args);
const setActivePageSettingsTab = (...args) => sidebarPageSettings.setActivePageSettingsTab(...args);
@@ -351,6 +359,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
const applyRemoveDocumentDelta = (...args) => sidebarTreeLiveApply.applyRemoveDocumentDelta(...args);
const setTreeLiveApplyError = (...args) => sidebarTreeLiveApply.setTreeLiveApplyError(...args);
const objectIdentityAttr = (...args) => sidebarTreeLiveApply.objectIdentityAttr(...args);
const renderPageProjection = (...args) => sidebarTreeLiveApply.renderPageProjection(...args);
const renderSidebarSnapshot = (...args) => sidebarTreeLiveApply.renderSidebarSnapshot(...args);
const refreshLocalFolderSidebarSnapshot = (...args) => sidebarTreeLiveApply.refreshLocalFolderSidebarSnapshot(...args);
const startLocalFolderSidebarWatch = (...args) => sidebarTreeLiveApply.startLocalFolderSidebarWatch(...args);
@@ -432,6 +441,21 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return true;
}
function openCurrentNavigationPage(trigger) {
if (currentSourceKind() === 'local_folder') {
var rootUri = currentRootUri();
var scope = currentFileTreeScope();
var workspaceId = currentWorkspaceId() || sidebarShortcutWorkspaceId() || resolveWorkspaceId(trigger || document.body);
var title = scope ? scope.split('/').filter(Boolean).pop() : '本地文件夹';
return openNavigationPageForFolder(rootUri, scope, workspaceId, title || scope || '本地文件夹');
}
var targetUrl = new URL('/', window.location.origin);
var workspaceId = currentWorkspaceId() || resolveWorkspaceId(trigger || document.body);
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
window.location.assign(targetUrl.pathname + targetUrl.search);
return true;
}
function currentTopbarTitle() {
var title = document.querySelector('[data-page-title-current="true"]');
return title && title.textContent ? title.textContent.trim() : '无标题';
@@ -821,9 +845,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
var sidebarPayload = await sidebarResponse.json().catch(function() { return null; });
if (!sidebarResponse.ok) throw new Error(sidebarPayload && (sidebarPayload.error || sidebarPayload.message) || 'local_page_tree_failed_' + sidebarResponse.status);
var sidebarProjection = sidebarPayload && (sidebarPayload.result || sidebarPayload) || {};
var renderedPage = renderSidebarSnapshot(Object.assign({}, sidebarProjection, {
dataset: Object.assign({}, sidebarProjection.dataset || {})
}));
var renderedPage = sidebarProjection ? await renderPageProjection(sidebarProjection) : false;
root = document.getElementById('sidebar-file-tree-root');
if (root instanceof HTMLElement) {
root.setAttribute('data-workspace-id', workspaceId);
@@ -2198,6 +2220,83 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
escapeHtml(cleanTitle.slice(index + cleanQuery.length));
}
function searchResultEvidenceLocator(item) {
if (item && item.evidence && item.evidence.source && typeof item.evidence.source === 'object') return item.evidence.source;
if (item && item.source && item.source.locator && typeof item.source.locator === 'object') return item.source.locator;
if (item && Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].source && typeof item.evidence[0].source === 'object') return item.evidence[0].source;
return null;
}
function evidenceLocatorResourceKind(locator, fallback) {
return searchText(locator && (locator.resourceKind || locator.resource_kind) || fallback || '').toLowerCase();
}
function evidenceLocatorBbox(locator) {
var bbox = locator && locator.bbox;
if (Array.isArray(bbox)) return bbox.slice(0, 4).join(',');
if (bbox && typeof bbox === 'object') return [bbox.x0, bbox.y0, bbox.x1, bbox.y1].join(',');
return '';
}
function evidenceLocatorOpenUrl(locator) {
var action = locator && locator.openAction && typeof locator.openAction === 'object' ? locator.openAction : null;
return searchText(action && action.url);
}
async function openEvidenceSearchResult(item, event) {
var locator = searchResultEvidenceLocator(item);
if (!locator) {
var fallbackId = searchText(item && (item.documentId || item.nodeId || item.id));
if (fallbackId) window.location.assign('/documents/' + encodeURIComponent(fallbackId));
return;
}
var resourcePath = searchText(locator.resourcePath || locator.resource_path || locator.ownerDocumentPath || locator.owner_document_path);
var ownerDocumentId = searchText(locator.ownerDocumentId || locator.owner_document_id || item.documentId || '');
var resourceKind = evidenceLocatorResourceKind(locator, item && item.resourceType);
var openTarget = event && event.altKey ? 'side' : 'active-tab';
if (resourcePath && resourceKind && resourceKind !== 'markdown') {
var fileName = resourcePath.split('/').filter(Boolean).pop() || resourcePath;
var localFileUrl = buildLocalFileOpenUrl(resourcePath, false);
var href = resourceKind === 'pdf' ? buildPdfPreviewOpenUrl(localFileUrl, fileName) : localFileUrl;
await openLocalResourceInActiveTab({
path: resourcePath,
title: fileName,
kind: resourceKind,
href: href,
assetId: 'local-file:' + resourcePath,
documentId: ownerDocumentId || currentDocumentId() || '',
ownerDocumentId: ownerDocumentId || currentDocumentId() || '',
workspaceId: resolveWorkspaceId(document.body) || '',
sourceKind: currentSourceKind() || 'local_folder',
rootUri: searchText(locator.rootUri || locator.root_uri || currentRootUri()),
paneRole: openTarget === 'side' ? 'secondary' : 'primary',
evidenceLocator: locator,
page: locator.page,
bbox: locator.bbox,
sourceMapPath: searchText(locator.sourceMapPath || locator.source_map_path),
blockId: searchText(locator.blockId || locator.block_id),
lineRange: locator.lineRange || locator.line_range || null,
charRange: locator.charRange || locator.char_range || null
});
closeSearchModal();
return;
}
var url = evidenceLocatorOpenUrl(locator) || ('/documents/' + encodeURIComponent(ownerDocumentId || currentDocumentId() || ''));
try {
var target = new URL(url, window.location.origin);
var blockId = searchText(locator.blockId || locator.block_id);
var sourceMapPath = searchText(locator.sourceMapPath || locator.source_map_path);
if (blockId) target.searchParams.set('blockId', blockId);
if (locator.page) target.searchParams.set('page', String(locator.page));
var bbox = evidenceLocatorBbox(locator);
if (bbox) target.searchParams.set('bbox', bbox);
if (sourceMapPath) target.searchParams.set('sourceMapPath', sourceMapPath);
window.location.assign(target.pathname + target.search + target.hash);
} catch (_) {
window.location.assign(url);
}
}
function renderSearchRecentState(overlay) {
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
@@ -2246,7 +2345,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
filters: {
titleOnly: searchSwitchValue(overlay, 'title'),
exact: searchSwitchValue(overlay, 'exact'),
includeOcr: false,
includeOcr: true,
onlyCurrentPage: searchSwitchValue(overlay, 'page'),
timeRange: 'any',
timeField: 'updated'
@@ -2263,16 +2362,20 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
}
results.innerHTML = items.map(function(item) {
var title = searchText(item.title || item.name || item.documentTitle || '无标题');
var snippet = searchText(item.snippet || (Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].snippet) || '');
var snippet = searchText(item.snippet || item.evidence && item.evidence.quote || (Array.isArray(item.evidence) && item.evidence[0] && (item.evidence[0].quote || item.evidence[0].snippet)) || '');
var path = searchText(item.path || item.parentTitle || item.workspaceName || currentWorkspaceName());
var resourceType = searchText(item.resourceType || item.resourceKind || 'page');
return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="rust-kernel" data-document-id="' + escapeHtml(item.documentId || item.nodeId || item.id || '') + '" data-resource-type="' + escapeHtml(resourceType) + '">' +
var locator = searchResultEvidenceLocator(item);
var locatorAttr = locator ? escapeHtml(JSON.stringify(locator)) : '';
var index = items.indexOf(item);
return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="rust-kernel" data-search-result-index="' + String(index) + '" data-document-id="' + escapeHtml(item.documentId || item.nodeId || item.id || '') + '" data-resource-type="' + escapeHtml(resourceType) + '"' + (locatorAttr ? ' data-evidence-locator="' + locatorAttr + '"' : '') + '>' +
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="article" aria-hidden="true"></span>' +
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(title, query) + '</span>' +
'<span class="wolai-search-result-snippet">' + (snippet ? highlightedHtml(snippet) : escapeHtml(path)) + '</span>' +
'<span class="wolai-search-result-path"><span>' + escapeHtml(path) + '</span><span class="wolai-search-result-type">' + escapeHtml(resourceType) + '</span></span></span>' +
'</button>';
}).join('');
window.__mnoteSearchResults = items;
} catch (error) {
if (requestId !== activeSearchRequestId) return;
meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
@@ -2473,9 +2576,30 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return;
}
var indexSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-index-settings"]');
if (indexSettingsTrigger) {
e.preventDefault();
openPageIndexSettingsPopover();
return;
}
var ocrSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-ocr-settings"], [data-mnote-action="toggle-ocr-tasks"]');
if (ocrSettingsTrigger) {
e.preventDefault();
openLocalOcrSettingsPopover();
return;
}
var settingsClose = closestAction(e.target, '[data-settings-action="close"]');
if (settingsClose) {
e.preventDefault();
closeAllSettingsPopovers();
return;
}
var pageSettingsPanel = closestAction(e.target, '.wolai-page-settings-panel');
if (isPageSettingsOpen() && !pageSettingsPanel) {
closePageSettingsPopover();
if (isAnySettingsOpen() && !pageSettingsPanel) {
closeAllSettingsPopovers();
}
var pageSettingsTab = closestAction(e.target, '[data-page-settings-tab]');
@@ -2485,6 +2609,39 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return;
}
var localIndexAction = closestAction(e.target, '[data-local-index-action]');
if (localIndexAction) {
e.preventDefault();
var localIndexActionName = localIndexAction.getAttribute('data-local-index-action') || '';
if (localIndexActionName === 'save') {
void persistLocalIndexSettings();
return;
}
if (localIndexActionName === 'refresh') {
void refreshLocalIndex();
return;
}
if (localIndexActionName === 'add-path') {
addLocalIndexRange();
return;
}
if (localIndexActionName === 'remove-path') {
removeLocalIndexRange(localIndexAction.getAttribute('data-local-index-path-index') || '0');
return;
}
}
var localOcrSettingsAction = closestAction(e.target, '[data-local-ocr-settings-action]');
if (localOcrSettingsAction) {
e.preventDefault();
var localOcrSettingsActionName = localOcrSettingsAction.getAttribute('data-local-ocr-settings-action') || '';
closeAllSettingsPopovers();
window.dispatchEvent(new CustomEvent('mnote:local-ocr-settings-action', {
detail: { action: localOcrSettingsActionName }
}));
return;
}
var pageSettingsAction = closestAction(e.target, '[data-page-settings-action]');
if (pageSettingsAction) {
e.preventDefault();
@@ -2507,6 +2664,13 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return;
}
var navigationPageTrigger = closestAction(e.target, '[data-mnote-action="open-navigation-page"]');
if (navigationPageTrigger) {
e.preventDefault();
openCurrentNavigationPage(navigationPageTrigger);
return;
}
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
if (searchTrigger) {
e.preventDefault();
@@ -2518,6 +2682,24 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
closeSearchModal();
return;
}
var searchResultRow = closestAction(e.target, '[data-testid="wolai-search-result-row"]');
if (searchResultRow) {
e.preventDefault();
var index = Number(searchResultRow.getAttribute('data-search-result-index') || -1);
var item = Array.isArray(window.__mnoteSearchResults) && index >= 0 ? window.__mnoteSearchResults[index] : null;
if (!item) {
var locatorPayload = searchResultRow.getAttribute('data-evidence-locator') || '';
try {
var locator = locatorPayload ? JSON.parse(locatorPayload) : null;
item = locator ? { evidence: { source: locator }, documentId: searchResultRow.getAttribute('data-document-id') || '' } : null;
} catch (_) {
item = null;
}
}
void openEvidenceSearchResult(item || { documentId: searchResultRow.getAttribute('data-document-id') || '' }, e)
.catch(function(error) { console.warn('mnote evidence 搜索结果打开失败', error); });
return;
}
var sourceMenuTrigger = closestAction(e.target, '[data-mnote-action="open-workspace-source-menu"]');
if (sourceMenuTrigger) {
@@ -2847,9 +3029,9 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
}
}
}
if (event.key === 'Escape' && isPageSettingsOpen()) {
if (event.key === 'Escape' && isAnySettingsOpen()) {
event.preventDefault();
closePageSettingsPopover();
closeAllSettingsPopovers();
return;
}
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {