//! 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) { var node = target && target.nodeType === Node.TEXT_NODE ? target.parentElement : target; return node && typeof node.closest === 'function' ? node.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, '>') .replace(/"/g, '"') .replace(/'/g, '''); } 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 = ['']; 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(' ' + escapeHtml(pageAiBlockText(block)) + ''); }); lines.push(''); 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 currentWorkspaceId() { return resolveWorkspaceId(document.body); } function currentSourceKind() { var params = new URLSearchParams(window.location.search); var fromUrl = (params.get('sourceKind') || '').trim(); if (fromUrl) return fromUrl; var fromBody = document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-source-kind') || '').trim() : ''; if (fromBody) return fromBody; if ( window.location.pathname === '/' && !(params.get('workspaceId') || '').trim() && !(params.get('rootUri') || '').trim() && !(params.get('pageId') || '').trim() ) { return 'local_folder'; } return 'convex_workspace'; } function currentRootUri() { var params = new URLSearchParams(window.location.search); var fromUrl = (params.get('rootUri') || '').trim(); if (fromUrl) return fromUrl; return document.body instanceof HTMLElement ? (document.body.getAttribute('data-mnote-root-uri') || '').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); }); var sourceKind = currentSourceKind(); var rootUri = currentRootUri(); if (sourceKind && !targetUrl.searchParams.get('sourceKind')) targetUrl.searchParams.set('sourceKind', sourceKind); if (rootUri && !targetUrl.searchParams.get('rootUri')) targetUrl.searchParams.set('rootUri', rootUri); } 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; }); var sourceKind = currentSourceKind(); var rootUri = currentRootUri(); if (sourceKind && !payload.sourceKind) payload.sourceKind = sourceKind; if (rootUri && !payload.rootUri) payload.rootUri = rootUri; 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 = [ '
', '

本地文件夹垃圾箱

', '

本地删除项保存在当前目录的 .mnote/trash.mnote/trash-index.json 中。

', '
当前弹窗已保持在本地文件夹上下文:' + escapeHtml(rootUri || '未选择本地目录') + '
', '
' ].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 = '
' + '
' + '' + '
正在加载垃圾箱...
' + '
'; 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(); var workspaceId = resolveWorkspaceId(trigger || document.body); var url = new URL('/trash', window.location.origin); if (sourceKind === 'local_folder') { var rootUri = currentRootUri(); if (!rootUri) { renderLocalFolderTrashPlaceholder(content, ''); return; } url.searchParams.set('sourceKind', 'local_folder'); url.searchParams.set('rootUri', rootUri); } else 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 = '
' + escapeHtml(error && error.message ? error.message : String(error)) + '
'; }); } 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) { if (typeof window.__mnoteDocumentPaneRuntime?.activatePageTab === 'function') { window.__mnoteDocumentPaneRuntime.activatePageTab(); } 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'); } } }); selectSidebarFileTreeDocument(nodeId, { scrollIntoView: 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'); selectSidebarFileTreeDocument(nodeId, { scrollIntoView: true }); }).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; var nextDocumentId = commandDocumentId(result, ''); if (nextDocumentId) { window.__mnoteLocalFolderSelfChangeSuppressions = window.__mnoteLocalFolderSelfChangeSuppressions || new Map(); window.__mnoteLocalFolderSelfChangeSuppressions.set(nextDocumentId, Date.now() + 5000); } if (nextDocumentId && currentSourceKind() === 'local_folder') { await refreshLocalFolderSidebarSnapshot(); selectSidebarFileTreeDocument(nextDocumentId, { scrollIntoView: true }); document.documentElement.setAttribute('data-mnote-create-page-selected-document-id', nextDocumentId); } navigateToDocument(nextDocumentId, nextWorkspaceId, { treeView: activeSidebarTreeMode() }); } async function createFileTreeFolder(trigger, parentId) { if (currentSourceKind() !== 'local_folder') return false; var workspaceId = resolveWorkspaceId(trigger || document.body); var effectiveParentId = String(parentId || '').trim(); var title = window.prompt('新建文件夹', '新建文件夹'); if (!title || !title.trim()) return false; var result = await dispatchTreeCommand(trigger || document.body, { action: 'create_folder', workspaceId: workspaceId, parentId: effectiveParentId || null, title: title.trim() }); document.documentElement.setAttribute('data-mnote-filetree-folder-created', 'true'); document.documentElement.setAttribute('data-mnote-filetree-folder-created-id', commandDocumentId(result, result.id || '')); void refreshLocalFolderSidebarSnapshot(); return true; } 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.execution && (result.execution.documentId || result.execution.id || result.execution.nodeId)) || 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.execution && result.execution.title) || 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 localCommandNeedsProjectionRefresh(action, result) { if (currentSourceKind() !== 'local_folder') return false; if (action === 'create' || action === 'rename' || action === 'move' || action === 'delete' || action === 'archive' || action === 'trash' || action === 'purge') { return true; } if (result && (result.previousDocumentId || result.previousRelativePath || result.relativePath || result.trashPath)) { return true; } return false; } 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 fileWorkspaceRelativePath(item) { var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {}; var workspacePath = meta.workspacePath && typeof meta.workspacePath === 'object' ? meta.workspacePath : {}; var fromWorkspacePath = String(workspacePath.relativePath || '').trim(); if (fromWorkspacePath) return fromWorkspacePath; var extra = meta.extra && typeof meta.extra === 'object' ? meta.extra : {}; var source = extra.source && typeof extra.source === 'object' ? extra.source : {}; var fromSource = String(source.relativePath || '').trim(); if (fromSource) return fromSource; return String(item && (item.relativePath || item.rootRelativePath) || '').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 ''; } 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 ? '' : ''; var childHtml = expandable ? '' : ''; var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"'; return '
  • ' + toggle + '
    ' + childHtml + '
  • '; }).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 = ''; 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 : {}; // Phase A1: workspacePath 优先于旧猜测逻辑 var wp = meta.workspacePath; if (wp && typeof wp === 'object' && wp.objectIdentity && typeof wp.objectIdentity === 'object') { return wp.objectIdentity; } 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 fileCapabilitiesAttr(item) { try { return JSON.stringify(Array.isArray(item && item.capabilities) ? item.capabilities : []); } catch (_error) { return '[]'; } } 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'; if (!isMindmap) return title || '无标题'; return title || shortMindmapFileName(assetId); } 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 relativePath = fileWorkspaceRelativePath(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 ? '' : ''; var createAction = rowKind === 'document' ? '' : ''; var childHtml = expandable ? '' : ''; return '
  • ' + toggle + '
    ' + createAction + '
    ' + childHtml + '
  • '; }).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 = ''; 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 workspaceId = currentWorkspaceId(); var rootUri = currentRootUri(); var currentId = currentDocumentId(); if (!workspaceId || !rootUri) return false; var sidebarUrl = new URL('/api/tree/projections/sidebar', window.location.origin); sidebarUrl.searchParams.set('workspaceId', workspaceId); sidebarUrl.searchParams.set('sourceKind', 'local_folder'); sidebarUrl.searchParams.set('rootUri', rootUri); if (currentId) sidebarUrl.searchParams.set('rootNodeId', currentId); var fileUrl = new URL('/api/tree/projections/file', window.location.origin); fileUrl.searchParams.set('workspaceId', workspaceId); fileUrl.searchParams.set('sourceKind', 'local_folder'); fileUrl.searchParams.set('rootUri', rootUri); if (currentId) fileUrl.searchParams.set('rootNodeId', currentId); var responses = await Promise.all([ fetch(sidebarUrl.toString(), { headers: { accept: 'application/json' } }), fetch(fileUrl.toString(), { headers: { accept: 'application/json' } }) ]); if (!responses[0].ok && !responses[1].ok) return false; var sidebarPayload = responses[0].ok ? await responses[0].json().catch(function() { return null; }) : null; var filePayload = responses[1].ok ? await responses[1].json().catch(function() { return null; }) : null; var renderedPage = sidebarPayload ? renderSidebarSnapshot(sidebarPayload.result || sidebarPayload) : false; var renderedFile = filePayload ? renderFileProjection(filePayload.result || filePayload) : false; if (!renderedPage && !renderedFile) return false; syncSidebarFileTreeSelection(); restoreSidebarTreeTab(); document.documentElement.setAttribute('data-mnote-local-folder-watch-applied', 'projection'); return true; } function startLocalFolderSidebarWatch() { if (currentSourceKind() !== 'local_folder') return; var rootUri = currentRootUri(); 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); var fileUrl = String(input.fileUrl || '').trim(); if (fileUrl) { try { fileUrl = new URL(fileUrl, window.location.origin).toString(); } catch (_error) {} } target.searchParams.set('fileUrl', 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 || 'view'); 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 || 'view'); if (input.documentId && input.assetId) { return '/office/' + encodeURIComponent(input.documentId) + '/' + encodeURIComponent(input.assetId) + '?' + params.toString(); } return '/onlyoffice?' + params.toString(); } function buildLocalOnlyOfficeOpenUrl(relativePath, fileName, documentId, assetId, mode) { var fileType = inferOnlyOfficeFileType(fileName || relativePath || '', ''); if (!fileType) return ''; var fileUrl = buildLocalFileOpenUrl(relativePath, false); if (!fileUrl) return ''; return buildOnlyOfficeOpenUrl({ fileUrl: fileUrl, fileName: fileName || relativePath || '未命名资源', fileType: fileType, assetId: assetId || ('local-file:' + relativePath), documentId: documentId || currentDocumentId() || '', userId: '', mode: mode || 'view' }); } function buildMindmapOpenPath(documentId, assetId) { var doc = String(documentId || '').trim(); var map = String(assetId || '').trim(); if (!doc || !map) return ''; var targetUrl = new URL('/mindmap/' + encodeURIComponent(doc) + '/' + encodeURIComponent(map), window.location.origin); copyWorkspaceSourceParams(targetUrl); return targetUrl.pathname + targetUrl.search; } 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; var fileName = assetId.indexOf('/') >= 0 ? assetId.split('/').pop() : assetId; return assetId.indexOf('mindmap_') === 0 || assetId.indexOf('mindmap-') === 0 || /\.mindmap\.json$/i.test(fileName) || (/^思维导图/i.test(fileName) && /\.json$/i.test(fileName)); } function localFilePathFromAssetId(assetId) { var value = String(assetId || '').trim(); return value.indexOf('local-file:') === 0 ? value.slice('local-file:'.length) : value.indexOf('local:asset:') === 0 ? value.slice('local:asset:'.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(); } async function openLocalResourceInActiveTab(input) { if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab !== 'function') return false; var relativePath = String(input && input.path || '').trim(); var rootUri = String(input && input.rootUri || currentRootUri() || '').trim(); if (!relativePath || !rootUri) return false; var title = String(input && input.title || '').trim() || relativePath.split('/').pop() || relativePath; var kind = String(input && input.kind || fileTreeIconKindForFileName(title) || '').trim(); var objectIdentity = 'resource:file:' + rootUri + ':' + relativePath; return await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ objectIdentity: objectIdentity, assetId: String(input && input.assetId || '').trim() || 'local-file:' + relativePath, title: title, fileName: title, kind: kind, rootUri: rootUri, path: relativePath, href: String(input && input.href || buildLocalFileOpenUrl(relativePath, false) || '').trim(), officeUrl: String(input && input.officeUrl || '').trim(), documentId: String(input && input.documentId || currentDocumentId() || '').trim(), workspaceId: String(input && input.workspaceId || resolveWorkspaceId(document.body) || '').trim() }); } 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 openTarget = String(detail && detail.openTarget || '').trim().toLowerCase(); var forceNewWindow = openTarget === 'new-window'; var forceEditMode = openTarget === 'edit-mode'; if (openTarget === 'side') { if (typeof window.__mnoteDocumentPaneRuntime?.openResourceAsSideTarget === 'function') { void window.__mnoteDocumentPaneRuntime.openResourceAsSideTarget({ objectIdentity: String(detail.objectIdentity || detail.assetId || ''), assetId: assetId, title: String(detail.title || detail.fileName || assetId || ''), kind: String(detail.iconKind || detail.assetType || 'file'), documentId: String(detail.documentId || currentDocumentId() || ''), workspaceId: String(detail.workspaceId || resolveWorkspaceId(document.body) || '') }); } return; } var localFilePath = localFilePathFromAssetId(assetId); if (localFilePath) { var localFileName = localFilePath.split('/').pop() || localFilePath; if (isMindmapAssetDetail(detail) || String(detail && detail.assetType || '').trim() === 'mindmap') { if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') { document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-resource-tab'); document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId); void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ objectIdentity: 'resource:mindmap:' + String(detail && detail.documentId || currentDocumentId() || '').trim() + ':' + assetId, assetId: assetId, mindmapId: assetId, title: String(detail && detail.title || localFileName || '思维导图').trim(), fileName: localFileName, kind: 'mindmap', documentId: String(detail && detail.documentId || currentDocumentId() || '').trim(), workspaceId: String(detail.workspaceId || '').trim() }); return; } document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'local-mindmap-object-shell'); document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId); navigateToMindmapObject(String(detail && detail.documentId || currentDocumentId() || '').trim(), assetId, String(detail.workspaceId || '').trim()); return; } var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail && detail.documentId || currentDocumentId() || '').trim(), assetId, forceEditMode ? 'edit' : 'view'); if (localOfficeUrl) { if (!forceNewWindow && await openLocalResourceInActiveTab({ path: localFilePath, title: localFileName, kind: 'office', assetId: assetId, documentId: String(detail && detail.documentId || currentDocumentId() || '').trim(), workspaceId: String(detail.workspaceId || '').trim(), officeUrl: localOfficeUrl })) return; window.open(localOfficeUrl, '_blank', 'noopener,noreferrer'); return; } var localFileUrl = buildLocalFileOpenUrl(localFilePath, false); if (localFileUrl) { if (!forceNewWindow && await openLocalResourceInActiveTab({ path: localFilePath, title: localFileName, kind: fileTreeIconKindForFileName(localFileName), assetId: assetId, documentId: String(detail && detail.documentId || currentDocumentId() || '').trim(), workspaceId: String(detail.workspaceId || '').trim(), href: localFileUrl })) return; window.open(localFileUrl, '_blank', 'noopener,noreferrer'); } return; } var documentId = String(detail && detail.documentId || '').trim(); if (isMindmapAssetDetail(detail) && documentId) { if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') { document.documentElement.setAttribute('data-mnote-last-mindmap-asset-open-mode', 'mindmap-resource-tab'); document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', assetId); void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ objectIdentity: 'resource:mindmap:' + documentId + ':' + assetId, assetId: assetId, mindmapId: assetId, title: String(detail.title || '思维导图').trim(), fileName: String(detail.title || '思维导图').trim(), kind: 'mindmap', documentId: documentId, workspaceId: String(detail.workspaceId || '').trim() }); return; } 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(); var officeUrl = buildOnlyOfficeOpenUrl({ fileUrl: fileUrl, fileName: fileName, fileType: fileType, assetId: assetId, documentId: String(asset.document_id || detail.documentId || '').trim(), userId: userId, mode: forceEditMode ? 'edit' : 'view' }); if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') { var didOpen = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ objectIdentity: 'resource:onlyoffice:' + String(asset.document_id || detail.documentId || '').trim() + ':' + assetId, assetId: assetId, title: fileName, fileName: fileName, kind: 'office', officeUrl: officeUrl, documentId: String(asset.document_id || detail.documentId || '').trim(), workspaceId: String(detail.workspaceId || '').trim() }); if (didOpen) return; } window.open(officeUrl, '_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 fileTreeRowCapabilities(row) { if (!(row instanceof HTMLElement)) return []; try { var parsed = JSON.parse(row.getAttribute('data-capabilities') || '[]'); return Array.isArray(parsed) ? parsed.map(function(item) { return String(item || '').trim(); }).filter(Boolean) : []; } catch (_) { return []; } } function fileTreeRowIsReadonly(row) { return fileTreeRowCapabilities(row).some(function(capability) { return ['readonly', 'readOnly', 'permissionDenied'].indexOf(capability) >= 0; }); } function blockReadonlyFileTreeAction(action, detail, message) { var normalizedAction = String(action || 'drop').trim() || 'drop'; var text = String(message || '目标位置是只读,不能拖放到这里').trim(); var targetRowId = String(detail && (detail.targetRowId || detail.rowId) || '').trim(); var documentId = String(detail && detail.documentId || '').trim(); var assetId = String(detail && detail.assetId || '').trim(); recordFileTreeAction(normalizedAction, { rowId: targetRowId, documentId: documentId, assetId: assetId, readonly: true }); recordFileTreeActionStatus('blocked', { rowId: targetRowId, documentId: documentId, assetId: assetId, readonly: true, fallback: 'alert' }); document.documentElement.setAttribute('data-mnote-filetree-readonly-blocked', normalizedAction); document.documentElement.setAttribute('data-mnote-filetree-readonly-message', text); if (targetRowId) { document.documentElement.setAttribute('data-mnote-filetree-readonly-target-row-id', targetRowId); var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(targetRowId) + '"]'); if (row instanceof HTMLElement) { row.setAttribute('data-readonly-blocked', normalizedAction); row.setAttribute('data-readonly-message', text); } } window.alert(text); return false; } function fileTreeDocumentParentsForPreflight() { return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]')).map(function(row) { var documentId = row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || ''; if (!documentId) return null; var parentRowId = row.getAttribute('data-parent-id') || ''; var parentDocumentId = parentRowId.indexOf('doc:') === 0 ? parentRowId.slice(4) : parentRowId || null; return { documentId: documentId, parentId: parentDocumentId }; }).filter(Boolean); } function fileTreeTargetChildrenForPreflight(targetRow) { var node = targetRow instanceof HTMLElement ? targetRow.closest('.tree-node') : null; if (!node) return []; return Array.from(node.querySelectorAll(':scope > .tree-children > .tree-node > .tree-row[data-shell-mode="filetree"]')).map(function(row) { return { 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, title: rowTitle(row) }; }); } function fileTreeDropPreflightRows() { return fileTreeRowsForUploadPreflight().map(function(row) { var domRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(row.rowId) + '"]'); row.title = rowTitle(domRow); return row; }); } async function ensureFileTreeWritableTarget(action, targetRow, rowIds, copy) { var normalizedAction = String(action || 'drop').trim() || 'drop'; if (!(targetRow instanceof HTMLElement)) return true; var detail = { targetRowId: targetRow.getAttribute('data-row-id') || '', rowId: targetRow.getAttribute('data-row-id') || '', documentId: targetRow.getAttribute('data-document-id') || targetRow.getAttribute('data-doc-id') || '', assetId: targetRow.getAttribute('data-asset-id') || '' }; if (fileTreeRowIsReadonly(targetRow)) { return blockReadonlyFileTreeAction(normalizedAction, detail, '目标位置是只读,不能拖放到这里'); } try { var response = await fetch('/api/tree/filetree/drop-preflight', { method: 'POST', credentials: 'include', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ workspaceId: resolveWorkspaceId(targetRow), copy: Boolean(copy), sourceCapabilities: ['read', 'write', 'move'], targetCapabilities: fileTreeRowCapabilities(targetRow), targetDocumentId: detail.documentId || null, targetRowId: detail.targetRowId || null, focusedRowId: sidebarFileTreeSelection.focusedRowId || null, activeDocumentId: currentDocumentId() || null, rowIds: Array.isArray(rowIds) ? rowIds : [], rows: fileTreeDropPreflightRows(), targetChildren: fileTreeTargetChildrenForPreflight(targetRow), documentParents: fileTreeDocumentParentsForPreflight() }) }); if (response.ok) return true; var payload = await response.json().catch(function() { return null; }); var message = payload && (payload.error || payload.message) ? String(payload.error || payload.message) : '目标位置是只读,不能拖放到这里'; if (message.indexOf('只读') >= 0 || message.toLowerCase().indexOf('readonly') >= 0) { return blockReadonlyFileTreeAction(normalizedAction, detail, message); } return true; } catch (_) { return true; } } 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 (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { return { workspaceId: workspaceId, targetDocumentId: documentId, targetMindmapId: null, targetSubPath: null, targetRelativePath: String(detail.targetRelativePath || '') }; } if (!workspaceId || !documentId) { throw new Error('请选择一个目标页面后再拖入文件'); } return { workspaceId: workspaceId, targetDocumentId: documentId, targetMindmapId: null, targetSubPath: null }; } async function resolveFileTreeUploadTarget(detail) { if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { return fallbackFileTreeUploadTarget(detail || {}); } try { var plan = await preflightFileTreeUploadTarget(detail || {}); if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) { plan.targetRelativePath = String(detail.targetRelativePath || ''); } return plan; } 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 localAssetOpenUrl(asset, download) { if (!isLocalUploadedAsset(asset)) return ''; var rootUri = String(asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri(); var rootRelativePath = String(asset && (asset.rootRelativePath || asset.root_relative_path) || '').trim(); if (!rootUri || !rootRelativePath) return ''; var url = new URL('/api/local-folder/files/open', window.location.origin); url.searchParams.set('rootUri', rootUri); url.searchParams.set('path', rootRelativePath); if (download) url.searchParams.set('download', 'true'); return url.toString(); } function uploadedAssetType(asset) { return String(asset && (asset.asset_type || asset.assetType || asset.mime_type || '') || '').trim(); } function fileTreeIconKindForFileName(fileName) { var name = String(fileName || '').trim().toLowerCase(); var ext = name.indexOf('.') >= 0 ? name.split('.').pop() : ''; if (['doc', 'docx', 'odt', 'rtf', 'ppt', 'pptx', 'odp', 'xls', 'xlsx', 'ods', 'csv'].indexOf(ext) >= 0) return 'office'; if (ext === 'md' || ext === 'markdown') return 'markdown'; if (ext === 'pdf') return 'pdf'; if (['png', 'jpg', 'jpeg', 'gif', 'webp', 'svg', 'bmp'].indexOf(ext) >= 0) return 'image'; return ''; } 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) { var title = uploadedAssetTitle(asset); var fileType = inferOnlyOfficeFileType(title, asset && asset.mime_type); if (!fileType) return ''; var assetId = String(asset && asset.id || '').trim(); if (isLocalUploadedAsset(asset)) { var localUrl = localAssetOpenUrl(asset, false); if (!localUrl) return ''; return buildOnlyOfficeOpenUrl({ fileUrl: localUrl, fileName: title, fileType: fileType, assetId: assetId, documentId: String(asset && (asset.document_id || asset.documentId) || currentDocumentId() || '').trim(), userId: userId || '', mode: 'view' }); } 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: 'view' }); } 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; } var projectionUrl = new URL('/api/tree/projections/file', window.location.origin); projectionUrl.searchParams.set('documentId', documentId); projectionUrl.searchParams.set('workspaceId', workspaceId); var sourcePayload = currentWorkspaceSourcePayload(); if (sourcePayload.sourceKind) projectionUrl.searchParams.set('sourceKind', sourcePayload.sourceKind); if (sourcePayload.rootUri) projectionUrl.searchParams.set('rootUri', sourcePayload.rootUri); legacyOfficeAttachmentIndexPending = fetch(projectionUrl.toString(), { 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: 'view' })); 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; var localFilePath = localFilePathFromAssetId(assetId); if (localFilePath) { var localMeta = { assetId: assetId, fileSize: String(detail && detail.fileSize || '').trim() }; attachmentMetaCache[assetId] = localMeta; applyEditorAttachmentMeta(link, localMeta); 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 false; if (revealFileTreeAssetRow(assetId)) return true; var objectKind = String(asset && (asset.objectKind || asset.resourceKind || '') || '').trim(); if (!objectKind && (String(asset && (asset.asset_type || asset.assetType) || '').trim() === 'mindmap' || /\.mindmap\.json$/i.test(assetId))) { objectKind = 'mindmap'; } 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 && currentSourceKind() === 'local_folder' && objectKind === 'mindmap') return false; 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 false; 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'; var objectIdentity = { objectKind: objectKind || 'attachment', documentId: targetDocumentId || null, blockId: null, assetId: assetId }; li.setAttribute('data-node-id', 'asset:' + assetId); var title = uploadedAssetTitle(asset); var iconKind = fileTreeIconKindForFileName(title) || uploadedAssetType(asset) || 'file'; if (objectKind === 'mindmap') iconKind = 'mindmap'; li.innerHTML = ''; container.appendChild(li); revealFileTreeRow(li.querySelector('.tree-row')); document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId); return true; } 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, writeResult) { var docId = String(documentId || currentDocumentId() || '').trim(); var assetId = String(mindmapId || '').trim(); if (!docId || !assetId) return null; var result = writeResult && typeof writeResult === 'object' ? writeResult : {}; var resultAssetId = String(result.assetId || result.id || '').trim(); var relativePath = String(result.relativePath || result.rootRelativePath || '').trim(); var fileName = String(result.fileName || '').trim() || shortMindmapFileName(assetId); return { id: resultAssetId || (relativePath ? 'local-file:' + relativePath : assetId), document_id: docId, asset_type: 'mindmap', file_name: fileName, file_url: relativePath || ('/documents/' + encodeURIComponent(docId) + '/' + encodeURIComponent(fileName)), sourcePath: relativePath, rootRelativePath: relativePath, sourceKind: currentSourceKind(), rootUri: currentRootUri(), objectKind: 'mindmap' }; } function shortMindmapFileName(mindmapId) { var raw = String(mindmapId || '').trim(); var pathName = raw.indexOf('/') >= 0 ? raw.split('/').pop() : raw; if (/\.json$/i.test(pathName)) return pathName; var digits = raw.match(/(\d{4,})$/); var suffix = digits ? digits[1].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 withLocalMindmapSourceParams(input) { var target = parseMindmapApiTarget(input); if (!target || currentSourceKind() !== 'local_folder') return input; var rootUri = currentRootUri(); if (!rootUri) return input; var rawUrl = typeof input === 'string' ? input : input && typeof input.url === 'string' ? input.url : ''; if (!rawUrl) return input; var url = new URL(rawUrl, window.location.origin); url.searchParams.set('sourceKind', 'local_folder'); url.searchParams.set('rootUri', rootUri); if (typeof input === 'string') return url.pathname + url.search; if (typeof Request !== 'undefined' && input instanceof Request) { return new Request(url.toString(), input); } return url.toString(); } 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, payload) { if (!target || !target.documentId || !target.mindmapId) return; var asset = mindmapAssetFromTarget(target.documentId, target.mindmapId, payload && payload.writeResult); if (!asset) return; var appended = appendUploadedAssetRow(asset, target.documentId); if (!appended && currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); document.documentElement.setAttribute('data-mnote-assets-local-applied', 'true'); document.documentElement.setAttribute('data-mnote-last-mindmap-asset-id', 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 nextInput = withLocalMindmapSourceParams(input); var target = parseMindmapApiTarget(nextInput); var method = requestMethod(input, init); var createOnly = requestBodyHasMindmapCreateOnly(init || {}); return originalFetch(nextInput, init).then(function(response) { if (target && method === 'POST' && response && response.ok) { var cloned = response.clone(); void cloned.json().then(function(payload) { if (createOnly || target.documentId === currentDocumentId()) { applyMindmapApiMutationToFileTree(target, payload); } }).catch(function() { if (createOnly || target.documentId === currentDocumentId()) { applyMindmapApiMutationToFileTree(target, null); } }); } return response; }); }; } async function archiveLocalFileTreeAsset(row, assetId) { if (!assetId) return false; await dispatchTreeCommand(row || document.body, { action: 'archive', workspaceId: resolveWorkspaceId(row || document.body), documentId: assetId }); removeFileTreeAssetRow(assetId); return true; } async function deleteSingleFileTreeAsset(detail, trigger) { var assetId = String(detail && detail.assetId || '').trim(); if (!assetId) return false; var row = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : null; if (!row) row = document.querySelector('.tree-row[data-shell-mode="filetree"][data-asset-id="' + cssEscape(assetId) + '"]'); if (currentSourceKind() === 'local_folder') { return archiveLocalFileTreeAsset(row, assetId); } var documentId = String(detail && detail.documentId || '').trim(); var kind = classifySidebarFileTreeAsset(row); if (kind === 'mindmap') { var response = await fetch('/api/mindmap/' + encodeURIComponent(documentId) + '/' + encodeURIComponent(assetId), { method: 'DELETE' }); if (!response.ok) throw new Error('mindmap_delete_failed_' + response.status); removeFileTreeAssetRow(assetId); return true; } if (kind === 'table') { var tableResponse = await fetch('/api/tables/' + encodeURIComponent(assetId), { method: 'DELETE' }); if (!tableResponse.ok) throw new Error('table_delete_failed_' + tableResponse.status); removeFileTreeAssetRow(assetId); return true; } await postSidebarFileTreeJson('/api/media/batch', { action: 'delete', assetIds: [assetId] }); removeFileTreeAssetRow(assetId); return true; } 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 = localAssetOpenUrl(asset, false) || 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 = buildOnlyOfficeAssetOpenUrl(asset, userId); if (onlyOfficeUrl && assetId) { userId = await fetchCurrentOnlyOfficeUserId(); onlyOfficeUrl = buildOnlyOfficeAssetOpenUrl(asset, userId); } var href = onlyOfficeUrl || url; if (href) { var storedHref = onlyOfficeUrl ? (isLocalAsset ? 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: 'view' })) : 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(); var hasFolderTarget = plan && Object.prototype.hasOwnProperty.call(plan, 'targetRelativePath'); if (!rootUri || (!documentId && !hasFolderTarget)) { throw new Error('本地 Markdown 上传缺少 rootUri 或 documentId'); } var localForm = new FormData(); localForm.append('file', file); localForm.append('rootUri', rootUri); if (documentId) localForm.append('documentId', documentId); if (hasFolderTarget) localForm.append('targetRelativePath', String(plan.targetRelativePath || '')); 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 triggerBrowserDownload(url) { if (!url) return false; var link = document.createElement('a'); link.href = url; link.target = '_blank'; link.rel = 'noopener noreferrer'; link.download = ''; link.style.position = 'fixed'; link.style.left = '-9999px'; link.style.top = '0'; document.body.appendChild(link); try { link.click(); } catch (_) { if (typeof window.open === 'function') window.open(url, '_blank', 'noopener,noreferrer'); } window.setTimeout(function() { if (link.parentElement) link.parentElement.removeChild(link); }, 1000); return true; } function recordFileTreeAction(action, detail) { var normalized = String(action || '').trim() || 'unknown'; var rowId = String(detail && detail.rowId || '').trim(); var documentId = String(detail && detail.documentId || '').trim(); var assetId = String(detail && detail.assetId || '').trim(); document.documentElement.setAttribute('data-mnote-filetree-last-action', normalized); if (rowId) document.documentElement.setAttribute('data-mnote-filetree-last-action-row-id', rowId); if (documentId) document.documentElement.setAttribute('data-mnote-filetree-last-action-document-id', documentId); if (assetId) document.documentElement.setAttribute('data-mnote-filetree-last-action-asset-id', assetId); window.dispatchEvent(new CustomEvent('tree.filetree.action', { detail: Object.assign({}, detail || {}, { action: normalized }) })); } function recordFileTreeActionStatus(status, detail) { var normalized = String(status || '').trim() || 'unknown'; document.documentElement.setAttribute('data-mnote-filetree-last-action-status', normalized); window.dispatchEvent(new CustomEvent('tree.filetree.action.status', { detail: Object.assign({}, detail || {}, { status: normalized }) })); } 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 fileTreeCopyPath(detail, trigger) { // 对 local_folder 复制真实相对路径,不复制标题 if (currentSourceKind() === 'local_folder') { if (detail.assetId) { var path = localFilePathFromAssetId(detail.assetId); if (path) { try { return decodeURIComponent(path.replace(/~2F/g, '/')); } catch (_) { return path.replace(/~2F/g, '/'); } } } if (detail.documentId) { var docPath = String(detail.documentId || '') .replace(/^local-md:/, '') .replace(/^local-dir:/, '') .replace(/~2F/g, '/'); if (docPath) { try { return decodeURIComponent(docPath); } catch (_) { return docPath; } } } } return detail.title || ''; } function fileTreeMenuTargetParentId(detail, trigger) { var rowKind = String(detail && detail.rowKind || '').trim(); var rowId = String(detail && detail.rowId || '').trim(); var documentId = String(detail && detail.documentId || '').trim(); if (rowKind === 'folder' && rowId) return rowId; if (rowKind === 'directory' && rowId) return rowId; if (documentId) return documentId; if (trigger && typeof trigger.getAttribute === 'function') { var triggerKind = String(trigger.getAttribute('data-row-kind') || '').trim(); var triggerRowId = String(trigger.getAttribute('data-row-id') || '').trim(); if ((triggerKind === 'folder' || triggerKind === 'directory') && triggerRowId) return triggerRowId; } return ''; } function withOfficeEditModeGuard(callback) { document.documentElement.setAttribute('data-mnote-last-office-edit-mode-requested', 'true'); document.documentElement.setAttribute('data-mnote-last-office-edit-mode-guard', 'silent'); callback(); } 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 === 'new-window') { openEditorAttachmentNewWindow(detail); return; } if (action === 'new-window-edit') { withOfficeEditModeGuard(function() { openEditorAttachmentNewWindow(detail, 'edit'); }); return; } if (action === 'open-edit-mode') { withOfficeEditModeGuard(function() { void openEditorAttachmentEditTab(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 || '无标题'; var isAsset = detail.contextKind === 'filetree' && detail.assetId && detail.rowKind !== 'document' && detail.rowKind !== 'index'; if (detail.contextKind === 'filetree' && action === 'download') { downloadSelectedFileTreeAssetRows(detail, trigger); return; } if (isAsset && action === 'new-window') { recordFileTreeAction('new-window', detail); void openConvexAssetFromFileTree({ ...detail, openTarget: 'new-window' }); return; } if (isAsset && action === 'open-edit-mode') { withOfficeEditModeGuard(function() { recordFileTreeAction('open-edit-mode', detail); void openConvexAssetFromFileTree({ ...detail, openTarget: 'edit-mode' }); }); return; } if (isAsset && action === 'open-right') { recordFileTreeAction('open-right', detail); void openConvexAssetFromFileTree({ ...detail, openTarget: 'side' }); return; } if (action === 'open-right') { recordFileTreeAction('open-right', detail); 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 === 'delete-trash' && isAsset) { if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return; recordFileTreeAction('delete-trash', detail); recordFileTreeActionStatus('pending', detail); void deleteSingleFileTreeAsset(detail, trigger).then(function() { recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' })); if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); }).catch(function(error) { recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); window.alert(error && error.message ? error.message : '资源删除失败'); }); return; } if (action === 'new-file') { void createPage(trigger || document.body, fileTreeMenuTargetParentId(detail, trigger) || null); return; } if (action === 'new-folder') { recordFileTreeAction('new-folder', detail); void createFileTreeFolder(trigger || document.body, fileTreeMenuTargetParentId(detail, trigger) || null).then(function(ok) { recordFileTreeActionStatus(ok ? 'created' : 'skipped', detail); }).catch(function(error) { recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); window.alert(error && error.message ? error.message : '新建文件夹失败'); }); return; } if (action === 'paste-into') { var pasteRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger; recordFileTreeAction('paste-into', detail); void pasteSidebarFileTreeClipboard(pasteRow).then(function(ok) { recordFileTreeActionStatus(ok ? 'applied' : 'skipped', detail); }).catch(function(error) { recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); window.alert(error && error.message ? error.message : '粘贴失败'); }); return; } if (action === 'copy-path') { void copyTreeContextValue(fileTreeCopyPath(detail, trigger), '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; } var deleteTargetId = documentId || String(detail.rowId || '').trim(); if (action === 'delete-trash' && deleteTargetId) { if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return; recordFileTreeAction('delete-trash', detail); recordFileTreeActionStatus('pending', detail); void dispatchTreeCommand(trigger || document.body, { action: 'archive', workspaceId: workspaceId, documentId: deleteTargetId }).then(function() { recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' })); if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); }).catch(function(error) { recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' })); window.alert(error && error.message ? error.message : '删除失败'); }); } } 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 isFileTreeDownload = kind === 'filetree' && detail.downloadable; 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: 'open-edit-mode', icon: 'edit_note', label: '弹窗编辑' }, { action: 'new-window-edit', icon: 'open_in_new', label: '新窗口编辑' }, { action: 'new-window', icon: 'open_in_new', 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-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' }, { action: 'new-window', icon: 'open_in_new', label: '在新窗口打开' }, { action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) > 1 ? '下载 ' + Number(detail.selectedDownloadCount || detail.selectedAssetCount || 0) + ' 个项目' : '下载' }, { action: 'rename', icon: 'edit', label: '重命名', shortcut: 'F2' }, { action: 'copy-id', icon: 'tag', label: '复制资源 ID' }, { action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true }, { 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+' }, { action: 'download', icon: 'download', label: Number(detail.selectedDownloadCount || 0) > 1 ? '下载 ' + Number(detail.selectedDownloadCount || 0) + ' 个项目' : '下载', disabled: !isFileTreeDownload, title: isFileTreeDownload ? '下载当前本地文件或文件夹' : '当前项目没有可下载的本地路径' }, { 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: currentSourceKind() !== 'local_folder', title: currentSourceKind() === 'local_folder' ? '在当前目录下创建子文件夹' : '仅 local folder 支持创建文件夹' }, { action: 'paste-into', icon: 'content_paste', label: '粘贴到此处', disabled: !sidebarFileTreeClipboard, title: sidebarFileTreeClipboard ? '粘贴到当前文件树目标' : '剪贴板为空' }, { 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: '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: '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') || ''; var selectedDownloadRows = selectedSidebarFileTreeRowsForDownload(row); var selectedAssetRows = selectedDownloadRows.filter(isFileTreeDownloadableAssetRow); openTreeContextMenu('filetree', { documentId: documentId, rowId: row.getAttribute('data-row-id') || '', rowKind: row.getAttribute('data-row-kind') || '', assetId: row.getAttribute('data-asset-id') || '', localRelativePath: fileTreeRowLocalRelativePath(row), title: rowTitle(row), workspaceId: resolveWorkspaceId(row), downloadable: isFileTreeDownloadableRow(row), selectedDownloadCount: selectedDownloadRows.length, selectedDownloadRowIds: selectedDownloadRows.map(function(downloadRow) { return downloadRow.getAttribute('data-row-id') || ''; }).filter(Boolean), selectedAssetCount: selectedAssetRows.length, selectedAssetRowIds: selectedAssetRows.map(function(assetRow) { return assetRow.getAttribute('data-row-id') || ''; }).filter(Boolean) }, x, y, trigger || row); } function visibleFileTreeRows() { return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]')) .filter(function(row) { if (!(row instanceof HTMLElement)) return false; if (row.closest('.tree-children--collapsed')) return false; return row.offsetParent !== null || row.getClientRects().length > 0; }); } 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 selectSidebarFileTreeDocument(documentId, options) { var id = String(documentId || '').trim(); if (!id) return false; var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape('doc:' + id) + '"]') || document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-document-id="' + cssEscape(id) + '"]') || document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-doc-id="' + cssEscape(id) + '"]'); if (!(row instanceof HTMLElement)) return false; var rowId = row.getAttribute('data-row-id') || ''; if (!rowId) return false; sidebarFileTreeSelection.selectedRowIds = new Set([rowId]); sidebarFileTreeSelection.anchorRowId = rowId; sidebarFileTreeSelection.focusedRowId = rowId; document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-active="true"]').forEach(function(activeRow) { if (activeRow instanceof HTMLElement) activeRow.setAttribute('data-active', 'false'); }); syncSidebarFileTreeSelection(); row.setAttribute('data-active', 'true'); if (options && options.scrollIntoView !== false) { try { row.scrollIntoView({ block: 'nearest' }); } catch (_) { row.scrollIntoView(); } } return true; } 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 decodeLocalEncodedPath(value) { var path = String(value || '').trim().replace(/~2F/g, '/'); if (!path) return ''; try { return decodeURIComponent(path); } catch (_) { return path; } } function fileTreeRowLocalRelativePath(row) { if (!(row instanceof HTMLElement)) return ''; var direct = String(row.getAttribute('data-local-relative-path') || '').trim(); if (direct) return direct; var assetPath = localFilePathFromAssetId(fileTreeRowAssetId(row)); if (assetPath) return assetPath; var rowId = String(row.getAttribute('data-row-id') || '').trim(); var nodeId = String(row.getAttribute('data-node-id') || '').trim(); var documentId = fileTreeRowDocumentId(row); var kind = fileTreeRowKind(row); if (rowId.indexOf('local:asset:') === 0) return rowId.slice('local:asset:'.length); if (rowId.indexOf('local:markdown:') === 0) return rowId.slice('local:markdown:'.length); if (rowId.indexOf('local:folder:') === 0) return rowId.slice('local:folder:'.length); if (rowId.indexOf('local:node:') === 0) return rowId.slice('local:node:'.length); if (nodeId.indexOf('local:node:') === 0) return nodeId.slice('local:node:'.length); if ((kind === 'document' || kind === 'doc' || kind === 'markdown') && documentId.indexOf('local-md:') === 0) { return decodeLocalEncodedPath(documentId.slice('local-md:'.length)); } if ((kind === 'folder' || kind === 'directory' || kind === 'index') && documentId.indexOf('local-dir:') === 0) { return decodeLocalEncodedPath(documentId.slice('local-dir:'.length)); } return ''; } function fileTreeRowKind(row) { if (!(row instanceof HTMLElement)) return ''; return String(row.getAttribute('data-row-kind') || '').trim(); } function isFileTreeDownloadableAssetRow(row) { if (!(row instanceof HTMLElement)) return false; var kind = fileTreeRowKind(row); if (kind === 'document' || kind === 'doc' || kind === 'index' || kind === 'markdown' || kind === 'folder' || kind === 'directory') return false; return Boolean(fileTreeRowAssetId(row)); } function isFileTreeDownloadableRow(row) { if (!(row instanceof HTMLElement)) return false; if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row); return Boolean(fileTreeRowLocalRelativePath(row) || isFileTreeDownloadableAssetRow(row)); } function fileTreeAssetDownloadDetail(row) { if (!isFileTreeDownloadableRow(row)) return null; var title = rowTitle(row); var relativePath = fileTreeRowLocalRelativePath(row); var assetId = fileTreeRowAssetId(row) || (relativePath ? 'local-file:' + relativePath : ''); return { contextKind: 'filetree', documentId: fileTreeRowDocumentId(row), rowId: row.getAttribute('data-row-id') || '', rowKind: fileTreeRowKind(row), assetId: assetId, localRelativePath: relativePath, title: title, fileName: title, workspaceId: resolveWorkspaceId(row) }; } function selectedSidebarFileTreeRowsForDownload(contextRow) { var contextRowId = contextRow instanceof HTMLElement ? contextRow.getAttribute('data-row-id') || '' : ''; var selectedRows = selectedSidebarFileTreeRows().filter(isFileTreeDownloadableRow); if (contextRowId && sidebarFileTreeSelection.selectedRowIds.has(contextRowId) && selectedRows.length > 0) return selectedRows; return isFileTreeDownloadableRow(contextRow) ? [contextRow] : selectedRows; } function selectedSidebarFileTreeAssetRowsForDownload(contextRow) { return selectedSidebarFileTreeRowsForDownload(contextRow).filter(isFileTreeDownloadableAssetRow); } function downloadSelectedFileTreeAssetRows(detail, trigger) { var contextRow = trigger && trigger.closest ? trigger.closest('.tree-row[data-shell-mode="filetree"]') : trigger; if (!(contextRow instanceof HTMLElement) && detail && detail.rowId) { contextRow = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(detail.rowId) + '"]'); } var rows = selectedSidebarFileTreeRowsForDownload(contextRow); var seen = new Set(); var downloads = []; rows.forEach(function(row) { var item = fileTreeAssetDownloadDetail(row); var key = item && (item.localRelativePath || item.assetId || item.rowId); if (!item || !key || seen.has(key)) return; seen.add(key); downloads.push(item); }); if (downloads.length === 0 && detail && detail.assetId) { downloads.push(Object.assign({}, detail, { fileName: detail.fileName || detail.title || '附件' })); } else if (downloads.length === 0 && detail && detail.localRelativePath) { downloads.push(Object.assign({}, detail, { assetId: detail.assetId || 'local-file:' + detail.localRelativePath, fileName: detail.fileName || detail.title || '附件' })); } if (downloads.length === 0) { recordFileTreeActionStatus('blocked', Object.assign({}, detail || {}, { reason: 'no-downloadable-assets' })); return false; } var primary = Object.assign({}, downloads[0], { count: downloads.length, assetIds: downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean), localRelativePaths: downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean) }); recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary); recordFileTreeActionStatus('requested', primary); document.documentElement.setAttribute('data-mnote-filetree-download-count', String(downloads.length)); document.documentElement.setAttribute('data-mnote-filetree-download-asset-ids', downloads.map(function(item) { return item.assetId || ''; }).filter(Boolean).join(',')); document.documentElement.setAttribute('data-mnote-filetree-download-paths', downloads.map(function(item) { return item.localRelativePath || ''; }).filter(Boolean).join(',')); downloads.forEach(function(item) { void openEditorAttachmentDownload(item); }); return true; } 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 folderRows = []; 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 (kind === 'folder' && currentSourceKind() === 'local_folder') { folderRows.push(row); return; } if (fileTreeRowAssetId(row)) assetRows.push(row); }); var selectedContainerRowIds = new Set(docRows.concat(folderRows).map(function(row) { return row.getAttribute('data-row-id') || ''; }).filter(Boolean)); var topDocRows = []; var topFolderRows = []; var seenDocs = new Set(); var seenFolders = new Set(); docRows.forEach(function(row) { var documentId = fileTreeRowDocumentId(row); if (!documentId || seenDocs.has(documentId)) return; if (hasSelectedDocumentAncestor(row, selectedContainerRowIds)) return; seenDocs.add(documentId); topDocRows.push(row); }); folderRows.forEach(function(row) { var rowId = row.getAttribute('data-row-id') || ''; if (!rowId || seenFolders.has(rowId)) return; if (hasSelectedDocumentAncestor(row, selectedContainerRowIds)) return; seenFolders.add(rowId); topFolderRows.push(row); }); var selectedTopContainerRows = new Set(topDocRows.concat(topFolderRows).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, selectedTopContainerRows)) 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, folderRows: topFolderRows, fileAssetRows: fileAssetRows, mindmapRows: mindmapRows, tableRows: tableRows }; } function sidebarFileTreeDeleteConfirmText(plan) { var docCount = plan.docRows.length; var folderCount = plan.folderRows.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 (folderCount > 0) parts.push(folderCount + ' 个文件夹(删除到垃圾桶)'); if (fileCount > 0) parts.push(fileCount + ' 个附件(删除,10 分钟内可撤销)'); if (mindmapCount > 0) parts.push(mindmapCount + ' 个思维导图(移入垃圾桶,10 分钟内可恢复)'); if (tableCount > 0) parts.push(tableCount + (currentSourceKind() === 'local_folder' ? ' 个在线表格(移入垃圾桶,10 分钟内可恢复)' : ' 个在线表格(删除)')); 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 writable = await ensureFileTreeWritableTarget('paste', targetRow, sidebarFileTreeClipboard.rowIds, action === 'copy'); if (!writable) return false; recordFileTreeAction('paste', { rowId: targetRow ? targetRow.getAttribute('data-row-id') || '' : '', documentId: targetDocumentId, sourceRowIds: sidebarFileTreeClipboard.rowIds, clipboardAction: sidebarFileTreeClipboard.action }); 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 }); recordFileTreeActionStatus('copy-requested', { documentId: 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) { recordFileTreeActionStatus('failed', { documentId: targetDocumentId, fallback: 'alert' }); window.alert('部分对象移动失败:' + failures.join(';')); return false; } sidebarFileTreeClipboard = null; recordFileTreeActionStatus('applied', { documentId: targetDocumentId }); return true; } async function deleteSelectedSidebarFileTreeRows(trigger) { var rows = selectedSidebarFileTreeRows(); var plan = buildSidebarFileTreeDeletePlan(rows); var total = plan.docRows.length + plan.folderRows.length + plan.fileAssetRows.length + plan.mindmapRows.length + plan.tableRows.length; if (total === 0) return false; if (!window.confirm(sidebarFileTreeDeleteConfirmText(plan))) return false; recordFileTreeAction('bulk-delete', { rowId: trigger instanceof HTMLElement ? trigger.getAttribute('data-row-id') || '' : '', count: total }); recordFileTreeActionStatus('pending', { count: total }); 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); } } for (var fd = 0; fd < plan.folderRows.length; fd += 1) { var folderRow = plan.folderRows[fd]; var folderId = folderRow.getAttribute('data-row-id') || folderRow.getAttribute('data-node-id') || ''; try { await dispatchTreeCommand(trigger || folderRow, { action: 'archive', workspaceId: resolveWorkspaceId(folderRow), documentId: folderId }); } catch (error) { failures.push(folderId); } } 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 { if (currentSourceKind() === 'local_folder') { await dispatchTreeCommand(trigger || mindmapRow, { action: 'archive', workspaceId: resolveWorkspaceId(mindmapRow), documentId: mindmapId }); } else { 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 { if (currentSourceKind() === 'local_folder') { await dispatchTreeCommand(trigger || tableRow, { action: 'archive', workspaceId: resolveWorkspaceId(tableRow), documentId: tableId }); } else { var tableResponse = await fetch('/api/tables/' + encodeURIComponent(tableId), { method: 'DELETE' }); if (!tableResponse.ok) throw new Error('table_delete_failed_' + tableResponse.status); window.dispatchEvent(new CustomEvent('online-table-deleted', { detail: { tableId: tableId } })); } removeFileTreeAssetRow(tableId); } catch (error) { failures.push(tableId); } } sidebarFileTreeSelection.selectedRowIds = new Set(); sidebarFileTreeSelection.focusedRowId = null; syncSidebarFileTreeSelection(); if (failures.length > 0) { recordFileTreeActionStatus('failed', { count: total, failures: failures.slice(0, 20), fallback: 'alert' }); window.alert('部分对象删除失败:' + failures.slice(0, 5).join(', ') + (failures.length > 5 ? '…' : '')); return false; } document.documentElement.setAttribute('data-mnote-filetree-bulk-delete-applied', 'true'); recordFileTreeActionStatus('archived', { count: total, undo: 'trash-modal' }); if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); 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 = '' + '
    ' + '
    ' + '' + '' + '' + '
    ' + '' + '
    ' + '
    ' + '
    '; 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(/<mark>/g, '') .replace(/<\/mark>/g, ''); } 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)) + '' + escapeHtml(cleanTitle.slice(index, index + cleanQuery.length)) + '' + 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 = '最近浏览Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开'; if (results) { results.innerHTML = '
    暂无最近浏览
    '; } } 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 = '搜索中…Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开'; results.innerHTML = '
    搜索中…
    '; 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 = '共 ' + items.length + ' 条匹配结果Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开'; if (!items.length) { results.innerHTML = '
    没有搜索结果
    '; 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 ''; }).join(''); } catch (error) { if (requestId !== activeSearchRequestId) return; meta.innerHTML = '共 0 条匹配结果Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开'; results.innerHTML = '
    没有搜索结果
    '; } } 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 '' + ''; } return '' + ''; } function createGlobalHeadingNumbersRow() { return '' + ''; } function renderGlobalOptions(popover) { var globalHeadingNumbers = readGlobalShowHeadingNumbers(); popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) { input.checked = globalHeadingNumbers; }); } function createPageFontRow() { return '' + ''; } 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 = '' + '
    ' + '
    ' + '
    页面历史
    当前会话内最近保存的 15 个快照。
    ' + '' + '
    ' + '
    ' + '
    '; 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 '' + '
    ' + '
    ' + '
    ' + escapeHtml(label) + '
    ' + '
    字数 ' + Number(stats.wordCount || 0) + ' · 字符 ' + Number(stats.characterCount || 0) + ' · 块数 ' + Number(stats.blockCount || 0) + '
    ' + '
    ' + '' + '
    '; }).join('') : '
    尚未产生历史快照,编辑后会自动生成。
    '; } 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 = '' + ''; 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; // 调用后端 resolve-permission 端点,让 ACP agent 得到真实响应 var runId = pageUiState.pageAiCurrentRunId; if (runId) { fetch('/api/hermes/client/runs/' + encodeURIComponent(runId) + '/resolve-permission', { method: 'POST', headers: { 'content-type': 'application/json' }, body: JSON.stringify({ permissionId: permissionId, decision: decision }) }).then(function(response) { if (!response.ok) console.warn('resolve-permission 后端返回异常', response.status); }).catch(function(err) { console.warn('resolve-permission 请求失败', err); }); } else { console.warn('resolve-permission: 无活跃 runId,只能本地更新'); } // 本地乐观更新 UI 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 = '' + ''; 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(); var rawLocations = payload.locations; 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), locations: Array.isArray(rawLocations) ? rawLocations.filter(function(l) { return typeof l === 'string' || (typeof l === 'object' && l && l.path); }).map(function(l) { return typeof l === 'string' ? l : l.path; }) : [], 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 协议直连 Reasonix(DeepSeek 缓存优先)', 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 rawLocations = toolEvent && toolEvent.locations; var locations = Array.isArray(rawLocations) ? rawLocations.filter(function(l) { return typeof l === 'string' || (typeof l === 'object' && l && l.path); }).map(function(l) { return typeof l === 'string' ? l : l.path; }) : []; 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: '', locations: locations, 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 (locations.length) existing.locations = locations; 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 ''; }).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 = '' + '
    ' + '
    ' + escapeHtml(rt.title || 'ACP Runtime') + '
    ' + '
    ' + '
    ' + '
    模型
    ' + escapeHtml(rt.model || 'deepseek-chat') + '
    ' + '
    ' + '
    ' + '
    Preset
    ' + escapeHtml(rt.preset || 'auto') + '
    ' + '
    ' + '
    ' + '
    API Key
    ' + escapeHtml(keyStatus) + '
    ' + '
    ' + '
    ' + '
    描述
    ' + escapeHtml(rt.description || '') + '
    ' + '
    '; } } 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 ''; }).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 = '
    暂无排队项。
    '; } else { queueList.innerHTML = pageUiState.pageAiQueuedItems.map(function(item, index) { var label = '队列 ' + (index + 1); return '' + '
    ' + '
    ' + escapeHtml(label) + '
    ' + escapeHtml(item.queueId) + '
    ' + '' + '
    '; }).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 '' + '
    ' + '
    ' + '
    ' + '
    ' + escapeHtml(label) + '
    ' + '
    保存到 Hermes profile: ' + escapeHtml(activeProfile) + '
    ' + '
    ' + '' + '
    ' + '' + '
    '; }).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 = '
    ' + escapeHtml(emptyText) + '
    '; } 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 '' + '
    ' + '
    ' + '
    ' + '
    ' + escapeHtml(skill.name) + '
    ' + '
    ' + escapeHtml(sourceText) + '
    ' + '
    ' + (hasDescription ? '
    ' + escapeHtml(description) + '
    ' : '') + '
    ' + '' + '
    '; }).join(''); } } var toolsList = drawer.querySelector('[data-page-ai-tool-list]'); if (toolsList instanceof HTMLElement) { var tools = pageAiNormalizeArray(pageUiState.pageAiTools); if (!tools.length) { toolsList.innerHTML = '
    尚未读取到 mnote tool manifest。
    '; } else { toolsList.innerHTML = tools.map(function(tool) { return '' + '
    ' + '
    ' + '
    ' + escapeHtml(tool.name) + '
    ' + '
    ' + escapeHtml(tool.scope || tool.kind || 'mnote') + ' · ' + escapeHtml(tool.status || 'available') + '
    ' + (tool.unavailableReason ? '
    ' + escapeHtml(tool.unavailableReason) + '
    ' : '') + '
    ' + '' + '
    '; }).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 = '
    打开 Runtime 后会检查 Hermes gateway 与当前 profile。
    '; } else { var gatewayInfo = gatewayHealth.gateway || {}; var profileInfo = gatewayHealth.profile || {}; var suggestionText = pageAiNormalizeArray(gatewayHealth.suggestions).join(';'); gatewayDetail.innerHTML = '' + '
    ' + '
    ' + escapeHtml(profileInfo.name || activeProfile) + '
    ' + '
    model.default: ' + escapeHtml(profileInfo.modelDefault || '未设置') + '
    ' + '
    provider: ' + escapeHtml(profileInfo.provider || '未设置') + ' · API key: ' + escapeHtml(profileInfo.apiKeyConfigured ? '已配置' : '未检测到') + '
    ' + '
    ' + '
    ' + '
    ' + escapeHtml(gatewayInfo.upstream || '未配置 upstream') + '
    ' + '
    gateway: ' + escapeHtml(gatewayInfo.status || 'unknown') + (gatewayInfo.httpStatus ? ' · HTTP ' + escapeHtml(gatewayInfo.httpStatus) : '') + '
    ' + (suggestionText ? '
    ' + escapeHtml(suggestionText) + '
    ' : '') + '
    '; } } 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 = '' + ''; 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 ''; }).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 = '
    尚未产生历史会话。
    '; 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 '' + '
    ' + '' + '
    ' + '' + '' + '' + '
    ' + '
    '; }).join(''); return; } if (!pageUiState.pageAiMessages.length) { conversation.innerHTML = '
    围绕当前页面提问,我会优先使用当前页内容、页面结构和已保存设置。
    '; 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 locationRows = Array.isArray(item.locations) && item.locations.length ? '
    位置:' + item.locations.map(function(loc, idx) { return '' + '' + escapeHtml(loc) + '' + '' + ''; }).join('') + '
    ' : ''; var detailRows = [ locationRows, item.argsSummary ? '
    参数 ' + escapeHtml(item.argsSummary) + '
    ' : '', item.resultSummary ? '
    结果 ' + escapeHtml(item.resultSummary) + '
    ' : '', item.changedFiles && item.changedFiles.length ? '
    ' + escapeHtml(pageAiFormatChangedFiles(item.changedFiles)) + '
    ' : '', (item.traceId || item.auditId) ? '
    ' + escapeHtml([item.traceId, item.auditId].filter(Boolean).join(' · ')) + '
    ' : '' ].filter(Boolean).join(''); return '' + '
    ' + '
    ' + escapeHtml(roleLabel) + '
    ' + '
    ' + '
    ' + '' + '' + escapeHtml(item.toolName || item.content || 'tool') + '' + '' + escapeHtml([statusLabel, item.toolKind, item.toolCallId].filter(Boolean).join(' · ')) + '' + '' + (detailRows || '
    暂无参数或结果详情。
    ') + '
    ' + '
    ' + '
    '; } if (item.kind === 'thought') { return '' + '
    ' + '思考过程' + '
    ' + escapeHtml(item.content || '') + '
    ' + '
    '; } if (item.kind === 'permission') { var permissionActions = item.resolved ? '' : ( '
    ' + '' + '' + '
    ' ); return '' + '
    ' + '
    权限
    ' + '
    ' + '' + escapeHtml(item.toolName || 'session/request_permission') + '
    ' + '' + escapeHtml(item.argsSummary || item.content || '') + '' + permissionActions + '
    ' + '
    '; } if (item.kind === 'plan') { var planEntries = Array.isArray(item.entries) ? item.entries : []; var listHtml = planEntries.map(function(entry, idx) { return '
  • ' + escapeHtml(String(entry || '')) + '
  • '; }).join(''); return '' + '
    ' + '
    ' + '执行计划 · ' + planEntries.length + ' 步' + '
    ' + '
      ' + listHtml + '
    ' + '
    ' + '
    ' + '
    '; } var streamingAttr = item.streaming ? ' data-page-ai-streaming="true"' : ''; return '' + '
    ' + '
    ' + escapeHtml(roleLabel) + '
    ' + '
    ' + escapeHtml(item.content || '') + '
    ' + '
    '; }).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 === 'session.info.updated') { try { var infoPayload = JSON.parse(payloadText || 'null') || {}; var newTitle = String(infoPayload.title || '').trim(); if (newTitle) { var sessionForTitle = pageAiCurrentSession(); if (sessionForTitle) { sessionForTitle.title = newTitle; sessionForTitle.updatedAt = Date.now(); pageAiPersistSessions(); renderPageAiControls(); } } } catch (_) {} } if (eventName === 'plan.updated') { try { var planPayload = JSON.parse(payloadText || 'null') || {}; var planEntries = Array.isArray(planPayload.entries) ? planPayload.entries : []; if (planEntries.length) { var planMsgs = pageUiState.pageAiMessages; var existingPlan = planMsgs.length > 0 && planMsgs[planMsgs.length - 1].kind === 'plan' ? planMsgs[planMsgs.length - 1] : null; if (existingPlan) { existingPlan.entries = planEntries; existingPlan.updatedAt = Date.now(); } else { planMsgs.push({ role: 'system', kind: 'plan', entries: planEntries, createdAt: Date.now(), updatedAt: Date.now() }); } pageAiSyncCurrentSessionMessages(); pageAiPersistSessions(); renderPageAiConversation(); } } catch (_) {} } 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 = '' + ''; 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 '
    ' + escapeHtml(message) + '
    '; } 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 '' + '
    ' + '
    ' + escapeHtml(title) + '
    ' + (path ? '
    ' + escapeHtml(path) + '
    ' : '') + (snippet ? '
    ' + escapeHtml(snippet) + '
    ' : '') + '
    '; }).join(''); } return rows.slice(0, 16).map(function(item) { var tag = searchText(item && item.tag) || 'untagged'; var count = Number(item && item.count || 0); return '' + '
    ' + '
    #' + escapeHtml(tag) + '
    ' + '
    ' + count + ' 个页面
    ' + '
    '; }).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 = '' + '字数 ' + Number(stats.wordCount || 0) + '' + '字符 ' + Number(stats.characterCount || 0) + '' + '块数 ' + Number(stats.blockCount || 0) + '' + '待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + ''; } 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 localFileOpenPathFromHref(href) { try { var url = new URL(String(href || ''), window.location.origin); if (url.pathname !== '/api/local-folder/files/open') return ''; return String(url.searchParams.get('path') || '').trim(); } catch (_) { return ''; } } function fileNameFromPath(path) { var value = String(path || '').trim(); return value.indexOf('/') >= 0 ? value.split('/').pop() : value; } 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') || 'view' }); } 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 localFilePath = localFileOpenPathFromHref(rawHref); var fileName = params.get('fileName') || fileNameFromPath(localFilePath) || (link ? link.textContent : '') || '未命名附件'; var fileType = params.get('fileType') || inferOnlyOfficeFileType(fileName, ''); var assetId = params.get('assetId') || (link ? link.getAttribute('data-asset-id') : '') || (localFilePath ? 'local-file:' + localFilePath : ''); 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: 'view' }); } 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 localFilePath = localFileOpenPathFromHref(href); var fileName = params.get('fileName') || fileNameFromPath(localFilePath) || link.textContent || ''; var className = link.getAttribute('class') || ''; var shouldEnhance = isOnlyOfficeAttachmentHref(href) || className.indexOf('mnote-uploaded-attachment-row') >= 0 || isOfficeFileName(fileName) || Boolean(localFilePath && (isPdfAttachmentFileName(fileName) || isCodeAttachmentFileName(fileName))); if (!shouldEnhance) return; link.setAttribute('data-mnote-attachment-link', 'true'); var assetId = params.get('assetId') || link.getAttribute('data-asset-id') || (localFilePath ? 'local-file:' + localFilePath : ''); 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') || 'view' })); } 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 = '' + '' + ''; 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; var localFilePath = localFilePathFromAssetId(detail.assetId); if (localFilePath) { var localFileName = localFilePath.split('/').pop() || localFilePath; var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail.documentId || currentDocumentId() || '').trim(), detail.assetId, 'view'); void openLocalResourceInActiveTab({ path: localFilePath, title: detail.fileName || localFileName, kind: localOfficeUrl ? 'office' : fileTreeIconKindForFileName(detail.fileName || localFileName), assetId: detail.assetId, documentId: detail.documentId || currentDocumentId() || '', workspaceId: detail.workspaceId || resolveWorkspaceId(document.body) || '', href: buildLocalFileOpenUrl(localFilePath, false), officeUrl: localOfficeUrl }).then(function(opened) { if (!opened) window.open(localOfficeUrl || buildLocalFileOpenUrl(localFilePath, false) || detail.href, '_blank', 'noopener,noreferrer'); }); return; } if (isPdfAttachmentFileName(detail.fileName)) { void openPdfEditorAttachment(detail); return; } if (isCodeAttachmentFileName(detail.fileName)) { void openCodeEditorAttachment(detail); return; } var fileType = String(detail.fileType || '').trim(); if (fileType && typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') { void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ objectIdentity: 'resource:onlyoffice:' + (detail.documentId || '') + ':' + (detail.assetId || ''), assetId: detail.assetId || '', title: detail.fileName || '附件', fileName: detail.fileName || '附件', kind: 'office', officeUrl: detail.href, documentId: detail.documentId || '', workspaceId: detail.workspaceId || '' }); return; } window.open(detail.href, '_blank', 'noopener,noreferrer'); } function openEditorAttachmentNewWindow(detail, mode) { if (!detail) return; var requestedMode = mode === 'edit' ? 'edit' : 'view'; var localFilePath = localFilePathFromAssetId(detail.assetId); if (localFilePath) { var localFileName = localFilePath.split('/').pop() || localFilePath; var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail.documentId || currentDocumentId() || '').trim(), detail.assetId, requestedMode); var localFileUrl = buildLocalFileOpenUrl(localFilePath, false); window.open(localOfficeUrl || localFileUrl || detail.href, '_blank', 'noopener,noreferrer'); return; } var href = detail.href || detail.fileUrl; if (detail.fileType && isOnlyOfficeAttachmentHref(href)) { try { var url = new URL(href, window.location.origin); url.searchParams.set('mode', requestedMode); href = url.toString(); } catch (_) {} } else if (detail.fileType) { href = buildOnlyOfficeOpenUrl({ fileUrl: detail.fileUrl || href || '', fileName: detail.fileName || '未命名附件', fileType: detail.fileType, assetId: detail.assetId || '', documentId: detail.documentId || currentDocumentId() || '', userId: '', mode: requestedMode }); } window.open(href, '_blank', 'noopener,noreferrer'); } async function openEditorAttachmentEditTab(detail) { if (!detail) return false; var localFilePath = localFilePathFromAssetId(detail.assetId); if (localFilePath) { var localFileName = localFilePath.split('/').pop() || localFilePath; var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail.documentId || currentDocumentId() || '').trim(), detail.assetId, 'edit'); if (localOfficeUrl) { var didOpenLocalEditTab = await openLocalResourceInActiveTab({ path: localFilePath, title: detail.fileName || localFileName, kind: 'office', assetId: detail.assetId, documentId: detail.documentId || currentDocumentId() || '', workspaceId: detail.workspaceId || resolveWorkspaceId(document.body) || '', href: buildLocalFileOpenUrl(localFilePath, false), officeUrl: localOfficeUrl }); if (!didOpenLocalEditTab) window.open(localOfficeUrl, '_blank', 'noopener,noreferrer'); return didOpenLocalEditTab; } } var href = detail.href || detail.fileUrl; if (detail.fileType && isOnlyOfficeAttachmentHref(href)) { try { var url = new URL(href, window.location.origin); url.searchParams.set('mode', 'edit'); href = url.toString(); } catch (_) {} } else if (detail.fileType) { href = buildOnlyOfficeOpenUrl({ fileUrl: detail.fileUrl || href || '', fileName: detail.fileName || '未命名附件', fileType: detail.fileType, assetId: detail.assetId || '', documentId: detail.documentId || currentDocumentId() || '', userId: '', mode: 'edit' }); } if (detail.fileType && typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') { var didOpenEditTab = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({ objectIdentity: 'resource:onlyoffice:' + (detail.documentId || '') + ':' + (detail.assetId || ''), assetId: detail.assetId || '', title: detail.fileName || '附件', fileName: detail.fileName || '附件', kind: 'office', officeUrl: href, documentId: detail.documentId || '', workspaceId: detail.workspaceId || '' }); if (!didOpenEditTab && href) window.open(href, '_blank', 'noopener,noreferrer'); return didOpenEditTab; } if (href) window.open(href, '_blank', 'noopener,noreferrer'); return false; } async function resolveEditorAttachmentUrl(detail) { var assetId = String(detail && detail.assetId || '').trim(); var localFilePath = localFilePathFromAssetId(assetId); if (localFilePath) { var localUrl = buildLocalFileOpenUrl(localFilePath, false); if (localUrl) { return { url: localUrl, asset: { file_name: String(detail && detail.fileName || '').trim(), fileSize: String(detail && detail.fileSize || '').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; var localFilePath = localFilePathFromAssetId(detail.assetId); if (localFilePath) { var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true); if (localDownloadUrl) { triggerBrowserDownload(localDownloadUrl); 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 localRelativePath = String(detail.localRelativePath || '').trim(); if (localRelativePath) { var localPathDownloadUrl = buildLocalFileOpenUrl(localRelativePath, true); if (localPathDownloadUrl) { triggerBrowserDownload(localPathDownloadUrl); return; } } var target = detail.fileUrl || detail.href; if (!target) return; window.open(target, '_blank', 'noopener,noreferrer'); } function openEditorAttachmentMenu(link, trigger, point) { var detail = detailFromEditorAttachmentLink(link); var rect = trigger && trigger.getBoundingClientRect ? trigger.getBoundingClientRect() : link.getBoundingClientRect(); var x = point && typeof point.x === 'number' ? point.x : rect.right; var y = point && typeof point.y === 'number' ? point.y : rect.bottom + 4; openTreeContextMenu('attachment', detail, x, y, 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 }); function interceptEditorAttachmentLink(event) { var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row'); if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return; event.preventDefault(); event.stopPropagation(); if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation(); openEditorAttachmentLink(editorAttachmentLink); } function suppressEditorAttachmentLinkDefault(event) { var editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row'); if (!(editorAttachmentLink instanceof HTMLAnchorElement)) return; event.preventDefault(); event.stopPropagation(); if (typeof event.stopImmediatePropagation === 'function') event.stopImmediatePropagation(); } window.addEventListener('mousedown', suppressEditorAttachmentLinkDefault, true); window.addEventListener('click', interceptEditorAttachmentLink, 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 pageAiOpenLocation = closestAction(e.target, '[data-page-ai-open-location]'); if (pageAiOpenLocation) { e.preventDefault(); var loc = String(pageAiOpenLocation.getAttribute('data-page-ai-open-location') || '').trim(); if (loc) { openLocalResourceInActiveTab({ path: loc }).then(function(opened) { if (!opened) { var href = buildLocalFileOpenUrl(loc, false); if (href) window.open(href, '_blank', 'noopener,noreferrer'); } }); } 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(); if (rowId && !sidebarFileTreeSelection.selectedRowIds.has(rowId)) { 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 pageRow = closestAction(e.target, '.tree-row[data-shell-mode="page"]'); var btn = closestAction(e.target, '[data-rust-action]'); if (!btn && !pageRow) return; var nodeId = (btn && btn.getAttribute('data-node-id')) || (pageRow && pageRow.getAttribute('data-node-id')) || ''; var action = btn ? btn.getAttribute('data-rust-action') : 'open'; if (action === 'toggle') { var row = btn.closest('.tree-row'); if (!row) return; toggleChildren(row, btn); e.preventDefault(); } else if (action === 'open') { var openTrigger = btn || pageRow; if (openTrigger && openTrigger.getAttribute('data-page-openable') === 'false') { e.preventDefault(); return; } var workspaceId = resolveWorkspaceId(openTrigger); 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 editorAttachmentLink = closestAction(event.target, '.editor-surface .ProseMirror a[data-mnote-attachment-link="true"], .editor-surface .ProseMirror a.mnote-uploaded-attachment-row'); if (editorAttachmentLink instanceof HTMLAnchorElement) { event.preventDefault(); event.stopPropagation(); enhanceEditorAttachmentLink(editorAttachmentLink); openEditorAttachmentMenu(editorAttachmentLink, editorAttachmentLink, { x: event.clientX, y: event.clientY }); return; } 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, targetRelativePath: targetRow && (targetRow.getAttribute('data-row-kind') === 'folder' || targetRow.getAttribute('data-row-kind') === 'directory') ? fileTreeRowLocalRelativePath(targetRow) : 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 { void Promise.resolve(ensureFileTreeWritableTarget('drop', targetRow, rowIds, event.altKey === true || event.ctrlKey === true || event.metaKey === true)).then(function(writable) { if (!writable) return; 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 || {}; var previousDocumentId = detail.previousDocumentId || (detail.payload && detail.payload.result && detail.payload.result.previousDocumentId) || ''; if (previousDocumentId && previousDocumentId !== detail.documentId) { removeDocumentRowForMode('page', previousDocumentId); removeDocumentRowForMode('filetree', previousDocumentId); if (currentDocumentId() === previousDocumentId) { navigateToDocument(detail.documentId, detail.workspaceId || currentWorkspaceId(), { treeView: activeSidebarTreeMode() }); } } updateTitleEverywhere(detail.documentId, detail.title); if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot(); 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(); var result = detail.result || {}; if (action === 'create') { applyCreatedDocumentLocally(result, body.parentId || null, body.title || '新页面'); if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot(); return; } if (action === 'rename' && body.documentId && body.title) { var newDocumentId = commandDocumentId(result, body.documentId); if (newDocumentId && newDocumentId !== body.documentId) { removeDocumentRowForMode('page', body.documentId); removeDocumentRowForMode('filetree', body.documentId); if (currentDocumentId() === body.documentId) { navigateToDocument(newDocumentId, body.workspaceId || currentWorkspaceId(), { treeView: activeSidebarTreeMode() }); } } else { updateTitleEverywhere(body.documentId, body.title); } if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot(); 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'); } if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot(); 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'); } if (localCommandNeedsProjectionRefresh(action, result)) void refreshLocalFolderSidebarSnapshot(); } }); 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(); window.addEventListener('mnote:primary-document-activated', function(event) { var detail = event && event.detail ? event.detail : {}; selectSidebarFileTreeDocument(detail.documentId, { scrollIntoView: false }); }); 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 liveDeltaNeedsResync(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 === 'resync_required') return true; if (data && data.requiresResync === true) return true; return false; } 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 }); if (liveDeltaNeedsResync(payload) && 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(); if (bootstrap.disabled === true || bootstrap.transport === 'disabled') { applyTransport('disabled'); applyStatus('disabled'); return; } 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! { /// ///
    ...
    ///
    /// } /// ``` #[component] pub fn PageLayout( children: Children, current_nav: &'static str, /// 侧栏页面树 HTML(可选),由路由 handler 渲染 #[prop(optional)] sidebar_tree_html: Option, /// 工作区名称(可选),显示在侧栏顶部 #[prop(optional)] workspace_name: Option, /// workspace shell 侧栏 sections HTML(可选),由 projection 渲染 #[prop(optional)] workspace_sidebar_html: Option, /// 顶栏当前页面标题(可选) #[prop(optional)] topbar_title: Option, /// 是否显示管理员授权入口 #[prop(optional)] show_admin_access_policy: bool, /// 是否启用树实时流 #[prop(optional, default = true)] enable_tree_live: 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", "disabled": !enable_tree_live, "transport": if enable_tree_live { "convex-command-log-ws" } else { "disabled" }, "workspaceId": null, "rootIds": [], "initialRevision": null, "endpoint": "/api/tree/events", "wsEndpoint": "/api/realtime/ws", "views": ["page-tree", "file-tree"] }) .to_string(); view! {
    "全网公开"
    {children()}
    } } #[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("^思维导图")); 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("value.indexOf('local:asset:') === 0 ? value.slice('local:asset:'.length)")); 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(officeUrl,")); 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("if (sourceKind && !payload.sourceKind) payload.sourceKind = sourceKind")); assert!( SIDEBAR_TREE_JS.contains("if (rootUri && !payload.rootUri) payload.rootUri = rootUri") ); 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 事件不能继续展示假审批按钮" ); assert!( SIDEBAR_TREE_JS.contains("session.info.updated"), "ACP SessionInfoUpdate 事件应通过 session.info.updated SSE 转发到前端" ); assert!( SIDEBAR_TREE_JS.contains("plan.updated"), "ACP PlanUpdate 事件应通过 plan.updated SSE 转发到前端" ); assert!( SIDEBAR_TREE_JS.contains("data-page-ai-plan"), "plan 消息应渲染为 data-page-ai-plan 标记的轻量系统状态面板" ); assert!( SIDEBAR_TREE_JS.contains("执行计划 · "), "plan 面板标题应显示执行计划和步数" ); } #[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("function currentWorkspaceId()")); assert!(SIDEBAR_TREE_JS.contains("var rootUri = currentRootUri();")); assert!(SIDEBAR_TREE_JS.contains("/api/tree/local-folder-watch")); assert!(SIDEBAR_TREE_JS.contains("/api/tree/projections/sidebar")); assert!(SIDEBAR_TREE_JS.contains("/api/tree/projections/file")); assert!(SIDEBAR_TREE_JS .contains("renderSidebarSnapshot(sidebarPayload.result || sidebarPayload)")); assert!(SIDEBAR_TREE_JS.contains("renderFileProjection(filePayload.result || filePayload)")); assert!(SIDEBAR_TREE_JS.contains("data-mnote-local-folder-watch-applied")); assert!(!SIDEBAR_TREE_JS .contains("fetch(window.location.href, { headers: { accept: 'text/html' } })")); assert!(!SIDEBAR_TREE_JS .contains("replaceSidebarTreeFromDocument(nextDocument, 'sidebar-tree-root')")); 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("buildLocalOnlyOfficeOpenUrl")); assert!(SIDEBAR_TREE_JS.contains("localOfficeUrl")); assert!(SIDEBAR_TREE_JS.contains("fileTreeIconKindForFileName(title)")); assert!(SIDEBAR_TREE_JS.contains("refreshLocalFolderSidebarSnapshot")); assert!(SIDEBAR_TREE_JS.contains("wolai:local-assets-changed")); } #[test] fn sidebar_runtime_routes_local_mindmap_and_office_assets() { assert!(SIDEBAR_TREE_JS.contains("withLocalMindmapSourceParams")); assert!(SIDEBAR_TREE_JS.contains("url.searchParams.set('sourceKind', 'local_folder')")); assert!(SIDEBAR_TREE_JS.contains("url.searchParams.set('rootUri', rootUri)")); assert!(SIDEBAR_TREE_JS.contains("if (!parentRow && currentSourceKind() === 'local_folder' && objectKind === 'mindmap') return false;")); assert!(SIDEBAR_TREE_JS.contains("if (!appended && currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();")); assert!(SIDEBAR_TREE_JS .contains("data-mnote-last-mindmap-asset-open-mode', 'local-mindmap-object-shell'")); assert!(SIDEBAR_TREE_JS.contains("buildLocalOnlyOfficeOpenUrl")); assert!(SIDEBAR_TREE_JS.contains("isLocalAsset ? onlyOfficeUrl")); } #[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("function createFileTreeFolder")); assert!(SIDEBAR_TREE_JS.contains("action: 'create_folder'")); assert!(SIDEBAR_TREE_JS.contains("function fileTreeMenuTargetParentId")); assert!(SIDEBAR_TREE_JS.contains("if (action === 'new-folder')")); assert!(SIDEBAR_TREE_JS.contains("fileTreeCopyPath(detail, trigger)")); assert!(SIDEBAR_TREE_JS .contains("if (detail.contextKind === 'filetree' && action === 'download')")); assert!(SIDEBAR_TREE_JS.contains("downloadSelectedFileTreeAssetRows(detail, trigger)")); assert!(SIDEBAR_TREE_JS.contains("function selectedSidebarFileTreeRowsForDownload")); assert!(SIDEBAR_TREE_JS.contains("function selectedSidebarFileTreeAssetRowsForDownload")); assert!(SIDEBAR_TREE_JS.contains("data-local-relative-path")); assert!(SIDEBAR_TREE_JS.contains("targetRelativePath")); assert!(SIDEBAR_TREE_JS.contains( "recordFileTreeAction(downloads.length > 1 ? 'bulk-download' : 'download', primary)" )); assert!(SIDEBAR_TREE_JS.contains("data-mnote-filetree-download-count")); assert!(SIDEBAR_TREE_JS.contains("function triggerBrowserDownload")); assert!(SIDEBAR_TREE_JS.contains("openEditorAttachmentMenu(editorAttachmentLink, editorAttachmentLink, { x: event.clientX, y: event.clientY });")); assert!(SIDEBAR_TREE_JS.contains("var localFilePath = localFilePathFromAssetId(assetId);")); assert!(SIDEBAR_TREE_JS.contains("attachmentMetaCache[assetId] = localMeta;")); assert!(SIDEBAR_TREE_JS.contains( "var pageRow = closestAction(e.target, '.tree-row[data-shell-mode=\"page\"]');" )); assert!(SIDEBAR_TREE_JS .contains("var action = btn ? btn.getAttribute('data-rust-action') : 'open';")); assert!(SIDEBAR_TREE_JS.contains("var openTrigger = btn || pageRow;")); assert!(SIDEBAR_TREE_JS.contains("var workspaceId = resolveWorkspaceId(openTrigger);")); 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)")); 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() { let delete_trash_start = SIDEBAR_TREE_JS .find("if (action === 'delete-trash'") .expect("delete-trash branch"); let delete_trash_end = SIDEBAR_TREE_JS[delete_trash_start..] .find("function appendTreeContextMenuButton") .map(|offset| delete_trash_start + offset) .expect("delete-trash branch end"); let delete_trash_branch = &SIDEBAR_TREE_JS[delete_trash_start..delete_trash_end]; assert!(delete_trash_branch.contains("action: 'archive'")); assert!(!delete_trash_branch.contains("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_filetree_blocks_readonly_paste_and_drop_with_action_status() { assert!(SIDEBAR_TREE_JS.contains("function blockReadonlyFileTreeAction")); assert!(SIDEBAR_TREE_JS.contains("data-mnote-filetree-readonly-blocked")); assert!(SIDEBAR_TREE_JS.contains("data-mnote-filetree-readonly-message")); assert!(SIDEBAR_TREE_JS.contains("recordFileTreeActionStatus('blocked'")); assert!(SIDEBAR_TREE_JS.contains("/api/tree/filetree/drop-preflight")); assert!(SIDEBAR_TREE_JS.contains("ensureFileTreeWritableTarget('paste'")); assert!(SIDEBAR_TREE_JS.contains("ensureFileTreeWritableTarget('drop'")); } #[test] fn sidebar_filetree_local_table_bulk_delete_uses_tree_command() { let table_loop_start = SIDEBAR_TREE_JS .find("for (var t = 0; t < plan.tableRows.length; t += 1)") .expect("table bulk delete loop"); let table_loop_end = SIDEBAR_TREE_JS[table_loop_start..] .find("sidebarFileTreeSelection.selectedRowIds") .map(|offset| table_loop_start + offset) .expect("table bulk delete loop end"); let table_loop = &SIDEBAR_TREE_JS[table_loop_start..table_loop_end]; assert!(table_loop.contains("currentSourceKind() === 'local_folder'")); assert!(table_loop.contains("dispatchTreeCommand(trigger || tableRow")); assert!(table_loop.contains("action: 'archive'")); assert!(table_loop.contains("documentId: tableId")); assert!(table_loop.contains("/api/tables/")); } #[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_office_assets_through_resource_shell() { assert!(SIDEBAR_TREE_JS.contains("function buildOnlyOfficeOpenPath")); assert!(SIDEBAR_TREE_JS.contains( "return '/office/' + encodeURIComponent(input.documentId) + '/' + encodeURIComponent(input.assetId)" )); assert!(SIDEBAR_TREE_JS.contains("return '/onlyoffice?' + params.toString();")); assert!(SIDEBAR_TREE_JS.contains("target.searchParams.set('mode', input.mode || 'view');")); assert!(SIDEBAR_TREE_JS.contains("params.set('mode', input.mode || 'view');")); assert!(SIDEBAR_TREE_JS.contains( "{ action: 'open-edit-mode', icon: 'edit_note', label: '使用编辑模式打开' }" )); assert!(SIDEBAR_TREE_JS .contains("{ action: 'open-edit-mode', icon: 'edit_note', label: '弹窗编辑' }")); assert!(SIDEBAR_TREE_JS .contains("{ action: 'new-window-edit', icon: 'open_in_new', label: '新窗口编辑' }")); assert!(SIDEBAR_TREE_JS.contains("function withOfficeEditModeGuard(callback)")); assert!(SIDEBAR_TREE_JS.contains("data-mnote-last-office-edit-mode-requested")); assert!(SIDEBAR_TREE_JS.contains("function openEditorAttachmentEditTab(detail)")); assert!(SIDEBAR_TREE_JS.contains("void openEditorAttachmentEditTab(detail);")); assert!(SIDEBAR_TREE_JS.contains("buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail.documentId || currentDocumentId() || '').trim(), detail.assetId, 'edit')")); assert!(SIDEBAR_TREE_JS.contains("openEditorAttachmentNewWindow(detail, 'edit')")); assert!(SIDEBAR_TREE_JS.contains("forceEditMode ? 'edit' : 'view'")); assert!(SIDEBAR_TREE_JS.contains("url.searchParams.set('mode', requestedMode);")); assert!(SIDEBAR_TREE_JS.contains("mode: requestedMode")); } #[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("var localFilePath = localFilePathFromAssetId(assetId)")); assert!(SIDEBAR_TREE_JS .contains("var localDownloadUrl = buildLocalFileOpenUrl(localFilePath, true)")); 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("liveDeltaNeedsResync(payload)")); assert!(TREE_LIVE_CONTROLLER_JS.contains("op === 'resync_required'")); assert!(!TREE_LIVE_CONTROLLER_JS.contains("resyncEndpoint")); assert!(!TREE_LIVE_CONTROLLER_JS.contains("tree:resync-requested")); assert!(!TREE_LIVE_CONTROLLER_JS.contains("Delta indicates something changed")); } }