9652 lines
445 KiB
JavaScript
9652 lines
445 KiB
JavaScript
import { createSidebarWorkspaceRuntime } from './sidebar-workspace-runtime.js';
|
||
|
||
(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 sidebarShellRuntimeFunction(name) {
|
||
var runtime = window.__mnoteSidebarShellRuntime;
|
||
return runtime && typeof runtime[name] === 'function' ? runtime[name] : null;
|
||
}
|
||
|
||
function toggleWorkspaceSidebar(trigger) {
|
||
var runtimeFn = sidebarShellRuntimeFunction('toggleWorkspaceSidebar');
|
||
if (runtimeFn) return runtimeFn(trigger);
|
||
}
|
||
|
||
function installWorkspaceSidebarResizer() {
|
||
var runtimeFn = sidebarShellRuntimeFunction('installWorkspaceSidebarResizer');
|
||
if (runtimeFn) return runtimeFn();
|
||
}
|
||
|
||
function escapeHtml(value) {
|
||
return String(value == null ? '' : value)
|
||
.replace(/&/g, '&')
|
||
.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\/([^\/]+)\/([^\/]+)/);
|
||
if (match) return decodeURIComponent(match[1]);
|
||
var params = new URLSearchParams(window.location.search);
|
||
var fromQuery = (params.get('documentId') || params.get('pageId') || '').trim();
|
||
if (fromQuery) return fromQuery;
|
||
var activePane = document.querySelector('.document-pane[data-pane-role="primary"][data-pane-document-id], .document-pane[data-pane-visible="true"][data-pane-document-id]');
|
||
if (activePane instanceof HTMLElement) {
|
||
var paneDocumentId = (activePane.getAttribute('data-pane-document-id') || '').trim();
|
||
if (paneDocumentId) return paneDocumentId;
|
||
}
|
||
var shell = document.querySelector('.document-shell[data-document-id]');
|
||
if (shell instanceof HTMLElement) return (shell.getAttribute('data-document-id') || '').trim();
|
||
return '';
|
||
}
|
||
|
||
function localFolderSelfChangeSuppressions() {
|
||
window.__mnoteLocalFolderSelfChangeSuppressions = window.__mnoteLocalFolderSelfChangeSuppressions || new Map();
|
||
return window.__mnoteLocalFolderSelfChangeSuppressions;
|
||
}
|
||
|
||
function persistLocalFolderSelfChangeSuppression(documentId, expiresAt) {
|
||
var doc = String(documentId || '').trim();
|
||
if (!doc) return;
|
||
localFolderSelfChangeSuppressions().set(doc, expiresAt);
|
||
try {
|
||
var key = 'mnote.localFolder.selfChangeSuppressions.v1';
|
||
var existing = JSON.parse(window.sessionStorage.getItem(key) || '{}');
|
||
existing[doc] = expiresAt;
|
||
window.sessionStorage.setItem(key, JSON.stringify(existing));
|
||
} catch (_) {}
|
||
}
|
||
|
||
var EDITOR_UPLOAD_ROOT_SELECTOR = '[data-editor-host-kind="leptos_tiptap_island"], [data-editor-host-kind="leptos_tiptap_resource"]';
|
||
|
||
function editorUploadRootFromElement(target) {
|
||
if (!(target instanceof Element)) return null;
|
||
var root = target.closest(EDITOR_UPLOAD_ROOT_SELECTOR);
|
||
return root instanceof HTMLElement ? root : null;
|
||
}
|
||
|
||
function rememberEditorUploadRootFromTarget(target) {
|
||
var root = editorUploadRootFromElement(target);
|
||
if (root instanceof HTMLElement) {
|
||
window.__mnoteLastEditorUploadRoot = root;
|
||
return root;
|
||
}
|
||
return null;
|
||
}
|
||
|
||
document.addEventListener('pointerdown', function(event) {
|
||
rememberEditorUploadRootFromTarget(event.target);
|
||
}, true);
|
||
|
||
document.addEventListener('focusin', function(event) {
|
||
rememberEditorUploadRootFromTarget(event.target);
|
||
}, true);
|
||
|
||
function currentFileTreeActiveRowId() {
|
||
var explicitRowId = new URL(window.location.href).searchParams.get('restoreFocusRowId') || '';
|
||
if (explicitRowId) return explicitRowId;
|
||
var mindmapMatch = window.location.pathname.match(/^\/mindmap\/([^\/]+)\/([^\/]+)/);
|
||
if (mindmapMatch) return 'asset:' + decodeURIComponent(mindmapMatch[2]);
|
||
var documentId = currentDocumentId();
|
||
return documentId ? 'doc:' + documentId : '';
|
||
}
|
||
|
||
function currentPageAggregate() {
|
||
return parseJsonScript('__MNOTE_PAGE_AGGREGATE__') || {};
|
||
}
|
||
|
||
function textFromUnknown(value) {
|
||
if (value == null) return '';
|
||
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') return String(value);
|
||
if (Array.isArray(value)) return value.map(textFromUnknown).filter(Boolean).join(' ');
|
||
if (typeof value !== 'object') return '';
|
||
var parts = [];
|
||
['text', 'title', 'content', 'children', 'blocks', 'body'].forEach(function(key) {
|
||
if (Object.prototype.hasOwnProperty.call(value, key)) {
|
||
var text = textFromUnknown(value[key]);
|
||
if (text) parts.push(text);
|
||
}
|
||
});
|
||
return parts.join(' ');
|
||
}
|
||
|
||
function readLocalEditorBlocks() {
|
||
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
|
||
if (!(editor instanceof HTMLElement)) return [];
|
||
return Array.from(editor.children).filter(function(node) {
|
||
return node instanceof HTMLElement;
|
||
}).map(function(node, index) {
|
||
var tag = String(node.tagName || '').toUpperCase();
|
||
var headingMatch = tag.match(/^H([1-6])$/);
|
||
var type = headingMatch ? 'heading' : 'paragraph';
|
||
var text = searchText(node.textContent || '');
|
||
var id = searchText(node.getAttribute('data-id') || node.id || 'local-block-' + index);
|
||
var props = headingMatch ? { level: Number(headingMatch[1]) } : {};
|
||
return { id: id, type: type, props: props, content: text };
|
||
}).filter(function(block) {
|
||
return block.content || block.type === 'heading';
|
||
});
|
||
}
|
||
|
||
function buildPageAiLocalSubtree(blocks, title) {
|
||
var documentId = currentDocumentId() || 'current-page';
|
||
var rootNodeId = 'page:' + documentId;
|
||
var headingCounters = [0, 0, 0, 0, 0, 0];
|
||
var headingStack = [];
|
||
var nodes = [{
|
||
id: rootNodeId,
|
||
nodeId: rootNodeId,
|
||
nodeType: 'page',
|
||
blockId: null,
|
||
blockType: 'page',
|
||
title: title || '',
|
||
parentNodeId: null,
|
||
headingLevel: null
|
||
}];
|
||
var outline = [];
|
||
var evidence = [];
|
||
blocks.forEach(function(block, index) {
|
||
var level = block.type === 'heading' ? Math.max(1, Math.min(6, Number(block.props && block.props.level || 1))) : null;
|
||
if (level != null) {
|
||
while (headingStack.length && headingStack[headingStack.length - 1].level >= level) headingStack.pop();
|
||
}
|
||
var parentNodeId = headingStack.length ? headingStack[headingStack.length - 1].nodeId : rootNodeId;
|
||
var nodeId = 'local-node:' + String(block.id || index);
|
||
var titleText = block.content || (block.type === 'heading' ? '未命名标题' : '');
|
||
nodes.push({
|
||
id: nodeId,
|
||
nodeId: nodeId,
|
||
nodeType: 'block',
|
||
blockId: block.id,
|
||
blockType: block.type,
|
||
title: titleText,
|
||
parentNodeId: parentNodeId,
|
||
headingLevel: level
|
||
});
|
||
if (level != null) {
|
||
headingCounters[level - 1] += 1;
|
||
for (var i = level; i < headingCounters.length; i += 1) headingCounters[i] = 0;
|
||
var numbering = headingCounters.slice(0, level).filter(function(count) { return count > 0; }).join('.');
|
||
outline.push({
|
||
id: 'local-outline:' + String(block.id || index),
|
||
nodeId: nodeId,
|
||
anchorBlockId: block.id,
|
||
level: level,
|
||
title: titleText,
|
||
numbering: numbering
|
||
});
|
||
headingStack.push({ level: level, nodeId: nodeId });
|
||
}
|
||
if (titleText) {
|
||
evidence.push({
|
||
id: 'local-evidence:' + String(block.id || index),
|
||
nodeId: nodeId,
|
||
anchorBlockId: block.id,
|
||
text: titleText,
|
||
kind: block.type
|
||
});
|
||
}
|
||
});
|
||
return {
|
||
projectionId: 'local-editor-dom:' + documentId,
|
||
rootNode: {
|
||
id: rootNodeId,
|
||
documentId: documentId,
|
||
title: title || '',
|
||
nodeType: 'page'
|
||
},
|
||
subtree: {
|
||
rootNodeId: rootNodeId,
|
||
nodes: nodes
|
||
},
|
||
outline: outline,
|
||
evidence: evidence,
|
||
stats: {
|
||
nodeCount: nodes.length,
|
||
headingCount: outline.length,
|
||
evidenceCount: evidence.length
|
||
},
|
||
source: 'local'
|
||
};
|
||
}
|
||
|
||
function currentPageAiContextSnapshot() {
|
||
var aggregate = currentPageAggregate();
|
||
var body = aggregate.body || {};
|
||
var serverSubtree = aggregate.tree && aggregate.tree.pageSubtree ? aggregate.tree.pageSubtree : null;
|
||
return {
|
||
aggregate: aggregate,
|
||
body: body,
|
||
subtree: serverSubtree,
|
||
pageSubtreeSource: serverSubtree ? 'server' : 'none'
|
||
};
|
||
}
|
||
|
||
function currentPageAiSelectedText() {
|
||
try {
|
||
var selection = window.getSelection ? window.getSelection() : null;
|
||
return selection ? searchText(selection.toString() || '') : '';
|
||
} catch (_) {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function pageAiProjectionBlocks(aggregate) {
|
||
var blocks = aggregate && aggregate.body && aggregate.body.blockDocument && aggregate.body.blockDocument.blocks;
|
||
return Array.isArray(blocks) ? blocks : [];
|
||
}
|
||
|
||
function pageAiBlockText(block) {
|
||
return searchText(block && (block.text || block.title || block.content) || '');
|
||
}
|
||
|
||
function pageAiSelectedBlockIdsFromSelection() {
|
||
try {
|
||
var selection = window.getSelection ? window.getSelection() : null;
|
||
if (!selection || selection.rangeCount === 0 || searchText(selection.toString() || '') === '') return [];
|
||
var range = selection.getRangeAt(0);
|
||
var editor = document.querySelector('[data-testid="mnote-leptos-tiptap-island-editor-root"] .editor-surface .ProseMirror');
|
||
if (!(editor instanceof HTMLElement)) return [];
|
||
return Array.from(editor.children).filter(function(node) {
|
||
if (!(node instanceof HTMLElement)) return false;
|
||
try {
|
||
return range.intersectsNode(node);
|
||
} catch (_) {
|
||
return false;
|
||
}
|
||
}).map(function(node) {
|
||
return searchText(node.getAttribute('data-id') || node.id || '');
|
||
}).filter(Boolean);
|
||
} catch (_) {
|
||
return [];
|
||
}
|
||
}
|
||
|
||
function pageAiBlocksToPageXml(blocks, aggregate) {
|
||
var revision = aggregate && aggregate.body ? String(aggregate.body.revision || '') : '';
|
||
var pageId = currentDocumentId() || 'current-page';
|
||
var lines = ['<page id="' + escapeHtml(pageId) + '" revision="' + escapeHtml(revision) + '">'];
|
||
blocks.forEach(function(block) {
|
||
var blockId = String(block && (block.blockId || block.id) || '');
|
||
var type = String(block && block.type || 'paragraph');
|
||
var revisionRef = String(block && block.revisionRef || '');
|
||
var level = block && block.attrs && block.attrs.level ? ' level="' + escapeHtml(block.attrs.level) + '"' : '';
|
||
lines.push(' <block id="' + escapeHtml(blockId) + '" type="' + escapeHtml(type) + '" revisionRef="' + escapeHtml(revisionRef) + '"' + level + '>' + escapeHtml(pageAiBlockText(block)) + '</block>');
|
||
});
|
||
lines.push('</page>');
|
||
return lines.join('\n');
|
||
}
|
||
|
||
function buildPageAiContext(contextSnapshot, scope, selectedText) {
|
||
var aggregate = contextSnapshot.aggregate || {};
|
||
var body = aggregate.body || {};
|
||
var allBlocks = pageAiProjectionBlocks(aggregate);
|
||
var selectedBlockIds = scope === 'selection' ? pageAiSelectedBlockIdsFromSelection() : [];
|
||
var selectedSet = {};
|
||
selectedBlockIds.forEach(function(id) { selectedSet[id] = true; });
|
||
var selectedBlocks = selectedBlockIds.length
|
||
? allBlocks.filter(function(block) { return selectedSet[String(block.blockId || block.id || '')]; })
|
||
: [];
|
||
var contextBlocks = selectedBlocks.length ? selectedBlocks : allBlocks.slice(0, 120);
|
||
var truncated = !selectedBlocks.length && allBlocks.length > contextBlocks.length;
|
||
return {
|
||
schema: 'mnote.page_ai_context.v1',
|
||
workspaceId: resolveWorkspaceId(document.body),
|
||
documentId: currentDocumentId(),
|
||
scope: scope,
|
||
revision: body.revision || null,
|
||
conflictDetectionKey: body.conflictDetectionKey || null,
|
||
selectedText: selectedText || '',
|
||
selectedBlockIds: selectedBlockIds,
|
||
allowedTargetBlockIds: selectedBlockIds,
|
||
selectedBlocks: selectedBlocks,
|
||
contextBlocks: contextBlocks,
|
||
pageText: contextBlocks.map(pageAiBlockText).filter(Boolean).join('\n'),
|
||
pageXml: pageAiBlocksToPageXml(contextBlocks, aggregate),
|
||
truncated: truncated,
|
||
warnings: truncated ? [{ code: 'page_ai_context_truncated', message: '页面 AI context 已按前 120 个块裁剪' }] : []
|
||
};
|
||
}
|
||
|
||
function pageAiScopedPageContext(contextSnapshot) {
|
||
var aggregate = contextSnapshot.aggregate || {};
|
||
var body = contextSnapshot.body || {};
|
||
var subtree = contextSnapshot.subtree || null;
|
||
var outline = subtree && subtree.outline ? subtree.outline : null;
|
||
var scope = pageUiState.pageAiContextScope || 'page';
|
||
var title = aggregate.head && aggregate.head.title ? aggregate.head.title : '';
|
||
var selectedText = scope === 'selection' ? currentPageAiSelectedText() : '';
|
||
var aiContext = buildPageAiContext(contextSnapshot, scope, selectedText);
|
||
return {
|
||
pageContext: {
|
||
contextScope: scope,
|
||
documentBlocks: null,
|
||
node: {
|
||
documentId: currentDocumentId(),
|
||
title: title
|
||
},
|
||
subtree: null,
|
||
outline: null,
|
||
pageSubtreeSource: scope === 'page' ? (contextSnapshot.pageSubtreeSource || 'none') : 'scope:' + scope,
|
||
evidence: selectedText ? [{ kind: 'selection', text: selectedText }] : null,
|
||
pageOptions: scope === 'options' || scope === 'page' ? currentPageOptions() : null,
|
||
contentAccess: 'mnote.doc.fetch',
|
||
aiContext: aiContext
|
||
},
|
||
selectedText: selectedText,
|
||
selectedBlockId: aiContext.selectedBlockIds[0] || null
|
||
};
|
||
}
|
||
|
||
function defaultPageOptions() {
|
||
return {
|
||
wideLayout: false,
|
||
smallText: false,
|
||
layoutDensity: 'normal',
|
||
showHeadingNumbers: false,
|
||
showToc: false,
|
||
showStructure: false,
|
||
protectEditing: false,
|
||
showWordCount: true,
|
||
collapseBacklinks: false,
|
||
pageFont: 'default',
|
||
hideChildPages: false,
|
||
showBlockRefCount: false,
|
||
hideTitleHeader: 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'
|
||
|| (name === 'hideTitleHeader' && currentSourceKind() === 'local_folder');
|
||
}
|
||
|
||
function pageOptionDescription(name) {
|
||
if (name === 'wideLayout') return '自适应宽度';
|
||
if (name === 'smallText') return '小字体';
|
||
if (name === 'hideTitleHeader') return '隐藏本地 Markdown 文件标题';
|
||
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 === 'hideTitleHeader') return '已接通:本地 Markdown 页头标题立即隐藏或显示';
|
||
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));
|
||
document.documentElement.setAttribute('data-page-hide-title-header', String(Boolean(options.hideTitleHeader)));
|
||
var titleHeader = document.querySelector('.document-pane[data-pane-role="primary"] .document-shell-header') || document.querySelector('.document-shell-header');
|
||
if (titleHeader instanceof HTMLElement) {
|
||
var hidden = Boolean(options.hideTitleHeader);
|
||
titleHeader.hidden = hidden;
|
||
titleHeader.setAttribute('data-page-title-hidden', String(hidden));
|
||
}
|
||
}
|
||
|
||
const sidebarWorkspace = createSidebarWorkspaceRuntime({
|
||
cssEscape,
|
||
currentDocumentId,
|
||
escapeHtml,
|
||
parseJsonScript,
|
||
setCommandPending: (...args) => setCommandPending(...args),
|
||
sidebarShellRuntimeFunction,
|
||
});
|
||
const normalizeSidebarTreeMode = (...args) => sidebarWorkspace.normalizeSidebarTreeMode(...args);
|
||
const readStoredSidebarTreeMode = (...args) => sidebarWorkspace.readStoredSidebarTreeMode(...args);
|
||
const persistSidebarTreeMode = (...args) => sidebarWorkspace.persistSidebarTreeMode(...args);
|
||
const activeSidebarTreeMode = (...args) => sidebarWorkspace.activeSidebarTreeMode(...args);
|
||
const resolveWorkspaceId = (...args) => sidebarWorkspace.resolveWorkspaceId(...args);
|
||
const currentWorkspaceId = (...args) => sidebarWorkspace.currentWorkspaceId(...args);
|
||
const currentSourceKind = (...args) => sidebarWorkspace.currentSourceKind(...args);
|
||
const currentRootUri = (...args) => sidebarWorkspace.currentRootUri(...args);
|
||
const copyWorkspaceSourceParams = (...args) => sidebarWorkspace.copyWorkspaceSourceParams(...args);
|
||
const currentWorkspaceSourcePayload = (...args) => sidebarWorkspace.currentWorkspaceSourcePayload(...args);
|
||
const openTrashModal = (...args) => sidebarWorkspace.openTrashModal(...args);
|
||
const closeTrashModal = (...args) => sidebarWorkspace.closeTrashModal(...args);
|
||
const closeLocalFolderDialog = (...args) => sidebarWorkspace.closeLocalFolderDialog(...args);
|
||
const requestOpenLocalFolder = (...args) => sidebarWorkspace.requestOpenLocalFolder(...args);
|
||
const createDefaultLocalWorkspace = (...args) => sidebarWorkspace.createDefaultLocalWorkspace(...args);
|
||
const closeWorkspaceSourceMenu = (...args) => sidebarWorkspace.closeWorkspaceSourceMenu(...args);
|
||
const openWorkspaceSourceMenu = (...args) => sidebarWorkspace.openWorkspaceSourceMenu(...args);
|
||
const closeAccountMenu = (...args) => sidebarWorkspace.closeAccountMenu(...args);
|
||
const openAccountMenu = (...args) => sidebarWorkspace.openAccountMenu(...args);
|
||
sidebarWorkspace.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) {
|
||
persistLocalFolderSelfChangeSuppression(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) {
|
||
var runtimeFn = sidebarShellRuntimeFunction('applySidebarTreeTab');
|
||
if (runtimeFn) return runtimeFn(mode, shell);
|
||
}
|
||
|
||
function switchSidebarTreeTab(trigger) {
|
||
var runtimeFn = sidebarShellRuntimeFunction('switchSidebarTreeTab');
|
||
if (runtimeFn) return runtimeFn(trigger);
|
||
}
|
||
|
||
function restoreSidebarTreeTab() {
|
||
var runtimeFn = sidebarShellRuntimeFunction('restoreSidebarTreeTab');
|
||
if (runtimeFn) return runtimeFn();
|
||
}
|
||
|
||
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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('readProjection');
|
||
if (runtimeFn) return runtimeFn(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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('readSidebarDataset');
|
||
if (runtimeFn) return runtimeFn(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 runtimeFn = fileTreeRuntimeFunction('readDatasetProjection');
|
||
if (runtimeFn) return runtimeFn(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 runtimeFn = fileTreeRuntimeFunction('projectionItems');
|
||
if (runtimeFn) return runtimeFn(projection);
|
||
var resolved = readProjection(projection);
|
||
return resolved && Array.isArray(resolved.items) ? resolved.items : [];
|
||
}
|
||
|
||
function hasProjectionItems(projection) {
|
||
var runtimeFn = fileTreeRuntimeFunction('hasProjectionItems');
|
||
if (runtimeFn) return runtimeFn(projection);
|
||
var resolved = readProjection(projection);
|
||
return Boolean(resolved && Array.isArray(resolved.items));
|
||
}
|
||
|
||
function nodeIdOf(item) {
|
||
var runtimeFn = fileTreeRuntimeFunction('nodeIdOf');
|
||
if (runtimeFn) return runtimeFn(item);
|
||
return String(item && (item.nodeId || item.id || item.documentId) || '').trim();
|
||
}
|
||
|
||
function rowIdOf(item) {
|
||
var runtimeFn = fileTreeRuntimeFunction('rowIdOf');
|
||
if (runtimeFn) return runtimeFn(item);
|
||
return String(item && (item.rowId || item.nodeId || item.id) || '').trim();
|
||
}
|
||
|
||
function parentIdOf(item) {
|
||
var runtimeFn = fileTreeRuntimeFunction('parentIdOf');
|
||
if (runtimeFn) return runtimeFn(item);
|
||
return String(item && (item.parentNodeId || item.parentId || '') || '').trim();
|
||
}
|
||
|
||
function titleOf(item) {
|
||
var runtimeFn = fileTreeRuntimeFunction('titleOf');
|
||
if (runtimeFn) return runtimeFn(item);
|
||
return String(item && item.title || '无标题').trim() || '无标题';
|
||
}
|
||
|
||
function fileWorkspaceRelativePath(item) {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileWorkspaceRelativePath');
|
||
if (runtimeFn) return runtimeFn(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 runtimeFn = fileTreeRuntimeFunction('groupRowsByParent');
|
||
if (runtimeFn) return runtimeFn(rows);
|
||
var ids = new Set(rows.map(nodeIdOf).filter(Boolean));
|
||
var grouped = new Map();
|
||
rows.forEach(function(item) {
|
||
var parentId = parentIdOf(item);
|
||
if (!ids.has(parentId)) parentId = '';
|
||
if (!grouped.has(parentId)) grouped.set(parentId, []);
|
||
grouped.get(parentId).push(item);
|
||
});
|
||
return grouped;
|
||
}
|
||
|
||
function pageTreeChevronSvg() {
|
||
return '<svg class="tree-toggle-icon" viewBox="0 0 20 20" width="20" height="20" aria-hidden="true" focusable="false"><path d="M7.84 14.955c.206 0 .37-.07.505-.21l4.277-4.179a.79.79 0 0 0 .264-.574.78.78 0 0 0-.258-.574L8.35 5.24a.7.7 0 0 0-.51-.21.721.721 0 0 0-.498 1.247l3.814 3.721-3.814 3.709a.721.721 0 0 0 .498 1.248"></path></svg>';
|
||
}
|
||
|
||
function renderPageRows(parentId, grouped, activeId, inheritedDepth) {
|
||
var computedDepth = Number(inheritedDepth || 0);
|
||
return (grouped.get(parentId) || []).map(function(item) {
|
||
var nodeId = nodeIdOf(item);
|
||
var title = titleOf(item);
|
||
var depth = computedDepth;
|
||
var parent = parentIdOf(item);
|
||
var children = grouped.get(nodeId) || [];
|
||
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
|
||
var expanded = expandable && item.expandedByDefault !== false;
|
||
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
||
var openable = Boolean(meta.documentId || item.documentId || !String(nodeId).startsWith('local-dir:'));
|
||
var toggle = expandable
|
||
? '<button type="button" class="tree-toggle" data-testid="tree-node-toggle" data-rust-action="toggle" data-node-id="' + escapeHtml(nodeId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '" aria-expanded="' + String(expanded) + '">' + pageTreeChevronSvg() + '</button>'
|
||
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
||
var childHtml = expandable
|
||
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderPageRows(nodeId, grouped, activeId, depth + 1) + '</ul>'
|
||
: '';
|
||
var currentAttr = nodeId === activeId ? ' aria-current="page" aria-selected="true"' : ' aria-selected="false"';
|
||
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="page" data-testid="wolai-sidebar-row" data-tree-testid="tree-node-open" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-depth="' + depth + '" data-shell-mode="page" data-active="' + String(nodeId === activeId) + '"' + currentAttr + ' data-focused="false" data-page-openable="' + String(openable) + '" data-draggable="true" draggable="true" tabindex="-1">' + toggle + '<button type="button" class="tree-link" data-testid="tree-node-open" data-rust-action="open" data-node-id="' + escapeHtml(nodeId) + '" data-page-openable="' + String(openable) + '" title="' + escapeHtml(title) + '"><span class="tree-link-title" title="' + escapeHtml(title) + '">' + escapeHtml(title) + '</span></button><div class="tree-actions"><button type="button" class="tree-action" data-testid="tree-action-menu" data-rust-action="menu" data-node-id="' + escapeHtml(nodeId) + '" aria-label="更多操作">…</button><button type="button" class="tree-action" data-testid="tree-action-create" data-rust-action="create" data-node-id="' + escapeHtml(nodeId) + '" aria-label="新建子页面">+</button></div></div>' + childHtml + '</li>';
|
||
}).join('');
|
||
}
|
||
|
||
function renderPageProjection(projection) {
|
||
var tree = document.getElementById('sidebar-tree-root');
|
||
if (!tree) return false;
|
||
var rows = projectionItems(projection).filter(function(item) {
|
||
return String(item.rowKind || 'document') === 'document';
|
||
});
|
||
var activeId = currentDocumentId();
|
||
var activeRowId = currentFileTreeActiveRowId();
|
||
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-page-renderer="initial_v1">' + (rows.length ? renderPageRows('', groupRowsByParent(rows), activeId, 0) : '') + '</ul>';
|
||
return true;
|
||
}
|
||
|
||
function fileDocumentId(item) {
|
||
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
||
if (meta.documentId) return String(meta.documentId).trim();
|
||
if (item && item.documentId) return String(item.documentId).trim();
|
||
if (item && item.rowKind === 'document') return nodeIdOf(item);
|
||
if (item && item.rowKind === 'index') return nodeIdOf(item).replace(/^index:/, '');
|
||
return '';
|
||
}
|
||
|
||
function fileAssetId(item) {
|
||
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
||
if (meta.assetId) return String(meta.assetId).trim();
|
||
if (item && item.assetId) return String(item.assetId).trim();
|
||
if (item && item.rowKind === 'asset') return nodeIdOf(item).replace(/^asset:/, '');
|
||
return '';
|
||
}
|
||
|
||
function fileObjectIdentity(item) {
|
||
var meta = item && item.resourceMeta && typeof item.resourceMeta === 'object' ? item.resourceMeta : {};
|
||
// 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 fileOwnerDocumentId(item, fallbackDocumentId, objectIdentity) {
|
||
var identity = objectIdentity && typeof objectIdentity === 'object' ? objectIdentity : fileObjectIdentity(item);
|
||
var owner = String(identity && identity.documentId || '').trim();
|
||
if (owner) return owner;
|
||
var assetId = fileAssetId(item);
|
||
var localPath = localFilePathFromAssetId(assetId);
|
||
if (localPath && localPath.indexOf('/') > 0) {
|
||
var parts = localPath.split('/');
|
||
var bundleName = parts[0] || '';
|
||
if (bundleName) return 'local-md:' + bundleName + '~2F' + bundleName + '.md';
|
||
}
|
||
return String(fallbackDocumentId || '').trim();
|
||
}
|
||
|
||
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 ownerDocumentId = fileOwnerDocumentId(item, documentId, objectIdentity);
|
||
var iconKind = iconKindOf(item);
|
||
var title = isFileTreeProjectionPageRow(rowKind, assetId)
|
||
? fileTreePageTitle(rawTitle)
|
||
: normalizeMindmapFileTreeTitle(rawTitle, assetId, iconKind, objectIdentity);
|
||
var children = grouped.get(nodeId) || [];
|
||
var expandable = Boolean(item.expandable || item.childCount > 0 || children.length);
|
||
var expanded = expandable && item.expandedByDefault !== false;
|
||
var selected = activeRowId ? rowId === activeRowId : rowId === 'doc:' + activeId;
|
||
var testId = rowKind === 'document' ? 'filetree-doc-row' : rowKind === 'index' ? 'filetree-index-row' : 'filetree-asset-row';
|
||
var toggle = expandable
|
||
? '<button type="button" class="tree-toggle" data-testid="filetree-toggle" data-rust-action="toggle" data-row-id="' + escapeHtml(rowId) + '" aria-label="' + (expanded ? '折叠 ' : '展开 ') + escapeHtml(title) + '">' + (expanded ? '▾' : '▸') + '</button>'
|
||
: '<span class="tree-spacer" aria-hidden="true"></span>';
|
||
var createAction = rowKind === 'document'
|
||
? '<button type="button" class="tree-action" data-testid="filetree-create" data-rust-action="create" data-row-id="' + escapeHtml(rowId) + '" data-node-id="' + escapeHtml(documentId || nodeId) + '" aria-label="新建子页面">+</button>'
|
||
: '';
|
||
var childHtml = expandable
|
||
? '<ul class="tree-children' + (expanded ? '' : ' tree-children--collapsed') + '">' + renderFileRows(nodeId, grouped, activeId, activeRowId) + '</ul>'
|
||
: '';
|
||
return '<li class="tree-node" data-node-id="' + escapeHtml(nodeId) + '"><div class="tree-row" role="treeitem" aria-level="' + (depth + 1) + '" aria-expanded="' + String(expandable && expanded) + '" data-rust-rendered-row="filetree" data-testid="' + testId + '" data-row-id="' + escapeHtml(rowId) + '" data-row-kind="' + escapeHtml(rowKind) + '" data-node-id="' + escapeHtml(nodeId) + '"' + (parent ? ' data-parent-id="' + escapeHtml(parent) + '"' : '') + ' data-document-id="' + escapeHtml(documentId) + '" data-doc-id="' + escapeHtml(documentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(String(objectIdentity.objectKind || '')) + '" data-capabilities="' + escapeHtml(fileCapabilitiesAttr(item)) + '" data-shell-mode="filetree" data-selected="' + String(selected) + '" data-active="false" draggable="true">' + toggle + '<span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span><button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml(rowId) + '" data-document-id="' + escapeHtml(documentId) + '" data-owner-document-id="' + escapeHtml(ownerDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-local-relative-path="' + escapeHtml(relativePath) + '" title="' + escapeHtml(title) + '"><span class="tree-link-title" title="' + escapeHtml(title) + '">' + escapeHtml(title) + '</span></button><div class="tree-actions">' + createAction + '<button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml(rowId) + '" aria-label="更多操作">…</button></div></div>' + childHtml + '</li>';
|
||
}).join('');
|
||
}
|
||
|
||
function renderFileProjection(projection) {
|
||
var tree = document.getElementById('sidebar-file-tree-root');
|
||
if (!tree) return false;
|
||
var rows = projectionItems(projection);
|
||
var activeId = currentDocumentId();
|
||
var activeRowId = currentFileTreeActiveRowId();
|
||
tree.innerHTML = '<ul class="tree-root" role="tree" data-rust-filetree-renderer="initial_v1">' + (rows.length ? renderFileRows('', groupRowsByParent(rows), activeId, activeRowId) : '') + '</ul>';
|
||
return true;
|
||
}
|
||
|
||
function renderSidebarSnapshot(payload) {
|
||
var renderedPage = hasProjectionItems(payload) ? renderPageProjection(payload) : false;
|
||
var fileProjection = readDatasetProjection(payload, 'kernel_file_tree_projection', 'kernelFileTreeProjection');
|
||
var renderedFile = fileProjection && hasProjectionItems(fileProjection) ? renderFileProjection(fileProjection) : false;
|
||
return renderedPage || renderedFile;
|
||
}
|
||
|
||
function replaceSidebarTreeFromDocument(nextDocument, rootId) {
|
||
var runtimeFn = fileTreeRuntimeFunction('replaceSidebarTreeFromDocument');
|
||
if (runtimeFn) return runtimeFn(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;
|
||
}
|
||
|
||
function applyLocalFolderSidebarSnapshot(nextDocument, options) {
|
||
var runtimeFn = fileTreeRuntimeFunction('applyLocalFolderSidebarSnapshot');
|
||
if (runtimeFn) return runtimeFn(nextDocument, options);
|
||
options = options || {};
|
||
var pageRootId = String(options.pageRootId || 'sidebar-tree-root');
|
||
var fileRootId = String(options.fileRootId || 'sidebar-file-tree-root');
|
||
var appliedPage = replaceSidebarTreeFromDocument(nextDocument, pageRootId);
|
||
var appliedFile = replaceSidebarTreeFromDocument(nextDocument, fileRootId);
|
||
var applied = appliedPage || appliedFile;
|
||
if (applied && typeof options.onApplied === 'function') options.onApplied({
|
||
pageRootApplied: appliedPage,
|
||
fileRootApplied: appliedFile
|
||
});
|
||
return applied;
|
||
}
|
||
|
||
function markLocalFolderWatchApplied(value) {
|
||
var runtimeFn = fileTreeRuntimeFunction('markLocalFolderWatchApplied');
|
||
if (runtimeFn) return runtimeFn(value);
|
||
var appliedValue = String(value || 'projection');
|
||
document.documentElement.setAttribute('data-mnote-local-folder-watch-applied', appliedValue);
|
||
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();
|
||
schedulePendingLocalFolderRestoreFocus();
|
||
restoreSidebarTreeTab();
|
||
refreshEditorLocalAttachmentExistence();
|
||
markLocalFolderWatchApplied('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;
|
||
// If tree live SSE transport is active for local_folder, skip polling (fallback)
|
||
var treeTransport = document.documentElement.getAttribute('data-mnote-tree-live-transport') || '';
|
||
if (treeTransport === 'local-folder-events') 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 _rto_ = window.__mnoteResourceOpenRuntime;
|
||
if (_rto_ && typeof _rto_.buildOnlyOfficeOpenPath === 'function') {
|
||
return _rto_.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'
|
||
});
|
||
}
|
||
|
||
async function openLocalOfficeFileInActiveTab(detail, mode) {
|
||
var localFilePath = localFilePathFromAssetId(detail.assetId);
|
||
if (!localFilePath) return false;
|
||
var localFileName = localFilePath.split('/').pop() || localFilePath;
|
||
var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail.documentId || currentDocumentId() || '').trim(), detail.assetId, mode || 'view');
|
||
if (!localOfficeUrl) return false;
|
||
var opened = 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,
|
||
paneRole: detail.paneRole || 'primary'
|
||
});
|
||
if (!opened) window.open(localOfficeUrl, '_blank', 'noopener,noreferrer');
|
||
return true;
|
||
}
|
||
|
||
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 _rto_ = window.__mnoteResourceOpenRuntime;
|
||
if (_rto_ && typeof _rto_.localFilePathFromAssetId === 'function') {
|
||
return _rto_.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 _rto_ = window.__mnoteResourceOpenRuntime;
|
||
if (_rto_ && typeof _rto_.buildLocalFileOpenUrl === 'function') {
|
||
return _rto_.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(),
|
||
paneRole: String(input && input.paneRole || 'primary').trim() || 'primary'
|
||
});
|
||
}
|
||
|
||
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') {
|
||
var sideLocalFilePath = localFilePathFromAssetId(assetId);
|
||
var sideRootUri = String(detail.rootUri || currentRootUri() || '').trim();
|
||
var sideFileName = String(detail.title || detail.fileName || (sideLocalFilePath ? sideLocalFilePath.split('/').pop() : '') || assetId || '').trim();
|
||
var sideKind = String(detail.iconKind || detail.assetType || fileTreeIconKindForFileName(sideFileName) || 'file').trim();
|
||
var sideHref = sideLocalFilePath ? buildLocalFileOpenUrl(sideLocalFilePath, false) : '';
|
||
var sideOfficeUrl = sideLocalFilePath ? buildLocalOnlyOfficeOpenUrl(sideLocalFilePath, sideFileName, String(detail.documentId || currentDocumentId() || '').trim(), assetId, 'view') : '';
|
||
if (sideOfficeUrl) sideKind = 'office';
|
||
void window.__mnoteDocumentPaneRuntime.openResourceAsSideTarget({
|
||
objectIdentity: sideLocalFilePath && sideRootUri ? 'resource:file:' + sideRootUri + ':' + sideLocalFilePath : String(detail.objectIdentity || detail.assetId || ''),
|
||
assetId: assetId,
|
||
title: sideFileName,
|
||
fileName: sideFileName,
|
||
kind: sideKind,
|
||
path: sideLocalFilePath,
|
||
rootUri: sideRootUri,
|
||
href: sideHref,
|
||
officeUrl: sideOfficeUrl,
|
||
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 requestedOfficeMode = forceNewWindow || forceEditMode ? 'edit' : 'view';
|
||
var localOfficeUrl = buildLocalOnlyOfficeOpenUrl(localFilePath, localFileName, String(detail && detail.documentId || currentDocumentId() || '').trim(), assetId, requestedOfficeMode);
|
||
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) {
|
||
var shouldOpenInNewWindow = forceNewWindow || shouldOpenLocalResourceInNewWindow(localFileName);
|
||
if (!shouldOpenInNewWindow && 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() {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowsForUploadPreflight');
|
||
if (runtimeFn) return runtimeFn(fileTreeRuntimeDeps());
|
||
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() {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeDocumentParentsForPreflight');
|
||
if (runtimeFn) return runtimeFn(fileTreeRuntimeDeps());
|
||
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 runtimeFn = fileTreeRuntimeFunction('fileTreeTargetChildrenForPreflight');
|
||
if (runtimeFn) return runtimeFn(targetRow, fileTreeRuntimeDeps());
|
||
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() {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeDropPreflightRows');
|
||
if (runtimeFn) return runtimeFn(Object.assign({}, fileTreeRuntimeDeps(), {
|
||
fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight
|
||
}));
|
||
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 runtimeFn = fileTreeRuntimeFunction('fileTreeDocumentWorkspacesForUploadPreflight');
|
||
if (runtimeFn) {
|
||
return runtimeFn(workspaceId, Object.assign({}, fileTreeRuntimeDeps(), {
|
||
fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight
|
||
}));
|
||
}
|
||
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 bodyRuntime = fileTreeRuntimeFunction('fileTreeUploadTargetPreflightBody');
|
||
var body = bodyRuntime ? bodyRuntime(detail || {}, Object.assign({}, fileTreeRuntimeDeps(), {
|
||
focusedRowId: sidebarFileTreeSelection.focusedRowId || null,
|
||
fileTreeRowsForUploadPreflight: fileTreeRowsForUploadPreflight,
|
||
currentDocumentId: currentDocumentId,
|
||
resolveWorkspaceId: resolveWorkspaceId
|
||
})) : (function() {
|
||
var workspaceId = String(detail && detail.workspaceId || resolveWorkspaceId(document.body) || '').trim();
|
||
return {
|
||
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 runtimeFn = fileTreeRuntimeFunction('fileTreeUploadTargetFallback');
|
||
if (runtimeFn) {
|
||
return runtimeFn(detail || {}, {
|
||
resolveWorkspaceId: resolveWorkspaceId,
|
||
currentDocumentId: currentDocumentId
|
||
});
|
||
}
|
||
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 || ''),
|
||
uploadIntent: String(detail.uploadIntent || 'filetree.folder.drop')
|
||
};
|
||
}
|
||
if (!workspaceId || !documentId) {
|
||
throw new Error('请选择一个目标页面后再拖入文件');
|
||
}
|
||
return {
|
||
workspaceId: workspaceId,
|
||
targetDocumentId: documentId,
|
||
targetMindmapId: null,
|
||
targetSubPath: null,
|
||
uploadIntent: String(detail && detail.uploadIntent || 'editor.markdown.attach')
|
||
};
|
||
}
|
||
|
||
async function resolveFileTreeUploadTarget(detail) {
|
||
if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) {
|
||
return fallbackFileTreeUploadTarget(detail || {});
|
||
}
|
||
try {
|
||
var plan = await preflightFileTreeUploadTarget(detail || {});
|
||
var normalizePlanRuntime = fileTreeRuntimeFunction('normalizeFileTreeUploadTargetPlan');
|
||
if (normalizePlanRuntime) {
|
||
return normalizePlanRuntime(detail || {}, plan);
|
||
}
|
||
if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) {
|
||
plan.targetRelativePath = String(detail.targetRelativePath || '');
|
||
}
|
||
if (detail && detail.uploadIntent) {
|
||
plan.uploadIntent = String(detail.uploadIntent);
|
||
} else if (detail && Object.prototype.hasOwnProperty.call(detail, 'targetRelativePath')) {
|
||
plan.uploadIntent = 'filetree.folder.drop';
|
||
} else if (!plan.uploadIntent) {
|
||
plan.uploadIntent = 'editor.markdown.attach';
|
||
}
|
||
return plan;
|
||
} catch (error) {
|
||
console.warn('[mnote upload] upload target preflight fallback', error);
|
||
return fallbackFileTreeUploadTarget(detail || {});
|
||
}
|
||
}
|
||
|
||
function localUploadRuntimeFunction(name) {
|
||
var runtime = window.__mnoteLocalUploadRuntime;
|
||
var fn = runtime && runtime[name];
|
||
return typeof fn === 'function' ? fn : null;
|
||
}
|
||
|
||
function fileTreeRuntimeFunction(name) {
|
||
var runtime = window.__mnoteFileTreeRuntime;
|
||
var fn = runtime && runtime[name];
|
||
return typeof fn === 'function' ? fn : null;
|
||
}
|
||
|
||
function fileTreeRuntimeDeps() {
|
||
return {
|
||
currentSourceKind: currentSourceKind,
|
||
currentDocumentId: currentDocumentId,
|
||
localFilePathFromAssetId: localFilePathFromAssetId,
|
||
rowTitle: rowTitle,
|
||
resolveWorkspaceId: resolveWorkspaceId,
|
||
cssEscape: cssEscape,
|
||
escapeHtml: escapeHtml,
|
||
objectIdentityAttr: objectIdentityAttr,
|
||
uploadedAssetTitle: uploadedAssetTitle,
|
||
uploadedAssetType: uploadedAssetType,
|
||
fileTreeIconKindForFileName: fileTreeIconKindForFileName
|
||
};
|
||
}
|
||
|
||
function fileTreeSelectionRuntimeFunction(name) {
|
||
var runtime = window.__mnoteFileTreeSelectionRuntime;
|
||
var fn = runtime && runtime[name];
|
||
return typeof fn === 'function' ? fn : null;
|
||
}
|
||
|
||
function fileTreeSelectionRuntimeDeps() {
|
||
return {
|
||
cssEscape: cssEscape
|
||
};
|
||
}
|
||
|
||
function uploadedAssetTitle(asset) {
|
||
var runtimeFn = localUploadRuntimeFunction('uploadedAssetTitle');
|
||
if (runtimeFn) return runtimeFn(asset);
|
||
return String(asset && (asset.file_name || asset.title || asset.name) || '未命名附件').trim() || '未命名附件';
|
||
}
|
||
|
||
function uploadedAssetUrl(asset) {
|
||
var runtimeFn = localUploadRuntimeFunction('uploadedAssetUrl');
|
||
if (runtimeFn) return runtimeFn(asset);
|
||
return String(asset && (asset.sourcePath || asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim();
|
||
}
|
||
|
||
function localAssetOpenUrl(asset, download) {
|
||
var runtimeFn = localUploadRuntimeFunction('localAssetOpenUrl');
|
||
if (runtimeFn) return runtimeFn(asset, download, { rootUri: currentRootUri() });
|
||
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) {
|
||
var runtimeFn = localUploadRuntimeFunction('uploadedAssetType');
|
||
if (runtimeFn) return runtimeFn(asset);
|
||
return String(asset && (asset.asset_type || asset.assetType || asset.mime_type || '') || '').trim();
|
||
}
|
||
|
||
function fileTreeIconKindForFileName(fileName) {
|
||
var runtimeFn = localUploadRuntimeFunction('fileTreeIconKindForFileName');
|
||
if (runtimeFn) return runtimeFn(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 shouldOpenLocalResourceInNewWindow(fileName) {
|
||
return false;
|
||
}
|
||
|
||
function isLocalUploadedAsset(asset) {
|
||
var runtimeFn = localUploadRuntimeFunction('isLocalUploadedAsset');
|
||
if (runtimeFn) return runtimeFn(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 runtimeFn = localUploadRuntimeFunction('uploadedAssetExtension');
|
||
if (runtimeFn) return runtimeFn(asset);
|
||
var match = uploadedAssetTitle(asset).toLowerCase().match(/\.([a-z0-9]+)$/);
|
||
return match ? match[1] : '';
|
||
}
|
||
|
||
function isNonOfficeAttachmentName(name, ext) {
|
||
var runtimeFn = localUploadRuntimeFunction('isNonOfficeAttachmentName');
|
||
if (runtimeFn) return runtimeFn(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 runtimeFn = localUploadRuntimeFunction('attachmentExtensionFromFileName');
|
||
if (runtimeFn) return runtimeFn(fileName);
|
||
var name = String(fileName || '').trim().toLowerCase();
|
||
return name.indexOf('.') >= 0 ? name.split('.').pop() : '';
|
||
}
|
||
|
||
function isPdfAttachmentFileName(fileName) {
|
||
var runtimeFn = localUploadRuntimeFunction('isPdfAttachmentFileName');
|
||
if (runtimeFn) return runtimeFn(fileName);
|
||
return attachmentExtensionFromFileName(fileName) === 'pdf';
|
||
}
|
||
|
||
function isCodeAttachmentFileName(fileName) {
|
||
var runtimeFn = localUploadRuntimeFunction('isCodeAttachmentFileName');
|
||
if (runtimeFn) return runtimeFn(fileName);
|
||
var name = String(fileName || '').trim().toLowerCase();
|
||
var ext = attachmentExtensionFromFileName(name);
|
||
return ext !== 'pdf' && isNonOfficeAttachmentName(name, ext);
|
||
}
|
||
|
||
function inferCodeAttachmentLanguage(fileName) {
|
||
var runtimeFn = localUploadRuntimeFunction('inferCodeAttachmentLanguage');
|
||
if (runtimeFn) return runtimeFn(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 runtimeFn = localUploadRuntimeFunction('attachmentClassForFileName');
|
||
if (runtimeFn) return runtimeFn(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) {
|
||
var runtimeFn = localUploadRuntimeFunction('uploadedAttachmentClass');
|
||
if (runtimeFn) return runtimeFn(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 runtimeFn = localUploadRuntimeFunction('uploadedFileSize');
|
||
if (runtimeFn) return runtimeFn(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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('revealFileTreeRow');
|
||
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||
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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('revealFileTreeAssetRow');
|
||
if (runtimeFn) return runtimeFn(assetId, fileTreeRuntimeDeps());
|
||
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 runtimeFn = fileTreeRuntimeFunction('appendUploadedAssetRow');
|
||
if (runtimeFn) return runtimeFn(asset, documentId, fileTreeRuntimeDeps());
|
||
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 =
|
||
'<div class="tree-row" role="treeitem" aria-level="' + (parentRow ? '2' : '1') + '" aria-expanded="false" data-rust-rendered-row="filetree" data-testid="filetree-asset-row" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-row-kind="asset" data-node-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-doc-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '" data-object-identity="' + escapeHtml(objectIdentityAttr(objectIdentity)) + '" data-object-kind="' + escapeHtml(objectKind || 'attachment') + '" data-shell-mode="filetree" data-selected="false" data-active="false" draggable="true">' +
|
||
'<span class="tree-spacer" aria-hidden="true"></span><span class="tree-kind-badge" data-kind="' + escapeHtml(iconKind) + '" aria-hidden="true"></span>' +
|
||
'<button type="button" class="tree-link" data-rust-action="open" data-row-id="' + escapeHtml('asset:' + assetId) + '" data-document-id="' + escapeHtml(targetDocumentId) + '" data-asset-id="' + escapeHtml(assetId) + '"><span class="tree-link-title">' + escapeHtml(title) + '</span></button>' +
|
||
'<div class="tree-actions"><button type="button" class="tree-action" data-testid="filetree-action-menu" data-rust-action="menu" data-row-id="' + escapeHtml('asset:' + assetId) + '" aria-label="更多操作">…</button></div></div>';
|
||
container.appendChild(li);
|
||
revealFileTreeRow(li.querySelector('.tree-row'));
|
||
document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId);
|
||
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 runtimeFn = fileTreeRuntimeFunction('shortMindmapFileName');
|
||
if (runtimeFn) return runtimeFn(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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('parseMindmapApiTarget');
|
||
if (runtimeFn) return runtimeFn(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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('requestMethod');
|
||
if (runtimeFn) return runtimeFn(input, init);
|
||
return String(
|
||
init && init.method
|
||
? init.method
|
||
: input && typeof input.method === 'string'
|
||
? input.method
|
||
: 'GET'
|
||
).toUpperCase();
|
||
}
|
||
|
||
function requestBodyHasMindmapCreateOnly(init) {
|
||
var runtimeFn = fileTreeRuntimeFunction('requestBodyHasMindmapCreateOnly');
|
||
if (runtimeFn) return runtimeFn(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;
|
||
}
|
||
|
||
function resolveEditorUploadContext(detail) {
|
||
var runtimeFn = localUploadRuntimeFunction('resolveEditorUploadContext');
|
||
if (runtimeFn) {
|
||
return runtimeFn(detail || {}, {
|
||
rootSelector: EDITOR_UPLOAD_ROOT_SELECTOR,
|
||
editorUploadRootFromElement: editorUploadRootFromElement,
|
||
currentDocumentId: currentDocumentId,
|
||
resolveWorkspaceId: resolveWorkspaceId
|
||
});
|
||
}
|
||
var root = null;
|
||
var selector = detail && detail.editorRootSelector ? String(detail.editorRootSelector) : '';
|
||
if (selector) {
|
||
try {
|
||
var selected = document.querySelector(selector);
|
||
if (selected instanceof HTMLElement) root = selected;
|
||
} catch (_) {}
|
||
}
|
||
if (!root && window.__mnoteIntendedSlashRoot instanceof HTMLElement && window.__mnoteIntendedSlashRoot.isConnected) {
|
||
root = window.__mnoteIntendedSlashRoot;
|
||
}
|
||
if (!root && window.__mnoteLastEditorUploadRoot instanceof HTMLElement && window.__mnoteLastEditorUploadRoot.isConnected) {
|
||
root = window.__mnoteLastEditorUploadRoot;
|
||
}
|
||
if (!root && document.activeElement instanceof Element) {
|
||
root = editorUploadRootFromElement(document.activeElement);
|
||
}
|
||
if (!root) {
|
||
var focused = document.querySelector(EDITOR_UPLOAD_ROOT_SELECTOR + ' .ProseMirror:focus-within');
|
||
root = editorUploadRootFromElement(focused);
|
||
}
|
||
if (!root) {
|
||
root = document.querySelector('[data-editor-host-kind="leptos_tiptap_island"][data-pane-role="primary"]');
|
||
}
|
||
var pane = root instanceof Element ? root.closest('.document-pane[data-pane-role]') : null;
|
||
var shell = root instanceof Element ? root.closest('.document-shell[data-document-id]') : null;
|
||
return {
|
||
root: root instanceof HTMLElement ? root : null,
|
||
documentId: String(
|
||
detail && detail.documentId
|
||
|| (root instanceof HTMLElement && root.getAttribute('data-document-id'))
|
||
|| (pane instanceof HTMLElement && pane.getAttribute('data-pane-document-id'))
|
||
|| (shell instanceof HTMLElement && shell.getAttribute('data-document-id'))
|
||
|| currentDocumentId()
|
||
|| ''
|
||
).trim(),
|
||
workspaceId: String(
|
||
detail && detail.workspaceId
|
||
|| (root instanceof HTMLElement && root.getAttribute('data-workspace-id'))
|
||
|| (pane instanceof HTMLElement && pane.getAttribute('data-pane-workspace-id'))
|
||
|| (shell instanceof HTMLElement && shell.getAttribute('data-workspace-id'))
|
||
|| resolveWorkspaceId(document.body)
|
||
|| ''
|
||
).trim()
|
||
};
|
||
}
|
||
|
||
function editorRootFromUploadOptions(options) {
|
||
var runtimeFn = localUploadRuntimeFunction('editorRootFromUploadOptions');
|
||
if (runtimeFn) {
|
||
return runtimeFn(options || {}, {
|
||
rootSelector: EDITOR_UPLOAD_ROOT_SELECTOR,
|
||
editorUploadRootFromElement: editorUploadRootFromElement
|
||
});
|
||
}
|
||
if (options && options.editorRoot instanceof HTMLElement) return options.editorRoot;
|
||
if (window.__mnoteIntendedSlashRoot instanceof HTMLElement && window.__mnoteIntendedSlashRoot.isConnected) {
|
||
return window.__mnoteIntendedSlashRoot;
|
||
}
|
||
if (window.__mnoteLastEditorUploadRoot instanceof HTMLElement && window.__mnoteLastEditorUploadRoot.isConnected) {
|
||
return window.__mnoteLastEditorUploadRoot;
|
||
}
|
||
var focused = document.querySelector(EDITOR_UPLOAD_ROOT_SELECTOR + ' .ProseMirror:focus-within');
|
||
return editorUploadRootFromElement(focused);
|
||
}
|
||
|
||
async function fetchWithTimeout(input, init, timeoutMs, label) {
|
||
var runtimeFn = localUploadRuntimeFunction('fetchWithTimeout');
|
||
if (runtimeFn) {
|
||
return await runtimeFn(input, init || {}, timeoutMs, label);
|
||
}
|
||
var controller = typeof AbortController === 'function' ? new AbortController() : null;
|
||
var timer = 0;
|
||
try {
|
||
if (controller) {
|
||
timer = window.setTimeout(function() {
|
||
controller.abort();
|
||
}, Math.max(1000, Number(timeoutMs) || 15000));
|
||
}
|
||
var nextInit = Object.assign({}, init || {});
|
||
if (controller) nextInit.signal = controller.signal;
|
||
return await fetch(input, nextInit);
|
||
} catch (error) {
|
||
if (error && error.name === 'AbortError') {
|
||
throw new Error((label || '请求') + '超时');
|
||
}
|
||
throw error;
|
||
} finally {
|
||
if (timer) window.clearTimeout(timer);
|
||
}
|
||
}
|
||
|
||
async function insertUploadedAssetIntoEditor(asset, targetRoot) {
|
||
var runtimeFn = localUploadRuntimeFunction('insertUploadedAssetIntoEditor');
|
||
if (runtimeFn) {
|
||
return await runtimeFn(asset, targetRoot, {
|
||
uploadedAssetTitle: uploadedAssetTitle,
|
||
localAssetOpenUrl: localAssetOpenUrl,
|
||
uploadedAssetUrl: uploadedAssetUrl,
|
||
uploadedAssetType: uploadedAssetType,
|
||
isLocalUploadedAsset: isLocalUploadedAsset,
|
||
buildOnlyOfficeAssetOpenUrl: buildOnlyOfficeAssetOpenUrl,
|
||
fetchCurrentOnlyOfficeUserId: fetchCurrentOnlyOfficeUserId,
|
||
buildOnlyOfficeOpenPath: buildOnlyOfficeOpenPath,
|
||
inferOnlyOfficeFileType: inferOnlyOfficeFileType,
|
||
currentDocumentId: currentDocumentId,
|
||
uploadedAttachmentClass: uploadedAttachmentClass,
|
||
uploadedFileSize: uploadedFileSize,
|
||
cssEscape: cssEscape,
|
||
enhanceEditorAttachmentLinks: enhanceEditorAttachmentLinks
|
||
});
|
||
}
|
||
var editorRoot = targetRoot instanceof HTMLElement
|
||
? targetRoot.querySelector('.editor-surface .ProseMirror')
|
||
: document.querySelector('.editor-surface .ProseMirror');
|
||
if (targetRoot instanceof HTMLElement) window.__mnoteLastEditorUploadRoot = targetRoot;
|
||
var editor = editorRoot && editorRoot.editor;
|
||
if (!editor || !editor.chain) {
|
||
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false');
|
||
document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'editor_unavailable');
|
||
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)
|
||
}
|
||
}]
|
||
}]
|
||
},
|
||
{ type: 'paragraph' }
|
||
]).focus('end').run() === true;
|
||
if (inserted) {
|
||
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'true');
|
||
document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId || '');
|
||
document.documentElement.removeAttribute('data-mnote-last-upload-insert-error');
|
||
} else {
|
||
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'false');
|
||
document.documentElement.setAttribute('data-mnote-last-upload-insert-error', 'insert_content_failed');
|
||
}
|
||
window.setTimeout(function() {
|
||
if (targetRoot instanceof HTMLElement) window.__mnoteLastEditorUploadRoot = targetRoot;
|
||
enhanceEditorAttachmentLinks();
|
||
var selector = assetId
|
||
? '.editor-surface .ProseMirror a[href*="' + cssEscape(assetId) + '"]'
|
||
: '.editor-surface .ProseMirror a';
|
||
var link = targetRoot instanceof HTMLElement
|
||
? targetRoot.querySelector(selector)
|
||
: 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) {
|
||
var runtimeFn = localUploadRuntimeFunction('uploadFileToMediaAsset');
|
||
if (runtimeFn) {
|
||
return await runtimeFn(file, plan, options || {}, {
|
||
currentSourceKind: currentSourceKind,
|
||
currentRootUri: currentRootUri,
|
||
currentDocumentId: currentDocumentId,
|
||
editorRootFromUploadOptions: editorRootFromUploadOptions,
|
||
appendUploadedAssetRow: appendUploadedAssetRow,
|
||
refreshLocalFolderSidebarSnapshot: refreshLocalFolderSidebarSnapshot,
|
||
dispatchEvent: function(event) { return window.dispatchEvent(event); },
|
||
CustomEvent: CustomEvent,
|
||
buildOnlyOfficeAssetOpenUrl: buildOnlyOfficeAssetOpenUrl,
|
||
fetchCurrentOnlyOfficeUserId: fetchCurrentOnlyOfficeUserId,
|
||
buildOnlyOfficeOpenPath: buildOnlyOfficeOpenPath,
|
||
inferOnlyOfficeFileType: inferOnlyOfficeFileType,
|
||
enhanceEditorAttachmentLinks: enhanceEditorAttachmentLinks,
|
||
cssEscape: cssEscape
|
||
});
|
||
}
|
||
if (currentSourceKind() === 'local_folder') {
|
||
var rootUri = currentRootUri();
|
||
var documentId = String(plan && plan.targetDocumentId || currentDocumentId() || '').trim();
|
||
var hasFolderTarget = plan && Object.prototype.hasOwnProperty.call(plan, 'targetRelativePath');
|
||
var uploadIntent = String(plan && plan.uploadIntent || (hasFolderTarget ? 'filetree.folder.drop' : 'editor.markdown.attach')).trim();
|
||
if (!rootUri || !uploadIntent) {
|
||
throw new Error('本地 Markdown 上传缺少 rootUri 或 documentId');
|
||
}
|
||
var localUploadRuntime = localUploadRuntimeFunction('uploadLocalFolderAsset');
|
||
var localPayload = null;
|
||
if (localUploadRuntime) {
|
||
var localResult = await localUploadRuntime(file, plan, {
|
||
rootUri: rootUri,
|
||
documentId: documentId,
|
||
timeoutMs: 15000
|
||
});
|
||
localPayload = localResult && localResult.asset ? { asset: localResult.asset } : null;
|
||
} else {
|
||
var localForm = new FormData();
|
||
localForm.append('file', file);
|
||
localForm.append('rootUri', rootUri);
|
||
localForm.append('uploadIntent', uploadIntent);
|
||
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 fetchWithTimeout('/api/local-folder/assets/upload', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: localForm
|
||
}, 15000, '本地上传');
|
||
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, editorRootFromUploadOptions(options));
|
||
}
|
||
void refreshLocalFolderSidebarSnapshot();
|
||
window.dispatchEvent(new CustomEvent('wolai:local-assets-changed', {
|
||
detail: { docId: documentId, asset: localPayload.asset, assetIds: [localPayload.asset.id] }
|
||
}));
|
||
return localPayload.asset;
|
||
}
|
||
var mediaUploadRuntime = localUploadRuntimeFunction('uploadMediaAsset');
|
||
var payload = null;
|
||
if (mediaUploadRuntime) {
|
||
var mediaResult = await mediaUploadRuntime(file, plan, { timeoutMs: 15000 });
|
||
payload = mediaResult && mediaResult.asset ? { asset: mediaResult.asset } : null;
|
||
if (!payload || !payload.asset) {
|
||
throw new Error('上传失败');
|
||
}
|
||
} else {
|
||
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 fetchWithTimeout('/api/media/upload', {
|
||
method: 'POST',
|
||
credentials: 'include',
|
||
body: form
|
||
}, 15000, '上传');
|
||
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, editorRootFromUploadOptions(options));
|
||
}
|
||
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 runtimeFn = localUploadRuntimeFunction('uploadFilesWithResolvedTarget');
|
||
if (runtimeFn) {
|
||
return await runtimeFn(files, detail || {}, options || {}, {
|
||
resolveFileTreeUploadTarget: resolveFileTreeUploadTarget,
|
||
uploadFileToMediaAsset: uploadFileToMediaAsset,
|
||
alert: function(message) { window.alert(message); }
|
||
});
|
||
}
|
||
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 runtimeFn = localUploadRuntimeFunction('openEditorUploadFilePicker');
|
||
if (runtimeFn) {
|
||
return runtimeFn(detail || {}, {
|
||
rootSelector: EDITOR_UPLOAD_ROOT_SELECTOR,
|
||
editorUploadRootFromElement: editorUploadRootFromElement,
|
||
currentDocumentId: currentDocumentId,
|
||
resolveWorkspaceId: resolveWorkspaceId,
|
||
uploadFilesWithResolvedTarget: uploadFilesWithResolvedTarget
|
||
});
|
||
}
|
||
var uploadContext = resolveEditorUploadContext(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: uploadContext.workspaceId || resolveWorkspaceId(document.body),
|
||
documentId: uploadContext.documentId || currentDocumentId(),
|
||
targetRowId: null,
|
||
uploadIntent: 'editor.markdown.attach'
|
||
}, {
|
||
insertIntoEditor: detail && detail.insertIntoEditor !== false,
|
||
editorRoot: uploadContext.root
|
||
});
|
||
}, { 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,
|
||
uploadIntent: 'editor.markdown.attach'
|
||
}, {
|
||
insertIntoEditor: true,
|
||
editorRoot: editorUploadRootFromElement(editorTarget)
|
||
});
|
||
}, 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' && detail.contextKind === 'filetree') {
|
||
var selectedFileTreeRows = selectedSidebarFileTreeRows();
|
||
if (selectedFileTreeRows.length > 1) {
|
||
void deleteSelectedSidebarFileTreeRows(trigger || document.body);
|
||
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);
|
||
}
|
||
|
||
// ── CommandContext 启用态辅助:与 Rust core-protocol CommandContext 保持同一口径 ──
|
||
|
||
function buildSidebarFileTreeContext(kind) {
|
||
var runtime = window.__mnoteFileTreeContextMenuRuntime;
|
||
if (runtime && typeof runtime.buildSidebarFileTreeContext === 'function') {
|
||
try {
|
||
return runtime.buildSidebarFileTreeContext(kind, {
|
||
selection: sidebarFileTreeSelection,
|
||
currentSourceKind: currentSourceKind,
|
||
workspaceReadonly: function() {
|
||
return document.documentElement.getAttribute('data-mnote-workspace-readonly') === 'true';
|
||
},
|
||
queryRowById: function(rowId) {
|
||
return document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowId) + '"]');
|
||
}
|
||
});
|
||
} catch (_) {}
|
||
}
|
||
var s = sidebarFileTreeSelection;
|
||
var rowIds = Array.from(s.selectedRowIds || []);
|
||
var rows = [];
|
||
for (var i = 0; i < rowIds.length; i += 1) {
|
||
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(rowIds[i]) + '"]');
|
||
if (row instanceof HTMLElement) rows.push(row);
|
||
}
|
||
var resourceKinds = {};
|
||
for (var i = 0; i < rows.length; i += 1) {
|
||
var rk = rows[i].getAttribute('data-row-kind') || 'unknown';
|
||
resourceKinds[rk] = true;
|
||
}
|
||
var rkKeys = Object.keys(resourceKinds);
|
||
return {
|
||
'workspace.sourceKind': currentSourceKind() || '',
|
||
'workspace.readonly': document.documentElement.getAttribute('data-mnote-workspace-readonly') === 'true',
|
||
'tree.focusKind': kind === 'filetree' ? 'file_tree' : kind === 'page' ? 'page_tree' : kind,
|
||
'tree.selectionCount': rows.length,
|
||
'tree.selectionResourceKind': rkKeys.length === 1 ? rkKeys[0] : 'mixed'
|
||
};
|
||
}
|
||
|
||
function evaluateSidebarFileTreeWhen(ctx, expr) {
|
||
var runtime = window.__mnoteFileTreeContextMenuRuntime;
|
||
if (runtime && typeof runtime.evaluateSidebarFileTreeWhen === 'function') {
|
||
return runtime.evaluateSidebarFileTreeWhen(ctx || {}, expr || '');
|
||
}
|
||
if (!expr) return true;
|
||
try {
|
||
expr = expr.trim();
|
||
// 词法切分:支持 key、!key、==、!=、&&、|| 和括号分组
|
||
var tokens = [];
|
||
var i = 0;
|
||
while (i < expr.length) {
|
||
var ch = expr[i];
|
||
if (ch === ' ' || ch === '\t') { i++; continue; }
|
||
if (ch === '(') { tokens.push({ t: '(' }); i++; continue; }
|
||
if (ch === ')') { tokens.push({ t: ')' }); i++; continue; }
|
||
if (ch === '&' && expr[i+1] === '&') { tokens.push({ t: '&&' }); i += 2; continue; }
|
||
if (ch === '|' && expr[i+1] === '|') { tokens.push({ t: '||' }); i += 2; continue; }
|
||
if (ch === '=' && expr[i+1] === '=') { tokens.push({ t: '==' }); i += 2; continue; }
|
||
if (ch === '!' && expr[i+1] === '=') { tokens.push({ t: '!=' }); i += 2; continue; }
|
||
if (ch === '!') { tokens.push({ t: '!' }); i++; continue; }
|
||
if (ch === '"' || ch === "'") {
|
||
i++;
|
||
var str = '';
|
||
while (i < expr.length && expr[i] !== ch) { str += expr[i]; i++; }
|
||
if (i < expr.length) i++;
|
||
tokens.push({ t: 'str', v: str });
|
||
continue;
|
||
}
|
||
if ((ch >= 'a' && ch <= 'z') || (ch >= 'A' && ch <= 'Z') || ch === '_' || ch === '.' || (ch >= '0' && ch <= '9')) {
|
||
var id = '';
|
||
while (i < expr.length && ((expr[i] >= 'a' && expr[i] <= 'z') || (expr[i] >= 'A' && expr[i] <= 'Z') || (expr[i] >= '0' && expr[i] <= '9') || expr[i] === '_' || expr[i] === '.')) { id += expr[i]; i++; }
|
||
tokens.push({ t: 'id', v: id });
|
||
continue;
|
||
}
|
||
throw new Error('Unexpected char: ' + ch);
|
||
}
|
||
var pos = 0;
|
||
function peek() { return tokens[pos]; }
|
||
function consume() { return tokens[pos++]; }
|
||
function expect(t) {
|
||
var tok = consume();
|
||
if (!tok || tok.t !== t) throw new Error('Expected ' + t + ' got ' + (tok ? tok.t : 'EOF'));
|
||
return tok;
|
||
}
|
||
function ctxVal(key) {
|
||
var v = ctx[key];
|
||
if (v === undefined || v === null) return '';
|
||
return v;
|
||
}
|
||
function parseOr() {
|
||
var left = parseAnd();
|
||
while (peek() && peek().t === '||') { consume(); var right = parseAnd(); left = left || right; }
|
||
return left;
|
||
}
|
||
function parseAnd() {
|
||
var left = parsePrimary();
|
||
while (peek() && peek().t === '&&') { consume(); var right = parsePrimary(); left = left && right; }
|
||
return left;
|
||
}
|
||
function parsePrimary() {
|
||
if (!peek()) throw new Error('Unexpected EOF');
|
||
if (peek().t === '(') {
|
||
consume();
|
||
var val = parseOr();
|
||
expect(')');
|
||
return val;
|
||
}
|
||
if (peek().t === '!') {
|
||
consume();
|
||
var keyTok = consume();
|
||
if (!keyTok || keyTok.t !== 'id') throw new Error('Expected key after !');
|
||
return !ctxVal(keyTok.v);
|
||
}
|
||
if (peek().t === 'id') {
|
||
var keyTok = consume();
|
||
if (peek() && peek().t === '==') {
|
||
consume();
|
||
var valTok = consume();
|
||
if (!valTok) throw new Error('Expected value after ==');
|
||
var val = valTok.t === 'str' ? valTok.v : (valTok.v || '');
|
||
// 与 Rust evaluate_when 保持一致:按 string / bool / number 比较。
|
||
var cv = ctxVal(keyTok.v);
|
||
if (val === 'true') return cv === true || cv === 'true';
|
||
if (val === 'false') return cv === false || cv === '' || cv === 'false';
|
||
var num = Number(val);
|
||
if (!isNaN(num) && String(num) === val) return Number(cv) === num;
|
||
return String(cv) === val;
|
||
}
|
||
if (peek() && peek().t === '!=') {
|
||
consume();
|
||
var valTok = consume();
|
||
if (!valTok) throw new Error('Expected value after !=');
|
||
var val = valTok.t === 'str' ? valTok.v : (valTok.v || '');
|
||
var cv = ctxVal(keyTok.v);
|
||
if (val === 'true') return !(cv === true || cv === 'true');
|
||
if (val === 'false') return !(cv === false || cv === '' || cv === 'false');
|
||
var num = Number(val);
|
||
if (!isNaN(num) && String(num) === val) return Number(cv) !== num;
|
||
return String(cv) !== val;
|
||
}
|
||
// 单独 key:按 truthy 规则判断。
|
||
var cv = ctxVal(keyTok.v);
|
||
if (typeof cv === 'boolean') return cv;
|
||
if (typeof cv === 'number') return cv !== 0;
|
||
return cv !== '' && cv !== undefined && cv !== null;
|
||
}
|
||
throw new Error('Unexpected token: ' + peek().t);
|
||
}
|
||
return parseOr();
|
||
} catch(e) {
|
||
return true;
|
||
}
|
||
}
|
||
|
||
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 ctx = buildSidebarFileTreeContext(kind);
|
||
var items = isAttachment ? [
|
||
{ action: 'duplicate', icon: 'file_copy', label: '拷贝副本', shortcut: 'Ctrl + D' },
|
||
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' },
|
||
{ 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', when: '!workspace.readonly' },
|
||
{ action: 'copy-id', icon: 'tag', label: '复制资源 ID' },
|
||
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' },
|
||
{ 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', when: '!workspace.readonly' },
|
||
{ action: 'new-folder', icon: 'create_new_folder', label: 'New Folder', disabled: currentSourceKind() !== 'local_folder', title: currentSourceKind() === 'local_folder' ? '在当前目录下创建子文件夹' : '仅 local folder 支持创建文件夹', when: '!workspace.readonly' },
|
||
{ action: 'paste-into', icon: 'content_paste', label: '粘贴到此处', disabled: !sidebarFileTreeClipboard, title: sidebarFileTreeClipboard ? '粘贴到当前文件树目标' : '剪贴板为空', when: '!workspace.readonly' },
|
||
{ 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', when: '!workspace.readonly' },
|
||
{ separator: true },
|
||
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }
|
||
] : [
|
||
{ 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: '重命名', when: '!workspace.readonly' },
|
||
{ separator: true },
|
||
{ action: 'delete-trash', icon: 'delete', label: '删除', shortcut: 'Del', danger: true, when: '!workspace.readonly' }
|
||
];
|
||
items.forEach(function(item) {
|
||
if (item.when !== undefined && !evaluateSidebarFileTreeWhen(ctx, item.when)) {
|
||
item = Object.assign({}, item, { disabled: true, title: item.title || '当前上下文不支持此操作' });
|
||
}
|
||
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() {
|
||
var runtimeFn = fileTreeSelectionRuntimeFunction('visibleFileTreeRows');
|
||
if (runtimeFn) return runtimeFn(fileTreeSelectionRuntimeDeps());
|
||
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 runtimeFn = fileTreeSelectionRuntimeFunction('syncSidebarFileTreeSelection');
|
||
if (runtimeFn) {
|
||
runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps());
|
||
return;
|
||
}
|
||
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) {
|
||
var runtimeFn = fileTreeSelectionRuntimeFunction('selectSidebarFileTreeRow');
|
||
if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection, modifiers, fileTreeSelectionRuntimeDeps());
|
||
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;
|
||
selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false });
|
||
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');
|
||
});
|
||
row.setAttribute('data-active', 'true');
|
||
if (options && options.scrollIntoView !== false) {
|
||
try {
|
||
row.scrollIntoView({ block: 'nearest' });
|
||
} catch (_) {
|
||
row.scrollIntoView();
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function selectSidebarFileTreeRowById(rowId, options) {
|
||
var id = String(rowId || '').trim();
|
||
if (!id) return false;
|
||
var row = document.querySelector('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"][data-row-id="' + cssEscape(id) + '"]');
|
||
if (!(row instanceof HTMLElement)) return false;
|
||
selectSidebarFileTreeRow(row, { ctrlKey: false, metaKey: false, shiftKey: false });
|
||
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');
|
||
});
|
||
row.setAttribute('data-active', 'true');
|
||
if (options && options.scrollIntoView !== false) {
|
||
try {
|
||
row.scrollIntoView({ block: 'nearest' });
|
||
} catch (_) {
|
||
row.scrollIntoView();
|
||
}
|
||
}
|
||
return true;
|
||
}
|
||
|
||
function pendingLocalFolderRestoreRowId() {
|
||
if (window.__mnotePendingLocalFolderRestoreFiletreeRowId) {
|
||
return String(window.__mnotePendingLocalFolderRestoreFiletreeRowId || '').trim();
|
||
}
|
||
if (!window.localStorage) return '';
|
||
var rowId = '';
|
||
try {
|
||
rowId = String(window.localStorage.getItem('mnote.pendingLocalFolderRestoreFiletreeRowId') || '').trim();
|
||
} catch (_) {
|
||
rowId = '';
|
||
}
|
||
if (!rowId && typeof window.name === 'string' && window.name.indexOf('mnote.pendingLocalFolderRestoreFiletreeRowId=') === 0) {
|
||
rowId = decodeURIComponent(window.name.slice('mnote.pendingLocalFolderRestoreFiletreeRowId='.length));
|
||
}
|
||
if (rowId) window.__mnotePendingLocalFolderRestoreFiletreeRowId = rowId;
|
||
return rowId;
|
||
}
|
||
|
||
function clearPendingLocalFolderRestoreRowId() {
|
||
window.__mnotePendingLocalFolderRestoreFiletreeRowId = '';
|
||
try {
|
||
window.localStorage.removeItem('mnote.pendingLocalFolderRestoreFiletreeRowId');
|
||
} catch (_) {}
|
||
if (typeof window.name === 'string' && window.name.indexOf('mnote.pendingLocalFolderRestoreFiletreeRowId=') === 0) {
|
||
window.name = '';
|
||
}
|
||
}
|
||
|
||
function applyPendingLocalFolderRestoreFocusOnce() {
|
||
var rowId = pendingLocalFolderRestoreRowId();
|
||
if (!rowId) return false;
|
||
document.documentElement.setAttribute('data-mnote-local-folder-restore-pending-row-id', rowId);
|
||
if (!selectSidebarFileTreeRowById(rowId, { scrollIntoView: true })) {
|
||
document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'row-missing');
|
||
return false;
|
||
}
|
||
document.documentElement.setAttribute('data-mnote-local-folder-restore-focused-row-id', rowId);
|
||
document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'focused');
|
||
return true;
|
||
}
|
||
|
||
function schedulePendingLocalFolderRestoreFocus() {
|
||
if (!pendingLocalFolderRestoreRowId()) return false;
|
||
if (window.__mnotePendingLocalFolderRestoreFocusTimer) return false;
|
||
var attempts = 0;
|
||
var focused = false;
|
||
focused = applyPendingLocalFolderRestoreFocusOnce() || focused;
|
||
window.__mnotePendingLocalFolderRestoreFocusTimer = window.setInterval(function() {
|
||
attempts += 1;
|
||
focused = applyPendingLocalFolderRestoreFocusOnce() || focused;
|
||
if (attempts >= 20) {
|
||
if (focused) clearPendingLocalFolderRestoreRowId();
|
||
else document.documentElement.setAttribute('data-mnote-local-folder-restore-focus-status', 'timeout');
|
||
window.clearInterval(window.__mnotePendingLocalFolderRestoreFocusTimer);
|
||
window.__mnotePendingLocalFolderRestoreFocusTimer = 0;
|
||
}
|
||
}, 250);
|
||
return focused;
|
||
}
|
||
|
||
function selectedSidebarFileTreeRowIdsForDrag(row) {
|
||
var runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRowIdsForDrag');
|
||
if (runtimeFn) return runtimeFn(row, sidebarFileTreeSelection);
|
||
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 runtimeFn = fileTreeSelectionRuntimeFunction('selectedSidebarFileTreeRows');
|
||
if (runtimeFn) return runtimeFn(sidebarFileTreeSelection, fileTreeSelectionRuntimeDeps());
|
||
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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowDocumentId');
|
||
if (runtimeFn) return runtimeFn(row);
|
||
if (!(row instanceof HTMLElement)) return '';
|
||
return String(row.getAttribute('data-document-id') || row.getAttribute('data-doc-id') || '').trim();
|
||
}
|
||
|
||
function fileTreeRowAssetId(row) {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowAssetId');
|
||
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||
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 runtimeFn = fileTreeRuntimeFunction('decodeLocalEncodedPath');
|
||
if (runtimeFn) return runtimeFn(value);
|
||
var path = String(value || '').trim().replace(/~2F/g, '/');
|
||
if (!path) return '';
|
||
try {
|
||
return decodeURIComponent(path);
|
||
} catch (_) {
|
||
return path;
|
||
}
|
||
}
|
||
|
||
function fileTreeRowLocalRelativePath(row) {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalRelativePath');
|
||
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||
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 fileTreeRowLocalUploadTargetRelativePath(row) {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowLocalUploadTargetRelativePath');
|
||
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||
if (!(row instanceof HTMLElement)) return '';
|
||
var kind = fileTreeRowKind(row);
|
||
var relativePath = fileTreeRowLocalRelativePath(row);
|
||
if (!relativePath) return '';
|
||
if (kind === 'folder' || kind === 'directory') return relativePath;
|
||
var lastSlash = relativePath.lastIndexOf('/');
|
||
return lastSlash >= 0 ? relativePath.slice(0, lastSlash) : '';
|
||
}
|
||
|
||
function fileTreeRowKind(row) {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowKind');
|
||
if (runtimeFn) return runtimeFn(row);
|
||
if (!(row instanceof HTMLElement)) return '';
|
||
return String(row.getAttribute('data-row-kind') || '').trim();
|
||
}
|
||
|
||
function isFileTreeDownloadableAssetRow(row) {
|
||
var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableAssetRow');
|
||
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||
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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('isFileTreeDownloadableRow');
|
||
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||
if (!(row instanceof HTMLElement)) return false;
|
||
if (currentSourceKind() !== 'local_folder') return isFileTreeDownloadableAssetRow(row);
|
||
return Boolean(fileTreeRowLocalRelativePath(row) || isFileTreeDownloadableAssetRow(row));
|
||
}
|
||
|
||
function fileTreeAssetDownloadDetail(row) {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeAssetDownloadDetail');
|
||
if (runtimeFn) return runtimeFn(row, fileTreeRuntimeDeps());
|
||
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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeRowsByRowIds');
|
||
if (runtimeFn) return runtimeFn(rowIds, fileTreeRuntimeDeps());
|
||
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) {
|
||
var runtimeFn = fileTreeRuntimeFunction('fileTreeChildCount');
|
||
if (runtimeFn) return runtimeFn(documentId, fileTreeRuntimeDeps());
|
||
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 = '' +
|
||
'<div class="wolai-search-dialog">' +
|
||
'<div class="wolai-search-input-row">' +
|
||
'<span class="material-symbols-outlined wolai-search-input-icon" data-icon="search" aria-hidden="true"></span>' +
|
||
'<input data-testid="wolai-search-input" class="wolai-search-input" type="search" autocomplete="off" placeholder="在当前工作区中搜索" />' +
|
||
'<button type="button" class="wolai-search-close" data-testid="wolai-search-close" aria-label="关闭搜索">×</button>' +
|
||
'</div>' +
|
||
'<div class="wolai-search-options" data-testid="wolai-search-options" aria-label="搜索选项" hidden>' +
|
||
'<div class="wolai-search-options-left">' +
|
||
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch" data-search-switch="title" role="switch" aria-checked="false" aria-label="仅匹配标题"></button></span>' +
|
||
'<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
|
||
'<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
|
||
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
|
||
'</div>' +
|
||
'<div class="wolai-search-options-right">' +
|
||
'<span class="wolai-search-switch-control"><span>页面内搜索</span><button type="button" class="wolai-search-switch" data-search-switch="page" role="switch" aria-checked="false" aria-label="页面内搜索"></button></span>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="wolai-search-result-meta" data-testid="wolai-search-result-meta"></div>' +
|
||
'<div class="wolai-search-results" data-testid="wolai-search-results" data-search-results-owner="rust-kernel"></div>' +
|
||
'</div>';
|
||
|
||
document.body.appendChild(overlay);
|
||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||
var closeButton = overlay.querySelector('[data-testid="wolai-search-close"]');
|
||
if (input) input.addEventListener('input', scheduleSearchResultsRender);
|
||
overlay.querySelectorAll('[data-search-switch]').forEach(function(button) {
|
||
button.addEventListener('click', function() {
|
||
var isOn = button.getAttribute('aria-checked') !== 'true';
|
||
button.setAttribute('aria-checked', isOn ? 'true' : 'false');
|
||
button.classList.toggle('is-on', isOn);
|
||
scheduleSearchResultsRender();
|
||
});
|
||
});
|
||
if (closeButton) closeButton.addEventListener('click', closeSearchModal);
|
||
overlay.addEventListener('click', function(event) {
|
||
if (event.target === overlay) closeSearchModal();
|
||
});
|
||
return overlay;
|
||
}
|
||
|
||
var activeSearchRequestId = 0;
|
||
var searchRenderTimer = 0;
|
||
|
||
function searchText(value) {
|
||
return String(value == null ? '' : value).replace(/\s+/g, ' ').trim();
|
||
}
|
||
|
||
function currentWorkspaceName() {
|
||
var name = document.querySelector('.sidebar-workspace-name');
|
||
return searchText(name && name.textContent) || '当前工作区';
|
||
}
|
||
|
||
function currentDocumentId() {
|
||
var shell = document.querySelector('.document-shell[data-document-id]');
|
||
var bodyId = document.body && document.body.getAttribute('data-document-id');
|
||
return searchText(shell && shell.getAttribute('data-document-id')) || searchText(bodyId);
|
||
}
|
||
|
||
function searchSwitchValue(overlay, name) {
|
||
var button = overlay.querySelector('[data-search-switch="' + name + '"]');
|
||
return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true';
|
||
}
|
||
|
||
function highlightedHtml(value) {
|
||
return escapeHtml(value)
|
||
.replace(/<mark>/g, '<mark>')
|
||
.replace(/<\/mark>/g, '</mark>');
|
||
}
|
||
|
||
function highlightSearchTitle(title, query) {
|
||
var cleanTitle = searchText(title);
|
||
var cleanQuery = searchText(query);
|
||
if (!cleanQuery) return escapeHtml(cleanTitle);
|
||
var index = cleanTitle.toLowerCase().indexOf(cleanQuery.toLowerCase());
|
||
if (index < 0) return escapeHtml(cleanTitle);
|
||
return escapeHtml(cleanTitle.slice(0, index)) +
|
||
'<mark>' + escapeHtml(cleanTitle.slice(index, index + cleanQuery.length)) + '</mark>' +
|
||
escapeHtml(cleanTitle.slice(index + cleanQuery.length));
|
||
}
|
||
|
||
function renderSearchRecentState(overlay) {
|
||
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
|
||
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
|
||
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
|
||
if (options instanceof HTMLElement) options.hidden = true;
|
||
if (meta) meta.innerHTML = '<span>最近浏览</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||
if (results) {
|
||
results.innerHTML = '<div class="wolai-search-recent" data-testid="wolai-search-recent">暂无最近浏览</div>';
|
||
}
|
||
}
|
||
|
||
function scheduleSearchResultsRender() {
|
||
window.clearTimeout(searchRenderTimer);
|
||
searchRenderTimer = window.setTimeout(function() { void renderSearchResults(); }, 120);
|
||
}
|
||
|
||
async function renderSearchResults() {
|
||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||
if (!(overlay instanceof HTMLElement)) return;
|
||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||
var options = overlay.querySelector('[data-testid="wolai-search-options"]');
|
||
var meta = overlay.querySelector('[data-testid="wolai-search-result-meta"]');
|
||
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
|
||
if (!input || !meta || !results) return;
|
||
var query = searchText(input.value);
|
||
if (!query) {
|
||
activeSearchRequestId += 1;
|
||
renderSearchRecentState(overlay);
|
||
return;
|
||
}
|
||
if (options instanceof HTMLElement) options.hidden = false;
|
||
var requestId = ++activeSearchRequestId;
|
||
meta.innerHTML = '<span>搜索中…</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||
results.innerHTML = '<div class="wolai-search-empty" data-search-loading="true">搜索中…</div>';
|
||
try {
|
||
var response = await fetch('/api/search/documents', {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify({
|
||
workspaceId: resolveWorkspaceId(document.body),
|
||
sourceKind: currentSourceKind() || null,
|
||
rootUri: currentRootUri() || null,
|
||
documentId: currentDocumentId() || null,
|
||
query: query,
|
||
limit: 30,
|
||
filters: {
|
||
titleOnly: searchSwitchValue(overlay, 'title'),
|
||
exact: searchSwitchValue(overlay, 'exact'),
|
||
includeOcr: false,
|
||
onlyCurrentPage: searchSwitchValue(overlay, 'page'),
|
||
timeRange: 'any',
|
||
timeField: 'updated'
|
||
}
|
||
})
|
||
});
|
||
var payload = await response.json();
|
||
if (requestId !== activeSearchRequestId) return;
|
||
var items = Array.isArray(payload.results) ? payload.results : [];
|
||
meta.innerHTML = '<span>共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||
if (!items.length) {
|
||
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
|
||
return;
|
||
}
|
||
results.innerHTML = items.map(function(item) {
|
||
var title = searchText(item.title || item.name || item.documentTitle || '无标题');
|
||
var snippet = searchText(item.snippet || (Array.isArray(item.evidence) && item.evidence[0] && item.evidence[0].snippet) || '');
|
||
var path = searchText(item.path || item.parentTitle || item.workspaceName || currentWorkspaceName());
|
||
var resourceType = searchText(item.resourceType || item.resourceKind || 'page');
|
||
return '<button type="button" class="wolai-search-result-row" data-testid="wolai-search-result-row" data-search-result-owner="rust-kernel" data-document-id="' + escapeHtml(item.documentId || item.nodeId || item.id || '') + '" data-resource-type="' + escapeHtml(resourceType) + '">' +
|
||
'<span class="material-symbols-outlined wolai-search-result-icon" data-icon="article" aria-hidden="true"></span>' +
|
||
'<span class="wolai-search-result-main"><span class="wolai-search-result-title">' + highlightSearchTitle(title, query) + '</span>' +
|
||
'<span class="wolai-search-result-snippet">' + (snippet ? highlightedHtml(snippet) : escapeHtml(path)) + '</span>' +
|
||
'<span class="wolai-search-result-path"><span>' + escapeHtml(path) + '</span><span class="wolai-search-result-type">' + escapeHtml(resourceType) + '</span></span></span>' +
|
||
'</button>';
|
||
}).join('');
|
||
} catch (error) {
|
||
if (requestId !== activeSearchRequestId) return;
|
||
meta.innerHTML = '<span>共 0 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
|
||
}
|
||
}
|
||
|
||
function isSearchModalOpen() {
|
||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||
return overlay instanceof HTMLElement && !overlay.hidden;
|
||
}
|
||
|
||
function openSearchModal() {
|
||
var overlay = ensureSearchModal();
|
||
overlay.hidden = false;
|
||
document.documentElement.setAttribute('data-mnote-search-modal-open', 'true');
|
||
var input = overlay.querySelector('[data-testid="wolai-search-input"]');
|
||
if (input) input.setAttribute('placeholder', '在 ' + currentWorkspaceName() + ' 中搜索');
|
||
void renderSearchResults();
|
||
if (input) {
|
||
setTimeout(function() { input.focus(); input.select(); }, 0);
|
||
}
|
||
}
|
||
|
||
function closeSearchModal() {
|
||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||
if (overlay instanceof HTMLElement) overlay.hidden = true;
|
||
document.documentElement.removeAttribute('data-mnote-search-modal-open');
|
||
}
|
||
|
||
function toggleSearchModal() {
|
||
if (isSearchModalOpen()) closeSearchModal();
|
||
else openSearchModal();
|
||
}
|
||
|
||
function updatePageSettingsTriggerState() {
|
||
var trigger = document.querySelector('[data-testid="wolai-page-settings-trigger"]');
|
||
if (!(trigger instanceof HTMLElement)) return;
|
||
trigger.setAttribute('data-state', pageUiState.pageSettingsOpen ? 'open' : 'closed');
|
||
trigger.setAttribute('aria-expanded', pageUiState.pageSettingsOpen ? 'true' : 'false');
|
||
}
|
||
|
||
function updatePageAiTriggerState() {
|
||
var trigger = document.querySelector('[data-testid="wolai-floating-ai"]');
|
||
if (!(trigger instanceof HTMLElement)) return;
|
||
trigger.setAttribute('data-state', pageUiState.pageAiOpen ? 'open' : 'closed');
|
||
trigger.setAttribute('aria-expanded', pageUiState.pageAiOpen ? 'true' : 'false');
|
||
}
|
||
|
||
function createPageOptionRow(key, type) {
|
||
var inputType = type || 'checkbox';
|
||
var supported = pageOptionIsSupported(key);
|
||
if (inputType === 'checkbox') {
|
||
return '' +
|
||
'<label class="wolai-page-setting-row' + (supported ? '' : ' is-pending') + '" data-page-setting-row="' + key + '">' +
|
||
'<span class="wolai-page-setting-copy">' +
|
||
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
|
||
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
|
||
'</span>' +
|
||
'<input type="checkbox" class="wolai-page-setting-checkbox" data-page-option-checkbox="' + key + '"' + (supported ? '' : ' data-setting-pending="true"') + ' />' +
|
||
'</label>';
|
||
}
|
||
return '' +
|
||
'<label class="wolai-page-setting-row" data-page-setting-row="' + key + '">' +
|
||
'<span class="wolai-page-setting-copy">' +
|
||
'<span class="wolai-page-setting-label">' + escapeHtml(pageOptionDescription(key)) + '</span>' +
|
||
'<span class="wolai-page-setting-hint">' + escapeHtml(pageOptionHint(key)) + '</span>' +
|
||
'</span>' +
|
||
'<select class="wolai-page-setting-select" data-page-option-select="' + key + '">' +
|
||
'<option value="compact">紧凑</option>' +
|
||
'<option value="normal">默认</option>' +
|
||
'<option value="spacious">宽松</option>' +
|
||
'</select>' +
|
||
'</label>';
|
||
}
|
||
|
||
function createGlobalHeadingNumbersRow() {
|
||
return '' +
|
||
'<label class="wolai-page-setting-row" data-page-setting-row="globalShowHeadingNumbers">' +
|
||
'<span class="wolai-page-setting-copy">' +
|
||
'<span class="wolai-page-setting-label">标题自动编号</span>' +
|
||
'</span>' +
|
||
'<input type="checkbox" class="wolai-page-setting-checkbox" data-global-option-checkbox="showHeadingNumbers" />' +
|
||
'</label>';
|
||
}
|
||
|
||
function renderGlobalOptions(popover) {
|
||
var globalHeadingNumbers = readGlobalShowHeadingNumbers();
|
||
popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
|
||
input.checked = globalHeadingNumbers;
|
||
});
|
||
}
|
||
|
||
function createPageFontRow() {
|
||
return '' +
|
||
'<label class="wolai-page-setting-row" data-page-setting-row="pageFont">' +
|
||
'<span class="wolai-page-setting-copy">' +
|
||
'<span class="wolai-page-setting-label">页面字体</span>' +
|
||
'<span class="wolai-page-setting-hint">已接通:仅对当前页面生效</span>' +
|
||
'</span>' +
|
||
'<select class="wolai-page-setting-select" data-page-option-select="pageFont">' +
|
||
'<option value="default">默认</option>' +
|
||
'<option value="song">宋体</option>' +
|
||
'<option value="kai">楷体</option>' +
|
||
'</select>' +
|
||
'</label>';
|
||
}
|
||
|
||
function ensurePageHistoryDrawer() {
|
||
var existing = document.querySelector('[data-testid="wolai-page-history-drawer"]');
|
||
if (existing instanceof HTMLElement) return existing;
|
||
var drawer = document.createElement('aside');
|
||
drawer.className = 'wolai-page-history-drawer';
|
||
drawer.setAttribute('data-testid', 'wolai-page-history-drawer');
|
||
drawer.setAttribute('data-mnote-surface', 'page-history');
|
||
drawer.hidden = true;
|
||
drawer.innerHTML = '' +
|
||
'<div class="wolai-page-history-panel">' +
|
||
'<div class="wolai-page-history-header">' +
|
||
'<div><div class="wolai-page-history-title">页面历史</div><div class="wolai-page-history-subtitle">当前会话内最近保存的 15 个快照。</div></div>' +
|
||
'<button type="button" class="wolai-surface-close" data-page-history-action="close" aria-label="关闭页面历史">×</button>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-history-list" data-page-history-list></div>' +
|
||
'</div>';
|
||
drawer.addEventListener('click', function(event) {
|
||
if (event.target === drawer) closePageHistoryDrawer();
|
||
});
|
||
document.body.appendChild(drawer);
|
||
return drawer;
|
||
}
|
||
|
||
function renderPageHistoryDrawer() {
|
||
var drawer = ensurePageHistoryDrawer();
|
||
var list = drawer.querySelector('[data-page-history-list]');
|
||
if (!(list instanceof HTMLElement)) return;
|
||
ensureHistorySnapshotsSeeded();
|
||
list.innerHTML = pageUiState.historySnapshots.length
|
||
? pageUiState.historySnapshots.map(function(snapshot) {
|
||
var stats = snapshot.stats || {};
|
||
var label = new Date(snapshot.timestamp).toLocaleString('zh-CN', {
|
||
year: 'numeric',
|
||
month: '2-digit',
|
||
day: '2-digit',
|
||
hour: '2-digit',
|
||
minute: '2-digit'
|
||
});
|
||
return '' +
|
||
'<div class="wolai-page-history-item">' +
|
||
'<div class="wolai-page-history-item-copy">' +
|
||
'<div class="wolai-page-history-item-title">' + escapeHtml(label) + '</div>' +
|
||
'<div class="wolai-page-history-item-meta">字数 ' + Number(stats.wordCount || 0) + ' · 字符 ' + Number(stats.characterCount || 0) + ' · 块数 ' + Number(stats.blockCount || 0) + '</div>' +
|
||
'</div>' +
|
||
'<button type="button" class="wolai-page-history-item-ghost" data-page-history-action="noop">仅查看</button>' +
|
||
'</div>';
|
||
}).join('')
|
||
: '<div class="wolai-page-history-empty">尚未产生历史快照,编辑后会自动生成。</div>';
|
||
}
|
||
|
||
function openPageHistoryDrawer() {
|
||
renderPageHistoryDrawer();
|
||
var drawer = ensurePageHistoryDrawer();
|
||
drawer.hidden = false;
|
||
document.documentElement.setAttribute('data-mnote-page-history-open', 'true');
|
||
}
|
||
|
||
function closePageHistoryDrawer() {
|
||
var drawer = document.querySelector('[data-testid="wolai-page-history-drawer"]');
|
||
if (drawer instanceof HTMLElement) drawer.hidden = true;
|
||
document.documentElement.removeAttribute('data-mnote-page-history-open');
|
||
}
|
||
|
||
function ensurePageShareDialog() {
|
||
var existing = document.querySelector('[data-testid="wolai-page-share-dialog"]');
|
||
if (existing instanceof HTMLElement) return existing;
|
||
var dialog = document.createElement('div');
|
||
dialog.className = 'wolai-page-share-dialog';
|
||
dialog.setAttribute('data-testid', 'wolai-page-share-dialog');
|
||
dialog.setAttribute('data-mnote-surface', 'page-share');
|
||
dialog.hidden = true;
|
||
dialog.innerHTML = '' +
|
||
'<div class="wolai-page-share-card" role="dialog" aria-modal="true">' +
|
||
'<div class="wolai-page-share-header">' +
|
||
'<div><div class="wolai-page-share-title">公开分享页面</div><div class="wolai-page-share-subtitle">当前 3000 公开入口由 mnote-web 持有。</div></div>' +
|
||
'<button type="button" class="wolai-surface-close" data-page-share-action="close" aria-label="关闭公开分享页面">×</button>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-share-state">' +
|
||
'<span class="wolai-public-pill wolai-public-pill--inline">全网公开</span>' +
|
||
'<span class="wolai-page-share-copy">任何拥有链接的人都可以访问当前页面。</span>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-share-url-row">' +
|
||
'<input class="wolai-page-share-url" type="text" readonly data-page-share-url value="" />' +
|
||
'<button type="button" class="wolai-page-share-copy-button" data-page-share-action="copy-link">复制链接</button>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-share-footer">更多共享者、群组公开和权限策略仍待接线。</div>' +
|
||
'</div>';
|
||
dialog.addEventListener('click', function(event) {
|
||
if (event.target === dialog) closePageShareDialog();
|
||
});
|
||
document.body.appendChild(dialog);
|
||
return dialog;
|
||
}
|
||
|
||
function openPageShareDialog() {
|
||
var dialog = ensurePageShareDialog();
|
||
var input = dialog.querySelector('[data-page-share-url]');
|
||
if (input instanceof HTMLInputElement) input.value = window.location.href;
|
||
dialog.hidden = false;
|
||
document.documentElement.setAttribute('data-mnote-page-share-open', 'true');
|
||
}
|
||
|
||
function closePageShareDialog() {
|
||
var dialog = document.querySelector('[data-testid="wolai-page-share-dialog"]');
|
||
if (dialog instanceof HTMLElement) dialog.hidden = true;
|
||
document.documentElement.removeAttribute('data-mnote-page-share-open');
|
||
}
|
||
|
||
function pageAiSuggestions() {
|
||
var title = searchText(document.querySelector('[data-page-title-current="true"]')?.textContent) || '当前页面';
|
||
return [
|
||
'帮我总结《' + title + '》当前内容',
|
||
'把当前页面改写得更简洁一些',
|
||
'提炼当前页的关键待办和行动项',
|
||
'基于当前页内容生成一个三段式摘要'
|
||
];
|
||
}
|
||
|
||
function pageAiStorageKey() {
|
||
return 'hermes_page_ai_session:' + currentDocumentId();
|
||
}
|
||
|
||
function pageAiTimestamp(value) {
|
||
if (typeof value === 'number' && Number.isFinite(value)) return value;
|
||
if (typeof value === 'string' && value.trim()) {
|
||
var parsed = Date.parse(value);
|
||
if (Number.isFinite(parsed)) return parsed;
|
||
}
|
||
return Date.now();
|
||
}
|
||
|
||
function pageAiBackendSessionQuery(extra) {
|
||
var params = new URLSearchParams();
|
||
params.set('source', 'acp');
|
||
params.set('workspaceId', resolveWorkspaceId(document.body));
|
||
params.set('documentId', currentDocumentId());
|
||
params.set('profile', pageAiRunProfile());
|
||
params.set('sourceKind', currentSourceKind());
|
||
if (currentRootUri()) params.set('rootUri', currentRootUri());
|
||
Object.keys(extra || {}).forEach(function(key) {
|
||
var value = extra[key];
|
||
if (value !== undefined && value !== null && String(value).trim() !== '') {
|
||
params.set(key, String(value));
|
||
}
|
||
});
|
||
return params.toString();
|
||
}
|
||
|
||
function pageAiNewSession(title) {
|
||
var now = Date.now();
|
||
return {
|
||
id: 'sess_' + now + '_' + Math.random().toString(16).slice(2, 8),
|
||
title: title || '新会话',
|
||
profile: pageAiCurrentProfile(),
|
||
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
|
||
createdAt: now,
|
||
updatedAt: now,
|
||
source: 'local',
|
||
usage: null,
|
||
status: 'idle',
|
||
messages: []
|
||
};
|
||
}
|
||
|
||
function pageAiUsageSummary(usage) {
|
||
if (!usage || typeof usage !== 'object') return '';
|
||
var used = usage.used ?? usage.contextUsed ?? usage.input_tokens ?? usage.inputTokens ?? usage.prompt_tokens ?? usage.promptTokens;
|
||
var size = usage.size ?? usage.contextSize ?? usage.total_tokens ?? usage.totalTokens;
|
||
var output = usage.output_tokens ?? usage.outputTokens ?? usage.completion_tokens ?? usage.completionTokens;
|
||
var parts = [];
|
||
if (Number.isFinite(Number(used))) parts.push('ctx ' + Number(used));
|
||
if (Number.isFinite(Number(size)) && Number(size) > 0) parts.push('/ ' + Number(size));
|
||
if (Number.isFinite(Number(output)) && Number(output) > 0) parts.push('out ' + Number(output));
|
||
return parts.join(' ') || '';
|
||
}
|
||
|
||
function pageAiPermissionMessage(payload, eventType) {
|
||
payload = payload && typeof payload === 'object' ? payload : {};
|
||
var permissionId = String(payload.permissionId || payload.permission_id || payload.id || ('perm_' + Date.now())).trim();
|
||
var toolName = String(payload.toolName || payload.tool || payload.name || payload.method || 'session/request_permission').trim();
|
||
var args = payload.arguments || payload.args || payload.input || payload.params || payload;
|
||
var decision = String(payload.decision || payload.result || '').trim();
|
||
if (!decision && eventType === 'permission.denied') decision = 'denied';
|
||
if (!decision && eventType === 'permission.allowed') decision = 'allowed';
|
||
return {
|
||
role: 'tool',
|
||
kind: 'permission',
|
||
permissionId: permissionId,
|
||
toolName: toolName,
|
||
argsSummary: pageAiPreviewValue(args),
|
||
content: decision === 'denied' ? '已自动拒绝权限请求' : (decision === 'allowed' ? '已自动允许权限请求' : '等待权限确认'),
|
||
resolved: decision === 'denied' || decision === 'allowed',
|
||
decision: decision
|
||
};
|
||
}
|
||
|
||
function pageAiApplyPermissionEvent(eventName, payloadText) {
|
||
var payload = null;
|
||
try {
|
||
payload = JSON.parse(payloadText || 'null');
|
||
} catch (_) {
|
||
payload = {};
|
||
}
|
||
var message = pageAiPermissionMessage(payload, eventName);
|
||
var existing = pageUiState.pageAiMessages.find(function(item) {
|
||
return item.kind === 'permission' && item.permissionId === message.permissionId;
|
||
});
|
||
if (existing) {
|
||
Object.assign(existing, message);
|
||
} else {
|
||
pageUiState.pageAiMessages.push(message);
|
||
}
|
||
pageUiState.pageAiPermissionRequests = pageUiState.pageAiPermissionRequests.filter(function(item) {
|
||
return item.permissionId !== message.permissionId;
|
||
}).concat([message]).slice(-20);
|
||
if (!message.resolved) {
|
||
pageAiShowPermissionDialog(message);
|
||
} else {
|
||
pageAiHidePermissionDialog();
|
||
}
|
||
}
|
||
|
||
function pageAiResolvePermission(permissionId, decision) {
|
||
permissionId = String(permissionId || '').trim();
|
||
if (!permissionId) return;
|
||
// 调用后端 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 = '' +
|
||
'<div class="wolai-page-ai-permission-panel" role="dialog" aria-modal="false">' +
|
||
'<div class="wolai-page-ai-memory-title" data-page-ai-permission-tool></div>' +
|
||
'<div class="wolai-page-ai-tool-meta" data-page-ai-permission-args></div>' +
|
||
'<div class="wolai-page-ai-message-actions">' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-dialog-action>允许</button>' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-dialog-action>拒绝</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
document.body.appendChild(dialog);
|
||
}
|
||
var tool = dialog.querySelector('[data-page-ai-permission-tool]');
|
||
if (tool instanceof HTMLElement) tool.textContent = message.toolName || 'session/request_permission';
|
||
var args = dialog.querySelector('[data-page-ai-permission-args]');
|
||
if (args instanceof HTMLElement) args.textContent = message.argsSummary || message.content || '';
|
||
dialog.querySelectorAll('[data-page-ai-permission-dialog-action]').forEach(function(button) {
|
||
if (button instanceof HTMLButtonElement) {
|
||
button.setAttribute('data-page-ai-permission-id', message.permissionId || '');
|
||
button.disabled = Boolean(message.resolved);
|
||
}
|
||
});
|
||
dialog.hidden = false;
|
||
}
|
||
|
||
function pageAiNormalizeSessions(sessions) {
|
||
return (Array.isArray(sessions) ? sessions : [])
|
||
.slice(0, 20)
|
||
.map(function(session) {
|
||
var sessionStorage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
|
||
var persistence = String(session && session.persistence || '').trim();
|
||
return {
|
||
id: String(session && session.id || '').trim() || pageAiNewSession().id,
|
||
title: String(session && session.title || '').trim() || '新会话',
|
||
profile: String(session && session.profile || pageAiCurrentProfile()).trim() || 'default',
|
||
acpRuntime: String(session && session.acpRuntime || 'reasonix').trim(),
|
||
createdAt: pageAiTimestamp(session && session.createdAt),
|
||
updatedAt: pageAiTimestamp(session && session.updatedAt),
|
||
source: String(session && session.source || 'local').trim() || 'local',
|
||
persistence: persistence,
|
||
sessionStorage: sessionStorage,
|
||
permissionLevel: String(session && (session.permissionLevel || session.permission_level) || '').trim(),
|
||
shareId: String(session && (session.shareId || session.share_id) || '').trim(),
|
||
runId: String(session && (session.runId || session.run_id) || '').trim(),
|
||
status: String(session && session.status || '').trim(),
|
||
usage: session && session.usage && typeof session.usage === 'object' ? session.usage : null,
|
||
preview: String(session && session.preview || '').trim(),
|
||
messages: Array.isArray(session && session.messages) ? session.messages.slice(-300) : []
|
||
};
|
||
})
|
||
.sort(function(a, b) {
|
||
return Number(b.updatedAt || 0) - Number(a.updatedAt || 0);
|
||
});
|
||
}
|
||
|
||
function pageAiNormalizeBackendSessionRow(row) {
|
||
if (!row || typeof row !== 'object') return null;
|
||
var payload = row.payload && typeof row.payload === 'object' ? row.payload : {};
|
||
var sessionId = String(row.sessionId || row.session_id || '').trim();
|
||
if (!sessionId) return null;
|
||
var title = String(row.title || payload.title || payload.message || '').trim();
|
||
if (title.length > 28) title = title.slice(0, 28) + '…';
|
||
var persistence = String(row.persistence || payload.persistence || '').trim();
|
||
var sessionStorage = String(row.sessionStorage || row.session_storage || payload.sessionStorage || '').trim();
|
||
if (!sessionStorage && persistence === 'convex_acp_runtime_store') sessionStorage = 'cloud';
|
||
if (!sessionStorage && persistence === 'local_ai_session_jsonl') sessionStorage = String(row.shareId || payload.shareId || '').trim() ? 'local_shared' : 'local_private';
|
||
return {
|
||
id: sessionId,
|
||
title: title || '当前页问答',
|
||
profile: String(row.profile || payload.profile || pageAiRunProfile()).trim() || 'default',
|
||
acpRuntime: String(row.acpRuntime || row.acp_runtime || payload.acpRuntime || 'reasonix').trim(),
|
||
createdAt: pageAiTimestamp(row.createdAt || row.created_at),
|
||
updatedAt: pageAiTimestamp(row.updatedAt || row.updated_at),
|
||
source: sessionStorage === 'local_private' || sessionStorage === 'local_shared' ? 'local' : 'acp',
|
||
persistence: persistence,
|
||
sessionStorage: sessionStorage,
|
||
permissionLevel: String(row.permissionLevel || row.permission_level || payload.permissionLevel || '').trim(),
|
||
shareId: String(row.shareId || row.share_id || payload.shareId || '').trim(),
|
||
runId: String(row.runId || row.run_id || '').trim(),
|
||
status: String(row.status || (row.runtime && row.runtime.status) || '').trim(),
|
||
usage: row.usage && typeof row.usage === 'object' ? row.usage : null,
|
||
preview: String(payload.message || row.snippet || '').trim(),
|
||
messages: []
|
||
};
|
||
}
|
||
|
||
function pageAiMergeSessions(localSessions, backendSessions) {
|
||
var byId = {};
|
||
pageAiNormalizeSessions(localSessions).forEach(function(session) {
|
||
byId[session.id] = session;
|
||
});
|
||
pageAiNormalizeSessions(backendSessions).forEach(function(session) {
|
||
var existing = byId[session.id];
|
||
byId[session.id] = Object.assign({}, existing || {}, session, {
|
||
messages: session.messages.length ? session.messages : (existing && existing.messages || [])
|
||
});
|
||
});
|
||
return pageAiNormalizeSessions(Object.keys(byId).map(function(id) { return byId[id]; }));
|
||
}
|
||
|
||
function pageAiSessionStorageLabel(session) {
|
||
var storage = String(session && (session.sessionStorage || session.session_storage) || '').trim();
|
||
var persistence = String(session && session.persistence || '').trim();
|
||
if (storage === 'local_shared') return '共享会话';
|
||
if (storage === 'local_private') return '本地私有';
|
||
if (storage === 'cloud' || persistence === 'convex_acp_runtime_store') return '云端会话';
|
||
if (persistence === 'local_ai_session_jsonl') return '本地私有';
|
||
return String(session && session.source || '').trim() === 'acp' ? '云端会话' : '本地私有';
|
||
}
|
||
|
||
function pageAiLoadSessions() {
|
||
if (pageUiState.pageAiSessions.length && pageUiState.pageAiActiveSessionId) return;
|
||
try {
|
||
var raw = window.localStorage.getItem(pageAiStorageKey());
|
||
var parsed = raw ? JSON.parse(raw) : null;
|
||
var activeId = String(parsed && parsed.activeSessionId || '').trim();
|
||
var activeProfile = String(parsed && parsed.activeProfileName || '').trim();
|
||
var activeAcpRuntime = String(parsed && parsed.activeAcpRuntime || '').trim();
|
||
var sessions = pageAiNormalizeSessions(parsed && parsed.sessions);
|
||
var storageVersion = Number(parsed && parsed.version || 0);
|
||
if (storageVersion >= PAGE_AI_SESSION_STORAGE_VERSION && activeAcpRuntime) pageUiState.pageAiAcpRuntime = activeAcpRuntime;
|
||
if (activeProfile) pageAiSetActiveProfile(activeProfile);
|
||
if (sessions.length) {
|
||
pageUiState.pageAiSessions = sessions;
|
||
var activeSession = sessions.find(function(session) { return session.id === activeId; }) || sessions[0];
|
||
pageUiState.pageAiActiveSessionId = activeSession.id;
|
||
if (activeSession.profile) pageAiSetActiveProfile(activeSession.profile);
|
||
if (!pageUiState.pageAiAcpRuntime && activeSession.acpRuntime) pageUiState.pageAiAcpRuntime = activeSession.acpRuntime;
|
||
pageUiState.pageAiMessages = Array.isArray(activeSession.messages) ? activeSession.messages.slice() : [];
|
||
return;
|
||
}
|
||
if (activeId) {
|
||
pageUiState.pageAiSessions = [pageAiNewSession('当前页问答')];
|
||
pageUiState.pageAiSessions[0].id = activeId;
|
||
pageUiState.pageAiActiveSessionId = activeId;
|
||
pageUiState.pageAiMessages = [];
|
||
return;
|
||
}
|
||
} catch (_) {}
|
||
var fresh = pageAiNewSession();
|
||
pageUiState.pageAiSessions = [fresh];
|
||
pageUiState.pageAiActiveSessionId = fresh.id;
|
||
pageUiState.pageAiMessages = [];
|
||
}
|
||
|
||
async function pageAiLoadBackendSessions() {
|
||
var response = await fetch('/api/hermes/client/sessions?' + pageAiBackendSessionQuery({ limit: 50 }), {
|
||
headers: { 'accept': 'application/json' }
|
||
});
|
||
var payload = await response.json().catch(function(){ return null; });
|
||
if (!response.ok || !payload || payload.ok !== true) {
|
||
throw new Error(pageAiErrorMessage(payload, 'session_list_failed_' + response.status));
|
||
}
|
||
var backendSessions = pageAiNormalizeArray(payload.sessions).map(pageAiNormalizeBackendSessionRow).filter(Boolean);
|
||
if (!backendSessions.length) return [];
|
||
pageUiState.pageAiSessions = pageAiMergeSessions(pageUiState.pageAiSessions, backendSessions);
|
||
if (!pageUiState.pageAiActiveSessionId || !pageUiState.pageAiSessions.find(function(session) { return session.id === pageUiState.pageAiActiveSessionId; })) {
|
||
pageUiState.pageAiActiveSessionId = pageUiState.pageAiSessions[0].id;
|
||
}
|
||
var active = pageAiCurrentSession();
|
||
if (active) {
|
||
if (active.profile) pageAiSetActiveProfile(active.profile);
|
||
if (!pageUiState.pageAiAcpRuntime && active.acpRuntime) pageUiState.pageAiAcpRuntime = active.acpRuntime;
|
||
pageUiState.pageAiMessages = Array.isArray(active.messages) ? active.messages.slice() : pageUiState.pageAiMessages;
|
||
}
|
||
pageUiState.pageAiSessionError = '';
|
||
pageAiPersistSessions();
|
||
renderPageAiConversation();
|
||
renderPageAiControls();
|
||
return backendSessions;
|
||
}
|
||
|
||
function pageAiMessageFromRuntimeEvent(event) {
|
||
if (!event || typeof event !== 'object') return null;
|
||
var eventType = String(event.eventType || event.event_type || event.event || '').trim();
|
||
var payload = event.payload && typeof event.payload === 'object' ? event.payload : event;
|
||
if (eventType === 'message.delta') {
|
||
var delta = String(payload.delta || payload.text || payload.output_text || '').trim();
|
||
return delta ? { role: 'assistant', content: delta } : null;
|
||
}
|
||
if (eventType === 'thought.delta') {
|
||
var thought = String(payload.delta || payload.text || '').trim();
|
||
return thought ? { role: 'assistant', kind: 'thought', content: thought } : null;
|
||
}
|
||
if (eventType === 'tool.started' || eventType === 'tool.completed' || eventType === 'tool.failed') {
|
||
var toolName = String(payload.toolName || payload.tool || payload.name || eventType).trim();
|
||
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 '<option value="' + escapeHtml(rt.name || '') + '"' + ((rt.name || '') === pageUiState.pageAiAcpRuntime ? ' selected' : '') + '>' + escapeHtml(rt.title || rt.name) + '</option>';
|
||
}).join('');
|
||
acpSelect.value = pageUiState.pageAiAcpRuntime || 'reasonix';
|
||
}
|
||
// Show/hide Hermes-specific profile select
|
||
var profileLabel = drawer.querySelector('[data-page-ai-hermes-profile]');
|
||
if (profileLabel instanceof HTMLElement) {
|
||
profileLabel.style.display = pageUiState.pageAiAcpRuntime === 'reasonix' ? 'none' : '';
|
||
}
|
||
// When ACP is selected, populate agent panel with ACP runtime info
|
||
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
|
||
if (agentPanel instanceof HTMLElement) {
|
||
if (pageUiState.pageAiAcpRuntime === 'reasonix') {
|
||
var rt = pageUiState.pageAiAcpRuntimes.find(function(r) { return r.name === pageUiState.pageAiAcpRuntime; }) || {};
|
||
var keyStatus = rt.apiKeyConfigured ? '已配置' : '未检测到 DEEPSEEK_API_KEY';
|
||
agentPanel.innerHTML = '' +
|
||
'<section class="wolai-page-ai-memory-card">' +
|
||
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">' + escapeHtml(rt.title || 'ACP Runtime') + '</div></div>' +
|
||
'</section>' +
|
||
'<section class="wolai-page-ai-memory-card">' +
|
||
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">模型</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(rt.model || 'deepseek-chat') + '</div></div>' +
|
||
'</section>' +
|
||
'<section class="wolai-page-ai-memory-card">' +
|
||
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">Preset</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(rt.preset || 'auto') + '</div></div>' +
|
||
'</section>' +
|
||
'<section class="wolai-page-ai-memory-card">' +
|
||
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">API Key</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(keyStatus) + '</div></div>' +
|
||
'</section>' +
|
||
'<section class="wolai-page-ai-memory-card">' +
|
||
'<div class="wolai-page-ai-memory-head"><div class="wolai-page-ai-memory-title">描述</div><div class="wolai-page-ai-memory-scope">' + escapeHtml(rt.description || '') + '</div></div>' +
|
||
'</section>';
|
||
}
|
||
}
|
||
var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
|
||
if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
|
||
var profileSummary = drawer.querySelector('[data-page-ai-profile-summary]');
|
||
if (profileSummary instanceof HTMLElement) profileSummary.textContent = activeProfile;
|
||
var profileSelect = drawer.querySelector('[data-page-ai-profile-select]');
|
||
if (profileSelect instanceof HTMLSelectElement) {
|
||
var profiles = pageUiState.pageAiProfiles.length ? pageUiState.pageAiProfiles : [{ name: activeProfile, active: true }];
|
||
profileSelect.innerHTML = profiles.map(function(profile) {
|
||
var name = pageAiProfileValue(profile) || 'default';
|
||
var model = [profile.model, profile.gateway].filter(Boolean).join(' / ');
|
||
var label = [name, profile.alias, model].filter(Boolean).join(' · ');
|
||
return '<option value="' + escapeHtml(name) + '"' + (name === activeProfile ? ' selected' : '') + '>' + escapeHtml(label) + '</option>';
|
||
}).join('');
|
||
profileSelect.value = activeProfile;
|
||
}
|
||
var runStatus = drawer.querySelector('[data-page-ai-run-status]');
|
||
if (runStatus instanceof HTMLElement) {
|
||
var queueSuffix = pageUiState.pageAiQueueLength > 0 ? ' · 队列 ' + pageUiState.pageAiQueueLength : '';
|
||
runStatus.textContent = pageAiRunStatusLabel(pageUiState.pageAiRunStatus) + queueSuffix;
|
||
}
|
||
var stopButton = drawer.querySelector('[data-page-ai-action="stop-run"]');
|
||
if (stopButton instanceof HTMLButtonElement) {
|
||
var canStop = ['queued', 'running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0 && pageUiState.pageAiCurrentRunId;
|
||
stopButton.disabled = !canStop;
|
||
stopButton.setAttribute('aria-disabled', canStop ? 'false' : 'true');
|
||
}
|
||
var settingsLink = drawer.querySelector('[data-page-ai-action="open-hermes-settings"]');
|
||
if (settingsLink instanceof HTMLButtonElement) {
|
||
settingsLink.disabled = !pageAiHermesSettingsUrl();
|
||
settingsLink.title = pageAiHermesSettingsUrl() ? '打开 Hermes 设置' : '未配置 MNOTE_WEB_HERMES_UPSTREAM_URL';
|
||
}
|
||
var queueList = drawer.querySelector('[data-page-ai-queue-list]');
|
||
if (queueList instanceof HTMLElement) {
|
||
if (!pageUiState.pageAiQueuedItems.length) {
|
||
queueList.innerHTML = '<div class="wolai-page-ai-empty">暂无排队项。</div>';
|
||
} else {
|
||
queueList.innerHTML = pageUiState.pageAiQueuedItems.map(function(item, index) {
|
||
var label = '队列 ' + (index + 1);
|
||
return '' +
|
||
'<div class="wolai-page-ai-tool-row" data-page-ai-queue-item="' + escapeHtml(item.queueId) + '">' +
|
||
'<div><strong>' + escapeHtml(label) + '</strong><br /><span>' + escapeHtml(item.queueId) + '</span></div>' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="cancel-queued-run" data-page-ai-queue-id="' + escapeHtml(item.queueId) + '">取消</button>' +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
}
|
||
var sessionNode = drawer.querySelector('[data-page-ai-session-status]');
|
||
if (sessionNode instanceof HTMLElement) {
|
||
var session = pageAiCurrentSession();
|
||
var usageText = session && session.usage ? pageAiUsageSummary(session.usage) : '';
|
||
sessionNode.textContent = session && session.id
|
||
? [String(session.title || '当前页问答'), pageAiSessionStorageLabel(session), usageText].filter(Boolean).join(' · ')
|
||
: '等待 Hermes session';
|
||
}
|
||
var modelNode = drawer.querySelector('[data-page-ai-model-status]');
|
||
if (modelNode instanceof HTMLElement) modelNode.textContent = pageAiCurrentModelLabel();
|
||
var scopeSelect = drawer.querySelector('[data-page-ai-context-scope]');
|
||
if (scopeSelect instanceof HTMLSelectElement) scopeSelect.value = pageUiState.pageAiContextScope;
|
||
var scopeLabel = drawer.querySelector('[data-page-ai-context-scope-label]');
|
||
if (scopeLabel instanceof HTMLElement) scopeLabel.textContent = pageAiContextScopeLabel(pageUiState.pageAiContextScope);
|
||
drawer.querySelectorAll('[data-page-ai-tab]').forEach(function(button) {
|
||
var target = button.getAttribute('data-page-ai-tab') || 'chat';
|
||
var active = target === pageUiState.pageAiPage;
|
||
button.classList.toggle('is-active', active);
|
||
button.setAttribute('aria-selected', active ? 'true' : 'false');
|
||
});
|
||
drawer.querySelectorAll('[data-page-ai-panel]').forEach(function(panel) {
|
||
if (panel instanceof HTMLElement) {
|
||
panel.hidden = panel.getAttribute('data-page-ai-panel') !== pageUiState.pageAiPage;
|
||
}
|
||
});
|
||
var profileError = drawer.querySelector('[data-page-ai-profile-error]');
|
||
if (profileError instanceof HTMLElement) {
|
||
profileError.textContent = pageUiState.pageAiProfileError || '';
|
||
profileError.hidden = !pageUiState.pageAiProfileError;
|
||
}
|
||
var memoryError = drawer.querySelector('[data-page-ai-memory-error]');
|
||
if (memoryError instanceof HTMLElement) {
|
||
var memoryErrorText = pageUiState.pageAiProfileMemoryError || '';
|
||
if (memoryErrorText && memoryErrorText === pageUiState.pageAiProfileError) memoryErrorText = '';
|
||
memoryError.textContent = memoryErrorText;
|
||
memoryError.hidden = !memoryErrorText;
|
||
}
|
||
var agentPanel = drawer.querySelector('[data-page-ai-agent-panel]');
|
||
if (agentPanel instanceof HTMLElement && pageUiState.pageAiAcpRuntime !== 'reasonix') {
|
||
agentPanel.innerHTML = ['soul', 'user', 'memory'].map(function(section) {
|
||
var label = pageAiMemoryFileLabel(section);
|
||
var value = pageUiState.pageAiProfileMemoryDrafts[section] || '';
|
||
return '' +
|
||
'<section class="wolai-page-ai-memory-card">' +
|
||
'<div class="wolai-page-ai-memory-head">' +
|
||
'<div>' +
|
||
'<div class="wolai-page-ai-memory-title">' + escapeHtml(label) + '</div>' +
|
||
'<div class="wolai-page-ai-memory-scope">保存到 Hermes profile: ' + escapeHtml(activeProfile) + '</div>' +
|
||
'</div>' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-memory-save="' + escapeHtml(section) + '">保存</button>' +
|
||
'</div>' +
|
||
'<textarea class="wolai-page-ai-memory-editor" data-page-ai-memory-editor="' + escapeHtml(section) + '" spellcheck="false">' + escapeHtml(value) + '</textarea>' +
|
||
'</section>';
|
||
}).join('');
|
||
}
|
||
var skillError = drawer.querySelector('[data-page-ai-skill-error]');
|
||
if (skillError instanceof HTMLElement) {
|
||
skillError.textContent = pageUiState.pageAiSkillError || '';
|
||
skillError.hidden = !pageUiState.pageAiSkillError;
|
||
}
|
||
var sessionError = drawer.querySelector('[data-page-ai-session-error]');
|
||
if (sessionError instanceof HTMLElement) {
|
||
sessionError.textContent = pageUiState.pageAiSessionError || '';
|
||
sessionError.hidden = !pageUiState.pageAiSessionError;
|
||
}
|
||
var sessionSearch = drawer.querySelector('[data-page-ai-session-search]');
|
||
if (sessionSearch instanceof HTMLInputElement && document.activeElement !== sessionSearch) {
|
||
sessionSearch.value = pageUiState.pageAiSessionSearchQuery || '';
|
||
}
|
||
var skillSearch = drawer.querySelector('[data-page-ai-skill-search]');
|
||
if (skillSearch instanceof HTMLInputElement && document.activeElement !== skillSearch) {
|
||
skillSearch.value = pageUiState.pageAiSkillQuery;
|
||
}
|
||
var skillList = drawer.querySelector('[data-page-ai-skill-list]');
|
||
if (skillList instanceof HTMLElement) {
|
||
var skills = pageAiFilteredSkillEntries();
|
||
if (!skills.length) {
|
||
var emptyText = String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() === 'reasonix'
|
||
? '没有匹配的 Reasonix skill。'
|
||
: '没有匹配的 Hermes skill。';
|
||
skillList.innerHTML = '<div class="wolai-page-ai-empty">' + escapeHtml(emptyText) + '</div>';
|
||
} else {
|
||
skillList.innerHTML = skills.map(function(skill) {
|
||
var sourceText = pageAiSkillOriginLabel(skill) + (skill.modified ? ' · modified' : '');
|
||
var description = String(skill.description || '').trim();
|
||
var hasDescription = description && description !== '---' && description !== '无描述';
|
||
var canToggle = skill.toggleable !== false && String(pageUiState.pageAiAcpRuntime || 'reasonix').trim() !== 'reasonix';
|
||
return '' +
|
||
'<div class="wolai-page-ai-skill-row">' +
|
||
'<div class="wolai-page-ai-skill-copy">' +
|
||
'<div class="wolai-page-ai-skill-main">' +
|
||
'<div class="wolai-page-ai-skill-name">' + escapeHtml(skill.name) + '</div>' +
|
||
'<div class="wolai-page-ai-skill-source">' + escapeHtml(sourceText) + '</div>' +
|
||
'</div>' +
|
||
(hasDescription ? '<div class="wolai-page-ai-skill-desc">' + escapeHtml(description) + '</div>' : '') +
|
||
'</div>' +
|
||
'<button type="button" class="wolai-page-ai-skill-switch' + (skill.enabled !== false ? ' is-on' : '') + '" data-page-ai-skill-toggle="' + escapeHtml(skill.name) + '" aria-pressed="' + (skill.enabled !== false ? 'true' : 'false') + '"' + (canToggle ? '' : ' disabled title="Reasonix skills 当前为只读展示"') + '>' +
|
||
'<span></span>' +
|
||
'</button>' +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
}
|
||
var toolsList = drawer.querySelector('[data-page-ai-tool-list]');
|
||
if (toolsList instanceof HTMLElement) {
|
||
var tools = pageAiNormalizeArray(pageUiState.pageAiTools);
|
||
if (!tools.length) {
|
||
toolsList.innerHTML = '<div class="wolai-page-ai-empty">尚未读取到 mnote tool manifest。</div>';
|
||
} else {
|
||
toolsList.innerHTML = tools.map(function(tool) {
|
||
return '' +
|
||
'<div class="wolai-page-ai-tool-row">' +
|
||
'<div class="wolai-page-ai-skill-copy">' +
|
||
'<div class="wolai-page-ai-tool-name">' + escapeHtml(tool.name) + '</div>' +
|
||
'<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.scope || tool.kind || 'mnote') + ' · ' + escapeHtml(tool.status || 'available') + '</div>' +
|
||
(tool.unavailableReason ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml(tool.unavailableReason) + '</div>' : '') +
|
||
'</div>' +
|
||
'<button type="button" class="wolai-page-ai-skill-switch' + (tool.enabled !== false ? ' is-on' : '') + '" data-page-ai-tool-toggle="' + escapeHtml(tool.name) + '" aria-pressed="' + (tool.enabled !== false ? 'true' : 'false') + '">' +
|
||
'<span></span>' +
|
||
'</button>' +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
}
|
||
var gatewayError = drawer.querySelector('[data-page-ai-gateway-error]');
|
||
if (gatewayError instanceof HTMLElement) {
|
||
gatewayError.textContent = pageUiState.pageAiGatewayHealthError || '';
|
||
gatewayError.hidden = !pageUiState.pageAiGatewayHealthError;
|
||
}
|
||
var gatewayStatusNode = drawer.querySelector('[data-page-ai-gateway-status]');
|
||
var gatewayDetail = drawer.querySelector('[data-page-ai-gateway-detail]');
|
||
var gatewayHealth = pageUiState.pageAiGatewayHealth;
|
||
if (gatewayStatusNode instanceof HTMLElement) {
|
||
if (!gatewayHealth) {
|
||
gatewayStatusNode.textContent = '未检查';
|
||
} else {
|
||
var gateway = gatewayHealth.gateway || {};
|
||
var profile = gatewayHealth.profile || {};
|
||
gatewayStatusNode.textContent = gatewayHealth.ok ? '可用' : '需要设置';
|
||
if (gateway.status) gatewayStatusNode.textContent += ' · ' + gateway.status;
|
||
if (profile.name) gatewayStatusNode.textContent += ' · ' + profile.name;
|
||
}
|
||
}
|
||
if (gatewayDetail instanceof HTMLElement) {
|
||
if (!gatewayHealth) {
|
||
gatewayDetail.innerHTML = '<div class="wolai-page-ai-empty">打开 Runtime 后会检查 Hermes gateway 与当前 profile。</div>';
|
||
} else {
|
||
var gatewayInfo = gatewayHealth.gateway || {};
|
||
var profileInfo = gatewayHealth.profile || {};
|
||
var suggestionText = pageAiNormalizeArray(gatewayHealth.suggestions).join(';');
|
||
gatewayDetail.innerHTML = '' +
|
||
'<div class="wolai-page-ai-tool-row">' +
|
||
'<div class="wolai-page-ai-tool-name">' + escapeHtml(profileInfo.name || activeProfile) + '</div>' +
|
||
'<div class="wolai-page-ai-tool-meta">model.default: ' + escapeHtml(profileInfo.modelDefault || '未设置') + '</div>' +
|
||
'<div class="wolai-page-ai-tool-meta">provider: ' + escapeHtml(profileInfo.provider || '未设置') + ' · API key: ' + escapeHtml(profileInfo.apiKeyConfigured ? '已配置' : '未检测到') + '</div>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-tool-row">' +
|
||
'<div class="wolai-page-ai-tool-name">' + escapeHtml(gatewayInfo.upstream || '未配置 upstream') + '</div>' +
|
||
'<div class="wolai-page-ai-tool-meta">gateway: ' + escapeHtml(gatewayInfo.status || 'unknown') + (gatewayInfo.httpStatus ? ' · HTTP ' + escapeHtml(gatewayInfo.httpStatus) : '') + '</div>' +
|
||
(suggestionText ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml(suggestionText) + '</div>' : '') +
|
||
'</div>';
|
||
}
|
||
}
|
||
var toolError = drawer.querySelector('[data-page-ai-tool-error]');
|
||
if (toolError instanceof HTMLElement) {
|
||
toolError.textContent = pageUiState.pageAiToolsError || '';
|
||
toolError.hidden = !pageUiState.pageAiToolsError;
|
||
}
|
||
var lastTool = drawer.querySelector('[data-page-ai-last-tool]');
|
||
if (lastTool instanceof HTMLElement) {
|
||
var call = pageUiState.pageAiLastToolCall;
|
||
lastTool.textContent = call
|
||
? [call.event, call.name, call.runId, call.traceId || call.auditId].filter(Boolean).join(' · ')
|
||
: '暂无 tool call';
|
||
}
|
||
}
|
||
|
||
function humanizePageAiResponse(rawText, promptText) {
|
||
var providerLabel = pageAiProviderLabel(pageUiState.pageAiProvider);
|
||
var text = String(rawText || '').trim();
|
||
if (!text) {
|
||
return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”,但这次没有返回可读内容。';
|
||
}
|
||
if (text.startsWith('{')) {
|
||
try {
|
||
var payload = JSON.parse(text);
|
||
var operation = payload && payload.operation ? payload.operation : {};
|
||
var normalized = operation && operation.normalized_input ? operation.normalized_input : {};
|
||
var args = normalized && normalized.args ? normalized.args : {};
|
||
var documentId = searchText(args.documentId || args.pageId || currentDocumentId());
|
||
var toolName = searchText(operation.tool_name || normalized.toolName || 'doc_get');
|
||
return providerLabel + ' 已收到你的问题“' + searchText(promptText) + '”。当前已通过 Hermes 调用 mnote plugin 工具,目标页面是 ' + (documentId || '当前页面') + ',本次选择的工具是 ' + toolName + '。如果你继续追问,我会沿当前页面上下文继续回复。';
|
||
} catch (_) {
|
||
return providerLabel + ' 已返回结果,但当前结果是结构化文本,已为你保留原始内容。';
|
||
}
|
||
}
|
||
return text;
|
||
}
|
||
|
||
function ensurePageAiDrawer() {
|
||
var existing = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
|
||
if (existing instanceof HTMLElement) return existing;
|
||
var drawer = document.createElement('aside');
|
||
drawer.className = 'wolai-page-ai-drawer';
|
||
drawer.setAttribute('data-testid', 'wolai-page-ai-drawer');
|
||
drawer.setAttribute('data-mnote-surface', 'page-ai');
|
||
drawer.hidden = true;
|
||
drawer.innerHTML = '' +
|
||
'<div class="wolai-page-ai-panel" role="dialog" aria-modal="false">' +
|
||
'<div class="wolai-page-ai-header">' +
|
||
'<div class="wolai-page-ai-header-copy">' +
|
||
'<h2 class="wolai-page-ai-title" data-title="page-ai-title">页面 AI</h2>' +
|
||
'<div class="wolai-page-ai-subtitle">' +
|
||
'<span>Hermes</span>' +
|
||
'<span data-page-ai-profile-summary>default</span>' +
|
||
'<span data-page-ai-model-status>由 Hermes 决定</span>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-header-actions">' +
|
||
'<button type="button" class="wolai-page-ai-icon" data-page-ai-tab="agent" aria-label="打开页面 AI 设置">⚙</button>' +
|
||
'<button type="button" class="wolai-page-ai-icon" data-page-ai-action="close" aria-label="关闭页面 AI">×</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-body">' +
|
||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="chat">' +
|
||
'<div class="wolai-page-ai-chat-meta">' +
|
||
'<button type="button" class="wolai-page-ai-session-button" data-page-ai-action="history">' +
|
||
'<span data-page-ai-session-status>等待 Hermes session</span>' +
|
||
'</button>' +
|
||
'<span data-page-ai-context-scope-label>当前页</span>' +
|
||
'<span data-page-ai-run-status>空闲</span>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
|
||
'<div class="wolai-page-ai-suggestions">' +
|
||
'<div class="wolai-page-ai-suggestions-header">' +
|
||
'<span>推荐问题</span>' +
|
||
'<div class="wolai-page-ai-intents">' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-summary">创建 Summary</button>' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-intent="create-ai-note">创建 AI Note</button>' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-action="rotate">换一换</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-suggestion-list" data-page-ai-suggestion-list></div>' +
|
||
'</div>' +
|
||
'</section>' +
|
||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="agent" hidden>' +
|
||
'<div class="wolai-page-ai-settings-head">' +
|
||
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
|
||
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
|
||
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="agent" role="tab" aria-selected="true">Agent</button>' +
|
||
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="skills" role="tab" aria-selected="false">Skills</button>' +
|
||
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">Runtime</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-settings-grid">' +
|
||
'<label class="wolai-page-ai-profile-select">' +
|
||
'<span>ACP</span>' +
|
||
'<select data-page-ai-acp-runtime></select>' +
|
||
'</label>' +
|
||
'<label class="wolai-page-ai-profile-select" data-page-ai-hermes-profile>' +
|
||
'<span>agent / profile</span>' +
|
||
'<select data-page-ai-profile-select></select>' +
|
||
'</label>' +
|
||
'<label class="wolai-page-ai-context-select">' +
|
||
'<span>上下文</span>' +
|
||
'<select data-page-ai-context-scope>' +
|
||
'<option value="page">当前页</option>' +
|
||
'<option value="selection">当前选区</option>' +
|
||
'<option value="block">当前块</option>' +
|
||
'<option value="options">页面设置</option>' +
|
||
'</select>' +
|
||
'</label>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-inline-error" data-page-ai-profile-error hidden></div>' +
|
||
'<div class="wolai-page-ai-inline-error" data-page-ai-memory-error hidden></div>' +
|
||
'<div class="wolai-page-ai-memory-grid" data-page-ai-agent-panel></div>' +
|
||
'</section>' +
|
||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="skills" hidden>' +
|
||
'<div class="wolai-page-ai-settings-head">' +
|
||
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
|
||
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
|
||
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Agent</button>' +
|
||
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="skills" role="tab" aria-selected="true">Skills</button>' +
|
||
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="runtime" role="tab" aria-selected="false">Runtime</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-skills-toolbar">' +
|
||
'<label class="wolai-page-ai-skill-search">' +
|
||
'<span>搜索技能</span>' +
|
||
'<input type="search" data-page-ai-skill-search placeholder="搜索技能…" />' +
|
||
'</label>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-inline-error" data-page-ai-skill-error hidden></div>' +
|
||
'<div class="wolai-page-ai-skill-list" data-page-ai-skill-list></div>' +
|
||
'</section>' +
|
||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="runtime" hidden>' +
|
||
'<div class="wolai-page-ai-settings-head">' +
|
||
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
|
||
'<div class="wolai-page-ai-settings-tabs" role="tablist" aria-label="页面 AI 设置">' +
|
||
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="agent" role="tab" aria-selected="false">Agent</button>' +
|
||
'<button type="button" class="wolai-page-ai-tab" data-page-ai-tab="skills" role="tab" aria-selected="false">Skills</button>' +
|
||
'<button type="button" class="wolai-page-ai-tab is-active" data-page-ai-tab="runtime" role="tab" aria-selected="true">Runtime</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<button type="button" class="wolai-page-ai-settings-link" data-page-ai-action="open-hermes-settings">打开 Hermes 设置</button>' +
|
||
'<div class="wolai-page-ai-inline-error" data-page-ai-gateway-error hidden></div>' +
|
||
'<div class="wolai-page-ai-tools-panel">' +
|
||
'<div class="wolai-page-ai-tools-head">' +
|
||
'<span>gateway</span>' +
|
||
'<span data-page-ai-gateway-status>未检查</span>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-tool-list" data-page-ai-gateway-detail></div>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-tools-panel">' +
|
||
'<div class="wolai-page-ai-tools-head">' +
|
||
'<span>queue</span>' +
|
||
'<span data-page-ai-queue-status>由 mnote-web 管理</span>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-tool-list" data-page-ai-queue-list></div>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-tools-panel">' +
|
||
'<div class="wolai-page-ai-tools-head">' +
|
||
'<span>mnote tools</span>' +
|
||
'<span data-page-ai-last-tool>暂无 tool call</span>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-inline-error" data-page-ai-tool-error hidden></div>' +
|
||
'<div class="wolai-page-ai-tool-list" data-page-ai-tool-list></div>' +
|
||
'</div>' +
|
||
'</section>' +
|
||
'<section class="wolai-page-ai-panel-section" data-page-ai-panel="history" hidden>' +
|
||
'<div class="wolai-page-ai-settings-head">' +
|
||
'<button type="button" class="wolai-page-ai-back" data-page-ai-tab="chat">← 聊天</button>' +
|
||
'</div>' +
|
||
'<label class="wolai-page-ai-skill-search">' +
|
||
'<span>搜索会话</span>' +
|
||
'<input type="search" data-page-ai-session-search placeholder="搜索后端 AI 会话…" />' +
|
||
'</label>' +
|
||
'<div class="wolai-page-ai-inline-error" data-page-ai-session-error hidden></div>' +
|
||
'<div class="wolai-page-ai-conversation" data-page-ai-conversation></div>' +
|
||
'</section>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-ai-footer">' +
|
||
'<div class="wolai-page-ai-composer">' +
|
||
'<textarea class="wolai-page-ai-input" data-page-ai-input rows="3" placeholder="问 Hermes…"></textarea>' +
|
||
'<div class="wolai-page-ai-composer-bar">' +
|
||
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="new-session" title="新会话">+</button>' +
|
||
'<button type="button" class="wolai-page-ai-tool-button" data-page-ai-action="history" title="历史会话">⌕</button>' +
|
||
'<span class="wolai-page-ai-composer-spacer"></span>' +
|
||
'<button type="button" class="wolai-page-ai-stop" data-page-ai-action="stop-run" disabled>停止</button>' +
|
||
'<button type="button" class="wolai-page-ai-send" data-page-ai-action="send" aria-label="发送">发送</button>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'</div>';
|
||
document.body.appendChild(drawer);
|
||
return drawer;
|
||
}
|
||
|
||
function renderPageAiSuggestions() {
|
||
var drawer = ensurePageAiDrawer();
|
||
var list = drawer.querySelector('[data-page-ai-panel="chat"] [data-page-ai-suggestion-list]');
|
||
if (!(list instanceof HTMLElement)) return;
|
||
var container = list.closest('.wolai-page-ai-suggestions');
|
||
if (container instanceof HTMLElement) {
|
||
container.hidden = pageUiState.pageAiPage !== 'chat' || pageUiState.pageAiMessages.length > 0;
|
||
}
|
||
var suggestions = pageAiSuggestions();
|
||
var offset = pageUiState.pageAiSuggestionIndex % suggestions.length;
|
||
var ordered = suggestions.slice(offset).concat(suggestions.slice(0, offset)).slice(0, 3);
|
||
list.innerHTML = ordered.map(function(text) {
|
||
return '<button type="button" class="wolai-page-ai-suggestion" data-page-ai-suggestion="' + escapeHtml(text) + '">' + escapeHtml(text) + '</button>';
|
||
}).join('');
|
||
}
|
||
|
||
function renderPageAiConversation() {
|
||
var drawer = ensurePageAiDrawer();
|
||
renderPageAiSuggestions();
|
||
var conversation = drawer.querySelector('[data-page-ai-panel="' + cssEscape(pageUiState.pageAiPage || 'chat') + '"] [data-page-ai-conversation]');
|
||
if (!(conversation instanceof HTMLElement)) return;
|
||
if (pageUiState.pageAiPage === 'history') {
|
||
var historyRows = pageUiState.pageAiSessionSearchResults.length
|
||
? pageUiState.pageAiSessionSearchResults
|
||
: pageUiState.pageAiSessions;
|
||
if (!historyRows.length) {
|
||
conversation.innerHTML = '<div class="wolai-page-ai-empty">尚未产生历史会话。</div>';
|
||
return;
|
||
}
|
||
conversation.innerHTML = historyRows.map(function(session) {
|
||
var preview = Array.isArray(session.messages) && session.messages.length
|
||
? session.messages.slice(-1)[0].content
|
||
: (session.snippet || session.preview || '暂无消息');
|
||
var active = session.id === pageUiState.pageAiActiveSessionId;
|
||
var usage = pageAiUsageSummary(session.usage);
|
||
var meta = [pageAiSessionStorageLabel(session), session.permissionLevel, session.shareId, session.status, usage].filter(Boolean).join(' · ');
|
||
return '' +
|
||
'<div class="wolai-page-ai-message wolai-page-ai-message--history' + (active ? ' is-active' : '') + '" data-page-ai-session-row="' + escapeHtml(session.id) + '">' +
|
||
'<button type="button" class="wolai-page-ai-message-text" data-page-ai-session="' + escapeHtml(session.id) + '">' +
|
||
'<strong>' + escapeHtml(session.title || '新会话') + '</strong><br />' +
|
||
'<span>' + escapeHtml(preview) + '</span>' +
|
||
(meta ? '<br /><span class="wolai-page-ai-tool-meta">' + escapeHtml(meta) + '</span>' : '') +
|
||
'</button>' +
|
||
'<div class="wolai-page-ai-message-actions">' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-resume="' + escapeHtml(session.id) + '">恢复</button>' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-rename="' + escapeHtml(session.id) + '">重命名</button>' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-session-delete="' + escapeHtml(session.id) + '">删除</button>' +
|
||
'</div>' +
|
||
'</div>';
|
||
}).join('');
|
||
return;
|
||
}
|
||
if (!pageUiState.pageAiMessages.length) {
|
||
conversation.innerHTML = '<div class="wolai-page-ai-empty">围绕当前页面提问,我会优先使用当前页内容、页面结构和已保存设置。</div>';
|
||
return;
|
||
}
|
||
conversation.innerHTML = pageUiState.pageAiMessages.map(function(item) {
|
||
var roleLabel = item.role === 'user' ? '你' : (item.role === 'tool' ? '工具' : 'AI');
|
||
if (item.role === 'tool') {
|
||
var statusLabel = item.status === 'completed' ? '完成' : (item.status === 'failed' ? '失败' : '运行中');
|
||
var locationRows = Array.isArray(item.locations) && item.locations.length
|
||
? '<div class="wolai-page-ai-tool-meta">位置:' + item.locations.map(function(loc, idx) {
|
||
return '<span style="display:inline-flex;align-items:center;gap:4px;margin-right:8px">' +
|
||
'<span>' + escapeHtml(loc) + '</span>' +
|
||
'<button type="button" class="wolai-page-ai-ghost" style="font-size:0.85em;padding:0 4px;min-width:unset" data-page-ai-open-location="' + escapeHtml(loc) + '" data-page-ai-tool-call-id="' + escapeHtml(item.toolCallId || '') + '" title="打开文件">打开</button>' +
|
||
'</span>';
|
||
}).join('') + '</div>'
|
||
: '';
|
||
var detailRows = [
|
||
locationRows,
|
||
item.argsSummary ? '<div class="wolai-page-ai-tool-meta">参数 ' + escapeHtml(item.argsSummary) + '</div>' : '',
|
||
item.resultSummary ? '<div class="wolai-page-ai-tool-meta">结果 ' + escapeHtml(item.resultSummary) + '</div>' : '',
|
||
item.changedFiles && item.changedFiles.length ? '<pre class="wolai-page-ai-tool-meta wolai-page-ai-changed-files">' + escapeHtml(pageAiFormatChangedFiles(item.changedFiles)) + '</pre>' : '',
|
||
(item.traceId || item.auditId) ? '<div class="wolai-page-ai-tool-meta">' + escapeHtml([item.traceId, item.auditId].filter(Boolean).join(' · ')) + '</div>' : ''
|
||
].filter(Boolean).join('');
|
||
return '' +
|
||
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-tool-card data-page-ai-tool-call-id="' + escapeHtml(item.toolCallId || '') + '" data-page-ai-tool-status="' + escapeHtml(item.status || '') + '">' +
|
||
'<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
|
||
'<div class="wolai-page-ai-message-text">' +
|
||
'<details class="wolai-page-ai-tool-details">' +
|
||
'<summary>' +
|
||
'<strong>' + escapeHtml(item.toolName || item.content || 'tool') + '</strong>' +
|
||
'<span>' + escapeHtml([statusLabel, item.toolKind, item.toolCallId].filter(Boolean).join(' · ')) + '</span>' +
|
||
'</summary>' +
|
||
(detailRows || '<div class="wolai-page-ai-tool-meta">暂无参数或结果详情。</div>') +
|
||
'</details>' +
|
||
'</div>' +
|
||
'</div>';
|
||
}
|
||
if (item.kind === 'thought') {
|
||
return '' +
|
||
'<details class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-thought-delta="true">' +
|
||
'<summary class="wolai-page-ai-message-role" style="cursor:pointer;user-select:none;opacity:0.7;font-size:0.85em">思考过程</summary>' +
|
||
'<div class="wolai-page-ai-message-text" style="margin-top:4px;opacity:0.6;font-size:0.9em;border-left:2px solid var(--wolai-border-color, #ddd);padding-left:8px">' + escapeHtml(item.content || '') + '</div>' +
|
||
'</details>';
|
||
}
|
||
if (item.kind === 'permission') {
|
||
var permissionActions = item.resolved ? '' : (
|
||
'<div class="wolai-page-ai-message-actions">' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="allow" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">允许</button>' +
|
||
'<button type="button" class="wolai-page-ai-ghost" data-page-ai-permission-action="deny" data-page-ai-permission-id="' + escapeHtml(item.permissionId || '') + '">拒绝</button>' +
|
||
'</div>'
|
||
);
|
||
return '' +
|
||
'<div class="wolai-page-ai-message wolai-page-ai-message--tool" data-page-ai-permission-request="' + escapeHtml(item.permissionId || '') + '">' +
|
||
'<div class="wolai-page-ai-message-role">权限</div>' +
|
||
'<div class="wolai-page-ai-message-text">' +
|
||
'<strong>' + escapeHtml(item.toolName || 'session/request_permission') + '</strong><br />' +
|
||
'<span>' + escapeHtml(item.argsSummary || item.content || '') + '</span>' +
|
||
permissionActions +
|
||
'</div>' +
|
||
'</div>';
|
||
}
|
||
if (item.kind === 'plan') {
|
||
var planEntries = Array.isArray(item.entries) ? item.entries : [];
|
||
var listHtml = planEntries.map(function(entry, idx) {
|
||
return '<li style="margin:2px 0">' + escapeHtml(String(entry || '')) + '</li>';
|
||
}).join('');
|
||
return '' +
|
||
'<div class="wolai-page-ai-message wolai-page-ai-message--assistant" data-page-ai-plan="true">' +
|
||
'<details class="wolai-page-ai-plan-details" open>' +
|
||
'<summary class="wolai-page-ai-message-role" style="cursor:pointer;user-select:none;opacity:0.8;font-size:0.85em">执行计划 · ' + planEntries.length + ' 步</summary>' +
|
||
'<div class="wolai-page-ai-message-text" style="margin-top:4px;opacity:0.75;font-size:0.9em;border-left:2px solid var(--wolai-border-color, #ddd);padding-left:8px">' +
|
||
'<ol style="margin:4px 0;padding-left:20px">' + listHtml + '</ol>' +
|
||
'</div>' +
|
||
'</details>' +
|
||
'</div>';
|
||
}
|
||
var streamingAttr = item.streaming ? ' data-page-ai-streaming="true"' : '';
|
||
return '' +
|
||
'<div class="wolai-page-ai-message wolai-page-ai-message--' + escapeHtml(item.role || 'assistant') + '"' + streamingAttr + '>' +
|
||
'<div class="wolai-page-ai-message-role">' + escapeHtml(roleLabel) + '</div>' +
|
||
'<div class="wolai-page-ai-message-text">' + escapeHtml(item.content || '') + '</div>' +
|
||
'</div>';
|
||
}).join('');
|
||
conversation.scrollTop = conversation.scrollHeight;
|
||
}
|
||
|
||
function openPageAiDrawer() {
|
||
pageAiLoadSessions();
|
||
renderPageAiSuggestions();
|
||
renderPageAiConversation();
|
||
renderPageAiProviderButtons();
|
||
renderPageAiControls();
|
||
var drawer = ensurePageAiDrawer();
|
||
drawer.hidden = false;
|
||
pageUiState.pageAiOpen = true;
|
||
updatePageAiTriggerState();
|
||
Promise.all([
|
||
pageAiLoadProfiles(),
|
||
pageAiLoadProfileMemory(),
|
||
pageAiLoadSkills(),
|
||
pageAiLoadTools(),
|
||
pageAiLoadGatewayHealth(),
|
||
pageAiLoadBackendSessions()
|
||
]).then(function() {
|
||
renderPageAiControls();
|
||
}).catch(function() {}).then(function() {
|
||
return pageAiEnsureHermesSession();
|
||
}).then(function() {
|
||
return pageAiRestoreHermesSession();
|
||
}).then(function() {
|
||
renderPageAiControls();
|
||
}).catch(function(error) {
|
||
pageUiState.pageAiMessages.push({
|
||
role: 'assistant',
|
||
content: 'Hermes 当前不可用:' + (error instanceof Error ? error.message : String(error))
|
||
});
|
||
renderPageAiConversation();
|
||
renderPageAiControls();
|
||
});
|
||
}
|
||
|
||
function closePageAiDrawer() {
|
||
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
|
||
if (drawer instanceof HTMLElement) drawer.hidden = true;
|
||
pageUiState.pageAiOpen = false;
|
||
updatePageAiTriggerState();
|
||
}
|
||
|
||
function isPageAiDrawerOpen() {
|
||
var drawer = document.querySelector('[data-testid="wolai-page-ai-drawer"]');
|
||
return drawer instanceof HTMLElement && !drawer.hidden;
|
||
}
|
||
|
||
async function streamPageAiResponse(response, onEvent) {
|
||
if (!response.body || typeof response.body.getReader !== 'function') return;
|
||
var reader = response.body.getReader();
|
||
var decoder = new TextDecoder();
|
||
var buffer = '';
|
||
while (true) {
|
||
var chunk = await reader.read();
|
||
if (chunk.done) break;
|
||
buffer += decoder.decode(chunk.value, { stream: true });
|
||
var frames = buffer.split('\n\n');
|
||
buffer = frames.pop() || '';
|
||
frames.forEach(function(frame) {
|
||
var eventName = '';
|
||
var dataLines = [];
|
||
frame.split('\n').forEach(function(line) {
|
||
if (line.startsWith('event:')) eventName = line.slice(6).trim();
|
||
if (line.startsWith('data:')) dataLines.push(line.slice(5).trim());
|
||
});
|
||
var payloadText = dataLines.join('\n');
|
||
if (!eventName && payloadText) {
|
||
try {
|
||
var parsed = JSON.parse(payloadText);
|
||
eventName = parsed && parsed.event ? String(parsed.event) : '';
|
||
} catch (_) {}
|
||
}
|
||
if (eventName) onEvent(eventName, payloadText);
|
||
});
|
||
}
|
||
}
|
||
|
||
function pageAiDecodeDeltaText(payloadText) {
|
||
try {
|
||
var payload = JSON.parse(payloadText || 'null');
|
||
return String((payload && (payload.text || payload.delta)) || '');
|
||
} catch (_) {
|
||
return String(payloadText || '');
|
||
}
|
||
}
|
||
|
||
function pageAiEnsureStreamingAssistantMessage(runId) {
|
||
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
|
||
var existing = pageUiState.pageAiMessages.find(function(item) {
|
||
return item.role === 'assistant' && item.streaming === true && item.runId === id;
|
||
});
|
||
if (existing) return existing;
|
||
existing = {
|
||
role: 'assistant',
|
||
content: '',
|
||
runId: id,
|
||
streaming: true
|
||
};
|
||
pageUiState.pageAiMessages.push(existing);
|
||
return existing;
|
||
}
|
||
|
||
function pageAiAppendStreamingAssistantDelta(runId, deltaText) {
|
||
var delta = String(deltaText || '');
|
||
if (!delta) return '';
|
||
var message = pageAiEnsureStreamingAssistantMessage(runId);
|
||
message.content = String(message.content || '') + delta;
|
||
pageAiSyncCurrentSessionMessages();
|
||
renderPageAiConversation();
|
||
return message.content;
|
||
}
|
||
|
||
function pageAiFinishStreamingAssistantMessage(runId, finalText, promptText) {
|
||
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
|
||
var message = pageUiState.pageAiMessages.find(function(item) {
|
||
return item.role === 'assistant' && item.streaming === true && item.runId === id;
|
||
});
|
||
var text = String(finalText || (message && message.content) || '');
|
||
var content = humanizePageAiResponse(text, promptText);
|
||
if (message) {
|
||
message.content = content;
|
||
message.streaming = false;
|
||
} else if (content) {
|
||
pageUiState.pageAiMessages.push({
|
||
role: 'assistant',
|
||
content: content,
|
||
runId: id
|
||
});
|
||
}
|
||
pageAiSyncCurrentSessionMessages();
|
||
pageAiPersistSessions();
|
||
}
|
||
|
||
function pageAiLooksLikeBlockEdit(prompt) {
|
||
var text = searchText(prompt);
|
||
return ['新增', '添加', '插入', '删除', '删掉', '修改', '替换', '改成', '移动', '移到', 'move', 'replace', 'delete', 'insert'].some(function(word) {
|
||
return text.indexOf(word) >= 0;
|
||
});
|
||
}
|
||
|
||
async function pageAiTryBlockEditWorkflow(prompt, scopedContext) {
|
||
if (currentSourceKind() === 'local_folder') return false;
|
||
if (!pageAiLooksLikeBlockEdit(prompt)) return false;
|
||
var runId = 'page-ai-fast-' + Date.now().toString(36);
|
||
var traceId = 'page-ai-fast-' + Date.now().toString(36);
|
||
pageAiSetRunStatus('running', runId);
|
||
renderPageAiControls();
|
||
var started = Date.now();
|
||
var response = await fetch('/api/page-ai/block-edit-workflow', {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify({
|
||
workspaceId: resolveWorkspaceId(document.body),
|
||
documentId: currentDocumentId(),
|
||
sourceKind: currentSourceKind(),
|
||
rootUri: currentRootUri(),
|
||
sessionId: pageUiState.pageAiActiveSessionId,
|
||
runId: runId,
|
||
profile: pageAiCurrentProfile(),
|
||
model: pageAiMnoteToolModel(),
|
||
message: prompt,
|
||
pageContext: scopedContext.pageContext,
|
||
selectedBlockId: scopedContext.selectedBlockId,
|
||
selectedText: scopedContext.selectedText,
|
||
traceId: traceId
|
||
})
|
||
});
|
||
var payload = await response.json().catch(function() { return null; });
|
||
if (!response.ok || !payload || payload.ok !== true) {
|
||
var code = payload && payload.code ? String(payload.code) : '';
|
||
if (code === 'page_ai_workflow_not_block_edit') return false;
|
||
pageUiState.pageAiMessages.push({
|
||
role: 'tool',
|
||
toolName: 'mnote.page_ai.block_edit_workflow',
|
||
status: 'failed',
|
||
toolCallId: runId,
|
||
resultSummary: pageAiErrorMessage(payload, 'fast_path_failed_' + response.status)
|
||
});
|
||
renderPageAiConversation();
|
||
pageAiSetRunStatus('failed', runId);
|
||
renderPageAiControls();
|
||
return true;
|
||
}
|
||
pageUiState.pageAiMessages.push({
|
||
role: 'tool',
|
||
toolName: 'mnote.page_ai.block_edit_workflow',
|
||
status: 'completed',
|
||
toolCallId: runId,
|
||
resultSummary: 'operations=' + String(Array.isArray(payload.operations) ? payload.operations.length : 0) + ' · ' + String(Date.now() - started) + 'ms'
|
||
});
|
||
pageUiState.pageAiMessages.push({
|
||
role: 'assistant',
|
||
content: payload.message || '已通过页面块编辑快路径完成写入。'
|
||
});
|
||
pageAiSetRunStatus('completed', runId);
|
||
try {
|
||
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
|
||
detail: {
|
||
toolName: 'mnote.doc.apply_block_ops',
|
||
normalizedToolName: 'mnote.doc.apply.block.ops',
|
||
documentId: currentDocumentId(),
|
||
workspaceId: resolveWorkspaceId(document.body),
|
||
runId: runId,
|
||
traceId: traceId,
|
||
toolCallId: runId
|
||
}
|
||
}));
|
||
} catch (_) {}
|
||
var currentSession = pageAiCurrentSession();
|
||
if (currentSession) {
|
||
currentSession.messages = pageUiState.pageAiMessages.slice();
|
||
currentSession.updatedAt = Date.now();
|
||
}
|
||
pageAiPersistSessions();
|
||
renderPageAiConversation();
|
||
renderPageAiControls();
|
||
return true;
|
||
}
|
||
|
||
async function sendPageAiMessage(text) {
|
||
var allowQueue = ['running', 'tool_calling'].indexOf(pageUiState.pageAiRunStatus) >= 0;
|
||
if (pageUiState.pageAiBusy && !allowQueue) return;
|
||
var prompt = searchText(text);
|
||
if (!prompt) return;
|
||
if (!allowQueue) pageUiState.pageAiBusy = true;
|
||
var currentSession = null;
|
||
try {
|
||
await pageAiEnsureHermesSession();
|
||
if (!allowQueue) pageAiSetRunStatus('queued');
|
||
renderPageAiControls();
|
||
var contextSnapshot = currentPageAiContextSnapshot();
|
||
var scopedContext = pageAiScopedPageContext(contextSnapshot);
|
||
pageUiState.pageAiMessages.push({ role: 'user', content: prompt });
|
||
currentSession = pageAiCurrentSession();
|
||
if (currentSession) {
|
||
if (currentSession.title === '新会话') {
|
||
currentSession.title = prompt.length > 18 ? prompt.slice(0, 18) + '…' : prompt;
|
||
}
|
||
currentSession.messages = pageUiState.pageAiMessages.slice();
|
||
currentSession.updatedAt = Date.now();
|
||
}
|
||
pageAiPersistSessions();
|
||
renderPageAiConversation();
|
||
if (!allowQueue && await pageAiTryBlockEditWorkflow(prompt, scopedContext)) {
|
||
return;
|
||
}
|
||
var response = await fetch('/api/hermes/client/runs', {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify({
|
||
workspaceId: resolveWorkspaceId(document.body),
|
||
documentId: currentDocumentId(),
|
||
sourceKind: currentSourceKind(),
|
||
rootUri: currentRootUri(),
|
||
sessionId: pageUiState.pageAiActiveSessionId,
|
||
profile: pageAiRunProfile(),
|
||
acpRuntime: pageUiState.pageAiAcpRuntime || 'reasonix',
|
||
contextScope: pageUiState.pageAiContextScope,
|
||
message: prompt,
|
||
model: pageAiMnoteToolModel(),
|
||
pageContext: scopedContext.pageContext,
|
||
selectedBlockId: scopedContext.selectedBlockId,
|
||
selectedText: scopedContext.selectedText,
|
||
traceId: 'page-ai-run-' + Date.now().toString(36)
|
||
})
|
||
});
|
||
if (!response.ok) {
|
||
var errorPayload = await response.json().catch(function(){ return null; });
|
||
throw new Error(pageAiErrorMessage(errorPayload, 'page_ai_failed_' + response.status));
|
||
}
|
||
var runPayload = await response.json().catch(function(){ return null; });
|
||
if (pageAiApplyQueuedRun(runPayload)) {
|
||
pageUiState.pageAiMessages.push({
|
||
role: 'assistant',
|
||
content: '已加入 Hermes 队列,前一条 run 完成后继续处理。'
|
||
});
|
||
if (currentSession) {
|
||
currentSession.messages = pageUiState.pageAiMessages.slice();
|
||
currentSession.updatedAt = Date.now();
|
||
}
|
||
pageAiPersistSessions();
|
||
renderPageAiConversation();
|
||
renderPageAiControls();
|
||
return;
|
||
}
|
||
if (allowQueue) {
|
||
throw new Error('hermes_queue_expected_queued_response');
|
||
}
|
||
var upstream = runPayload && runPayload.upstream ? runPayload.upstream : runPayload;
|
||
var runId = upstream && (upstream.run_id || upstream.runId);
|
||
if (!runId) throw new Error('hermes_run_missing_run_id');
|
||
var runTraceId = String((upstream && (upstream.trace_id || upstream.traceId)) || (runPayload && (runPayload.trace_id || runPayload.traceId)) || '');
|
||
pageAiSetRunStatus('running', runId);
|
||
renderPageAiControls();
|
||
var eventResponse = await fetch('/api/hermes/client/events/' + encodeURIComponent(runId), {
|
||
headers: { 'accept': 'text/event-stream' }
|
||
});
|
||
if (!eventResponse.ok) {
|
||
var eventError = await eventResponse.json().catch(function(){ return null; });
|
||
throw new Error(pageAiErrorMessage(eventError, 'hermes_events_failed_' + eventResponse.status));
|
||
}
|
||
var assistantText = '';
|
||
await streamPageAiResponse(eventResponse, function(eventName, payloadText) {
|
||
if (eventName === 'message.delta') {
|
||
if (pageUiState.pageAiStoppedRunIds[runId]) return;
|
||
assistantText = pageAiAppendStreamingAssistantDelta(runId, pageAiDecodeDeltaText(payloadText));
|
||
}
|
||
if (eventName === 'thought.delta') {
|
||
try {
|
||
var thoughtPayload = JSON.parse(payloadText || 'null');
|
||
var thoughtText = String((thoughtPayload && (thoughtPayload.delta || thoughtPayload.text)) || '');
|
||
if (thoughtText) {
|
||
var msgs = pageUiState.pageAiMessages;
|
||
var lastThought = msgs.length > 0 && msgs[msgs.length - 1].kind === 'thought' ? msgs[msgs.length - 1] : null;
|
||
if (lastThought) {
|
||
lastThought.content += thoughtText;
|
||
} else {
|
||
msgs.push({ role: 'assistant', kind: 'thought', content: thoughtText });
|
||
}
|
||
pageAiSyncCurrentSessionMessages();
|
||
pageAiPersistSessions();
|
||
renderPageAiConversation();
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
if (eventName === 'usage.updated') {
|
||
try {
|
||
var usagePayload = JSON.parse(payloadText || 'null') || {};
|
||
var sessionForUsage = pageAiCurrentSession();
|
||
if (sessionForUsage) {
|
||
sessionForUsage.usage = {
|
||
source: 'usage_update',
|
||
used: Number(usagePayload.used ?? usagePayload.contextUsed ?? 0),
|
||
size: Number(usagePayload.size ?? usagePayload.contextSize ?? 0)
|
||
};
|
||
pageAiPersistSessions();
|
||
}
|
||
} catch (_) {}
|
||
renderPageAiControls();
|
||
}
|
||
if (eventName === 'permission.requested' || eventName === 'permission.denied' || eventName === 'permission.allowed') {
|
||
pageAiApplyPermissionEvent(eventName, payloadText);
|
||
pageAiSyncCurrentSessionMessages();
|
||
pageAiPersistSessions();
|
||
renderPageAiConversation();
|
||
renderPageAiControls();
|
||
}
|
||
if (eventName === 'run.completed') {
|
||
try {
|
||
var completed = JSON.parse(payloadText || 'null');
|
||
if (completed && completed.output) assistantText = String(completed.output || '');
|
||
if (completed && completed.usage) {
|
||
var completedSession = pageAiCurrentSession();
|
||
if (completedSession) completedSession.usage = completed.usage;
|
||
}
|
||
var agentAudit = completed && completed.agentAudit && typeof completed.agentAudit === 'object' ? completed.agentAudit : null;
|
||
var changedFiles = pageAiNormalizeArray(agentAudit && agentAudit.changedFiles);
|
||
if (changedFiles.length) {
|
||
pageUiState.pageAiMessages.push({
|
||
role: 'tool',
|
||
content: 'agent.changed_files',
|
||
toolCallId: runId + ':agent.changed_files',
|
||
toolName: 'agent.changed_files',
|
||
toolKind: 'audit',
|
||
status: 'completed',
|
||
argsSummary: String(agentAudit.rootUri || ''),
|
||
resultSummary: String(agentAudit.diffSummary || changedFiles.length + ' changed file(s)'),
|
||
changedFiles: changedFiles,
|
||
traceId: runTraceId,
|
||
auditId: String(agentAudit.eventId || '')
|
||
});
|
||
try {
|
||
var currentId = currentDocumentId();
|
||
var currentPath = String(currentId || '').replace(/^local-md:/, '').replace(/~2F/g, '/');
|
||
var touchesCurrent = changedFiles.some(function(file) {
|
||
var path = String(file && (file.documentId || file.path || file.filePath || '') || '');
|
||
return path === currentId || (currentPath && path.indexOf(currentPath) >= 0);
|
||
});
|
||
if (touchesCurrent) {
|
||
window.dispatchEvent(new CustomEvent('mnote:page-ai-tool-write-completed', {
|
||
detail: {
|
||
toolName: 'agent.changed_files',
|
||
normalizedToolName: 'agent.changed_files',
|
||
documentId: currentId,
|
||
workspaceId: resolveWorkspaceId(document.body),
|
||
runId: runId,
|
||
traceId: runTraceId,
|
||
toolCallId: runId + ':agent.changed_files'
|
||
}
|
||
}));
|
||
}
|
||
} catch (_) {}
|
||
}
|
||
} catch (_) {}
|
||
pageAiSetRunStatus('completed', runId);
|
||
}
|
||
if (eventName === 'run.failed') {
|
||
try {
|
||
var failed = JSON.parse(payloadText || 'null');
|
||
assistantText = String((failed && (failed.message || failed.code || failed.error)) || 'Hermes run failed');
|
||
} catch (_) {}
|
||
pageAiSetRunStatus('failed', runId);
|
||
}
|
||
if (eventName === 'run.aborted' || eventName === 'run.cancelled' || eventName === 'run.canceled') {
|
||
pageUiState.pageAiStoppedRunIds[runId] = true;
|
||
pageAiSetRunStatus('aborted', runId);
|
||
}
|
||
if (eventName === '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 = '' +
|
||
'<div class="wolai-page-settings-panel" role="dialog" aria-modal="false">' +
|
||
'<div class="wolai-page-settings-tabs" data-testid="wolai-page-settings-tabs" role="tablist" aria-label="页面设置分组">' +
|
||
'<button type="button" class="wolai-page-settings-tab is-active" role="tab" aria-selected="true" data-page-settings-tab="page">页面选项</button>' +
|
||
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="custom">自定义页面</button>' +
|
||
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="index">索引</button>' +
|
||
'<button type="button" class="wolai-page-settings-tab" role="tab" aria-selected="false" data-page-settings-tab="global">全局选项</button>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-settings-section" data-page-settings-panel="page">' +
|
||
createPageOptionRow('wideLayout', 'checkbox') +
|
||
createPageOptionRow('smallText', 'checkbox') +
|
||
createPageOptionRow('showToc', 'checkbox') +
|
||
createPageOptionRow('protectEditing', 'checkbox') +
|
||
createPageOptionRow('hideTitleHeader', 'checkbox') +
|
||
'</div>' +
|
||
'<div class="wolai-page-settings-section" data-page-settings-panel="custom" hidden>' +
|
||
createPageFontRow() +
|
||
createPageOptionRow('layoutDensity', 'select') +
|
||
createPageOptionRow('collapseBacklinks', 'checkbox') +
|
||
createPageOptionRow('hideChildPages', 'checkbox') +
|
||
createPageOptionRow('showBlockRefCount', 'checkbox') +
|
||
'</div>' +
|
||
'<div class="wolai-page-settings-section" data-page-settings-panel="index" hidden>' +
|
||
'<div class="wolai-page-settings-index-panel" data-testid="wolai-page-settings-local-index-panel">' +
|
||
'<div class="wolai-page-settings-index-status" data-testid="wolai-page-settings-local-index-status"></div>' +
|
||
'<section class="wolai-page-settings-index-group">' +
|
||
'<div class="wolai-page-settings-index-title">反链</div>' +
|
||
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-backlinks"></div>' +
|
||
'</section>' +
|
||
'<section class="wolai-page-settings-index-group">' +
|
||
'<div class="wolai-page-settings-index-title">标签</div>' +
|
||
'<div class="wolai-page-settings-index-list" data-testid="wolai-page-settings-local-index-tags"></div>' +
|
||
'</section>' +
|
||
'</div>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
|
||
createGlobalHeadingNumbersRow() +
|
||
'</div>' +
|
||
'<div class="wolai-page-settings-actions">' +
|
||
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="history">页面历史...</button>' +
|
||
'<button type="button" class="wolai-page-settings-action" data-page-settings-action="share">公开分享页面...</button>' +
|
||
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="move" disabled>移动到...</button>' +
|
||
'<button type="button" class="wolai-page-settings-action is-disabled" data-page-settings-action="embed" disabled>嵌入到...</button>' +
|
||
'<button type="button" class="wolai-page-settings-action is-danger is-disabled" data-page-settings-action="delete" disabled>删除页面</button>' +
|
||
'</div>' +
|
||
'<div class="wolai-page-settings-stats" data-testid="wolai-page-settings-stats"></div>' +
|
||
'</div>';
|
||
document.body.appendChild(popover);
|
||
return popover;
|
||
}
|
||
|
||
function pageSettingsLocalIndexScopeKey() {
|
||
return [
|
||
currentSourceKind(),
|
||
currentRootUri(),
|
||
resolveWorkspaceId(document.body),
|
||
currentDocumentId()
|
||
].join('|');
|
||
}
|
||
|
||
function pageSettingsLocalIndexIsAvailable() {
|
||
return currentSourceKind() === 'local_folder' && Boolean(currentRootUri()) && Boolean(currentDocumentId());
|
||
}
|
||
|
||
function pageSettingsLocalIndexEmpty(message) {
|
||
return '<div class="wolai-page-settings-index-empty">' + escapeHtml(message) + '</div>';
|
||
}
|
||
|
||
function renderPageSettingsLocalIndexList(items, kind) {
|
||
var rows = Array.isArray(items) ? items : [];
|
||
if (!rows.length) {
|
||
return pageSettingsLocalIndexEmpty(kind === 'backlinks' ? '暂无反链' : '暂无标签');
|
||
}
|
||
if (kind === 'backlinks') {
|
||
return rows.slice(0, 12).map(function(item) {
|
||
var title = searchText(item && item.title) || searchText(item && item.path) || '未命名页面';
|
||
var path = searchText(item && item.path);
|
||
var snippet = searchText(item && item.snippet);
|
||
return '' +
|
||
'<div class="wolai-page-settings-index-row">' +
|
||
'<div class="wolai-page-settings-index-row-title">' + escapeHtml(title) + '</div>' +
|
||
(path ? '<div class="wolai-page-settings-index-row-meta">' + escapeHtml(path) + '</div>' : '') +
|
||
(snippet ? '<div class="wolai-page-settings-index-row-snippet">' + escapeHtml(snippet) + '</div>' : '') +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
return rows.slice(0, 16).map(function(item) {
|
||
var tag = searchText(item && item.tag) || 'untagged';
|
||
var count = Number(item && item.count || 0);
|
||
return '' +
|
||
'<div class="wolai-page-settings-index-row is-tag">' +
|
||
'<div class="wolai-page-settings-index-row-title">#' + escapeHtml(tag) + '</div>' +
|
||
'<div class="wolai-page-settings-index-row-meta">' + count + ' 个页面</div>' +
|
||
'</div>';
|
||
}).join('');
|
||
}
|
||
|
||
function renderPageSettingsLocalIndex(popover) {
|
||
popover = popover || ensurePageSettingsPopover();
|
||
var statusNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-status"]');
|
||
var backlinksNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-backlinks"]');
|
||
var tagsNode = popover.querySelector('[data-testid="wolai-page-settings-local-index-tags"]');
|
||
if (!(statusNode instanceof HTMLElement) || !(backlinksNode instanceof HTMLElement) || !(tagsNode instanceof HTMLElement)) return;
|
||
|
||
if (!pageSettingsLocalIndexIsAvailable()) {
|
||
statusNode.textContent = '本地索引仅在本地工作区页面可用';
|
||
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
|
||
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('未连接本地 root');
|
||
return;
|
||
}
|
||
|
||
var summary = pageUiState.localIndexSummary || {};
|
||
if (summary.loading) {
|
||
statusNode.textContent = '正在读取本地索引...';
|
||
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
|
||
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('加载中');
|
||
return;
|
||
}
|
||
if (summary.error) {
|
||
statusNode.textContent = '本地索引读取失败';
|
||
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
|
||
tagsNode.innerHTML = pageSettingsLocalIndexEmpty(summary.error);
|
||
return;
|
||
}
|
||
if (summary.scopeKey !== pageSettingsLocalIndexScopeKey()) {
|
||
statusNode.textContent = '切换到索引页签后读取本地索引';
|
||
backlinksNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
|
||
tagsNode.innerHTML = pageSettingsLocalIndexEmpty('等待读取');
|
||
return;
|
||
}
|
||
|
||
statusNode.textContent = '来自当前授权 root 的 .mnote/index/search-index.json';
|
||
backlinksNode.innerHTML = renderPageSettingsLocalIndexList(summary.backlinks, 'backlinks');
|
||
tagsNode.innerHTML = renderPageSettingsLocalIndexList(summary.tags, 'tags');
|
||
}
|
||
|
||
async function loadPageSettingsLocalIndex(force) {
|
||
if (!pageSettingsLocalIndexIsAvailable()) {
|
||
renderPageSettingsLocalIndex();
|
||
return;
|
||
}
|
||
var scopeKey = pageSettingsLocalIndexScopeKey();
|
||
var current = pageUiState.localIndexSummary || {};
|
||
if (!force && current.scopeKey === scopeKey && !current.error && !current.loading) {
|
||
renderPageSettingsLocalIndex();
|
||
return;
|
||
}
|
||
pageUiState.localIndexSummary = {
|
||
scopeKey: scopeKey,
|
||
loading: true,
|
||
error: '',
|
||
backlinks: null,
|
||
tags: null
|
||
};
|
||
renderPageSettingsLocalIndex();
|
||
try {
|
||
var baseParams = new URLSearchParams();
|
||
baseParams.set('workspaceId', resolveWorkspaceId(document.body));
|
||
baseParams.set('rootUri', currentRootUri());
|
||
var backlinksParams = new URLSearchParams(baseParams);
|
||
backlinksParams.set('documentId', currentDocumentId());
|
||
var backlinksUrl = '/api/search/local-index/backlinks?' + backlinksParams.toString();
|
||
var tagsUrl = '/api/search/local-index/tags?' + baseParams.toString();
|
||
var responses = await Promise.all([
|
||
fetch(backlinksUrl, { headers: { accept: 'application/json' } }),
|
||
fetch(tagsUrl, { headers: { accept: 'application/json' } })
|
||
]);
|
||
var backlinksPayload = await responses[0].json().catch(function(){ return null; });
|
||
var tagsPayload = await responses[1].json().catch(function(){ return null; });
|
||
if (!responses[0].ok || !backlinksPayload || backlinksPayload.ok !== true) {
|
||
throw new Error('backlinks_' + responses[0].status);
|
||
}
|
||
if (!responses[1].ok || !tagsPayload || tagsPayload.ok !== true) {
|
||
throw new Error('tags_' + responses[1].status);
|
||
}
|
||
pageUiState.localIndexSummary = {
|
||
scopeKey: scopeKey,
|
||
loading: false,
|
||
error: '',
|
||
backlinks: backlinksPayload.result && Array.isArray(backlinksPayload.result.backlinks) ? backlinksPayload.result.backlinks : [],
|
||
tags: tagsPayload.result && Array.isArray(tagsPayload.result.tags) ? tagsPayload.result.tags : []
|
||
};
|
||
} catch (error) {
|
||
pageUiState.localIndexSummary = {
|
||
scopeKey: scopeKey,
|
||
loading: false,
|
||
error: error instanceof Error ? error.message : String(error),
|
||
backlinks: [],
|
||
tags: []
|
||
};
|
||
}
|
||
renderPageSettingsLocalIndex();
|
||
}
|
||
|
||
function renderPageSettingsPopover() {
|
||
var popover = ensurePageSettingsPopover();
|
||
var options = currentPageOptions();
|
||
popover.querySelectorAll('[data-page-option-checkbox]').forEach(function(input) {
|
||
var key = input.getAttribute('data-page-option-checkbox');
|
||
input.checked = Boolean(options[key]);
|
||
input.disabled = !pageOptionIsSupported(key);
|
||
});
|
||
popover.querySelectorAll('[data-page-option-select]').forEach(function(select) {
|
||
var key = select.getAttribute('data-page-option-select');
|
||
var value = key === 'layoutDensity' ? String(options.layoutDensity || 'normal') : String(options.pageFont || 'default');
|
||
select.value = value;
|
||
});
|
||
renderGlobalOptions(popover);
|
||
var statsNode = popover.querySelector('[data-testid="wolai-page-settings-stats"]');
|
||
if (statsNode instanceof HTMLElement) {
|
||
var stats = computeLivePageStats();
|
||
statsNode.innerHTML = '' +
|
||
'<span>字数 ' + Number(stats.wordCount || 0) + '</span>' +
|
||
'<span>字符 ' + Number(stats.characterCount || 0) + '</span>' +
|
||
'<span>块数 ' + Number(stats.blockCount || 0) + '</span>' +
|
||
'<span>待办 ' + Number(stats.todoDone || 0) + '/' + Number(stats.todoTotal || 0) + '</span>';
|
||
}
|
||
renderPageSettingsLocalIndex(popover);
|
||
}
|
||
|
||
function setActivePageSettingsTab(tabName) {
|
||
var popover = ensurePageSettingsPopover();
|
||
popover.querySelectorAll('[data-page-settings-tab]').forEach(function(tab) {
|
||
var active = tab.getAttribute('data-page-settings-tab') === tabName;
|
||
tab.classList.toggle('is-active', active);
|
||
tab.setAttribute('aria-selected', active ? 'true' : 'false');
|
||
});
|
||
popover.querySelectorAll('[data-page-settings-panel]').forEach(function(panel) {
|
||
panel.hidden = panel.getAttribute('data-page-settings-panel') !== tabName;
|
||
});
|
||
if (tabName === 'index') void loadPageSettingsLocalIndex(false);
|
||
}
|
||
|
||
async function persistPageOptionsPatch(patch) {
|
||
var previous = Object.assign({}, currentPageOptions());
|
||
pageUiState.pageOptions = Object.assign({}, previous, patch);
|
||
var nextOptions = Object.assign({}, pageUiState.pageOptions);
|
||
applyPageOptionsToShell();
|
||
renderPageSettingsPopover();
|
||
try {
|
||
var response = await fetch('/api/documents/options', {
|
||
method: 'POST',
|
||
headers: { 'content-type': 'application/json' },
|
||
body: JSON.stringify({
|
||
documentId: currentDocumentId(),
|
||
workspaceId: resolveWorkspaceId(document.body),
|
||
...currentWorkspaceSourcePayload(),
|
||
options: nextOptions,
|
||
commandName: 'page.layout.updateOptions'
|
||
})
|
||
});
|
||
var payload = await response.json().catch(function(){ return null; });
|
||
if (!response.ok || !payload || payload.ok !== true) {
|
||
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'page_settings_save_failed_' + response.status);
|
||
}
|
||
document.documentElement.setAttribute('data-mnote-page-options-saved', 'true');
|
||
} catch (error) {
|
||
pageUiState.pageOptions = previous;
|
||
applyPageOptionsToShell();
|
||
renderPageSettingsPopover();
|
||
document.documentElement.setAttribute('data-mnote-page-options-error', error instanceof Error ? error.message : String(error));
|
||
}
|
||
}
|
||
|
||
function isPageSettingsOpen() {
|
||
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
|
||
return popover instanceof HTMLElement && !popover.hidden;
|
||
}
|
||
|
||
function openPageSettingsPopover() {
|
||
if (!currentDocumentId()) return;
|
||
var popover = ensurePageSettingsPopover();
|
||
renderPageSettingsPopover();
|
||
setActivePageSettingsTab('page');
|
||
popover.hidden = false;
|
||
pageUiState.pageSettingsOpen = true;
|
||
updatePageSettingsTriggerState();
|
||
}
|
||
|
||
function closePageSettingsPopover() {
|
||
var popover = document.querySelector('[data-testid="wolai-page-settings-popover"]');
|
||
if (popover instanceof HTMLElement) popover.hidden = true;
|
||
pageUiState.pageSettingsOpen = false;
|
||
updatePageSettingsTriggerState();
|
||
}
|
||
|
||
function togglePageSettingsPopover() {
|
||
if (isPageSettingsOpen()) closePageSettingsPopover();
|
||
else openPageSettingsPopover();
|
||
}
|
||
|
||
function attachmentQueryParams(href) {
|
||
try {
|
||
return new URL(String(href || ''), window.location.origin).searchParams;
|
||
} catch (_) {
|
||
return new URLSearchParams();
|
||
}
|
||
}
|
||
|
||
function 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 localFileOpenRootUriFromHref(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('rootUri') || '').trim();
|
||
} catch (_) {
|
||
return '';
|
||
}
|
||
}
|
||
|
||
function buildLocalFileStatusUrl(relativePath, rootUri) {
|
||
var effectiveRootUri = String(rootUri || currentRootUri() || '').trim();
|
||
if (!effectiveRootUri || !relativePath) return '';
|
||
var url = new URL('/api/local-folder/files/stat', window.location.origin);
|
||
url.searchParams.set('rootUri', effectiveRootUri);
|
||
url.searchParams.set('path', relativePath);
|
||
return url.toString();
|
||
}
|
||
|
||
function setEditorAttachmentMissingState(link, missing) {
|
||
if (!(link instanceof HTMLAnchorElement)) return;
|
||
var value = Boolean(missing);
|
||
if (value) {
|
||
link.setAttribute('data-mnote-attachment-missing', 'true');
|
||
link.classList.add('mnote-uploaded-attachment-missing');
|
||
link.setAttribute('aria-label', (link.textContent || '附件') + '(文件不存在)');
|
||
} else {
|
||
if (link.getAttribute('data-mnote-attachment-missing') !== 'true') return;
|
||
link.removeAttribute('data-mnote-attachment-missing');
|
||
link.classList.remove('mnote-uploaded-attachment-missing');
|
||
link.removeAttribute('aria-label');
|
||
}
|
||
}
|
||
|
||
async function refreshLocalAttachmentExistence(link) {
|
||
if (!(link instanceof HTMLAnchorElement)) return;
|
||
var href = link.getAttribute('href') || link.href || '';
|
||
var localFilePath = localFileOpenPathFromHref(href);
|
||
if (!localFilePath) return;
|
||
var statusUrl = buildLocalFileStatusUrl(localFilePath, localFileOpenRootUriFromHref(href));
|
||
if (!statusUrl) return;
|
||
try {
|
||
var response = await fetch(statusUrl, { headers: { accept: 'application/json' }, cache: 'no-store' });
|
||
var payload = await response.json().catch(function() { return null; });
|
||
var exists = Boolean(response.ok && payload && payload.ok === true && payload.result && payload.result.exists === true);
|
||
setEditorAttachmentMissingState(link, !exists);
|
||
} catch (_) {}
|
||
}
|
||
|
||
function refreshEditorLocalAttachmentExistence() {
|
||
document.querySelectorAll('.editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]').forEach(function(link) {
|
||
if (link instanceof HTMLAnchorElement) void refreshLocalAttachmentExistence(link);
|
||
});
|
||
}
|
||
window.__mnoteRefreshEditorLocalAttachmentExistence = refreshEditorLocalAttachmentExistence;
|
||
|
||
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 editorAttachmentPaneContext(link) {
|
||
var pane = link && typeof link.closest === 'function' ? link.closest('[data-document-pane="true"]') : null;
|
||
var roleHost = link && typeof link.closest === 'function' ? link.closest('[data-pane-role]') : null;
|
||
var paneRole = (
|
||
pane instanceof HTMLElement && pane.getAttribute('data-pane-role') === 'secondary'
|
||
) || (
|
||
roleHost instanceof HTMLElement && roleHost.getAttribute('data-pane-role') === 'secondary'
|
||
) ? 'secondary' : 'primary';
|
||
var paneDocumentId = '';
|
||
var paneWorkspaceId = '';
|
||
if (pane instanceof HTMLElement) {
|
||
paneDocumentId = (pane.getAttribute('data-pane-document-id') || '').trim();
|
||
paneWorkspaceId = (pane.getAttribute('data-pane-workspace-id') || '').trim();
|
||
var shell = pane.querySelector('.document-shell[data-document-id]');
|
||
if (!paneDocumentId && shell instanceof HTMLElement) paneDocumentId = (shell.getAttribute('data-document-id') || '').trim();
|
||
if (!paneWorkspaceId && shell instanceof HTMLElement) paneWorkspaceId = (shell.getAttribute('data-workspace-id') || '').trim();
|
||
}
|
||
if (roleHost instanceof HTMLElement) {
|
||
if (!paneDocumentId) paneDocumentId = (roleHost.getAttribute('data-document-id') || '').trim();
|
||
if (!paneWorkspaceId) paneWorkspaceId = (roleHost.getAttribute('data-workspace-id') || '').trim();
|
||
var roleHostShell = roleHost.matches('.document-shell') ? roleHost : roleHost.querySelector?.('.document-shell[data-document-id]');
|
||
if (!paneDocumentId && roleHostShell instanceof HTMLElement) paneDocumentId = (roleHostShell.getAttribute('data-document-id') || '').trim();
|
||
if (!paneWorkspaceId && roleHostShell instanceof HTMLElement) paneWorkspaceId = (roleHostShell.getAttribute('data-workspace-id') || '').trim();
|
||
}
|
||
return {
|
||
paneRole: paneRole,
|
||
documentId: paneDocumentId || currentDocumentId() || '',
|
||
workspaceId: paneWorkspaceId || resolveWorkspaceId(document.body) || ''
|
||
};
|
||
}
|
||
|
||
function detailFromEditorAttachmentLink(link) {
|
||
var rawHref = link instanceof HTMLAnchorElement ? link.href : '';
|
||
var params = attachmentQueryParams(rawHref);
|
||
var paneContext = editorAttachmentPaneContext(link);
|
||
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 documentId = params.get('documentId') || paneContext.documentId || '';
|
||
var href = rawHref;
|
||
if (!isOnlyOfficeAttachmentHref(rawHref) && fileType) {
|
||
fileUrl = rawHref;
|
||
href = buildOnlyOfficeOpenUrl({
|
||
fileUrl: fileUrl,
|
||
fileName: fileName,
|
||
fileType: fileType,
|
||
assetId: assetId,
|
||
documentId: documentId,
|
||
userId: '',
|
||
mode: 'view'
|
||
});
|
||
} else if (isOnlyOfficeAttachmentHref(rawHref)) {
|
||
href = normalizeOnlyOfficeAttachmentHref(rawHref);
|
||
}
|
||
return {
|
||
href: href,
|
||
fileUrl: fileUrl,
|
||
fileName: fileName,
|
||
title: fileName,
|
||
fileType: fileType,
|
||
assetId: assetId,
|
||
documentId: documentId,
|
||
workspaceId: paneContext.workspaceId,
|
||
paneRole: paneContext.paneRole,
|
||
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 paneContext = editorAttachmentPaneContext(link);
|
||
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);
|
||
if (!shouldEnhance) return;
|
||
if (localFilePath && !isOnlyOfficeAttachmentHref(href)) {
|
||
void refreshLocalAttachmentExistence(link);
|
||
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') || paneContext.documentId || '',
|
||
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();
|
||
}
|
||
window.__mnoteEnhanceEditorAttachmentLinks = function() {
|
||
observeEditorAttachmentRoots();
|
||
enhanceEditorAttachmentLinks();
|
||
};
|
||
|
||
function ensureAttachmentActions() {
|
||
var existing = document.querySelector('[data-testid="mnote-attachment-actions"]');
|
||
if (existing instanceof HTMLElement) return existing;
|
||
var actions = document.createElement('div');
|
||
actions.className = 'mnote-attachment-actions';
|
||
actions.setAttribute('data-testid', 'mnote-attachment-actions');
|
||
actions.hidden = true;
|
||
actions.innerHTML = '' +
|
||
'<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-download" data-attachment-action="download" aria-label="下载附件"><span class="material-symbols-outlined" data-icon="download" aria-hidden="true"></span></button>' +
|
||
'<button type="button" class="mnote-attachment-action" data-testid="mnote-attachment-action-menu" data-attachment-action="menu" aria-label="更多操作"><span class="material-symbols-outlined" data-icon="more_horiz" aria-hidden="true"></span></button>';
|
||
actions.addEventListener('mouseenter', function() {
|
||
if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer);
|
||
});
|
||
actions.addEventListener('mouseleave', scheduleHideAttachmentActions);
|
||
document.body.appendChild(actions);
|
||
return actions;
|
||
}
|
||
|
||
function positionAttachmentActions(link) {
|
||
if (!(link instanceof HTMLElement)) return;
|
||
var actions = ensureAttachmentActions();
|
||
var rect = link.getBoundingClientRect();
|
||
actions.hidden = false;
|
||
actions.style.left = Math.min(window.innerWidth - 76, Math.max(8, rect.right + 6)) + 'px';
|
||
actions.style.top = Math.max(8, rect.top + (rect.height - 28) / 2) + 'px';
|
||
activeEditorAttachmentLink = link;
|
||
}
|
||
|
||
function scheduleHideAttachmentActions() {
|
||
if (attachmentActionsHideTimer) window.clearTimeout(attachmentActionsHideTimer);
|
||
attachmentActionsHideTimer = window.setTimeout(function() {
|
||
var actions = document.querySelector('[data-testid="mnote-attachment-actions"]');
|
||
if (actions instanceof HTMLElement) actions.hidden = true;
|
||
activeEditorAttachmentLink = null;
|
||
}, 220);
|
||
}
|
||
|
||
async function openEditorAttachmentDetail(detail) {
|
||
if (!detail || !detail.href) return;
|
||
var localFilePath = localFilePathFromAssetId(detail.assetId);
|
||
if (localFilePath) {
|
||
if (await openLocalOfficeFileInActiveTab(detail, 'view')) return;
|
||
// 非 Office 本地文件:用对应图标类型打开 active tab,失败则新窗口
|
||
void openLocalResourceInActiveTab({
|
||
path: localFilePath,
|
||
title: detail.fileName || localFilePath.split('/').pop() || localFilePath,
|
||
kind: fileTreeIconKindForFileName(detail.fileName || localFilePath.split('/').pop() || localFilePath),
|
||
assetId: detail.assetId,
|
||
documentId: detail.documentId || currentDocumentId() || '',
|
||
workspaceId: detail.workspaceId || resolveWorkspaceId(document.body) || '',
|
||
href: buildLocalFileOpenUrl(localFilePath, false),
|
||
paneRole: detail.paneRole || 'primary'
|
||
}).then(function(opened) {
|
||
if (!opened) window.open(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 || '',
|
||
paneRole: detail.paneRole || 'primary'
|
||
});
|
||
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) {
|
||
if (await openLocalOfficeFileInActiveTab(detail, 'edit')) return true;
|
||
}
|
||
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 || '',
|
||
paneRole: detail.paneRole || 'primary'
|
||
});
|
||
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 attachmentEnhanceFrame = 0;
|
||
var attachmentEditorObserver = null;
|
||
var attachmentObservedEditors = typeof WeakSet === 'function' ? new WeakSet() : null;
|
||
function scheduleEditorAttachmentEnhance() {
|
||
if (attachmentEnhanceFrame) return;
|
||
attachmentEnhanceFrame = window.requestAnimationFrame(function() {
|
||
attachmentEnhanceFrame = 0;
|
||
enhanceEditorAttachmentLinks();
|
||
observeEditorAttachmentRoots();
|
||
});
|
||
}
|
||
function addedNodeMayContainEditorAttachmentLink(node) {
|
||
return node instanceof HTMLElement && (
|
||
node.matches('.editor-surface .ProseMirror, .editor-surface .ProseMirror a[href], .editor-surface .ProseMirror p, .editor-surface .ProseMirror span, .editor-surface .ProseMirror div')
|
||
|| node.querySelector?.('.editor-surface .ProseMirror, .editor-surface .ProseMirror a[href]')
|
||
);
|
||
}
|
||
function observeEditorAttachmentRoots() {
|
||
if (!attachmentEditorObserver) return;
|
||
document.querySelectorAll('.editor-surface .ProseMirror').forEach(function(editor) {
|
||
if (!(editor instanceof HTMLElement)) return;
|
||
if (attachmentObservedEditors && attachmentObservedEditors.has(editor)) return;
|
||
if (attachmentObservedEditors) attachmentObservedEditors.add(editor);
|
||
attachmentEditorObserver.observe(editor, { childList: true, subtree: true });
|
||
});
|
||
}
|
||
attachmentEditorObserver = new MutationObserver(function(records) {
|
||
var shouldEnhance = Array.isArray(records) && records.some(function(record) {
|
||
if (!record || record.type !== 'childList') return false;
|
||
return Array.from(record.addedNodes || []).some(function(node) {
|
||
return addedNodeMayContainEditorAttachmentLink(node);
|
||
});
|
||
});
|
||
if (!shouldEnhance) return;
|
||
scheduleEditorAttachmentEnhance();
|
||
});
|
||
attachmentEditorObserver.observe(document.documentElement, { childList: true, subtree: true });
|
||
observeEditorAttachmentRoots();
|
||
window.addEventListener('mnote:editor-attachment-links-changed', function() {
|
||
window.__mnoteEnhanceEditorAttachmentLinks();
|
||
scheduleEditorAttachmentEnhance();
|
||
});
|
||
|
||
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, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
|
||
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, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
|
||
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, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
|
||
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, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
|
||
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, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
|
||
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 accountMenuTrigger = closestAction(e.target, '[data-mnote-action="open-account-menu"]');
|
||
if (accountMenuTrigger) {
|
||
e.preventDefault();
|
||
var existingAccountMenu = document.querySelector('[data-testid="mnote-account-menu"]');
|
||
if (existingAccountMenu) closeAccountMenu();
|
||
else openAccountMenu(accountMenuTrigger);
|
||
return;
|
||
}
|
||
var accountMenu = closestAction(e.target, '[data-testid="mnote-account-menu"]');
|
||
if (!accountMenu) closeAccountMenu();
|
||
|
||
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 ownerDocumentId = fileRow.getAttribute('data-owner-document-id') || documentId;
|
||
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: ownerDocumentId || 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: ownerDocumentId || null, assetId: assetId, assetType: assetType || null, objectIdentity: objectIdentity, workspaceId: resolveWorkspaceId(fileRow), openTarget: e.shiftKey || e.ctrlKey || e.metaKey ? 'new-window' : 'active-tab' });
|
||
}
|
||
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, .editor-surface .ProseMirror a[href*="/api/local-folder/files/open"]');
|
||
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) {
|
||
var _kbRT_ = window.__mnoteFileTreeKeyboardRuntime;
|
||
if (_kbRT_ && typeof _kbRT_.handleFileTreeKeyDown === 'function') {
|
||
var handled = _kbRT_.handleFileTreeKeyDown(event, {
|
||
closestAction: closestAction,
|
||
beginFileTreeInlineRename: beginFileTreeInlineRename,
|
||
selectedSidebarFileTreeRows: selectedSidebarFileTreeRows,
|
||
buildSidebarFileTreeContext: buildSidebarFileTreeContext,
|
||
evaluateSidebarFileTreeWhen: evaluateSidebarFileTreeWhen,
|
||
deleteSelectedSidebarFileTreeRows: deleteSelectedSidebarFileTreeRows,
|
||
pasteSidebarFileTreeClipboard: pasteSidebarFileTreeClipboard,
|
||
});
|
||
if (handled) return;
|
||
} else {
|
||
// inline fallback
|
||
if (event.key === 'F2') {
|
||
event.preventDefault();
|
||
beginFileTreeInlineRename(fileTreeRowForKey);
|
||
return;
|
||
}
|
||
if ((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') {
|
||
event.preventDefault();
|
||
var delCtx = buildSidebarFileTreeContext('filetree');
|
||
if (!evaluateSidebarFileTreeWhen(delCtx, '!workspace.readonly')) {
|
||
return;
|
||
}
|
||
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();
|
||
installWorkspaceSidebarResizer();
|
||
}
|
||
|
||
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) {
|
||
var _dndRT_ = window.__mnoteFileTreeDndRuntime;
|
||
if (_dndRT_ && typeof _dndRT_.startFileTreeDrag === 'function') {
|
||
var dragInfo = _dndRT_.startFileTreeDrag(fileRow, {
|
||
selectedSidebarFileTreeRowIdsForDrag: selectedSidebarFileTreeRowIdsForDrag,
|
||
});
|
||
draggingFileTreeRowIds = _dndRT_.draggingFileTreeRowIds;
|
||
if (event.dataTransfer && dragInfo) {
|
||
event.dataTransfer.effectAllowed = dragInfo.effectAllowed || 'copyMove';
|
||
event.dataTransfer.setData(_dndRT_.FILETREE_DRAG_MIME, dragInfo.payload);
|
||
event.dataTransfer.setData('application/x-mnote-file-tree', dragInfo.payload);
|
||
event.dataTransfer.setData('text/plain', dragInfo.payload);
|
||
}
|
||
} else {
|
||
// inline fallback
|
||
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 _dndRT_ = window.__mnoteFileTreeDndRuntime;
|
||
if (_dndRT_ && typeof _dndRT_.handleFileTreeDragOver === 'function') {
|
||
if (_dndRT_.handleFileTreeDragOver(event, { closestAction: closestAction })) {
|
||
return;
|
||
}
|
||
} else {
|
||
// inline fallback
|
||
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 _dndRT_ = window.__mnoteFileTreeDndRuntime;
|
||
if (_dndRT_ && typeof _dndRT_.handleFileTreeDrop === 'function') {
|
||
_dndRT_.handleFileTreeDrop(event, {
|
||
closestAction: closestAction,
|
||
resolveWorkspaceId: resolveWorkspaceId,
|
||
fileTreeRowLocalUploadTargetRelativePath: fileTreeRowLocalUploadTargetRelativePath,
|
||
ensureFileTreeWritableTarget: ensureFileTreeWritableTarget,
|
||
dispatchSidebarEvent: dispatchSidebarEvent,
|
||
});
|
||
} else {
|
||
// inline fallback
|
||
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 ? fileTreeRowLocalUploadTargetRelativePath(targetRow) : '',
|
||
uploadIntent: 'filetree.folder.drop'
|
||
};
|
||
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 = '';
|
||
var _dndRT_ = window.__mnoteFileTreeDndRuntime;
|
||
if (_dndRT_ && typeof _dndRT_.resetFileTreeDragState === 'function') {
|
||
_dndRT_.resetFileTreeDragState();
|
||
} else {
|
||
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)) {
|
||
refreshEditorLocalAttachmentExistence();
|
||
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();
|
||
schedulePendingLocalFolderRestoreFocus();
|
||
window.addEventListener('mnote:primary-document-activated', function(event) {
|
||
var detail = event && event.detail ? event.detail : {};
|
||
selectSidebarFileTreeDocument(detail.documentId, { scrollIntoView: false });
|
||
});
|
||
restoreSidebarTreeTab();
|
||
startLocalFolderSidebarWatch();
|
||
})();
|