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
@@ -74,6 +74,23 @@ export const mergeTiptapMarks = (...groups) => {
});
};
const legacyBlockAttrs = (block) => (
{
...(block?.attrs && typeof block.attrs === 'object' ? block.attrs : {}),
...(block?.props && typeof block.props === 'object' ? block.props : {}),
}
);
const legacyBlockAttr = (block, ...keys) => {
const props = block?.props && typeof block.props === 'object' ? block.props : {};
const attrs = block?.attrs && typeof block.attrs === 'object' ? block.attrs : {};
for (const key of keys) {
if (props[key] !== undefined) return props[key];
if (attrs[key] !== undefined) return attrs[key];
}
return undefined;
};
export const legacyInlineContentToTiptap = (value) => {
if (typeof value === 'string') return value ? [{ type: 'text', text: value }] : [];
if (Array.isArray(value)) return value.flatMap(legacyInlineContentToTiptap);
@@ -102,16 +119,17 @@ export const legacyInlineContentToTiptap = (value) => {
export const legacyBlockToTiptap = (block, index = 0) => {
const type = typeof block?.type === 'string' ? block.type : 'paragraph';
const blockAttrs = legacyBlockAttrs(block);
const blockId = typeof block?.id === 'string' && block.id.trim()
? block.id.trim()
: typeof block?.blockId === 'string' && block.blockId.trim()
? block.blockId.trim()
: `block-${index + 1}`;
const content = legacyInlineContentToTiptap(block?.content ?? block?.contentNodes);
const textAlign = typeof block?.props?.textAlign === 'string'
? block.props.textAlign
: typeof block?.props?.text_align === 'string'
? block.props.text_align
const textAlign = typeof legacyBlockAttr(block, 'textAlign') === 'string'
? legacyBlockAttr(block, 'textAlign')
: typeof legacyBlockAttr(block, 'text_align') === 'string'
? legacyBlockAttr(block, 'text_align')
: undefined;
const withTextAlign = (attrs = {}) => textAlign ? { ...attrs, textAlign } : attrs;
const nestedChildren = Array.isArray(block?.children)
@@ -130,24 +148,24 @@ export const legacyBlockToTiptap = (block, index = 0) => {
}],
});
if (type === 'heading') {
const level = Number(block?.props?.level || block?.level || 1) || 1;
const collapsed = typeof block?.props?.collapsed === 'boolean' ? { collapsed: block.props.collapsed } : {};
const level = Number(legacyBlockAttr(block, 'level', 'headingLevel') || block?.level || 1) || 1;
const collapsed = typeof legacyBlockAttr(block, 'collapsed') === 'boolean' ? { collapsed: legacyBlockAttr(block, 'collapsed') } : {};
return { type: 'heading', attrs: withTextAlign({ blockId, level: Math.max(1, Math.min(6, level)), ...collapsed }), content };
}
if (type === 'bulletListItem' || type === 'bullet_list_item') return withListChildren('listItem', 'bulletList');
if (type === 'numberedListItem' || type === 'numbered_list_item') return withListChildren('listItem', 'orderedList');
if (type === 'checkListItem' || type === 'advancedTodo' || type === 'todo') {
return withListChildren('taskItem', 'taskList', { checked: Boolean(block?.props?.checked) });
return withListChildren('taskItem', 'taskList', { checked: legacyBlockAttr(block, 'checked') === true });
}
if (type === 'quote' || type === 'blockquote') {
return { type: 'blockquote', attrs: withTextAlign({ blockId }), content: [{ type: 'paragraph', attrs: withTextAlign({ blockId }), content }] };
}
if (type === 'codeBlock' || type === 'code_block') {
return { type: 'codeBlock', attrs: withTextAlign({ blockId, language: block?.props?.language || null }), content };
return { type: 'codeBlock', attrs: withTextAlign({ blockId, language: legacyBlockAttr(block, 'language') || null }), content };
}
if (type === 'divider' || type === 'horizontalRule' || type === 'horizontal_rule') return { type: 'horizontalRule', attrs: { blockId } };
if (type === 'mindmap') {
const attrs = block?.attrs && typeof block.attrs === 'object' ? block.attrs : null;
const attrs = blockAttrs;
const data = block?.props?.data && typeof block.props.data === 'object' ? block.props.data : null;
const mindmapId = firstNonEmptyText(
block?.props?.mindmapId,