Files
mnote/rust/crates/mnote-web/src/ssr/pages/layout.rs
T
lix-2026 fc4a47e597 推进本地索引与 AI changed-files 审计
- 为本地工作区补充 search/backlinks/tags/resource 引用索引与 watcher 单文件刷新
- 在页面设置中增加索引页签,并展示本地反链、标签和 AI changed-files 审计
- 补充本地搜索与本地 AI changed-files 浏览器 smoke,并回填当前优先级 checklist
2026-05-19 10:22:01 +08:00

8728 lines
398 KiB
Rust
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
//! MNOTE 通用页面布局组件(Wolai / Notion 风格)
use leptos::prelude::*;
const SIDEBAR_TREE_JS: &str = r##"
(function(){
if (window.__mnoteSidebarTreeRuntimeStarted) return;
window.__mnoteSidebarTreeRuntimeStarted = true;
var PAGE_AI_SESSION_STORAGE_VERSION = 3;
var PAGE_DRAG_MIME = 'application/x-mnote-page-tree-node';
var FILETREE_DRAG_MIME = 'application/x-mnote-filetree-row-ids';
var MNOTE_SIDEBAR_TREE_MODE_KEY = 'mnote.sidebar.tree.mode';
var MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX = 'mnote.localFolder.recentRoots:';
var MNOTE_LAST_CLOUD_WORKSPACE_KEY = 'mnote.workspace.lastCloudWorkspaceId';
var MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY = 'mnote.global.showHeadingNumbers';
var mnoteNavigationInFlight = '';
var draggingPageNodeId = '';
var activePageDropRow = null;
var draggingFileTreeRowIds = [];
var activeFileTreeDropRow = null;
var sidebarFileTreeClipboard = null;
var sidebarFileTreeSelection = {
selectedRowIds: new Set(),
anchorRowId: null,
focusedRowId: null
};
var activeTreeContextMenu = null;
var activeEditorAttachmentLink = null;
var attachmentActionsHideTimer = 0;
var pageUiState = {
pageOptions: null,
historySnapshots: [],
pageSettingsOpen: false,
pageAiOpen: false,
pageAiBusy: false,
pageAiMessages: [],
pageAiSuggestionIndex: 0,
pageAiProvider: 'hermes',
pageAiPage: 'chat',
pageAiRunStatus: 'idle',
pageAiCurrentRunId: '',
pageAiAcpRuntime: 'reasonix',
pageAiAcpRuntimes: [],
pageAiQueueLength: 0,
pageAiQueuedItems: [],
pageAiStoppedRunIds: {},
pageAiAbortController: null,
pageAiContextScope: 'page',
pageAiTools: [],
pageAiToolsError: '',
pageAiGatewayHealth: null,
pageAiGatewayHealthError: '',
pageAiLastToolCall: null,
pageAiProfiles: [],
pageAiActiveProfileName: 'mnoteai',
pageAiProfileError: '',
pageAiProfileMemory: { memory: '', user: '', soul: '' },
pageAiProfileMemoryDrafts: { memory: '', user: '', soul: '' },
pageAiProfileMemoryError: '',
pageAiSkills: { categories: [], archived: [] },
pageAiSkillQuery: '',
pageAiSkillError: '',
pageAiSessions: [],
pageAiActiveSessionId: '',
pageAiSessionSearchQuery: '',
pageAiSessionSearchResults: [],
pageAiSessionSearchTimer: 0,
pageAiSessionError: '',
pageAiPermissionRequests: [],
localIndexSummary: {
scopeKey: '',
loading: false,
error: '',
backlinks: null,
tags: null
}
};
function closestAction(target, selector) {
return target && typeof target.closest === 'function' ? target.closest(selector) : null;
}
function toggleWorkspaceSidebar(trigger) {
var shell = document.querySelector('.mnote-shell, .wolai-workspace-shell');
if (!(shell instanceof HTMLElement)) return;
var collapsed = shell.getAttribute('data-mnote-sidebar-collapsed') === 'true';
var next = !collapsed;
shell.setAttribute('data-mnote-sidebar-collapsed', String(next));
document.documentElement.setAttribute('data-mnote-sidebar-collapsed', String(next));
document.documentElement.setAttribute('data-mnote-sidebar-toggle-applied', 'true');
if (trigger instanceof HTMLElement) {
trigger.setAttribute('aria-pressed', String(next));
trigger.setAttribute('title', next ? '展开侧栏' : '切换侧栏');
}
}
function escapeHtml(value) {
return String(value == null ? '' : value)
.replace(/&/g, '&')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
function cssEscape(value) {
if (window.CSS && typeof window.CSS.escape === 'function') return window.CSS.escape(value);
return String(value).replace(/["\\]/g, '\\$&');
}
function parseJsonScript(id) {
var node = document.getElementById(id);
if (!node) return null;
try {
return JSON.parse(node.textContent || 'null');
} catch (_) {
return null;
}
}
function currentDocumentId() {
var match = window.location.pathname.match(/^\/documents\/([^\/]+)/);
if (!match) match = window.location.pathname.match(/^\/mindmap\/([^\/]+)\/([^\/]+)/);
return match ? decodeURIComponent(match[1]) : '';
}
function currentFileTreeActiveRowId() {
var mindmapMatch = window.location.pathname.match(/^\/mindmap\/([^\/]+)\/([^\/]+)/);
if (mindmapMatch) return 'asset:' + decodeURIComponent(mindmapMatch[2]);
var documentId = currentDocumentId();
return documentId ? 'doc:' + documentId : '';
}
function currentPageAggregate() {
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 serverSubtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
return {
aggregate: aggregate,
body: body,
subtree: serverSubtree,
pageSubtreeSource: serverSubtree ? 'server' : 'none'
};
}
function currentPageAiSelectedText() {
try {
var selection = window.getSelection ? window.getSelection() : null;
return selection ? searchText(selection.toString() || '') : '';
} catch (_) {
return '';
}
}
function pageAiProjectionBlocks(aggregate) {
var blocks = aggregate && aggregate.body && aggregate.body.blockDocument && aggregate.body.blockDocument.blocks;
return Array.isArray(blocks) ? blocks : [];
}
function pageAiBlockText(block) {
return searchText(block && (block.text || block.title || block.content) || '');
}
function pageAiSelectedBlockIdsFromSelection() {
try {
var selection = window.getSelection ? window.getSelection() : null;
if (!selection || selection.rangeCount === 0 || searchText(selection.toString() || '') === '') return [];
var range = selection.getRangeAt(0);
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) {
if (!(node instanceof HTMLElement)) return false;
try {
return range.intersectsNode(node);
} catch (_) {
return false;
}
}).map(function(node) {
return searchText(node.getAttribute('data-id') || node.id || '');
}).filter(Boolean);
} catch (_) {
return [];
}
}
function pageAiBlocksToPageXml(blocks, aggregate) {
var revision = aggregate && aggregate.body ? String(aggregate.body.revision || '') : '';
var pageId = currentDocumentId() || 'current-page';
var lines = ['<page id="' + escapeHtml(pageId) + '" revision="' + escapeHtml(revision) + '">'];
blocks.forEach(function(block) {
var blockId = String(block && (block.blockId || block.id) || '');
var type = String(block && block.type || 'paragraph');
var revisionRef = String(block && block.revisionRef || '');
var level = block && block.attrs && block.attrs.level ? ' level="' + escapeHtml(block.attrs.level) + '"' : '';
lines.push(' <block id="' + escapeHtml(blockId) + '" type="' + escapeHtml(type) + '" revisionRef="' + escapeHtml(revisionRef) + '"' + level + '>' + escapeHtml(pageAiBlockText(block)) + '</block>');
});
lines.push('</page>');
return lines.join('\n');
}
function buildPageAiContext(contextSnapshot, scope, selectedText) {
var aggregate = contextSnapshot.aggregate || {};
var body = aggregate.body || {};
var allBlocks = pageAiProjectionBlocks(aggregate);
var selectedBlockIds = scope === 'selection' ? pageAiSelectedBlockIdsFromSelection() : [];
var selectedSet = {};
selectedBlockIds.forEach(function(id) { selectedSet[id] = true; });
var selectedBlocks = selectedBlockIds.length
? allBlocks.filter(function(block) { return selectedSet[String(block.blockId || block.id || '')]; })
: [];
var contextBlocks = selectedBlocks.length ? selectedBlocks : allBlocks.slice(0, 120);
var truncated = !selectedBlocks.length && allBlocks.length > contextBlocks.length;
return {
schema: 'mnote.page_ai_context.v1',
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
scope: scope,
revision: body.revision || null,
conflictDetectionKey: body.conflictDetectionKey || null,
selectedText: selectedText || '',
selectedBlockIds: selectedBlockIds,
allowedTargetBlockIds: selectedBlockIds,
selectedBlocks: selectedBlocks,
contextBlocks: contextBlocks,
pageText: contextBlocks.map(pageAiBlockText).filter(Boolean).join('\n'),
pageXml: pageAiBlocksToPageXml(contextBlocks, aggregate),
truncated: truncated,
warnings: truncated ? [{ code: 'page_ai_context_truncated', message: '页面 AI context 已按前 120 个块裁剪' }] : []
};
}
function pageAiScopedPageContext(contextSnapshot) {
var aggregate = contextSnapshot.aggregate || {};
var body = contextSnapshot.body || {};
var subtree = contextSnapshot.subtree || null;
var outline = subtree && subtree.outline ? subtree.outline : null;
var scope = pageUiState.pageAiContextScope || 'page';
var title = aggregate.head && aggregate.head.title ? aggregate.head.title : '';
var selectedText = scope === 'selection' ? currentPageAiSelectedText() : '';
var aiContext = buildPageAiContext(contextSnapshot, scope, selectedText);
return {
pageContext: {
contextScope: scope,
documentBlocks: null,
node: {
documentId: currentDocumentId(),
title: title
},
subtree: null,
outline: null,
pageSubtreeSource: scope === 'page' ? (contextSnapshot.pageSubtreeSource || 'none') : 'scope:' + scope,
evidence: selectedText ? [{ kind: 'selection', text: selectedText }] : null,
pageOptions: scope === 'options' || scope === 'page' ? currentPageOptions() : null,
contentAccess: 'mnote.doc.fetch',
aiContext: aiContext
},
selectedText: selectedText,
selectedBlockId: aiContext.selectedBlockIds[0] || null
};
}
function defaultPageOptions() {
return {
wideLayout: false,
smallText: false,
layoutDensity: 'normal',
showHeadingNumbers: false,
showToc: false,
showStructure: false,
protectEditing: false,
showWordCount: true,
collapseBacklinks: false,
pageFont: 'default',
hideChildPages: false,
showBlockRefCount: false,
embedDefaultBlockId: null
};
}
function currentPageOptions() {
if (pageUiState.pageOptions) return pageUiState.pageOptions;
var aggregate = currentPageAggregate();
var current = aggregate && aggregate.layout && aggregate.layout.pageOptions && typeof aggregate.layout.pageOptions === 'object'
? aggregate.layout.pageOptions
: null;
if (!current) {
return defaultPageOptions();
}
pageUiState.pageOptions = Object.assign(defaultPageOptions(), current);
return pageUiState.pageOptions;
}
function readGlobalShowHeadingNumbers() {
try {
var raw = window.localStorage ? window.localStorage.getItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY) : '';
return raw === 'true' || raw === '1';
} catch (_) {
return false;
}
}
function writeGlobalShowHeadingNumbers(value) {
try {
if (window.localStorage) {
window.localStorage.setItem(MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY, value ? 'true' : 'false');
}
} catch (_) {}
document.documentElement.setAttribute('data-global-show-heading-numbers', String(Boolean(value)));
}
function effectiveShowHeadingNumbers(options) {
return readGlobalShowHeadingNumbers();
}
function pageOptionIsSupported(name) {
return name === 'wideLayout'
|| name === 'smallText'
|| name === 'layoutDensity'
|| name === 'pageFont'
|| name === 'showHeadingNumbers';
}
function pageOptionDescription(name) {
if (name === 'wideLayout') return '自适应宽度';
if (name === 'smallText') return '小字体';
if (name === 'showToc') return '标题目录';
if (name === 'showHeadingNumbers') return '标题自动编号';
if (name === 'protectEditing') return '编辑保护';
if (name === 'collapseBacklinks') return '折叠反向引用';
if (name === 'hideChildPages') return '隐藏子页面';
if (name === 'showBlockRefCount') return '显示块引用数字';
return name;
}
function pageOptionHint(name) {
if (name === 'wideLayout') return '已接通:主内容列宽度立即变化';
if (name === 'smallText') return '已接通:正文排版会更紧凑';
if (name === 'showHeadingNumbers') return '已接通:标题前显示顺序编号';
if (name === 'layoutDensity') return '已接通:段落与列表间距会变化';
if (name === 'pageFont') return '已接通:当前页面字体会切换';
if (name === 'showToc') return '待接线:当前 Rust 壳还没有正式目录面板';
if (name === 'protectEditing') return '待接线:当前主编辑器只显示降级说明';
if (name === 'collapseBacklinks') return '待接线:当前 Rust 壳未挂回链面板';
if (name === 'hideChildPages') return '待接线:当前页面壳还没有子页面块显隐';
if (name === 'showBlockRefCount') return '待接线:当前页面壳未显示块引用计数';
return '待接线';
}
function ensureHistorySnapshotsSeeded() {
if (pageUiState.historySnapshots.length > 0) return;
var aggregate = currentPageAggregate();
var stats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
pageUiState.historySnapshots = [{
id: 'snapshot-initial',
timestamp: Date.now(),
stats: {
wordCount: Number(stats.wordCount || 0),
characterCount: Number(stats.characterCount || 0),
blockCount: Number(stats.blockCount || 0),
todoTotal: Number(stats.todoTotal || 0),
todoDone: Number(stats.todoDone || 0)
}
}];
}
function computeLivePageStats() {
var aggregate = currentPageAggregate();
var fallbackStats = aggregate && aggregate.stats && typeof aggregate.stats === 'object' ? aggregate.stats : {};
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
if (!(editor instanceof HTMLElement)) {
return {
wordCount: Number(fallbackStats.wordCount || 0),
characterCount: Number(fallbackStats.characterCount || 0),
blockCount: Number(fallbackStats.blockCount || 0),
todoTotal: Number(fallbackStats.todoTotal || 0),
todoDone: Number(fallbackStats.todoDone || 0)
};
}
var text = (editor.textContent || '').trim();
var compact = text.replace(/\s+/g, ' ').trim();
var wordCount = compact ? compact.split(' ').filter(Boolean).length : 0;
var characterCount = text.replace(/\s/g, '').length;
var blockCount = editor.querySelectorAll(':scope > *').length;
var todos = Array.from(editor.querySelectorAll('input[type="checkbox"]'));
return {
wordCount: wordCount || Number(fallbackStats.wordCount || 0),
characterCount: characterCount || Number(fallbackStats.characterCount || 0),
blockCount: blockCount || Number(fallbackStats.blockCount || 0),
todoTotal: todos.length || Number(fallbackStats.todoTotal || 0),
todoDone: todos.filter(function(node){ return node.checked; }).length || Number(fallbackStats.todoDone || 0)
};
}
function recordPageHistorySnapshot(reason, stats) {
ensureHistorySnapshotsSeeded();
pageUiState.historySnapshots = [{
id: 'snapshot-' + Date.now(),
timestamp: Date.now(),
reason: reason || 'save',
stats: stats || computeLivePageStats()
}].concat(pageUiState.historySnapshots).slice(0, 15);
renderPageHistoryDrawer();
}
function applyPageOptionsToShell() {
var options = currentPageOptions();
var globalShowHeadingNumbers = readGlobalShowHeadingNumbers();
var showHeadingNumbers = effectiveShowHeadingNumbers(options);
var shell = document.querySelector('.document-shell');
var editorRoot = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"]');
var editorSurface = editorRoot && editorRoot.querySelector('.editor-surface');
if (shell instanceof HTMLElement) {
shell.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
shell.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
shell.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
shell.setAttribute('data-page-font', String(options.pageFont || 'default'));
shell.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
shell.style.width = '100%';
shell.style.maxWidth = options.wideLayout ? '980px' : '760px';
}
if (editorRoot instanceof HTMLElement) {
editorRoot.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
editorRoot.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
editorRoot.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
editorRoot.setAttribute('data-page-embed-default-block-id', options.embedDefaultBlockId == null ? '' : String(options.embedDefaultBlockId));
}
if (editorSurface instanceof HTMLElement) {
editorSurface.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
editorSurface.setAttribute('data-page-font', String(options.pageFont || 'default'));
editorSurface.setAttribute('data-show-heading-numbers', String(showHeadingNumbers));
}
document.documentElement.setAttribute('data-page-wide-layout', String(Boolean(options.wideLayout)));
document.documentElement.setAttribute('data-page-small-text', String(Boolean(options.smallText)));
document.documentElement.setAttribute('data-layout-density', String(options.layoutDensity || 'normal'));
document.documentElement.setAttribute('data-page-font', String(options.pageFont || 'default'));
document.documentElement.setAttribute('data-global-show-heading-numbers', String(globalShowHeadingNumbers));
document.documentElement.setAttribute('data-page-show-heading-numbers', String(showHeadingNumbers));
}
function normalizeSidebarTreeMode(value) {
var mode = String(value || '').trim();
return mode === 'filetree' ? 'filetree' : 'page';
}
function readStoredSidebarTreeMode() {
var params = new URLSearchParams(window.location.search);
var fromUrl = normalizeSidebarTreeMode(params.get('treeView') || params.get('sidebarTree'));
if (params.has('treeView') || params.has('sidebarTree')) return fromUrl;
try {
var stored = window.sessionStorage ? window.sessionStorage.getItem(MNOTE_SIDEBAR_TREE_MODE_KEY) : '';
return normalizeSidebarTreeMode(stored);
} catch (_) {
return 'page';
}
}
function persistSidebarTreeMode(mode) {
var normalized = normalizeSidebarTreeMode(mode);
document.documentElement.setAttribute('data-mnote-sidebar-tree-mode', normalized);
try {
if (window.sessionStorage) window.sessionStorage.setItem(MNOTE_SIDEBAR_TREE_MODE_KEY, normalized);
} catch (_) {}
return normalized;
}
function activeSidebarTreeMode() {
var active = document.querySelector('[data-mnote-sidebar-tree-tab][aria-selected="true"]');
if (active instanceof HTMLElement) {
return normalizeSidebarTreeMode(active.getAttribute('data-mnote-sidebar-tree-tab'));
}
return readStoredSidebarTreeMode();
}
function resolveWorkspaceId(trigger) {
var direct = (trigger.getAttribute('data-workspace-id') || '').trim();
if (direct) return direct;
var root = trigger.closest('[data-workspace-id]');
if (root) {
var value = (root.getAttribute('data-workspace-id') || '').trim();
if (value) return value;
}
var documentId = currentDocumentId();
if (documentId) {
var shell = document.querySelector('.document-shell[data-document-id="' + cssEscape(documentId) + '"][data-workspace-id]');
if (shell instanceof HTMLElement) {
var shellWorkspaceId = (shell.getAttribute('data-workspace-id') || '').trim();
if (shellWorkspaceId) return shellWorkspaceId;
}
}
var activePane = document.querySelector('[data-pane-visible="true"][data-pane-workspace-id]');
if (activePane instanceof HTMLElement) {
var paneWorkspaceId = (activePane.getAttribute('data-pane-workspace-id') || '').trim();
if (paneWorkspaceId) return paneWorkspaceId;
}
var anyDocumentShell = document.querySelector('.document-shell[data-workspace-id]');
if (anyDocumentShell instanceof HTMLElement) {
var documentWorkspaceId = (anyDocumentShell.getAttribute('data-workspace-id') || '').trim();
if (documentWorkspaceId) return documentWorkspaceId;
}
var fromDom = document.querySelector('[data-workspace-id]');
if (fromDom instanceof HTMLElement) {
var domWorkspaceId = (fromDom.getAttribute('data-workspace-id') || '').trim();
if (domWorkspaceId) return domWorkspaceId;
}
return (new URLSearchParams(window.location.search).get('workspaceId') || 'default').trim() || 'default';
}
function currentSourceKind() {
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('sourceKind') || '').trim();
if (fromUrl) return fromUrl;
if (
window.location.pathname === '/' &&
!(params.get('workspaceId') || '').trim() &&
!(params.get('rootUri') || '').trim() &&
!(params.get('pageId') || '').trim()
) {
return 'local_folder';
}
return 'convex_workspace';
}
function currentRootUri() {
return (new URLSearchParams(window.location.search).get('rootUri') || '').trim();
}
function rememberCloudWorkspaceId(workspaceId) {
var normalized = String(workspaceId || '').trim();
if (!normalized || normalized === 'local-folder' || normalized === 'default') return;
try {
if (window.localStorage) window.localStorage.setItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY, normalized);
} catch (_) {}
}
function readCurrentCloudWorkspaceIdFromPage() {
if (currentSourceKind() === 'local_folder') return '';
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('workspaceId') || '').trim();
if (fromUrl && fromUrl !== 'default') return fromUrl;
var fromDom = document.querySelector('[data-workspace-id]');
if (fromDom instanceof HTMLElement) {
var value = (fromDom.getAttribute('data-workspace-id') || '').trim();
if (value && value !== 'local-folder' && value !== 'default') return value;
}
return '';
}
function rememberCurrentCloudWorkspaceId() {
rememberCloudWorkspaceId(readCurrentCloudWorkspaceIdFromPage());
}
function readLastCloudWorkspaceId() {
try {
var stored = window.localStorage ? window.localStorage.getItem(MNOTE_LAST_CLOUD_WORKSPACE_KEY) : '';
if (stored && stored.trim() && stored.trim() !== 'default') return stored.trim();
} catch (_) {}
var params = new URLSearchParams(window.location.search);
var fromUrl = (params.get('workspaceId') || '').trim();
if (fromUrl && currentSourceKind() !== 'local_folder') return fromUrl;
var fromDom = document.querySelector('[data-workspace-id]');
if (fromDom instanceof HTMLElement) {
var value = (fromDom.getAttribute('data-workspace-id') || '').trim();
if (value && value !== 'local-folder') return value;
}
return '';
}
function copyWorkspaceSourceParams(targetUrl) {
var params = new URLSearchParams(window.location.search);
['sourceKind', 'rootUri', 'secondaryDocumentId', 'secondarySourceKind', 'secondaryRootUri'].forEach(function(name) {
var value = (params.get(name) || '').trim();
if (value) targetUrl.searchParams.set(name, value);
});
}
function currentWorkspaceSourcePayload() {
var params = new URLSearchParams(window.location.search);
var payload = {};
['sourceKind', 'rootUri'].forEach(function(name) {
var value = (params.get(name) || '').trim();
if (value) payload[name] = value;
});
return payload;
}
function currentActorStorageId() {
var fromBody = document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-actor-id') || '').trim() : '';
if (fromBody && fromBody !== 'anonymous') return fromBody;
return '';
}
function recentLocalRootsStorageKey() {
var actorId = currentActorStorageId();
return actorId ? MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX + encodeURIComponent(actorId) : '';
}
function readRecentLocalRoots() {
try {
var storageKey = recentLocalRootsStorageKey();
if (!storageKey) return [];
var raw = window.localStorage ? window.localStorage.getItem(storageKey) : '';
var parsed = raw ? JSON.parse(raw) : [];
return Array.isArray(parsed) ? parsed.filter(function(value) {
return typeof value === 'string' && value.trim();
}).slice(0, 10) : [];
} catch (_) {
return [];
}
}
function rememberLocalRoot(rootUri) {
try {
if (!window.localStorage) return;
var storageKey = recentLocalRootsStorageKey();
if (!storageKey) return;
var roots = readRecentLocalRoots().filter(function(value) { return value !== rootUri; });
roots.unshift(rootUri);
window.localStorage.setItem(storageKey, JSON.stringify(roots.slice(0, 10)));
} catch (_) {}
}
function pathToFileRootUri(value) {
var trimmed = String(value || '').trim();
if (!trimmed) return '';
if (/^file:\/\//i.test(trimmed)) return trimmed;
if (trimmed.charAt(0) !== '/') return '';
return 'file://' + trimmed.split('/').map(function(part, index) {
return index === 0 ? '' : encodeURIComponent(part);
}).join('/');
}
function fileRootUriToPathInput(rootUri) {
var value = String(rootUri || '').replace(/^file:\/\//, '');
try {
return decodeURIComponent(value);
} catch (_) {
return value;
}
}
function openLocalFolderRoot(rootUri) {
if (currentSourceKind() !== 'local_folder') {
rememberCurrentCloudWorkspaceId();
}
rememberLocalRoot(rootUri);
var targetUrl = new URL('/', window.location.origin);
targetUrl.searchParams.set('treeView', 'filetree');
targetUrl.searchParams.set('sourceKind', 'local_folder');
targetUrl.searchParams.set('rootUri', rootUri);
window.location.href = targetUrl.toString();
}
function switchToCloudWorkspace() {
var targetUrl = new URL('/', window.location.origin);
var workspaceId = readLastCloudWorkspaceId();
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
targetUrl.searchParams.set('sourceKind', 'convex_workspace');
window.location.href = targetUrl.toString();
}
function closeTrashModal() {
var modal = document.querySelector('[data-testid="mnote-trash-modal"]');
if (!modal) return;
var workbench = modal.querySelector('[data-testid="mnote-trash-workbench"]');
if (workbench && workbench.__mnoteTrashEventSource && typeof workbench.__mnoteTrashEventSource.close === 'function') {
workbench.__mnoteTrashEventSource.close();
}
var fileScrollTop = Number(modal.getAttribute('data-file-scroll-top') || '0');
var fileRoot = document.getElementById('sidebar-file-tree-root');
if (fileRoot instanceof HTMLElement && Number.isFinite(fileScrollTop)) fileRoot.scrollTop = fileScrollTop;
if (modal.parentElement) modal.parentElement.removeChild(modal);
document.documentElement.removeAttribute('data-mnote-trash-modal-open');
}
function executeTrashWorkbenchScripts(parsed) {
parsed.querySelectorAll('script').forEach(function(script) {
var nextScript = document.createElement('script');
Array.from(script.attributes || []).forEach(function(attr) {
nextScript.setAttribute(attr.name, attr.value);
});
nextScript.textContent = script.textContent || '';
document.body.appendChild(nextScript);
if (nextScript.parentElement) nextScript.parentElement.removeChild(nextScript);
});
}
function renderLocalFolderTrashPlaceholder(content, rootUri) {
content.innerHTML = [
'<section class="mnote-trash-workbench" data-testid="mnote-trash-workbench" data-trash-source-kind="local_folder">',
'<header class="mnote-trash-header"><h1>本地文件夹垃圾箱</h1>',
'<p>本地删除项保存在当前目录的 <code>.mnote/trash</code> 与 <code>.mnote/trash-index.json</code> 中。</p></header>',
'<section class="mnote-trash-section"><div class="mnote-trash-empty">当前弹窗已保持在本地文件夹上下文:' + escapeHtml(rootUri || '未选择本地目录') + '</div></section>',
'</section>'
].join('');
}
function openTrashModal(trigger) {
var existing = document.querySelector('[data-testid="mnote-trash-modal"]');
if (existing) return;
var fileRoot = document.getElementById('sidebar-file-tree-root');
var activeRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-selected="true"], #sidebar-tree-root .tree-row[data-active="true"]');
var overlay = document.createElement('div');
overlay.className = 'mnote-trash-modal';
overlay.setAttribute('data-testid', 'mnote-trash-modal');
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.setAttribute('aria-label', '垃圾箱');
overlay.setAttribute('data-file-scroll-top', fileRoot instanceof HTMLElement ? String(fileRoot.scrollTop) : '0');
if (activeRow instanceof HTMLElement) {
overlay.setAttribute('data-active-row-id', activeRow.getAttribute('data-row-id') || activeRow.getAttribute('data-node-id') || '');
}
overlay.innerHTML = '<div class="mnote-trash-modal__backdrop" data-mnote-trash-modal-close="true"></div>' +
'<section class="mnote-trash-modal__panel">' +
'<button type="button" class="mnote-trash-modal__close" data-testid="mnote-trash-modal-close" data-mnote-trash-modal-close="true" aria-label="关闭垃圾箱">×</button>' +
'<div class="mnote-trash-modal__content" data-testid="mnote-trash-modal-content"><div class="mnote-trash-empty">正在加载垃圾箱...</div></div>' +
'</section>';
document.body.appendChild(overlay);
document.documentElement.setAttribute('data-mnote-trash-modal-open', 'true');
var content = overlay.querySelector('[data-testid="mnote-trash-modal-content"]');
var sourceKind = currentSourceKind();
if (sourceKind === 'local_folder') {
renderLocalFolderTrashPlaceholder(content, new URLSearchParams(window.location.search).get('rootUri') || '');
return;
}
var workspaceId = resolveWorkspaceId(trigger || document.body);
var url = new URL('/trash', window.location.origin);
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
fetch(url.toString(), { headers: { 'x-mnote-trash-modal': '1' } }).then(function(response) {
return response.text().then(function(html) {
if (!response.ok) throw new Error('trash_modal_load_failed_' + response.status);
var parsed = new DOMParser().parseFromString(html, 'text/html');
var workbench = parsed.querySelector('[data-testid="mnote-trash-workbench"]');
if (!workbench) throw new Error('trash_modal_missing_workbench');
content.innerHTML = '';
content.appendChild(workbench);
executeTrashWorkbenchScripts(parsed);
});
}).catch(function(error) {
content.innerHTML = '<div class="mnote-trash-empty" data-testid="mnote-trash-modal-error">' + escapeHtml(error && error.message ? error.message : String(error)) + '</div>';
});
}
function closeLocalFolderDialog() {
var existing = document.querySelector('[data-testid="mnote-local-folder-dialog"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
}
function readPathFromDirectoryFiles(files) {
var first = files && files.length ? files[0] : null;
if (!first) return '';
var rawPath = typeof first.path === 'string' ? first.path : '';
var relative = typeof first.webkitRelativePath === 'string' ? first.webkitRelativePath : '';
if (rawPath && relative) {
var suffix = relative.split('/').filter(Boolean).join('/');
if (suffix && rawPath.endsWith(suffix)) {
return rawPath.slice(0, rawPath.length - suffix.length).replace(/[\/\\]$/, '');
}
}
if (rawPath) return rawPath;
return '';
}
function requestBrowserFolderChoice(statusNode) {
return new Promise(function(resolve) {
var input = document.createElement('input');
input.type = 'file';
input.multiple = true;
input.setAttribute('webkitdirectory', '');
input.setAttribute('directory', '');
input.style.position = 'fixed';
input.style.left = '-9999px';
input.addEventListener('change', function() {
var selectedPath = readPathFromDirectoryFiles(input.files || []);
if (input.parentElement) input.parentElement.removeChild(input);
if (!selectedPath && statusNode instanceof HTMLElement) {
statusNode.textContent = '当前浏览器没有暴露本机绝对路径,请在下方输入路径。';
}
resolve(selectedPath);
}, { once: true });
document.body.appendChild(input);
input.click();
});
}
async function requestNativeFolderChoice(statusNode) {
var desktopPicker = window.__mnoteDesktop && typeof window.__mnoteDesktop.selectLocalFolder === 'function'
? window.__mnoteDesktop.selectLocalFolder
: null;
if (desktopPicker) {
var selected = await desktopPicker();
return typeof selected === 'string' ? selected : '';
}
if (typeof window.showDirectoryPicker === 'function') {
var handle = await window.showDirectoryPicker({ mode: 'read' });
var handlePath = handle && (handle.path || handle.mnotePath || handle.nativePath);
if (typeof handlePath === 'string' && handlePath.trim()) return handlePath;
if (statusNode instanceof HTMLElement) {
statusNode.textContent = '已选择“' + (handle && handle.name ? handle.name : '文件夹') + '”,但浏览器没有暴露本机绝对路径,请在下方确认路径。';
}
return '';
}
return requestBrowserFolderChoice(statusNode);
}
function openLocalFolderDialog(initialMessage) {
closeLocalFolderDialog();
var recent = readRecentLocalRoots();
var dialog = document.createElement('div');
dialog.className = 'mnote-local-folder-dialog';
dialog.setAttribute('data-testid', 'mnote-local-folder-dialog');
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.style.position = 'fixed';
dialog.style.inset = '0';
dialog.style.zIndex = '2147483646';
dialog.style.background = 'rgba(15, 23, 42, 0.28)';
dialog.style.display = 'flex';
dialog.style.alignItems = 'center';
dialog.style.justifyContent = 'center';
var card = document.createElement('div');
card.className = 'mnote-local-folder-dialog__card';
card.style.width = 'min(640px, calc(100vw - 32px))';
card.style.background = '#fff';
card.style.border = '1px solid rgba(27, 28, 28, 0.12)';
card.style.borderRadius = '8px';
card.style.padding = '16px';
card.style.boxShadow = '0 18px 48px rgba(15, 23, 42, 0.22)';
card.style.display = 'grid';
card.style.gap = '12px';
var title = document.createElement('h2');
title.textContent = '打开本地文件夹';
title.style.margin = '0';
title.style.fontSize = '18px';
title.style.lineHeight = '1.4';
var status = document.createElement('p');
status.className = 'mnote-local-folder-dialog__status';
status.setAttribute('data-testid', 'mnote-local-folder-status');
status.textContent = initialMessage || '选择一个本机文件夹,或输入绝对路径。';
status.style.margin = '0';
status.style.color = '#4b5563';
status.style.fontSize = '13px';
var input = document.createElement('input');
input.type = 'text';
input.autocomplete = 'off';
input.spellcheck = false;
input.placeholder = '/mnt/Data1T/mnote/design/04-tree-domain/done';
input.setAttribute('data-testid', 'mnote-local-folder-path-input');
input.value = recent.length ? fileRootUriToPathInput(recent[0]) : '';
input.style.width = '100%';
input.style.boxSizing = 'border-box';
input.style.border = '1px solid rgba(27, 28, 28, 0.18)';
input.style.borderRadius = '6px';
input.style.padding = '10px 12px';
input.style.fontSize = '14px';
var actions = document.createElement('div');
actions.className = 'mnote-local-folder-dialog__actions';
actions.style.display = 'flex';
actions.style.gap = '8px';
actions.style.justifyContent = 'flex-end';
var choose = document.createElement('button');
choose.type = 'button';
choose.textContent = '选择文件夹';
choose.setAttribute('data-testid', 'mnote-local-folder-native-picker');
var cancel = document.createElement('button');
cancel.type = 'button';
cancel.textContent = '取消';
var confirm = document.createElement('button');
confirm.type = 'button';
confirm.textContent = '打开';
confirm.setAttribute('data-testid', 'mnote-local-folder-open-confirm');
[choose, cancel, confirm].forEach(function(button) {
button.style.border = '1px solid rgba(27, 28, 28, 0.14)';
button.style.borderRadius = '6px';
button.style.padding = '8px 12px';
button.style.background = '#fff';
button.style.cursor = 'pointer';
button.style.fontSize = '14px';
});
function submit() {
var rootUri = pathToFileRootUri(input.value);
if (!rootUri) {
status.textContent = '请输入以 / 开头的绝对路径,或 file:// URI。';
input.focus();
return;
}
closeLocalFolderDialog();
openLocalFolderRoot(rootUri);
}
choose.addEventListener('click', function(event) {
event.preventDefault();
requestNativeFolderChoice(status).then(function(selectedPath) {
if (selectedPath) {
input.value = selectedPath;
submit();
}
}).catch(function(error) {
status.textContent = error && error.name === 'AbortError'
? '已取消选择。'
: '无法打开系统文件夹选择器,请输入路径。';
});
});
cancel.addEventListener('click', function(event) {
event.preventDefault();
closeLocalFolderDialog();
});
confirm.addEventListener('click', function(event) {
event.preventDefault();
submit();
});
input.addEventListener('keydown', function(event) {
if (event.key === 'Enter') {
event.preventDefault();
submit();
}
});
actions.appendChild(choose);
actions.appendChild(cancel);
actions.appendChild(confirm);
card.appendChild(title);
card.appendChild(status);
card.appendChild(input);
if (recent.length) {
var recentTitle = document.createElement('div');
recentTitle.style.fontSize = '12px';
recentTitle.style.color = '#6b7280';
recentTitle.textContent = '最近使用';
card.appendChild(recentTitle);
var recentList = document.createElement('div');
recentList.className = 'mnote-local-folder-dialog__recent';
recent.slice(0, 5).forEach(function(rootUri) {
var button = document.createElement('button');
button.type = 'button';
button.textContent = rootUri.replace(/^file:\/\//, '');
button.addEventListener('click', function(event) {
event.preventDefault();
closeLocalFolderDialog();
openLocalFolderRoot(rootUri);
});
recentList.appendChild(button);
});
card.appendChild(recentList);
}
card.appendChild(actions);
dialog.appendChild(card);
dialog.addEventListener('click', function(event) {
if (event.target === dialog) closeLocalFolderDialog();
});
document.body.appendChild(dialog);
input.focus();
input.select();
}
function requestOpenLocalFolder() {
openLocalFolderDialog('');
}
async function createDefaultLocalWorkspace(trigger) {
setCommandPending(trigger, true);
try {
var response = await fetch('/api/local-folder/workspaces/default', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: '{}'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) {
throw new Error((payload && payload.message) || ('local_workspace_create_failed_' + response.status));
}
var workspace = payload && payload.workspace || {};
var rootUri = String(workspace.rootUri || '').trim();
if (!rootUri) throw new Error('local_workspace_create_missing_root_uri');
openLocalFolderRoot(rootUri);
} catch (error) {
var status = document.querySelector('[data-testid="mnote-local-folder-status"]');
if (status instanceof HTMLElement) {
status.textContent = error && error.message ? error.message : String(error);
} else {
openLocalFolderDialog(error && error.message ? error.message : String(error));
}
} finally {
setCommandPending(trigger, false);
}
}
function autoOpenRecentLocalRootOnHome() {
if (window.location.pathname !== '/') return false;
var params = new URLSearchParams(window.location.search);
if ((params.get('sourceKind') || '').trim()) return false;
if ((params.get('rootUri') || '').trim()) return false;
if ((params.get('workspaceId') || '').trim()) return false;
if ((params.get('pageId') || '').trim()) return false;
var recent = readRecentLocalRoots();
if (!recent.length) return false;
openLocalFolderRoot(recent[0]);
return true;
}
function closeWorkspaceSourceMenu() {
var existing = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
if (existing && existing.parentElement) existing.parentElement.removeChild(existing);
document.querySelectorAll('[data-testid="mnote-workspace-source-trigger"]').forEach(function(trigger) {
if (trigger instanceof HTMLElement) trigger.setAttribute('aria-expanded', 'false');
});
}
function workspaceSourceLabel(rootUri) {
var label = String(rootUri || '').replace(/^file:\/\//, '');
try {
label = decodeURIComponent(label);
} catch (_) {}
return label || '本地文件夹';
}
function openWorkspaceSourceMenu(trigger) {
closeWorkspaceSourceMenu();
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('aria-expanded', 'true');
var menu = document.createElement('div');
menu.className = 'mnote-workspace-source-menu';
menu.setAttribute('data-testid', 'mnote-workspace-source-menu');
menu.setAttribute('role', 'menu');
var cloudButton = document.createElement('button');
cloudButton.type = 'button';
cloudButton.className = 'mnote-workspace-source-menu__item';
cloudButton.setAttribute('data-testid', 'mnote-switch-cloud-workspace');
cloudButton.setAttribute('role', 'menuitem');
cloudButton.textContent = '云空间';
cloudButton.addEventListener('click', function(event) {
event.preventDefault();
closeWorkspaceSourceMenu();
switchToCloudWorkspace();
});
menu.appendChild(cloudButton);
var recent = readRecentLocalRoots();
if (recent.length) {
var label = document.createElement('div');
label.className = 'mnote-workspace-source-menu__label';
label.textContent = '最近本地文件夹';
menu.appendChild(label);
recent.slice(0, 8).forEach(function(rootUri) {
var button = document.createElement('button');
button.type = 'button';
button.className = 'mnote-workspace-source-menu__item';
button.setAttribute('data-testid', 'mnote-recent-local-root');
button.setAttribute('data-root-uri', rootUri);
button.setAttribute('role', 'menuitem');
button.textContent = workspaceSourceLabel(rootUri);
button.addEventListener('click', function(event) {
event.preventDefault();
closeWorkspaceSourceMenu();
openLocalFolderRoot(rootUri);
});
menu.appendChild(button);
});
}
var openOther = document.createElement('button');
openOther.type = 'button';
openOther.className = 'mnote-workspace-source-menu__item mnote-workspace-source-menu__item--primary';
openOther.setAttribute('data-testid', 'mnote-open-other-local-folder');
openOther.setAttribute('role', 'menuitem');
openOther.textContent = '打开其他本地文件夹';
openOther.addEventListener('click', function(event) {
event.preventDefault();
closeWorkspaceSourceMenu();
requestOpenLocalFolder();
});
menu.appendChild(openOther);
trigger.closest('[data-testid="wolai-workspace-identity"]')?.appendChild(menu);
}
autoOpenRecentLocalRootOnHome();
function setCommandPending(trigger, pending) {
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-pending', pending ? 'true' : 'false');
if ('disabled' in trigger) {
if (pending) trigger.setAttribute('disabled', 'disabled');
else trigger.removeAttribute('disabled');
}
}
async function dispatchTreeCommand(trigger, body) {
setCommandPending(trigger, true);
var commandBody = Object.assign({}, currentWorkspaceSourcePayload(), body || {});
try {
var response = await fetch('/api/tree/commands', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(commandBody)
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || !payload.result) {
throw new Error((payload && payload.message) || 'tree_command_failed_' + response.status);
}
window.dispatchEvent(new CustomEvent('tree:local-command', { detail: { body: commandBody, result: payload.result } }));
setCommandPending(trigger, false);
return payload.result;
} catch (error) {
setCommandPending(trigger, false);
if (trigger instanceof HTMLElement) {
trigger.setAttribute('data-error', error instanceof Error ? error.message : String(error));
}
throw error;
}
}
function navigateToDocument(nodeId, workspaceId, options) {
if (!nodeId) return;
var treeView = normalizeSidebarTreeMode(options && options.treeView ? options.treeView : activeSidebarTreeMode());
persistSidebarTreeMode(treeView);
if (currentDocumentId() === nodeId && window.location.pathname.indexOf('/documents/') === 0) {
document.querySelectorAll('.tree-row[data-active="true"], .tree-row[data-selected="true"]').forEach(function(row) {
if (row instanceof HTMLElement) {
row.setAttribute('data-active', 'false');
row.setAttribute('data-selected', 'false');
}
});
document.querySelectorAll('.tree-row[data-node-id="' + cssEscape(nodeId) + '"], .tree-row[data-document-id="' + cssEscape(nodeId) + '"], .tree-row[data-doc-id="' + cssEscape(nodeId) + '"]').forEach(function(row) {
if (row instanceof HTMLElement) {
if (row.getAttribute('data-shell-mode') === 'filetree') {
row.setAttribute('data-selected', String(row.getAttribute('data-row-id') === 'doc:' + nodeId));
} else {
row.setAttribute('data-active', 'true');
}
}
});
return;
}
var targetUrl = new URL('/documents/' + encodeURIComponent(nodeId), window.location.origin);
if (workspaceId) targetUrl.searchParams.set('workspaceId', workspaceId);
if (treeView === 'filetree') targetUrl.searchParams.set('treeView', 'filetree');
copyWorkspaceSourceParams(targetUrl);
var url = targetUrl.pathname + targetUrl.search;
if (mnoteNavigationInFlight === url) return;
if (typeof window.__mnoteDocumentPaneRuntime?.openPrimaryDocument === 'function') {
mnoteNavigationInFlight = url;
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
window.__mnoteDocumentPaneRuntime.openPrimaryDocument({
documentId: nodeId,
workspaceId: workspaceId || '',
sourceKind: targetUrl.searchParams.get('sourceKind') || 'convex_workspace',
rootUri: targetUrl.searchParams.get('rootUri') || '',
url: targetUrl,
}).then(function(){
if (mnoteNavigationInFlight === url) mnoteNavigationInFlight = '';
document.documentElement.removeAttribute('data-mnote-navigation-pending');
}).catch(function(error){
console.warn('mnote pane 内导航失败,将回退整页导航', error);
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
window.__mnoteTreeLiveEventSource.close();
}
window.location.assign(url);
});
return;
}
mnoteNavigationInFlight = url;
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
document.documentElement.setAttribute('data-mnote-navigation-target', nodeId);
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
window.__mnoteTreeLiveEventSource.close();
}
window.location.assign(url);
}
async function createPage(trigger, parentId) {
var workspaceId = resolveWorkspaceId(trigger);
var effectiveParentId = (parentId || trigger.getAttribute('data-parent-id') || '').trim();
if (!workspaceId) return;
var result = await dispatchTreeCommand(trigger, {
action: 'create',
workspaceId: workspaceId,
parentId: effectiveParentId || null,
title: '新页面'
});
var nextWorkspaceId = result.workspaceId || workspaceId;
navigateToDocument(result.documentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() });
}
function applySidebarTreeTab(mode, shell) {
mode = persistSidebarTreeMode(mode);
shell = shell || document.querySelector('[data-testid="wolai-sidebar-page-tree-shell"]');
if (!shell) return;
var tabs = shell.querySelectorAll('[data-mnote-sidebar-tree-tab]');
for (var i = 0; i < tabs.length; i++) {
var isActive = tabs[i].getAttribute('data-mnote-sidebar-tree-tab') === mode;
tabs[i].setAttribute('aria-selected', isActive ? 'true' : 'false');
tabs[i].classList.toggle('wolai-sidebar-tab-active', isActive);
tabs[i].classList.toggle('wolai-sidebar-tab-muted', !isActive);
}
var panels = shell.querySelectorAll('[data-mnote-sidebar-tree-panel]');
for (var j = 0; j < panels.length; j++) {
var isCurrentPanel = panels[j].getAttribute('data-mnote-sidebar-tree-panel') === mode;
panels[j].hidden = !isCurrentPanel;
}
}
function switchSidebarTreeTab(trigger) {
var mode = trigger ? trigger.getAttribute('data-mnote-sidebar-tree-tab') : 'page';
applySidebarTreeTab(mode, trigger ? trigger.closest('[data-testid="wolai-sidebar-page-tree-shell"]') : null);
}
function restoreSidebarTreeTab() {
applySidebarTreeTab(readStoredSidebarTreeMode(), null);
}
function updateTitleEverywhere(documentId, title) {
if (!documentId) return;
var escaped = cssEscape(documentId);
var escapedDocRowId = cssEscape('doc:' + documentId);
var isCurrentDocument = currentDocumentId() === documentId;
var pageSelectors = [
'.tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title',
'.wolai-page-row[data-node-id="' + escaped + '"] > .wolai-row-title',
'a[href="/documents/' + escaped + '"] > .wolai-row-title',
'a[href^="/documents/' + escaped + '?"] > .wolai-row-title'
];
pageSelectors.forEach(function(selector) {
document.querySelectorAll(selector).forEach(function(node) {
if (node instanceof HTMLElement) node.textContent = title;
});
});
document.querySelectorAll('.tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title').forEach(function(node) {
if (node instanceof HTMLElement) node.textContent = fileTreePageTitle(title);
});
document.querySelectorAll('[data-page-title-input="true"][data-document-id="' + escaped + '"]').forEach(function(node) {
if (!(node instanceof HTMLTextAreaElement)) return;
node.value = title;
node.setAttribute('data-title-last-saved', title);
node.setAttribute('data-title-save-status', 'saved');
node.style.height = 'auto';
node.style.height = Math.max(48, node.scrollHeight) + 'px';
});
document.querySelectorAll('[data-document-pane="true"][data-pane-document-id="' + escaped + '"] [data-page-title-current="true"]').forEach(function(node) {
if (node instanceof HTMLElement) node.textContent = title;
});
if (isCurrentDocument) {
document.title = title;
var topbarCurrent = document.querySelector('.wolai-breadcrumb-current [data-page-title-current]');
if (topbarCurrent instanceof HTMLElement) topbarCurrent.textContent = title;
}
}
function fileTreePageTitle(title) {
var normalized = String(title || '无标题').trim() || '无标题';
return normalized.endsWith('.md') ? normalized : normalized + '.md';
}
function isFileTreePageRow(row) {
if (!(row instanceof HTMLElement)) return false;
if (row.getAttribute('data-asset-id')) return false;
var rowKind = row.getAttribute('data-row-kind') || '';
return rowKind === 'document' || rowKind === 'doc' || rowKind === 'index' || rowKind === 'markdown';
}
function normalizeFileTreePageRenameTitle(value) {
var normalized = String(value || '').trim();
if (/\.md$/i.test(normalized)) normalized = normalized.slice(0, -3).trim();
return normalized;
}
function validateFileTreeRename(row, draft) {
var raw = String(draft || '').trim();
if (!raw) return '名称不能为空';
if (/[\\/:*?"<>|]/.test(raw)) return '文件名不能包含 / \\ : * ? " < > |';
if (!isFileTreePageRow(row)) return '';
var pageTitle = normalizeFileTreePageRenameTitle(raw);
if (!pageTitle) return '名称不能为空';
var expectedFileName = fileTreePageTitle(pageTitle).toLocaleLowerCase();
var rowId = row.getAttribute('data-row-id') || '';
var parentId = row.getAttribute('data-parent-id') || '';
var siblings = Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-kind="document"], #sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-kind="doc"], #sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-kind="markdown"]'));
var duplicate = siblings.some(function(sibling) {
if (!(sibling instanceof HTMLElement)) return false;
if ((sibling.getAttribute('data-row-id') || '') === rowId) return false;
if ((sibling.getAttribute('data-parent-id') || '') !== parentId) return false;
return fileTreePageTitle(normalizeFileTreePageRenameTitle(rowTitle(sibling))).toLocaleLowerCase() === expectedFileName;
});
return duplicate ? '同级已存在同名页面' : '';
}
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 commandDocumentId(result, fallback) {
var value = result && (
result.documentId ||
result.id ||
result.nodeId ||
(result.document && (result.document.id || result.document.documentId)) ||
(result.node && (result.node.id || result.node.documentId)) ||
(result.payload && result.payload.documentId)
);
return String(value || fallback || '').trim();
}
function commandDocumentTitle(result, fallback) {
var value = result && (
result.title ||
(result.document && result.document.title) ||
(result.node && result.node.title) ||
(result.payload && result.payload.title)
);
return String(value || fallback || '新页面').trim() || '新页面';
}
function normalizeDocumentRowId(value) {
return String(value || '').trim().replace(/^doc:/, '').replace(/^index:/, '');
}
function appendRenderedTreeNode(container, html) {
if (!container || !html) return false;
var empty = container.querySelector(':scope > .tree-empty');
if (empty && empty.parentElement) empty.parentElement.removeChild(empty);
var template = document.createElement('template');
template.innerHTML = html;
var node = template.content.firstElementChild;
if (!node) return false;
container.appendChild(node);
return true;
}
function localPageInsertDepth(parentId) {
if (!parentId) return 0;
var parentRow = document.querySelector(rowSelectorForDocument('page', parentId));
var depth = parentRow ? Number(parentRow.getAttribute('data-depth') || 0) : 0;
return Number.isFinite(depth) ? depth + 1 : 1;
}
function upsertPageDocumentRow(documentId, parentId, title) {
if (!documentId) return false;
var existing = document.querySelector(rowSelectorForDocument('page', documentId));
if (existing instanceof HTMLElement) {
updateTitleEverywhere(documentId, title);
moveDocumentRowForMode('page', documentId, parentId || null);
return true;
}
var root = treeRootForMode('page');
if (!root) return false;
var targetContainer = root;
if (parentId) {
var parentRow = document.querySelector(rowSelectorForDocument('page', parentId));
targetContainer = ensureTreeChildren(parentRow);
if (!targetContainer) targetContainer = root;
}
var grouped = new Map();
grouped.set(parentId || '', [{
id: documentId,
nodeId: documentId,
documentId: documentId,
parentNodeId: parentId || '',
rowKind: 'document',
title: title,
expandable: false,
childCount: 0
}]);
return appendRenderedTreeNode(targetContainer, renderPageRows(parentId || '', grouped, currentDocumentId(), localPageInsertDepth(parentId)));
}
function fileTreeParentContainer(parentId) {
var root = treeRootForMode('filetree');
if (!root) return null;
if (!parentId) return root;
var parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + parentId) + '"]');
var children = ensureTreeChildren(parentRow);
return children || root;
}
function localFileInsertDepth(parentId) {
if (!parentId) return 0;
var parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + parentId) + '"]');
var level = parentRow ? Number(parentRow.getAttribute('aria-level') || 1) : 1;
return Number.isFinite(level) ? level : 1;
}
function upsertFileDocumentRows(documentId, parentId, title) {
if (!documentId) return false;
var existing = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
if (existing instanceof HTMLElement) {
updateTitleEverywhere(documentId, title);
moveDocumentRowForMode('filetree', documentId, parentId || null);
return true;
}
var targetContainer = fileTreeParentContainer(parentId);
if (!targetContainer) return false;
var parentNodeId = parentId ? 'doc:' + parentId : '';
var docNodeId = 'doc:' + documentId;
var depth = localFileInsertDepth(parentId);
var grouped = new Map();
grouped.set(parentNodeId, [{
id: docNodeId,
nodeId: docNodeId,
rowId: docNodeId,
rowKind: 'document',
title: fileTreePageTitle(title),
documentId: documentId,
parentNodeId: parentNodeId,
depth: depth,
expandable: false,
childCount: 0
}]);
return appendRenderedTreeNode(targetContainer, renderFileRows(parentNodeId, grouped, currentDocumentId()));
}
function applyCreatedDocumentLocally(result, parentId, fallbackTitle) {
var documentId = commandDocumentId(result, null);
if (!documentId) return false;
var normalizedParentId = normalizeDocumentRowId(parentId);
var title = commandDocumentTitle(result, fallbackTitle);
var pageChanged = upsertPageDocumentRow(documentId, normalizedParentId, title);
var fileChanged = upsertFileDocumentRows(documentId, normalizedParentId, title);
if (pageChanged || fileChanged) {
document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'create');
}
return pageChanged || fileChanged;
}
function sortOrderFromDelta(data) {
var raw = data && (data.sortOrder ?? data.sort_order);
var value = Number(raw);
return Number.isFinite(value) && value >= 0 ? Math.floor(value) : null;
}
function insertTreeNodeAtSortOrder(targetContainer, node, sortOrder) {
if (!targetContainer || !node) return false;
if (sortOrder === null || sortOrder === undefined) {
targetContainer.appendChild(node);
return true;
}
var siblings = Array.from(targetContainer.querySelectorAll(':scope > .tree-node')).filter(function(candidate) {
return candidate !== node;
});
var targetIndex = Math.max(0, Math.min(Number(sortOrder), siblings.length));
var referenceNode = siblings[targetIndex] || null;
if (referenceNode) targetContainer.insertBefore(node, referenceNode);
else targetContainer.appendChild(node);
return true;
}
function moveDocumentRowForMode(mode, documentId, parentId, sortOrder) {
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');
}
return insertTreeNodeAtSortOrder(targetContainer, node, sortOrder);
}
function applyMoveDocumentDelta(data) {
var documentId = documentIdFromDelta(data);
if (!documentId) return false;
var parentId = parentIdFromDelta(data);
var sortOrder = sortOrderFromDelta(data);
var movedPage = moveDocumentRowForMode('page', documentId, parentId, sortOrder);
var movedFile = moveDocumentRowForMode('filetree', documentId, parentId, sortOrder);
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;
if (value.snapshot && value.snapshot.tree) return value.snapshot.tree;
if (value.data && value.data.tree) return value.data.tree;
if (value.tree && typeof value.tree === 'object') return value.tree;
return value;
}
function readSidebarDataset(value) {
if (!value || typeof value !== 'object') return null;
if (value.snapshot && value.snapshot.dataset && typeof value.snapshot.dataset === 'object') return value.snapshot.dataset;
if (value.data && value.data.dataset && typeof value.data.dataset === 'object') return value.data.dataset;
if (value.dataset && typeof value.dataset === 'object') return value.dataset;
if (value.sidebar && typeof value.sidebar === 'object') return value.sidebar;
return null;
}
function readDatasetProjection(value, snakeCaseKey, camelCaseKey) {
var dataset = readSidebarDataset(value);
if (!dataset || typeof dataset !== 'object') return null;
if (dataset[snakeCaseKey] && typeof dataset[snakeCaseKey] === 'object') return dataset[snakeCaseKey];
if (dataset[camelCaseKey] && typeof dataset[camelCaseKey] === 'object') return dataset[camelCaseKey];
return null;
}
function setTreeLiveApplyError(reason) {
document.documentElement.setAttribute('data-mnote-tree-live-apply-error', String(reason || 'tree_live_apply_failed'));
}
function projectionItems(projection) {
var resolved = readProjection(projection);
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();
}
function rowIdOf(item) {
return String(item && (item.rowId || item.nodeId || item.id) || '').trim();
}
function parentIdOf(item) {
return String(item && (item.parentNodeId || item.parentId || '') || '').trim();
}
function titleOf(item) {
return String(item && item.title || '无标题').trim() || '无标题';
}
function groupRowsByParent(rows) {
var ids = new Set(rows.map(nodeIdOf).filter(Boolean));
var grouped = new Map();
rows.forEach(function(item) {
var parentId = parentIdOf(item);
if (!ids.has(parentId)) parentId = '';
if (!grouped.has(parentId)) grouped.set(parentId, []);
grouped.get(parentId).push(item);
});
return grouped;
}
function pageTreeChevronSvg() {
return '<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>';
}
function renderPageRows(parentId, grouped, activeId, inheritedDepth) {
var computedDepth = Number(inheritedDepth || 0);
return (grouped.get(parentId) || []).map(function(item) {
var nodeId = nodeIdOf(item);
var title = titleOf(item);
var depth = computedDepth;
var parent = parentIdOf(item);
var children = grouped.get(nodeId) || [];
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
var expanded = expandable && item.expandedByDefault !== false;
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
var openable = Boolean(meta.documentId || item.documentId || !String(nodeId).startsWith('local-dir:'));
var toggle = expandable
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '" aria-expanded="' + String(expanded) + '">' + pageTreeChevronSvg() + '</button>'
: '<span class="tree-spacer" aria-hidden="true"></span>';
var childHtml = expandable
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
: '';
var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '"' + currentAttr + ' data-focused="false" data-page-openable="' + String(openable) + '" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '" data-page-openable="' + String(openable) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button></div></div>' + childHtml + '</li>';
}).join('');
}
function renderPageProjection(projection) {
var tree = document.getElementById('sidebar-tree-root');
if (!tree) return false;
var rows = projectionItems(projection).filter(function(item) {
return String(item.rowKind || 'document') === 'document';
});
var activeId = currentDocumentId();
var activeRowId = currentFileTreeActiveRowId();
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '<li class="tree-empty" data-rust-rendered-row="page-empty">暂无页面</li>') + '</ul>';
return true;
}
function fileDocumentId(item) {
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
if (meta.documentId) return String(meta.documentId).trim();
if (item && item.documentId) return String(item.documentId).trim();
if (item && item.rowKind === 'document') return nodeIdOf(item);
if (item && item.rowKind === 'index') return nodeIdOf(item).replace(/^index:/, '');
return '';
}
function fileAssetId(item) {
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
if (meta.assetId) return String(meta.assetId).trim();
if (item && item.assetId) return String(item.assetId).trim();
if (item && item.rowKind === 'asset') return nodeIdOf(item).replace(/^asset:/, '');
return '';
}
function fileObjectIdentity(item) {
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
if (meta.objectIdentity && typeof meta.objectIdentity === 'object') return meta.objectIdentity;
var rowKind = String(item && item.rowKind || '');
var documentId = fileDocumentId(item) || null;
var assetId = fileAssetId(item) || null;
var iconKind = iconKindOf(item);
var objectKind = rowKind === 'document' ? 'page' : rowKind === 'index' ? 'index' : iconKind === 'mindmap' ? 'mindmap' : 'attachment';
return { objectKind: objectKind, documentId: documentId, blockId: null, assetId: assetId };
}
function objectIdentityAttr(identity) {
try {
return JSON.stringify(identity || {});
} catch (_error) {
return '';
}
}
function iconKindOf(item) {
return String(item && (item.iconHint || item.rowKind || 'file') || 'file').trim() || 'file';
}
function isFileTreeProjectionPageRow(rowKind, assetId) {
if (assetId) return false;
return rowKind === 'document' || rowKind === 'doc' || rowKind === 'markdown';
}
function normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity) {
var title = String(rawTitle || '').trim();
var isMindmap = iconKind === 'mindmap' || String(objectIdentity && objectIdentity.objectKind || '') === 'mindmap';
var generated = /^mindmap[-_]/i.test(title);
if (!isMindmap || (!generated && title.length <= 24)) return title || '无标题';
return shortMindmapFileName(assetId || title);
}
function renderFileRows(parentId, grouped, activeId, activeRowId) {
return (grouped.get(parentId) || []).map(function(item) {
var nodeId = nodeIdOf(item);
var rowId = rowIdOf(item);
var rowKind = String(item.rowKind || 'document');
var rawTitle = titleOf(item);
var depth = Number(item.depth || 0);
var parent = parentIdOf(item);
var documentId = fileDocumentId(item);
var assetId = fileAssetId(item);
var objectIdentity = fileObjectIdentity(item);
var iconKind = iconKindOf(item);
var title = isFileTreeProjectionPageRow(rowKind, assetId)
? fileTreePageTitle(rawTitle)
: normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity);
var children = grouped.get(nodeId) || [];
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
var expanded = expandable && item.expandedByDefault !== false;
var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId;
var testId = rowKind === 'document' ? 'filetree-doc-row' : rowKind === 'index' ? 'filetree-index-row' : 'filetree-asset-row';
var toggle = expandable
? '<button type="button" class="tree-toggle" data-testid="filetree-toggle" data-rust-action="toggle" data-row-id="' + escapeHtml(rowId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '▸') + '</button>'
: '<span class="tree-spacer" aria-hidden="true"></span>';
var createAction = rowKind === 'document'
? '<button type="button" class="tree-action" data-testid="filetree-create" data-rust-action="create" data-row-id="' + escapeHtml(rowId) + '" data-node-id="' + escapeHtml(documentId || nodeId) + '" aria-label="新建子页面">+</button>'
: '';
var childHtml = expandable
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderFileRows(nodeId, grouped, activeId, activeRowId) + '</ul>'
: '';
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
}).join('');
}
function renderFileProjection(projection) {
var tree = document.getElementById('sidebar-file-tree-root');
if (!tree) return false;
var rows = projectionItems(projection);
var activeId = currentDocumentId();
var activeRowId = currentFileTreeActiveRowId();
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId, activeRowId) : '<li class="tree-empty" data-rust-rendered-row="filetree-empty">暂无文件或页面</li>') + '</ul>';
return true;
}
function renderSidebarSnapshot(payload) {
var renderedPage = hasProjectionItems(payload) ? renderPageProjection(payload) : false;
var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
var renderedFile = fileProjection && hasProjectionItems(fileProjection) ? renderFileProjection(fileProjection) : false;
return renderedPage || renderedFile;
}
function replaceSidebarTreeFromDocument(nextDocument, rootId) {
var current = document.getElementById(rootId);
var next = nextDocument ? nextDocument.getElementById(rootId) : null;
if (!(current instanceof HTMLElement) || !(next instanceof HTMLElement)) return false;
current.innerHTML = next.innerHTML;
Array.from(next.attributes || []).forEach(function(attr) {
current.setAttribute(attr.name, attr.value);
});
return true;
}
async function refreshLocalFolderSidebarSnapshot() {
var response = await fetch(window.location.href, { headers: { accept: 'text/html' } });
if (!response.ok) return false;
var html = await response.text();
var nextDocument = new DOMParser().parseFromString(html, 'text/html');
var renderedPage = replaceSidebarTreeFromDocument(nextDocument, 'sidebar-tree-root');
var renderedFile = replaceSidebarTreeFromDocument(nextDocument, 'sidebar-file-tree-root');
if (!renderedPage && !renderedFile) return false;
syncSidebarFileTreeSelection();
restoreSidebarTreeTab();
document.documentElement.setAttribute('data-mnote-local-folder-watch-applied', 'snapshot');
return true;
}
function startLocalFolderSidebarWatch() {
var params = new URLSearchParams(window.location.search);
if ((params.get('sourceKind') || '').trim() !== 'local_folder') return;
var rootUri = (params.get('rootUri') || '').trim();
if (!rootUri) return;
var revision = '';
var refreshTimer = 0;
var scheduleRefresh = function() {
if (refreshTimer) return;
refreshTimer = window.setTimeout(function() {
refreshTimer = 0;
void refreshLocalFolderSidebarSnapshot();
}, 180);
};
var poll = async function() {
if (document.hidden) return;
var url = new URL('/api/tree/local-folder-watch', window.location.origin);
url.searchParams.set('rootUri', rootUri);
var response = await fetch(url.toString(), { headers: { accept: 'application/json' } });
if (!response.ok) return;
var payload = await response.json();
var nextRevision = payload && payload.result && typeof payload.result.revision === 'string'
? payload.result.revision
: '';
if (!nextRevision) return;
if (!revision) {
revision = nextRevision;
return;
}
if (nextRevision !== revision) {
revision = nextRevision;
scheduleRefresh();
}
};
window.setInterval(function() {
void poll();
}, 1200);
void poll();
}
function isTitleOnlyDocumentPatch(candidate) {
if (!candidate || typeof candidate !== 'object') return false;
var allowedKeys = {
id: true,
documentId: true,
title: true,
updatedAt: true,
updated_at: true
};
return Object.keys(candidate).every(function(key) {
return allowedKeys[key] === true;
});
}
function deltaNeedsProjectionRefresh(payload) {
var data = payload && (payload.data || payload.delta || payload);
var op = data && typeof data.op === 'string' ? String(data.op).trim() : '';
if (!op || op === 'noop') return false;
if (op === 'upsert_document') {
return !isTitleOnlyDocumentPatch(data.node || data.document || null);
}
if (op === 'upsert_documents') {
var documents = Array.isArray(data.upsertDocuments) ? data.upsertDocuments : Array.isArray(data.upsert_documents) ? data.upsert_documents : [];
if (documents.length > 0 && documents.every(isTitleOnlyDocumentPatch)) {
return false;
}
}
return true;
}
function toggleChildren(row, button) {
var li = row && row.parentElement;
var children = li ? li.querySelector(':scope > .tree-children') : null;
if (!children) return;
children.classList.toggle('tree-children--collapsed');
var collapsed = children.classList.contains('tree-children--collapsed');
row.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
if (button && button.getAttribute('data-testid') === 'tree-node-toggle') {
button.setAttribute('aria-expanded', collapsed ? 'false' : 'true');
} else if (button) {
button.textContent = collapsed ? '▸' : '▾';
}
}
function dispatchSidebarEvent(name, detail) {
document.documentElement.setAttribute('data-mnote-last-tree-action', name);
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
}
function inferOnlyOfficeFileType(fileName, mimeType) {
var name = String(fileName || '').trim().toLowerCase();
var mt = String(mimeType || '').trim().toLowerCase();
var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : '';
if (['doc', 'docx', 'odt', 'rtf'].indexOf(ext) >= 0) return ext;
if (['ppt', 'pptx', 'odp'].indexOf(ext) >= 0) return ext;
if (['xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return ext;
if (isNonOfficeAttachmentName(name, ext)) return '';
if (mt.indexOf('wordprocessingml') >= 0) return 'docx';
if (mt.indexOf('presentationml') >= 0) return 'pptx';
if (mt.indexOf('spreadsheetml') >= 0) return 'xlsx';
return '';
}
function buildOnlyOfficeOpenUrl(input) {
var target = new URL('/onlyoffice', window.location.origin);
target.searchParams.set('fileUrl', input.fileUrl || '');
target.searchParams.set('fileName', input.fileName || '未命名资源');
target.searchParams.set('fileType', input.fileType || 'docx');
if (input.assetId) target.searchParams.set('assetId', input.assetId);
if (input.documentId) target.searchParams.set('documentId', input.documentId);
if (input.userId) target.searchParams.set('userId', input.userId);
target.searchParams.set('mode', input.mode || 'edit');
return target.toString();
}
function buildOnlyOfficeOpenPath(input) {
var params = new URLSearchParams();
params.set('fileUrl', input.fileUrl || '');
params.set('fileName', input.fileName || '未命名资源');
params.set('fileType', input.fileType || 'docx');
if (input.assetId) params.set('assetId', input.assetId);
if (input.documentId) params.set('documentId', input.documentId);
if (input.userId) params.set('userId', input.userId);
params.set('mode', input.mode || 'edit');
return '/onlyoffice?' + params.toString();
}
function buildMindmapOpenPath(documentId, assetId) {
var doc = String(documentId || '').trim();
var map = String(assetId || '').trim();
if (!doc || !map) return '';
return '/mindmap/' + encodeURIComponent(doc) + '/' + encodeURIComponent(map);
}
function navigateToMindmapObject(documentId, assetId, workspaceId) {
var mindmapPath = buildMindmapOpenPath(documentId, assetId);
if (!mindmapPath) return false;
var targetUrl = new URL(mindmapPath, window.location.origin);
var url = targetUrl.pathname + targetUrl.search;
if (mnoteNavigationInFlight === url) return true;
if (typeof window.__mnoteDocumentPaneRuntime?.openPrimaryMindmap === 'function') {
mnoteNavigationInFlight = url;
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
document.documentElement.setAttribute('data-mnote-navigation-target', assetId);
window.__mnoteDocumentPaneRuntime.openPrimaryMindmap({
documentId: documentId,
mindmapId: assetId,
workspaceId: workspaceId || '',
url: targetUrl
}).then(function(){
if (mnoteNavigationInFlight === url) mnoteNavigationInFlight = '';
document.documentElement.removeAttribute('data-mnote-navigation-pending');
}).catch(function(error){
console.warn('mnote mindmap pane 内导航失败,将回退整页导航', error);
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
window.__mnoteTreeLiveEventSource.close();
}
window.location.assign(url);
});
return true;
}
mnoteNavigationInFlight = url;
document.documentElement.setAttribute('data-mnote-navigation-pending', 'true');
document.documentElement.setAttribute('data-mnote-navigation-target', assetId);
if (window.__mnoteTreeLiveEventSource && typeof window.__mnoteTreeLiveEventSource.close === 'function') {
window.__mnoteTreeLiveEventSource.close();
}
window.location.assign(url);
return true;
}
function isMindmapAssetDetail(detail) {
var assetId = String(detail && detail.assetId || '').trim();
var assetType = String(detail && detail.assetType || '').trim();
if (assetType === 'mindmap') return true;
return assetId.indexOf('mindmap_') === 0 || assetId.indexOf('mindmap-') === 0;
}
function localFilePathFromAssetId(assetId) {
var value = String(assetId || '').trim();
return value.indexOf('local-file:') === 0 ? value.slice('local-file:'.length) : '';
}
function buildLocalFileOpenUrl(relativePath, download) {
var rootUri = currentRootUri();
if (!rootUri || !relativePath) return '';
var url = new URL('/api/local-folder/files/open', window.location.origin);
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('path', relativePath);
if (download) url.searchParams.set('download', 'true');
return url.toString();
}
function readFileTreeObjectIdentity(row) {
if (!row) return null;
var raw = row.getAttribute('data-object-identity') || '';
if (!raw) return null;
try {
var parsed = JSON.parse(raw);
return parsed && typeof parsed === 'object' ? parsed : null;
} catch (_error) {
return null;
}
}
async function fetchCurrentOnlyOfficeUserId() {
try {
var response = await fetch('/api/auth/whoami', {
method: 'GET',
credentials: 'include',
cache: 'no-store'
});
var payload = await response.json().catch(function() { return null; });
return String(payload && payload.userId || '').trim();
} catch (_error) {
return '';
}
}
async function openConvexAssetFromFileTree(detail) {
var assetId = String(detail && detail.assetId || '').trim();
if (!assetId) return;
var localFilePath = localFilePathFromAssetId(assetId);
if (localFilePath) {
var localFileUrl = buildLocalFileOpenUrl(localFilePath, false);
if (localFileUrl) window.open(localFileUrl, '_blank', 'noopener,noreferrer');
return;
}
var documentId = String(detail && detail.documentId || '').trim();
if (isMindmapAssetDetail(detail) && documentId) {
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-object-shell');
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId);
navigateToMindmapObject(documentId, assetId, String(detail.workspaceId || '').trim());
return;
}
try {
var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
method: 'GET',
credentials: 'include'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) {
throw new Error(payload && payload.error ? payload.error : '生成签名链接失败');
}
var asset = payload && payload.asset && typeof payload.asset === 'object' ? payload.asset : {};
var fileUrl = String(payload && payload.signedUrl || asset.file_url || asset.signed_url || '').trim();
if (!fileUrl) throw new Error('附件链接不可用');
var fileName = String(asset.file_name || detail.fileName || '未命名资源').trim() || '未命名资源';
var fileType = inferOnlyOfficeFileType(fileName, asset.mime_type);
if (fileType) {
var userId = await fetchCurrentOnlyOfficeUserId();
window.open(buildOnlyOfficeOpenUrl({
fileUrl: fileUrl,
fileName: fileName,
fileType: fileType,
assetId: assetId,
documentId: String(asset.document_id || detail.documentId || '').trim(),
userId: userId,
mode: 'edit'
}), '_blank', 'noopener,noreferrer');
return;
}
if (isCodeAttachmentFileName(fileName) && String(asset.document_id || detail.documentId || '').trim() === currentDocumentId()) {
await openCodeEditorAttachment({
href: fileUrl,
fileUrl: fileUrl,
fileName: fileName,
assetId: assetId,
documentId: String(asset.document_id || detail.documentId || '').trim(),
fileSize: uploadedFileSize(asset)
});
return;
}
window.open(fileUrl, '_blank', 'noopener,noreferrer');
} catch (error) {
window.alert(error && error.message ? error.message : '打开附件失败');
}
}
window.addEventListener('tree.asset.open', function(event) {
void openConvexAssetFromFileTree(event.detail || {});
});
function fileTreeRowsForUploadPreflight() {
return Array.from(document.querySelectorAll('.tree-row[data-shell-mode="filetree"]')).map(function(row) {
return {
rowId: row.getAttribute('data-row-id') || '',
rowKind: row.getAttribute('data-row-kind') || '',
documentId: row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || null,
assetId: row.getAttribute('data-asset-id') || null,
assetDocumentId: row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || null,
assetType: row.querySelector('.tree-kind-badge') ? row.querySelector('.tree-kind-badge').getAttribute('data-kind') : null,
storagePath: null
};
}).filter(function(row) {
return row.rowId || row.documentId || row.assetId;
});
}
function fileTreeDocumentWorkspacesForUploadPreflight(workspaceId) {
var seen = new Set();
return fileTreeRowsForUploadPreflight().filter(function(row) {
if (!row.documentId || seen.has(row.documentId)) return false;
seen.add(row.documentId);
return true;
}).map(function(row) {
return { documentId: row.documentId, workspaceId: workspaceId || null };
});
}
async function preflightFileTreeUploadTarget(detail) {
var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim();
var body = {
workspaceId: workspaceId || null,
targetDocumentId: detail && detail.documentId ? String(detail.documentId) : null,
targetRowId: detail && detail.targetRowId ? String(detail.targetRowId) : null,
focusedRowId: sidebarFileTreeSelection.focusedRowId || null,
activeDocumentId: currentDocumentId() || null,
rows: fileTreeRowsForUploadPreflight(),
documentWorkspaces: fileTreeDocumentWorkspacesForUploadPreflight(workspaceId)
};
var response = await fetch('/api/tree/filetree/upload-target-preflight', {
method: 'POST',
credentials: 'include',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body)
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload || !payload.plan) {
throw new Error(payload && payload.error ? payload.error : '文件树上传目标预检失败');
}
return payload.plan;
}
function fallbackFileTreeUploadTarget(detail) {
var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim();
var documentId = String(detail && detail.documentId || currentDocumentId() || '').trim();
if (!workspaceId || !documentId) {
throw new Error('请选择一个目标页面后再拖入文件');
}
return {
workspaceId: workspaceId,
targetDocumentId: documentId,
targetMindmapId: null,
targetSubPath: null
};
}
async function resolveFileTreeUploadTarget(detail) {
try {
return await preflightFileTreeUploadTarget(detail || {});
} catch (error) {
console.warn('[mnote upload] upload target preflight fallback', error);
return fallbackFileTreeUploadTarget(detail || {});
}
}
function uploadedAssetTitle(asset) {
return String(asset && (asset.file_name || asset.title || asset.name) || '未命名附件').trim() || '未命名附件';
}
function uploadedAssetUrl(asset) {
return String(asset && (asset.sourcePath || asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim();
}
function uploadedAssetType(asset) {
return String(asset && (asset.asset_type || asset.assetType || asset.mime_type || '') || '').trim();
}
function isLocalUploadedAsset(asset) {
var id = String(asset && asset.id || '').trim();
return String(asset && asset.sourceKind || '').trim() === 'local_folder' || id.indexOf('local:asset:') === 0;
}
function uploadedAssetExtension(asset) {
var match = uploadedAssetTitle(asset).toLowerCase().match(/\.([a-z0-9]+)$/);
return match ? match[1] : '';
}
function isNonOfficeAttachmentName(name, ext) {
var codeFileNames = [
'.dockerignore', '.editorconfig', '.env', '.eslintrc', '.gitattributes', '.gitignore', '.npmrc', '.prettierrc',
'dockerfile', 'makefile', 'cmakelists.txt', 'gemfile', 'rakefile', 'procfile'
];
return [
'pdf', 'toml', 'json', 'yaml', 'yml', 'md', 'markdown', 'txt', 'ini', 'env', 'xml', 'html', 'htm', 'css', 'scss',
'less', 'js', 'jsx', 'ts', 'tsx', 'mjs', 'cjs', 'py', 'rs', 'go', 'java', 'c', 'cpp', 'h', 'hpp', 'cs',
'php', 'rb', 'sh', 'bash', 'zsh', 'sql', 'lock', 'log', 'vue', 'svelte', 'astro', 'jsonc', 'json5',
'mts', 'cts', 'lua', 'dart', 'kt', 'kts', 'swift', 'scala', 'gradle', 'groovy', 'clj',
'ex', 'exs', 'erl', 'hrl', 'fs', 'fsx', 'r', 'jl', 'm', 'mm', 'pl', 'pm', 'ps1', 'bat', 'cmd',
'psm1', 'psd1', 'dockerfile', 'containerfile', 'proto', 'graphql', 'gql', 'prisma', 'tf', 'tfvars',
'hcl', 'nix', 'cmake', 'bazel', 'bzl', 'properties', 'conf', 'cfg', 'config', 'service', 'desktop',
'gitignore', 'gitattributes', 'editorconfig', 'npmrc', 'prettierrc', 'eslintrc'
].indexOf(ext) >= 0 || codeFileNames.indexOf(name) >= 0;
}
function attachmentExtensionFromFileName(fileName) {
var name = String(fileName || '').trim().toLowerCase();
return name.indexOf('.') >= 0 ? name.split('.').pop() : '';
}
function isPdfAttachmentFileName(fileName) {
return attachmentExtensionFromFileName(fileName) === 'pdf';
}
function isCodeAttachmentFileName(fileName) {
var name = String(fileName || '').trim().toLowerCase();
var ext = attachmentExtensionFromFileName(name);
return ext !== 'pdf' && isNonOfficeAttachmentName(name, ext);
}
function inferCodeAttachmentLanguage(fileName) {
var name = String(fileName || '').trim().toLowerCase();
var ext = attachmentExtensionFromFileName(name);
var byName = {
'dockerfile': 'dockerfile',
'makefile': 'makefile',
'cmakelists.txt': 'cmake',
'.gitignore': 'gitignore',
'.gitattributes': 'gitattributes',
'.editorconfig': 'ini',
'.env': 'dotenv'
};
if (byName[name]) return byName[name];
var byExt = {
bash: 'bash', bat: 'batch', c: 'c', cjs: 'javascript', cmd: 'batch', conf: 'text', cpp: 'cpp',
cs: 'csharp', css: 'css', cts: 'typescript', dart: 'dart', dockerfile: 'dockerfile', env: 'dotenv',
go: 'go', gql: 'graphql', gradle: 'groovy', graphql: 'graphql', h: 'c', hcl: 'hcl', hpp: 'cpp',
htm: 'html', html: 'html', ini: 'ini', java: 'java', js: 'javascript', json: 'json', json5: 'json',
jsonc: 'jsonc', jsx: 'javascript', kt: 'kotlin', kts: 'kotlin', less: 'less', log: 'text',
lua: 'lua', m: 'objective-c', markdown: 'markdown', md: 'markdown', mjs: 'javascript',
mts: 'typescript', nix: 'nix', php: 'php', pl: 'perl', pm: 'perl', prisma: 'prisma',
proto: 'protobuf', ps1: 'powershell', py: 'python', r: 'r', rb: 'ruby', rs: 'rust',
scss: 'scss', sh: 'bash', sql: 'sql', svelte: 'svelte', swift: 'swift', tf: 'terraform',
tfvars: 'terraform', toml: 'toml', ts: 'typescript', tsx: 'typescript', txt: 'text',
vue: 'vue', xml: 'xml', yaml: 'yaml', yml: 'yaml', zsh: 'bash'
};
return byExt[ext] || 'text';
}
function attachmentClassForFileName(fileName) {
var name = String(fileName || '').trim().toLowerCase();
var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : '';
if (['doc', 'docx', 'odt', 'rtf'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-word';
if (['ppt', 'pptx', 'odp'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-ppt';
if (['xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-sheet';
if (ext === 'pdf') return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-pdf';
if (isNonOfficeAttachmentName(name, ext)) {
return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-code';
}
return 'mnote-uploaded-attachment-row mnote-uploaded-attachment-file';
}
function uploadedAttachmentClass(asset) {
return attachmentClassForFileName(uploadedAssetTitle(asset));
}
function buildOnlyOfficeAssetOpenUrl(asset, userId) {
if (isLocalUploadedAsset(asset)) return '';
var title = uploadedAssetTitle(asset);
var fileType = inferOnlyOfficeFileType(title, asset && asset.mime_type);
if (!fileType) return '';
var assetId = String(asset && asset.id || '').trim();
return buildOnlyOfficeOpenUrl({
fileUrl: assetId ? '' : uploadedAssetUrl(asset),
fileName: title,
fileType: fileType,
assetId: assetId,
documentId: String(asset && (asset.document_id || asset.documentId) || currentDocumentId() || '').trim(),
userId: userId || '',
mode: 'edit'
});
}
function uploadedFileSize(asset) {
var size = Number(asset && (asset.file_size || asset.fileSize) || 0);
if (!Number.isFinite(size) || size <= 0) return '';
if (size >= 1024 * 1024) return (size / 1024 / 1024).toFixed(size >= 10 * 1024 * 1024 ? 1 : 2) + ' MB';
if (size >= 1024) return (size / 1024).toFixed(size >= 100 * 1024 ? 0 : 2) + ' KB';
return String(Math.round(size)) + ' B';
}
var attachmentMetaCache = Object.create(null);
var attachmentMetaPending = Object.create(null);
var legacyOfficeAttachmentIndex = null;
var legacyOfficeAttachmentIndexPending = null;
function parseCurrentWorkspaceId() {
return (new URLSearchParams(window.location.search).get('workspaceId') || '').trim();
}
async function fetchLegacyOfficeAttachmentIndex() {
if (legacyOfficeAttachmentIndex) return legacyOfficeAttachmentIndex;
if (legacyOfficeAttachmentIndexPending) return legacyOfficeAttachmentIndexPending;
var documentId = currentDocumentId();
var workspaceId = parseCurrentWorkspaceId();
if (!documentId || !workspaceId) {
legacyOfficeAttachmentIndex = Object.create(null);
return legacyOfficeAttachmentIndex;
}
legacyOfficeAttachmentIndexPending = fetch(
'/api/tree/projections/file?documentId=' + encodeURIComponent(documentId) + '&workspaceId=' + encodeURIComponent(workspaceId),
{
method: 'GET',
credentials: 'include',
cache: 'no-store'
}
).then(function(response) {
return response.json().catch(function() { return null; }).then(function(payload) {
var items = payload && payload.ok && payload.result && Array.isArray(payload.result.items)
? payload.result.items
: [];
var index = Object.create(null);
items.forEach(function(item) {
if (!item || item.rowKind !== 'asset') return;
var title = String(item.title || '').trim();
if (!title || index[title]) return;
var fileType = inferOnlyOfficeFileType(title, '');
if (!fileType) return;
var rowId = String(item.rowId || '').trim();
var assetId = String(item.assetId || '').trim();
if (!assetId && rowId.indexOf('asset:') === 0) assetId = rowId.slice('asset:'.length);
if (!assetId) return;
index[title] = {
assetId: assetId,
fileName: title,
fileType: fileType,
documentId: documentId
};
});
legacyOfficeAttachmentIndex = index;
return index;
});
}).catch(function() {
var empty = Object.create(null);
legacyOfficeAttachmentIndex = empty;
return empty;
}).finally(function() {
legacyOfficeAttachmentIndexPending = null;
});
return legacyOfficeAttachmentIndexPending;
}
async function healLegacyOfficeAttachmentParagraphs() {
var editor = document.querySelector('.editor-surface .ProseMirror');
if (!(editor instanceof HTMLElement)) return;
var index = await fetchLegacyOfficeAttachmentIndex();
var paragraphs = Array.from(editor.querySelectorAll('p'));
paragraphs.forEach(function(paragraph) {
if (!(paragraph instanceof HTMLParagraphElement)) return;
if (paragraph.querySelector('a, img, video, audio, table, iframe, canvas')) return;
if (paragraph.childNodes.length !== 1 || paragraph.firstChild?.nodeType !== Node.TEXT_NODE) return;
var fileName = String(paragraph.textContent || '').trim();
if (!fileName) return;
var detail = index[fileName];
if (!detail) return;
var link = document.createElement('a');
link.textContent = fileName;
link.setAttribute('href', buildOnlyOfficeOpenPath({
fileUrl: '',
fileName: detail.fileName,
fileType: detail.fileType,
assetId: detail.assetId,
documentId: detail.documentId || currentDocumentId() || '',
userId: '',
mode: 'edit'
}));
link.setAttribute('data-mnote-attachment-link', 'true');
link.setAttribute('data-asset-id', detail.assetId);
attachmentClassForFileName(fileName).split(/\s+/).forEach(function(name) {
if (name) link.classList.add(name);
});
paragraph.replaceChildren(link);
enhanceEditorAttachmentLink(link);
});
}
function applyEditorAttachmentMeta(link, meta) {
if (!(link instanceof HTMLAnchorElement) || !meta) return;
if (meta.assetId) link.setAttribute('data-asset-id', meta.assetId);
if (meta.fileSize) link.setAttribute('data-file-size', meta.fileSize);
}
async function hydrateEditorAttachmentMeta(link) {
if (!(link instanceof HTMLAnchorElement)) return;
var detail = detailFromEditorAttachmentLink(link);
var assetId = String(detail && detail.assetId || '').trim();
if (!assetId) return;
if (attachmentMetaCache[assetId]) {
applyEditorAttachmentMeta(link, attachmentMetaCache[assetId]);
return;
}
if (attachmentMetaPending[assetId]) {
try { await attachmentMetaPending[assetId]; } catch (_) {}
applyEditorAttachmentMeta(link, attachmentMetaCache[assetId]);
return;
}
attachmentMetaPending[assetId] = fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
method: 'GET',
credentials: 'include',
cache: 'no-store'
}).then(function(response) {
return response.json().catch(function() { return null; }).then(function(payload) {
if (!response.ok || !payload) return null;
var asset = payload.asset && typeof payload.asset === 'object' ? payload.asset : {};
var meta = {
assetId: assetId,
fileSize: uploadedFileSize(asset)
};
attachmentMetaCache[assetId] = meta;
return meta;
});
}).catch(function() {
return null;
}).finally(function() {
delete attachmentMetaPending[assetId];
});
try {
var meta = await attachmentMetaPending[assetId];
applyEditorAttachmentMeta(link, meta);
} catch (_) {}
}
function revealFileTreeRow(row) {
if (!(row instanceof HTMLElement)) return;
var node = row.closest('.tree-node');
while (node && node.parentElement) {
if (node.parentElement.classList && node.parentElement.classList.contains('tree-children')) {
node.parentElement.classList.remove('tree-children--collapsed');
var parentNode = node.parentElement.closest('.tree-node');
var parentRow = parentNode ? parentNode.querySelector(':scope > .tree-row') : null;
if (parentRow instanceof HTMLElement) {
parentRow.setAttribute('aria-expanded', 'true');
var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
if (toggle) toggle.setAttribute('aria-expanded', 'true');
}
}
node = node.parentElement.closest('.tree-node');
}
try { row.scrollIntoView({ block: 'nearest' }); } catch (_) {}
}
function revealFileTreeAssetRow(assetId) {
if (!assetId) return false;
var row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(assetId) + '"]');
if (!(row instanceof HTMLElement)) return false;
revealFileTreeRow(row);
return true;
}
function appendUploadedAssetRow(asset, documentId) {
var assetId = String(asset && asset.id || '').trim();
if (!assetId) return;
if (revealFileTreeAssetRow(assetId)) return;
var targetDocumentId = String(documentId || asset.document_id || asset.documentId || currentDocumentId() || '').trim();
var parentRow = targetDocumentId
? document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + targetDocumentId) + '"]')
: null;
if (!parentRow) parentRow = document.querySelector('.tree-row[data-shell-mode="filetree"][data-row-kind="document"]');
var root = document.querySelector('#sidebar-file-tree-root .tree-root');
if (!root && !parentRow) return;
var parentLi = parentRow ? parentRow.closest('.tree-node') : null;
var children = parentLi ? parentLi.querySelector(':scope > .tree-children') : null;
if (parentLi && !children) {
children = document.createElement('ul');
children.className = 'tree-children';
parentLi.appendChild(children);
}
if (children) {
children.classList.remove('tree-children--collapsed');
if (parentRow) {
parentRow.setAttribute('aria-expanded', 'true');
var toggle = parentRow.querySelector('[data-rust-action="toggle"]');
if (toggle) toggle.setAttribute('aria-expanded', 'true');
}
}
var container = children || root;
var li = document.createElement('li');
li.className = 'tree-node';
li.setAttribute('data-node-id', 'asset:' + assetId);
var title = uploadedAssetTitle(asset);
var iconKind = uploadedAssetType(asset) || 'file';
li.innerHTML =
'<div class="tree-row" role="treeitem" aria-level="' + (parentRow ? '2' : '1') + '" aria-expanded="false" data-rust-rendered-row="filetree" data-testid="filetree-asset-row" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-row-kind="asset" data-node-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-doc-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-shell-mode="filetree" data-selected="false" data-active="false" draggable="true">' +
'<span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span>' +
'<button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button>' +
'<div class="tree-actions"><button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml('asset:' + assetId) + '" aria-label="更多操作">…</button></div></div>';
container.appendChild(li);
revealFileTreeRow(li.querySelector('.tree-row'));
document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId);
}
function removeFileTreeAssetRow(assetId) {
var normalized = String(assetId || '').trim();
if (!normalized) return false;
var row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(normalized) + '"]');
var node = row ? row.closest('.tree-node') : null;
if (!node || !node.parentElement) return false;
node.parentElement.removeChild(node);
return true;
}
function applyAssetsChangedToFileTree(detail) {
detail = detail || {};
var docId = String(detail.docId || detail.documentId || currentDocumentId() || '').trim();
var changed = false;
var explicitAsset = detail.asset && typeof detail.asset === 'object' ? detail.asset : null;
if (explicitAsset) {
appendUploadedAssetRow(explicitAsset, docId);
changed = true;
}
var mindmapAssetIds = Array.isArray(detail.mindmapAssetIds) ? detail.mindmapAssetIds : [];
if (detail.mindmapDeleted === true) {
mindmapAssetIds.forEach(function(assetId) {
if (removeFileTreeAssetRow(assetId)) changed = true;
});
}
if (changed) {
document.documentElement.setAttribute('data-mnote-assets-local-applied', 'true');
}
}
function mindmapAssetFromTarget(documentId, mindmapId) {
var docId = String(documentId || currentDocumentId() || '').trim();
var assetId = String(mindmapId || '').trim();
if (!docId || !assetId) return null;
var fileName = shortMindmapFileName(assetId);
return {
id: assetId,
document_id: docId,
asset_type: 'mindmap',
file_name: fileName,
file_url: '/documents/' + encodeURIComponent(docId) + '/' + encodeURIComponent(fileName)
};
}
function shortMindmapFileName(mindmapId) {
var raw = String(mindmapId || '').trim();
var digits = raw.match(/(\d{4,})$/);
var suffix = digits ? digits[1].slice(-4) : raw.replace(/^mindmap[_-]?/i, '').slice(-6);
return suffix ? '思维导图-' + suffix + '.json' : '思维导图.json';
}
function parseMindmapApiTarget(input) {
try {
var rawUrl = typeof input === 'string'
? input
: input && typeof input.url === 'string'
? input.url
: '';
if (!rawUrl) return null;
var url = new URL(rawUrl, window.location.origin);
var match = /^\/api\/mindmap\/([^/]+)\/([^/]+)$/.exec(url.pathname);
if (!match) return null;
return {
documentId: decodeURIComponent(match[1]),
mindmapId: decodeURIComponent(match[2])
};
} catch (_) {
return null;
}
}
function requestMethod(input, init) {
return String(
init && init.method
? init.method
: input && typeof input.method === 'string'
? input.method
: 'GET'
).toUpperCase();
}
function requestBodyHasMindmapCreateOnly(init) {
var body = init && typeof init.body === 'string' ? init.body : '';
if (!body) return false;
try {
var payload = JSON.parse(body);
return payload && payload.createOnly === true;
} catch (_) {
return false;
}
}
function applyMindmapApiMutationToFileTree(target) {
if (!target || !target.documentId || !target.mindmapId) return;
var asset = mindmapAssetFromTarget(target.documentId, target.mindmapId);
if (!asset) return;
appendUploadedAssetRow(asset, target.documentId);
document.documentElement.setAttribute('data-mnote-assets-local-applied', 'true');
document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', target.mindmapId);
}
function installMindmapAssetFetchObserver() {
if (window.__mnoteMindmapAssetFetchObserverInstalled === true) return;
if (typeof window.fetch !== 'function') return;
window.__mnoteMindmapAssetFetchObserverInstalled = true;
var originalFetch = window.fetch.bind(window);
window.fetch = function(input, init) {
var target = parseMindmapApiTarget(input);
var method = requestMethod(input, init);
var createOnly = requestBodyHasMindmapCreateOnly(init || {});
return originalFetch(input, init).then(function(response) {
if (target && method === 'POST' && response && response.ok) {
if (createOnly || target.documentId === currentDocumentId()) {
applyMindmapApiMutationToFileTree(target);
}
}
return response;
});
};
}
async function insertUploadedAssetIntoEditor(asset) {
var editorRoot = document.querySelector('.editor-surface .ProseMirror');
var editor = editorRoot && editorRoot.editor;
if (!editor || !editor.chain) return false;
var title = uploadedAssetTitle(asset);
var url = uploadedAssetUrl(asset);
var type = uploadedAssetType(asset);
var assetId = String(asset && asset.id || '').trim();
var sizeLabel = uploadedFileSize(asset);
try {
if (type === 'image' && url) {
return editor.chain().focus().setImage({ src: url, alt: title, title: title }).run() === true;
}
var isLocalAsset = isLocalUploadedAsset(asset);
var userId = '';
var onlyOfficeUrl = isLocalAsset ? '' : buildOnlyOfficeAssetOpenUrl(asset, userId);
if (onlyOfficeUrl && assetId) {
userId = await fetchCurrentOnlyOfficeUserId();
onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId);
}
var href = onlyOfficeUrl || url;
if (href) {
var storedHref = onlyOfficeUrl
? buildOnlyOfficeOpenPath({
fileUrl: '',
fileName: title,
fileType: inferOnlyOfficeFileType(title, asset && asset.mime_type) || 'docx',
assetId: assetId,
documentId: String(asset && (asset.document_id || asset.documentId) || currentDocumentId() || '').trim(),
userId: userId || '',
mode: 'edit'
})
: href;
var inserted = editor.chain().focus().insertContent({
type: 'paragraph',
content: [{
type: 'text',
text: title,
marks: [{
type: 'link',
attrs: {
href: storedHref,
target: '_blank',
rel: 'noopener noreferrer nofollow',
class: uploadedAttachmentClass(asset)
}
}]
}]
}).run() === true;
window.setTimeout(function() {
enhanceEditorAttachmentLinks();
var selector = assetId
? '.editor-surface .ProseMirror a[href*="' + cssEscape(assetId) + '"]'
: '.editor-surface .ProseMirror a';
var link = document.querySelector(selector);
if (link instanceof HTMLElement) {
link.setAttribute('data-mnote-attachment-link', 'true');
if (assetId) link.setAttribute('data-asset-id', assetId);
if (sizeLabel) link.setAttribute('data-file-size', sizeLabel);
}
}, 0);
return inserted;
}
} catch (error) {
console.warn('[mnote upload] insert uploaded asset failed', error);
}
return false;
}
async function uploadFileToMediaAsset(file, plan, options) {
if (currentSourceKind() === 'local_folder') {
var rootUri = (new URLSearchParams(window.location.search).get('rootUri') || '').trim();
var documentId = String(plan && plan.targetDocumentId || currentDocumentId() || '').trim();
if (!rootUri || !documentId) {
throw new Error('本地 Markdown 上传缺少 rootUri 或 documentId');
}
var localForm = new FormData();
localForm.append('file', file);
localForm.append('rootUri', rootUri);
localForm.append('documentId', documentId);
localForm.append('kind', file && String(file.type || '').indexOf('image/') === 0 ? 'image' : 'attachment');
var localResponse = await fetch('/api/local-folder/assets/upload', {
method: 'POST',
credentials: 'include',
body: localForm
});
var localPayload = await localResponse.json().catch(function() { return null; });
if (!localResponse.ok || !localPayload || !localPayload.asset) {
throw new Error(localPayload && localPayload.error ? localPayload.error : '上传失败');
}
if (options && options.insertIntoEditor) {
await insertUploadedAssetIntoEditor(localPayload.asset);
}
void refreshLocalFolderSidebarSnapshot();
window.dispatchEvent(new CustomEvent('wolai:local-assets-changed', {
detail: { docId: documentId, asset: localPayload.asset, assetIds: [localPayload.asset.id] }
}));
return localPayload.asset;
}
var form = new FormData();
form.append('file', file);
form.append('workspaceId', plan.workspaceId);
form.append('documentId', plan.targetDocumentId);
if (plan.targetMindmapId) form.append('mindmapId', plan.targetMindmapId);
var response = await fetch('/api/media/upload', {
method: 'POST',
credentials: 'include',
body: form
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload || !payload.asset) {
throw new Error(payload && payload.error ? payload.error : '上传失败');
}
appendUploadedAssetRow(payload.asset, plan.targetDocumentId);
if (options && options.insertIntoEditor) {
await insertUploadedAssetIntoEditor(payload.asset);
}
window.dispatchEvent(new CustomEvent('wolai:assets-changed', {
detail: { docId: plan.targetDocumentId, asset: payload.asset, assetIds: [payload.asset.id] }
}));
return payload.asset;
}
window.addEventListener('wolai:assets-changed', function(event) {
applyAssetsChangedToFileTree(event.detail || {});
});
installMindmapAssetFetchObserver();
async function uploadFilesWithResolvedTarget(files, detail, options) {
var list = Array.from(files || []).filter(Boolean);
if (!list.length) return [];
var plan = await resolveFileTreeUploadTarget(detail || {});
var uploaded = [];
var errors = [];
for (var i = 0; i < list.length; i += 1) {
try {
uploaded.push(await uploadFileToMediaAsset(list[i], plan, options || {}));
} catch (error) {
errors.push(list[i].name + ': ' + (error && error.message ? error.message : '上传失败'));
}
}
if (errors.length) {
window.alert('部分文件上传失败:\n' + errors.slice(0, 6).join('\n') + (errors.length > 6 ? '\n...' : ''));
}
return uploaded;
}
function openEditorUploadFilePicker(detail) {
var input = document.createElement('input');
input.type = 'file';
input.multiple = detail && detail.multiple !== false;
if (detail && detail.accept) input.accept = String(detail.accept);
input.style.position = 'fixed';
input.style.left = '-9999px';
input.style.top = '-9999px';
document.body.appendChild(input);
input.addEventListener('change', function() {
var files = Array.from(input.files || []);
input.remove();
void uploadFilesWithResolvedTarget(files, {
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
targetRowId: null
}, {
insertIntoEditor: detail && detail.insertIntoEditor !== false
});
}, { once: true });
input.click();
}
window.addEventListener('mnote:editor-upload-request', function(event) {
openEditorUploadFilePicker(event.detail || {});
});
window.addEventListener('tree.filetree.external-drop', function(event) {
var detail = event.detail || {};
void uploadFilesWithResolvedTarget(detail.files || [], detail, {
insertIntoEditor: String(detail.documentId || '') === currentDocumentId()
});
});
document.addEventListener('dragover', function(event) {
var editorTarget = closestAction(event.target, '[data-testid="mnote-leptos-tiptap-editor-stage"], .editor-surface .ProseMirror');
var hasFiles = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], 'Files') >= 0;
if (!editorTarget || !hasFiles) return;
event.preventDefault();
event.stopPropagation();
if (event.dataTransfer) event.dataTransfer.dropEffect = 'copy';
}, true);
document.addEventListener('drop', function(event) {
var editorTarget = closestAction(event.target, '[data-testid="mnote-leptos-tiptap-editor-stage"], .editor-surface .ProseMirror');
var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
if (!editorTarget || !files.length) return;
event.preventDefault();
event.stopPropagation();
void uploadFilesWithResolvedTarget(files, {
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
targetRowId: null
}, {
insertIntoEditor: true
});
}, true);
function rowTitle(row) {
var title = row ? row.querySelector(':scope > .tree-link > .tree-link-title') : null;
return title && title.textContent ? title.textContent.trim() : '无标题';
}
function rowCenter(row) {
var rect = row.getBoundingClientRect();
return { x: rect.left + Math.min(rect.width - 12, 180), y: rect.top + Math.min(rect.height, 22) };
}
async function renameFileTreeAsset(assetId, title) {
var response = await fetch('/api/media/batch', {
method: 'POST',
credentials: 'include',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ action: 'rename', assetIds: [assetId], newName: title })
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error((payload && payload.error) || '重命名附件失败');
return payload;
}
function beginFileTreeInlineRename(row) {
if (!(row instanceof HTMLElement)) return false;
if (row.querySelector('.tree-rename-input')) return true;
var rowKind = row.getAttribute('data-row-kind') || '';
var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
var assetId = row.getAttribute('data-asset-id') || '';
if (!documentId && !assetId) return false;
if (rowKind && ['document', 'doc', 'index', 'markdown', 'asset'].indexOf(rowKind) < 0 && !assetId) return false;
var link = row.querySelector(':scope > .tree-link');
var title = rowTitle(row);
if (!(link instanceof HTMLElement)) return false;
link.hidden = true;
var input = document.createElement('input');
input.type = 'text';
input.className = 'tree-rename-input';
input.setAttribute('data-rename-id', row.getAttribute('data-row-id') || '');
input.value = title;
input.style.minWidth = '0';
input.style.flex = '1 1 auto';
input.style.height = '22px';
input.style.border = '1px solid #93c5fd';
input.style.borderRadius = '3px';
input.style.padding = '0 4px';
input.style.font = 'inherit';
input.style.background = '#fff';
input.style.color = '#1f2937';
var validation = document.createElement('div');
validation.setAttribute('data-testid', 'tree-rename-validation');
validation.setAttribute('data-mnote-rename-validation', 'true');
validation.style.fontSize = '12px';
validation.style.color = '#dc2626';
validation.style.padding = '2px 4px';
validation.hidden = true;
var setValidation = function(message) {
var text = String(message || '').trim();
validation.textContent = text;
validation.hidden = !text;
input.setAttribute('aria-invalid', text ? 'true' : 'false');
};
var closed = false;
var committing = false;
var close = function() {
if (closed) return;
closed = true;
if (input.parentElement) input.parentElement.removeChild(input);
if (validation.parentElement) validation.parentElement.removeChild(validation);
link.hidden = false;
};
var commit = function() {
if (closed || committing) return;
var nextTitle = input.value.trim();
if (!nextTitle || nextTitle === title) {
close();
return;
}
var validationMessage = validateFileTreeRename(row, nextTitle);
if (validationMessage) {
setValidation(validationMessage);
return;
}
setValidation('');
var commandTitle = isFileTreePageRow(row) ? normalizeFileTreePageRenameTitle(nextTitle) : nextTitle;
committing = true;
input.disabled = true;
var work = assetId
? Promise.resolve().then(function(){
document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-asset-id="' + cssEscape(assetId) + '"] .tree-link-title').forEach(function(titleNode) {
titleNode.textContent = commandTitle;
});
close();
return renameFileTreeAsset(assetId, commandTitle);
}).then(function(){ return null; })
: dispatchTreeCommand(row, {
action: 'rename',
workspaceId: resolveWorkspaceId(row),
documentId: documentId,
title: commandTitle
}).then(function(){ updateTitleEverywhere(documentId, commandTitle); });
void work.then(close).catch(function(error) {
committing = false;
input.disabled = false;
window.alert(error && error.message ? error.message : '重命名失败');
});
};
input.addEventListener('click', function(event) { event.stopPropagation(); });
input.addEventListener('dblclick', function(event) { event.stopPropagation(); });
input.addEventListener('keydown', function(event) {
event.stopPropagation();
if (event.key === 'Enter') {
event.preventDefault();
commit();
} else if (event.key === 'Escape') {
event.preventDefault();
close();
}
});
input.addEventListener('blur', commit);
link.parentElement.insertBefore(input, link.nextSibling);
link.parentElement.insertBefore(validation, input.nextSibling);
window.requestAnimationFrame(function() {
input.focus();
input.select();
});
return true;
}
function closeTreeContextMenu() {
if (activeTreeContextMenu && activeTreeContextMenu.parentElement) {
activeTreeContextMenu.parentElement.removeChild(activeTreeContextMenu);
}
activeTreeContextMenu = null;
}
function copyTreeContextValue(value, actionName) {
var text = String(value || '');
var done = function() {
document.documentElement.setAttribute('data-mnote-tree-context-last-copy', actionName || 'copy');
};
if (navigator.clipboard && typeof navigator.clipboard.writeText === 'function') {
return navigator.clipboard.writeText(text).then(done).catch(function(){});
}
var textarea = document.createElement('textarea');
textarea.value = text;
textarea.setAttribute('readonly', 'readonly');
textarea.style.position = 'fixed';
textarea.style.left = '-9999px';
document.body.appendChild(textarea);
textarea.select();
try { document.execCommand('copy'); } catch (_) {}
document.body.removeChild(textarea);
done();
return Promise.resolve();
}
function documentHref(documentId, workspaceId) {
var url = new URL('/documents/' + encodeURIComponent(documentId), window.location.origin);
if (workspaceId) url.searchParams.set('workspaceId', workspaceId);
copyWorkspaceSourceParams(url);
return url.toString();
}
function convertToPreviousSiblingChild(trigger, detail) {
var documentId = detail.documentId || '';
var row = document.querySelector('#sidebar-tree-root .tree-row[data-node-id="' + cssEscape(documentId) + '"]');
if (!(row instanceof HTMLElement)) return;
var parentId = row.getAttribute('data-parent-id') || '';
var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(candidate) {
return (candidate.getAttribute('data-parent-id') || '') === parentId;
});
var index = siblings.indexOf(row);
if (index <= 0) {
window.alert('当前页面前面没有同级页面。');
return;
}
var previous = siblings[index - 1];
var previousId = previous.getAttribute('data-node-id') || '';
var children = previous.parentElement ? previous.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : [];
void dispatchTreeCommand(trigger || row, {
action: 'move',
workspaceId: detail.workspaceId || resolveWorkspaceId(row),
documentId: documentId,
parentId: previousId,
sortOrder: children.length
});
}
function handleTreeContextMenuAction(action, detail, trigger) {
closeTreeContextMenu();
detail = detail || {};
if (detail.contextKind === 'attachment') {
if (action === 'copy-link') {
void copyTreeContextValue(detail.href || '', 'attachment-copy-link');
return;
}
if (action === 'download') {
openEditorAttachmentDownload(detail);
return;
}
if (action === 'popup-preview') {
openEditorAttachmentDetail(detail);
return;
}
if (action === 'right-preview') {
dispatchSidebarEvent('tree.attachment.open-right', detail);
return;
}
if (action === 'copy-id') {
void copyTreeContextValue(detail.assetId || '', 'attachment-copy-id');
return;
}
dispatchSidebarEvent('tree.attachment.action', { action: action, attachment: detail });
return;
}
var documentId = detail.documentId || '';
var workspaceId = detail.workspaceId || resolveWorkspaceId(trigger || document.body);
var title = detail.title || '无标题';
if (action === 'open-right') {
dispatchSidebarEvent('tree.page.open-right', detail);
return;
}
if (action === 'share') {
dispatchSidebarEvent('tree.page.share', detail);
return;
}
if (action === 'copy-link') {
void copyTreeContextValue(documentHref(documentId, workspaceId), 'copy-link');
return;
}
if (action === 'copy-link-title') {
void copyTreeContextValue(title + ' ' + documentHref(documentId, workspaceId), 'copy-link-title');
return;
}
if (action === 'copy-reference-inline') {
void copyTreeContextValue('((' + title + ' ' + documentId + '))', 'copy-reference-inline');
return;
}
if (action === 'copy-reference-embed') {
void copyTreeContextValue('{{' + title + ' ' + documentId + '}}', 'copy-reference-embed');
return;
}
if (action === 'copy-id') {
void copyTreeContextValue(documentId || detail.assetId || detail.rowId || '', 'copy-id');
return;
}
if (action === 'new-file') {
void createPage(trigger || document.body, documentId || null);
return;
}
if (action === 'copy-path') {
void copyTreeContextValue(title, 'copy-path');
return;
}
if (action === 'refresh') {
document.documentElement.setAttribute('data-mnote-filetree-refresh-requested', 'true');
dispatchSidebarEvent('tree.filetree.refresh', detail);
return;
}
if (action === 'collapse-all') {
document.querySelectorAll('#sidebar-file-tree-root .tree-row[aria-expanded="true"]').forEach(function(row) {
if (!(row instanceof HTMLElement)) return;
row.setAttribute('aria-expanded', 'false');
var node = row.closest('.tree-node');
var children = node ? node.querySelector(':scope > .tree-children') : null;
if (children) children.classList.add('tree-children--collapsed');
});
return;
}
if (action === 'reveal') {
if (trigger && typeof trigger.scrollIntoView === 'function') {
trigger.scrollIntoView({ block: 'nearest' });
trigger.focus && trigger.focus();
}
return;
}
if (action === 'duplicate') {
dispatchSidebarEvent('tree.page.duplicate', detail);
return;
}
if (action === 'rename') {
if (detail.contextKind === 'filetree' && trigger) {
var renameRow = trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger;
if (beginFileTreeInlineRename(renameRow)) return;
}
var nextTitle = window.prompt('重命名页面', title);
if (nextTitle && nextTitle.trim() && documentId) {
void dispatchTreeCommand(trigger || document.body, {
action: 'rename',
workspaceId: workspaceId,
documentId: documentId,
title: nextTitle.trim()
}).then(function(){ updateTitleEverywhere(documentId, nextTitle.trim()); });
}
return;
}
if (action === 'create-child') {
void createPage(trigger || document.body, documentId);
return;
}
if (action === 'convert-child') {
convertToPreviousSiblingChild(trigger, detail);
return;
}
if (action === 'delete-trash' && documentId) {
if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return;
void dispatchTreeCommand(trigger || document.body, {
action: 'archive',
workspaceId: workspaceId,
documentId: documentId
});
}
}
function appendTreeContextMenuButton(menu, item, detail, trigger) {
if (item.separator) {
var sep = document.createElement('div');
sep.className = 'mnote-tree-context-menu__separator';
sep.setAttribute('role', 'separator');
menu.appendChild(sep);
return;
}
var button = document.createElement('button');
button.type = 'button';
button.className = item.danger ? 'mnote-tree-context-menu__item mnote-tree-context-menu__item--danger' : 'mnote-tree-context-menu__item';
button.setAttribute('role', 'menuitem');
button.setAttribute('data-action', item.action);
button.disabled = item.disabled === true;
if (item.title) button.title = item.title;
var icon = document.createElement('span');
icon.className = 'material-symbols-outlined mnote-tree-context-menu__icon';
icon.setAttribute('aria-hidden', 'true');
icon.setAttribute('data-icon', item.icon || 'radio_button_unchecked');
var label = document.createElement('span');
label.className = 'mnote-tree-context-menu__label';
label.textContent = item.label;
button.appendChild(icon);
button.appendChild(label);
if (item.shortcut) {
var shortcut = document.createElement('span');
shortcut.className = 'mnote-tree-context-menu__shortcut';
shortcut.textContent = item.shortcut;
button.appendChild(shortcut);
}
button.addEventListener('click', function(event) {
event.preventDefault();
event.stopPropagation();
handleTreeContextMenuAction(item.action, detail, trigger);
});
menu.appendChild(button);
}
function openTreeContextMenu(kind, detail, x, y, trigger) {
closeTreeContextMenu();
detail = Object.assign({}, detail || {}, { contextKind: kind });
var menu = document.createElement('div');
menu.className = 'mnote-tree-context-menu';
menu.setAttribute('role', 'menu');
menu.setAttribute('data-testid', 'mnote-tree-context-menu');
menu.setAttribute('data-kind', kind);
var isAttachment = kind === 'attachment';
var isAsset = kind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index';
var items = isAttachment ? [
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本', shortcut: 'Ctrl + D' },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '复制链接' },
{ action: 'move-embed', icon: 'subdirectory_arrow_right', label: '移动/嵌入到...', shortcut: 'Alt+Shift+M/G' },
{ action: 'history', icon: 'history', label: '块历史...' },
{ separator: true },
{ action: 'popup-preview', icon: 'preview', label: '弹窗预览' },
{ action: 'right-preview', icon: 'right_panel_open', label: '右侧预览' },
{ action: 'download', icon: 'download', label: '下载' },
{ action: 'replace-file', icon: 'sync', label: '更换文件' },
{ action: 'rename-attachment', icon: 'drive_file_rename_outline', label: '重命名' },
{ action: 'comment', icon: 'mode_comment', label: '评论', shortcut: 'Ctrl+Alt+M' },
{ action: 'caption', icon: 'notes', label: '添加说明文字' },
{ separator: true },
{ action: 'color', icon: 'format_paint', label: '颜色' }
] : isAsset ? [
{ action: 'open-right', icon: 'open_in_new', label: '在右侧边栏打开', shortcut: 'Alt + O' },
{ action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2' },
{ action: 'copy-id', icon: 'tag', label: '复制资源 ID' },
{ separator: true },
{ action: 'copy-path', icon: 'content_copy', label: 'Copy Path' },
{ action: 'refresh', icon: 'refresh', label: 'Refresh' },
{ action: 'reveal', icon: 'my_location', label: 'Reveal' }
] : kind === 'filetree' ? [
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '复制访问链接' },
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
{ action: 'copy-id', icon: 'tag', label: '复制页面ID' },
{ action: 'copy-path', icon: 'content_copy', label: 'Copy Path' },
{ separator: true },
{ action: 'new-file', icon: 'note_add', label: 'New File' },
{ action: 'new-folder', icon: 'create_new_folder', label: 'New Folder', disabled: true, title: 'Convex 文件夹对象尚未进入正式 tree command' },
{ action: 'paste-into', icon: 'content_paste', label: 'Paste Into', disabled: true, title: '请使用 Ctrl/Cmd+V 粘贴;右键 Paste Into 待接 selection target' },
{ action: 'refresh', icon: 'refresh', label: 'Refresh' },
{ action: 'collapse-all', icon: 'unfold_less', label: 'Collapse All' },
{ action: 'reveal', icon: 'my_location', label: 'Reveal' },
{ separator: true },
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本' },
{ separator: true },
{ action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2' },
{ separator: true },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true }
] : [
{ action: 'open-right', icon: 'right_panel_open', label: '在右侧边栏打开', shortcut: 'Alt+' },
{ separator: true },
{ action: 'copy-link', icon: 'link', label: '复制访问链接' },
{ action: 'copy-reference-inline', icon: 'content_copy', label: '复制页面引用链接' },
{ action: 'copy-id', icon: 'tag', label: '复制页面ID' },
{ separator: true },
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本' },
{ separator: true },
{ action: 'rename', icon: 'edit', label: '重命名' },
{ separator: true },
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true }
];
items.forEach(function(item) { appendTreeContextMenuButton(menu, item, detail, trigger); });
document.body.appendChild(menu);
var rect = menu.getBoundingClientRect();
var left = Math.min(Math.max(8, x || 8), Math.max(8, window.innerWidth - rect.width - 8));
var top = Math.min(Math.max(8, y || 8), Math.max(8, window.innerHeight - rect.height - 8));
menu.style.left = left + 'px';
menu.style.top = top + 'px';
activeTreeContextMenu = menu;
}
function openPageTreeContextMenu(row, x, y, trigger) {
if (!(row instanceof HTMLElement)) return;
var documentId = row.getAttribute('data-node-id') || '';
openTreeContextMenu('page', {
documentId: documentId,
rowId: documentId,
rowKind: 'document',
title: rowTitle(row),
workspaceId: resolveWorkspaceId(row)
}, x, y, trigger || row);
}
function openFileTreeContextMenu(row, x, y, trigger) {
if (!(row instanceof HTMLElement)) return;
var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '';
openTreeContextMenu('filetree', {
documentId: documentId,
rowId: row.getAttribute('data-row-id') || '',
rowKind: row.getAttribute('data-row-kind') || '',
assetId: row.getAttribute('data-asset-id') || '',
title: rowTitle(row),
workspaceId: resolveWorkspaceId(row)
}, x, y, trigger || row);
}
function visibleFileTreeRows() {
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
.filter(function(row) { return row instanceof HTMLElement && row.offsetParent !== null; });
}
function syncSidebarFileTreeSelection() {
var rows = visibleFileTreeRows();
rows.forEach(function(row) {
var rowId = row.getAttribute('data-row-id') || '';
row.setAttribute('data-selected', String(Boolean(rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId))));
row.setAttribute('data-focused', String(rowId === sidebarFileTreeSelection.focusedRowId));
});
window.dispatchEvent(new CustomEvent('tree.filetree.selection.changed', {
detail: {
selectedRowIds: Array.from(sidebarFileTreeSelection.selectedRowIds),
anchorRowId: sidebarFileTreeSelection.anchorRowId,
focusedRowId: sidebarFileTreeSelection.focusedRowId
}
}));
}
function selectSidebarFileTreeRow(row, modifiers) {
if (!(row instanceof HTMLElement)) return [];
var rowId = row.getAttribute('data-row-id') || '';
if (!rowId) return [];
var rows = visibleFileTreeRows();
var visibleRowIds = rows.map(function(item) { return item.getAttribute('data-row-id') || ''; }).filter(Boolean);
var selected = new Set(sidebarFileTreeSelection.selectedRowIds);
var shiftKey = Boolean(modifiers && modifiers.shiftKey);
var ctrlKey = Boolean(modifiers && (modifiers.ctrlKey || modifiers.metaKey));
if (shiftKey && sidebarFileTreeSelection.anchorRowId) {
var anchorIndex = visibleRowIds.indexOf(sidebarFileTreeSelection.anchorRowId);
var targetIndex = visibleRowIds.indexOf(rowId);
if (anchorIndex >= 0 && targetIndex >= 0) {
selected = new Set(visibleRowIds.slice(Math.min(anchorIndex, targetIndex), Math.max(anchorIndex, targetIndex) + 1));
} else {
selected = new Set([rowId]);
sidebarFileTreeSelection.anchorRowId = rowId;
}
} else if (ctrlKey) {
if (selected.has(rowId) && selected.size > 1) selected.delete(rowId);
else selected.add(rowId);
sidebarFileTreeSelection.anchorRowId = rowId;
} else {
selected = new Set([rowId]);
sidebarFileTreeSelection.anchorRowId = rowId;
}
sidebarFileTreeSelection.selectedRowIds = selected;
sidebarFileTreeSelection.focusedRowId = rowId;
syncSidebarFileTreeSelection();
return Array.from(selected);
}
function selectedSidebarFileTreeRowIdsForDrag(row) {
var rowId = row instanceof HTMLElement ? row.getAttribute('data-row-id') || '' : '';
if (rowId && sidebarFileTreeSelection.selectedRowIds.has(rowId)) {
return Array.from(sidebarFileTreeSelection.selectedRowIds);
}
return rowId ? [rowId] : [];
}
function selectedSidebarFileTreeRows() {
var selectedIds = sidebarFileTreeSelection.selectedRowIds;
var rows = visibleFileTreeRows().filter(function(row) {
var rowId = row.getAttribute('data-row-id') || '';
return rowId && selectedIds.has(rowId);
});
if (rows.length > 0) return rows;
if (sidebarFileTreeSelection.focusedRowId) {
var focused = document.querySelector(
'#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(sidebarFileTreeSelection.focusedRowId) + '"]'
);
if (focused instanceof HTMLElement) return [focused];
}
return [];
}
function fileTreeRowDocumentId(row) {
if (!(row instanceof HTMLElement)) return '';
return String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim();
}
function fileTreeRowAssetId(row) {
if (!(row instanceof HTMLElement)) return '';
var assetId = String(row.getAttribute('data-asset-id') || '').trim();
if (assetId) return assetId;
if (currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset') {
return String(row.getAttribute('data-row-id') || '').trim();
}
return '';
}
function fileTreeRowKind(row) {
if (!(row instanceof HTMLElement)) return '';
return String(row.getAttribute('data-row-kind') || '').trim();
}
function hasSelectedDocumentAncestor(row, selectedDocRowIds) {
var node = row instanceof HTMLElement ? row.closest('.tree-node') : null;
while (node && node.parentElement) {
var parentChildren = node.parentElement.closest('.tree-children');
var parentNode = parentChildren ? parentChildren.closest('.tree-node') : null;
var parentRow = parentNode ? parentNode.querySelector(':scope > .tree-row[data-shell-mode="filetree"]') : null;
if (parentRow instanceof HTMLElement) {
var parentRowId = parentRow.getAttribute('data-row-id') || '';
if (selectedDocRowIds.has(parentRowId)) return true;
}
node = parentNode;
}
return false;
}
function classifySidebarFileTreeAsset(row) {
var badge = row instanceof HTMLElement ? row.querySelector('.tree-kind-badge') : null;
var iconKind = badge instanceof HTMLElement ? String(badge.getAttribute('data-kind') || '').trim() : '';
var objectKind = row instanceof HTMLElement ? String(row.getAttribute('data-object-kind') || '').trim() : '';
var title = rowTitle(row).toLowerCase();
if (objectKind === 'mindmap' || iconKind === 'mindmap') return 'mindmap';
if (objectKind === 'table' || iconKind === 'table' || iconKind === 'luckysheet' || title.indexOf('.luckysheet') >= 0) return 'table';
return 'file';
}
function buildSidebarFileTreeDeletePlan(rows) {
var docRows = [];
var assetRows = [];
rows.forEach(function(row) {
var kind = fileTreeRowKind(row);
if ((kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown') && fileTreeRowDocumentId(row)) {
docRows.push(row);
return;
}
if (fileTreeRowAssetId(row)) assetRows.push(row);
});
var selectedDocRowIds = new Set(docRows.map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean));
var topDocRows = [];
var seenDocs = new Set();
docRows.forEach(function(row) {
var documentId = fileTreeRowDocumentId(row);
if (!documentId || seenDocs.has(documentId)) return;
if (hasSelectedDocumentAncestor(row, selectedDocRowIds)) return;
seenDocs.add(documentId);
topDocRows.push(row);
});
var selectedTopDocRows = new Set(topDocRows.map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean));
var fileAssetRows = [];
var mindmapRows = [];
var tableRows = [];
var seenAssets = new Set();
assetRows.forEach(function(row) {
var assetId = fileTreeRowAssetId(row);
if (!assetId || seenAssets.has(assetId)) return;
if (hasSelectedDocumentAncestor(row, selectedTopDocRows)) return;
seenAssets.add(assetId);
var assetKind = classifySidebarFileTreeAsset(row);
if (assetKind === 'mindmap') mindmapRows.push(row);
else if (assetKind === 'table') tableRows.push(row);
else fileAssetRows.push(row);
});
return {
docRows: topDocRows,
fileAssetRows: fileAssetRows,
mindmapRows: mindmapRows,
tableRows: tableRows
};
}
function sidebarFileTreeDeleteConfirmText(plan) {
var docCount = plan.docRows.length;
var fileCount = plan.fileAssetRows.length;
var mindmapCount = plan.mindmapRows.length;
var tableCount = plan.tableRows.length;
var parts = [];
if (docCount > 0) parts.push(docCount + ' 个页面(删除到垃圾桶)');
if (fileCount > 0) parts.push(fileCount + ' 个附件(删除,10 分钟内可撤销)');
if (mindmapCount > 0) parts.push(mindmapCount + ' 个思维导图(移入垃圾桶,10 分钟内可恢复)');
if (tableCount > 0) parts.push(tableCount + ' 个在线表格(删除)');
return '确认删除选中的 ' + parts.join(' + ') + ' 吗?';
}
async function postSidebarFileTreeJson(url, body) {
var response = await fetch(url, {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify(body || {})
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) {
throw new Error((payload && (payload.error || payload.message)) || (url + ' failed: ' + response.status));
}
return payload;
}
function fileTreeRowsByRowIds(rowIds) {
return (rowIds || []).map(function(rowId) {
return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]');
}).filter(function(row) { return row instanceof HTMLElement; });
}
function fileTreeChildCount(documentId) {
if (!documentId) return 0;
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + documentId) + '"]');
var node = row ? row.closest('.tree-node') : null;
var children = node ? node.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="document"], :scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"][data-row-kind="doc"]') : [];
return children.length;
}
async function pasteSidebarFileTreeClipboard(trigger) {
if (!sidebarFileTreeClipboard || !Array.isArray(sidebarFileTreeClipboard.rowIds) || sidebarFileTreeClipboard.rowIds.length === 0) return false;
var targetRow = trigger instanceof HTMLElement ? trigger : null;
if (!targetRow && sidebarFileTreeSelection.focusedRowId) {
targetRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(sidebarFileTreeSelection.focusedRowId) + '"]');
}
var targetDocumentId = fileTreeRowDocumentId(targetRow) || currentDocumentId();
if (!targetDocumentId) return false;
var action = sidebarFileTreeClipboard.action === 'cut' ? 'move' : 'copy';
var rows = fileTreeRowsByRowIds(sidebarFileTreeClipboard.rowIds);
if (rows.length === 0) return false;
var plan = buildSidebarFileTreeDeletePlan(rows);
var workspaceId = resolveWorkspaceId(targetRow || document.body);
var failures = [];
if (action === 'copy') {
dispatchSidebarEvent('tree.filetree.copy-requested', { rowIds: sidebarFileTreeClipboard.rowIds, targetDocumentId: targetDocumentId });
return true;
}
for (var i = 0; i < plan.docRows.length; i += 1) {
var docRow = plan.docRows[i];
var documentId = fileTreeRowDocumentId(docRow);
if (!documentId || documentId === targetDocumentId) continue;
try {
await dispatchTreeCommand(targetRow || docRow, {
action: 'move',
workspaceId: workspaceId,
documentId: documentId,
parentId: targetDocumentId,
sortOrder: fileTreeChildCount(targetDocumentId) + i
});
} catch (error) {
failures.push(documentId + ': ' + (error && error.message ? error.message : '移动页面失败'));
}
}
var assetIds = plan.fileAssetRows.map(fileTreeRowAssetId).filter(Boolean);
if (assetIds.length > 0) {
try {
await postSidebarFileTreeJson('/api/media/batch', {
action: 'move',
assetIds: assetIds,
targetDocumentId: targetDocumentId
});
} catch (error) {
failures.push(assetIds.join(',') + ': ' + (error && error.message ? error.message : '移动附件失败'));
}
}
if (failures.length > 0) {
window.alert('部分对象移动失败:' + failures.join(''));
return false;
}
sidebarFileTreeClipboard = null;
return true;
}
async function deleteSelectedSidebarFileTreeRows(trigger) {
var rows = selectedSidebarFileTreeRows();
var plan = buildSidebarFileTreeDeletePlan(rows);
var total = plan.docRows.length + plan.fileAssetRows.length + plan.mindmapRows.length + plan.tableRows.length;
if (total === 0) return false;
if (!window.confirm(sidebarFileTreeDeleteConfirmText(plan))) return false;
var failures = [];
for (var i = 0; i < plan.docRows.length; i += 1) {
var docRow = plan.docRows[i];
var documentId = fileTreeRowDocumentId(docRow);
try {
await dispatchTreeCommand(trigger || docRow, {
action: 'archive',
workspaceId: resolveWorkspaceId(docRow),
documentId: documentId
});
applyRemoveDocumentDelta({ documentId: documentId });
} catch (error) {
failures.push(documentId);
}
}
if (plan.fileAssetRows.length > 0) {
var fileAssetIds = plan.fileAssetRows.map(fileTreeRowAssetId).filter(Boolean);
try {
if (currentSourceKind() === 'local_folder') {
for (var lf = 0; lf < fileAssetIds.length; lf += 1) {
await dispatchTreeCommand(trigger || plan.fileAssetRows[lf], {
action: 'archive',
workspaceId: resolveWorkspaceId(plan.fileAssetRows[lf]),
documentId: fileAssetIds[lf]
});
}
} else {
await postSidebarFileTreeJson('/api/media/batch', { action: 'delete', assetIds: fileAssetIds });
}
fileAssetIds.forEach(removeFileTreeAssetRow);
} catch (error) {
failures = failures.concat(fileAssetIds);
}
}
for (var m = 0; m < plan.mindmapRows.length; m += 1) {
var mindmapRow = plan.mindmapRows[m];
var mindmapId = fileTreeRowAssetId(mindmapRow);
try {
var mindmapDocId = fileTreeRowDocumentId(mindmapRow);
var mindmapResponse = await fetch('/api/mindmap/' + encodeURIComponent(mindmapDocId) + '/' + encodeURIComponent(mindmapId), { method: 'DELETE' });
if (!mindmapResponse.ok) throw new Error('mindmap_delete_failed_' + mindmapResponse.status);
removeFileTreeAssetRow(mindmapId);
} catch (error) {
failures.push(mindmapId);
}
}
for (var t = 0; t < plan.tableRows.length; t += 1) {
var tableRow = plan.tableRows[t];
var tableId = fileTreeRowAssetId(tableRow);
try {
var tableResponse = await fetch('/api/tables/' + encodeURIComponent(tableId), { method: 'DELETE' });
if (!tableResponse.ok) throw new Error('table_delete_failed_' + tableResponse.status);
removeFileTreeAssetRow(tableId);
window.dispatchEvent(new CustomEvent('online-table-deleted', { detail: { tableId: tableId } }));
} catch (error) {
failures.push(tableId);
}
}
sidebarFileTreeSelection.selectedRowIds = new Set();
sidebarFileTreeSelection.focusedRowId = null;
syncSidebarFileTreeSelection();
if (failures.length > 0) {
window.alert('部分对象删除失败:' + failures.slice(0, 5).join(', ') + (failures.length > 5 ? '…' : ''));
return false;
}
document.documentElement.setAttribute('data-mnote-filetree-bulk-delete-applied', 'true');
return true;
}
function ensureSearchModal() {
var existing = document.querySelector('[data-testid="wolai-search-modal"]');
if (existing instanceof HTMLElement) return existing;
var overlay = document.createElement('div');
overlay.className = 'wolai-search-overlay';
overlay.setAttribute('data-testid', 'wolai-search-modal');
overlay.setAttribute('role', 'dialog');
overlay.setAttribute('aria-modal', 'true');
overlay.hidden = true;
overlay.innerHTML = '' +
'<div class="wolai-search-dialog">' +
'<div class="wolai-search-input-row">' +
'<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' +
'<input data-testid="wolai-search-input" class="wolai-search-input" type="search" autocomplete="off" placeholder="在当前工作区中搜索" />' +
'<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
'</div>' +
'<div class="wolai-search-options" data-testid="wolai-search-options" aria-label="搜索选项" hidden>' +
'<div class="wolai-search-options-left">' +
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch" data-search-switch="title" role="switch" aria-checked="false" aria-label="仅匹配标题"></button></span>' +
'<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
'<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
'</div>' +
'<div class="wolai-search-options-right">' +
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
'</div>' +
'</div>' +
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
'<div class="wolai-search-results" data-testid="wolai-search-results" data-search-results-owner="rust-kernel"></div>' +
'</div>';
document.body.appendChild(overlay);
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
if (input) input.addEventListener('input', scheduleSearchResultsRender);
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
button.addEventListener('click', function() {
var isOn = button.getAttribute('aria-checked') !== 'true';
button.setAttribute('aria-checked', isOn ? 'true' : 'false');
button.classList.toggle('is-on', isOn);
scheduleSearchResultsRender();
});
});
if (closeButton) closeButton.addEventListener('click', closeSearchModal);
overlay.addEventListener('click', function(event) {
if (event.target === overlay) closeSearchModal();
});
return overlay;
}
var activeSearchRequestId = 0;
var searchRenderTimer = 0;
function searchText(value) {
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
}
function currentWorkspaceName() {
var name = document.querySelector('.sidebar-workspace-name');
return searchText(name && name.textContent) || '当前工作区';
}
function currentDocumentId() {
var shell = document.querySelector('.document-shell[data-document-id]');
var bodyId = document.body && document.body.getAttribute('data-document-id');
return searchText(shell && shell.getAttribute('data-document-id')) || searchText(bodyId);
}
function searchSwitchValue(overlay, name) {
var button = overlay.querySelector('[data-search-switch="' + name + '"]');
return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true';
}
function highlightedHtml(value) {
return escapeHtml(value)
.replace(/&lt;mark&gt;/g, '<mark>')
.replace(/&lt;\/mark&gt;/g, '</mark>');
}
function highlightSearchTitle(title, query) {
var cleanTitle = searchText(title);
var cleanQuery = searchText(query);
if (!cleanQuery) return escapeHtml(cleanTitle);
var index = cleanTitle.toLowerCase().indexOf(cleanQuery.toLowerCase());
if (index < 0) return escapeHtml(cleanTitle);
return escapeHtml(cleanTitle.slice(0, index)) +
'<mark>' + escapeHtml(cleanTitle.slice(index, index + cleanQuery.length)) + '</mark>' +
escapeHtml(cleanTitle.slice(index + cleanQuery.length));
}
function renderSearchRecentState(overlay) {
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
if (options instanceof HTMLElement) options.hidden = true;
if (meta) meta.innerHTML = '<span>最近浏览</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
if (results) {
results.innerHTML = '<div class="wolai-search-recent" data-testid="wolai-search-recent">暂无最近浏览</div>';
}
}
function scheduleSearchResultsRender() {
window.clearTimeout(searchRenderTimer);
searchRenderTimer = window.setTimeout(function() { void renderSearchResults(); }, 120);
}
async function renderSearchResults() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
if (!(overlay instanceof HTMLElement)) return;
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
if (!input || !meta || !results) return;
var query = searchText(input.value);
if (!query) {
activeSearchRequestId += 1;
renderSearchRecentState(overlay);
return;
}
if (options instanceof HTMLElement) options.hidden = false;
var requestId = ++activeSearchRequestId;
meta.innerHTML = '<span>搜索中…</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
results.innerHTML = '<div class="wolai-search-empty" data-search-loading="true">搜索中…</div>';
try {
var response = await fetch('/api/search/documents', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
sourceKind: currentSourceKind() || null,
rootUri: currentRootUri() || null,
documentId: currentDocumentId() || null,
query: query,
limit: 30,
filters: {
titleOnly: searchSwitchValue(overlay, 'title'),
exact: searchSwitchValue(overlay, 'exact'),
includeOcr: false,
onlyCurrentPage: searchSwitchValue(overlay, 'page'),
timeRange: 'any',
timeField: 'updated'
}
})
});
var payload = await response.json();
if (requestId !== activeSearchRequestId) return;
var items = Array.isArray(payload.results) ? payload.results : [];
meta.innerHTML = '<span>共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
if (!items.length) {
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
return;
}
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 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) + '">' +
'<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('');
} catch (error) {
if (requestId !== activeSearchRequestId) return;
meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
}
}
function isSearchModalOpen() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
return overlay instanceof HTMLElement && !overlay.hidden;
}
function openSearchModal() {
var overlay = ensureSearchModal();
overlay.hidden = false;
document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
if (input) input.setAttribute('placeholder', '在 ' + currentWorkspaceName() + ' 中搜索');
void renderSearchResults();
if (input) {
setTimeout(function() { input.focus(); input.select(); }, 0);
}
}
function closeSearchModal() {
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
if (overlay instanceof HTMLElement) overlay.hidden = true;
document.documentElement.removeAttribute('data-mnote-search-modal-open');
}
function toggleSearchModal() {
if (isSearchModalOpen()) closeSearchModal();
else openSearchModal();
}
function updatePageSettingsTriggerState() {
var trigger = document.querySelector('[data-testid="wolai-page-settings-trigger"]');
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-state', pageUiState.pageSettingsOpen ? 'open' : 'closed');
trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
}
function updatePageAiTriggerState() {
var trigger = document.querySelector('[data-testid="wolai-floating-ai"]');
if (!(trigger instanceof HTMLElement)) return;
trigger.setAttribute('data-state', pageUiState.pageAiOpen ? 'open' : 'closed');
trigger.setAttribute('aria-expanded', pageUiState.pageAiOpen ? 'true' : 'false');
}
function createPageOptionRow(key, type) {
var inputType = type || 'checkbox';
var supported = pageOptionIsSupported(key);
if (inputType === 'checkbox') {
return '' +
'<label class="wolai-page-setting-row' + (supported ? '' : ' is-pending') + '" data-page-setting-row="' + key + '">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
'</span>' +
'<input type="checkbox" class="wolai-page-setting-checkbox" data-page-option-checkbox="' + key + '"' + (supported ? '' : ' data-setting-pending="true"') + ' />' +
'</label>';
}
return '' +
'<label class="wolai-page-setting-row" data-page-setting-row="' + key + '">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
'</span>' +
'<select class="wolai-page-setting-select" data-page-option-select="' + key + '">' +
'<option value="compact">紧凑</option>' +
'<option value="normal">默认</option>' +
'<option value="spacious">宽松</option>' +
'</select>' +
'</label>';
}
function createGlobalHeadingNumbersRow() {
return '' +
'<label class="wolai-page-setting-row" data-page-setting-row="globalShowHeadingNumbers">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">标题自动编号</span>' +
'</span>' +
'<input type="checkbox" class="wolai-page-setting-checkbox" data-global-option-checkbox="showHeadingNumbers" />' +
'</label>';
}
function renderGlobalOptions(popover) {
var globalHeadingNumbers = readGlobalShowHeadingNumbers();
popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
input.checked = globalHeadingNumbers;
});
}
function createPageFontRow() {
return '' +
'<label class="wolai-page-setting-row" data-page-setting-row="pageFont">' +
'<span class="wolai-page-setting-copy">' +
'<span class="wolai-page-setting-label">页面字体</span>' +
'<span class="wolai-page-setting-hint">已接通:仅对当前页面生效</span>' +
'</span>' +
'<select class="wolai-page-setting-select" data-page-option-select="pageFont">' +
'<option value="default">默认</option>' +
'<option value="song">宋体</option>' +
'<option value="kai">楷体</option>' +
'</select>' +
'</label>';
}
function ensurePageHistoryDrawer() {
var existing = document.querySelector('[data-testid="wolai-page-history-drawer"]');
if (existing instanceof HTMLElement) return existing;
var drawer = document.createElement('aside');
drawer.className = 'wolai-page-history-drawer';
drawer.setAttribute('data-testid', 'wolai-page-history-drawer');
drawer.setAttribute('data-mnote-surface', 'page-history');
drawer.hidden = true;
drawer.innerHTML = '' +
'<div class="wolai-page-history-panel">' +
'<div class="wolai-page-history-header">' +
'<div><div class="wolai-page-history-title">页面历史</div><div class="wolai-page-history-subtitle">当前会话内最近保存的 15 个快照。</div></div>' +
'<button type="button" class="wolai-surface-close" data-page-history-action="close" aria-label="关闭页面历史">×</button>' +
'</div>' +
'<div class="wolai-page-history-list" data-page-history-list></div>' +
'</div>';
drawer.addEventListener('click', function(event) {
if (event.target === drawer) closePageHistoryDrawer();
});
document.body.appendChild(drawer);
return drawer;
}
function renderPageHistoryDrawer() {
var drawer = ensurePageHistoryDrawer();
var list = drawer.querySelector('[data-page-history-list]');
if (!(list instanceof HTMLElement)) return;
ensureHistorySnapshotsSeeded();
list.innerHTML = pageUiState.historySnapshots.length
? pageUiState.historySnapshots.map(function(snapshot) {
var stats = snapshot.stats || {};
var label = new Date(snapshot.timestamp).toLocaleString('zh-CN', {
year: 'numeric',
month: '2-digit',
day: '2-digit',
hour: '2-digit',
minute: '2-digit'
});
return '' +
'<div class="wolai-page-history-item">' +
'<div class="wolai-page-history-item-copy">' +
'<div class="wolai-page-history-item-title">' + escapeHtml(label) + '</div>' +
'<div class="wolai-page-history-item-meta">字数 ' + Number(stats.wordCount || 0) + ' · 字符 ' + Number(stats.characterCount || 0) + ' · 块数 ' + Number(stats.blockCount || 0) + '</div>' +
'</div>' +
'<button type="button" class="wolai-page-history-item-ghost" data-page-history-action="noop">仅查看</button>' +
'</div>';
}).join('')
: '<div class="wolai-page-history-empty">尚未产生历史快照,编辑后会自动生成。</div>';
}
function openPageHistoryDrawer() {
renderPageHistoryDrawer();
var drawer = ensurePageHistoryDrawer();
drawer.hidden = false;
document.documentElement.setAttribute('data-mnote-page-history-open', 'true');
}
function closePageHistoryDrawer() {
var drawer = document.querySelector('[data-testid="wolai-page-history-drawer"]');
if (drawer instanceof HTMLElement) drawer.hidden = true;
document.documentElement.removeAttribute('data-mnote-page-history-open');
}
function ensurePageShareDialog() {
var existing = document.querySelector('[data-testid="wolai-page-share-dialog"]');
if (existing instanceof HTMLElement) return existing;
var dialog = document.createElement('div');
dialog.className = 'wolai-page-share-dialog';
dialog.setAttribute('data-testid', 'wolai-page-share-dialog');
dialog.setAttribute('data-mnote-surface', 'page-share');
dialog.hidden = true;
dialog.innerHTML = '' +
'<div class="wolai-page-share-card" role="dialog" aria-modal="true">' +
'<div class="wolai-page-share-header">' +
'<div><div class="wolai-page-share-title">公开分享页面</div><div class="wolai-page-share-subtitle">当前 3000 公开入口由 mnote-web 持有。</div></div>' +
'<button type="button" class="wolai-surface-close" data-page-share-action="close" aria-label="关闭公开分享页面">×</button>' +
'</div>' +
'<div class="wolai-page-share-state">' +
'<span class="wolai-public-pill wolai-public-pill--inline">全网公开</span>' +
'<span class="wolai-page-share-copy">任何拥有链接的人都可以访问当前页面。</span>' +
'</div>' +
'<div class="wolai-page-share-url-row">' +
'<input class="wolai-page-share-url" type="text" readonly data-page-share-url value="" />' +
'<button type="button" class="wolai-page-share-copy-button" data-page-share-action="copy-link">复制链接</button>' +
'</div>' +
'<div class="wolai-page-share-footer">更多共享者、群组公开和权限策略仍待接线。</div>' +
'</div>';
dialog.addEventListener('click', function(event) {
if (event.target === dialog) closePageShareDialog();
});
document.body.appendChild(dialog);
return dialog;
}
function openPageShareDialog() {
var dialog = ensurePageShareDialog();
var input = dialog.querySelector('[data-page-share-url]');
if (input instanceof HTMLInputElement) input.value = window.location.href;
dialog.hidden = false;
document.documentElement.setAttribute('data-mnote-page-share-open', 'true');
}
function closePageShareDialog() {
var dialog = document.querySelector('[data-testid="wolai-page-share-dialog"]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
document.documentElement.removeAttribute('data-mnote-page-share-open');
}
function pageAiSuggestions() {
var title = searchText(document.querySelector('[data-page-title-current="true"]')?.textContent) || '当前页面';
return [
'帮我总结《' + title + '》当前内容',
'把当前页面改写得更简洁一些',
'提炼当前页的关键待办和行动项',
'基于当前页内容生成一个三段式摘要'
];
}
function pageAiStorageKey() {
return 'hermes_page_ai_session:' + currentDocumentId();
}
function pageAiTimestamp(value) {
if (typeof value === 'number' && Number.isFinite(value)) return value;
if (typeof value === 'string' && value.trim()) {
var parsed = Date.parse(value);
if (Number.isFinite(parsed)) return parsed;
}
return Date.now();
}
function pageAiBackendSessionQuery(extra) {
var params = new URLSearchParams();
params.set('source', 'acp');
params.set('workspaceId', resolveWorkspaceId(document.body));
params.set('documentId', currentDocumentId());
params.set('profile', pageAiRunProfile());
params.set('sourceKind', currentSourceKind());
if (currentRootUri()) params.set('rootUri', currentRootUri());
Object.keys(extra || {}).forEach(function(key) {
var value = extra[key];
if (value !== undefined && value !== null && String(value).trim() !== '') {
params.set(key, String(value));
}
});
return params.toString();
}
function pageAiNewSession(title) {
var now = Date.now();
return {
id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
title: title || '新会话',
profile: pageAiCurrentProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
createdAt: now,
updatedAt: now,
source: 'local',
usage: null,
status: 'idle',
messages: []
};
}
function pageAiUsageSummary(usage) {
if (!usage || typeof usage !== 'object') return '';
var used = usage.used ?? usage.contextUsed ?? usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens;
var size = usage.size ?? usage.contextSize ?? usage.total_tokens ?? usage.totalTokens;
var output = usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens;
var parts = [];
if (Number.isFinite(Number(used))) parts.push('ctx ' + Number(used));
if (Number.isFinite(Number(size)) && Number(size) > 0) parts.push('/ ' + Number(size));
if (Number.isFinite(Number(output)) && Number(output) > 0) parts.push('out ' + Number(output));
return parts.join(' ') || '';
}
function pageAiPermissionMessage(payload, eventType) {
payload = payload && typeof payload === 'object' ? payload : {};
var permissionId = String(payload.permissionId || payload.permission_id || payload.id || ('perm_' + Date.now())).trim();
var toolName = String(payload.toolName || payload.tool || payload.name || payload.method || 'session/request_permission').trim();
var args = payload.arguments || payload.args || payload.input || payload.params || payload;
var decision = String(payload.decision || payload.result || '').trim();
if (!decision && eventType === 'permission.denied') decision = 'denied';
if (!decision && eventType === 'permission.allowed') decision = 'allowed';
return {
role: 'tool',
kind: 'permission',
permissionId: permissionId,
toolName: toolName,
argsSummary: pageAiPreviewValue(args),
content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'),
resolved: decision === 'denied' || decision === 'allowed',
decision: decision
};
}
function pageAiApplyPermissionEvent(eventName, payloadText) {
var payload = null;
try {
payload = JSON.parse(payloadText || 'null');
} catch (_) {
payload = {};
}
var message = pageAiPermissionMessage(payload, eventName);
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.kind === 'permission' && item.permissionId === message.permissionId;
});
if (existing) {
Object.assign(existing, message);
} else {
pageUiState.pageAiMessages.push(message);
}
pageUiState.pageAiPermissionRequests = pageUiState.pageAiPermissionRequests.filter(function(item) {
return item.permissionId !== message.permissionId;
}).concat([message]).slice(-20);
if (!message.resolved) {
pageAiShowPermissionDialog(message);
} else {
pageAiHidePermissionDialog();
}
}
function pageAiResolvePermission(permissionId, decision) {
permissionId = String(permissionId || '').trim();
if (!permissionId) return;
pageUiState.pageAiMessages.forEach(function(item) {
if (item.kind === 'permission' && item.permissionId === permissionId) {
item.resolved = true;
item.decision = decision;
item.content = decision === 'allow' ? '已允许权限请求' : '已拒绝权限请求';
}
});
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
renderPageAiConversation();
}
function pageAiHidePermissionDialog() {
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
if (dialog instanceof HTMLElement) dialog.hidden = true;
}
function pageAiShowPermissionDialog(message) {
if (!message || message.kind !== 'permission') return;
if (message.resolved) {
pageAiHidePermissionDialog();
return;
}
var dialog = document.querySelector('[data-page-ai-permission-dialog]');
if (!(dialog instanceof HTMLElement)) {
dialog = document.createElement('div');
dialog.className = 'wolai-page-ai-permission-dialog';
dialog.setAttribute('data-page-ai-permission-dialog', 'true');
dialog.innerHTML = '' +
'<div class="wolai-page-ai-permission-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-ai-memory-title" data-page-ai-permission-tool></div>' +
'<div class="wolai-page-ai-tool-meta" data-page-ai-permission-args></div>' +
'<div class="wolai-page-ai-message-actions">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-dialog-action>允许</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-dialog-action>拒绝</button>' +
'</div>' +
'</div>';
document.body.appendChild(dialog);
}
var tool = dialog.querySelector('[data-page-ai-permission-tool]');
if (tool instanceof HTMLElement) tool.textContent = message.toolName || 'session/request_permission';
var args = dialog.querySelector('[data-page-ai-permission-args]');
if (args instanceof HTMLElement) args.textContent = message.argsSummary || message.content || '';
dialog.querySelectorAll('[data-page-ai-permission-dialog-action]').forEach(function(button) {
if (button instanceof HTMLButtonElement) {
button.setAttribute('data-page-ai-permission-id', message.permissionId || '');
button.disabled = Boolean(message.resolved);
}
});
dialog.hidden = false;
}
function pageAiNormalizeSessions(sessions) {
return (Array.isArray(sessions) ? sessions : [])
.slice(0, 20)
.map(function(session) {
var sessionStorage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
var persistence = String(session && session.persistence || '').trim();
return {
id: String(session && session.id || '').trim() || pageAiNewSession().id,
title: String(session && session.title || '').trim() || '新会话',
profile: String(session && session.profile || pageAiCurrentProfile()).trim() || 'default',
acpRuntime: String(session && session.acpRuntime || 'reasonix').trim(),
createdAt: pageAiTimestamp(session && session.createdAt),
updatedAt: pageAiTimestamp(session && session.updatedAt),
source: String(session && session.source || 'local').trim() || 'local',
persistence: persistence,
sessionStorage: sessionStorage,
permissionLevel: String(session && (session.permissionLevel || session.permission_level) || '').trim(),
shareId: String(session && (session.shareId || session.share_id) || '').trim(),
runId: String(session && (session.runId || session.run_id) || '').trim(),
status: String(session && session.status || '').trim(),
usage: session && session.usage && typeof session.usage === 'object' ? session.usage : null,
preview: String(session && session.preview || '').trim(),
messages: Array.isArray(session && session.messages) ? session.messages.slice(-300) : []
};
})
.sort(function(a, b) {
return Number(b.updatedAt || 0) - Number(a.updatedAt || 0);
});
}
function pageAiNormalizeBackendSessionRow(row) {
if (!row || typeof row !== 'object') return null;
var payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
var sessionId = String(row.sessionId || row.session_id || '').trim();
if (!sessionId) return null;
var title = String(row.title || payload.title || payload.message || '').trim();
if (title.length > 28) title = title.slice(0, 28) + '…';
var persistence = String(row.persistence || payload.persistence || '').trim();
var sessionStorage = String(row.sessionStorage || row.session_storage || payload.sessionStorage || '').trim();
if (!sessionStorage && persistence === 'convex_acp_runtime_store') sessionStorage = 'cloud';
if (!sessionStorage && persistence === 'local_ai_session_jsonl') sessionStorage = String(row.shareId || payload.shareId || '').trim() ? 'local_shared' : 'local_private';
return {
id: sessionId,
title: title || '当前页问答',
profile: String(row.profile || payload.profile || pageAiRunProfile()).trim() || 'default',
acpRuntime: String(row.acpRuntime || row.acp_runtime || payload.acpRuntime || 'reasonix').trim(),
createdAt: pageAiTimestamp(row.createdAt || row.created_at),
updatedAt: pageAiTimestamp(row.updatedAt || row.updated_at),
source: sessionStorage === 'local_private' || sessionStorage === 'local_shared' ? 'local' : 'acp',
persistence: persistence,
sessionStorage: sessionStorage,
permissionLevel: String(row.permissionLevel || row.permission_level || payload.permissionLevel || '').trim(),
shareId: String(row.shareId || row.share_id || payload.shareId || '').trim(),
runId: String(row.runId || row.run_id || '').trim(),
status: String(row.status || (row.runtime && row.runtime.status) || '').trim(),
usage: row.usage && typeof row.usage === 'object' ? row.usage : null,
preview: String(payload.message || row.snippet || '').trim(),
messages: []
};
}
function pageAiMergeSessions(localSessions, backendSessions) {
var byId = {};
pageAiNormalizeSessions(localSessions).forEach(function(session) {
byId[session.id] = session;
});
pageAiNormalizeSessions(backendSessions).forEach(function(session) {
var existing = byId[session.id];
byId[session.id] = Object.assign({}, existing || {}, session, {
messages: session.messages.length ? session.messages : (existing && existing.messages || [])
});
});
return pageAiNormalizeSessions(Object.keys(byId).map(function(id) { return byId[id]; }));
}
function pageAiSessionStorageLabel(session) {
var storage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
var persistence = String(session && session.persistence || '').trim();
if (storage === 'local_shared') return '共享会话';
if (storage === 'local_private') return '本地私有';
if (storage === 'cloud' || persistence === 'convex_acp_runtime_store') return '云端会话';
if (persistence === 'local_ai_session_jsonl') return '本地私有';
return String(session && session.source || '').trim() === 'acp' ? '云端会话' : '本地私有';
}
function pageAiLoadSessions() {
if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return;
try {
var raw = window.localStorage.getItem(pageAiStorageKey());
var parsed = raw ? JSON.parse(raw) : null;
var activeId = String(parsed && parsed.activeSessionId || '').trim();
var activeProfile = String(parsed && parsed.activeProfileName || '').trim();
var activeAcpRuntime = String(parsed && parsed.activeAcpRuntime || '').trim();
var sessions = pageAiNormalizeSessions(parsed && parsed.sessions);
var storageVersion = Number(parsed && parsed.version || 0);
if (storageVersion >= PAGE_AI_SESSION_STORAGE_VERSION && activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime;
if (activeProfile) pageAiSetActiveProfile(activeProfile);
if (sessions.length) {
pageUiState.pageAiSessions = sessions;
var activeSession = sessions.find(function(session) { return session.id === activeId; }) || sessions[0];
pageUiState.pageAiActiveSessionId = activeSession.id;
if (activeSession.profile) pageAiSetActiveProfile(activeSession.profile);
if (!pageUiState.pageAiAcpRuntime && activeSession.acpRuntime) pageUiState.pageAiAcpRuntime = activeSession.acpRuntime;
pageUiState.pageAiMessages = Array.isArray(activeSession.messages) ? activeSession.messages.slice() : [];
return;
}
if (activeId) {
pageUiState.pageAiSessions = [pageAiNewSession('当前页问答')];
pageUiState.pageAiSessions[0].id = activeId;
pageUiState.pageAiActiveSessionId = activeId;
pageUiState.pageAiMessages = [];
return;
}
} catch (_) {}
var fresh = pageAiNewSession();
pageUiState.pageAiSessions = [fresh];
pageUiState.pageAiActiveSessionId = fresh.id;
pageUiState.pageAiMessages = [];
}
async function pageAiLoadBackendSessions() {
var response = await fetch('/api/hermes/client/sessions?' + pageAiBackendSessionQuery({ limit: 50 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_list_failed_' + response.status));
}
var backendSessions = pageAiNormalizeArray(payload.sessions).map(pageAiNormalizeBackendSessionRow).filter(Boolean);
if (!backendSessions.length) return [];
pageUiState.pageAiSessions = pageAiMergeSessions(pageUiState.pageAiSessions, backendSessions);
if (!pageUiState.pageAiActiveSessionId || !pageUiState.pageAiSessions.find(function(session) { return session.id === pageUiState.pageAiActiveSessionId; })) {
pageUiState.pageAiActiveSessionId = pageUiState.pageAiSessions[0].id;
}
var active = pageAiCurrentSession();
if (active) {
if (active.profile) pageAiSetActiveProfile(active.profile);
if (!pageUiState.pageAiAcpRuntime && active.acpRuntime) pageUiState.pageAiAcpRuntime = active.acpRuntime;
pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : pageUiState.pageAiMessages;
}
pageUiState.pageAiSessionError = '';
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
return backendSessions;
}
function pageAiMessageFromRuntimeEvent(event) {
if (!event || typeof event !== 'object') return null;
var eventType = String(event.eventType || event.event_type || event.event || '').trim();
var payload = event.payload && typeof event.payload === 'object' ? event.payload : event;
if (eventType === 'message.delta') {
var delta = String(payload.delta || payload.text || payload.output_text || '').trim();
return delta ? { role: 'assistant', content: delta } : null;
}
if (eventType === 'thought.delta') {
var thought = String(payload.delta || payload.text || '').trim();
return thought ? { role: 'assistant', kind: 'thought', content: thought } : null;
}
if (eventType === 'tool.started' || eventType === 'tool.completed' || eventType === 'tool.failed') {
var toolName = String(payload.toolName || payload.tool || payload.name || eventType).trim();
return {
role: 'tool',
content: toolName,
toolName: toolName,
toolCallId: String(payload.toolCallId || payload.tool_call_id || payload.id || ''),
toolKind: String(payload.kind || ''),
status: eventType === 'tool.completed' ? 'completed' : (eventType === 'tool.failed' ? 'failed' : 'running'),
argsSummary: pageAiPreviewValue(payload.args || payload.arguments || payload.input),
resultSummary: pageAiPreviewValue(payload.summary || payload.result || payload.output || payload.error),
traceId: String(payload.traceId || payload.trace_id || ''),
auditId: String(payload.auditId || payload.audit_id || '')
};
}
if (eventType === 'permission.requested' || eventType === 'permission.denied' || eventType === 'permission.allowed') {
return pageAiPermissionMessage(payload, eventType);
}
return null;
}
function pageAiApplyBackendSessionDetail(payload) {
var sessionPayload = payload && payload.session ? payload.session : {};
var runs = pageAiNormalizeArray(sessionPayload.runs);
var latest = runs.length ? pageAiNormalizeBackendSessionRow(runs[0]) : null;
var events = pageAiNormalizeArray(payload && payload.events);
var messages = pageAiNormalizeArray(sessionPayload.messages).map(function(message) {
return {
role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
content: String(message.content || '')
};
}).filter(function(message) { return message.content; });
events.forEach(function(event) {
var message = pageAiMessageFromRuntimeEvent(event);
if (message) messages.push(message);
});
var current = pageAiCurrentSession();
if (latest && current) {
Object.assign(current, latest);
}
if (current) {
current.messages = messages.slice(-300);
current.updatedAt = Math.max(Number(current.updatedAt || 0), Date.now());
if (latest && latest.usage) current.usage = latest.usage;
}
pageAiApplyRuntimeState(payload && payload.runtime);
pageUiState.pageAiMessages = messages.slice(-300);
pageAiPersistSessions();
}
async function pageAiLoadBackendSessionDetail(sessionId) {
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return null;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({ limit: 200 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_detail_failed_' + response.status));
}
pageAiApplyBackendSessionDetail(payload);
renderPageAiConversation();
renderPageAiControls();
return payload;
}
async function pageAiSearchBackendSessions(query) {
var q = String(query || '').trim();
pageUiState.pageAiSessionSearchQuery = q;
if (!q) {
pageUiState.pageAiSessionSearchResults = [];
renderPageAiConversation();
return [];
}
var response = await fetch('/api/hermes/client/sessions/search?' + pageAiBackendSessionQuery({ q: q, limit: 20 }), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_search_failed_' + response.status));
}
pageUiState.pageAiSessionSearchResults = pageAiNormalizeArray(payload.results).map(function(row) {
var normalized = pageAiNormalizeBackendSessionRow(row) || {};
normalized.snippet = String(row && row.snippet || normalized.preview || '').trim();
return normalized;
}).filter(function(row) { return row.id; });
renderPageAiConversation();
return pageUiState.pageAiSessionSearchResults;
}
function pageAiPersistSessions() {
document.documentElement.setAttribute('data-mnote-page-ai-session-owner', 'hermes');
document.documentElement.setAttribute('data-mnote-page-ai-session-key', pageAiStorageKey());
try {
pageAiSyncCurrentSessionMessages();
window.localStorage.setItem(pageAiStorageKey(), JSON.stringify({
version: PAGE_AI_SESSION_STORAGE_VERSION,
activeSessionId: pageUiState.pageAiActiveSessionId,
activeProfileName: pageAiCurrentProfile(),
activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
sessions: pageAiNormalizeSessions(pageUiState.pageAiSessions)
}));
} catch (_) {}
}
async function pageAiEnsureHermesSession(forceCreate) {
pageAiLoadSessions();
var current = pageAiCurrentSession();
if (!forceCreate && current && String(current.id || '').startsWith('mnote_') && current.profile === pageAiCurrentProfile()) return current;
var response = await fetch('/api/hermes/client/sessions', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
traceId: 'page-ai-' + Date.now().toString(36),
profile: pageAiCurrentProfile(),
title: current && current.title ? current.title : '当前页问答'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'hermes_session_failed_' + response.status));
}
var session = {
id: String(payload.sessionId || '').trim(),
title: String(payload.title || '当前页问答'),
profile: String(payload.profile || pageAiCurrentProfile()).trim() || 'default',
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
persistence: String(payload.persistence || '').trim(),
sessionStorage: String(payload.sessionStorage || '').trim(),
permissionLevel: String(payload.permissionLevel || '').trim(),
shareId: String(payload.shareId || '').trim(),
createdAt: Date.now(),
updatedAt: Date.now(),
messages: pageUiState.pageAiMessages.slice()
};
pageUiState.pageAiSessions = pageAiNormalizeSessions([session]);
pageUiState.pageAiActiveSessionId = session.id;
pageAiPersistSessions();
renderPageAiConversation();
return session;
}
async function pageAiRestoreHermesSession() {
var current = pageAiCurrentSession();
if (!current || !String(current.id || '').startsWith('mnote_')) return;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(current.id) + '/resume?' + pageAiBackendSessionQuery({}), {
method: 'POST',
headers: { 'accept': 'application/json' }
});
if (!response.ok) return;
var payload = await response.json().catch(function(){ return null; });
if (payload && payload.persistence === 'convex_acp_runtime_store') {
pageAiApplyBackendSessionDetail(payload);
renderPageAiConversation();
renderPageAiControls();
return;
}
var session = payload && (payload.session || (payload.upstream && payload.upstream.session) || payload);
var messages = session && Array.isArray(session.messages) ? session.messages : [];
pageAiApplyRuntimeState(payload && payload.runtime);
if (session && (session.profile || session.profileName)) {
current.profile = String(session.profile || session.profileName || current.profile || pageAiCurrentProfile());
}
if (!messages.length) {
renderPageAiControls();
return;
}
pageUiState.pageAiMessages = messages.slice(-300).map(function(message) {
return {
role: message.role === 'user' ? 'user' : (message.role === 'tool' ? 'tool' : 'assistant'),
content: String(message.content || '')
};
});
current.messages = pageUiState.pageAiMessages.slice();
current.updatedAt = Date.now();
renderPageAiConversation();
renderPageAiControls();
}
function pageAiCurrentSession() {
return pageUiState.pageAiSessions.find(function(session) {
return session.id === pageUiState.pageAiActiveSessionId;
}) || null;
}
function pageAiSyncCurrentSessionMessages() {
var session = pageAiCurrentSession();
if (!session) return;
session.messages = Array.isArray(pageUiState.pageAiMessages) ? pageUiState.pageAiMessages.slice(-300) : [];
session.profile = pageAiCurrentProfile();
session.acpRuntime = pageUiState.pageAiAcpRuntime || 'reasonix';
session.updatedAt = Date.now();
}
function pageAiSetActiveSession(sessionId) {
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
if (!session) return;
pageUiState.pageAiActiveSessionId = session.id;
pageUiState.pageAiMessages = Array.isArray(session.messages) ? session.messages.slice() : [];
pageUiState.pageAiPage = 'chat';
pageAiPersistSessions();
renderPageAiControls();
renderPageAiConversation();
if (session.source === 'acp' || String(session.id || '').startsWith('mnote_')) {
void pageAiLoadBackendSessionDetail(session.id).catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
}
}
function pageAiStartNewSession() {
var session = pageAiNewSession();
pageUiState.pageAiSessions = pageAiNormalizeSessions([session].concat(pageUiState.pageAiSessions));
pageUiState.pageAiActiveSessionId = session.id;
pageUiState.pageAiMessages = [];
pageUiState.pageAiPage = 'chat';
pageAiPersistSessions();
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiRenameBackendSession(sessionId) {
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
if (!session) return;
var title = window.prompt('重命名 AI 会话', session.title || '当前页问答');
if (title === null) return;
title = String(title || '').trim();
if (!title) return;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/rename?' + pageAiBackendSessionQuery({}), {
method: 'POST',
headers: { 'content-type': 'application/json', 'accept': 'application/json' },
body: JSON.stringify({ title: title })
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_rename_failed_' + response.status));
}
session.title = String((payload.result && payload.result.title) || title);
session.updatedAt = Date.now();
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
}
async function pageAiDeleteBackendSession(sessionId) {
var session = pageUiState.pageAiSessions.find(function(item) { return item.id === sessionId; });
if (!session) return;
if (!window.confirm('确定删除 AI 会话“' + (session.title || sessionId) + '”吗?')) return;
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '?' + pageAiBackendSessionQuery({}), {
method: 'DELETE',
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_delete_failed_' + response.status));
}
pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(item) { return item.id !== sessionId; });
if (pageUiState.pageAiActiveSessionId === sessionId) {
var next = pageUiState.pageAiSessions[0] || pageAiNewSession();
if (!pageUiState.pageAiSessions.length) pageUiState.pageAiSessions = [next];
pageUiState.pageAiActiveSessionId = next.id;
pageUiState.pageAiMessages = Array.isArray(next.messages) ? next.messages.slice() : [];
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
}
async function pageAiResumeBackendSession(sessionId) {
sessionId = String(sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return;
pageAiSetActiveSession(sessionId);
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/resume?' + pageAiBackendSessionQuery({}), {
method: 'POST',
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(pageAiErrorMessage(payload, 'session_resume_failed_' + response.status));
}
pageAiApplyBackendSessionDetail(payload);
pageUiState.pageAiPage = 'chat';
renderPageAiConversation();
renderPageAiControls();
}
function pageAiProviderLabel(provider) {
if (provider === 'codex') return 'Codex';
if (provider === 'claudecode') return 'ClaudeCode';
return 'Hermes';
}
function pageAiNormalizeArray(value) {
return Array.isArray(value) ? value : [];
}
function pageAiDefaultAcpRuntimes() {
return [
{
name: 'reasonix',
title: 'ACP · Reasonix',
description: '通过 ACP 协议直连 ReasonixDeepSeek 缓存优先)',
model: 'deepseek-chat',
preset: 'auto'
},
{
name: 'hermes',
title: 'ACP · Hermes',
description: '通过 ACP 协议直连 Hermes agent runtime'
}
];
}
function pageAiNormalizeAcpRuntimes(runtimes) {
var byName = {};
pageAiDefaultAcpRuntimes().forEach(function(runtime) {
byName[runtime.name] = Object.assign({}, runtime);
});
pageAiNormalizeArray(runtimes).forEach(function(runtime) {
var name = String(runtime && runtime.name || '').trim();
if (name !== 'reasonix' && name !== 'hermes') return;
byName[name] = Object.assign({}, byName[name] || {}, runtime, { name: name });
});
return ['reasonix', 'hermes'].map(function(name) { return byName[name]; }).filter(Boolean);
}
function pageAiUnwrapUpstream(payload) {
if (payload && typeof payload === 'object' && payload.upstream) return payload.upstream;
return payload || null;
}
function pageAiProfileValue(profile) {
if (profile && typeof profile === 'object') {
return String(profile.name || profile.profile || profile.id || '').trim();
}
return String(profile || '').trim();
}
function pageAiCurrentProfile() {
var active = String(pageUiState.pageAiActiveProfileName || '').trim();
if (active) return active;
var selected = pageUiState.pageAiProfiles.find(function(profile) {
return profile && profile.active;
});
return pageAiProfileValue(selected) || 'mnoteai';
}
function pageAiRunProfile() {
return String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix' ? 'reasonix' : pageAiCurrentProfile();
}
function pageAiMnoteToolModel() {
return String(document.documentElement.getAttribute('data-mnote-page-ai-tool-model') || 'deepseek-v4-flash').trim() || 'deepseek-v4-flash';
}
function pageAiCurrentProfileRecord() {
var active = pageAiCurrentProfile();
return pageUiState.pageAiProfiles.find(function(profile) {
return pageAiProfileValue(profile) === active;
}) || null;
}
function pageAiCurrentModelLabel() {
var profile = pageAiCurrentProfileRecord();
var toolModel = pageAiMnoteToolModel();
if (!profile) return 'tool: ' + toolModel;
var model = String(profile.model || '').trim();
var gateway = String(profile.gateway || '').trim();
var profileLabel = [model, gateway].filter(Boolean).join(' · ') || '由 Hermes 决定';
return 'tool: ' + toolModel + ' · profile: ' + profileLabel;
}
function pageAiNormalizeProfiles(payload) {
var upstream = pageAiUnwrapUpstream(payload);
var profiles = pageAiNormalizeArray(upstream && upstream.profiles ? upstream.profiles : upstream);
return profiles.map(function(profile) {
return {
name: pageAiProfileValue(profile) || 'default',
active: Boolean(profile && profile.active),
model: String(profile && profile.model || '').trim(),
gateway: String(profile && profile.gateway || '').trim(),
alias: String(profile && profile.alias || '').trim()
};
});
}
function pageAiNormalizeSkills(payload) {
var upstream = pageAiUnwrapUpstream(payload);
var categories = pageAiNormalizeArray(upstream && upstream.categories ? upstream.categories : []);
var archived = pageAiNormalizeArray(upstream && upstream.archived ? upstream.archived : []);
return {
categories: categories.map(function(category) {
return {
name: String(category && category.name || '').trim() || 'misc',
description: String(category && category.description || '').trim(),
skills: pageAiNormalizeArray(category && category.skills).map(function(skill) {
return {
name: String(skill && skill.name || '').trim(),
description: String(skill && skill.description || '').trim(),
enabled: skill && skill.enabled !== false,
source: String(skill && skill.source || 'local').trim() || 'local',
origin: String(skill && skill.origin || '').trim(),
createdBy: String(skill && skill.createdBy || '').trim(),
patchCount: Number(skill && skill.patchCount || 0),
modified: Boolean(skill && skill.modified)
};
})
};
}),
archived: archived.map(function(skill) {
return {
name: String(skill && skill.name || '').trim(),
description: String(skill && skill.description || '').trim(),
enabled: skill && skill.enabled !== false,
source: String(skill && skill.source || 'local').trim() || 'local',
origin: String(skill && skill.origin || '').trim(),
createdBy: String(skill && skill.createdBy || '').trim(),
patchCount: Number(skill && skill.patchCount || 0),
modified: Boolean(skill && skill.modified)
};
})
};
}
function pageAiSkillListEntries() {
var result = [];
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
pageAiNormalizeArray(category.skills).forEach(function(skill) {
result.push({
category: category.name,
name: skill.name,
description: skill.description,
enabled: skill.enabled !== false,
source: skill.source || 'local',
origin: skill.origin || '',
createdBy: skill.createdBy || '',
patchCount: Number(skill.patchCount || 0),
modified: Boolean(skill.modified)
});
});
});
return result.concat(pageAiNormalizeArray(pageUiState.pageAiSkills.archived));
}
function pageAiSetActiveProfile(profileName) {
var next = String(profileName || '').trim() || 'mnoteai';
pageUiState.pageAiActiveProfileName = next;
document.documentElement.setAttribute('data-mnote-page-ai-profile', next);
document.documentElement.setAttribute('data-mnote-page-ai-tool-model', pageAiMnoteToolModel());
}
function pageAiSetRunStatus(status, runId) {
pageUiState.pageAiRunStatus = status || 'idle';
pageUiState.pageAiCurrentRunId = runId || pageUiState.pageAiCurrentRunId || '';
document.documentElement.setAttribute('data-mnote-page-ai-run-status', pageUiState.pageAiRunStatus);
if (pageUiState.pageAiCurrentRunId) {
document.documentElement.setAttribute('data-mnote-page-ai-run-id', pageUiState.pageAiCurrentRunId);
}
}
function pageAiApplyRuntimeState(runtime) {
if (!runtime || typeof runtime !== 'object') return;
var status = String(runtime.status || '').trim();
var runId = String(runtime.runId || runtime.run_id || '').trim();
if (status) pageAiSetRunStatus(status, runId);
var queueLength = Number(runtime.queueLength || runtime.queue_length || 0);
if (Number.isFinite(queueLength)) pageUiState.pageAiQueueLength = Math.max(0, queueLength);
var toolName = String(runtime.lastToolName || runtime.last_tool_name || '').trim();
if (toolName) {
pageUiState.pageAiLastToolCall = {
event: String(runtime.lastEvent || runtime.last_event || ''),
name: toolName,
runId: runId,
traceId: String(runtime.traceId || runtime.trace_id || ''),
auditId: String(runtime.lastAuditId || runtime.last_audit_id || '')
};
}
}
function pageAiApplyQueuedRun(payload) {
if (!payload || payload.queued !== true) return false;
var queueId = String(payload.queueId || payload.queue_id || '').trim();
var queueLength = Number(payload.queueLength || payload.queue_length || 0);
pageUiState.pageAiQueueLength = Number.isFinite(queueLength) ? Math.max(0, queueLength) : 1;
if (queueId) {
pageUiState.pageAiQueuedItems = pageUiState.pageAiQueuedItems.filter(function(item) {
return item.queueId !== queueId;
}).concat([{
queueId: queueId,
sessionId: String(payload.sessionId || payload.session_id || pageUiState.pageAiActiveSessionId || ''),
traceId: String(payload.traceId || payload.trace_id || ''),
queuedAt: Number(payload.queuedAt || payload.queued_at || Date.now())
}]);
}
pageAiSetRunStatus('queued', pageUiState.pageAiCurrentRunId);
return true;
}
function pageAiPreviewValue(value) {
if (value == null || value === '') return '';
if (typeof value === 'string') return value.length > 120 ? value.slice(0, 120) + '…' : value;
try {
var text = JSON.stringify(value);
return text.length > 120 ? text.slice(0, 120) + '…' : text;
} catch (_) {
return String(value);
}
}
function pageAiNormalizeToolName(name) {
return String(name || '').trim().replace(/_/g, '.');
}
function pageAiFormatChangedFiles(files) {
return pageAiNormalizeArray(files).map(function(file) {
var path = String(file && file.path || '').trim();
var changeType = String(file && file.changeType || file.change_type || 'modified').trim();
var summary = String(file && file.summary || '').trim();
return [changeType, path, summary].filter(Boolean).join(' · ');
}).filter(Boolean).join('\n');
}
function pageAiToolEventDeepFindString(value, keys, depth) {
if (!value || typeof value !== 'object' || depth > 5) return '';
for (var index = 0; index < keys.length; index += 1) {
var key = keys[index];
if (Object.prototype.hasOwnProperty.call(value, key) && typeof value[key] === 'string' && value[key].trim()) {
return value[key].trim();
}
}
if (Array.isArray(value)) {
for (var arrayIndex = 0; arrayIndex < value.length; arrayIndex += 1) {
var fromArray = pageAiToolEventDeepFindString(value[arrayIndex], keys, depth + 1);
if (fromArray) return fromArray;
}
return '';
}
var preferred = ['audit', 'args', 'arguments', 'input', 'result', 'summary', 'output', 'upstream'];
for (var prefIndex = 0; prefIndex < preferred.length; prefIndex += 1) {
var child = value[preferred[prefIndex]];
var fromPreferred = pageAiToolEventDeepFindString(child, keys, depth + 1);
if (fromPreferred) return fromPreferred;
}
var objectKeys = Object.keys(value);
for (var objectIndex = 0; objectIndex < objectKeys.length; objectIndex += 1) {
var fromObject = pageAiToolEventDeepFindString(value[objectKeys[objectIndex]], keys, depth + 1);
if (fromObject) return fromObject;
}
return '';
}
function pageAiNotifyToolWriteCompleted(toolName, toolEvent, runId, runTraceId) {
var normalizedTool = pageAiNormalizeToolName(toolName);
var writesCurrentPage = [
'mnote.page.save',
'mnote.page.update.title',
'mnote.page.update.options',
'mnote.doc.apply.block.ops',
'mnote.block.replace',
'mnote.block.insert.after',
'mnote.block.delete',
'mnote.block.move.after'
].indexOf(normalizedTool) >= 0 || [
'mnote.page.update_title',
'mnote.page.update_options',
'mnote.doc.apply_block_ops',
'mnote.block.replace',
'mnote.block.insert_after',
'mnote.block.delete',
'mnote.block.move_after'
].indexOf(String(toolName || '').trim()) >= 0;
if (!writesCurrentPage) return;
var documentId = pageAiToolEventDeepFindString(toolEvent, ['documentId', 'document_id'], 0) || currentDocumentId();
var workspaceId = pageAiToolEventDeepFindString(toolEvent, ['workspaceId', 'workspace_id'], 0) || resolveWorkspaceId(document.body);
try {
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
detail: {
toolName: toolName,
normalizedToolName: normalizedTool,
documentId: documentId,
workspaceId: workspaceId,
runId: String(toolEvent && (toolEvent.run_id || toolEvent.runId) || runId || ''),
traceId: String(toolEvent && (toolEvent.trace_id || toolEvent.traceId) || runTraceId || ''),
toolCallId: String(toolEvent && (toolEvent.tool_call_id || toolEvent.toolCallId || toolEvent.id) || '')
}
}));
} catch (error) {
console.warn('mnote 页面 AI 写入刷新事件派发失败', error);
}
}
function pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId) {
var toolEvent = null;
try {
toolEvent = JSON.parse(payloadText || 'null');
} catch (_) {
toolEvent = {};
}
var rawToolName = toolEvent && (toolEvent.name || toolEvent.tool || toolEvent.toolName || '');
var toolName = String(rawToolName || eventName);
var toolCallId = String(toolEvent && (toolEvent.tool_call_id || toolEvent.toolCallId || toolEvent.id) || (runId + ':' + toolName));
var eventStatus = String(toolEvent && toolEvent.status || '').trim();
var status = eventName === 'tool.completed'
? (toolEvent && toolEvent.error ? 'failed' : 'completed')
: (eventName === 'tool.failed' ? 'failed' : (eventStatus || 'running'));
if (status === 'in_progress' || status === 'pending') status = 'running';
var traceId = String(toolEvent && (toolEvent.trace_id || toolEvent.traceId) || runTraceId || '');
var auditId = String(toolEvent && (toolEvent.audit_id || toolEvent.auditId) || '');
var argsSummary = pageAiPreviewValue(toolEvent && (toolEvent.arguments || toolEvent.args || toolEvent.input));
var resultSource = toolEvent && (toolEvent.summary || toolEvent.result || toolEvent.output);
if (!resultSource && status === 'failed') {
resultSource = [toolEvent && toolEvent.code, toolEvent && toolEvent.error].filter(Boolean).join(' ');
}
var resultSummary = pageAiPreviewValue(resultSource);
pageUiState.pageAiLastToolCall = {
event: eventName,
name: toolName,
runId: String(toolEvent && (toolEvent.run_id || toolEvent.runId) || runId),
traceId: traceId,
auditId: auditId
};
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'tool' && item.toolCallId === toolCallId;
});
if (!rawToolName && existing && existing.toolName) toolName = existing.toolName;
if (!existing) {
existing = {
role: 'tool',
content: toolName,
toolCallId: toolCallId,
toolName: toolName,
toolKind: String(toolEvent && toolEvent.kind || ''),
status: status,
argsSummary: '',
resultSummary: '',
traceId: traceId,
auditId: auditId
};
pageUiState.pageAiMessages.push(existing);
}
existing.content = toolName;
existing.toolName = toolName;
existing.toolKind = String(toolEvent && toolEvent.kind || existing.toolKind || '');
existing.status = status;
existing.traceId = traceId || existing.traceId || '';
existing.auditId = auditId || existing.auditId || '';
if (argsSummary) existing.argsSummary = argsSummary;
if (resultSummary) existing.resultSummary = resultSummary;
if (status === 'completed') {
pageAiNotifyToolWriteCompleted(toolName, toolEvent, runId, runTraceId);
}
}
async function pageAiCancelQueuedRun(queueId) {
queueId = String(queueId || '').trim();
if (!queueId) return;
var item = pageUiState.pageAiQueuedItems.find(function(entry) {
return entry.queueId === queueId;
});
var sessionId = String(item && item.sessionId || pageUiState.pageAiActiveSessionId || '').trim();
if (!sessionId) return;
try {
var response = await fetch('/api/hermes/client/sessions/' + encodeURIComponent(sessionId) + '/queue/' + encodeURIComponent(queueId), {
method: 'DELETE',
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'queue_cancel_failed_' + response.status));
pageUiState.pageAiQueuedItems = pageUiState.pageAiQueuedItems.filter(function(entry) {
return entry.queueId !== queueId;
});
var queueLength = Number(payload && (payload.queueLength || payload.queue_length || 0));
pageUiState.pageAiQueueLength = Number.isFinite(queueLength) ? Math.max(0, queueLength) : pageUiState.pageAiQueuedItems.length;
pageUiState.pageAiMessages.push({ role: 'assistant', content: '已取消一条 Hermes 队列项。' });
} catch (error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '取消 Hermes 队列项失败:' + (error instanceof Error ? error.message : String(error))
});
}
renderPageAiConversation();
renderPageAiControls();
}
function pageAiSetContextScope(scope) {
var next = String(scope || '').trim() || 'page';
pageUiState.pageAiContextScope = next;
document.documentElement.setAttribute('data-mnote-page-ai-context-scope', next);
}
function pageAiRunStatusLabel(status) {
if (status === 'queued') return '排队中';
if (status === 'running') return '运行中';
if (status === 'tool_calling') return '调用工具';
if (status === 'completed') return '已完成';
if (status === 'failed') return '失败';
if (status === 'aborted') return '已停止';
return '空闲';
}
function pageAiMemoryFileLabel(section) {
if (section === 'soul') return 'SOUL.md';
if (section === 'user') return 'USER.md';
return 'MEMORY.md';
}
function pageAiContextScopeLabel(scope) {
if (scope === 'selection') return '当前选区';
if (scope === 'block') return '当前块';
if (scope === 'options') return '页面设置';
return '当前页';
}
function pageAiHermesSettingsUrl() {
var configured = String(window.__mnoteHermesSettingsUrl || '').trim();
return configured || '';
}
function pageAiOpenHermesSettings() {
var url = pageAiHermesSettingsUrl();
if (url) {
window.open(url, '_blank', 'noopener,noreferrer');
return;
}
pageUiState.pageAiProfileError = '未配置 Hermes 设置入口:请设置 MNOTE_WEB_HERMES_UPSTREAM_URL。';
renderPageAiControls();
}
function pageAiNormalizeTools(payload) {
var upstream = pageAiUnwrapUpstream(payload) || {};
var tools = pageAiNormalizeArray(upstream.tools || upstream);
return tools.map(function(tool) {
var name = String(tool && (tool.name || tool.toolName || tool.tool) || '').trim();
if (!name) return null;
return {
name: name,
description: String(tool && tool.description || '').trim(),
scope: String(tool && (tool.scope || tool.requiredScope || '') || '').trim(),
kind: String(tool && (tool.kind || tool.mode || '') || '').trim(),
status: String(tool && (tool.status || tool.permission || 'available') || '').trim(),
enabled: tool && tool.enabled !== false,
unavailableReason: String(tool && (tool.unavailableReason || tool.reason || '') || '').trim()
};
}).filter(Boolean);
}
function pageAiErrorMessage(payload, fallback) {
if (!payload || typeof payload !== 'object') return fallback;
return String(payload.message || payload.error || payload.code || fallback || '').trim() || fallback;
}
function pageAiNormalizeGatewayHealth(payload) {
var upstream = pageAiUnwrapUpstream(payload) || payload || {};
return {
ok: Boolean(upstream.ok),
profile: upstream.profile || null,
gateway: upstream.gateway || null,
suggestions: pageAiNormalizeArray(upstream.suggestions).map(function(item) {
return String(item || '').trim();
}).filter(Boolean)
};
}
function pageAiSetDraftForSection(section, value) {
pageUiState.pageAiProfileMemoryDrafts[section] = String(value == null ? '' : value);
}
function pageAiApplyProfileMemory(payload) {
var upstream = pageAiUnwrapUpstream(payload) || {};
pageUiState.pageAiProfileMemory = {
memory: String(upstream.memory || ''),
user: String(upstream.user || ''),
soul: String(upstream.soul || '')
};
pageUiState.pageAiProfileMemoryDrafts = {
memory: pageUiState.pageAiProfileMemory.memory,
user: pageUiState.pageAiProfileMemory.user,
soul: pageUiState.pageAiProfileMemory.soul
};
}
async function pageAiLoadTools() {
try {
var response = await fetch('/api/hermes/client/tools?scope=mnote&profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'tools_failed_' + response.status));
pageUiState.pageAiTools = pageAiNormalizeTools(payload);
pageUiState.pageAiToolsError = '';
} catch (error) {
pageUiState.pageAiToolsError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
}
async function pageAiLoadGatewayHealth() {
try {
var response = await fetch('/api/hermes/client/gateway/health?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'gateway_health_failed_' + response.status));
pageUiState.pageAiGatewayHealth = pageAiNormalizeGatewayHealth(payload);
pageUiState.pageAiGatewayHealthError = '';
} catch (error) {
pageUiState.pageAiGatewayHealthError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
}
async function pageAiStopRun() {
var runId = String(pageUiState.pageAiCurrentRunId || '').trim();
if (!runId || pageUiState.pageAiRunStatus === 'idle' || pageUiState.pageAiRunStatus === 'completed') return;
pageUiState.pageAiStoppedRunIds[runId] = true;
try {
var response = await fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/abort', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
reason: 'page_ai_user_stop'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'run_abort_failed_' + response.status));
pageAiApplyRuntimeState(payload && payload.runtime);
pageAiSetRunStatus('aborted', runId);
pageUiState.pageAiMessages.push({ role: 'assistant', content: '已请求 Hermes 停止当前 run。' });
} catch (error) {
pageAiSetRunStatus('failed', runId);
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '停止 Hermes run 失败:' + (error instanceof Error ? error.message : String(error))
});
}
renderPageAiControls();
renderPageAiConversation();
}
function pageAiFilteredSkillEntries() {
var query = String(pageUiState.pageAiSkillQuery || '').trim().toLowerCase();
return pageAiSkillListEntries().filter(function(skill) {
if (!query) return true;
return String(skill.name || '').toLowerCase().indexOf(query) >= 0
|| String(skill.description || '').toLowerCase().indexOf(query) >= 0
|| String(skill.category || '').toLowerCase().indexOf(query) >= 0
|| pageAiSkillOriginLabel(skill).toLowerCase().indexOf(query) >= 0;
});
}
function pageAiSkillOriginLabel(skill) {
var origin = String(skill && skill.origin || '').trim();
if (origin === 'generated' || String(skill && skill.createdBy || '') === 'agent') return '生成';
if (origin === 'installed') return '安装';
if (origin === 'builtin') return '内置';
if (origin === 'copied') return '本地';
var source = String(skill && skill.source || '').trim();
if (source === 'hub') return '安装';
if (source === 'builtin') return '内置';
if (source === 'reasonix') {
if (origin === 'project') return 'Reasonix 项目';
if (origin === 'global') return 'Reasonix 全局';
return 'Reasonix';
}
return '本地';
}
async function pageAiLoadProfiles() {
try {
var response = await fetch('/api/hermes/client/profiles', {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profiles_failed_' + response.status));
var profiles = pageAiNormalizeProfiles(payload);
pageUiState.pageAiProfiles = profiles.length ? profiles : [{ name: 'mnoteai', active: true }];
var acpRuntimes = Array.isArray(payload.acpRuntimes) ? payload.acpRuntimes : [];
pageUiState.pageAiAcpRuntimes = pageAiNormalizeAcpRuntimes(acpRuntimes);
var current = pageAiCurrentProfile();
var hasCurrent = pageUiState.pageAiProfiles.some(function(profile) { return pageAiProfileValue(profile) === current; });
var hasMnoteAi = pageUiState.pageAiProfiles.some(function(profile) { return pageAiProfileValue(profile) === 'mnoteai'; });
var active = pageAiProfileValue(pageUiState.pageAiProfiles.find(function(profile) { return profile.active; }));
pageAiSetActiveProfile(hasCurrent && current !== 'default' ? current : (hasMnoteAi ? 'mnoteai' : (active || current)));
pageUiState.pageAiProfileError = '';
void pageAiLoadGatewayHealth();
} catch (error) {
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
if (!pageUiState.pageAiProfiles.length) pageUiState.pageAiProfiles = [{ name: 'mnoteai', active: true }];
pageUiState.pageAiAcpRuntimes = pageAiNormalizeAcpRuntimes(pageUiState.pageAiAcpRuntimes);
pageAiSetActiveProfile(pageAiCurrentProfile());
}
renderPageAiProviderButtons();
renderPageAiControls();
}
async function pageAiSwitchProfile(profileName) {
var next = String(profileName || '').trim();
if (!next) return;
pageAiSetActiveProfile(next);
renderPageAiProviderButtons();
renderPageAiControls();
try {
var response = await fetch('/api/hermes/client/profiles/active', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({ name: next })
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profile_switch_failed_' + response.status));
pageUiState.pageAiSessions = pageUiState.pageAiSessions.filter(function(session) {
return session && session.profile === next;
});
pageUiState.pageAiActiveSessionId = '';
pageUiState.pageAiMessages = [];
pageAiPersistSessions();
await pageAiEnsureHermesSession(true);
await pageAiLoadProfileMemory();
await pageAiLoadSkills();
await pageAiLoadTools();
await pageAiLoadGatewayHealth();
renderPageAiControls();
} catch (error) {
pageUiState.pageAiProfileError = error instanceof Error ? error.message : String(error);
}
renderPageAiConversation();
}
async function pageAiLoadProfileMemory() {
try {
var response = await fetch('/api/hermes/client/profile-memory?profile=' + encodeURIComponent(pageAiCurrentProfile()), {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profile_memory_failed_' + response.status));
pageAiApplyProfileMemory(payload);
pageUiState.pageAiProfileMemoryError = '';
} catch (error) {
pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiSaveProfileMemory(section) {
var normalized = String(section || '').trim();
if (['memory', 'user', 'soul'].indexOf(normalized) < 0) return;
var content = String(pageUiState.pageAiProfileMemoryDrafts[normalized] || '');
try {
var response = await fetch('/api/hermes/client/profile-memory', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
profile: pageAiCurrentProfile(),
section: normalized,
content: content
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'profile_memory_save_failed_' + response.status));
pageUiState.pageAiProfileMemory[normalized] = content;
pageUiState.pageAiProfileMemoryError = '';
document.documentElement.setAttribute('data-mnote-page-ai-memory-saved', normalized);
} catch (error) {
pageUiState.pageAiProfileMemoryError = error instanceof Error ? error.message : String(error);
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiLoadSkills() {
try {
var runtime = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim();
var params = runtime === 'reasonix'
? 'runtime=reasonix'
: 'profile=' + encodeURIComponent(pageAiCurrentProfile());
var response = await fetch('/api/hermes/client/skills?' + params, {
headers: { 'accept': 'application/json' }
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'skills_failed_' + response.status));
pageUiState.pageAiSkills = pageAiNormalizeSkills(payload);
pageUiState.pageAiSkillError = '';
} catch (error) {
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
pageUiState.pageAiSkills = { categories: [], archived: [] };
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiToggleSkill(skillName, enabled) {
var name = String(skillName || '').trim();
if (!name) return;
if (String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix') return;
var previous = null;
pageAiSkillListEntries().forEach(function(skill) {
if (skill.name === name && previous == null) previous = skill.enabled !== false;
});
try {
var response = await fetch('/api/hermes/client/skills/toggle', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
profile: pageAiCurrentProfile(),
name: name,
enabled: Boolean(enabled)
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'skill_toggle_failed_' + response.status));
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
pageAiNormalizeArray(category.skills).forEach(function(skill) {
if (skill.name === name) skill.enabled = Boolean(enabled);
});
});
document.documentElement.setAttribute('data-mnote-page-ai-skill-toggled', name);
pageUiState.pageAiSkillError = '';
} catch (error) {
pageUiState.pageAiSkillError = error instanceof Error ? error.message : String(error);
if (previous != null) {
pageAiNormalizeArray(pageUiState.pageAiSkills.categories).forEach(function(category) {
pageAiNormalizeArray(category.skills).forEach(function(skill) {
if (skill.name === name) skill.enabled = previous;
});
});
}
}
renderPageAiControls();
renderPageAiConversation();
}
async function pageAiToggleTool(toolName, enabled) {
var name = String(toolName || '').trim();
if (!name) return;
var previous = null;
pageAiNormalizeArray(pageUiState.pageAiTools).forEach(function(tool) {
if (tool.name === name && previous == null) previous = tool.enabled !== false;
});
try {
var response = await fetch('/api/hermes/client/tools/toggle', {
method: 'PUT',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
profile: pageAiCurrentProfile(),
name: name,
enabled: Boolean(enabled)
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok) throw new Error(pageAiErrorMessage(payload, 'tool_toggle_failed_' + response.status));
pageAiNormalizeArray(pageUiState.pageAiTools).forEach(function(tool) {
if (tool.name === name) {
tool.enabled = Boolean(enabled);
tool.status = Boolean(enabled) ? 'available' : 'disabled';
tool.unavailableReason = Boolean(enabled) ? '' : '当前 Hermes profile 已关闭该 mnote tool';
}
});
document.documentElement.setAttribute('data-mnote-page-ai-tool-toggled', name);
pageUiState.pageAiToolsError = '';
} catch (error) {
pageUiState.pageAiToolsError = error instanceof Error ? error.message : String(error);
if (previous != null) {
pageAiNormalizeArray(pageUiState.pageAiTools).forEach(function(tool) {
if (tool.name === name) tool.enabled = previous;
});
}
}
renderPageAiControls();
}
function renderPageAiProviderButtons() {
var drawer = ensurePageAiDrawer();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
drawer.querySelectorAll('[data-page-ai-provider]').forEach(function(button) {
var provider = button.getAttribute('data-page-ai-provider') || 'hermes';
var active = provider === pageUiState.pageAiProvider;
button.classList.toggle('is-active', active);
button.setAttribute('aria-pressed', active ? 'true' : 'false');
});
var providerNode = drawer.querySelector('.wolai-page-ai-subtitle span:first-child');
if (providerNode instanceof HTMLElement) {
providerNode.textContent = isAcp ? 'ACP · ' + (pageUiState.pageAiAcpRuntime === 'reasonix' ? 'Reasonix' : 'Hermes') : pageAiProviderLabel(pageUiState.pageAiProvider);
}
}
function renderPageAiControls() {
var drawer = ensurePageAiDrawer();
var isAcp = pageUiState.pageAiAcpRuntime !== '';
var activeProfile = pageAiRunProfile();
drawer.setAttribute('data-page-ai-page', pageUiState.pageAiPage || 'chat');
drawer.setAttribute('data-mnote-acp-runtime', pageUiState.pageAiAcpRuntime || 'reasonix');
// Populate ACP runtime dropdown
var acpSelect = drawer.querySelector('[data-page-ai-acp-runtime]');
if (acpSelect instanceof HTMLSelectElement) {
var runtimes = pageUiState.pageAiAcpRuntimes.length ? pageUiState.pageAiAcpRuntimes : pageAiNormalizeAcpRuntimes([]);
acpSelect.innerHTML = runtimes.map(function(rt) {
return '<option value="' + escapeHtml(rt.name || '') + '"' + ((rt.name || '') === pageUiState.pageAiAcpRuntime ? ' selected' : '') + '>' + escapeHtml(rt.title || rt.name) + '</option>';
}).join('');
acpSelect.value = pageUiState.pageAiAcpRuntime || 'reasonix';
}
// Show/hide Hermes-specific profile select
var profileLabel = drawer.querySelector('[data-page-ai-hermes-profile]');
if (profileLabel instanceof HTMLElement) {
profileLabel.style.display = pageUiState.pageAiAcpRuntime === 'reasonix' ? 'none' : '';
}
// When ACP is selected, populate agent panel with ACP runtime info
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
if (agentPanel instanceof HTMLElement) {
if (pageUiState.pageAiAcpRuntime === 'reasonix') {
var rt = pageUiState.pageAiAcpRuntimes.find(function(r) { return r.name === pageUiState.pageAiAcpRuntime; }) || {};
var keyStatus = rt.apiKeyConfigured ? '已配置' : '未检测到 DEEPSEEK_API_KEY';
agentPanel.innerHTML = '' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">' + escapeHtml(rt.title || 'ACP Runtime') + '</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">模型</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(rt.model || 'deepseek-chat') + '</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">Preset</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(rt.preset || 'auto') + '</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">API Key</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(keyStatus) + '</div></div>' +
'</section>' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">描述</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(rt.description || '') + '</div></div>' +
'</section>';
}
}
var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
var profileSelect = drawer.querySelector('[data-page-ai-profile-select]');
if (profileSelect instanceof HTMLSelectElement) {
var profiles = pageUiState.pageAiProfiles.length ? pageUiState.pageAiProfiles : [{ name: activeProfile, active: true }];
profileSelect.innerHTML = profiles.map(function(profile) {
var name = pageAiProfileValue(profile) || 'default';
var model = [profile.model, profile.gateway].filter(Boolean).join(' / ');
var label = [name, profile.alias, model].filter(Boolean).join(' · ');
return '<option value="' + escapeHtml(name) + '"' + (name === activeProfile ? ' selected' : '') + '>' + escapeHtml(label) + '</option>';
}).join('');
profileSelect.value = activeProfile;
}
var runStatus = drawer.querySelector('[data-page-ai-run-status]');
if (runStatus instanceof HTMLElement) {
var queueSuffix = pageUiState.pageAiQueueLength > 0 ? ' · 队列 ' + pageUiState.pageAiQueueLength : '';
runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus) + queueSuffix;
}
var stopButton = drawer.querySelector('[data-page-ai-action="stop-run"]');
if (stopButton instanceof HTMLButtonElement) {
var canStop = ['queued', 'running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0 && pageUiState.pageAiCurrentRunId;
stopButton.disabled = !canStop;
stopButton.setAttribute('aria-disabled', canStop ? 'false' : 'true');
}
var settingsLink = drawer.querySelector('[data-page-ai-action="open-hermes-settings"]');
if (settingsLink instanceof HTMLButtonElement) {
settingsLink.disabled = !pageAiHermesSettingsUrl();
settingsLink.title = pageAiHermesSettingsUrl() ? '打开 Hermes 设置' : '未配置 MNOTE_WEB_HERMES_UPSTREAM_URL';
}
var queueList = drawer.querySelector('[data-page-ai-queue-list]');
if (queueList instanceof HTMLElement) {
if (!pageUiState.pageAiQueuedItems.length) {
queueList.innerHTML = '<div class="wolai-page-ai-empty">暂无排队项。</div>';
} else {
queueList.innerHTML = pageUiState.pageAiQueuedItems.map(function(item, index) {
var label = '队列 ' + (index + 1);
return '' +
'<div class="wolai-page-ai-tool-row" data-page-ai-queue-item="' + escapeHtml(item.queueId) + '">' +
'<div><strong>' + escapeHtml(label) + '</strong><br /><span>' + escapeHtml(item.queueId) + '</span></div>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="cancel-queued-run" data-page-ai-queue-id="' + escapeHtml(item.queueId) + '">取消</button>' +
'</div>';
}).join('');
}
}
var sessionNode = drawer.querySelector('[data-page-ai-session-status]');
if (sessionNode instanceof HTMLElement) {
var session = pageAiCurrentSession();
var usageText = session && session.usage ? pageAiUsageSummary(session.usage) : '';
sessionNode.textContent = session && session.id
? [String(session.title || '当前页问答'), pageAiSessionStorageLabel(session), usageText].filter(Boolean).join(' · ')
: '等待 Hermes session';
}
var modelNode = drawer.querySelector('[data-page-ai-model-status]');
if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel();
var scopeSelect = drawer.querySelector('[data-page-ai-context-scope]');
if (scopeSelect instanceof HTMLSelectElement) scopeSelect.value = pageUiState.pageAiContextScope;
var scopeLabel = drawer.querySelector('[data-page-ai-context-scope-label]');
if (scopeLabel instanceof HTMLElement) scopeLabel.textContent = pageAiContextScopeLabel(pageUiState.pageAiContextScope);
drawer.querySelectorAll('[data-page-ai-tab]').forEach(function(button) {
var target = button.getAttribute('data-page-ai-tab') || 'chat';
var active = target === pageUiState.pageAiPage;
button.classList.toggle('is-active', active);
button.setAttribute('aria-selected', active ? 'true' : 'false');
});
drawer.querySelectorAll('[data-page-ai-panel]').forEach(function(panel) {
if (panel instanceof HTMLElement) {
panel.hidden = panel.getAttribute('data-page-ai-panel') !== pageUiState.pageAiPage;
}
});
var profileError = drawer.querySelector('[data-page-ai-profile-error]');
if (profileError instanceof HTMLElement) {
profileError.textContent = pageUiState.pageAiProfileError || '';
profileError.hidden = !pageUiState.pageAiProfileError;
}
var memoryError = drawer.querySelector('[data-page-ai-memory-error]');
if (memoryError instanceof HTMLElement) {
var memoryErrorText = pageUiState.pageAiProfileMemoryError || '';
if (memoryErrorText && memoryErrorText === pageUiState.pageAiProfileError) memoryErrorText = '';
memoryError.textContent = memoryErrorText;
memoryError.hidden = !memoryErrorText;
}
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
if (agentPanel instanceof HTMLElement && pageUiState.pageAiAcpRuntime !== 'reasonix') {
agentPanel.innerHTML = ['soul', 'user', 'memory'].map(function(section) {
var label = pageAiMemoryFileLabel(section);
var value = pageUiState.pageAiProfileMemoryDrafts[section] || '';
return '' +
'<section class="wolai-page-ai-memory-card">' +
'<div class="wolai-page-ai-memory-head">' +
'<div>' +
'<div class="wolai-page-ai-memory-title">' + escapeHtml(label) + '</div>' +
'<div class="wolai-page-ai-memory-scope">保存到 Hermes profile: ' + escapeHtml(activeProfile) + '</div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-memory-save="' + escapeHtml(section) + '">保存</button>' +
'</div>' +
'<textarea class="wolai-page-ai-memory-editor" data-page-ai-memory-editor="' + escapeHtml(section) + '" spellcheck="false">' + escapeHtml(value) + '</textarea>' +
'</section>';
}).join('');
}
var skillError = drawer.querySelector('[data-page-ai-skill-error]');
if (skillError instanceof HTMLElement) {
skillError.textContent = pageUiState.pageAiSkillError || '';
skillError.hidden = !pageUiState.pageAiSkillError;
}
var sessionError = drawer.querySelector('[data-page-ai-session-error]');
if (sessionError instanceof HTMLElement) {
sessionError.textContent = pageUiState.pageAiSessionError || '';
sessionError.hidden = !pageUiState.pageAiSessionError;
}
var sessionSearch = drawer.querySelector('[data-page-ai-session-search]');
if (sessionSearch instanceof HTMLInputElement && document.activeElement !== sessionSearch) {
sessionSearch.value = pageUiState.pageAiSessionSearchQuery || '';
}
var skillSearch = drawer.querySelector('[data-page-ai-skill-search]');
if (skillSearch instanceof HTMLInputElement && document.activeElement !== skillSearch) {
skillSearch.value = pageUiState.pageAiSkillQuery;
}
var skillList = drawer.querySelector('[data-page-ai-skill-list]');
if (skillList instanceof HTMLElement) {
var skills = pageAiFilteredSkillEntries();
if (!skills.length) {
var emptyText = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix'
? '没有匹配的 Reasonix skill。'
: '没有匹配的 Hermes skill。';
skillList.innerHTML = '<div class="wolai-page-ai-empty">' + escapeHtml(emptyText) + '</div>';
} else {
skillList.innerHTML = skills.map(function(skill) {
var sourceText = pageAiSkillOriginLabel(skill) + (skill.modified ? ' · modified' : '');
var description = String(skill.description || '').trim();
var hasDescription = description && description !== '---' && description !== '无描述';
var canToggle = skill.toggleable !== false && String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() !== 'reasonix';
return '' +
'<div class="wolai-page-ai-skill-row">' +
'<div class="wolai-page-ai-skill-copy">' +
'<div class="wolai-page-ai-skill-main">' +
'<div class="wolai-page-ai-skill-name">' + escapeHtml(skill.name) + '</div>' +
'<div class="wolai-page-ai-skill-source">' + escapeHtml(sourceText) + '</div>' +
'</div>' +
(hasDescription ? '<div class="wolai-page-ai-skill-desc">' + escapeHtml(description) + '</div>' : '') +
'</div>' +
'<button type="button" class="wolai-page-ai-skill-switch' + (skill.enabled !== false ? ' is-on' : '') + '" data-page-ai-skill-toggle="' + escapeHtml(skill.name) + '" aria-pressed="' + (skill.enabled !== false ? 'true' : 'false') + '"' + (canToggle ? '' : ' disabled title="Reasonix skills 当前为只读展示"') + '>' +
'<span></span>' +
'</button>' +
'</div>';
}).join('');
}
}
var toolsList = drawer.querySelector('[data-page-ai-tool-list]');
if (toolsList instanceof HTMLElement) {
var tools = pageAiNormalizeArray(pageUiState.pageAiTools);
if (!tools.length) {
toolsList.innerHTML = '<div class="wolai-page-ai-empty">尚未读取到 mnote tool manifest。</div>';
} else {
toolsList.innerHTML = tools.map(function(tool) {
return '' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-skill-copy">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(tool.name) + '</div>' +
'<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.scope || tool.kind || 'mnote') + ' · ' + escapeHtml(tool.status || 'available') + '</div>' +
(tool.unavailableReason ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.unavailableReason) + '</div>' : '') +
'</div>' +
'<button type="button" class="wolai-page-ai-skill-switch' + (tool.enabled !== false ? ' is-on' : '') + '" data-page-ai-tool-toggle="' + escapeHtml(tool.name) + '" aria-pressed="' + (tool.enabled !== false ? 'true' : 'false') + '">' +
'<span></span>' +
'</button>' +
'</div>';
}).join('');
}
}
var gatewayError = drawer.querySelector('[data-page-ai-gateway-error]');
if (gatewayError instanceof HTMLElement) {
gatewayError.textContent = pageUiState.pageAiGatewayHealthError || '';
gatewayError.hidden = !pageUiState.pageAiGatewayHealthError;
}
var gatewayStatusNode = drawer.querySelector('[data-page-ai-gateway-status]');
var gatewayDetail = drawer.querySelector('[data-page-ai-gateway-detail]');
var gatewayHealth = pageUiState.pageAiGatewayHealth;
if (gatewayStatusNode instanceof HTMLElement) {
if (!gatewayHealth) {
gatewayStatusNode.textContent = '未检查';
} else {
var gateway = gatewayHealth.gateway || {};
var profile = gatewayHealth.profile || {};
gatewayStatusNode.textContent = gatewayHealth.ok ? '可用' : '需要设置';
if (gateway.status) gatewayStatusNode.textContent += ' · ' + gateway.status;
if (profile.name) gatewayStatusNode.textContent += ' · ' + profile.name;
}
}
if (gatewayDetail instanceof HTMLElement) {
if (!gatewayHealth) {
gatewayDetail.innerHTML = '<div class="wolai-page-ai-empty">打开 Runtime 后会检查 Hermes gateway 与当前 profile。</div>';
} else {
var gatewayInfo = gatewayHealth.gateway || {};
var profileInfo = gatewayHealth.profile || {};
var suggestionText = pageAiNormalizeArray(gatewayHealth.suggestions).join('');
gatewayDetail.innerHTML = '' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(profileInfo.name || activeProfile) + '</div>' +
'<div class="wolai-page-ai-tool-meta">model.default: ' + escapeHtml(profileInfo.modelDefault || '未设置') + '</div>' +
'<div class="wolai-page-ai-tool-meta">provider: ' + escapeHtml(profileInfo.provider || '未设置') + ' · API key: ' + escapeHtml(profileInfo.apiKeyConfigured ? '已配置' : '未检测到') + '</div>' +
'</div>' +
'<div class="wolai-page-ai-tool-row">' +
'<div class="wolai-page-ai-tool-name">' + escapeHtml(gatewayInfo.upstream || '未配置 upstream') + '</div>' +
'<div class="wolai-page-ai-tool-meta">gateway: ' + escapeHtml(gatewayInfo.status || 'unknown') + (gatewayInfo.httpStatus ? ' · HTTP ' + escapeHtml(gatewayInfo.httpStatus) : '') + '</div>' +
(suggestionText ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml(suggestionText) + '</div>' : '') +
'</div>';
}
}
var toolError = drawer.querySelector('[data-page-ai-tool-error]');
if (toolError instanceof HTMLElement) {
toolError.textContent = pageUiState.pageAiToolsError || '';
toolError.hidden = !pageUiState.pageAiToolsError;
}
var lastTool = drawer.querySelector('[data-page-ai-last-tool]');
if (lastTool instanceof HTMLElement) {
var call = pageUiState.pageAiLastToolCall;
lastTool.textContent = call
? [call.event, call.name, call.runId, call.traceId || call.auditId].filter(Boolean).join(' · ')
: '暂无 tool call';
}
}
function humanizePageAiResponse(rawText, promptText) {
var providerLabel = pageAiProviderLabel(pageUiState.pageAiProvider);
var text = String(rawText || '').trim();
if (!text) {
return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”,但这次没有返回可读内容。';
}
if (text.startsWith('{')) {
try {
var payload = JSON.parse(text);
var operation = payload && payload.operation ? payload.operation : {};
var normalized = operation && operation.normalized_input ? operation.normalized_input : {};
var args = normalized && normalized.args ? normalized.args : {};
var documentId = searchText(args.documentId || args.pageId || currentDocumentId());
var toolName = searchText(operation.tool_name || normalized.toolName || 'doc_get');
return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”。当前已通过 Hermes 调用 mnote plugin 工具,目标页面是 ' + (documentId || '当前页面') + ',本次选择的工具是 ' + toolName + '。如果你继续追问,我会沿当前页面上下文继续回复。';
} catch (_) {
return providerLabel + ' 已返回结果,但当前结果是结构化文本,已为你保留原始内容。';
}
}
return text;
}
function ensurePageAiDrawer() {
var existing = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (existing instanceof HTMLElement) return existing;
var drawer = document.createElement('aside');
drawer.className = 'wolai-page-ai-drawer';
drawer.setAttribute('data-testid', 'wolai-page-ai-drawer');
drawer.setAttribute('data-mnote-surface', 'page-ai');
drawer.hidden = true;
drawer.innerHTML = '' +
'<div class="wolai-page-ai-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-ai-header">' +
'<div class="wolai-page-ai-header-copy">' +
'<h2 class="wolai-page-ai-title" data-title="page-ai-title">页面 AI</h2>' +
'<div class="wolai-page-ai-subtitle">' +
'<span>Hermes</span>' +
'<span data-page-ai-profile-summary>default</span>' +
'<span data-page-ai-model-status>由 Hermes 决定</span>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-header-actions">' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-tab="agent" aria-label="打开页面 AI 设置">⚙</button>' +
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-body">' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="chat">' +
'<div class="wolai-page-ai-chat-meta">' +
'<button type="button" class="wolai-page-ai-session-button" data-page-ai-action="history">' +
'<span data-page-ai-session-status>等待 Hermes session</span>' +
'</button>' +
'<span data-page-ai-context-scope-label>当前页</span>' +
'<span data-page-ai-run-status>空闲</span>' +
'</div>' +
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
'<div class="wolai-page-ai-suggestions">' +
'<div class="wolai-page-ai-suggestions-header">' +
'<span>推荐问题</span>' +
'<div class="wolai-page-ai-intents">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-summary">创建 Summary</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-ai-note">创建 AI Note</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate">换一换</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-list></div>' +
'</div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="agent" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="agent" role="tab" aria-selected="true">Agent</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="skills" role="tab" aria-selected="false">Skills</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">Runtime</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-settings-grid">' +
'<label class="wolai-page-ai-profile-select">' +
'<span>ACP</span>' +
'<select data-page-ai-acp-runtime></select>' +
'</label>' +
'<label class="wolai-page-ai-profile-select" data-page-ai-hermes-profile>' +
'<span>agent / profile</span>' +
'<select data-page-ai-profile-select></select>' +
'</label>' +
'<label class="wolai-page-ai-context-select">' +
'<span>上下文</span>' +
'<select data-page-ai-context-scope>' +
'<option value="page">当前页</option>' +
'<option value="selection">当前选区</option>' +
'<option value="block">当前块</option>' +
'<option value="options">页面设置</option>' +
'</select>' +
'</label>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-profile-error hidden></div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-memory-error hidden></div>' +
'<div class="wolai-page-ai-memory-grid" data-page-ai-agent-panel></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="skills" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Agent</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="skills" role="tab" aria-selected="true">Skills</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">Runtime</button>' +
'</div>' +
'</div>' +
'<div class="wolai-page-ai-skills-toolbar">' +
'<label class="wolai-page-ai-skill-search">' +
'<span>搜索技能</span>' +
'<input type="search" data-page-ai-skill-search placeholder="搜索技能…" />' +
'</label>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-skill-error hidden></div>' +
'<div class="wolai-page-ai-skill-list" data-page-ai-skill-list></div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="runtime" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Agent</button>' +
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="skills" role="tab" aria-selected="false">Skills</button>' +
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="runtime" role="tab" aria-selected="true">Runtime</button>' +
'</div>' +
'</div>' +
'<button type="button" class="wolai-page-ai-settings-link" data-page-ai-action="open-hermes-settings">打开 Hermes 设置</button>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-gateway-error hidden></div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>gateway</span>' +
'<span data-page-ai-gateway-status>未检查</span>' +
'</div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-gateway-detail></div>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>queue</span>' +
'<span data-page-ai-queue-status>由 mnote-web 管理</span>' +
'</div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-queue-list></div>' +
'</div>' +
'<div class="wolai-page-ai-tools-panel">' +
'<div class="wolai-page-ai-tools-head">' +
'<span>mnote tools</span>' +
'<span data-page-ai-last-tool>暂无 tool call</span>' +
'</div>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-tool-error hidden></div>' +
'<div class="wolai-page-ai-tool-list" data-page-ai-tool-list></div>' +
'</div>' +
'</section>' +
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="history" hidden>' +
'<div class="wolai-page-ai-settings-head">' +
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
'</div>' +
'<label class="wolai-page-ai-skill-search">' +
'<span>搜索会话</span>' +
'<input type="search" data-page-ai-session-search placeholder="搜索后端 AI 会话…" />' +
'</label>' +
'<div class="wolai-page-ai-inline-error" data-page-ai-session-error hidden></div>' +
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
'</section>' +
'</div>' +
'<div class="wolai-page-ai-footer">' +
'<div class="wolai-page-ai-composer">' +
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="问 Hermes…"></textarea>' +
'<div class="wolai-page-ai-composer-bar">' +
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="new-session" title="新会话"></button>' +
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="history" title="历史会话">⌕</button>' +
'<span class="wolai-page-ai-composer-spacer"></span>' +
'<button type="button" class="wolai-page-ai-stop" data-page-ai-action="stop-run" disabled>停止</button>' +
'<button type="button" class="wolai-page-ai-send" data-page-ai-action="send" aria-label="发送">发送</button>' +
'</div>' +
'</div>' +
'</div>' +
'</div>';
document.body.appendChild(drawer);
return drawer;
}
function renderPageAiSuggestions() {
var drawer = ensurePageAiDrawer();
var list = drawer.querySelector('[data-page-ai-panel="chat"] [data-page-ai-suggestion-list]');
if (!(list instanceof HTMLElement)) return;
var container = list.closest('.wolai-page-ai-suggestions');
if (container instanceof HTMLElement) {
container.hidden = pageUiState.pageAiPage !== 'chat' || pageUiState.pageAiMessages.length > 0;
}
var suggestions = pageAiSuggestions();
var offset = pageUiState.pageAiSuggestionIndex % suggestions.length;
var ordered = suggestions.slice(offset).concat(suggestions.slice(0, offset)).slice(0, 3);
list.innerHTML = ordered.map(function(text) {
return '<button type="button" class="wolai-page-ai-suggestion" data-page-ai-suggestion="' + escapeHtml(text) + '">' + escapeHtml(text) + '</button>';
}).join('');
}
function renderPageAiConversation() {
var drawer = ensurePageAiDrawer();
renderPageAiSuggestions();
var conversation = drawer.querySelector('[data-page-ai-panel="' + cssEscape(pageUiState.pageAiPage || 'chat') + '"] [data-page-ai-conversation]');
if (!(conversation instanceof HTMLElement)) return;
if (pageUiState.pageAiPage === 'history') {
var historyRows = pageUiState.pageAiSessionSearchResults.length
? pageUiState.pageAiSessionSearchResults
: pageUiState.pageAiSessions;
if (!historyRows.length) {
conversation.innerHTML = '<div class="wolai-page-ai-empty">尚未产生历史会话。</div>';
return;
}
conversation.innerHTML = historyRows.map(function(session) {
var preview = Array.isArray(session.messages) && session.messages.length
? session.messages.slice(-1)[0].content
: (session.snippet || session.preview || '暂无消息');
var active = session.id === pageUiState.pageAiActiveSessionId;
var usage = pageAiUsageSummary(session.usage);
var meta = [pageAiSessionStorageLabel(session), session.permissionLevel, session.shareId, session.status, usage].filter(Boolean).join(' · ');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + '" data-page-ai-session-row="' + escapeHtml(session.id) + '">' +
'<button type="button" class="wolai-page-ai-message-text" data-page-ai-session="' + escapeHtml(session.id) + '">' +
'<strong>' + escapeHtml(session.title || '新会话') + '</strong><br />' +
'<span>' + escapeHtml(preview) + '</span>' +
(meta ? '<br /><span class="wolai-page-ai-tool-meta">' + escapeHtml(meta) + '</span>' : '') +
'</button>' +
'<div class="wolai-page-ai-message-actions">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-resume="' + escapeHtml(session.id) + '">恢复</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-rename="' + escapeHtml(session.id) + '">重命名</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-delete="' + escapeHtml(session.id) + '">删除</button>' +
'</div>' +
'</div>';
}).join('');
return;
}
if (!pageUiState.pageAiMessages.length) {
conversation.innerHTML = '<div class="wolai-page-ai-empty">围绕当前页面提问,我会优先使用当前页内容、页面结构和已保存设置。</div>';
return;
}
conversation.innerHTML = pageUiState.pageAiMessages.map(function(item) {
var roleLabel = item.role === 'user' ? '你' : (item.role === 'tool' ? '工具' : 'AI');
if (item.role === 'tool') {
var statusLabel = item.status === 'completed' ? '完成' : (item.status === 'failed' ? '失败' : '运行中');
var detailRows = [
item.argsSummary ? '<div class="wolai-page-ai-tool-meta">参数 ' + escapeHtml(item.argsSummary) + '</div>' : '',
item.resultSummary ? '<div class="wolai-page-ai-tool-meta">结果 ' + escapeHtml(item.resultSummary) + '</div>' : '',
item.changedFiles && item.changedFiles.length ? '<pre class="wolai-page-ai-tool-meta wolai-page-ai-changed-files">' + escapeHtml(pageAiFormatChangedFiles(item.changedFiles)) + '</pre>' : '',
(item.traceId || item.auditId) ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml([item.traceId, item.auditId].filter(Boolean).join(' · ')) + '</div>' : ''
].filter(Boolean).join('');
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-tool-card data-page-ai-tool-call-id="' + escapeHtml(item.toolCallId || '') + '" data-page-ai-tool-status="' + escapeHtml(item.status || '') + '">' +
'<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
'<div class="wolai-page-ai-message-text">' +
'<details class="wolai-page-ai-tool-details">' +
'<summary>' +
'<strong>' + escapeHtml(item.toolName || item.content || 'tool') + '</strong>' +
'<span>' + escapeHtml([statusLabel, item.toolKind, item.toolCallId].filter(Boolean).join(' · ')) + '</span>' +
'</summary>' +
(detailRows || '<div class="wolai-page-ai-tool-meta">暂无参数或结果详情。</div>') +
'</details>' +
'</div>' +
'</div>';
}
if (item.kind === 'thought') {
return '' +
'<details class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-thought-delta="true">' +
'<summary class="wolai-page-ai-message-role" style="cursor:pointer;user-select:none;opacity:0.7;font-size:0.85em">思考过程</summary>' +
'<div class="wolai-page-ai-message-text" style="margin-top:4px;opacity:0.6;font-size:0.9em;border-left:2px solid var(--wolai-border-color, #ddd);padding-left:8px">' + escapeHtml(item.content || '') + '</div>' +
'</details>';
}
if (item.kind === 'permission') {
var permissionActions = item.resolved ? '' : (
'<div class="wolai-page-ai-message-actions">' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">允许</button>' +
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">拒绝</button>' +
'</div>'
);
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-permission-request="' + escapeHtml(item.permissionId || '') + '">' +
'<div class="wolai-page-ai-message-role">权限</div>' +
'<div class="wolai-page-ai-message-text">' +
'<strong>' + escapeHtml(item.toolName || 'session/request_permission') + '</strong><br />' +
'<span>' + escapeHtml(item.argsSummary || item.content || '') + '</span>' +
permissionActions +
'</div>' +
'</div>';
}
var streamingAttr = item.streaming ? ' data-page-ai-streaming="true"' : '';
return '' +
'<div class="wolai-page-ai-message wolai-page-ai-message--' + escapeHtml(item.role || 'assistant') + '"' + streamingAttr + '>' +
'<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
'<div class="wolai-page-ai-message-text">' + escapeHtml(item.content || '') + '</div>' +
'</div>';
}).join('');
conversation.scrollTop = conversation.scrollHeight;
}
function openPageAiDrawer() {
pageAiLoadSessions();
renderPageAiSuggestions();
renderPageAiConversation();
renderPageAiProviderButtons();
renderPageAiControls();
var drawer = ensurePageAiDrawer();
drawer.hidden = false;
pageUiState.pageAiOpen = true;
updatePageAiTriggerState();
Promise.all([
pageAiLoadProfiles(),
pageAiLoadProfileMemory(),
pageAiLoadSkills(),
pageAiLoadTools(),
pageAiLoadGatewayHealth(),
pageAiLoadBackendSessions()
]).then(function() {
renderPageAiControls();
}).catch(function() {}).then(function() {
return pageAiEnsureHermesSession();
}).then(function() {
return pageAiRestoreHermesSession();
}).then(function() {
renderPageAiControls();
}).catch(function(error) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: 'Hermes 当前不可用:' + (error instanceof Error ? error.message : String(error))
});
renderPageAiConversation();
renderPageAiControls();
});
}
function closePageAiDrawer() {
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (drawer instanceof HTMLElement) drawer.hidden = true;
pageUiState.pageAiOpen = false;
updatePageAiTriggerState();
}
function isPageAiDrawerOpen() {
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
return drawer instanceof HTMLElement && !drawer.hidden;
}
async function streamPageAiResponse(response, onEvent) {
if (!response.body || typeof response.body.getReader !== 'function') return;
var reader = response.body.getReader();
var decoder = new TextDecoder();
var buffer = '';
while (true) {
var chunk = await reader.read();
if (chunk.done) break;
buffer += decoder.decode(chunk.value, { stream: true });
var frames = buffer.split('\n\n');
buffer = frames.pop() || '';
frames.forEach(function(frame) {
var eventName = '';
var dataLines = [];
frame.split('\n').forEach(function(line) {
if (line.startsWith('event:')) eventName = line.slice(6).trim();
if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
});
var payloadText = dataLines.join('\n');
if (!eventName && payloadText) {
try {
var parsed = JSON.parse(payloadText);
eventName = parsed && parsed.event ? String(parsed.event) : '';
} catch (_) {}
}
if (eventName) onEvent(eventName, payloadText);
});
}
}
function pageAiDecodeDeltaText(payloadText) {
try {
var payload = JSON.parse(payloadText || 'null');
return String((payload && (payload.text || payload.delta)) || '');
} catch (_) {
return String(payloadText || '');
}
}
function pageAiEnsureStreamingAssistantMessage(runId) {
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
var existing = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.streaming === true && item.runId === id;
});
if (existing) return existing;
existing = {
role: 'assistant',
content: '',
runId: id,
streaming: true
};
pageUiState.pageAiMessages.push(existing);
return existing;
}
function pageAiAppendStreamingAssistantDelta(runId, deltaText) {
var delta = String(deltaText || '');
if (!delta) return '';
var message = pageAiEnsureStreamingAssistantMessage(runId);
message.content = String(message.content || '') + delta;
pageAiSyncCurrentSessionMessages();
renderPageAiConversation();
return message.content;
}
function pageAiFinishStreamingAssistantMessage(runId, finalText, promptText) {
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
var message = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.streaming === true && item.runId === id;
});
var text = String(finalText || (message && message.content) || '');
var content = humanizePageAiResponse(text, promptText);
if (message) {
message.content = content;
message.streaming = false;
} else if (content) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: content,
runId: id
});
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
}
function pageAiLooksLikeBlockEdit(prompt) {
var text = searchText(prompt);
return ['新增', '添加', '插入', '删除', '删掉', '修改', '替换', '改成', '移动', '移到', 'move', 'replace', 'delete', 'insert'].some(function(word) {
return text.indexOf(word) >= 0;
});
}
async function pageAiTryBlockEditWorkflow(prompt, scopedContext) {
if (currentSourceKind() === 'local_folder') return false;
if (!pageAiLooksLikeBlockEdit(prompt)) return false;
var runId = 'page-ai-fast-' + Date.now().toString(36);
var traceId = 'page-ai-fast-' + Date.now().toString(36);
pageAiSetRunStatus('running', runId);
renderPageAiControls();
var started = Date.now();
var response = await fetch('/api/page-ai/block-edit-workflow', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
sessionId: pageUiState.pageAiActiveSessionId,
runId: runId,
profile: pageAiCurrentProfile(),
model: pageAiMnoteToolModel(),
message: prompt,
pageContext: scopedContext.pageContext,
selectedBlockId: scopedContext.selectedBlockId,
selectedText: scopedContext.selectedText,
traceId: traceId
})
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload || payload.ok !== true) {
var code = payload && payload.code ? String(payload.code) : '';
if (code === 'page_ai_workflow_not_block_edit') return false;
pageUiState.pageAiMessages.push({
role: 'tool',
toolName: 'mnote.page_ai.block_edit_workflow',
status: 'failed',
toolCallId: runId,
resultSummary: pageAiErrorMessage(payload, 'fast_path_failed_' + response.status)
});
renderPageAiConversation();
pageAiSetRunStatus('failed', runId);
renderPageAiControls();
return true;
}
pageUiState.pageAiMessages.push({
role: 'tool',
toolName: 'mnote.page_ai.block_edit_workflow',
status: 'completed',
toolCallId: runId,
resultSummary: 'operations=' + String(Array.isArray(payload.operations) ? payload.operations.length : 0) + ' · ' + String(Date.now() - started) + 'ms'
});
pageUiState.pageAiMessages.push({
role: 'assistant',
content: payload.message || '已通过页面块编辑快路径完成写入。'
});
pageAiSetRunStatus('completed', runId);
try {
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
detail: {
toolName: 'mnote.doc.apply_block_ops',
normalizedToolName: 'mnote.doc.apply.block.ops',
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
runId: runId,
traceId: traceId,
toolCallId: runId
}
}));
} catch (_) {}
var currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
return true;
}
async function sendPageAiMessage(text) {
var allowQueue = ['running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0;
if (pageUiState.pageAiBusy && !allowQueue) return;
var prompt = searchText(text);
if (!prompt) return;
if (!allowQueue) pageUiState.pageAiBusy = true;
var currentSession = null;
try {
await pageAiEnsureHermesSession();
if (!allowQueue) pageAiSetRunStatus('queued');
renderPageAiControls();
var contextSnapshot = currentPageAiContextSnapshot();
var scopedContext = pageAiScopedPageContext(contextSnapshot);
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
currentSession = pageAiCurrentSession();
if (currentSession) {
if (currentSession.title === '新会话') {
currentSession.title = prompt.length > 18 ? prompt.slice(0, 18) + '…' : prompt;
}
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
if (!allowQueue && await pageAiTryBlockEditWorkflow(prompt, scopedContext)) {
return;
}
var response = await fetch('/api/hermes/client/runs', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body),
documentId: currentDocumentId(),
sourceKind: currentSourceKind(),
rootUri: currentRootUri(),
sessionId: pageUiState.pageAiActiveSessionId,
profile: pageAiRunProfile(),
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
contextScope: pageUiState.pageAiContextScope,
message: prompt,
model: pageAiMnoteToolModel(),
pageContext: scopedContext.pageContext,
selectedBlockId: scopedContext.selectedBlockId,
selectedText: scopedContext.selectedText,
traceId: 'page-ai-run-' + Date.now().toString(36)
})
});
if (!response.ok) {
var errorPayload = await response.json().catch(function(){ return null; });
throw new Error(pageAiErrorMessage(errorPayload, 'page_ai_failed_' + response.status));
}
var runPayload = await response.json().catch(function(){ return null; });
if (pageAiApplyQueuedRun(runPayload)) {
pageUiState.pageAiMessages.push({
role: 'assistant',
content: '已加入 Hermes 队列,前一条 run 完成后继续处理。'
});
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
return;
}
if (allowQueue) {
throw new Error('hermes_queue_expected_queued_response');
}
var upstream = runPayload && runPayload.upstream ? runPayload.upstream : runPayload;
var runId = upstream && (upstream.run_id || upstream.runId);
if (!runId) throw new Error('hermes_run_missing_run_id');
var runTraceId = String((upstream && (upstream.trace_id || upstream.traceId)) || (runPayload && (runPayload.trace_id || runPayload.traceId)) || '');
pageAiSetRunStatus('running', runId);
renderPageAiControls();
var eventResponse = await fetch('/api/hermes/client/events/' + encodeURIComponent(runId), {
headers: { 'accept': 'text/event-stream' }
});
if (!eventResponse.ok) {
var eventError = await eventResponse.json().catch(function(){ return null; });
throw new Error(pageAiErrorMessage(eventError, 'hermes_events_failed_' + eventResponse.status));
}
var assistantText = '';
await streamPageAiResponse(eventResponse, function(eventName, payloadText) {
if (eventName === 'message.delta') {
if (pageUiState.pageAiStoppedRunIds[runId]) return;
assistantText = pageAiAppendStreamingAssistantDelta(runId, pageAiDecodeDeltaText(payloadText));
}
if (eventName === 'thought.delta') {
try {
var thoughtPayload = JSON.parse(payloadText || 'null');
var thoughtText = String((thoughtPayload && (thoughtPayload.delta || thoughtPayload.text)) || '');
if (thoughtText) {
var msgs = pageUiState.pageAiMessages;
var lastThought = msgs.length > 0 && msgs[msgs.length - 1].kind === 'thought' ? msgs[msgs.length - 1] : null;
if (lastThought) {
lastThought.content += thoughtText;
} else {
msgs.push({ role: 'assistant', kind: 'thought', content: thoughtText });
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
}
} catch (_) {}
}
if (eventName === 'usage.updated') {
try {
var usagePayload = JSON.parse(payloadText || 'null') || {};
var sessionForUsage = pageAiCurrentSession();
if (sessionForUsage) {
sessionForUsage.usage = {
source: 'usage_update',
used: Number(usagePayload.used ?? usagePayload.contextUsed ?? 0),
size: Number(usagePayload.size ?? usagePayload.contextSize ?? 0)
};
pageAiPersistSessions();
}
} catch (_) {}
renderPageAiControls();
}
if (eventName === 'permission.requested' || eventName === 'permission.denied' || eventName === 'permission.allowed') {
pageAiApplyPermissionEvent(eventName, payloadText);
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
renderPageAiControls();
}
if (eventName === 'run.completed') {
try {
var completed = JSON.parse(payloadText || 'null');
if (completed && completed.output) assistantText = String(completed.output || '');
if (completed && completed.usage) {
var completedSession = pageAiCurrentSession();
if (completedSession) completedSession.usage = completed.usage;
}
var agentAudit = completed && completed.agentAudit && typeof completed.agentAudit === 'object' ? completed.agentAudit : null;
var changedFiles = pageAiNormalizeArray(agentAudit && agentAudit.changedFiles);
if (changedFiles.length) {
pageUiState.pageAiMessages.push({
role: 'tool',
content: 'agent.changed_files',
toolCallId: runId + ':agent.changed_files',
toolName: 'agent.changed_files',
toolKind: 'audit',
status: 'completed',
argsSummary: String(agentAudit.rootUri || ''),
resultSummary: String(agentAudit.diffSummary || changedFiles.length + ' changed file(s)'),
changedFiles: changedFiles,
traceId: runTraceId,
auditId: String(agentAudit.eventId || '')
});
try {
var currentId = currentDocumentId();
var currentPath = String(currentId || '').replace(/^local-md:/, '').replace(/~2F/g, '/');
var touchesCurrent = changedFiles.some(function(file) {
var path = String(file && (file.documentId || file.path || file.filePath || '') || '');
return path === currentId || (currentPath && path.indexOf(currentPath) >= 0);
});
if (touchesCurrent) {
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
detail: {
toolName: 'agent.changed_files',
normalizedToolName: 'agent.changed_files',
documentId: currentId,
workspaceId: resolveWorkspaceId(document.body),
runId: runId,
traceId: runTraceId,
toolCallId: runId + ':agent.changed_files'
}
}));
}
} catch (_) {}
}
} catch (_) {}
pageAiSetRunStatus('completed', runId);
}
if (eventName === 'run.failed') {
try {
var failed = JSON.parse(payloadText || 'null');
assistantText = String((failed && (failed.message || failed.code || failed.error)) || 'Hermes run failed');
} catch (_) {}
pageAiSetRunStatus('failed', runId);
}
if (eventName === 'run.aborted' || eventName === 'run.cancelled' || eventName === 'run.canceled') {
pageUiState.pageAiStoppedRunIds[runId] = true;
pageAiSetRunStatus('aborted', runId);
}
if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId);
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
pageAiSetRunStatus('tool_calling', runId);
renderPageAiControls();
}
});
if (!pageUiState.pageAiStoppedRunIds[runId]) {
pageAiFinishStreamingAssistantMessage(runId, assistantText, prompt);
}
currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} catch (error) {
pageAiSetRunStatus('failed');
pageUiState.pageAiMessages.push({
role: 'assistant',
content: 'Hermes 当前请求失败:' + (error instanceof Error ? error.message : String(error))
});
currentSession = pageAiCurrentSession();
if (currentSession) {
currentSession.messages = pageUiState.pageAiMessages.slice();
currentSession.updatedAt = Date.now();
}
pageAiPersistSessions();
} finally {
if (!allowQueue) pageUiState.pageAiBusy = false;
renderPageAiConversation();
renderPageAiControls();
}
}
function ensurePageSettingsPopover() {
var existing = document.querySelector('[data-testid="wolai-page-settings-popover"]');
if (existing instanceof HTMLElement) return existing;
var popover = document.createElement('div');
popover.className = 'wolai-page-settings-popover';
popover.setAttribute('data-testid', 'wolai-page-settings-popover');
popover.setAttribute('data-mnote-surface', 'page-settings');
popover.hidden = true;
popover.innerHTML = '' +
'<div class="wolai-page-settings-panel" role="dialog" aria-modal="false">' +
'<div class="wolai-page-settings-tabs" data-testid="wolai-page-settings-tabs" role="tablist" aria-label="页面设置分组">' +
'<button type="button" class="wolai-page-settings-tab is-active" role="tab" aria-selected="true" data-page-settings-tab="page">页面选项</button>' +
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="custom">自定义页面</button>' +
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="index">索引</button>' +
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="global">全局选项</button>' +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="page">' +
createPageOptionRow('wideLayout', 'checkbox') +
createPageOptionRow('smallText', 'checkbox') +
createPageOptionRow('showToc', 'checkbox') +
createPageOptionRow('protectEditing', 'checkbox') +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="custom" hidden>' +
createPageFontRow() +
createPageOptionRow('layoutDensity', 'select') +
createPageOptionRow('collapseBacklinks', 'checkbox') +
createPageOptionRow('hideChildPages', 'checkbox') +
createPageOptionRow('showBlockRefCount', 'checkbox') +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="index" hidden>' +
'<div class="wolai-page-settings-index-panel" data-testid="wolai-page-settings-local-index-panel">' +
'<div class="wolai-page-settings-index-status" data-testid="wolai-page-settings-local-index-status"></div>' +
'<section class="wolai-page-settings-index-group">' +
'<div class="wolai-page-settings-index-title">反链</div>' +
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-backlinks"></div>' +
'</section>' +
'<section class="wolai-page-settings-index-group">' +
'<div class="wolai-page-settings-index-title">标签</div>' +
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-tags"></div>' +
'</section>' +
'</div>' +
'</div>' +
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
createGlobalHeadingNumbersRow() +
'</div>' +
'<div class="wolai-page-settings-actions">' +
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="history">页面历史...</button>' +
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="share">公开分享页面...</button>' +
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="move" disabled>移动到...</button>' +
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="embed" disabled>嵌入到...</button>' +
'<button type="button" class="wolai-page-settings-action is-danger is-disabled" data-page-settings-action="delete" disabled>删除页面</button>' +
'</div>' +
'<div class="wolai-page-settings-stats" data-testid="wolai-page-settings-stats"></div>' +
'</div>';
document.body.appendChild(popover);
return popover;
}
function pageSettingsLocalIndexScopeKey() {
return [
currentSourceKind(),
currentRootUri(),
resolveWorkspaceId(document.body),
currentDocumentId()
].join('|');
}
function pageSettingsLocalIndexIsAvailable() {
return currentSourceKind() === 'local_folder' && Boolean(currentRootUri()) && Boolean(currentDocumentId());
}
function pageSettingsLocalIndexEmpty(message) {
return '<div class="wolai-page-settings-index-empty">' + escapeHtml(message) + '</div>';
}
function renderPageSettingsLocalIndexList(items, kind) {
var rows = Array.isArray(items) ? items : [];
if (!rows.length) {
return pageSettingsLocalIndexEmpty(kind === 'backlinks' ? '暂无反链' : '暂无标签');
}
if (kind === 'backlinks') {
return rows.slice(0, 12).map(function(item) {
var title = searchText(item && item.title) || searchText(item && item.path) || '未命名页面';
var path = searchText(item && item.path);
var snippet = searchText(item && item.snippet);
return '' +
'<div class="wolai-page-settings-index-row">' +
'<div class="wolai-page-settings-index-row-title">' + escapeHtml(title) + '</div>' +
(path ? '<div class="wolai-page-settings-index-row-meta">' + escapeHtml(path) + '</div>' : '') +
(snippet ? '<div class="wolai-page-settings-index-row-snippet">' + escapeHtml(snippet) + '</div>' : '') +
'</div>';
}).join('');
}
return rows.slice(0, 16).map(function(item) {
var tag = searchText(item && item.tag) || 'untagged';
var count = Number(item && item.count || 0);
return '' +
'<div class="wolai-page-settings-index-row is-tag">' +
'<div class="wolai-page-settings-index-row-title">#' + escapeHtml(tag) + '</div>' +
'<div class="wolai-page-settings-index-row-meta">' + count + ' 个页面</div>' +
'</div>';
}).join('');
}
function renderPageSettingsLocalIndex(popover) {
popover = popover || ensurePageSettingsPopover();
var statusNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-status"]');
var backlinksNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]');
var tagsNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-tags"]');
if (!(statusNode instanceof HTMLElement) || !(backlinksNode instanceof HTMLElement) || !(tagsNode instanceof HTMLElement)) return;
if (!pageSettingsLocalIndexIsAvailable()) {
statusNode.textContent = '本地索引仅在本地工作区页面可用';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
return;
}
var summary = pageUiState.localIndexSummary || {};
if (summary.loading) {
statusNode.textContent = '正在读取本地索引...';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
return;
}
if (summary.error) {
statusNode.textContent = '本地索引读取失败';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
tagsNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
return;
}
if (summary.scopeKey !== pageSettingsLocalIndexScopeKey()) {
statusNode.textContent = '切换到索引页签后读取本地索引';
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
return;
}
statusNode.textContent = '来自当前授权 root 的 .mnote/index/search-index.json';
backlinksNode.innerHTML = renderPageSettingsLocalIndexList(summary.backlinks, 'backlinks');
tagsNode.innerHTML = renderPageSettingsLocalIndexList(summary.tags, 'tags');
}
async function loadPageSettingsLocalIndex(force) {
if (!pageSettingsLocalIndexIsAvailable()) {
renderPageSettingsLocalIndex();
return;
}
var scopeKey = pageSettingsLocalIndexScopeKey();
var current = pageUiState.localIndexSummary || {};
if (!force && current.scopeKey === scopeKey && !current.error && !current.loading) {
renderPageSettingsLocalIndex();
return;
}
pageUiState.localIndexSummary = {
scopeKey: scopeKey,
loading: true,
error: '',
backlinks: null,
tags: null
};
renderPageSettingsLocalIndex();
try {
var baseParams = new URLSearchParams();
baseParams.set('workspaceId', resolveWorkspaceId(document.body));
baseParams.set('rootUri', currentRootUri());
var backlinksParams = new URLSearchParams(baseParams);
backlinksParams.set('documentId', currentDocumentId());
var backlinksUrl = '/api/search/local-index/backlinks?' + backlinksParams.toString();
var tagsUrl = '/api/search/local-index/tags?' + baseParams.toString();
var responses = await Promise.all([
fetch(backlinksUrl, { headers: { accept: 'application/json' } }),
fetch(tagsUrl, { headers: { accept: 'application/json' } })
]);
var backlinksPayload = await responses[0].json().catch(function(){ return null; });
var tagsPayload = await responses[1].json().catch(function(){ return null; });
if (!responses[0].ok || !backlinksPayload || backlinksPayload.ok !== true) {
throw new Error('backlinks_' + responses[0].status);
}
if (!responses[1].ok || !tagsPayload || tagsPayload.ok !== true) {
throw new Error('tags_' + responses[1].status);
}
pageUiState.localIndexSummary = {
scopeKey: scopeKey,
loading: false,
error: '',
backlinks: backlinksPayload.result && Array.isArray(backlinksPayload.result.backlinks) ? backlinksPayload.result.backlinks : [],
tags: tagsPayload.result && Array.isArray(tagsPayload.result.tags) ? tagsPayload.result.tags : []
};
} catch (error) {
pageUiState.localIndexSummary = {
scopeKey: scopeKey,
loading: false,
error: error instanceof Error ? error.message : String(error),
backlinks: [],
tags: []
};
}
renderPageSettingsLocalIndex();
}
function renderPageSettingsPopover() {
var popover = ensurePageSettingsPopover();
var options = currentPageOptions();
popover.querySelectorAll('[data-page-option-checkbox]').forEach(function(input) {
var key = input.getAttribute('data-page-option-checkbox');
input.checked = Boolean(options[key]);
input.disabled = !pageOptionIsSupported(key);
});
popover.querySelectorAll('[data-page-option-select]').forEach(function(select) {
var key = select.getAttribute('data-page-option-select');
var value = key === 'layoutDensity' ? String(options.layoutDensity || 'normal') : String(options.pageFont || 'default');
select.value = value;
});
renderGlobalOptions(popover);
var statsNode = popover.querySelector('[data-testid="wolai-page-settings-stats"]');
if (statsNode instanceof HTMLElement) {
var stats = computeLivePageStats();
statsNode.innerHTML = '' +
'<span>字数 ' + Number(stats.wordCount || 0) + '</span>' +
'<span>字符 ' + Number(stats.characterCount || 0) + '</span>' +
'<span>块数 ' + Number(stats.blockCount || 0) + '</span>' +
'<span>待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + '</span>';
}
renderPageSettingsLocalIndex(popover);
}
function setActivePageSettingsTab(tabName) {
var popover = ensurePageSettingsPopover();
popover.querySelectorAll('[data-page-settings-tab]').forEach(function(tab) {
var active = tab.getAttribute('data-page-settings-tab') === tabName;
tab.classList.toggle('is-active', active);
tab.setAttribute('aria-selected', active ? 'true' : 'false');
});
popover.querySelectorAll('[data-page-settings-panel]').forEach(function(panel) {
panel.hidden = panel.getAttribute('data-page-settings-panel') !== tabName;
});
if (tabName === 'index') void loadPageSettingsLocalIndex(false);
}
async function persistPageOptionsPatch(patch) {
var previous = Object.assign({}, currentPageOptions());
pageUiState.pageOptions = Object.assign({}, previous, patch);
var nextOptions = Object.assign({}, pageUiState.pageOptions);
applyPageOptionsToShell();
renderPageSettingsPopover();
try {
var response = await fetch('/api/documents/options', {
method: 'POST',
headers: { 'content-type': 'application/json' },
body: JSON.stringify({
documentId: currentDocumentId(),
workspaceId: resolveWorkspaceId(document.body),
...currentWorkspaceSourcePayload(),
options: nextOptions,
commandName: 'page.layout.updateOptions'
})
});
var payload = await response.json().catch(function(){ return null; });
if (!response.ok || !payload || payload.ok !== true) {
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'page_settings_save_failed_' + response.status);
}
document.documentElement.setAttribute('data-mnote-page-options-saved', 'true');
} catch (error) {
pageUiState.pageOptions = previous;
applyPageOptionsToShell();
renderPageSettingsPopover();
document.documentElement.setAttribute('data-mnote-page-options-error', error instanceof Error ? error.message : String(error));
}
}
function isPageSettingsOpen() {
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
return popover instanceof HTMLElement && !popover.hidden;
}
function openPageSettingsPopover() {
if (!currentDocumentId()) return;
var popover = ensurePageSettingsPopover();
renderPageSettingsPopover();
setActivePageSettingsTab('page');
popover.hidden = false;
pageUiState.pageSettingsOpen = true;
updatePageSettingsTriggerState();
}
function closePageSettingsPopover() {
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
if (popover instanceof HTMLElement) popover.hidden = true;
pageUiState.pageSettingsOpen = false;
updatePageSettingsTriggerState();
}
function togglePageSettingsPopover() {
if (isPageSettingsOpen()) closePageSettingsPopover();
else openPageSettingsPopover();
}
function attachmentQueryParams(href) {
try {
return new URL(String(href || ''), window.location.origin).searchParams;
} catch (_) {
return new URLSearchParams();
}
}
function isOnlyOfficeAttachmentHref(href) {
try {
var url = new URL(String(href || ''), window.location.origin);
return url.pathname === '/onlyoffice' && (url.searchParams.has('assetId') || url.searchParams.has('fileName'));
} catch (_) {
return false;
}
}
function normalizeOnlyOfficeAttachmentHref(href) {
try {
var url = new URL(String(href || ''), window.location.origin);
if (url.pathname !== '/onlyoffice') return String(href || '');
return buildOnlyOfficeOpenUrl({
fileUrl: url.searchParams.get('fileUrl') || '',
fileName: url.searchParams.get('fileName') || '未命名附件',
fileType: url.searchParams.get('fileType') || inferOnlyOfficeFileType(url.searchParams.get('fileName') || '', ''),
assetId: url.searchParams.get('assetId') || '',
documentId: url.searchParams.get('documentId') || currentDocumentId() || '',
userId: url.searchParams.get('userId') || '',
mode: url.searchParams.get('mode') || 'edit'
});
} catch (_) {
return String(href || '');
}
}
function isOfficeFileName(fileName) {
return Boolean(inferOnlyOfficeFileType(fileName, ''));
}
function detailFromEditorAttachmentLink(link) {
var rawHref = link instanceof HTMLAnchorElement ? link.href : '';
var params = attachmentQueryParams(rawHref);
var fileName = params.get('fileName') || (link ? link.textContent : '') || '未命名附件';
var fileType = params.get('fileType') || inferOnlyOfficeFileType(fileName, '');
var assetId = params.get('assetId') || (link ? link.getAttribute('data-asset-id') : '') || '';
var fileUrl = params.get('fileUrl') || '';
var href = rawHref;
if (!isOnlyOfficeAttachmentHref(rawHref) && fileType) {
fileUrl = rawHref;
href = buildOnlyOfficeOpenUrl({
fileUrl: fileUrl,
fileName: fileName,
fileType: fileType,
assetId: assetId,
documentId: currentDocumentId() || '',
userId: '',
mode: 'edit'
});
} else if (isOnlyOfficeAttachmentHref(rawHref)) {
href = normalizeOnlyOfficeAttachmentHref(rawHref);
}
return {
href: href,
fileUrl: fileUrl,
fileName: fileName,
title: fileName,
fileType: fileType,
assetId: assetId,
documentId: params.get('documentId') || currentDocumentId() || '',
workspaceId: resolveWorkspaceId(document.body),
fileSize: (link ? link.getAttribute('data-file-size') : '') || ''
};
}
function enhanceEditorAttachmentLink(link) {
if (!(link instanceof HTMLAnchorElement)) return;
var href = link.getAttribute('href') || '';
var params = attachmentQueryParams(href);
var fileName = params.get('fileName') || link.textContent || '';
var className = link.getAttribute('class') || '';
var shouldEnhance = isOnlyOfficeAttachmentHref(href)
|| className.indexOf('mnote-uploaded-attachment-row') >= 0
|| isOfficeFileName(fileName);
if (!shouldEnhance) return;
link.setAttribute('data-mnote-attachment-link', 'true');
var assetId = params.get('assetId') || link.getAttribute('data-asset-id') || '';
if (assetId) link.setAttribute('data-asset-id', assetId);
if (isOnlyOfficeAttachmentHref(href)) {
link.setAttribute('href', buildOnlyOfficeOpenPath({
fileUrl: params.get('fileUrl') || '',
fileName: fileName || '未命名附件',
fileType: params.get('fileType') || inferOnlyOfficeFileType(fileName, ''),
assetId: assetId,
documentId: params.get('documentId') || currentDocumentId() || '',
userId: params.get('userId') || '',
mode: params.get('mode') || 'edit'
}));
}
attachmentClassForFileName(fileName).split(/\s+/).forEach(function(name) {
if (name) link.classList.add(name);
});
link.setAttribute('target', '_blank');
link.setAttribute('rel', 'noopener noreferrer nofollow');
void hydrateEditorAttachmentMeta(link);
}
function enhanceEditorAttachmentLinks() {
document.querySelectorAll('.editor-surface .ProseMirror a[href]').forEach(enhanceEditorAttachmentLink);
void healLegacyOfficeAttachmentParagraphs();
}
function ensureAttachmentActions() {
var existing = document.querySelector('[data-testid="mnote-attachment-actions"]');
if (existing instanceof HTMLElement) return existing;
var actions = document.createElement('div');
actions.className = 'mnote-attachment-actions';
actions.setAttribute('data-testid', 'mnote-attachment-actions');
actions.hidden = true;
actions.innerHTML = '' +
'<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-download" data-attachment-action="download" aria-label="下载附件"><span class="material-symbols-outlined" data-icon="download" aria-hidden="true"></span></button>' +
'<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-menu" data-attachment-action="menu" aria-label="更多操作"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>';
actions.addEventListener('mouseenter', function() {
if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer);
});
actions.addEventListener('mouseleave', scheduleHideAttachmentActions);
document.body.appendChild(actions);
return actions;
}
function positionAttachmentActions(link) {
if (!(link instanceof HTMLElement)) return;
var actions = ensureAttachmentActions();
var rect = link.getBoundingClientRect();
actions.hidden = false;
actions.style.left = Math.min(window.innerWidth - 76, Math.max(8, rect.right + 6)) + 'px';
actions.style.top = Math.max(8, rect.top + (rect.height - 28) / 2) + 'px';
activeEditorAttachmentLink = link;
}
function scheduleHideAttachmentActions() {
if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer);
attachmentActionsHideTimer = window.setTimeout(function() {
var actions = document.querySelector('[data-testid="mnote-attachment-actions"]');
if (actions instanceof HTMLElement) actions.hidden = true;
activeEditorAttachmentLink = null;
}, 220);
}
function openEditorAttachmentDetail(detail) {
if (!detail || !detail.href) return;
if (isPdfAttachmentFileName(detail.fileName)) {
void openPdfEditorAttachment(detail);
return;
}
if (isCodeAttachmentFileName(detail.fileName)) {
void openCodeEditorAttachment(detail);
return;
}
window.open(detail.href, '_blank', 'noopener,noreferrer');
}
async function resolveEditorAttachmentUrl(detail) {
var assetId = String(detail && detail.assetId || '').trim();
if (assetId) {
var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(assetId), {
method: 'GET',
credentials: 'include',
cache: 'no-store'
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok || !payload) {
throw new Error(payload && payload.error ? payload.error : '生成签名链接失败');
}
var signedUrl = String(payload && payload.signedUrl || '').trim();
if (!signedUrl) throw new Error('附件链接不可用');
return {
url: signedUrl,
asset: payload.asset && typeof payload.asset === 'object' ? payload.asset : {}
};
}
var url = String(detail && (detail.fileUrl || detail.href) || '').trim();
if (!url) throw new Error('附件链接不可用');
return { url: url, asset: {} };
}
async function openPdfEditorAttachment(detail) {
try {
var resolved = await resolveEditorAttachmentUrl(detail);
window.open(resolved.url, '_blank', 'noopener,noreferrer');
} catch (error) {
window.alert(error && error.message ? error.message : '打开 PDF 失败');
}
}
async function openCodeEditorAttachment(detail) {
var resolved = null;
try {
resolved = await resolveEditorAttachmentUrl(detail);
var size = Number(resolved.asset && (resolved.asset.file_size || resolved.asset.fileSize) || 0);
if (Number.isFinite(size) && size > 1024 * 1024) {
window.open(resolved.url, '_blank', 'noopener,noreferrer');
return;
}
var response = await fetch(resolved.url, {
method: 'GET',
credentials: 'include',
cache: 'no-store'
});
if (!response.ok) throw new Error('读取附件内容失败');
var text = await response.text();
if (text.length > 1024 * 1024) {
window.open(resolved.url, '_blank', 'noopener,noreferrer');
return;
}
var editorRoot = document.querySelector('.editor-surface .ProseMirror');
var editor = editorRoot && editorRoot.editor;
if (!editor || !editor.chain) {
window.open(resolved.url, '_blank', 'noopener,noreferrer');
return;
}
var title = String(detail.fileName || resolved.asset.file_name || '附件').trim() || '附件';
var language = inferCodeAttachmentLanguage(title);
editor.chain().focus().insertContent([
{
type: 'paragraph',
content: [{ type: 'text', text: title }]
},
{
type: 'codeBlock',
attrs: { language: language },
content: text ? [{ type: 'text', text: text.replace(/\r\n?/g, '\n') }] : []
}
]).run();
} catch (error) {
console.warn('[mnote attachment] open code attachment failed', error);
if (resolved && resolved.url) {
window.open(resolved.url, '_blank', 'noopener,noreferrer');
return;
}
window.alert(error && error.message ? error.message : '打开代码附件失败');
}
}
async function openEditorAttachmentDownload(detail) {
if (!detail) return;
if (detail.assetId) {
try {
var response = await fetch('/api/media/sign?assetId=' + encodeURIComponent(detail.assetId), {
method: 'GET',
credentials: 'include'
});
var payload = await response.json().catch(function() { return null; });
var signedUrl = String(payload && payload.signedUrl || '').trim();
if (response.ok && signedUrl) {
window.open(signedUrl, '_blank', 'noopener,noreferrer');
return;
}
} catch (_) {}
}
var target = detail.fileUrl || detail.href;
if (!target) return;
window.open(target, '_blank', 'noopener,noreferrer');
}
function openEditorAttachmentMenu(link, trigger) {
var detail = detailFromEditorAttachmentLink(link);
var rect = trigger && trigger.getBoundingClientRect ? trigger.getBoundingClientRect() : link.getBoundingClientRect();
openTreeContextMenu('attachment', detail, rect.right, rect.bottom + 4, trigger || link);
}
function openEditorAttachmentLink(link) {
enhanceEditorAttachmentLink(link);
openEditorAttachmentDetail(detailFromEditorAttachmentLink(link));
}
enhanceEditorAttachmentLinks();
var attachmentObserver = new MutationObserver(function() { enhanceEditorAttachmentLinks(); });
attachmentObserver.observe(document.documentElement, { childList: true, subtree: true });
document.addEventListener('mouseover', function(event) {
var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
if (!(link instanceof HTMLAnchorElement)) return;
if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer);
enhanceEditorAttachmentLink(link);
positionAttachmentActions(link);
});
document.addEventListener('mouseout', function(event) {
var link = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
if (!(link instanceof HTMLAnchorElement)) return;
var next = event.relatedTarget;
var actions = document.querySelector('[data-testid="mnote-attachment-actions"]');
if (next && (link.contains(next) || (actions && actions.contains(next)))) return;
scheduleHideAttachmentActions();
});
document.addEventListener('click', function(e) {
if (activeTreeContextMenu && activeTreeContextMenu.contains(e.target)) return;
if (activeTreeContextMenu) closeTreeContextMenu();
var sidebarToggle = closestAction(e.target, '[data-mnote-action="toggle-sidebar"]');
if (sidebarToggle) {
e.preventDefault();
toggleWorkspaceSidebar(sidebarToggle);
return;
}
var attachmentAction = closestAction(e.target, '[data-attachment-action]');
if (attachmentAction) {
e.preventDefault();
e.stopPropagation();
var link = activeEditorAttachmentLink;
if (!(link instanceof HTMLAnchorElement)) return;
var attachmentDetail = detailFromEditorAttachmentLink(link);
var attachmentActionName = attachmentAction.getAttribute('data-attachment-action') || '';
if (attachmentActionName === 'download') {
openEditorAttachmentDownload(attachmentDetail);
return;
}
if (attachmentActionName === 'menu') {
openEditorAttachmentMenu(link, attachmentAction);
return;
}
return;
}
var editorAttachmentLink = closestAction(e.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row');
if (editorAttachmentLink instanceof HTMLAnchorElement) {
e.preventDefault();
openEditorAttachmentLink(editorAttachmentLink);
return;
}
var historyClose = closestAction(e.target, '[data-page-history-action="close"]');
if (historyClose) {
e.preventDefault();
closePageHistoryDrawer();
return;
}
var shareClose = closestAction(e.target, '[data-page-share-action="close"]');
if (shareClose) {
e.preventDefault();
closePageShareDialog();
return;
}
var shareCopy = closestAction(e.target, '[data-page-share-action="copy-link"]');
if (shareCopy) {
e.preventDefault();
void copyTreeContextValue(window.location.href, 'page-share-link');
return;
}
var pageHistoryTrigger = closestAction(e.target, '[data-mnote-action="open-page-history"]');
if (pageHistoryTrigger) {
e.preventDefault();
openPageHistoryDrawer();
return;
}
var pageSettingsTrigger = closestAction(e.target, '[data-mnote-action="open-page-settings"]');
if (pageSettingsTrigger) {
e.preventDefault();
togglePageSettingsPopover();
return;
}
var pageSettingsPanel = closestAction(e.target, '.wolai-page-settings-panel');
if (isPageSettingsOpen() && !pageSettingsPanel) {
closePageSettingsPopover();
}
var pageSettingsTab = closestAction(e.target, '[data-page-settings-tab]');
if (pageSettingsTab) {
e.preventDefault();
setActivePageSettingsTab(pageSettingsTab.getAttribute('data-page-settings-tab') || 'page');
return;
}
var pageSettingsAction = closestAction(e.target, '[data-page-settings-action]');
if (pageSettingsAction) {
e.preventDefault();
var actionName = pageSettingsAction.getAttribute('data-page-settings-action') || '';
if (actionName === 'history') {
openPageHistoryDrawer();
return;
}
if (actionName === 'share') {
openPageShareDialog();
return;
}
return;
}
var pageAiTrigger = closestAction(e.target, '[data-mnote-action="open-page-ai"]');
if (pageAiTrigger) {
e.preventDefault();
openPageAiDrawer();
return;
}
var pageAiClose = closestAction(e.target, '[data-page-ai-action="close"]');
if (pageAiClose) {
e.preventDefault();
closePageAiDrawer();
return;
}
var pageAiSettings = closestAction(e.target, '[data-page-ai-action="open-hermes-settings"]');
if (pageAiSettings) {
e.preventDefault();
pageAiOpenHermesSettings();
return;
}
var pageAiStop = closestAction(e.target, '[data-page-ai-action="stop-run"]');
if (pageAiStop) {
e.preventDefault();
void pageAiStopRun();
return;
}
var pageAiRotate = closestAction(e.target, '[data-page-ai-action="rotate"]');
if (pageAiRotate) {
e.preventDefault();
pageUiState.pageAiSuggestionIndex += 1;
renderPageAiSuggestions();
return;
}
var pageAiIntent = closestAction(e.target, '[data-page-ai-intent]');
if (pageAiIntent) {
e.preventDefault();
var intentName = pageAiIntent.getAttribute('data-page-ai-intent') || '';
if (intentName === 'create-summary') {
void sendPageAiMessage('请通过 Hermes 调用 mnote.artifact.create_summary,为当前页面创建或更新 AI Summary。');
return;
}
if (intentName === 'create-ai-note') {
void sendPageAiMessage('请通过 Hermes 调用 mnote.artifact.create_ai_note,基于当前页面创建一篇新的 AI Note。');
return;
}
}
var pageAiTab = closestAction(e.target, '[data-page-ai-tab]');
if (pageAiTab) {
e.preventDefault();
pageUiState.pageAiPage = pageAiTab.getAttribute('data-page-ai-tab') || 'chat';
if (pageUiState.pageAiPage === 'runtime') void pageAiLoadGatewayHealth();
renderPageAiControls();
renderPageAiConversation();
return;
}
var pageAiProvider = closestAction(e.target, '[data-page-ai-provider]');
if (pageAiProvider) {
e.preventDefault();
pageUiState.pageAiProvider = pageAiProvider.getAttribute('data-page-ai-provider') || 'hermes';
renderPageAiProviderButtons();
return;
}
var pageAiMemorySave = closestAction(e.target, '[data-page-ai-memory-save]');
if (pageAiMemorySave) {
e.preventDefault();
void pageAiSaveProfileMemory(pageAiMemorySave.getAttribute('data-page-ai-memory-save') || '');
return;
}
var pageAiSkillToggle = closestAction(e.target, '[data-page-ai-skill-toggle]');
if (pageAiSkillToggle) {
e.preventDefault();
var skillName = pageAiSkillToggle.getAttribute('data-page-ai-skill-toggle') || '';
var nextEnabled = pageAiSkillToggle.getAttribute('aria-pressed') !== 'true';
void pageAiToggleSkill(skillName, nextEnabled);
return;
}
var pageAiToolToggle = closestAction(e.target, '[data-page-ai-tool-toggle]');
if (pageAiToolToggle) {
e.preventDefault();
var toolName = pageAiToolToggle.getAttribute('data-page-ai-tool-toggle') || '';
var nextToolEnabled = pageAiToolToggle.getAttribute('aria-pressed') !== 'true';
void pageAiToggleTool(toolName, nextToolEnabled);
return;
}
var pageAiSessionResume = closestAction(e.target, '[data-page-ai-session-resume]');
if (pageAiSessionResume) {
e.preventDefault();
void pageAiResumeBackendSession(pageAiSessionResume.getAttribute('data-page-ai-session-resume') || '').catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
return;
}
var pageAiSessionRename = closestAction(e.target, '[data-page-ai-session-rename]');
if (pageAiSessionRename) {
e.preventDefault();
void pageAiRenameBackendSession(pageAiSessionRename.getAttribute('data-page-ai-session-rename') || '').catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
return;
}
var pageAiSessionDelete = closestAction(e.target, '[data-page-ai-session-delete]');
if (pageAiSessionDelete) {
e.preventDefault();
void pageAiDeleteBackendSession(pageAiSessionDelete.getAttribute('data-page-ai-session-delete') || '').catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
return;
}
var pageAiPermissionAction = closestAction(e.target, '[data-page-ai-permission-action]');
if (pageAiPermissionAction) {
e.preventDefault();
pageAiResolvePermission(
pageAiPermissionAction.getAttribute('data-page-ai-permission-id') || '',
pageAiPermissionAction.getAttribute('data-page-ai-permission-action') || 'deny'
);
return;
}
var pageAiSession = closestAction(e.target, '[data-page-ai-session]');
if (pageAiSession) {
e.preventDefault();
pageAiSetActiveSession(pageAiSession.getAttribute('data-page-ai-session') || '');
return;
}
var pageAiSuggestion = closestAction(e.target, '[data-page-ai-suggestion]');
if (pageAiSuggestion) {
e.preventDefault();
var text = pageAiSuggestion.getAttribute('data-page-ai-suggestion') || '';
var inputNode = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
if (inputNode instanceof HTMLTextAreaElement) {
inputNode.value = text;
inputNode.focus();
}
return;
}
var pageAiNewSession = closestAction(e.target, '[data-page-ai-action="new-session"]');
if (pageAiNewSession) {
e.preventDefault();
pageUiState.pageAiPage = 'chat';
pageAiStartNewSession();
return;
}
var pageAiHistory = closestAction(e.target, '[data-page-ai-action="history"]');
if (pageAiHistory) {
e.preventDefault();
pageAiLoadSessions();
pageUiState.pageAiPage = pageUiState.pageAiPage === 'history' ? 'chat' : 'history';
renderPageAiControls();
renderPageAiConversation();
if (pageUiState.pageAiPage === 'history') {
void pageAiLoadBackendSessions().catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
}
return;
}
var pageAiSend = closestAction(e.target, '[data-page-ai-action="send"]');
if (pageAiSend) {
e.preventDefault();
var input = ensurePageAiDrawer().querySelector('[data-page-ai-input]');
if (input instanceof HTMLTextAreaElement) {
var message = input.value;
input.value = '';
void sendPageAiMessage(message);
}
return;
}
var cancelQueuedRun = closestAction(e.target, '[data-page-ai-action="cancel-queued-run"]');
if (cancelQueuedRun) {
e.preventDefault();
void pageAiCancelQueuedRun(cancelQueuedRun.getAttribute('data-page-ai-queue-id'));
return;
}
var searchTrigger = closestAction(e.target, '[data-mnote-action="open-search-modal"]');
if (searchTrigger) {
e.preventDefault();
toggleSearchModal();
return;
}
var searchDialog = closestAction(e.target, '.wolai-search-dialog');
if (isSearchModalOpen() && !searchDialog) {
closeSearchModal();
return;
}
var sourceMenuTrigger = closestAction(e.target, '[data-mnote-action="open-workspace-source-menu"]');
if (sourceMenuTrigger) {
e.preventDefault();
var existingSourceMenu = document.querySelector('[data-testid="mnote-workspace-source-menu"]');
if (existingSourceMenu) closeWorkspaceSourceMenu();
else openWorkspaceSourceMenu(sourceMenuTrigger);
return;
}
var sourceMenu = closestAction(e.target, '[data-testid="mnote-workspace-source-menu"]');
if (!sourceMenu) closeWorkspaceSourceMenu();
var trashTrigger = closestAction(e.target, '[data-mnote-action="open-trash-modal"]');
if (trashTrigger) {
e.preventDefault();
openTrashModal(trashTrigger);
return;
}
var trashClose = closestAction(e.target, '[data-mnote-trash-modal-close]');
if (trashClose) {
e.preventDefault();
closeTrashModal();
return;
}
var tabTrigger = closestAction(e.target, '[data-mnote-sidebar-tree-tab]');
if (tabTrigger) {
e.preventDefault();
switchSidebarTreeTab(tabTrigger);
return;
}
var localFolderTrigger = closestAction(e.target, '[data-mnote-action="open-local-folder"]');
if (localFolderTrigger) {
e.preventDefault();
requestOpenLocalFolder();
return;
}
var createLocalWorkspaceTrigger = closestAction(e.target, '[data-mnote-action="create-local-workspace"]');
if (createLocalWorkspaceTrigger) {
e.preventDefault();
void createDefaultLocalWorkspace(createLocalWorkspaceTrigger);
return;
}
var createTrigger = closestAction(e.target, '[data-mnote-action="create-page"]');
if (createTrigger) {
e.preventDefault();
void createPage(createTrigger, null);
return;
}
var fileTree = document.getElementById('sidebar-file-tree-root');
if (fileTree && fileTree.contains(e.target)) {
var fileBtn = closestAction(e.target, '[data-rust-action]');
var fileRow = closestAction(e.target, '.tree-row[data-shell-mode="filetree"]');
if (!fileRow) return;
var fileAction = fileBtn ? fileBtn.getAttribute('data-rust-action') : 'open';
var rowId = fileRow.getAttribute('data-row-id') || '';
var rowKind = fileRow.getAttribute('data-row-kind') || '';
var documentId = fileRow.getAttribute('data-document-id') || fileRow.getAttribute('data-doc-id') || '';
var assetId = fileRow.getAttribute('data-asset-id') || '';
var assetType = '';
var kindBadge = fileRow.querySelector('.tree-kind-badge');
if (kindBadge instanceof HTMLElement) {
assetType = kindBadge.getAttribute('data-kind') || '';
}
if (fileAction === 'toggle') {
e.preventDefault();
toggleChildren(fileRow, fileBtn);
return;
}
if (fileAction === 'create') {
e.preventDefault();
void createPage(fileBtn || fileRow, documentId || fileRow.getAttribute('data-node-id'));
return;
}
if (fileAction === 'menu') {
e.preventDefault();
selectSidebarFileTreeRow(fileRow, { ctrlKey: false, metaKey: false, shiftKey: false });
var point = rowCenter(fileBtn || fileRow);
dispatchSidebarEvent('tree.filetree.context-menu', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null });
openFileTreeContextMenu(fileRow, point.x, point.y, fileBtn || fileRow);
return;
}
if (fileAction === 'open' && rowKind === 'folder') {
e.preventDefault();
selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
toggleChildren(fileRow, fileRow.querySelector('[data-rust-action="toggle"]'));
return;
}
e.preventDefault();
selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
var objectIdentity = readFileTreeObjectIdentity(fileRow);
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId || null, assetType: assetType || null, objectIdentity: objectIdentity, workspaceId: resolveWorkspaceId(fileRow) });
if (e.shiftKey || e.ctrlKey || e.metaKey) {
return;
}
if ((rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') && documentId) {
navigateToDocument(documentId, resolveWorkspaceId(fileRow), { treeView: 'filetree' });
} else if (assetId) {
dispatchSidebarEvent('tree.asset.open', { rowId: rowId, rowKind: rowKind, documentId: documentId || null, assetId: assetId, assetType: assetType || null, objectIdentity: objectIdentity, workspaceId: resolveWorkspaceId(fileRow) });
}
return;
}
var tree = document.getElementById('sidebar-tree-root');
if (!tree || !tree.contains(e.target)) return;
var btn = closestAction(e.target, '[data-rust-action]');
if (!btn) return;
var nodeId = btn.getAttribute('data-node-id');
var action = btn.getAttribute('data-rust-action');
if (action === 'toggle') {
var row = btn.closest('.tree-row');
if (!row) return;
toggleChildren(row, btn);
e.preventDefault();
} else if (action === 'open') {
if (btn.getAttribute('data-page-openable') === 'false') {
e.preventDefault();
return;
}
var workspaceId = resolveWorkspaceId(btn);
navigateToDocument(nodeId, workspaceId, { treeView: 'page' });
e.preventDefault();
} else if (action === 'create') {
e.preventDefault();
void createPage(btn, nodeId);
} else if (action === 'rename') {
e.preventDefault();
var title = window.prompt('重命名页面');
if (title && title.trim()) {
void dispatchTreeCommand(btn, {
action: 'rename',
workspaceId: resolveWorkspaceId(btn),
documentId: nodeId,
title: title.trim()
}).then(function(){
updateTitleEverywhere(nodeId, title.trim());
});
}
} else if (action === 'menu') {
e.preventDefault();
var menuPoint = rowCenter(btn);
dispatchSidebarEvent('tree.page.context-menu', { documentId: nodeId, target: { documentId: nodeId } });
openPageTreeContextMenu(btn.closest('.tree-row'), menuPoint.x, menuPoint.y, btn);
}
});
document.addEventListener('contextmenu', function(event) {
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
if (fileRow) {
event.preventDefault();
var contextRowId = fileRow.getAttribute('data-row-id') || '';
if (contextRowId && !sidebarFileTreeSelection.selectedRowIds.has(contextRowId)) {
selectSidebarFileTreeRow(fileRow, { ctrlKey: false, metaKey: false, shiftKey: false });
}
openFileTreeContextMenu(fileRow, event.clientX, event.clientY, fileRow);
return;
}
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
if (pageRow) {
event.preventDefault();
openPageTreeContextMenu(pageRow, event.clientX, event.clientY, pageRow);
}
});
document.addEventListener('keydown', function(event) {
var keyTarget = event.target;
var fileTreeRootForKey = document.getElementById('sidebar-file-tree-root');
var fileTreeRowForKey = closestAction(keyTarget, '.tree-row[data-shell-mode="filetree"]');
if (!fileTreeRowForKey && fileTreeRootForKey && fileTreeRootForKey.contains(document.activeElement)) {
fileTreeRowForKey = closestAction(document.activeElement, '.tree-row[data-shell-mode="filetree"]');
}
if (fileTreeRowForKey && event.key === 'F2') {
event.preventDefault();
beginFileTreeInlineRename(fileTreeRowForKey);
return;
}
if (fileTreeRowForKey && (event.ctrlKey || event.metaKey) && !event.altKey && event.key) {
var shortcutKey = event.key.toLowerCase();
if (shortcutKey === 'c' || shortcutKey === 'x') {
var selectedRows = selectedSidebarFileTreeRows();
var selectedRowIds = selectedRows.map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean);
if (selectedRowIds.length > 0) {
event.preventDefault();
sidebarFileTreeClipboard = { action: shortcutKey === 'x' ? 'cut' : 'copy', rowIds: selectedRowIds };
document.documentElement.setAttribute('data-mnote-filetree-clipboard-action', sidebarFileTreeClipboard.action);
}
return;
}
if (shortcutKey === 'v') {
event.preventDefault();
void pasteSidebarFileTreeClipboard(fileTreeRowForKey);
return;
}
}
if (event.key === 'Delete' || event.key === 'Backspace') {
if (fileTreeRowForKey) {
event.preventDefault();
void deleteSelectedSidebarFileTreeRows(fileTreeRowForKey);
return;
}
}
if (event.key === 'Escape' && isPageSettingsOpen()) {
event.preventDefault();
closePageSettingsPopover();
return;
}
if ((event.metaKey || event.ctrlKey) && !event.shiftKey && event.key && event.key.toLowerCase() === 'p') {
event.preventDefault();
toggleSearchModal();
return;
}
if (event.key === 'Escape') {
closeTrashModal();
closeSearchModal();
closeTreeContextMenu();
}
if (event.key === 'Enter' && !event.shiftKey) {
var aiInput = closestAction(event.target, '[data-page-ai-input]');
if (aiInput instanceof HTMLTextAreaElement) {
event.preventDefault();
var text = aiInput.value;
aiInput.value = '';
void sendPageAiMessage(text);
}
}
});
document.addEventListener('input', function(event) {
var skillSearch = closestAction(event.target, '[data-page-ai-skill-search]');
if (skillSearch instanceof HTMLInputElement) {
pageUiState.pageAiSkillQuery = skillSearch.value;
renderPageAiControls();
return;
}
var sessionSearch = closestAction(event.target, '[data-page-ai-session-search]');
if (sessionSearch instanceof HTMLInputElement) {
var sessionQuery = sessionSearch.value;
window.clearTimeout(pageUiState.pageAiSessionSearchTimer || 0);
pageUiState.pageAiSessionSearchTimer = window.setTimeout(function() {
void pageAiSearchBackendSessions(sessionQuery).catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
renderPageAiControls();
});
}, 200);
return;
}
var memoryEditor = closestAction(event.target, '[data-page-ai-memory-editor]');
if (memoryEditor instanceof HTMLTextAreaElement) {
var section = memoryEditor.getAttribute('data-page-ai-memory-editor') || '';
if (['memory', 'user', 'soul'].indexOf(section) >= 0) {
pageUiState.pageAiProfileMemoryDrafts[section] = memoryEditor.value;
}
}
});
document.addEventListener('change', function(event) {
var pageAiAcpRuntimeSelect = closestAction(event.target, '[data-page-ai-acp-runtime]');
if (pageAiAcpRuntimeSelect instanceof HTMLSelectElement) {
var next = String(pageAiAcpRuntimeSelect.value || 'reasonix').trim() || 'reasonix';
pageUiState.pageAiAcpRuntime = next;
pageUiState.pageAiSkills = { categories: [], archived: [] };
pageUiState.pageAiSkillError = '';
pageAiPersistSessions();
void pageAiLoadProfiles();
if (next !== 'reasonix') void pageAiLoadProfileMemory();
void pageAiLoadSkills();
void pageAiLoadBackendSessions().catch(function(error) {
pageUiState.pageAiSessionError = error instanceof Error ? error.message : String(error);
});
renderPageAiControls();
renderPageAiProviderButtons();
return;
}
var pageAiProfileSelect = closestAction(event.target, '[data-page-ai-profile-select]');
if (pageAiProfileSelect instanceof HTMLSelectElement) {
void pageAiSwitchProfile(pageAiProfileSelect.value);
return;
}
var pageAiContextSelect = closestAction(event.target, '[data-page-ai-context-scope]');
if (pageAiContextSelect instanceof HTMLSelectElement) {
pageAiSetContextScope(pageAiContextSelect.value);
renderPageAiControls();
return;
}
var globalCheckbox = closestAction(event.target, '[data-global-option-checkbox="showHeadingNumbers"]');
if (globalCheckbox instanceof HTMLInputElement) {
writeGlobalShowHeadingNumbers(globalCheckbox.checked);
applyPageOptionsToShell();
renderPageSettingsPopover();
return;
}
var checkbox = closestAction(event.target, '[data-page-option-checkbox]');
if (checkbox instanceof HTMLInputElement) {
var key = checkbox.getAttribute('data-page-option-checkbox') || '';
if (!pageOptionIsSupported(key)) return;
var patch = {};
patch[key] = checkbox.checked;
void persistPageOptionsPatch(patch);
return;
}
var select = closestAction(event.target, '[data-page-option-select]');
if (select instanceof HTMLSelectElement) {
var selectKey = select.getAttribute('data-page-option-select') || '';
var next = {};
if (selectKey === 'layoutDensity') next.layoutDensity = select.value;
if (selectKey === 'pageFont') next.pageFont = select.value;
if (Object.keys(next).length) {
void persistPageOptionsPatch(next);
}
}
});
function initializePageUiSurfaces() {
pageUiState.pageOptions = null;
applyPageOptionsToShell();
updatePageSettingsTriggerState();
updatePageAiTriggerState();
ensureHistorySnapshotsSeeded();
}
window.__mnoteRecordPageHistorySnapshot = recordPageHistorySnapshot;
window.__mnoteApplyPageOptionsToShell = applyPageOptionsToShell;
function scheduleInitializePageUiSurfaces() {
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', function() {
setTimeout(initializePageUiSurfaces, 0);
}, { once: true });
return;
}
setTimeout(initializePageUiSurfaces, 0);
}
scheduleInitializePageUiSurfaces();
function installMnoteDevHotReload() {
var bootId = '';
var failedOnce = false;
var timer = 0;
function tick() {
fetch('/api/dev/hot-reload', { cache: 'no-store', headers: { accept: 'application/json' } })
.then(function(response) { return response.ok ? response.json() : null; })
.then(function(payload) {
if (!payload || payload.enabled !== true || !payload.bootId) {
if (!bootId && timer) window.clearInterval(timer);
return;
}
document.documentElement.setAttribute('data-mnote-dev-hot-reload', 'enabled');
if (!bootId) {
bootId = String(payload.bootId);
return;
}
if (failedOnce || String(payload.bootId) !== bootId) {
window.location.assign(window.location.href);
}
})
.catch(function() {
if (bootId) failedOnce = true;
});
}
tick();
timer = window.setInterval(tick, 1000);
}
installMnoteDevHotReload();
function readPageDragNodeId(event) {
var fromTransfer = event.dataTransfer ? event.dataTransfer.getData(PAGE_DRAG_MIME) || event.dataTransfer.getData('text/plain') : '';
return (fromTransfer || draggingPageNodeId || '').trim();
}
function clearPageDropFeedback() {
if (activePageDropRow instanceof HTMLElement) {
activePageDropRow.setAttribute('data-drop-feedback', 'false');
}
activePageDropRow = null;
}
function canDropPage(sourceNodeId, targetRow) {
if (!sourceNodeId || !(targetRow instanceof HTMLElement)) return false;
var targetNodeId = targetRow.getAttribute('data-node-id') || '';
if (!targetNodeId || targetNodeId === sourceNodeId) return false;
var sourceNode = document.querySelector('#sidebar-tree-root .tree-node[data-node-id="' + cssEscape(sourceNodeId) + '"]');
return !(sourceNode instanceof HTMLElement && sourceNode.contains(targetRow));
}
function pageDropPosition(event, row) {
var rect = row.getBoundingClientRect();
var ratio = rect.height > 0 ? (event.clientY - rect.top) / rect.height : 0.5;
if (ratio < 0.25) return 'before';
if (ratio > 0.75) return 'after';
return 'inside';
}
function resolvePageMoveTarget(targetRow, position) {
var targetNodeId = targetRow.getAttribute('data-node-id') || '';
var parentId = targetRow.getAttribute('data-parent-id') || null;
if (position === 'inside') {
var children = targetRow.parentElement ? targetRow.parentElement.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="page"]') : [];
return { parentId: targetNodeId, sortOrder: children.length };
}
var siblings = Array.from(document.querySelectorAll('#sidebar-tree-root .tree-row[data-shell-mode="page"]')).filter(function(row) {
return (row.getAttribute('data-parent-id') || '') === (parentId || '');
});
var index = siblings.indexOf(targetRow);
return { parentId: parentId, sortOrder: Math.max(0, index + (position === 'after' ? 1 : 0)) };
}
document.addEventListener('dragstart', function(event) {
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"][draggable="true"]');
if (pageRow) {
draggingPageNodeId = pageRow.getAttribute('data-node-id') || '';
if (event.dataTransfer) {
event.dataTransfer.effectAllowed = 'move';
event.dataTransfer.setData(PAGE_DRAG_MIME, draggingPageNodeId);
event.dataTransfer.setData('text/plain', draggingPageNodeId);
}
return;
}
var fileRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"][draggable="true"]');
if (fileRow) {
draggingFileTreeRowIds = selectedSidebarFileTreeRowIdsForDrag(fileRow);
if (event.dataTransfer) {
var payload = JSON.stringify({ type: 'mnote-file-tree-dnd', version: 1, rowIds: draggingFileTreeRowIds });
event.dataTransfer.effectAllowed = 'copyMove';
event.dataTransfer.setData(FILETREE_DRAG_MIME, payload);
event.dataTransfer.setData('application/x-mnote-file-tree', payload);
event.dataTransfer.setData('text/plain', payload);
}
}
});
document.addEventListener('dragover', function(event) {
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
var sourceNodeId = readPageDragNodeId(event);
if (pageRow && canDropPage(sourceNodeId, pageRow)) {
event.preventDefault();
clearPageDropFeedback();
pageRow.setAttribute('data-drop-feedback', 'true');
pageRow.setAttribute('data-drop-position', pageDropPosition(event, pageRow));
activePageDropRow = pageRow;
if (event.dataTransfer) event.dataTransfer.dropEffect = 'move';
return;
}
var fileTree = document.getElementById('sidebar-file-tree-root');
if (fileTree && fileTree.contains(event.target)) {
var hasFiles = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], 'Files') >= 0;
var hasInternal = event.dataTransfer && Array.prototype.indexOf.call(event.dataTransfer.types || [], FILETREE_DRAG_MIME) >= 0;
if (!hasFiles && !hasInternal && draggingFileTreeRowIds.length === 0) return;
event.preventDefault();
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
activeFileTreeDropRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]') || fileTree;
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'true');
var copyModifier = event.altKey || event.ctrlKey || event.metaKey;
if (event.dataTransfer) event.dataTransfer.dropEffect = hasFiles || copyModifier ? 'copy' : 'move';
}
});
document.addEventListener('drop', function(event) {
var pageRow = closestAction(event.target, '.tree-row[data-shell-mode="page"]');
var sourceNodeId = readPageDragNodeId(event);
if (pageRow && canDropPage(sourceNodeId, pageRow)) {
event.preventDefault();
var position = pageDropPosition(event, pageRow);
var target = resolvePageMoveTarget(pageRow, position);
clearPageDropFeedback();
draggingPageNodeId = '';
void dispatchTreeCommand(pageRow, {
action: 'move',
workspaceId: resolveWorkspaceId(pageRow),
documentId: sourceNodeId,
parentId: target.parentId,
sortOrder: target.sortOrder
});
return;
}
var fileTree = document.getElementById('sidebar-file-tree-root');
if (fileTree && fileTree.contains(event.target)) {
var targetRow = closestAction(event.target, '.tree-row[data-shell-mode="filetree"]');
var files = event.dataTransfer ? Array.from(event.dataTransfer.files || []) : [];
var raw = event.dataTransfer ? event.dataTransfer.getData(FILETREE_DRAG_MIME) || event.dataTransfer.getData('application/x-mnote-file-tree') || '' : '';
var rowIds = draggingFileTreeRowIds.slice();
if (raw) {
try {
var parsed = JSON.parse(raw);
if (Array.isArray(parsed.rowIds)) rowIds = parsed.rowIds;
} catch (_) {}
}
if (!files.length && !rowIds.length) return;
event.preventDefault();
var detail = {
workspaceId: resolveWorkspaceId(targetRow || fileTree),
targetRowId: targetRow ? targetRow.getAttribute('data-row-id') : null,
targetRowKind: targetRow ? targetRow.getAttribute('data-row-kind') : 'root',
documentId: targetRow ? targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id') : null,
assetId: targetRow ? targetRow.getAttribute('data-asset-id') : null
};
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
activeFileTreeDropRow = null;
if (files.length) {
dispatchSidebarEvent('tree.filetree.external-drop', Object.assign({}, detail, { files: files }));
} else {
dispatchSidebarEvent('tree.filetree.internal-drop', Object.assign({}, detail, { rowIds: rowIds, copy: event.altKey === true || event.ctrlKey === true || event.metaKey === true }));
}
draggingFileTreeRowIds = [];
}
});
document.addEventListener('dragend', function() {
draggingPageNodeId = '';
draggingFileTreeRowIds = [];
clearPageDropFeedback();
if (activeFileTreeDropRow instanceof HTMLElement) activeFileTreeDropRow.setAttribute('data-drop-target', 'false');
activeFileTreeDropRow = null;
});
window.addEventListener('tree:title-updated', function(event) {
var detail = event.detail || {};
updateTitleEverywhere(detail.documentId, detail.title);
document.documentElement.setAttribute('data-mnote-title-local-applied', 'true');
});
window.addEventListener('tree:local-command', function(event) {
var detail = event.detail || {};
var body = detail.body || {};
var action = String(body.action || '').trim();
if (action === 'create') {
applyCreatedDocumentLocally(detail.result || {}, body.parentId || null, body.title || '新页面');
return;
}
if (action === 'rename' && body.documentId && body.title) {
updateTitleEverywhere(body.documentId, body.title);
document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'rename');
return;
}
if (action === 'move' && body.documentId) {
if (applyMoveDocumentDelta({ documentId: body.documentId, parentId: body.parentId || null, sortOrder: body.sortOrder })) {
document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'move');
}
return;
}
if ((action === 'purge' || action === 'delete' || action === 'archive') && body.documentId) {
if (applyRemoveDocumentDelta({ documentId: body.documentId })) {
document.documentElement.setAttribute('data-mnote-tree-local-command-applied', 'remove');
}
}
});
window.addEventListener('tree:snapshot', function(event) {
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
if (renderSidebarSnapshot(payload)) {
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'snapshot');
return;
}
setTreeLiveApplyError('tree_snapshot_missing_projection_payload');
});
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) {
updateTitleEverywhere(doc.id || doc.documentId, doc.title || '无标题');
});
}
if (renderSidebarSnapshot(payload)) {
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
return;
}
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'delta');
if (deltaNeedsProjectionRefresh(payload)) {
setTreeLiveApplyError('tree_delta_missing_projection_payload');
}
});
window.addEventListener('tree:resync', function(event) {
var payload = event.detail && event.detail.payload ? event.detail.payload : event.detail;
if (renderSidebarSnapshot(payload)) {
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
return;
}
document.documentElement.setAttribute('data-mnote-tree-live-applied', 'resync');
setTreeLiveApplyError('tree_resync_missing_projection_payload');
});
var tree = document.getElementById('sidebar-tree-root');
var activeId = currentDocumentId();
if (tree && activeId) {
var activeRow = tree.querySelector('.tree-row[data-node-id="' + cssEscape(activeId) + '"]');
if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'true');
}
var initiallySelectedFileRows = document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-selected="true"]');
initiallySelectedFileRows.forEach(function(row) {
if (!(row instanceof HTMLElement)) return;
var rowId = row.getAttribute('data-row-id') || '';
if (!rowId) return;
sidebarFileTreeSelection.selectedRowIds.add(rowId);
if (!sidebarFileTreeSelection.anchorRowId) sidebarFileTreeSelection.anchorRowId = rowId;
sidebarFileTreeSelection.focusedRowId = rowId;
});
syncSidebarFileTreeSelection();
restoreSidebarTreeTab();
startLocalFolderSidebarWatch();
})();
"##;
const TREE_LIVE_CONTROLLER_JS: &str = r##"
(function(){
if (window.__mnoteTreeLiveControllerStarted) return;
window.__mnoteTreeLiveControllerStarted = true;
function readBootstrap() {
var script = document.getElementById('__MNOTE_TREE_LIVE_BOOTSTRAP__');
var fallback = {
schema: 'mnote.tree_live_bootstrap.v1',
transport: 'convex-command-log-sse',
endpoint: '/api/tree/events',
rootIds: [],
initialRevision: null
};
if (!script || !script.textContent) return fallback;
try {
return Object.assign(fallback, JSON.parse(script.textContent));
} catch (_) {
return fallback;
}
}
function resolveWorkspaceId() {
var withWorkspace = document.querySelector('[data-workspace-id]');
if (withWorkspace) {
var value = (withWorkspace.getAttribute('data-workspace-id') || '').trim();
if (value) return value;
}
var params = new URLSearchParams(window.location.search);
return (params.get('workspaceId') || 'default').trim() || 'default';
}
function dispatchTreeEvent(name, detail) {
window.dispatchEvent(new CustomEvent(name, { detail: detail }));
}
function applyStatus(status) {
document.documentElement.setAttribute('data-mnote-tree-live-status', status);
}
function applyTransport(transport) {
document.documentElement.setAttribute('data-mnote-tree-live-transport', transport || 'convex-command-log-sse');
}
function closeActiveSource() {
var source = window.__mnoteTreeLiveEventSource;
if (source && typeof source.close === 'function') {
source.close();
window.__mnoteTreeLiveEventSource = null;
applyStatus('closed');
}
}
function startWithSse(bootstrap, workspaceId, url) {
var failures = 0;
var source = new EventSource(url.toString());
window.__mnoteTreeLiveEventSource = source;
applyStatus('connecting');
source.addEventListener('open', function(){
failures = 0;
applyStatus('connected');
});
source.addEventListener('snapshot', function(event){
var payload = JSON.parse(event.data || '{}');
var revision = payload.revision || payload.cursor || event.lastEventId || null;
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
dispatchTreeEvent('tree:snapshot', { payload: payload, revision: revision, bootstrap: bootstrap });
});
source.addEventListener('delta', function(event){
var payload = JSON.parse(event.data || '{}');
var revision = payload.revision || payload.cursor || event.lastEventId || null;
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
dispatchTreeEvent('tree:delta', { payload: payload, revision: revision, bootstrap: bootstrap });
});
source.addEventListener('resync', function(event){
var payload = JSON.parse(event.data || '{}');
var revision = payload.revision || payload.cursor || event.lastEventId || null;
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision || ''));
dispatchTreeEvent('tree:resync', { payload: payload, revision: revision, bootstrap: bootstrap });
});
source.addEventListener('block.delta', function(event){
var payload = JSON.parse(event.data || '{}');
dispatchTreeEvent('tree:block-delta', { payload: payload, bootstrap: bootstrap });
});
source.onerror = function(){
failures += 1;
applyStatus(failures >= 3 ? 'resync-pending' : 'reconnecting');
};
}
function startWithWebSocket(bootstrap, workspaceId, wsUrl) {
var proto = (window.location.protocol === 'https:') ? 'wss:' : 'ws:';
var url = new URL(wsUrl || bootstrap.wsEndpoint || '/api/realtime/ws', window.location.origin);
url.protocol = proto;
url.searchParams.set('workspaceId', workspaceId);
if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) {
url.searchParams.set('rootNodeId', bootstrap.rootIds[0]);
}
var ws = new WebSocket(url.toString());
window.__mnoteTreeLiveEventSource = ws;
applyStatus('connecting');
ws.onopen = function() {
applyStatus('connected');
};
ws.onmessage = function(event) {
var payload;
try { payload = JSON.parse(event.data); } catch (_) { return; }
var kind = payload.kind || '';
var revision = payload.revision || payload.cursor || '';
if (kind === 'snapshot') {
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision));
dispatchTreeEvent('tree:snapshot', { payload: payload, revision: revision, bootstrap: bootstrap });
} else if (kind === 'delta') {
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision));
dispatchTreeEvent('tree:delta', { payload: payload, revision: revision, bootstrap: bootstrap });
// Delta indicates something changed; request fresh resync from server
if (ws.readyState === WebSocket.OPEN) {
ws.send('resync');
}
} else if (kind === 'resync') {
document.documentElement.setAttribute('data-mnote-tree-live-revision', String(revision));
dispatchTreeEvent('tree:resync', { payload: payload, revision: revision, bootstrap: bootstrap });
} else if (kind === 'resync_hint') {
// Server suggests re-sync after lagged events
if (ws.readyState === WebSocket.OPEN) {
ws.send('resync');
}
}
};
ws.onclose = function() {
applyStatus('closed');
// Fall back to SSE after a short delay
setTimeout(function() {
if (window.__mnoteTreeLiveEventSource === ws) {
startWithSseFallback(bootstrap, workspaceId);
}
}, 2000);
};
ws.onerror = function() {
applyStatus('error');
};
}
function startWithSseFallback(bootstrap, workspaceId) {
applyTransport('convex-command-log-sse');
var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin);
url.searchParams.set('workspaceId', workspaceId);
if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) {
url.searchParams.set('rootNodeId', bootstrap.rootIds[0]);
}
startWithSse(bootstrap, workspaceId, url);
}
function start() {
var bootstrap = readBootstrap();
var params = new URLSearchParams(window.location.search);
var sourceKind = (params.get('sourceKind') || '').trim();
if (sourceKind === 'local_folder') {
applyTransport('local-folder-static');
applyStatus('static');
return;
}
var workspaceId = bootstrap.workspaceId || resolveWorkspaceId();
// Prefer WebSocket transport when available
var preferWs = bootstrap.transport === 'convex-command-log-ws' && 'WebSocket' in window;
if (preferWs) {
applyTransport('convex-command-log-ws');
startWithWebSocket(bootstrap, workspaceId, bootstrap.wsEndpoint || '/api/realtime/ws');
return;
}
// Fallback: SSE / EventSource
if (!('EventSource' in window)) {
applyStatus('unsupported');
return;
}
applyTransport(bootstrap.transport || 'convex-command-log-sse');
var url = new URL(bootstrap.endpoint || '/api/tree/events', window.location.origin);
url.searchParams.set('workspaceId', workspaceId);
if (Array.isArray(bootstrap.rootIds) && bootstrap.rootIds.length === 1) {
url.searchParams.set('rootNodeId', bootstrap.rootIds[0]);
}
startWithSse(bootstrap, workspaceId, url);
}
window.__mnoteTreeLiveClose = closeActiveSource;
window.addEventListener('pagehide', closeActiveSource, { once: true });
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', start, { once: true });
} else {
start();
}
})();
"##;
/// MNOTE Wolai 风格页面布局
///
/// 包含左侧栏 + 内容区的双栏布局。
/// 侧栏显示品牌、导航链接和可选的页面树。
///
/// # 用法
///
/// ```ignore
/// view! {
/// <PageLayout current_nav="home" sidebar_tree_html={None}>
/// <section>...</section>
/// </PageLayout>
/// }
/// ```
#[component]
pub fn PageLayout(
children: Children,
current_nav: &'static str,
/// 侧栏页面树 HTML(可选),由路由 handler 渲染
#[prop(optional)]
sidebar_tree_html: Option<String>,
/// 工作区名称(可选),显示在侧栏顶部
#[prop(optional)]
workspace_name: Option<String>,
/// workspace shell 侧栏 sections HTML(可选),由 projection 渲染
#[prop(optional)]
workspace_sidebar_html: Option<String>,
/// 顶栏当前页面标题(可选)
#[prop(optional)]
topbar_title: Option<String>,
/// 是否显示管理员授权入口
#[prop(optional)]
show_admin_access_policy: bool,
) -> impl IntoView {
let sidebar_tree_html = sidebar_tree_html.unwrap_or_default();
let ws_name = workspace_name
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "开发用户 的空间".to_string());
let topbar_title = topbar_title
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| "个人".to_string());
let sidebar_sections_html = workspace_sidebar_html
.map(|value| value.trim().to_string())
.filter(|value| !value.is_empty())
.unwrap_or_else(|| {
let dataset = serde_json::json!({
"workspaces": [{ "id": "default", "name": ws_name.clone() }],
"documents": []
});
let projection = crate::workspace_shell::build_workspace_shell_projection(
&dataset, "default", None, &ws_name,
);
crate::workspace_shell::render_workspace_shell_sidebar_html(
&projection,
Some(sidebar_tree_html.as_str()),
None,
)
});
let tree_live_bootstrap = serde_json::json!({
"schema": "mnote.tree_live_bootstrap.v1",
"transport": "convex-command-log-ws",
"workspaceId": null,
"rootIds": [],
"initialRevision": null,
"endpoint": "/api/tree/events",
"wsEndpoint": "/api/realtime/ws",
"views": ["page-tree", "file-tree"]
})
.to_string();
view! {
<div class="mnote-shell wolai-workspace-shell" data-mnote-web-owner="mnote-web" data-mnote-shell="workspace">
<aside class="mnote-sidebar wolai-sidebar" data-testid="wolai-sidebar">
<div class="mnote-sidebar-header wolai-sidebar-header" data-testid="wolai-workspace-identity">
<a href="/" class="mnote-sidebar-brand wolai-avatar" aria-label="工作区首页">"L"</a>
<button
type="button"
class="mnote-workspace-source-trigger"
data-testid="mnote-workspace-source-trigger"
data-mnote-action="open-workspace-source-menu"
aria-haspopup="menu"
aria-expanded="false"
title="切换云空间或本地文件夹"
>
<span class="sidebar-workspace-name">{ws_name.clone()}</span>
<span class="wolai-sidebar-chevron" aria-hidden="true">"⌄"</span>
</button>
</div>
<nav class="mnote-sidebar-nav wolai-quick-actions" aria-label="快捷操作">
<a href="/search" class:active={current_nav == "search"} title="搜索" aria-label="搜索" data-mnote-action="open-search-modal"><span class="material-symbols-outlined nav-icon" data-icon="search" aria-hidden="true"></span></a>
<a href="/graph" title="关系图" aria-label="关系图"><span class="material-symbols-outlined nav-icon" data-icon="account_tree" aria-hidden="true"></span></a>
<a href="/actions" title="快捷动作" aria-label="快捷动作"><span class="material-symbols-outlined nav-icon" data-icon="bolt" aria-hidden="true"></span></a>
<a href="/help" title="帮助" aria-label="帮助"><span class="material-symbols-outlined nav-icon" data-icon="help" aria-hidden="true"></span></a>
<a href="/files" title="文件" aria-label="文件"><span class="material-symbols-outlined nav-icon" data-icon="inventory_2" aria-hidden="true"></span></a>
<button type="button" title="打开本地文件夹" aria-label="打开本地文件夹" data-mnote-action="open-local-folder"><span class="material-symbols-outlined nav-icon" data-icon="folder_open" aria-hidden="true"></span></button>
{if show_admin_access_policy {
view! {
<a
href="/admin/access-policy"
class:active={current_nav == "admin"}
title="目录授权"
aria-label="目录授权"
data-testid="mnote-admin-access-policy-entry"
>
<span class="material-symbols-outlined nav-icon" data-icon="admin_panel_settings" aria-hidden="true"></span>
</a>
}.into_any()
} else {
view! {}.into_any()
}}
<a href="/more" title="更多" aria-label="更多"><span class="material-symbols-outlined nav-icon" data-icon="more_horiz" aria-hidden="true"></span></a>
</nav>
<div class="wolai-sidebar-body" inner_html={sidebar_sections_html}></div>
<script id="__MNOTE_TREE_LIVE_BOOTSTRAP__" type="application/json" inner_html={tree_live_bootstrap}></script>
<script inner_html={SIDEBAR_TREE_JS.to_string()}></script>
<script id="mnote-tree-live-controller" inner_html={TREE_LIVE_CONTROLLER_JS.to_string()}></script>
</aside>
<div class="mnote-main">
<header class="wolai-topbar" data-testid="wolai-topbar">
<div class="wolai-topbar-left">
<button type="button" class="wolai-icon-button wolai-menu-button" aria-label="切换侧栏" aria-pressed="false" data-mnote-action="toggle-sidebar" data-testid="wolai-sidebar-toggle"><span class="material-symbols-outlined" data-icon="menu" aria-hidden="true"></span></button>
<nav class="wolai-breadcrumb" aria-label="页面路径">
<span class="wolai-breadcrumb-root">{ws_name.clone()}</span>
<span class="wolai-breadcrumb-separator" aria-hidden="true">""</span>
<span class="wolai-breadcrumb-current"><span class="material-symbols-outlined wolai-home-icon" data-icon="home" aria-hidden="true"></span><span data-page-title-current="true">{topbar_title}</span></span>
</nav>
</div>
<div class="wolai-topbar-actions" aria-label="页面操作">
<span class="wolai-public-pill" data-testid="wolai-public-state">"全网公开"</span>
<button type="button" class="wolai-icon-button" title="收藏" aria-label="收藏"><span class="material-symbols-outlined" data-icon="star" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="演示" aria-label="演示"><span class="material-symbols-outlined" data-icon="slideshow" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="评论" aria-label="评论"><span class="material-symbols-outlined" data-icon="comment" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="分享" aria-label="分享"><span class="material-symbols-outlined" data-icon="share" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="成员" aria-label="成员"><span class="material-symbols-outlined" data-icon="person_add" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="页面历史" aria-label="历史" data-mnote-action="open-page-history"><span class="material-symbols-outlined" data-icon="history" aria-hidden="true"></span></button>
<button type="button" class="wolai-icon-button" title="页面选项和全局选项" aria-label="更多" data-testid="wolai-page-settings-trigger" data-mnote-action="open-page-settings"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>
</div>
</header>
<article class="mnote-content">
{children()}
</article>
<div class="wolai-floating-actions" aria-label="浮动操作">
<button type="button" data-testid="wolai-floating-help" class="wolai-floating-button wolai-floating-button--help" aria-label="帮助"><span class="material-symbols-outlined" data-icon="help" aria-hidden="true"></span></button>
<button type="button" data-testid="wolai-floating-ai" class="wolai-floating-button wolai-floating-button--ai" aria-label="AI 助手" title="点击 进入空间智能问答" data-mnote-action="open-page-ai"><span class="material-symbols-outlined material-symbols-filled" data-icon="auto_awesome" aria-hidden="true"></span></button>
</div>
</div>
</div>
}
}
#[cfg(test)]
mod tests {
use super::{SIDEBAR_TREE_JS, TREE_LIVE_CONTROLLER_JS};
#[test]
fn sidebar_tree_runtime_handles_navigation_drag_and_filetree_actions() {
assert!(SIDEBAR_TREE_JS.contains("mnoteNavigationInFlight"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-navigation-pending"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_SIDEBAR_TREE_MODE_KEY"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_GLOBAL_SHOW_HEADING_NUMBERS_KEY"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_LAST_CLOUD_WORKSPACE_KEY"));
assert!(SIDEBAR_TREE_JS.contains("data-global-option-checkbox=\"showHeadingNumbers\""));
assert!(SIDEBAR_TREE_JS.contains("restoreSidebarTreeTab"));
assert!(SIDEBAR_TREE_JS.contains("treeView"));
assert!(SIDEBAR_TREE_JS.contains("tree:local-command"));
assert!(SIDEBAR_TREE_JS.contains("applyCreatedDocumentLocally"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-tree-local-command-applied', 'create"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-tree-local-command-applied', 'remove"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-tree-local-command-applied', 'rename"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-tree-local-command-applied', 'move"));
assert!(SIDEBAR_TREE_JS.contains("function sortOrderFromDelta(data)"));
assert!(SIDEBAR_TREE_JS.contains("data.sortOrder ?? data.sort_order"));
assert!(SIDEBAR_TREE_JS.contains("function insertTreeNodeAtSortOrder"));
assert!(SIDEBAR_TREE_JS
.contains("applyMoveDocumentDelta({ documentId: body.documentId, parentId: body.parentId || null, sortOrder: body.sortOrder })"));
assert!(SIDEBAR_TREE_JS.contains("wolai:assets-changed"));
assert!(SIDEBAR_TREE_JS.contains("applyAssetsChangedToFileTree"));
assert!(SIDEBAR_TREE_JS.contains("installMindmapAssetFetchObserver"));
assert!(SIDEBAR_TREE_JS.contains("parseMindmapApiTarget"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-assets-local-applied"));
assert!(SIDEBAR_TREE_JS.contains("application/x-mnote-page-tree-node"));
assert!(SIDEBAR_TREE_JS.contains("dragstart"));
assert!(SIDEBAR_TREE_JS.contains("drop"));
assert!(SIDEBAR_TREE_JS.contains("data-drop-feedback"));
assert!(SIDEBAR_TREE_JS.contains("action: 'move'"));
assert!(SIDEBAR_TREE_JS.contains("sidebar-file-tree-root"));
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.open"));
assert!(SIDEBAR_TREE_JS.contains("tree.asset.open"));
assert!(SIDEBAR_TREE_JS.contains("openConvexAssetFromFileTree"));
assert!(SIDEBAR_TREE_JS.contains("buildMindmapOpenPath"));
assert!(SIDEBAR_TREE_JS.contains("isMindmapAssetDetail"));
assert!(!SIDEBAR_TREE_JS.contains("openMindmapAssetInDocumentShell"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-last-mindmap-asset-open-mode"));
assert!(SIDEBAR_TREE_JS.contains("mindmap-object-shell"));
assert!(SIDEBAR_TREE_JS.contains("navigateToMindmapObject(documentId, assetId"));
assert!(SIDEBAR_TREE_JS.contains("__mnoteDocumentPaneRuntime?.openPrimaryMindmap"));
assert!(SIDEBAR_TREE_JS.contains("shortMindmapFileName"));
assert!(SIDEBAR_TREE_JS.contains("assetType: assetType || null"));
assert!(SIDEBAR_TREE_JS.contains("data-object-identity"));
assert!(SIDEBAR_TREE_JS.contains("readFileTreeObjectIdentity"));
assert!(SIDEBAR_TREE_JS.contains("objectIdentity: objectIdentity"));
assert!(SIDEBAR_TREE_JS.contains("workspaceId: resolveWorkspaceId(fileRow)"));
assert!(SIDEBAR_TREE_JS.contains("/api/media/sign?assetId="));
assert!(SIDEBAR_TREE_JS.contains("function localFilePathFromAssetId"));
assert!(SIDEBAR_TREE_JS.contains("/api/local-folder/files/open"));
assert!(SIDEBAR_TREE_JS.contains("local-file:"));
assert!(SIDEBAR_TREE_JS.contains("fetchCurrentOnlyOfficeUserId"));
assert!(SIDEBAR_TREE_JS.contains("/api/auth/whoami"));
assert!(SIDEBAR_TREE_JS.contains("buildOnlyOfficeOpenUrl"));
assert!(SIDEBAR_TREE_JS.contains("target.searchParams.set('userId'"));
assert!(SIDEBAR_TREE_JS.contains("window.open(buildOnlyOfficeOpenUrl"));
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.internal-drop"));
assert!(SIDEBAR_TREE_JS.contains("tree.filetree.external-drop"));
assert!(SIDEBAR_TREE_JS.contains("beginFileTreeInlineRename"));
assert!(SIDEBAR_TREE_JS.contains("tree-rename-input"));
assert!(SIDEBAR_TREE_JS.contains("sidebarFileTreeClipboard"));
assert!(SIDEBAR_TREE_JS.contains("pasteSidebarFileTreeClipboard"));
assert!(SIDEBAR_TREE_JS.contains("MNOTE_RECENT_LOCAL_ROOTS_KEY_PREFIX"));
assert!(SIDEBAR_TREE_JS.contains("function recentLocalRootsStorageKey"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-actor-id"));
assert!(SIDEBAR_TREE_JS.contains("requestOpenLocalFolder"));
assert!(SIDEBAR_TREE_JS.contains("autoOpenRecentLocalRootOnHome"));
assert!(SIDEBAR_TREE_JS.contains("sourceKind', 'local_folder"));
assert!(SIDEBAR_TREE_JS.contains("switchToCloudWorkspace"));
assert!(SIDEBAR_TREE_JS.contains("mnote-switch-cloud-workspace"));
assert!(SIDEBAR_TREE_JS.contains("mnote-recent-local-root"));
}
#[test]
fn page_ai_context_uses_page_aggregate_subtree_as_single_truth() {
assert!(
!SIDEBAR_TREE_JS.contains("pageSubtreeSource: 'local'"),
"页面 AI context 不能从编辑器 DOM 派生 local page subtree;必须以 Rust Page Aggregate projection 为准"
);
assert!(
!SIDEBAR_TREE_JS.contains("buildPageAiLocalSubtree(localBlocks"),
"页面 AI context 不应调用本地 page subtree builder"
);
}
#[test]
fn page_ai_fast_path_is_not_local_first_main_path() {
assert!(
SIDEBAR_TREE_JS.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local-first 页面 AI 不应继续加厚 page-ai fast-path;本地编辑应走受控文件工具"
);
}
#[test]
fn page_ai_local_source_passes_file_reference_fields_to_agent_run() {
assert!(SIDEBAR_TREE_JS.contains("function currentRootUri()"));
assert!(SIDEBAR_TREE_JS.contains("sourceKind: currentSourceKind()"));
assert!(SIDEBAR_TREE_JS.contains("rootUri: currentRootUri()"));
assert!(SIDEBAR_TREE_JS.contains("pageContext: scopedContext.pageContext"));
assert!(
SIDEBAR_TREE_JS.contains("if (currentSourceKind() === 'local_folder') return false;"),
"local source 不应进入 page-ai fast-path,后端会把 run 收敛为文件引用 scope"
);
}
#[test]
fn page_ai_uses_backend_acp_session_runtime_store() {
assert!(SIDEBAR_TREE_JS.contains("function pageAiLoadBackendSessions"));
assert!(SIDEBAR_TREE_JS.contains("/api/hermes/client/sessions?"));
assert!(SIDEBAR_TREE_JS.contains("params.set('source', 'acp')"));
assert!(SIDEBAR_TREE_JS.contains("function pageAiLoadBackendSessionDetail"));
assert!(SIDEBAR_TREE_JS.contains("function pageAiSearchBackendSessions"));
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-session-rename"));
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-session-delete"));
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-session-resume"));
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-session-search"));
assert!(SIDEBAR_TREE_JS.contains("pageAiUsageSummary"));
assert!(SIDEBAR_TREE_JS.contains("usage.updated"));
assert!(SIDEBAR_TREE_JS.contains("thought.delta"));
assert!(SIDEBAR_TREE_JS.contains("permission.requested"));
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-permission-action=\"allow\""));
assert!(SIDEBAR_TREE_JS.contains("data-page-ai-permission-action=\"deny\""));
assert!(SIDEBAR_TREE_JS.contains("function pageAiHidePermissionDialog"));
assert!(SIDEBAR_TREE_JS.contains("if (!message.resolved)"));
assert!(
!SIDEBAR_TREE_JS.contains("(item.resolved ? ' disabled' : '')"),
"已决 ACP permission 事件不能继续展示假审批按钮"
);
}
#[test]
fn page_ai_session_ui_labels_local_shared_and_cloud_storage() {
assert!(SIDEBAR_TREE_JS.contains("function pageAiSessionStorageLabel"));
assert!(SIDEBAR_TREE_JS.contains("local_private"));
assert!(SIDEBAR_TREE_JS.contains("local_shared"));
assert!(SIDEBAR_TREE_JS.contains("convex_acp_runtime_store"));
assert!(SIDEBAR_TREE_JS.contains("本地私有"));
assert!(SIDEBAR_TREE_JS.contains("共享会话"));
assert!(SIDEBAR_TREE_JS.contains("云端会话"));
assert!(SIDEBAR_TREE_JS.contains("sessionStorage:"));
assert!(SIDEBAR_TREE_JS.contains("permissionLevel:"));
assert!(SIDEBAR_TREE_JS.contains("shareId:"));
}
#[test]
fn page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch() {
assert!(SIDEBAR_TREE_JS.contains("var PAGE_AI_SESSION_STORAGE_VERSION = 3"));
assert!(SIDEBAR_TREE_JS.contains("pageAiAcpRuntime: 'reasonix'"));
assert!(SIDEBAR_TREE_JS.contains("function pageAiNormalizeAcpRuntimes"));
assert!(SIDEBAR_TREE_JS.contains("return ['reasonix', 'hermes']"));
assert!(SIDEBAR_TREE_JS.contains("pageAiNormalizeAcpRuntimes(acpRuntimes)"));
assert!(SIDEBAR_TREE_JS.contains("pageAiAcpRuntimeSelect.value || 'reasonix'"));
assert!(SIDEBAR_TREE_JS
.contains("activeAcpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix'"));
assert!(!SIDEBAR_TREE_JS.contains("默认 (Hermes HTTP)"));
}
#[test]
fn sidebar_tree_runtime_does_not_use_retired_query_preferred_snapshot_selector() {
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
assert!(!SIDEBAR_TREE_JS.contains("usePreferredSidebarSnapshot"));
assert!(!SIDEBAR_TREE_JS.contains("preferredSidebarSnapshot"));
assert!(!SIDEBAR_TREE_JS.contains("liveSidebarTitle"));
}
#[test]
fn page_layout_sidebar_toggle_is_wired_to_shell_state() {
assert!(SIDEBAR_TREE_JS.contains("function toggleWorkspaceSidebar"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-sidebar-collapsed"));
assert!(SIDEBAR_TREE_JS.contains("[data-mnote-action=\"toggle-sidebar\"]"));
}
#[test]
fn sidebar_workspace_source_switch_remembers_real_cloud_workspace_before_local_folder() {
assert!(
!SIDEBAR_TREE_JS.contains("rememberCloudWorkspaceId(resolveWorkspaceId(document.body))"),
"进入本地文件夹前不能用 document.body 推导云空间 workspaceId;它会在无 workspaceId URL 时回退成 default"
);
assert!(
SIDEBAR_TREE_JS.contains("rememberCurrentCloudWorkspaceId()"),
"进入本地文件夹前应从 URL 或 DOM 子树读取真实云空间 workspaceId"
);
}
#[test]
fn sidebar_tree_runtime_polls_local_folder_without_browser_reload() {
assert!(SIDEBAR_TREE_JS.contains("startLocalFolderSidebarWatch"));
assert!(SIDEBAR_TREE_JS.contains("refreshLocalFolderSidebarSnapshot"));
assert!(SIDEBAR_TREE_JS.contains("/api/tree/local-folder-watch"));
assert!(SIDEBAR_TREE_JS.contains("replaceSidebarTreeFromDocument"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-local-folder-watch-applied"));
assert!(!SIDEBAR_TREE_JS.contains("window.location.reload"));
}
#[test]
fn sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder() {
assert!(SIDEBAR_TREE_JS.contains("currentSourceKind() === 'local_folder'"));
assert!(SIDEBAR_TREE_JS.contains("/api/local-folder/assets/upload"));
assert!(SIDEBAR_TREE_JS.contains("localForm.append('rootUri', rootUri)"));
assert!(SIDEBAR_TREE_JS.contains("localForm.append('documentId', documentId)"));
assert!(SIDEBAR_TREE_JS.contains("isLocalUploadedAsset(asset)"));
assert!(SIDEBAR_TREE_JS.contains("if (isLocalUploadedAsset(asset)) return '';"));
assert!(SIDEBAR_TREE_JS.contains("refreshLocalFolderSidebarSnapshot"));
assert!(SIDEBAR_TREE_JS.contains("wolai:local-assets-changed"));
}
#[test]
fn sidebar_tree_runtime_contains_dev_hot_reload_client() {
assert!(SIDEBAR_TREE_JS.contains("installMnoteDevHotReload"));
assert!(SIDEBAR_TREE_JS.contains("/api/dev/hot-reload"));
assert!(SIDEBAR_TREE_JS.contains("data-mnote-dev-hot-reload"));
assert!(SIDEBAR_TREE_JS.contains("window.clearInterval(timer)"));
}
#[test]
fn sidebar_tree_runtime_renders_context_menu_and_scoped_title_updates() {
assert!(SIDEBAR_TREE_JS.contains("openTreeContextMenu"));
assert!(SIDEBAR_TREE_JS.contains("mnote-tree-context-menu"));
assert!(SIDEBAR_TREE_JS.contains("复制访问链接"));
assert!(SIDEBAR_TREE_JS.contains("删除到垃圾桶"));
assert!(SIDEBAR_TREE_JS.contains(
r#".tree-row[data-shell-mode="page"][data-node-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
assert!(SIDEBAR_TREE_JS.contains(
r#".tree-row[data-shell-mode="filetree"][data-row-id="' + escapedDocRowId + '"] > .tree-link > .tree-link-title"#
));
assert!(SIDEBAR_TREE_JS
.contains(r#"[data-page-title-input="true"][data-document-id="' + escaped + '"]"#));
assert!(SIDEBAR_TREE_JS.contains(r#".wolai-breadcrumb-current [data-page-title-current]"#));
assert!(!SIDEBAR_TREE_JS.contains(
r#".tree-row[data-document-id="' + escaped + '"] > .tree-link > .tree-link-title"#
));
assert!(!SIDEBAR_TREE_JS
.contains(r#"[data-node-id="' + cssEscape(documentId) + '"] .tree-link-title"#));
assert!(SIDEBAR_TREE_JS.contains("renderSidebarSnapshot"));
assert!(SIDEBAR_TREE_JS.contains("kernel_file_tree_projection"));
assert!(SIDEBAR_TREE_JS.contains("deltaNeedsProjectionRefresh"));
assert!(SIDEBAR_TREE_JS.contains("upsert_documents"));
assert!(SIDEBAR_TREE_JS.contains("setTreeLiveApplyError"));
assert!(!SIDEBAR_TREE_JS.contains("scheduleProjectionRefresh"));
assert!(!SIDEBAR_TREE_JS.contains("/api/tree/projections/sidebar"));
assert!(SIDEBAR_TREE_JS.contains("/api/tree/projections/file"));
}
#[test]
fn sidebar_filetree_runtime_uses_markdown_page_row_without_local_index_child() {
assert!(SIDEBAR_TREE_JS.contains("fileTreePageTitle(title)"));
assert!(SIDEBAR_TREE_JS.contains("normalizeFileTreePageRenameTitle"));
assert!(SIDEBAR_TREE_JS.contains("validateFileTreeRename"));
assert!(SIDEBAR_TREE_JS.contains("同级已存在同名页面"));
assert!(SIDEBAR_TREE_JS.contains("文件名不能包含"));
assert!(SIDEBAR_TREE_JS.contains("rowId === 'doc:' + activeId"));
assert!(SIDEBAR_TREE_JS.contains("currentFileTreeActiveRowId"));
assert!(SIDEBAR_TREE_JS
.contains("window.location.pathname.match(/^\\/mindmap\\/([^\\/]+)\\/([^\\/]+)/)"));
assert!(SIDEBAR_TREE_JS.contains(
"var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId"
));
assert!(SIDEBAR_TREE_JS
.contains("renderFileRows('', groupRowsByParent(rows), activeId, activeRowId)"));
assert!(SIDEBAR_TREE_JS.contains("renderFileRows(nodeId, grouped, activeId, activeRowId)"));
assert!(SIDEBAR_TREE_JS.contains("normalizeMindmapFileTreeTitle"));
assert!(SIDEBAR_TREE_JS.contains("shortMindmapFileName(assetId || title)"));
assert!(
SIDEBAR_TREE_JS.contains("var title = isFileTreeProjectionPageRow(rowKind, assetId)")
);
assert!(!SIDEBAR_TREE_JS.contains("title: 'index.md'"));
assert!(!SIDEBAR_TREE_JS.contains("rowId === 'index:' + activeId"));
}
#[test]
fn sidebar_tree_delete_to_trash_dispatches_archive_not_purge() {
assert!(SIDEBAR_TREE_JS.contains(
"if (action === 'delete-trash' && documentId) {\n if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return;\n void dispatchTreeCommand(trigger || document.body, {\n action: 'archive',"
));
assert!(!SIDEBAR_TREE_JS.contains(
"if (action === 'delete-trash' && documentId) {\n if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return;\n void dispatchTreeCommand(trigger || document.body, {\n action: 'purge',"
));
}
#[test]
fn sidebar_filetree_delete_keys_support_mixed_doc_and_asset_selection() {
assert!(SIDEBAR_TREE_JS.contains("function deleteSelectedSidebarFileTreeRows"));
assert!(SIDEBAR_TREE_JS.contains("event.key === 'Delete' || event.key === 'Backspace'"));
assert!(SIDEBAR_TREE_JS.contains(
"currentSourceKind() === 'local_folder' && fileTreeRowKind(row) === 'asset'"
));
assert!(SIDEBAR_TREE_JS.contains("if (currentSourceKind() === 'local_folder')"));
assert!(SIDEBAR_TREE_JS.contains("documentId: fileAssetIds[lf]"));
assert!(SIDEBAR_TREE_JS.contains("/api/media/batch"));
assert!(SIDEBAR_TREE_JS.contains("/api/mindmap/"));
assert!(SIDEBAR_TREE_JS.contains("/api/tables/"));
assert!(SIDEBAR_TREE_JS.contains("确认删除选中的 "));
}
#[test]
fn sidebar_tree_runtime_keeps_pdf_and_code_assets_out_of_onlyoffice() {
assert!(SIDEBAR_TREE_JS.contains("function inferOnlyOfficeFileType"));
assert!(SIDEBAR_TREE_JS.contains("isNonOfficeAttachmentName(name, ext)"));
assert!(!SIDEBAR_TREE_JS.contains("if (ext === 'pdf') return ext;"));
assert!(!SIDEBAR_TREE_JS.contains("mt.indexOf('pdf') >= 0"));
assert!(SIDEBAR_TREE_JS.contains("mnote-uploaded-attachment-pdf"));
assert!(SIDEBAR_TREE_JS.contains("mnote-uploaded-attachment-code"));
assert!(SIDEBAR_TREE_JS.contains("'toml'"));
assert!(SIDEBAR_TREE_JS.contains("'json'"));
assert!(SIDEBAR_TREE_JS.contains("'yaml'"));
assert!(SIDEBAR_TREE_JS.contains("'md'"));
assert!(SIDEBAR_TREE_JS.contains("'vue'"));
assert!(SIDEBAR_TREE_JS.contains("'svelte'"));
assert!(SIDEBAR_TREE_JS.contains("'proto'"));
assert!(SIDEBAR_TREE_JS.contains("'dockerfile'"));
assert!(SIDEBAR_TREE_JS.contains("'.gitignore'"));
}
#[test]
fn sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools() {
assert!(SIDEBAR_TREE_JS.contains("function openPdfEditorAttachment"));
assert!(SIDEBAR_TREE_JS.contains("function openCodeEditorAttachment"));
assert!(SIDEBAR_TREE_JS.contains("function resolveEditorAttachmentUrl"));
assert!(SIDEBAR_TREE_JS.contains("type: 'codeBlock'"));
assert!(SIDEBAR_TREE_JS.contains("attrs: { language: language }"));
assert!(SIDEBAR_TREE_JS.contains("inferCodeAttachmentLanguage"));
assert!(SIDEBAR_TREE_JS.contains("if (isPdfAttachmentFileName(detail.fileName))"));
assert!(SIDEBAR_TREE_JS.contains("if (isCodeAttachmentFileName(detail.fileName))"));
assert!(SIDEBAR_TREE_JS
.contains("if (isCodeAttachmentFileName(fileName) && String(asset.document_id"));
assert!(SIDEBAR_TREE_JS.contains("await openCodeEditorAttachment({"));
}
#[test]
fn tree_live_controller_marks_transport_and_closes_source_on_pagehide() {
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-sse"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("convex-command-log-ws"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("new WebSocket(url.toString())"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("startWithSseFallback"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("data-mnote-tree-live-transport"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("pagehide"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("__mnoteTreeLiveEventSource"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:snapshot"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:delta"));
assert!(TREE_LIVE_CONTROLLER_JS.contains("tree:resync"));
assert!(!TREE_LIVE_CONTROLLER_JS.contains("resyncEndpoint"));
assert!(!TREE_LIVE_CONTROLLER_JS.contains("tree:resync-requested"));
}
}