chore: 收口 review 执行清单与 runtime 验证
- 补齐 design/10-review 执行清单、验收标准与相关设计治理记录 - 迁移已完成的 tree、mindmap、runtime fallback、AI kernel 等设计和缺陷条目 - 推进 Rust Web runtime、tree/sidebar、page aggregate、mindmap 与 OnlyOffice 路由侧验证支撑 - 增加 task177-task180 smoke/audit 脚本及前端相关测试覆盖
This commit is contained in:
@@ -77,6 +77,146 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
|
||||
}
|
||||
|
||||
function textFromUnknown(value) {
|
||||
if (value == null) return '';
|
||||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||||
if (Array.isArray(value)) return value.map(textFromUnknown).filter(Boolean).join(' ');
|
||||
if (typeof value !== 'object') return '';
|
||||
var parts = [];
|
||||
['text', 'title', 'content', 'children', 'blocks', 'body'].forEach(function(key) {
|
||||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||||
var text = textFromUnknown(value[key]);
|
||||
if (text) parts.push(text);
|
||||
}
|
||||
});
|
||||
return parts.join(' ');
|
||||
}
|
||||
|
||||
function readLocalEditorBlocks() {
|
||||
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
|
||||
if (!(editor instanceof HTMLElement)) return [];
|
||||
return Array.from(editor.children).filter(function(node) {
|
||||
return node instanceof HTMLElement;
|
||||
}).map(function(node, index) {
|
||||
var tag = String(node.tagName || '').toUpperCase();
|
||||
var headingMatch = tag.match(/^H([1-6])$/);
|
||||
var type = headingMatch ? 'heading' : 'paragraph';
|
||||
var text = searchText(node.textContent || '');
|
||||
var id = searchText(node.getAttribute('data-id') || node.id || 'local-block-' + index);
|
||||
var props = headingMatch ? { level: Number(headingMatch[1]) } : {};
|
||||
return { id: id, type: type, props: props, content: text };
|
||||
}).filter(function(block) {
|
||||
return block.content || block.type === 'heading';
|
||||
});
|
||||
}
|
||||
|
||||
function buildPageAiLocalSubtree(blocks, title) {
|
||||
var documentId = currentDocumentId() || 'current-page';
|
||||
var rootNodeId = 'page:' + documentId;
|
||||
var headingCounters = [0, 0, 0, 0, 0, 0];
|
||||
var headingStack = [];
|
||||
var nodes = [{
|
||||
id: rootNodeId,
|
||||
nodeId: rootNodeId,
|
||||
nodeType: 'page',
|
||||
blockId: null,
|
||||
blockType: 'page',
|
||||
title: title || '',
|
||||
parentNodeId: null,
|
||||
headingLevel: null
|
||||
}];
|
||||
var outline = [];
|
||||
var evidence = [];
|
||||
blocks.forEach(function(block, index) {
|
||||
var level = block.type === 'heading' ? Math.max(1, Math.min(6, Number(block.props && block.props.level || 1))) : null;
|
||||
if (level != null) {
|
||||
while (headingStack.length && headingStack[headingStack.length - 1].level >= level) headingStack.pop();
|
||||
}
|
||||
var parentNodeId = headingStack.length ? headingStack[headingStack.length - 1].nodeId : rootNodeId;
|
||||
var nodeId = 'local-node:' + String(block.id || index);
|
||||
var titleText = block.content || (block.type === 'heading' ? '未命名标题' : '');
|
||||
nodes.push({
|
||||
id: nodeId,
|
||||
nodeId: nodeId,
|
||||
nodeType: 'block',
|
||||
blockId: block.id,
|
||||
blockType: block.type,
|
||||
title: titleText,
|
||||
parentNodeId: parentNodeId,
|
||||
headingLevel: level
|
||||
});
|
||||
if (level != null) {
|
||||
headingCounters[level - 1] += 1;
|
||||
for (var i = level; i < headingCounters.length; i += 1) headingCounters[i] = 0;
|
||||
var numbering = headingCounters.slice(0, level).filter(function(count) { return count > 0; }).join('.');
|
||||
outline.push({
|
||||
id: 'local-outline:' + String(block.id || index),
|
||||
nodeId: nodeId,
|
||||
anchorBlockId: block.id,
|
||||
level: level,
|
||||
title: titleText,
|
||||
numbering: numbering
|
||||
});
|
||||
headingStack.push({ level: level, nodeId: nodeId });
|
||||
}
|
||||
if (titleText) {
|
||||
evidence.push({
|
||||
id: 'local-evidence:' + String(block.id || index),
|
||||
nodeId: nodeId,
|
||||
anchorBlockId: block.id,
|
||||
text: titleText,
|
||||
kind: block.type
|
||||
});
|
||||
}
|
||||
});
|
||||
return {
|
||||
projectionId: 'local-editor-dom:' + documentId,
|
||||
rootNode: {
|
||||
id: rootNodeId,
|
||||
documentId: documentId,
|
||||
title: title || '',
|
||||
nodeType: 'page'
|
||||
},
|
||||
subtree: {
|
||||
rootNodeId: rootNodeId,
|
||||
nodes: nodes
|
||||
},
|
||||
outline: outline,
|
||||
evidence: evidence,
|
||||
stats: {
|
||||
nodeCount: nodes.length,
|
||||
headingCount: outline.length,
|
||||
evidenceCount: evidence.length
|
||||
},
|
||||
source: 'local'
|
||||
};
|
||||
}
|
||||
|
||||
function currentPageAiContextSnapshot() {
|
||||
var aggregate = currentPageAggregate();
|
||||
var body = aggregate.body || {};
|
||||
var serverContent = body.content || null;
|
||||
var serverText = searchText(textFromUnknown(serverContent));
|
||||
var localBlocks = readLocalEditorBlocks();
|
||||
var localText = searchText(localBlocks.map(function(block) { return block.content || ''; }).join(' '));
|
||||
var serverSubtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
|
||||
var title = aggregate.head && aggregate.head.title ? aggregate.head.title : '';
|
||||
if (localBlocks.length && localText && localText !== serverText) {
|
||||
return {
|
||||
aggregate: aggregate,
|
||||
body: Object.assign({}, body, { content: localBlocks }),
|
||||
subtree: buildPageAiLocalSubtree(localBlocks, title),
|
||||
pageSubtreeSource: 'local'
|
||||
};
|
||||
}
|
||||
return {
|
||||
aggregate: aggregate,
|
||||
body: body,
|
||||
subtree: serverSubtree,
|
||||
pageSubtreeSource: serverSubtree ? 'server' : 'none'
|
||||
};
|
||||
}
|
||||
|
||||
function defaultPageOptions() {
|
||||
return {
|
||||
wideLayout: false,
|
||||
@@ -844,6 +984,90 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
});
|
||||
}
|
||||
|
||||
function documentIdFromDelta(data) {
|
||||
return String(data && (data.documentId || data.id || (data.document && data.document.id) || (data.node && data.node.id) || (data.args && data.args.documentId)) || '').trim();
|
||||
}
|
||||
|
||||
function parentIdFromDelta(data) {
|
||||
if (!data || typeof data !== 'object') return null;
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'parentId')) return data.parentId ? String(data.parentId).trim() : null;
|
||||
if (Object.prototype.hasOwnProperty.call(data, 'parent_id')) return data.parent_id ? String(data.parent_id).trim() : null;
|
||||
if (data.args && Object.prototype.hasOwnProperty.call(data.args, 'parentId')) return data.args.parentId ? String(data.args.parentId).trim() : null;
|
||||
return null;
|
||||
}
|
||||
|
||||
function treeRootForMode(mode) {
|
||||
var id = mode === 'filetree' ? 'sidebar-file-tree-root' : 'sidebar-tree-root';
|
||||
return document.querySelector('#' + id + ' .tree-root');
|
||||
}
|
||||
|
||||
function rowSelectorForDocument(mode, documentId) {
|
||||
var escaped = cssEscape(documentId);
|
||||
if (mode === 'filetree') {
|
||||
return '.tree-row[data-shell-mode="filetree"][data-doc-id="' + escaped + '"], .tree-row[data-shell-mode="filetree"][data-document-id="' + escaped + '"]';
|
||||
}
|
||||
return '.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"]';
|
||||
}
|
||||
|
||||
function ensureTreeChildren(parentRow) {
|
||||
var parentNode = parentRow ? parentRow.closest('.tree-node') : null;
|
||||
if (!parentNode) return null;
|
||||
var children = parentNode.querySelector(':scope > .tree-children');
|
||||
if (!children) {
|
||||
children = document.createElement('ul');
|
||||
children.className = 'tree-children';
|
||||
parentNode.appendChild(children);
|
||||
}
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
parentRow.setAttribute('aria-expanded', 'true');
|
||||
var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
|
||||
if (toggle) toggle.setAttribute('aria-expanded', 'true');
|
||||
return children;
|
||||
}
|
||||
|
||||
function removeDocumentRowForMode(mode, documentId) {
|
||||
var row = document.querySelector(rowSelectorForDocument(mode, documentId));
|
||||
var node = row ? row.closest('.tree-node') : null;
|
||||
if (!node || !node.parentElement) return false;
|
||||
node.parentElement.removeChild(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
function moveDocumentRowForMode(mode, documentId, parentId) {
|
||||
var row = document.querySelector(rowSelectorForDocument(mode, documentId));
|
||||
var node = row ? row.closest('.tree-node') : null;
|
||||
var root = treeRootForMode(mode);
|
||||
if (!row || !node || !root) return false;
|
||||
var targetContainer = root;
|
||||
if (parentId) {
|
||||
var parentRow = document.querySelector(rowSelectorForDocument(mode, parentId));
|
||||
targetContainer = ensureTreeChildren(parentRow);
|
||||
if (!targetContainer) return false;
|
||||
row.setAttribute('data-parent-id', parentId);
|
||||
} else {
|
||||
row.removeAttribute('data-parent-id');
|
||||
}
|
||||
targetContainer.appendChild(node);
|
||||
return true;
|
||||
}
|
||||
|
||||
function applyMoveDocumentDelta(data) {
|
||||
var documentId = documentIdFromDelta(data);
|
||||
if (!documentId) return false;
|
||||
var parentId = parentIdFromDelta(data);
|
||||
var movedPage = moveDocumentRowForMode('page', documentId, parentId);
|
||||
var movedFile = moveDocumentRowForMode('filetree', documentId, parentId);
|
||||
return movedPage || movedFile;
|
||||
}
|
||||
|
||||
function applyRemoveDocumentDelta(data) {
|
||||
var documentId = documentIdFromDelta(data);
|
||||
if (!documentId) return false;
|
||||
var removedPage = removeDocumentRowForMode('page', documentId);
|
||||
var removedFile = removeDocumentRowForMode('filetree', documentId);
|
||||
return removedPage || removedFile;
|
||||
}
|
||||
|
||||
function readProjection(value) {
|
||||
if (!value || typeof value !== 'object') return null;
|
||||
if (value.result && typeof value.result === 'object') return value.result;
|
||||
@@ -879,6 +1103,11 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
return resolved && Array.isArray(resolved.items) ? resolved.items : [];
|
||||
}
|
||||
|
||||
function hasProjectionItems(projection) {
|
||||
var resolved = readProjection(projection);
|
||||
return Boolean(resolved && Array.isArray(resolved.items));
|
||||
}
|
||||
|
||||
function nodeIdOf(item) {
|
||||
return String(item && (item.nodeId || item.id || item.documentId) || '').trim();
|
||||
}
|
||||
@@ -1024,9 +1253,9 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
}
|
||||
|
||||
function renderSidebarSnapshot(payload) {
|
||||
var renderedPage = renderPageProjection(payload);
|
||||
var renderedPage = hasProjectionItems(payload) ? renderPageProjection(payload) : false;
|
||||
var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
|
||||
var renderedFile = fileProjection ? renderFileProjection(fileProjection) : false;
|
||||
var renderedFile = fileProjection && hasProjectionItems(fileProjection) ? renderFileProjection(fileProjection) : false;
|
||||
return renderedPage || renderedFile;
|
||||
}
|
||||
|
||||
@@ -2805,9 +3034,10 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
var prompt = searchText(text);
|
||||
if (!prompt) return;
|
||||
pageAiLoadSessions();
|
||||
var aggregate = currentPageAggregate();
|
||||
var body = aggregate.body || {};
|
||||
var subtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
|
||||
var contextSnapshot = currentPageAiContextSnapshot();
|
||||
var aggregate = contextSnapshot.aggregate || {};
|
||||
var body = contextSnapshot.body || {};
|
||||
var subtree = contextSnapshot.subtree || null;
|
||||
var outline = subtree && subtree.outline ? subtree.outline : null;
|
||||
pageUiState.pageAiBusy = true;
|
||||
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
|
||||
@@ -2843,6 +3073,7 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
},
|
||||
subtree: subtree,
|
||||
outline: outline,
|
||||
pageSubtreeSource: contextSnapshot.pageSubtreeSource || 'none',
|
||||
evidence: null,
|
||||
pageOptions: currentPageOptions()
|
||||
},
|
||||
@@ -3890,6 +4121,18 @@ const SIDEBAR_TREE_JS: &str = r##"
|
||||
window.addEventListener('tree:delta', function(event) {
|
||||
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
|
||||
var data = payload && (payload.data || payload.delta || payload);
|
||||
var documentPatch = data && (data.document || data.node);
|
||||
if (data && data.op === 'upsert_document' && documentPatch) {
|
||||
updateTitleEverywhere(documentPatch.id || documentPatch.documentId, documentPatch.title || '无标题');
|
||||
}
|
||||
if (data && data.op === 'move_document' && applyMoveDocumentDelta(data)) {
|
||||
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
|
||||
return;
|
||||
}
|
||||
if (data && data.op === 'remove_document' && applyRemoveDocumentDelta(data)) {
|
||||
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
|
||||
return;
|
||||
}
|
||||
var documents = data && (data.upsertDocuments || data.upsert_documents);
|
||||
if (Array.isArray(documents)) {
|
||||
documents.forEach(function(doc) {
|
||||
|
||||
Reference in New Issue
Block a user