Harden auth/vault path sanitization and clean WeKnora docs
This commit is contained in:
@@ -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 基础运行时:全局 portal 与 toast API。
|
||||
// MNote UI 基础运行时:全局 portal、toast、居中 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 模式:确定=true;alert 模式始终 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, '&')
|
||||
.replace(/</g, '<')
|
||||
.replace(/>/g, '>')
|
||||
.replace(/"/g, '"');
|
||||
.replace(/"/g, '"')
|
||||
.replace(/'/g, ''');
|
||||
}
|
||||
|
||||
/**
|
||||
* 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',
|
||||
|
||||
Reference in New Issue
Block a user