Harden auth/vault path sanitization and clean WeKnora docs

This commit is contained in:
Agent Board
2026-07-28 17:04:27 +08:00
parent 2deaf59f7b
commit 26ff1a9c9a
190 changed files with 13454 additions and 4987 deletions
@@ -293,6 +293,8 @@ import {
};
const paneViewRegistry = new Map();
/** 每 pane 导航世代:快速连点时丢弃过期 replacePaneDocument 结果,保证 last-click-wins。 */
const paneReplaceGeneration = new Map();
const currentWebShellWorkspaceId = () => {
try {
return currentUrl().searchParams.get('workspaceId') || '';
@@ -709,6 +711,8 @@ import {
};
const replacePaneDocument = async (paneRole, descriptor, options = {}) => {
const generation = (paneReplaceGeneration.get(paneRole) || 0) + 1;
paneReplaceGeneration.set(paneRole, generation);
mindmapHost.unmountMindmapPane(paneRole);
const root = paneRole === 'primary'
? ensureLazyPrimaryPaneRoot()
@@ -719,8 +723,12 @@ import {
const aggregatePromise = options.aggregate
? Promise.resolve(options.aggregate)
: fetchPageAggregateForPane(descriptor);
const previousView = paneViewRegistry.get(paneRole);
const [runtime, aggregate] = await Promise.all([runtimePromise, aggregatePromise]);
// 过期导航:丢弃结果,避免覆盖更新的 last-click View 并泄漏中间实例。
if (paneReplaceGeneration.get(paneRole) !== generation) {
return null;
}
const previousView = paneViewRegistry.get(paneRole);
if (previousView) {
unmountEditorViewBinding(previousView);
paneViewRegistry.delete(paneRole);
@@ -754,6 +762,13 @@ import {
clearEmbeddedLocalDraft(runtimeDescriptor);
try {
const mountId = runtime.mount(runtimeDescriptor.root, mountOptions);
if (paneReplaceGeneration.get(paneRole) !== generation) {
try {
if (typeof runtime.unmount === 'function') runtime.unmount(mountId);
} catch (_) { /* ignore stale unmount */ }
unmountEditorViewBinding(view);
return null;
}
view.mountId = mountId;
runtimeDescriptor.root.setAttribute('data-runtime-mount-id', String(mountId));
runtimeDescriptor.root.setAttribute('data-editor-host-kind', 'leptos_tiptap_island');
@@ -1181,7 +1196,9 @@ import {
if (!id) return false;
const descriptor = descriptorFromCurrentUrl('primary', id, { workspaceId, sourceKind, rootUri });
descriptor.pageBlockNavigation = pageBlockNavigation === true;
await replacePaneDocument('primary', descriptor);
const view = await replacePaneDocument('primary', descriptor);
// 过期导航(快速连点被 generation 丢弃)不得改 URL / 激活 tab。
if (!view) return false;
activateMainEditorTab('', 'primary');
updatePrimaryUrl(descriptor, url instanceof URL ? url : null);
return true;
@@ -1202,7 +1219,8 @@ import {
const id = typeof documentId === 'string' ? documentId.trim() : '';
if (!id) return false;
const descriptor = descriptorFromCurrentUrl('secondary', id, { workspaceId, sourceKind, rootUri });
await replacePaneDocument('secondary', descriptor);
const view = await replacePaneDocument('secondary', descriptor);
if (!view) return false;
activateMainEditorTab('', 'secondary');
if (url instanceof URL) replaceUrlState(url);
return true;
@@ -1473,7 +1473,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
};
const knowledgeRagProvider = () => {
return 'weknora';
return 'lightrag_legacy';
};
const setKnowledgeRagStatus = (entry, status, message, job) => {
@@ -11,20 +11,65 @@ function uploadedAssetTitle(asset) {
return String(asset && (asset.file_name || asset.title || asset.name) || '未命名附件').trim() || '未命名附件';
}
/** Reject path escape segments and null bytes after light decode. */
function hasPathEscape(value) {
var s = String(value || '').replace(/\\/g, '/');
try {
s = decodeURIComponent(s);
} catch (_) {
// keep raw
}
s = s.replace(/\\/g, '/');
if (s.indexOf('\0') >= 0) return true;
var parts = s.split('/');
for (var i = 0; i < parts.length; i += 1) {
if (parts[i] === '..') return true;
}
return false;
}
/** Allow only safe URL schemes for editor img/src or link href. */
function isSafeAssetUrl(url) {
var value = String(url || '').trim();
if (!value) return false;
if (value.charAt(0) === '#' || value.charAt(0) === '/' || value.indexOf('./') === 0) {
return !hasPathEscape(value);
}
// scheme-relative
if (value.indexOf('//') === 0) return false;
var colon = value.indexOf(':');
if (colon < 0) {
// relative path without scheme
return !hasPathEscape(value);
}
var scheme = value.slice(0, colon).toLowerCase();
if (scheme === 'http' || scheme === 'https' || scheme === 'blob' || scheme === 'data') {
// data: raster images only. Reject svg+xml (can embed script even when base64 hides "script").
if (scheme === 'data') {
if (!/^data:image\//i.test(value)) return false;
if (/^data:image\/svg\+xml/i.test(value)) return false;
// deny explicit script markers in non-svg image payloads
if (value.toLowerCase().indexOf('script') >= 0) return false;
return true;
}
return true;
}
return false;
}
function uploadedAssetUrl(asset) {
return String(asset && (asset.sourcePath || asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim();
var raw = String(asset && (asset.sourcePath || asset.file_url || asset.signedUrl || asset.signed_url || asset.thumbnail_url) || '').trim();
return isSafeAssetUrl(raw) ? raw : '';
}
function uploadedAssetMarkdownHref(asset) {
var href = String(asset && (asset.markdownHref || asset.markdown_href) || '').trim();
if (href) {
var normalizedHref = href.replace(/\\/g, '/');
if (normalizedHref.indexOf('../') === 0 || normalizedHref.indexOf('/../') >= 0) return '';
if (hasPathEscape(href) || !isSafeAssetUrl(href)) return '';
return href;
}
var relativePath = String(asset && (asset.markdownRelativePath || asset.markdown_relative_path) || '').trim().replace(/\\/g, '/');
if (!relativePath) return '';
if (relativePath.indexOf('../') === 0 || relativePath.indexOf('/../') >= 0) return '';
if (!relativePath || hasPathEscape(relativePath)) return '';
if (relativePath.indexOf('./') === 0) return relativePath;
return './' + relativePath;
}
@@ -621,9 +666,9 @@ async function uploadFilesWithResolvedTarget(files, detail, options, deps) {
if (errors.length) {
var message = '部分文件上传失败:\n' + errors.slice(0, 6).join('\n') + (errors.length > 6 ? '\n...' : '');
if (typeof deps.alert === 'function') {
deps.alert(message);
await deps.alert(message);
} else {
window.alert(message);
await window.mnote.alert(message);
}
}
return uploaded;
@@ -633,7 +678,7 @@ function localAssetOpenUrl(asset, download, context) {
if (!isLocalUploadedAsset(asset)) return '';
var rootUri = String(context && context.rootUri || asset && (asset.rootUri || asset.root_uri) || '').trim() || currentRootUri();
var rootRelativePath = String(asset && (asset.rootRelativePath || asset.root_relative_path) || '').trim();
if (!rootUri || !rootRelativePath) return '';
if (!rootUri || !rootRelativePath || hasPathEscape(rootRelativePath)) return '';
var url = new URL('/api/local-folder/files/open', window.location.origin);
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('path', rootRelativePath);
@@ -1,8 +1,12 @@
// MNote UI 基础运行时:全局 portaltoast API
// MNote UI 基础运行时:全局 portaltoast、居中 confirm/alert
(function initMnoteUiRuntime() {
if (window.__mnoteUiRuntimeStarted) return;
window.__mnoteUiRuntimeStarted = true;
var nativeConfirm = typeof window.confirm === 'function' ? window.confirm.bind(window) : null;
var nativeAlert = typeof window.alert === 'function' ? window.alert.bind(window) : null;
var activeDialog = null;
function ensurePortalRoot() {
var root = document.getElementById('mnote-portal-root');
if (root instanceof HTMLElement) return root;
@@ -52,15 +56,192 @@
return item;
}
function closeActiveDialog(result) {
if (!activeDialog) return;
var state = activeDialog;
activeDialog = null;
document.removeEventListener('keydown', state.onKeyDown, true);
if (state.backdrop && state.backdrop.parentNode) {
state.backdrop.parentNode.removeChild(state.backdrop);
}
if (typeof state.resolve === 'function') state.resolve(result);
}
/**
* 居中对话框。
* @param {object} options
* @param {string} [options.title]
* @param {string} options.message
* @param {'confirm'|'alert'} [options.mode]
* @param {string} [options.confirmLabel]
* @param {string} [options.cancelLabel]
* @param {'primary'|'danger'} [options.tone]
* @returns {Promise<boolean>} confirm 模式:确定=truealert 模式始终 true
*/
function showDialog(options) {
var config = options || {};
var message = String(config.message || '').trim();
if (!message) return Promise.resolve(config.mode === 'alert');
return new Promise(function (resolve) {
if (activeDialog) closeActiveDialog(false);
var mode = config.mode === 'alert' ? 'alert' : 'confirm';
var tone = String(config.tone || 'primary');
var title =
String(config.title || (mode === 'alert' ? '提示' : '确认')).trim() ||
(mode === 'alert' ? '提示' : '确认');
var confirmLabel = String(config.confirmLabel || (mode === 'alert' ? '知道了' : '确定')).trim();
var cancelLabel = String(config.cancelLabel || '取消').trim();
var backdrop = document.createElement('div');
backdrop.className = 'mnote-dialog-backdrop';
backdrop.setAttribute('data-mnote-dialog-backdrop', 'true');
backdrop.setAttribute('data-mode', mode);
var dialog = document.createElement('div');
dialog.className =
'mnote-dialog' + (tone === 'danger' ? ' mnote-dialog--danger' : '');
dialog.setAttribute('role', 'dialog');
dialog.setAttribute('aria-modal', 'true');
dialog.setAttribute('data-mnote-dialog', 'true');
var body = document.createElement('div');
body.className = 'mnote-dialog__body';
var titleEl = document.createElement('h2');
titleEl.className = 'mnote-dialog__title';
titleEl.textContent = title;
body.appendChild(titleEl);
var messageEl = document.createElement('p');
messageEl.className = 'mnote-dialog__message';
messageEl.textContent = message;
body.appendChild(messageEl);
dialog.appendChild(body);
var actions = document.createElement('div');
actions.className = 'mnote-dialog__actions';
var confirmBtn = document.createElement('button');
confirmBtn.type = 'button';
confirmBtn.className =
'mnote-dialog__btn ' +
(tone === 'danger' ? 'mnote-dialog__btn--danger' : 'mnote-dialog__btn--primary');
confirmBtn.setAttribute('data-role', 'confirm');
confirmBtn.textContent = confirmLabel || '确定';
if (mode === 'confirm') {
var cancelBtn = document.createElement('button');
cancelBtn.type = 'button';
cancelBtn.className = 'mnote-dialog__btn';
cancelBtn.setAttribute('data-role', 'cancel');
cancelBtn.textContent = cancelLabel || '取消';
cancelBtn.addEventListener('click', function () {
closeActiveDialog(false);
});
actions.appendChild(cancelBtn);
}
confirmBtn.addEventListener('click', function () {
closeActiveDialog(true);
});
actions.appendChild(confirmBtn);
dialog.appendChild(actions);
backdrop.appendChild(dialog);
backdrop.addEventListener('click', function (event) {
if (event.target === backdrop) {
closeActiveDialog(mode === 'alert' ? true : false);
}
});
var onKeyDown = function (event) {
if (event.key === 'Escape') {
event.preventDefault();
closeActiveDialog(mode === 'alert' ? true : false);
} else if (event.key === 'Enter') {
var tag = (event.target && event.target.tagName) || '';
if (tag === 'TEXTAREA' || tag === 'BUTTON') return;
event.preventDefault();
closeActiveDialog(true);
}
};
activeDialog = {
backdrop: backdrop,
resolve: resolve,
onKeyDown: onKeyDown,
};
document.addEventListener('keydown', onKeyDown, true);
ensurePortalRoot().appendChild(backdrop);
(mode === 'confirm' ? actions.querySelector('[data-role="cancel"]') || confirmBtn : confirmBtn).focus({
preventScroll: true,
});
});
}
function confirmDialog(message, options) {
var config = options && typeof options === 'object' ? options : {};
return showDialog({
mode: 'confirm',
message: message,
title: config.title,
confirmLabel: config.confirmLabel || config.okText,
cancelLabel: config.cancelLabel || config.cancelText,
tone: config.tone || (config.danger ? 'danger' : 'primary'),
});
}
function alertDialog(message, options) {
var config = options && typeof options === 'object' ? options : {};
return showDialog({
mode: 'alert',
message: message,
title: config.title || '提示',
confirmLabel: config.confirmLabel || config.okText || '知道了',
tone: config.tone || (config.kind === 'error' || config.danger ? 'danger' : 'primary'),
}).then(function () {
return undefined;
});
}
window.mnote = window.mnote || {};
window.mnote.ensurePortalRoot = ensurePortalRoot;
window.mnote.toast = toast;
window.mnote.confirm = confirmDialog;
window.mnote.alert = alertDialog;
window.mnote.dialog = showDialog;
// 全局替换:统一返回 Promise。调用点需 await(已在各 runtime 迁移)。
window.alert = function mnoteWindowAlert(message) {
if (!document.body) {
if (nativeAlert) nativeAlert(message);
return Promise.resolve();
}
return alertDialog(message);
};
window.confirm = function mnoteWindowConfirm(message) {
if (!document.body) {
return Promise.resolve(nativeConfirm ? !!nativeConfirm(message) : true);
}
return confirmDialog(message);
};
window.addEventListener('mnote:toast', function onMnoteToast(event) {
var detail = event && event.detail ? event.detail : {};
toast(detail.message, detail);
});
window.addEventListener('mnote:confirm', function onMnoteConfirm(event) {
var detail = event && event.detail ? event.detail : {};
var message = detail.message || '';
confirmDialog(message, detail).then(function (ok) {
if (typeof detail.onResult === 'function') detail.onResult(ok);
});
});
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', ensurePortalRoot, { once: true });
} else {
@@ -783,7 +783,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
var resolved = await resolveEditorAttachmentUrl(detail);
var title = String(detail.fileName || resolved.asset.file_name || 'PDF').trim() || 'PDF';
var previewUrl = buildPdfPreviewOpenUrl(resolved.url, title);
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'async function') {
var didOpenPdfTab = await window.__mnoteDocumentPaneRuntime.openResourceInActiveTab({
objectIdentity: 'resource:pdf:' + (detail.documentId || '') + ':' + (detail.assetId || previewUrl || resolved.url),
assetId: detail.assetId || '',
@@ -799,7 +799,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
}
window.open(previewUrl || resolved.url, '_blank', 'noopener,noreferrer');
} catch (error) {
window.alert(error && error.message ? error.message : '打开 PDF 失败');
await window.mnote.alert(error && error.message ? error.message : '打开 PDF 失败');
}
}
@@ -848,7 +848,7 @@ export const createSidebarAttachmentOpenRuntime = (dependencies = {}) => {
window.open(resolved.url, '_blank', 'noopener,noreferrer');
return;
}
window.alert(error && error.message ? error.message : '打开代码附件失败');
await window.mnote.alert(error && error.message ? error.message : '打开代码附件失败');
}
}
@@ -148,10 +148,10 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
}).then(function(){
if (!localFolderSource && documentId) updateTitleEverywhere(documentId, commandTitle);
});
void work.then(close).catch(function(error) {
void work.then(close).catch(async function(error) {
committing = false;
input.disabled = false;
window.alert(error && error.message ? error.message : '重命名失败');
await window.mnote.alert(error && error.message ? error.message : '重命名失败');
});
};
input.addEventListener('click', function(event) { event.stopPropagation(); });
@@ -298,7 +298,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
return url.toString();
}
function convertToPreviousSiblingChild(trigger, detail) {
async 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;
@@ -308,7 +308,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
});
var index = siblings.indexOf(row);
if (index <= 0) {
window.alert('当前页面前面没有同级页面。');
await window.mnote.alert('当前页面前面没有同级页面。');
return;
}
var previous = siblings[index - 1];
@@ -369,9 +369,21 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
function joinLocalAbsolutePath(rootUri, relativePath) {
var rootPath = localRootPathFromRootUri(rootUri);
var normalized = decodeLocalEncodedPath(relativePath).replace(/^\/+/, '');
var normalized = decodeLocalEncodedPath(relativePath).replace(/^\/+/, '').replace(/\\/g, '/');
if (!rootPath || !normalized) return rootPath || normalized;
return rootPath.replace(/\/+$/g, '') + '/' + normalized;
// Collapse . / empty and resolve .. without climbing above workspace root.
var parts = [];
normalized.split('/').forEach(function (seg) {
if (!seg || seg === '.') return;
if (seg === '..') {
if (parts.length) parts.pop();
return;
}
parts.push(seg);
});
var safeRel = parts.join('/');
if (!safeRel) return rootPath.replace(/\/+$/g, '');
return rootPath.replace(/\/+$/g, '') + '/' + safeRel;
}
function fileTreeCopyLocalAbsolutePath(detail, trigger) {
@@ -488,7 +500,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
return payload;
}
function handleTreeContextMenuAction(action, detail, trigger) {
async function handleTreeContextMenuAction(action, detail, trigger) {
closeTreeContextMenu();
detail = detail || {};
if (action === 'knowledge-rag-index') {
@@ -499,13 +511,13 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
if (window.mnote && window.mnote.toast) {
window.mnote.toast('已加入知识库', { kind: 'success' });
}
}).catch(function(error) {
}).catch(async function(error) {
var msg = error && error.message ? error.message : '知识库索引失败';
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
if (window.mnote && window.mnote.toast) {
window.mnote.toast(msg, { kind: 'error' });
} else {
window.alert(msg);
await window.mnote.alert(msg);
}
});
return;
@@ -584,7 +596,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
return;
}
if (isAsset && action === 'open-edit-mode') {
withOfficeEditModeGuard(function() {
withOfficeEditModeGuard(async function() {
recordFileTreeAction('open-edit-mode', detail);
void openConvexAssetFromFileTree({ ...detail, openTarget: 'edit-mode' });
});
@@ -635,15 +647,15 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
}
}
if (action === 'delete-trash' && isAsset) {
if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return;
if (!(await window.mnote.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) {
}).catch(async function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '资源删除失败');
await window.mnote.alert(error && error.message ? error.message : '资源删除失败');
});
return;
}
@@ -655,9 +667,9 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
recordFileTreeAction('new-folder', detail);
void createFileTreeFolder(trigger || document.body, fileTreeMenuTargetParentId(detail, trigger) || null).then(function(ok) {
recordFileTreeActionStatus(ok ? 'created' : 'skipped', detail);
}).catch(function(error) {
}).catch(async function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '新建文件夹失败');
await window.mnote.alert(error && error.message ? error.message : '新建文件夹失败');
});
return;
}
@@ -690,9 +702,9 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
recordFileTreeAction('paste-into', detail);
void pasteSidebarFileTreeClipboard(pasteRow).then(function(ok) {
recordFileTreeActionStatus(ok ? 'applied' : 'skipped', detail);
}).catch(function(error) {
}).catch(async function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '粘贴失败');
await window.mnote.alert(error && error.message ? error.message : '粘贴失败');
});
return;
}
@@ -738,7 +750,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
workspaceId: workspaceId,
documentId: documentId,
title: nextTitle.trim()
}).then(function(){ updateTitleEverywhere(documentId, nextTitle.trim()); });
}).then(async function(){ updateTitleEverywhere(documentId, nextTitle.trim()); });
}
return;
}
@@ -747,12 +759,12 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
return;
}
if (action === 'convert-child') {
convertToPreviousSiblingChild(trigger, detail);
void convertToPreviousSiblingChild(trigger, detail);
return;
}
var deleteTargetId = documentId || String(detail.rowId || '').trim();
if (action === 'delete-trash' && deleteTargetId) {
if (!window.confirm('确定要将“' + title + '”删除到垃圾桶吗?')) return;
if (!(await window.mnote.confirm('确定要将“' + title + '”删除到垃圾桶吗?'))) return;
recordFileTreeAction('delete-trash', detail);
recordFileTreeActionStatus('pending', detail);
void dispatchTreeCommand(trigger || document.body, {
@@ -762,9 +774,9 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
}).then(function() {
recordFileTreeActionStatus('archived', Object.assign({}, detail, { undo: 'trash-modal' }));
if (currentSourceKind() === 'local_folder') void refreshLocalFolderSidebarSnapshot();
}).catch(function(error) {
}).catch(async function(error) {
recordFileTreeActionStatus('failed', Object.assign({}, detail, { fallback: 'alert' }));
window.alert(error && error.message ? error.message : '删除失败');
await window.mnote.alert(error && error.message ? error.message : '删除失败');
});
}
}
@@ -967,7 +979,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
document.head.appendChild(style);
}
// ── 知识库列表获取:只读 MNote 后端 registry/status避免前端直连 WeKnora service key。 ──
// ── 知识库列表获取:只读 MNote 后端 registry/status不直连外部知识库 service key。 ──
function fetchKnowledgeBaseList(detail) {
var rootUri = knowledgeRagRootUri(detail, null);
if (!rootUri) return Promise.resolve([]);
@@ -2125,7 +2137,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
window.dispatchEvent(new CustomEvent('tree:local-command-batch-complete', { detail: { batchId: batchId, action: 'move', failed: failures.length, count: movableRows.length } }));
if (failures.length > 0) {
recordFileTreeActionStatus('failed', { documentId: targetParentId, fallback: 'alert', batchId: batchId });
window.alert('部分对象移动失败:' + failures.join(''));
await window.mnote.alert('部分对象移动失败:' + failures.join(''));
return false;
}
recordFileTreeActionStatus('applied', { documentId: targetParentId, batchId: batchId });
@@ -2159,7 +2171,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
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;
if (!(await window.mnote.confirm(sidebarFileTreeDeleteConfirmText(plan)))) return false;
var batchId = nextFileTreeOperationBatchId('bulk-delete');
document.documentElement.setAttribute('data-mnote-filetree-bulk-delete-batch-id', batchId);
recordFileTreeAction('bulk-delete', { rowId: trigger instanceof HTMLElement ? trigger.getAttribute('data-row-id') || '' : '', count: total, batchId: batchId });
@@ -2264,7 +2276,7 @@ export const createSidebarFileTreeCommandRuntime = (dependencies = {}) => {
if (failures.length > 0) {
window.dispatchEvent(new CustomEvent('tree:local-command-batch-complete', { detail: { batchId: batchId, action: 'bulk-delete', failed: failures.length, count: total } }));
recordFileTreeActionStatus('failed', { count: total, failures: failures.slice(0, 20), fallback: 'alert', batchId: batchId });
window.alert('部分对象删除失败:' + failures.slice(0, 5).join(', ') + (failures.length > 5 ? '…' : ''));
await window.mnote.alert('部分对象删除失败:' + failures.slice(0, 5).join(', ') + (failures.length > 5 ? '…' : ''));
return false;
}
document.documentElement.setAttribute('data-mnote-filetree-bulk-delete-applied', 'true');
@@ -209,13 +209,39 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
|| (/^思维导图/i.test(fileName) && /\.json$/i.test(fileName));
}
function sanitizeWorkspaceRelativePath(rel) {
var value = String(rel || '').trim().replace(/\\/g, '/');
try {
value = decodeURIComponent(value);
} catch (_) {
// keep raw
}
value = value.replace(/\\/g, '/').replace(/^\/+/, '');
if (!value || value.indexOf('\0') >= 0) return '';
var parts = [];
value.split('/').forEach(function (seg) {
if (!seg || seg === '.') return;
if (seg === '..') {
if (parts.length) parts.pop();
return;
}
parts.push(seg);
});
return parts.join('/');
}
function localFilePathFromAssetId(assetId) {
var _rto_ = window.__mnoteResourceOpenRuntime;
if (_rto_ && typeof _rto_.localFilePathFromAssetId === 'function') {
return _rto_.localFilePathFromAssetId(assetId);
return sanitizeWorkspaceRelativePath(_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) : '';
var raw = value.indexOf('local-file:') === 0
? value.slice('local-file:'.length)
: value.indexOf('local:asset:') === 0
? value.slice('local:asset:'.length)
: '';
return sanitizeWorkspaceRelativePath(raw);
}
function workspacePathFromDetail(detail) {
@@ -249,15 +275,16 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
}
function buildLocalFileOpenUrl(relativePath, download) {
var safePath = sanitizeWorkspaceRelativePath(relativePath);
var _rto_ = window.__mnoteResourceOpenRuntime;
if (_rto_ && typeof _rto_.buildLocalFileOpenUrl === 'function') {
return _rto_.buildLocalFileOpenUrl(relativePath, download);
return safePath ? _rto_.buildLocalFileOpenUrl(safePath, download) : '';
}
var rootUri = currentRootUri();
if (!rootUri || !relativePath) return '';
if (!rootUri || !safePath) return '';
var url = new URL('/api/local-folder/files/open', window.location.origin);
url.searchParams.set('rootUri', rootUri);
url.searchParams.set('path', relativePath);
url.searchParams.set('path', safePath);
if (download) url.searchParams.set('download', 'true');
return url.toString();
}
@@ -466,7 +493,7 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
}
var documentId = String(detail && detail.documentId || '').trim();
if (isMindmapAssetDetail(detail) && documentId) {
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'async 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({
@@ -493,7 +520,7 @@ export const createSidebarFileTreeOpenRuntime = (dependencies = {}) => {
return;
}
document.documentElement.setAttribute('data-mnote-media-sign-retired', 'true');
window.alert('旧 Convex Files 附件签名链已退役;local-first 附件请通过本地文件夹资源打开。');
await window.mnote.alert('旧 Convex Files 附件签名链已退役;local-first 附件请通过本地文件夹资源打开。');
}
window.addEventListener('tree.asset.open', function(event) {
@@ -46,7 +46,7 @@ export const createSidebarFileTreeUploadRuntime = (dependencies = {}) => {
});
}
function blockReadonlyFileTreeAction(action, detail, message) {
async 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();
@@ -75,7 +75,7 @@ export const createSidebarFileTreeUploadRuntime = (dependencies = {}) => {
row.setAttribute('data-readonly-message', text);
}
}
window.alert(text);
await window.mnote.alert(text);
return false;
}
@@ -1186,15 +1186,15 @@
var documentId = String(urlDocumentId || activeWorkspacePath.documentId || active.documentId || '').trim();
var fileTreeWorkspacePath = activeFileTreeWorkspacePath(documentId) || {};
var workspacePath = Object.assign({}, activeWorkspacePath, fileTreeWorkspacePath);
var session = piLabState.session || {};
var pagePath = normalizeSlashes(urlPagePath || workspacePath.relativePath || workspacePath.path || active.relativePath || active.path || localMarkdownRelativePathFromDocumentId(documentId) || session.pagePath || '');
// Do not fall back to piLabState.session.* — that re-amplifies stale context after navigation.
var pagePath = normalizeSlashes(urlPagePath || workspacePath.relativePath || workspacePath.path || active.relativePath || active.path || localMarkdownRelativePathFromDocumentId(documentId) || '');
var resourceKind = String(workspacePath.resourceKind || workspacePath.kind || active.resourceKind || active.kind || '').trim().toLowerCase();
var isDirectory = resourceKind === 'directory' || resourceKind === 'folder' || resourceKind === 'workspace';
var isStandalonePiPage = currentUrl().pathname === '/page-ai/pi';
var isLocalMarkdownPage = documentId.indexOf('local-md:') === 0 || /\.md$/i.test(pagePath);
if (isDirectory || (isStandalonePiPage && pagePath && !isLocalMarkdownPage)) pagePath = '';
var rootUri = String(currentRootUri() || workspacePath.rootUri || active.rootUri || session.rootUri || '').trim();
var workspaceId = String(currentWorkspaceId() || workspacePath.workspaceId || active.workspaceId || session.workspaceId || '').trim();
var rootUri = String(currentRootUri() || workspacePath.rootUri || active.rootUri || '').trim();
var workspaceId = String(currentWorkspaceId() || workspacePath.workspaceId || active.workspaceId || '').trim();
var titleNode = document.querySelector('[data-page-title-current="true"]') || document.querySelector('.wolai-breadcrumb-current');
var title = String(active.title || workspacePath.title || (titleNode && titleNode.textContent) || document.title || '').trim();
var selection = currentSelectionText();
@@ -1278,12 +1278,13 @@
var context = currentPageContext();
refreshSelectionSummary();
if (!piLabState.session) piLabState.session = {};
if (context.pagePath) piLabState.session.pagePath = context.pagePath;
if (context.pageTitle) piLabState.session.pageTitle = context.pageTitle;
if (context.rootUri) piLabState.session.rootUri = context.rootUri;
if (context.workspaceId) piLabState.session.workspaceId = context.workspaceId;
// Always sync (including empty) so navigation away from a page clears stale context.
piLabState.session.pagePath = context.pagePath || '';
piLabState.session.pageTitle = context.pageTitle || '';
piLabState.session.rootUri = context.rootUri || '';
piLabState.session.workspaceId = context.workspaceId || '';
var folderPath = currentFolderPathFromPagePath(context.pagePath);
if (folderPath !== null) piLabState.session.folderPath = folderPath;
piLabState.session.folderPath = folderPath === null ? '' : folderPath;
updateContextStrip();
return context;
}
@@ -2294,11 +2295,11 @@
updateConfig();
}
function deleteHistorySession(sessionId) {
async function deleteHistorySession(sessionId) {
if (!sessionId) return Promise.resolve();
var existing = currentHistorySession(sessionId);
var title = existing ? historyTitle(existing) : sessionId;
if (!window.confirm('删除历史对话“' + title + '”?')) return Promise.resolve();
if (!(await window.mnote.confirm('删除历史对话“' + title + '”?'))) return Promise.resolve();
return fetch(API.SESSIONS + '/' + encodeURIComponent(sessionId), {
method: 'DELETE',
credentials: 'same-origin',
@@ -2313,10 +2314,10 @@
});
}
function clearHistorySessions() {
async function clearHistorySessions() {
var count = Array.isArray(piLabState.history) ? piLabState.history.length : 0;
if (!count) return Promise.resolve();
if (!window.confirm('清空全部 ' + count + ' 条 Pi 历史对话?')) return Promise.resolve();
if (!(await window.mnote.confirm('清空全部 ' + count + ' 条 Pi 历史对话?'))) return Promise.resolve();
return fetch(API.SESSIONS + '?limit=1000', {
method: 'DELETE',
credentials: 'same-origin',
@@ -2618,8 +2619,10 @@
if (lightrag) lightrag.textContent = '唯一默认';
if (changedFiles) {
var files = {};
piLabState.changedFiles.forEach(function (file) { if (file) files[file] = true; });
piLabState.messages.forEach(function (msg) {
(Array.isArray(piLabState.changedFiles) ? piLabState.changedFiles : []).forEach(function (file) {
if (file) files[file] = true;
});
(Array.isArray(piLabState.messages) ? piLabState.messages : []).forEach(function (msg) {
if (msg && msg.diffSummary && Array.isArray(msg.diffSummary.files)) {
msg.diffSummary.files.forEach(function (file) { if (file) files[file] = true; });
}
@@ -3264,7 +3267,7 @@
function collectPiArtifacts() {
var artifacts = [];
var seen = {};
piLabState.messages.forEach(function (msg) {
(Array.isArray(piLabState.messages) ? piLabState.messages : []).forEach(function (msg) {
if (!msg) return;
(Array.isArray(msg.citations) ? msg.citations : []).forEach(function (cit, index) {
var key = 'citation:' + (cit.url || cit.source || cit.title || index);
@@ -3307,7 +3310,7 @@
});
});
});
piLabState.changedFiles.forEach(function (file) {
(Array.isArray(piLabState.changedFiles) ? piLabState.changedFiles : []).forEach(function (file) {
if (!file || seen['diff:' + file]) return;
seen['diff:' + file] = true;
artifacts.push({ kind: 'diff', title: file, summary: '文件变更' });
@@ -3722,8 +3725,10 @@
function handleToolCallEventPayload(payload) {
payload = payload || {};
// Fail-closed: only explicit allowed===true triggers side effects / changedFiles.
var allowed = payload.allowed === true;
applyToolCallSideEffects({
ok: payload.allowed !== false,
ok: allowed,
toolName: payload.toolName,
result: {
rootUri: payload.rootUri,
@@ -3733,7 +3738,7 @@
});
piLabState.receipts.push({
toolName: payload.toolName,
allowed: payload.allowed !== false,
allowed: allowed,
denyReason: payload.denyReason || null,
diffSummary: payload.diffSummary || null,
citationCount: payload.citationCount || 0,
@@ -3746,9 +3751,9 @@
toolCallId: payload.toolCallId || payload.id || ('receipt:' + (payload.toolName || 'tool') + ':' + piLabState.receipts.length),
toolName: payload.toolName || 'tool',
args: payload.params || payload.args || {},
}, payload.allowed === false ? 'denied' : 'done', {
}, allowed ? 'done' : 'denied', {
result: {
allowed: payload.allowed !== false,
allowed: allowed,
denyReason: payload.denyReason || null,
diffSummary: payload.diffSummary || null,
citationCount: payload.citationCount || 0,
@@ -3757,7 +3762,9 @@
approvalConfirmed: payload.approvalConfirmed === true,
toolPolicy: payload.toolPolicy || '',
});
if (payload.normalizedFilePath && payload.diffSummary) piLabState.changedFiles.push(payload.normalizedFilePath);
if (allowed && payload.normalizedFilePath && payload.diffSummary) {
piLabState.changedFiles.push(payload.normalizedFilePath);
}
updateReceiptDisplay();
updateContextStrip();
}
@@ -4591,7 +4598,8 @@
function parseUnifiedPatch(patchText) {
if (!patchText) return [];
var files = [];
var fileLines = patchText.split('\\n');
// Real newlines (JSON-parsed patch text), not the two-char sequence \ + n.
var fileLines = String(patchText).replace(/\r\n/g, '\n').split('\n');
var currentFile = null;
var currentHunk = null;
@@ -4982,8 +4990,8 @@
}
var copyMarkdown = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-copy-markdown]') : null;
if (copyMarkdown) {
copyTextToClipboard(copyMarkdown.getAttribute('data-page-ai-pi-lab-copy-markdown') || '').then(function () {
showPiToast('Markdown copied', 'success');
copyTextToClipboard(copyMarkdown.getAttribute('data-page-ai-pi-lab-copy-markdown') || '').then(function (ok) {
showPiToast(ok ? 'Markdown copied' : '复制失败(剪贴板不可用)', ok ? 'success' : 'warning');
});
return;
}
@@ -4999,7 +5007,11 @@
}
var openArtifact = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-artifact-open]') : null;
if (openArtifact) {
var artUrl = openArtifact.getAttribute('data-page-ai-pi-lab-artifact-open') || openArtifact.getAttribute('href') || '';
var artUrl = String(openArtifact.getAttribute('data-page-ai-pi-lab-artifact-open') || openArtifact.getAttribute('href') || '').trim();
if (!artUrl || artUrl === '#') {
showPiToast('引用无效或无法访问', 'warning');
return;
}
window.dispatchEvent(new CustomEvent('mnote:open-reference', {
detail: {
source: 'page_ai_pi_lab_artifact',
@@ -5007,9 +5019,7 @@
url: artUrl,
},
}));
if (!artUrl || artUrl === '#') {
showPiToast('引用无效或无法访问', 'warning');
}
return;
}
var viewDiff = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-artifact-view-diff]') : null;
if (viewDiff) {
@@ -5020,15 +5030,17 @@
}
var copyArtifactMarkdown = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-artifact-copy-markdown]') : null;
if (copyArtifactMarkdown) {
copyTextToClipboard(copyArtifactMarkdown.getAttribute('data-page-ai-pi-lab-artifact-copy-markdown') || '').then(function () {
showPiToast('Artifact markdown copied', 'success');
copyTextToClipboard(copyArtifactMarkdown.getAttribute('data-page-ai-pi-lab-artifact-copy-markdown') || '').then(function (ok) {
showPiToast(ok ? 'Artifact markdown copied' : '复制失败(剪贴板不可用)', ok ? 'success' : 'warning');
});
return;
}
var copyArtifact = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-artifact-copy]') : null;
if (copyArtifact) {
var raw = copyArtifact.getAttribute('data-page-ai-pi-lab-artifact-copy') || '';
copyTextToClipboard(raw).then(function () { showPiToast('Artifact raw copied', 'success'); });
copyTextToClipboard(raw).then(function (ok) {
showPiToast(ok ? 'Artifact raw copied' : '复制失败(剪贴板不可用)', ok ? 'success' : 'warning');
});
return;
}
var deleteTarget = event.target && event.target.closest ? event.target.closest('[data-page-ai-pi-lab-history-delete]') : null;
@@ -14,8 +14,21 @@ export function createSidebarPageAiRuntime(context) {
function openPageAiDrawer() {
// 产品唯一入口:Pi Lab。旧 Hermes/OpenCode/Board drawer 不再打开。
if (window.createSidebarPageAiPiLabRuntime) {
if (typeof window.createSidebarPageAiPiLabRuntime !== 'function') {
console.warn('[page-ai] Pi Lab runtime not loaded');
if (window.mnote && typeof window.mnote.toast === 'function') {
window.mnote.toast('Page AI 运行时未加载', { kind: 'warning' });
}
return;
}
try {
window.createSidebarPageAiPiLabRuntime({});
} catch (err) {
console.error('[page-ai] Pi Lab runtime init failed', err);
if (window.mnote && typeof window.mnote.toast === 'function') {
window.mnote.toast('Page AI 启动失败', { kind: 'error' });
}
return;
}
window.postMessage({ source: 'mnote-sidebar', type: 'mnote:pi-lab-show' }, window.location.origin);
if (pageUiState) pageUiState.pageAiOpen = true;
@@ -691,49 +691,49 @@ export function createSidebarPageSettingsRuntime(context) {
function createKnowledgeRagSettingsPanelHtmlV2() {
return '' +
'<div class="wolai-page-settings-panel mnote-settings-panel mnote-knowledge-rag-settings-panel mnote-weknora-kb-page" role="dialog" aria-modal="false" aria-label="LightRAG 知识库" data-testid="mnote-weknora-knowledge-settings-panel" data-knowledge-rag-default-provider="lightrag" data-knowledge-rag-legacy-fallback-provider="lightrag" data-knowledge-rag-source-registry="mnote" data-knowledge-rag-provider-index="lightrag-source-registry" data-mnote-weknora-page-replacement="false">' +
'<div class="mnote-weknora-kb-page-header">' +
'<div class="mnote-weknora-kb-page-title">' +
'<span class="mnote-weknora-kb-page-eyebrow">LightRAG</span>' +
'<div class="wolai-page-settings-panel mnote-settings-panel mnote-knowledge-rag-settings-panel mnote-krag-kb-page" role="dialog" aria-modal="false" aria-label="LightRAG 知识库" data-testid="mnote-krag-knowledge-settings-panel" data-knowledge-rag-default-provider="lightrag" data-knowledge-rag-legacy-fallback-provider="lightrag" data-knowledge-rag-source-registry="mnote" data-knowledge-rag-provider-index="lightrag-source-registry" data-mnote-krag-page-replacement="false">' +
'<div class="mnote-krag-kb-page-header">' +
'<div class="mnote-krag-kb-page-title">' +
'<span class="mnote-krag-kb-page-eyebrow">LightRAG</span>' +
'<strong>知识库</strong>' +
'<span>资料、索引状态和检索</span>' +
'</div>' +
'<div class="mnote-weknora-kb-page-actions">' +
'<div class="mnote-krag-kb-page-actions">' +
'<button type="button" data-knowledge-rag-action="refresh" title="刷新"><span class="material-symbols-outlined" data-icon="refresh" aria-hidden="true"></span></button>' +
'<button type="button" data-knowledge-rag-action="open-dashboard" title="打开知识库" aria-label="打开知识库"><span class="material-symbols-outlined" data-icon="open_in_new" aria-hidden="true"></span></button>' +
'<button type="button" class="mnote-settings-panel-close" data-settings-action="close" aria-label="关闭知识库"><span class="material-symbols-outlined" data-icon="close" aria-hidden="true"></span></button>' +
'</div>' +
'</div>' +
'<div class="mnote-weknora-kb-page-status" data-testid="mnote-knowledge-rag-status"></div>' +
'<div class="mnote-weknora-kb-page-body" data-kb-list-hidden="true">' +
'<aside class="mnote-weknora-kb-list-pane" data-testid="mnote-weknora-kb-list-pane" hidden>' +
'<div class="mnote-weknora-kb-list-head">' +
'<div><strong>资料库</strong><span data-testid="mnote-weknora-kb-list-count">0</span></div>' +
'<div class="mnote-krag-kb-page-status" data-testid="mnote-knowledge-rag-status"></div>' +
'<div class="mnote-krag-kb-page-body" data-kb-list-hidden="true">' +
'<aside class="mnote-krag-kb-list-pane" data-testid="mnote-krag-kb-list-pane" hidden>' +
'<div class="mnote-krag-kb-list-head">' +
'<div><strong>资料库</strong><span data-testid="mnote-krag-kb-list-count">0</span></div>' +
'<button type="button" data-knowledge-rag-action="toggle-create-kb" title="新建资料库"><span class="material-symbols-outlined" data-icon="create_new_folder" aria-hidden="true"></span></button>' +
'</div>' +
'<div class="mnote-weknora-kb-card-wrap" data-testid="mnote-weknora-kb-card-list"></div>' +
'<div class="mnote-krag-kb-card-wrap" data-testid="mnote-krag-kb-card-list"></div>' +
'</aside>' +
'<section class="mnote-weknora-kb-detail-pane" data-testid="mnote-weknora-kb-detail-pane">' +
'<div class="mnote-weknora-kb-detail-hero" data-testid="mnote-weknora-kb-detail-hero">' +
'<section class="mnote-krag-kb-detail-pane" data-testid="mnote-krag-kb-detail-pane">' +
'<div class="mnote-krag-kb-detail-hero" data-testid="mnote-krag-kb-detail-hero">' +
'<div>' +
'<nav class="mnote-weknora-kb-breadcrumb" data-testid="mnote-weknora-kb-breadcrumb" aria-label="知识库面包屑"><span>知识库</span><span>/</span><strong data-testid="mnote-weknora-kb-breadcrumb-current">LightRAG 资料库</strong></nav>' +
'<span class="mnote-weknora-kb-type-pill"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span>资料来源</span>' +
'<h2 data-testid="mnote-weknora-kb-detail-title">LightRAG 资料库</h2>' +
'<p data-testid="mnote-weknora-kb-detail-description">MNote 管理本地来源、引用定位和权限边界,LightRAG 负责索引与召回。</p>' +
'<nav class="mnote-krag-kb-breadcrumb" data-testid="mnote-krag-kb-breadcrumb" aria-label="知识库面包屑"><span>知识库</span><span>/</span><strong data-testid="mnote-krag-kb-breadcrumb-current">LightRAG 资料库</strong></nav>' +
'<span class="mnote-krag-kb-type-pill"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span>资料来源</span>' +
'<h2 data-testid="mnote-krag-kb-detail-title">LightRAG 资料库</h2>' +
'<p data-testid="mnote-krag-kb-detail-description">MNote 管理本地来源、引用定位和权限边界,LightRAG 负责索引与召回。</p>' +
'</div>' +
'</div>' +
'<section class="mnote-knowledge-rag-kb-manager" data-testid="mnote-knowledge-rag-kb-manager">' +
'<div class="mnote-knowledge-rag-kb-manager-row">' +
'<label class="mnote-knowledge-rag-kb-select-field"><span>当前资料库</span><select class="mnote-kb-params-select" data-testid="mnote-weknora-kb-select" data-knowledge-rag-kb-select="true"><option value="">加载中...</option></select></label>' +
'<label class="mnote-knowledge-rag-kb-select-field"><span>当前资料库</span><select class="mnote-kb-params-select" data-testid="mnote-krag-kb-select" data-knowledge-rag-kb-select="true"><option value="">加载中...</option></select></label>' +
'<button type="button" data-knowledge-rag-action="toggle-create-kb"><span class="material-symbols-outlined" data-icon="folder_add" aria-hidden="true"></span>新建</button>' +
'<button type="button" data-knowledge-rag-action="delete-kb" data-knowledge-rag-delete-selected-kb="true"><span class="material-symbols-outlined" data-icon="delete" aria-hidden="true"></span>删除</button>' +
'</div>' +
'<section class="mnote-weknora-kb-create-card" data-testid="mnote-weknora-kb-controls" hidden>' +
'<div class="mnote-weknora-kb-create-title"><span class="material-symbols-outlined" data-icon="folder_add" aria-hidden="true"></span><strong>新建资料库</strong></div>' +
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-weknora-create-kb-name" data-knowledge-rag-create-kb-name="true" placeholder="知识库名称" spellcheck="false" />' +
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-weknora-create-kb-description" data-knowledge-rag-create-kb-description="true" placeholder="描述,可留空" spellcheck="false" />' +
'<section class="mnote-krag-kb-create-card" data-testid="mnote-krag-kb-controls" hidden>' +
'<div class="mnote-krag-kb-create-title"><span class="material-symbols-outlined" data-icon="folder_add" aria-hidden="true"></span><strong>新建资料库</strong></div>' +
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-krag-create-kb-name" data-knowledge-rag-create-kb-name="true" placeholder="知识库名称" spellcheck="false" />' +
'<input type="text" class="wolai-page-settings-index-path" data-testid="mnote-krag-create-kb-description" data-knowledge-rag-create-kb-description="true" placeholder="描述,可留空" spellcheck="false" />' +
'<button type="button" class="wolai-page-settings-index-add" data-knowledge-rag-action="create-kb">创建知识库</button>' +
'<span class="mnote-knowledge-rag-input-status" data-testid="mnote-weknora-create-kb-status" data-knowledge-rag-create-kb-status hidden></span>' +
'<span class="mnote-knowledge-rag-input-status" data-testid="mnote-krag-create-kb-status" data-knowledge-rag-create-kb-status hidden></span>' +
'</section>' +
'<div class="mnote-knowledge-rag-kb-manager-note" data-testid="mnote-knowledge-rag-kb-manager-note"></div>' +
'</section>' +
@@ -742,27 +742,27 @@ export function createSidebarPageSettingsRuntime(context) {
'<div class="mnote-knowledge-rag-meta" data-testid="mnote-knowledge-rag-meta"></div>' +
'</details>' +
'<div class="mnote-knowledge-rag-detail-tabs" role="tablist" aria-label="当前资料库内容">' +
'<button type="button" class="mnote-knowledge-rag-detail-tab is-active" role="tab" aria-selected="true" data-kb-rag-tab="sources" data-testid="mnote-weknora-tab-documents">资料</button>' +
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="wiki" data-testid="mnote-weknora-tab-wiki" hidden>Wiki</button>' +
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="graph" data-testid="mnote-weknora-tab-graph" hidden>Graph</button>' +
'<button type="button" class="mnote-knowledge-rag-detail-tab is-active" role="tab" aria-selected="true" data-kb-rag-tab="sources" data-testid="mnote-krag-tab-documents">资料</button>' +
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="wiki" data-testid="mnote-krag-tab-wiki" hidden>Wiki</button>' +
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="graph" data-testid="mnote-krag-tab-graph" hidden>Graph</button>' +
'<button type="button" class="mnote-knowledge-rag-detail-tab" role="tab" aria-selected="false" data-kb-rag-tab="search">检索</button>' +
'</div>' +
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="sources">' +
'<div class="mnote-weknora-doc-layout" data-testid="mnote-weknora-document-management">' +
'<aside class="mnote-weknora-doc-sidebar" data-testid="mnote-weknora-doc-source-sidebar" hidden>' +
'<div class="mnote-weknora-doc-sidebar-head"><strong>来源</strong><span>MNote registry</span></div>' +
'<div class="mnote-weknora-doc-source-tags" data-testid="mnote-weknora-doc-source-tags">' +
'<div class="mnote-krag-doc-layout" data-testid="mnote-krag-document-management">' +
'<aside class="mnote-krag-doc-sidebar" data-testid="mnote-krag-doc-source-sidebar" hidden>' +
'<div class="mnote-krag-doc-sidebar-head"><strong>来源</strong><span>MNote registry</span></div>' +
'<div class="mnote-krag-doc-source-tags" data-testid="mnote-krag-doc-source-tags">' +
'<button type="button" data-knowledge-rag-action="filter-sources" data-knowledge-rag-filter="all" data-active="true"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span>全部资料</button>' +
'<button type="button" data-knowledge-rag-action="filter-sources" data-knowledge-rag-filter="indexed"><span class="material-symbols-outlined" data-icon="check_circle" aria-hidden="true"></span>已索引</button>' +
'<button type="button" data-knowledge-rag-action="filter-sources" data-knowledge-rag-filter="indexing"><span class="material-symbols-outlined" data-icon="sync" aria-hidden="true"></span>处理中</button>' +
'</div>' +
'<div class="mnote-weknora-doc-sidebar-head"><strong>标签</strong><span>MNote views</span></div>' +
'<div class="mnote-weknora-doc-tags" data-testid="mnote-weknora-doc-tags"><span>PDF</span><span>Markdown</span><span>Folder</span><span>Citation</span></div>' +
'<div class="mnote-krag-doc-sidebar-head"><strong>标签</strong><span>MNote views</span></div>' +
'<div class="mnote-krag-doc-tags" data-testid="mnote-krag-doc-tags"><span>PDF</span><span>Markdown</span><span>Folder</span><span>Citation</span></div>' +
'</aside>' +
'<section class="mnote-weknora-doc-main">' +
'<div class="mnote-weknora-doc-toolbar">' +
'<div class="mnote-weknora-doc-search"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span><input type="text" data-testid="mnote-weknora-doc-filter-input" data-knowledge-rag-doc-filter-input="true" placeholder="搜索或筛选当前资料库内容" spellcheck="false" /></div>' +
'<div class="mnote-weknora-view-switch" role="group" aria-label="文档视图切换" data-testid="mnote-weknora-view-switch">' +
'<section class="mnote-krag-doc-main">' +
'<div class="mnote-krag-doc-toolbar">' +
'<div class="mnote-krag-doc-search"><span class="material-symbols-outlined" data-icon="search" aria-hidden="true"></span><input type="text" data-testid="mnote-krag-doc-filter-input" data-knowledge-rag-doc-filter-input="true" placeholder="搜索或筛选当前资料库内容" spellcheck="false" /></div>' +
'<div class="mnote-krag-view-switch" role="group" aria-label="文档视图切换" data-testid="mnote-krag-view-switch">' +
'<button type="button" class="is-active" data-knowledge-rag-view="list" aria-pressed="true" title="列表视图"><span class="material-symbols-outlined" data-icon="view_list" aria-hidden="true"></span></button>' +
'<button type="button" data-knowledge-rag-view="grid" aria-pressed="false" title="网格视图"><span class="material-symbols-outlined" data-icon="grid_view" aria-hidden="true"></span></button>' +
'</div>' +
@@ -774,24 +774,24 @@ export function createSidebarPageSettingsRuntime(context) {
'<div class="mnote-knowledge-rag-source-inputs" data-testid="mnote-knowledge-rag-source-inputs">' +
knowledgeRagSourceInputRow('') +
'</div>' +
'<div class="mnote-weknora-doc-actions">' +
'<div class="mnote-krag-doc-actions">' +
'<button type="button" class="mnote-knowledge-rag-primary-action" data-knowledge-rag-action="ingest">开始索引</button>' +
'<button type="button" data-knowledge-rag-action="prune">清除失效记录</button>' +
'</div>' +
'</div>' +
'<div class="mnote-knowledge-rag-source-filters" data-testid="mnote-knowledge-rag-source-filters"></div>' +
'<div class="mnote-weknora-doc-table" data-testid="mnote-knowledge-rag-sources" data-weknora-document-list-view="true"></div>' +
'<div class="mnote-krag-doc-table" data-testid="mnote-knowledge-rag-sources" data-krag-document-list-view="true"></div>' +
'</section>' +
'</div>' +
'</div>' +
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="wiki" hidden>' +
'<div class="mnote-weknora-placeholder-panel" data-testid="mnote-weknora-wiki-panel">' +
'<div class="mnote-krag-placeholder-panel" data-testid="mnote-krag-wiki-panel">' +
'<strong>Wiki</strong>' +
'<span>后续可基于 LightRAG 召回结果生成知识页;当前以 MNote open-reference 边界为准。</span>' +
'</div>' +
'</div>' +
'<div class="mnote-knowledge-rag-detail-panel" data-kb-rag-panel="graph" hidden>' +
'<div class="mnote-weknora-placeholder-panel" data-testid="mnote-weknora-graph-panel">' +
'<div class="mnote-krag-placeholder-panel" data-testid="mnote-krag-graph-panel">' +
'<strong>Graph</strong>' +
'<span>展示 LightRAG 图谱和文档关系入口,本地文件真相仍由 MNote source registry 管理。</span>' +
'</div>' +
@@ -1255,9 +1255,7 @@ export function createSidebarPageSettingsRuntime(context) {
function knowledgeRagProviderLabel(status) {
var providerName = knowledgeRagProviderName(status);
if (providerName === 'weknora') return 'WeKnora';
if (providerName === 'lightrag') return 'LightRAG';
if (providerName === 'lightrag_legacy') return 'LightRAG';
if (providerName === 'lightrag' || providerName === 'lightrag_legacy') return 'LightRAG';
return providerName;
}
@@ -1371,10 +1369,8 @@ export function createSidebarPageSettingsRuntime(context) {
}
function knowledgeRagProviderKnowledgeBaseIds(status) {
var config = status && status.providerConfig && typeof status.providerConfig === 'object' ? status.providerConfig : {};
var weknora = config.weknora && typeof config.weknora === 'object' ? config.weknora : {};
var ids = [];
var raw = weknora.knowledgeBaseIds || weknora.knowledge_base_ids || status && status.knowledgeBaseIds || [];
var raw = status && status.knowledgeBaseIds || [];
if (!Array.isArray(raw) && raw) raw = [raw];
(Array.isArray(raw) ? raw : []).forEach(function(id) {
var text = String(id || '').trim();
@@ -1542,10 +1538,10 @@ export function createSidebarPageSettingsRuntime(context) {
}
function renderKnowledgeRagKnowledgeBaseCards(popover, status, entries, providerDocs) {
var listNode = popover.querySelector('[data-testid="mnote-weknora-kb-card-list"]');
var countNode = popover.querySelector('[data-testid="mnote-weknora-kb-list-count"]');
var listPane = popover.querySelector('[data-testid="mnote-weknora-kb-list-pane"]');
var pageBody = popover.querySelector('.mnote-weknora-kb-page-body');
var listNode = popover.querySelector('[data-testid="mnote-krag-kb-card-list"]');
var countNode = popover.querySelector('[data-testid="mnote-krag-kb-list-count"]');
var listPane = popover.querySelector('[data-testid="mnote-krag-kb-list-pane"]');
var pageBody = popover.querySelector('.mnote-krag-kb-page-body');
if (!(listNode instanceof HTMLElement)) return;
var bases = knowledgeRagKnowledgeBases(status || {});
var selected = selectedKnowledgeRagProviderKnowledgeBaseId(status || {});
@@ -1563,9 +1559,9 @@ export function createSidebarPageSettingsRuntime(context) {
var baseStatus = String(base.status || 'active');
var baseStatusLabel = knowledgeRagBaseStatusLabel(baseStatus);
return '' +
'<button type="button" class="mnote-weknora-kb-card' + (isActive ? ' is-active' : '') + '" data-knowledge-rag-action="select-kb" data-knowledge-rag-kb-id="' + escapeHtml(base.providerKbId) + '" data-knowledge-rag-kb-provider="' + escapeHtml(providerName) + '" data-knowledge-rag-processing-count="' + escapeHtml(String(stats.processingCount)) + '">' +
'<span class="mnote-weknora-kb-card-star"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span></span>' +
'<span class="mnote-weknora-kb-card-main">' +
'<button type="button" class="mnote-krag-kb-card' + (isActive ? ' is-active' : '') + '" data-knowledge-rag-action="select-kb" data-knowledge-rag-kb-id="' + escapeHtml(base.providerKbId) + '" data-knowledge-rag-kb-provider="' + escapeHtml(providerName) + '" data-knowledge-rag-processing-count="' + escapeHtml(String(stats.processingCount)) + '">' +
'<span class="mnote-krag-kb-card-star"><span class="material-symbols-outlined" data-icon="folder" aria-hidden="true"></span></span>' +
'<span class="mnote-krag-kb-card-main">' +
'<strong title="' + escapeHtml(base.name) + '">' + escapeHtml(base.name) + '</strong>' +
'<em title="' + escapeHtml(base.description || '暂无描述') + '">' + escapeHtml(base.description || '暂无描述') + '</em>' +
'<span>' +
@@ -1574,16 +1570,16 @@ export function createSidebarPageSettingsRuntime(context) {
(stats.processingCount ? '<i class="is-processing"><span class="material-symbols-outlined" data-icon="sync" aria-hidden="true"></span>' + escapeHtml(String(stats.processingCount)) + '</i>' : '') +
'</span>' +
'</span>' +
'<span class="mnote-weknora-kb-card-status" data-status="' + escapeHtml(baseStatus) + '">' + escapeHtml(baseStatusLabel) + '</span>' +
'<span class="mnote-krag-kb-card-status" data-status="' + escapeHtml(baseStatus) + '">' + escapeHtml(baseStatusLabel) + '</span>' +
'</button>';
}).join('');
}
function renderKnowledgeRagKnowledgeBaseHero(popover, status, entries, providerDocs) {
var heroNode = popover.querySelector('[data-testid="mnote-weknora-kb-detail-hero"]');
var titleNode = popover.querySelector('[data-testid="mnote-weknora-kb-detail-title"]');
var descNode = popover.querySelector('[data-testid="mnote-weknora-kb-detail-description"]');
var breadcrumbNode = popover.querySelector('[data-testid="mnote-weknora-kb-breadcrumb-current"]');
var heroNode = popover.querySelector('[data-testid="mnote-krag-kb-detail-hero"]');
var titleNode = popover.querySelector('[data-testid="mnote-krag-kb-detail-title"]');
var descNode = popover.querySelector('[data-testid="mnote-krag-kb-detail-description"]');
var breadcrumbNode = popover.querySelector('[data-testid="mnote-krag-kb-breadcrumb-current"]');
if (!(titleNode instanceof HTMLElement) || !(descNode instanceof HTMLElement)) return;
var bases = knowledgeRagKnowledgeBases(status || {});
var selected = selectedKnowledgeRagProviderKnowledgeBaseId(status || {});
@@ -1646,14 +1642,11 @@ export function createSidebarPageSettingsRuntime(context) {
deleteButton.title = canDelete ? '删除当前空资料库' : (selectedValue === 'lightrag-default' ? '默认资料库不能删除' : '请先移除资料后再删除资料库');
}
if (noteNode instanceof HTMLElement) {
var providerName = knowledgeRagProviderName(status || {});
var activeName = activeBase.name || '当前资料库';
if (selectedValue === 'lightrag-default') {
noteNode.textContent = '默认资料库显示当前工作区全部已索引资料;新建资料库可管理独立资料范围。';
} else if (activeStats.sourceCount === 0 && activeStats.processingCount === 0) {
noteNode.textContent = activeName + ':空资料库,可添加资料或直接删除。';
} else if (providerName === 'weknora') {
noteNode.textContent = bases.length > 1 ? '已连接多个资料库,可用下拉框切换当前范围。' : '当前只有一个资料库,可新建后切换。';
} else {
noteNode.textContent = bases.length > 1 ? '当前可切换资料库;删除前需要先移除其中资料。' : '当前使用默认资料库,也可以新建一个资料库管理独立范围。';
}
@@ -1674,7 +1667,7 @@ export function createSidebarPageSettingsRuntime(context) {
var activeCount = Object.keys(activeKbIds).length || kbIds.length;
var processingCount = knowledgeRagInFlightCount(entries, providerDocs);
return '' +
'<div class="mnote-weknora-kb-summary-card" data-testid="mnote-weknora-kb-summary" data-knowledge-rag-kb-id="' + escapeHtml(kbIds[0] || '') + '" data-knowledge-rag-kb-ids="' + escapeHtml(kbIds.join(',')) + '" data-knowledge-rag-kb-provider="' + escapeHtml(knowledgeRagProviderName(status || {})) + '" data-knowledge-rag-kb-status="configured">' +
'<div class="mnote-krag-kb-summary-card" data-testid="mnote-krag-kb-summary" data-knowledge-rag-kb-id="' + escapeHtml(kbIds[0] || '') + '" data-knowledge-rag-kb-ids="' + escapeHtml(kbIds.join(',')) + '" data-knowledge-rag-kb-provider="' + escapeHtml(knowledgeRagProviderName(status || {})) + '" data-knowledge-rag-kb-status="configured">' +
'<div><span>知识库</span><strong>' + escapeHtml(activeCount ? String(activeCount) + ' 个' : '未配置') + '</strong></div>' +
'<div><span>文档</span><strong>' + escapeHtml(String((entries || []).length)) + '</strong></div>' +
'<div><span>索引文档</span><strong>' + escapeHtml(String((providerDocs || []).length)) + '</strong></div>' +
@@ -1928,7 +1921,7 @@ export function createSidebarPageSettingsRuntime(context) {
function renderKnowledgeRagSourceFilters(popover, rows) {
var filtersNode = popover.querySelector('[data-testid="mnote-knowledge-rag-source-filters"]');
var sourceTagsNode = popover.querySelector('[data-testid="mnote-weknora-doc-source-tags"]');
var sourceTagsNode = popover.querySelector('[data-testid="mnote-krag-doc-source-tags"]');
if (!(filtersNode instanceof HTMLElement)) return;
var counts = rows.reduce(function(acc, row) {
acc.all += 1;
@@ -1997,7 +1990,7 @@ export function createSidebarPageSettingsRuntime(context) {
if (filePath) providerDocsByPath.set(filePath, doc);
});
var listView = String(pageUiState.knowledgeRagDocumentView || 'list') === 'grid' ? 'grid' : 'list';
sourcesNode.setAttribute('data-weknora-document-view', listView);
sourcesNode.setAttribute('data-krag-document-view', listView);
sourcesNode.innerHTML = items.map(function(row) {
if (row.kind === 'provider') {
var doc = row.doc || {};
@@ -2022,7 +2015,7 @@ export function createSidebarPageSettingsRuntime(context) {
'<div class="mnote-knowledge-rag-source-main">' +
'<strong title="' + escapeHtml(providerTitle) + '">' + escapeHtml(providerTitle) + '</strong>' +
'<span>' + escapeHtml(providerMeta) + '</span>' +
'<span class="mnote-weknora-source-aux"><i>' + escapeHtml(knowledgeId || '索引记录') + '</i><i>' + escapeHtml(providerPath ? '引用可回跳' : '无法直接定位') + '</i></span>' +
'<span class="mnote-krag-source-aux"><i>' + escapeHtml(knowledgeId || '索引记录') + '</i><i>' + escapeHtml(providerPath ? '引用可回跳' : '无法直接定位') + '</i></span>' +
knowledgeRagSourceProgressHtml(providerProgress) +
'</div>' +
'<div class="mnote-knowledge-rag-source-actions"></div>' +
@@ -2075,7 +2068,7 @@ export function createSidebarPageSettingsRuntime(context) {
'<div class="mnote-knowledge-rag-source-main">' +
'<strong title="' + escapeHtml(title) + '">' + escapeHtml(title || '未命名资料') + '</strong>' +
'<span>' + escapeHtml(meta) + '</span>' +
'<span class="mnote-weknora-source-aux"><i>' + escapeHtml(kbId || '未绑定 KB') + '</i><i>' + escapeHtml(knowledgeId || '待生成 citation') + '</i></span>' +
'<span class="mnote-krag-source-aux"><i>' + escapeHtml(kbId || '未绑定 KB') + '</i><i>' + escapeHtml(knowledgeId || '待生成 citation') + '</i></span>' +
knowledgeRagSourceProgressHtml(entryProgress) +
'</div>' +
'<div class="mnote-knowledge-rag-source-actions">' +
@@ -2191,12 +2184,8 @@ export function createSidebarPageSettingsRuntime(context) {
function publicKnowledgeRagDashboardUrl(status) {
var config = status && status.providerConfig && typeof status.providerConfig === 'object' ? status.providerConfig : {};
var active = knowledgeRagProviderName(status || {});
var weknora = config.weknora && typeof config.weknora === 'object' ? config.weknora : {};
var lightrag = config.lightrag && typeof config.lightrag === 'object' ? config.lightrag : {};
var url = active === 'weknora'
? String((weknora && weknora.dashboardUrl) || (status && (status.dashboardUrl || status.endpoint)) || '').trim()
: String((status && (status.dashboardUrl || status.endpoint)) || (lightrag && lightrag.dashboardUrl) || '').trim();
var url = String((status && (status.dashboardUrl || status.endpoint)) || (lightrag && lightrag.dashboardUrl) || '').trim();
if (!url) return '';
try {
var parsed = new URL(url, window.location.href);
@@ -2360,7 +2349,7 @@ export function createSidebarPageSettingsRuntime(context) {
function setKnowledgeRagCreateCardVisible(visible) {
var popover = ensureKnowledgeRagSettingsPopover();
var card = popover.querySelector('[data-testid="mnote-weknora-kb-controls"]');
var card = popover.querySelector('[data-testid="mnote-krag-kb-controls"]');
var statusNode = popover.querySelector('[data-knowledge-rag-create-kb-status]');
if (card instanceof HTMLElement) card.hidden = !visible;
if (statusNode instanceof HTMLElement && visible) statusNode.hidden = true;
@@ -2372,7 +2361,7 @@ export function createSidebarPageSettingsRuntime(context) {
function toggleKnowledgeRagCreateCard() {
var popover = ensureKnowledgeRagSettingsPopover();
var card = popover.querySelector('[data-testid="mnote-weknora-kb-controls"]');
var card = popover.querySelector('[data-testid="mnote-krag-kb-controls"]');
setKnowledgeRagCreateCardVisible(!(card instanceof HTMLElement) || card.hidden);
}
@@ -2724,7 +2713,10 @@ export function createSidebarPageSettingsRuntime(context) {
}
// --- 检索测试 ---
function knowledgeRagSearchResultSourcePath(result) {
// depth 限制防止恶意/循环 source 对象导致栈溢出
function knowledgeRagSearchResultSourcePath(result, depth) {
if (depth == null) depth = 0;
if (depth > 4) return '';
var candidates = [
result && result.sourceRootRelativePath,
result && result.source_root_relative_path,
@@ -2740,7 +2732,7 @@ export function createSidebarPageSettingsRuntime(context) {
}
var source = result && result.source;
if (source && typeof source === 'object') {
return knowledgeRagSearchResultSourcePath(source);
return knowledgeRagSearchResultSourcePath(source, depth + 1);
}
if (typeof source === 'string' && source.trim()) return source.trim();
return '';
@@ -2100,7 +2100,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
return await runtimeFn(files, detail || {}, options || {}, {
resolveFileTreeUploadTarget: resolveFileTreeUploadTarget,
uploadFileToMediaAsset: uploadFileToMediaAsset,
alert: function(message) { window.alert(message); }
alert: async function(message) { await window.mnote.alert(message); }
});
}
var list = Array.from(files || []).filter(Boolean);
@@ -2116,7 +2116,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
}
}
if (errors.length) {
window.alert('部分文件上传失败:\n' + errors.slice(0, 6).join('\n') + (errors.length > 6 ? '\n...' : ''));
await window.mnote.alert('部分文件上传失败:\n' + errors.slice(0, 6).join('\n') + (errors.length > 6 ? '\n...' : ''));
}
return uploaded;
}
@@ -1966,7 +1966,7 @@ function startTreeShellRuntime() {
const message = error instanceof Error ? error.message : "创建页面失败";
setStatus(message, "error");
setLastAction("创建页面失败", "error");
window.alert(message);
await window.mnote.alert(message);
}
};
@@ -2040,7 +2040,7 @@ function startTreeShellRuntime() {
setLastAction("重命名失败", "error");
inlineRenameState = { mode: null, id: null, committing: false };
renderTree();
window.alert(message);
await window.mnote.alert(message);
}
};
@@ -2071,7 +2071,7 @@ function startTreeShellRuntime() {
const item = itemById.get(nodeId);
if (!item) return;
const siblings = getSiblings(item.parentNodeId);
const currentIndex = siblings.findIndex((entry) => entry.nodeId === nodeId);
const currentIndex = siblings.findIndex(entry) => entry.nodeId === nodeId);
if (currentIndex === -1) return;
const nextIndex = currentIndex + delta;
if (nextIndex < 0 || nextIndex >= siblings.length) return;
@@ -2099,7 +2099,7 @@ function startTreeShellRuntime() {
const message = error instanceof Error ? error.message : "移动页面失败";
setStatus(message, "error");
setLastAction("移动页面失败", "error");
window.alert(message);
await window.mnote.alert(message);
}
};
@@ -2108,7 +2108,7 @@ function startTreeShellRuntime() {
const targetItem = itemById.get(targetNodeId);
if (!sourceItem || !targetItem) return;
const siblings = getSiblings(targetItem.parentNodeId);
const targetIndex = siblings.findIndex((entry) => entry.nodeId === targetNodeId);
const targetIndex = siblings.findIndex(entry) => entry.nodeId === targetNodeId);
if (targetIndex < 0) return;
try {
const result = await sendCommand({
@@ -2134,7 +2134,7 @@ function startTreeShellRuntime() {
const message = error instanceof Error ? error.message : "拖拽移动失败";
setStatus(message, "error");
setLastAction("拖拽移动失败", "error");
window.alert(message);
await window.mnote.alert(message);
}
};
@@ -79,49 +79,77 @@
return data && data.result != null ? data.result : data;
}
function maskLabel(state) {
if (!state || state.state === 'absent') return '(无)';
if (state.state === 'masked') return '••••••••';
if (state.state === 'revealed') return state.value || '';
return '••••••••';
}
function escapeHtml(text) {
return String(text == null ? '' : text)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;');
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
}
/**
* Reusable helper for rendering a single secret row (password/apikey/token).
* Used heavily by account secrets, nested secrets, and the second-secret bug fix.
* Reduces duplication in render blocks.
* Display label for a secret field state. Always HTML-escaped so callers can
* safely inject into innerHTML (revealed values may contain user-controlled text).
*/
function renderSecretRow(field, secretState, accountId, secretId, showEmpty, opts = {}) {
var key = opts.secretId || '';
function maskLabel(secretState) {
if (!secretState || secretState.state === 'absent') return '(无)';
if (secretState.state === 'masked') return '••••••••';
if (secretState.state === 'revealed') return escapeHtml(secretState.value || '');
return '••••••••';
}
/**
* Optional reusable secret-row helper. Prefer bindWorkbench's secretRow for
* production paths; this stays fail-safe if called with uiState in opts.
* @param {object} [opts.uiState] workbench state with revealed / _revealedValues
*/
function renderSecretRow(field, secretState, accountId, secretId, showEmpty, opts) {
opts = opts || {};
var uiState = opts.uiState || { revealed: {}, _revealedValues: {} };
var revealed = uiState.revealed || {};
var revealedValues = uiState._revealedValues || {};
var key = opts.secretId || secretId || field || '';
var has = secretState && secretState.state === 'masked';
var canReveal = has || (secretState && secretState.state === 'revealed');
if (!canReveal && !showEmpty) return '';
var shown = state.revealed[key]
? state._revealedValues && state._revealedValues[key]
? escapeHtml(state._revealedValues[key])
: maskLabel(secretState)
var isOpen = !!revealed[key];
var shown = isOpen
? (revealedValues[key] != null
? escapeHtml(revealedValues[key])
: maskLabel(secretState))
: maskLabel(secretState);
var revealBtn = state.revealed[key] ? '隐藏' : '显示';
var copyBtn = '<button type="button" data-vault-copy-secret="" data-testid="vault-secret-' + escapeHtml(key) + '">复制</button>';
var revealBtn = isOpen ? '隐藏' : '显示';
var copyBtn =
'<button type="button" data-vault-copy-secret="" data-testid="vault-secret-' +
escapeHtml(key) +
'">复制</button>';
var actions =
'<button type="button" data-vault-reveal="" data-vault-secret-id="' +
escapeHtml(secretId || '') +
'" data-testid="vault-reveal-' +
escapeHtml(key) +
'">' +
revealBtn +
'</button>' +
copyBtn;
var actions = state.revealed[key]
? '<button type="button" data-vault-reveal="" data-vault-secret-id="' + escapeHtml(secretId) + '" data-testid="vault-reveal-' + escapeHtml(key) + '">' + revealBtn + '</button>' + copyBtn
: '<button type="button" data-vault-reveal="" data-vault-secret-id="' + escapeHtml(secretId) + '" data-testid="vault-reveal-' + escapeHtml(key) + '">' + revealBtn + '</button>' + copyBtn;
return '<div class="mnote-vault-field-row" data-vault-secret-row="' + escapeHtml(key) + '">'
+ '<div class="mnote-vault-secret-value" data-testid="vault-secret-' + escapeHtml(key) + '">' + shown + '</div>'
+ '<div class="mnote-vault-secret-actions">' + actions + '</div>'
+ '</div>';
return (
'<div class="mnote-vault-field-row" data-vault-secret-row="' +
escapeHtml(key) +
'">' +
'<div class="mnote-vault-secret-value" data-testid="vault-secret-' +
escapeHtml(key) +
'">' +
shown +
'</div>' +
'<div class="mnote-vault-secret-actions">' +
actions +
'</div>' +
'</div>'
);
}
function bindWorkbench(root) {
@@ -531,6 +559,10 @@
badge.classList.remove('is-ai');
}
}
var repairBtn = qs('[data-vault-repair-ai-folders]', root);
if (repairBtn) {
repairBtn.hidden = !state.isAiVault;
}
root.setAttribute('data-vault-role', state.isAiVault ? 'ai' : 'user');
}
@@ -540,9 +572,9 @@
statusEl.setAttribute('data-type', type || 'info');
}
function confirmDiscard() {
async function confirmDiscard() {
if (!state.dirty) return true;
return window.confirm('有未保存的修改,确定放弃?');
return await window.mnote.confirm('有未保存的修改,确定放弃?');
}
function filteredItems() {
@@ -840,6 +872,53 @@
return out;
}
/**
* URLs for the edit form: keep empty draft rows (so "+ 网址" can add a blank
* line). normalizeUrlsFromItem strips empties and would make add-url look dead.
*/
function urlsForFormEditor(item) {
if (item && Array.isArray(item.urls) && item.urls.length) {
return item.urls.map(function (u) {
return u == null ? '' : String(u);
});
}
var urls = normalizeUrlsFromItem(item);
if (!urls.length) urls = [''];
return urls;
}
/** Make a safe absolute href for external open (http/https only). */
function externalUrlHref(raw) {
var t = String(raw || '').trim();
if (!t) return '';
if (/^https?:\/\//i.test(t)) return t;
if (/^\/\//.test(t)) return 'https:' + t;
// Bare host/path: assume https (common for vault entries).
if (/^[a-z0-9][a-z0-9.-]*\.[a-z]{2,}([/:].*)?$/i.test(t)) {
return 'https://' + t;
}
return '';
}
/** View-mode URL as hyperlink look; open only via Ctrl/Cmd+click (see click handler). */
function urlLinkHtml(raw) {
var text = String(raw || '').trim();
if (!text) return '<span class="mnote-vault-muted">—</span>';
var href = externalUrlHref(text);
if (!href) {
return '<span class="mnote-vault-field-value">' + escapeHtml(text) + '</span>';
}
return (
'<a class="mnote-vault-url-link" href="' +
escapeHtml(href) +
'" data-vault-external-url="' +
escapeHtml(href) +
'" title="Ctrl/Cmd + 点击打开">' +
escapeHtml(text) +
'</a>'
);
}
function emptyForm(defaults) {
defaults = defaults || {};
var urls = normalizeUrlsFromItem(defaults);
@@ -1381,20 +1460,14 @@
if (urls.length) {
urlsHtml = urls
.map(function (u, idx) {
var label = idx === 0 ? '主网址' : '备用 ' + idx;
var link =
'<a href="' +
escapeHtml(u) +
'" target="_blank" rel="noopener noreferrer">' +
escapeHtml(u) +
'</a>';
var label = idx === 0 ? (urls.length > 1 ? '主网址' : '网址') : '备用 ' + idx;
return (
'<div class="mnote-vault-field-row">' +
'<label>' +
escapeHtml(label) +
'</label>' +
'<div class="mnote-vault-field-value">' +
link +
urlLinkHtml(u) +
'</div></div>'
);
})
@@ -1412,9 +1485,7 @@
' 个</span></div>' +
urlsHtml +
'</div>'
: urls.length === 1
? fieldRow('网址', urls[0], showEmpty)
: urlsHtml) +
: urlsHtml) +
'<div class="mnote-vault-section">' +
'<div class="mnote-vault-section-head"><span>登录账号</span>' +
(accounts.length > 1
@@ -1722,8 +1793,8 @@
var tagsStr = (item.tags || []).join(', ');
// 编辑时用 usernameTemplate/emailTemplate,避免把已展开明文写回。
var accounts = normalizeAccountsFromItem(item, true);
var urls = normalizeUrlsFromItem(item);
if (!urls.length) urls = [''];
// Prefer draft rows (may include empty slots after "+ 网址"); do not strip blanks.
var urls = urlsForFormEditor(item);
state._formUrls = urls.slice();
/**
* Rebuild draft slots from item projection.
@@ -2325,10 +2396,43 @@
} else {
renderDetail();
}
setStatus(
(state.isAiVault ? 'AI 密码本 · ' : '') + '共 ' + state.items.length + ' 条',
'success'
var repair = result && result.folderRepair;
var statusMsg =
(state.isAiVault ? 'AI 密码本 · ' : '') + '共 ' + state.items.length + ' 条';
if (repair && Number(repair.updated) > 0) {
statusMsg += ' · 已修复分组 ' + repair.updated + ' 条';
}
setStatus(statusMsg, 'success');
} catch (err) {
setStatus(err.message || String(err), 'error');
toast(err.message || String(err), 'error');
}
}
async function repairAiFolders() {
if (!state.isAiVault) return;
// 你已清掉其它人的分享时,可指定默认分享者;留空则自动从 from-* / sharedFrom 推断
var hint = window.prompt(
'修复 AI 分组为「用户名/原分组」。\n' +
'若库内只剩一人分享,可留空自动识别;否则填写分享者用户名(如 liaibo):',
'liaibo'
);
if (hint === null) return;
setStatus('正在修复 AI 分组…', 'info');
try {
var body = {};
var actor = String(hint || '').trim();
if (actor) body.defaultSourceActor = actor;
var payload = await apiJson('POST', '/api/vault/ai/repair-folders', state.rootUri, body);
var result = (payload && payload.result) || payload || {};
var n = Number(result.updated || 0);
toast(
n > 0
? '已修复 ' + n + ' 条 → 用户名/原分组(如 liaibo/系统)'
: '无需修复(分组已是 用户名/原分组)',
n > 0 ? 'success' : 'info'
);
await loadList();
} catch (err) {
setStatus(err.message || String(err), 'error');
toast(err.message || String(err), 'error');
@@ -2893,10 +2997,32 @@
}
}
root.addEventListener('click', function (ev) {
root.addEventListener('click', async function (ev) {
var t = ev.target;
if (!(t instanceof Element)) return;
// View-mode URL: look like a link, open only with Ctrl/Cmd+click.
var urlLink = t.closest('a.mnote-vault-url-link, a[data-vault-external-url]');
if (urlLink && root.contains(urlLink)) {
var href =
urlLink.getAttribute('data-vault-external-url') ||
urlLink.getAttribute('href') ||
'';
if (!(ev.ctrlKey || ev.metaKey)) {
ev.preventDefault();
return;
}
ev.preventDefault();
if (href) {
try {
window.open(href, '_blank', 'noopener,noreferrer');
} catch (_e) {
/* ignore popup block */
}
}
return;
}
var toggle = t.closest('[data-vault-toggle-folder]');
if (toggle && root.contains(toggle)) {
var fpath = toggle.getAttribute('data-vault-toggle-folder') || '';
@@ -3298,8 +3424,7 @@
var shareTitle =
(state.item && state.item.title) || state.selectedId;
var alreadyShared = itemIsSharedToAi(state.item);
if (
!window.confirm(
if (!(await window.mnote.confirm(
alreadyShared
? '将「' +
shareTitle +
@@ -3310,7 +3435,7 @@
'」复制到 AI 密码本(默认账号 mnote-e2e 的 my-space vault)?\n' +
'会新建一条带 ai-shared 标签的副本,并在本条目标记「已共享」;原条目保留。\n' +
'密文 [Key] 模板原样复制,并自动同步缺失密文 key。'
)
))
) {
return;
}
@@ -3343,7 +3468,7 @@
return loadList();
});
})
.catch(function (err) {
.catch(async function (err) {
toast(err.message || String(err), 'error');
setStatus(err.message || String(err), 'error');
});
@@ -3367,8 +3492,7 @@
state.item.sharedToAi &&
state.item.sharedToAi.targetActorId) ||
'mnote-e2e';
if (
!window.confirm(
if (!(await window.mnote.confirm(
'撤销「' +
unshareTitle +
'」对 AI 密码本(' +
@@ -3376,7 +3500,7 @@
')的共享?\n' +
'将清除本条「已共享」标记,并把 AI 密码本中的副本移入回收站。\n' +
'原条目与密文簿 key 保留;AI 侧密文 key 不删除(可能被其它条目引用)。'
)
))
) {
return;
}
@@ -3414,6 +3538,10 @@
return;
}
if (t.closest('[data-vault-repair-ai-folders]')) {
repairAiFolders();
return;
}
if (t.closest('[data-vault-cipher-book]')) {
if (!confirmDiscard()) return;
state.mode = 'cipher';
@@ -3472,7 +3600,7 @@
delete state.cipherRevealed[ek];
return loadCipherBook();
})
.catch(function (err) {
.catch(async function (err) {
toast(err.message || String(err), 'error');
});
return;
@@ -3480,7 +3608,7 @@
var cipherDel = t.closest('[data-vault-cipher-delete]');
if (cipherDel) {
var dk = cipherDel.getAttribute('data-vault-cipher-delete');
if (!window.confirm('删除密文簿 [' + dk + ']?引用它的密码将无法完整展开。')) return;
if (!(await window.mnote.confirm('删除密文簿 [' + dk + ']?引用它的密码将无法完整展开。'))) return;
apiJson(
'DELETE',
'/api/vault/cipher-book/' + encodeURIComponent(dk),
@@ -3570,27 +3698,6 @@
return;
}
var clearNestedSec = t.closest('[data-vault-clear-sec-value]');
if (clearNestedSec) {
var csid = clearNestedSec.getAttribute('data-vault-clear-sec-value');
var cAcc = clearNestedSec.getAttribute('data-acc-id');
var formClearSec = qs('[data-vault-form]', detailEl);
if (formClearSec) snapshotFormSlots(formClearSec);
(state._formAccounts || []).forEach(function (acc) {
if (cAcc && acc.id !== cAcc) return;
(acc.secrets || []).forEach(function (sec) {
if (sec.id !== csid) return;
sec.valueClear = true;
sec.valueText = '';
sec.valueState = { state: 'absent' };
});
});
state.dirty = true;
reRenderFormKeepingSlots(state.mode === 'create');
toast('密钥将在保存时清空', 'info');
return;
}
var clearBtn = t.closest('[data-vault-clear-secret]');
if (clearBtn) {
var cf = clearBtn.getAttribute('data-vault-clear-secret');
@@ -3606,7 +3713,7 @@
}
if (t.closest('[data-vault-delete]')) {
if (!window.confirm('确定将条目移入密码箱回收站?')) return;
if (!(await window.mnote.confirm('确定将条目移入密码箱回收站?'))) return;
apiJson(
'DELETE',
'/api/vault/items/' + encodeURIComponent(state.selectedId),
@@ -3637,13 +3744,13 @@
if (tabDeleted) tabDeleted.classList.remove('is-active');
return loadList();
})
.catch(function (err) {
.catch(async function (err) {
toast(err.message || String(err), 'error');
});
return;
}
if (t.closest('[data-vault-purge]')) {
if (!window.confirm('永久删除后无法恢复,确定?')) return;
if (!(await window.mnote.confirm('永久删除后无法恢复,确定?'))) return;
apiJson(
'POST',
'/api/vault/items/' + encodeURIComponent(state.selectedId) + '/purge',