fix: stabilize local folder AI document workflow
- scope local-folder PageTree revision and document sidebar rendering to fileTreeScope - preserve projected table/image attrs for local Markdown aggregate fallback - avoid FileTree restore forced layouts on cold design open - add API ChatOnly provider runtime and local OCR task handling regressions
This commit is contained in:
@@ -1,4 +1,5 @@
|
||||
import {
|
||||
localFileOpenPathFromTiptapHref,
|
||||
localMarkdownDocumentIdFromRelativePath,
|
||||
localMarkdownRelativePathFromDocumentId,
|
||||
localizeTiptapAssetUrls,
|
||||
@@ -996,6 +997,36 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
return override === 'mock' ? 'mock' : 'mineru';
|
||||
};
|
||||
|
||||
const localOcrAutoEnabled = async () => {
|
||||
const cached = window.__MNOTE_LOCAL_OCR_PREFERENCES;
|
||||
if (cached && typeof cached === 'object' && cached['localOcr.autoEnabled'] === true) return true;
|
||||
const documentId = currentWebShellDocumentId();
|
||||
const workspaceId = currentWebShellWorkspaceId();
|
||||
const sourceKind = currentWebShellSourceKind();
|
||||
const rootUri = currentWebShellRootUri();
|
||||
if (!documentId && !workspaceId) return false;
|
||||
try {
|
||||
const params = new URLSearchParams();
|
||||
if (documentId) params.set('documentId', documentId);
|
||||
if (workspaceId) params.set('workspaceId', workspaceId);
|
||||
if (sourceKind) params.set('sourceKind', sourceKind);
|
||||
if (rootUri) params.set('rootUri', rootUri);
|
||||
const response = await fetch('/api/ui/preferences/effective?' + params.toString(), {
|
||||
cache: 'no-store',
|
||||
headers: { accept: 'application/json' },
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok || !payload || payload.ok !== true) return false;
|
||||
const preferences = payload.result?.localOcrPreferences && typeof payload.result.localOcrPreferences === 'object'
|
||||
? payload.result.localOcrPreferences
|
||||
: {};
|
||||
window.__MNOTE_LOCAL_OCR_PREFERENCES = { 'localOcr.autoEnabled': false, ...preferences };
|
||||
return window.__MNOTE_LOCAL_OCR_PREFERENCES['localOcr.autoEnabled'] === true;
|
||||
} catch (_) {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
|
||||
const setLocalOcrStatus = (entry, status, message, job) => {
|
||||
if (!(entry?.panel instanceof HTMLElement)) return;
|
||||
const normalizedStatus = String(status || '').trim() || 'unknown';
|
||||
@@ -1044,17 +1075,93 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
jobsBySource: new Map(),
|
||||
drawerOpen: false,
|
||||
eventSource: null,
|
||||
fileTreeRefreshKeys: new Set(),
|
||||
};
|
||||
|
||||
const localOcrJobKey = (job) => String(job?.sourceRootRelativePath || job?.jobId || '').trim();
|
||||
|
||||
const localOcrParentRelativePath = (relativePath) => {
|
||||
const normalized = String(relativePath || '').trim().replace(/\\/g, '/').replace(/^\/+|\/+$/g, '');
|
||||
if (!normalized || normalized.indexOf('/') < 0) return '';
|
||||
return normalized.split('/').slice(0, -1).join('/');
|
||||
};
|
||||
|
||||
const dispatchLocalOcrFileTreeRefresh = (job, rootUri) => {
|
||||
const status = String(job?.status || '').trim();
|
||||
if (!['done', 'stale'].includes(status)) return;
|
||||
const ocrPath = String(job?.ocrRootRelativePath || '').trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
const normalizedRootUri = String(rootUri || localOcrTaskState.rootUri || '').trim();
|
||||
if (!ocrPath || !normalizedRootUri) return;
|
||||
const refreshKey = `${ocrPath}:${String(job?.updatedAtMs || job?.finishedAtMs || status)}`;
|
||||
if (localOcrTaskState.fileTreeRefreshKeys.has(refreshKey)) return;
|
||||
localOcrTaskState.fileTreeRefreshKeys.add(refreshKey);
|
||||
const ocrParent = localOcrParentRelativePath(ocrPath);
|
||||
const ocrParentParent = localOcrParentRelativePath(ocrParent);
|
||||
const affectedParents = [ocrParent, ocrParentParent]
|
||||
.filter((path, index, list) => index === list.indexOf(path))
|
||||
.map((relativePath) => ({ relativePath, reason: 'local-ocr-sidecar-written' }));
|
||||
const changedPaths = [
|
||||
{ relativePath: ocrPath, changeType: 'created' },
|
||||
ocrParent ? { relativePath: ocrParent, changeType: 'created' } : null,
|
||||
].filter(Boolean);
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-filetree-refresh', ocrPath);
|
||||
window.dispatchEvent(new CustomEvent('tree:local-folder-watch-batch', {
|
||||
detail: {
|
||||
payload: {
|
||||
schema: 'mnote.local_folder.watch_batch.v1',
|
||||
source: 'local_ocr.job.updated',
|
||||
rootUri: normalizedRootUri,
|
||||
revision: String(job?.updatedAtMs || Date.now()),
|
||||
changedPaths,
|
||||
affectedParents,
|
||||
},
|
||||
},
|
||||
}));
|
||||
};
|
||||
|
||||
const updateLocalOcrTaskState = (job) => {
|
||||
const key = localOcrJobKey(job);
|
||||
if (!key) return;
|
||||
if (String(job?.status || '').trim() === 'deleted') {
|
||||
localOcrTaskState.jobsBySource.delete(key);
|
||||
renderLocalOcrTaskDock();
|
||||
return;
|
||||
}
|
||||
localOcrTaskState.jobsBySource.set(key, job);
|
||||
renderLocalOcrTaskDock();
|
||||
};
|
||||
|
||||
const bindLocalOcrTopbarAction = () => {
|
||||
const toggle = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
|
||||
if (!(toggle instanceof HTMLButtonElement)) return null;
|
||||
if (toggle.getAttribute('data-mnote-local-ocr-bound') === 'true') return toggle;
|
||||
toggle.setAttribute('data-mnote-local-ocr-bound', 'true');
|
||||
toggle.addEventListener('click', () => {
|
||||
void runManualLocalOcrForActiveTarget(toggle).catch((error) => {
|
||||
console.warn('mnote local OCR 手动入口失败', error);
|
||||
});
|
||||
});
|
||||
return toggle;
|
||||
};
|
||||
|
||||
const localOcrJobSnapshotFromEntry = (entry, status, provider, message, timestamp = Date.now()) => {
|
||||
const sourceRootRelativePath = String(entry?.path || '').trim();
|
||||
return {
|
||||
jobId: `local-ocr-${status}:${sourceRootRelativePath || 'unknown'}:${timestamp}`,
|
||||
ownerDocumentId: String(entry?.ownerDocumentId || entry?.documentId || currentWebShellDocumentId() || '').trim(),
|
||||
sourceRootRelativePath,
|
||||
rootUri: String(entry?.rootUri || localOcrTaskState.rootUri || '').trim(),
|
||||
ocrRootRelativePath: '',
|
||||
provider: String(provider || localOcrProvider()),
|
||||
status,
|
||||
stageLabel: message || statusTextForLocalOcrJob({ status }),
|
||||
stale: false,
|
||||
updatedAtMs: timestamp,
|
||||
finishedAtMs: status === 'failed' ? timestamp : null,
|
||||
error: status === 'failed' ? String(message || '') : '',
|
||||
};
|
||||
};
|
||||
|
||||
const statusTextForLocalOcrJob = (job) => {
|
||||
const status = String(job?.status || '').trim();
|
||||
if (job?.stageLabel) return String(job.stageLabel);
|
||||
@@ -1067,20 +1174,58 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
|
||||
const ensureLocalOcrTaskDock = () => {
|
||||
let dock = document.querySelector('[data-testid="mnote-local-ocr-task-dock"]');
|
||||
if (dock instanceof HTMLElement) return dock;
|
||||
dock = document.createElement('section');
|
||||
dock.className = 'mnote-local-ocr-task-dock';
|
||||
dock.setAttribute('data-testid', 'mnote-local-ocr-task-dock');
|
||||
dock.innerHTML = '<button type="button" class="mnote-local-ocr-task-button" data-testid="mnote-local-ocr-task-toggle" aria-expanded="false">OCR 0</button><div class="mnote-local-ocr-task-drawer" data-testid="mnote-local-ocr-task-drawer" hidden><div class="mnote-local-ocr-task-head"><strong>OCR 任务</strong></div><div class="mnote-local-ocr-task-list" data-testid="mnote-local-ocr-task-list"></div></div>';
|
||||
document.body.appendChild(dock);
|
||||
const toggle = dock.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
|
||||
if (toggle instanceof HTMLButtonElement) {
|
||||
toggle.addEventListener('click', () => {
|
||||
localOcrTaskState.drawerOpen = !localOcrTaskState.drawerOpen;
|
||||
renderLocalOcrTaskDock();
|
||||
});
|
||||
if (!(dock instanceof HTMLElement)) {
|
||||
dock = document.createElement('section');
|
||||
dock.className = 'mnote-local-ocr-task-dock';
|
||||
dock.setAttribute('data-testid', 'mnote-local-ocr-task-dock');
|
||||
dock.innerHTML = '<div class="mnote-local-ocr-task-drawer" data-testid="mnote-local-ocr-task-drawer" hidden><div class="mnote-local-ocr-task-head"><strong>OCR 任务</strong><button type="button" class="mnote-local-ocr-task-close" data-mnote-local-ocr-task-close aria-label="收起 OCR 任务">×</button></div><div class="mnote-local-ocr-task-list" data-testid="mnote-local-ocr-task-list"></div></div>';
|
||||
document.body.appendChild(dock);
|
||||
}
|
||||
let toggle = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
|
||||
if (!(toggle instanceof HTMLButtonElement)) {
|
||||
const actions = document.querySelector('.wolai-topbar-actions');
|
||||
toggle = document.createElement('button');
|
||||
toggle.type = 'button';
|
||||
toggle.className = 'wolai-icon-button mnote-local-ocr-task-toggle';
|
||||
toggle.setAttribute('title', 'OCR 任务');
|
||||
toggle.setAttribute('aria-label', 'OCR 任务');
|
||||
toggle.setAttribute('data-testid', 'mnote-local-ocr-task-toggle');
|
||||
toggle.setAttribute('data-mnote-action', 'toggle-ocr-tasks');
|
||||
toggle.innerHTML = '<span class="material-symbols-outlined" data-icon="document_scanner" aria-hidden="true"></span><span class="mnote-local-ocr-task-badge" data-mnote-local-ocr-task-count hidden>0</span>';
|
||||
if (actions instanceof HTMLElement) actions.appendChild(toggle);
|
||||
else document.body.appendChild(toggle);
|
||||
}
|
||||
bindLocalOcrTopbarAction();
|
||||
if (dock.getAttribute('data-mnote-local-ocr-bound') === 'true') return dock;
|
||||
dock.setAttribute('data-mnote-local-ocr-bound', 'true');
|
||||
dock.addEventListener('click', (event) => {
|
||||
const closeButton = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-close]') : null;
|
||||
if (closeButton instanceof HTMLElement) {
|
||||
localOcrTaskState.drawerOpen = false;
|
||||
renderLocalOcrTaskDock();
|
||||
return;
|
||||
}
|
||||
const clearButton = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-clear]') : null;
|
||||
if (clearButton instanceof HTMLElement) {
|
||||
const sourcePath = clearButton.getAttribute('data-mnote-local-ocr-task-clear') || '';
|
||||
if (sourcePath) {
|
||||
localOcrTaskState.jobsBySource.delete(sourcePath);
|
||||
renderLocalOcrTaskDock();
|
||||
}
|
||||
return;
|
||||
}
|
||||
const deleteButton = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-delete]') : null;
|
||||
if (deleteButton instanceof HTMLElement) {
|
||||
const sourcePath = deleteButton.getAttribute('data-mnote-local-ocr-task-delete') || '';
|
||||
if (sourcePath) {
|
||||
void deleteLocalOcrJob(sourcePath).catch((error) => {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-delete-error', message);
|
||||
console.warn('mnote local OCR 删除失败', error);
|
||||
});
|
||||
}
|
||||
return;
|
||||
}
|
||||
const openButton = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-open]') : null;
|
||||
if (openButton instanceof HTMLElement) {
|
||||
const sourcePath = openButton.getAttribute('data-mnote-local-ocr-task-open') || '';
|
||||
@@ -1129,10 +1274,20 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const jobs = Array.from(localOcrTaskState.jobsBySource.values())
|
||||
.sort((left, right) => Number(right.updatedAtMs || 0) - Number(left.updatedAtMs || 0));
|
||||
const runningCount = jobs.filter((job) => !['done', 'failed', 'stale'].includes(String(job?.status || ''))).length;
|
||||
const toggle = dock.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
|
||||
const toggle = document.querySelector('[data-testid="mnote-local-ocr-task-toggle"]');
|
||||
if (toggle instanceof HTMLButtonElement) {
|
||||
toggle.textContent = runningCount > 0 ? `OCR ${runningCount} 处理中` : `OCR ${jobs.length}`;
|
||||
const label = runningCount > 0 ? `${runningCount} 个 OCR 处理中` : (jobs.length > 0 ? `${jobs.length} 个 OCR 任务` : 'OCR 任务');
|
||||
toggle.setAttribute('title', label);
|
||||
toggle.setAttribute('aria-label', label);
|
||||
toggle.setAttribute('aria-expanded', localOcrTaskState.drawerOpen ? 'true' : 'false');
|
||||
toggle.classList.toggle('has-mnote-local-ocr-tasks', jobs.length > 0);
|
||||
const badge = toggle.querySelector('[data-mnote-local-ocr-task-count]');
|
||||
if (badge instanceof HTMLElement) {
|
||||
badge.textContent = String(runningCount > 0 ? runningCount : jobs.length);
|
||||
badge.hidden = jobs.length === 0;
|
||||
} else {
|
||||
toggle.textContent = runningCount > 0 ? `OCR ${runningCount} 处理中` : `OCR ${jobs.length}`;
|
||||
}
|
||||
}
|
||||
const drawer = dock.querySelector('[data-testid="mnote-local-ocr-task-drawer"]');
|
||||
if (drawer instanceof HTMLElement) drawer.hidden = !localOcrTaskState.drawerOpen;
|
||||
@@ -1152,7 +1307,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
row.setAttribute('data-mnote-local-ocr-task-row', String(job.sourceRootRelativePath || ''));
|
||||
row.setAttribute('data-mnote-local-ocr-task-status', String(job.status || ''));
|
||||
const title = String(job.sourceRootRelativePath || '').split('/').filter(Boolean).pop() || 'OCR';
|
||||
row.innerHTML = '<div class="mnote-local-ocr-task-main"><strong></strong><span></span></div><div class="mnote-local-ocr-task-actions"><button type="button" data-mnote-local-ocr-task-open>打开 OCR</button><button type="button" data-mnote-local-ocr-task-retry>重试</button></div>';
|
||||
row.innerHTML = '<div class="mnote-local-ocr-task-main"><strong></strong><span></span></div><div class="mnote-local-ocr-task-actions"><button type="button" data-mnote-local-ocr-task-open>打开</button><button type="button" data-mnote-local-ocr-task-retry>重试</button><button type="button" data-mnote-local-ocr-task-clear>清除</button><button type="button" data-mnote-local-ocr-task-delete>删除</button></div>';
|
||||
row.querySelector('strong').textContent = title;
|
||||
row.querySelector('span').textContent = statusTextForLocalOcrJob(job);
|
||||
const open = row.querySelector('[data-mnote-local-ocr-task-open]');
|
||||
@@ -1165,6 +1320,15 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
retry.setAttribute('data-mnote-local-ocr-task-retry', String(job.sourceRootRelativePath || ''));
|
||||
retry.hidden = !['failed', 'stale'].includes(String(job.status || ''));
|
||||
}
|
||||
const clear = row.querySelector('[data-mnote-local-ocr-task-clear]');
|
||||
if (clear instanceof HTMLButtonElement) {
|
||||
clear.setAttribute('data-mnote-local-ocr-task-clear', String(job.sourceRootRelativePath || ''));
|
||||
}
|
||||
const deleteOcr = row.querySelector('[data-mnote-local-ocr-task-delete]');
|
||||
if (deleteOcr instanceof HTMLButtonElement) {
|
||||
deleteOcr.setAttribute('data-mnote-local-ocr-task-delete', String(job.sourceRootRelativePath || ''));
|
||||
deleteOcr.hidden = !job.ocrRootRelativePath;
|
||||
}
|
||||
list.appendChild(row);
|
||||
});
|
||||
};
|
||||
@@ -1178,7 +1342,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const response = await fetch(url.toString(), { cache: 'no-store', headers: { accept: 'application/json' } });
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok || !payload || payload.ok !== true) return;
|
||||
(Array.isArray(payload.jobs) ? payload.jobs : []).forEach(updateLocalOcrTaskState);
|
||||
(Array.isArray(payload.jobs) ? payload.jobs : []).forEach((job) => {
|
||||
if (job && typeof job === 'object') job.rootUri = normalizedRoot;
|
||||
updateLocalOcrTaskState(job);
|
||||
});
|
||||
renderLocalOcrTaskDock();
|
||||
};
|
||||
|
||||
@@ -1198,7 +1365,11 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
eventSource.addEventListener('local_ocr.job.updated', (event) => {
|
||||
let payload = null;
|
||||
try { payload = JSON.parse(event.data || '{}'); } catch (_) {}
|
||||
if (payload?.job) updateLocalOcrTaskState(payload.job);
|
||||
if (payload?.job) {
|
||||
if (payload.job && typeof payload.job === 'object') payload.job.rootUri = payload.rootUri || normalizedRoot;
|
||||
updateLocalOcrTaskState(payload.job);
|
||||
dispatchLocalOcrFileTreeRefresh(payload.job, payload.rootUri || normalizedRoot);
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
@@ -1247,8 +1418,13 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
|
||||
const createLocalOcrJob = async (entry) => {
|
||||
if (!isLocalOcrSourceEntry(entry)) return null;
|
||||
setLocalOcrStatus(entry, 'running', 'OCR 处理中', entry.localOcrJob || null);
|
||||
const provider = localOcrProvider();
|
||||
const entryRootUri = String(entry.rootUri || '').trim();
|
||||
if (entryRootUri) localOcrTaskState.rootUri = entryRootUri;
|
||||
const startedAt = Date.now();
|
||||
const pendingJob = localOcrJobSnapshotFromEntry(entry, 'running', provider, '处理中', startedAt);
|
||||
setLocalOcrStatus(entry, 'running', 'OCR 处理中', pendingJob);
|
||||
updateLocalOcrTaskState(pendingJob);
|
||||
const body = {
|
||||
rootUri: String(entry.rootUri || '').trim(),
|
||||
documentId: String(entry.ownerDocumentId || entry.documentId || currentWebShellDocumentId() || '').trim(),
|
||||
@@ -1266,15 +1442,159 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
const message = payload?.error?.message || `local_ocr_job_failed_${response.status}`;
|
||||
setLocalOcrStatus(entry, 'failed', message, entry.localOcrJob || null);
|
||||
const failedJob = {
|
||||
...pendingJob,
|
||||
status: 'failed',
|
||||
stageLabel: message,
|
||||
updatedAtMs: Date.now(),
|
||||
finishedAtMs: Date.now(),
|
||||
error: message,
|
||||
};
|
||||
setLocalOcrStatus(entry, 'failed', message, failedJob);
|
||||
updateLocalOcrTaskState(failedJob);
|
||||
throw new Error(message);
|
||||
}
|
||||
const job = payload.job || null;
|
||||
const job = payload.job && typeof payload.job === 'object' ? { ...payload.job, rootUri: entryRootUri } : null;
|
||||
setLocalOcrStatus(entry, String(job?.status || 'done'), job?.stale ? 'OCR 需更新' : 'OCR 已完成', job);
|
||||
if (job) updateLocalOcrTaskState(job);
|
||||
return job;
|
||||
};
|
||||
|
||||
const deleteLocalOcrJob = async (sourceRootRelativePath) => {
|
||||
const sourcePath = String(sourceRootRelativePath || '').trim();
|
||||
if (!sourcePath) return false;
|
||||
const job = localOcrTaskState.jobsBySource.get(sourcePath) || null;
|
||||
const rootUri = String(job?.rootUri || localOcrTaskState.rootUri || currentWebShellRootUri() || '').trim();
|
||||
const response = await fetch('/api/local-folder/ocr/delete', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', accept: 'application/json' },
|
||||
body: JSON.stringify({
|
||||
rootUri,
|
||||
sourceRootRelativePath: sourcePath,
|
||||
}),
|
||||
});
|
||||
const payload = await response.json().catch(() => null);
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(payload?.error?.message || `local_ocr_delete_failed_${response.status}`);
|
||||
}
|
||||
if (payload.deleted !== true) {
|
||||
throw new Error('local_ocr_delete_noop');
|
||||
}
|
||||
localOcrTaskState.jobsBySource.delete(sourcePath);
|
||||
renderLocalOcrTaskDock();
|
||||
const ocrPath = String(job?.ocrRootRelativePath || '').trim();
|
||||
if (ocrPath) {
|
||||
dispatchLocalOcrFileTreeRefresh({ ...job, status: 'stale', updatedAtMs: Date.now(), ocrRootRelativePath: ocrPath }, localOcrTaskState.rootUri);
|
||||
}
|
||||
return true;
|
||||
};
|
||||
|
||||
const maybeAutoCreateLocalOcrJob = async (entry) => {
|
||||
if (!isLocalOcrSourceEntry(entry)) return;
|
||||
if (!await localOcrAutoEnabled()) return;
|
||||
const existing = await readLocalOcrStatus(entry);
|
||||
const status = String(existing?.status || '').trim();
|
||||
if (existing && !existing.stale && ['done', 'running'].includes(status)) {
|
||||
updateLocalOcrTaskState(existing);
|
||||
return;
|
||||
}
|
||||
await createLocalOcrJob(entry);
|
||||
};
|
||||
|
||||
const activeResourceTabEntry = (paneRole = 'primary') => {
|
||||
const role = normalizePaneRole(paneRole);
|
||||
for (const entry of resourceTabRegistry.values()) {
|
||||
if (normalizePaneRole(entry?.paneRole) !== role) continue;
|
||||
if (entry?.tab instanceof HTMLElement && entry.tab.getAttribute('aria-selected') === 'true') return entry;
|
||||
}
|
||||
return null;
|
||||
};
|
||||
|
||||
const localOcrCandidatesFromActiveMarkdown = (paneRole = 'primary') => {
|
||||
const role = normalizePaneRole(paneRole);
|
||||
const sourceKind = currentWebShellSourceKind();
|
||||
const rootUri = currentWebShellRootUri();
|
||||
if (sourceKind !== 'local_folder' || !rootUri) return [];
|
||||
const documentId = documentIdForPane(role) || currentWebShellDocumentId();
|
||||
if (!documentId) return [];
|
||||
const workspaceId = currentWebShellWorkspaceId();
|
||||
const seen = new Set();
|
||||
const candidates = [];
|
||||
document.querySelectorAll(`.document-pane[data-pane-role="${role}"] .ProseMirror img`).forEach((image) => {
|
||||
if (!(image instanceof HTMLImageElement)) return;
|
||||
const sourcePath = localFileOpenPathFromTiptapHref(image.getAttribute('src') || image.src || '');
|
||||
if (!sourcePath || seen.has(sourcePath)) return;
|
||||
seen.add(sourcePath);
|
||||
const title = sourcePath.split('/').filter(Boolean).pop() || sourcePath;
|
||||
candidates.push({
|
||||
sourceKind: 'local_folder',
|
||||
rootUri,
|
||||
path: sourcePath,
|
||||
kind: sourcePath.toLowerCase().endsWith('.pdf') ? 'pdf' : 'image',
|
||||
title,
|
||||
fileName: title,
|
||||
documentId,
|
||||
ownerDocumentId: documentId,
|
||||
workspaceId,
|
||||
objectIdentity: `local-file:${sourcePath}`,
|
||||
assetId: `local-file:${sourcePath}`,
|
||||
});
|
||||
});
|
||||
return candidates;
|
||||
};
|
||||
|
||||
const localOcrCandidatesForActiveTarget = (paneRole = 'primary') => {
|
||||
const activeResource = activeResourceTabEntry(paneRole);
|
||||
if (isLocalOcrSourceEntry(activeResource)) return [activeResource];
|
||||
return localOcrCandidatesFromActiveMarkdown(paneRole);
|
||||
};
|
||||
|
||||
const runManualLocalOcrForActiveTarget = async (toggle) => {
|
||||
ensureLocalOcrTaskDock();
|
||||
const candidates = localOcrCandidatesForActiveTarget('primary');
|
||||
if (!candidates.length) {
|
||||
if (localOcrTaskState.jobsBySource.size > 0) {
|
||||
localOcrTaskState.drawerOpen = !localOcrTaskState.drawerOpen;
|
||||
renderLocalOcrTaskDock();
|
||||
}
|
||||
return [];
|
||||
}
|
||||
if (toggle instanceof HTMLButtonElement) toggle.disabled = true;
|
||||
const jobs = [];
|
||||
let createdCount = 0;
|
||||
try {
|
||||
for (const entry of candidates) {
|
||||
try {
|
||||
ensureLocalOcrTaskEvents(entry.rootUri);
|
||||
const existing = await readLocalOcrStatus(entry);
|
||||
const existingStatus = String(existing?.status || '').trim();
|
||||
if (existing && !existing.stale && ['done', 'running'].includes(existingStatus)) {
|
||||
updateLocalOcrTaskState(existing);
|
||||
jobs.push(existing);
|
||||
continue;
|
||||
}
|
||||
const job = await createLocalOcrJob(entry);
|
||||
if (job) {
|
||||
createdCount += 1;
|
||||
jobs.push(job);
|
||||
}
|
||||
} catch (error) {
|
||||
console.warn('mnote local OCR 手动任务失败', entry?.path, error);
|
||||
if (localOcrTaskState.jobsBySource.has(String(entry?.path || '').trim())) {
|
||||
createdCount += 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (createdCount === 0 && localOcrTaskState.jobsBySource.size > 0) {
|
||||
localOcrTaskState.drawerOpen = !localOcrTaskState.drawerOpen;
|
||||
}
|
||||
} finally {
|
||||
if (toggle instanceof HTMLButtonElement) toggle.disabled = false;
|
||||
renderLocalOcrTaskDock();
|
||||
}
|
||||
return jobs;
|
||||
};
|
||||
|
||||
const renderLocalOcrToolbar = (entry) => {
|
||||
if (!isLocalOcrSourceEntry(entry) || !(entry.panel instanceof HTMLElement)) return;
|
||||
const toolbar = entry.panel.querySelector('[data-testid="mnote-local-ocr-toolbar"]');
|
||||
@@ -1322,6 +1642,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
|
||||
const createResourceSession = (entry, input, readResult) => {
|
||||
const resourcePath = String(input.path || '');
|
||||
const resourceDocumentId = localMarkdownDocumentIdFromRelativePath(resourcePath) || entry.objectIdentity;
|
||||
const tiptapDocument = localizeTiptapAssetUrls(
|
||||
toTiptapDocument(readResult?.content, readResult?.text || ''),
|
||||
{
|
||||
@@ -1334,7 +1655,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const session = {
|
||||
key: `resource:${input.rootUri || ''}:${input.path || entry.objectIdentity}`,
|
||||
sessionKind: 'resource',
|
||||
documentId: entry.objectIdentity,
|
||||
documentId: resourceDocumentId,
|
||||
ownerDocumentId: String(input.documentId || currentWebShellDocumentId() || '').trim(),
|
||||
workspaceId: String(input.workspaceId || currentWebShellWorkspaceId() || ''),
|
||||
sourceKind: 'local_folder',
|
||||
@@ -1447,21 +1768,20 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const openPassiveResourceTab = (entry, input) => {
|
||||
const href = String(input.officeUrl || input.href || '').trim();
|
||||
if (entry.kind === 'image') {
|
||||
entry.panel.innerHTML = isLocalOcrSourceEntry(entry)
|
||||
? '<div class="mnote-resource-tab-passive-shell" data-mnote-local-ocr-source="true"><div class="mnote-local-ocr-toolbar" data-testid="mnote-local-ocr-toolbar"><span class="mnote-local-ocr-status" data-testid="mnote-local-ocr-status" data-mnote-local-ocr-status-text>OCR 未生成</span><button type="button" data-testid="mnote-local-ocr-run" data-mnote-local-ocr-action="run">生成 OCR</button><button type="button" data-testid="mnote-local-ocr-open" data-mnote-local-ocr-action="open" disabled>打开 OCR</button><button type="button" data-testid="mnote-local-ocr-insert" data-mnote-local-ocr-action="insert" disabled>插入正文</button></div><img class="mnote-resource-tab-image" alt=""></div>'
|
||||
: '<img class="mnote-resource-tab-image" alt="">';
|
||||
entry.panel.innerHTML = '<img class="mnote-resource-tab-image" alt="">';
|
||||
const img = entry.panel.querySelector('img');
|
||||
if (img instanceof HTMLImageElement) {
|
||||
img.src = href;
|
||||
img.alt = entry.title;
|
||||
}
|
||||
renderLocalOcrToolbar(entry);
|
||||
ensureLocalOcrTaskDock();
|
||||
ensureLocalOcrTaskEvents(entry.rootUri);
|
||||
void loadLocalOcrJobs(entry.rootUri).catch(() => undefined);
|
||||
void maybeAutoCreateLocalOcrJob(entry).catch((error) => console.warn('mnote local OCR 自动任务失败', error));
|
||||
installPassiveResourceWatch(entry);
|
||||
return;
|
||||
}
|
||||
entry.panel.innerHTML = isLocalOcrSourceEntry(entry)
|
||||
? '<div class="mnote-resource-tab-passive-shell" data-mnote-local-ocr-source="true"><div class="mnote-local-ocr-toolbar" data-testid="mnote-local-ocr-toolbar"><span class="mnote-local-ocr-status" data-testid="mnote-local-ocr-status" data-mnote-local-ocr-status-text>OCR 未生成</span><button type="button" data-testid="mnote-local-ocr-run" data-mnote-local-ocr-action="run">生成 OCR</button><button type="button" data-testid="mnote-local-ocr-open" data-mnote-local-ocr-action="open" disabled>打开 OCR</button><button type="button" data-testid="mnote-local-ocr-insert" data-mnote-local-ocr-action="insert" disabled>插入正文</button></div><iframe class="mnote-resource-tab-frame" title=""></iframe></div>'
|
||||
: '<iframe class="mnote-resource-tab-frame" title=""></iframe>';
|
||||
entry.panel.innerHTML = '<iframe class="mnote-resource-tab-frame" title=""></iframe>';
|
||||
const frame = entry.panel.querySelector('iframe');
|
||||
if (frame instanceof HTMLIFrameElement) {
|
||||
frame.title = entry.title;
|
||||
@@ -1473,7 +1793,12 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
}
|
||||
frame.src = href;
|
||||
}
|
||||
renderLocalOcrToolbar(entry);
|
||||
if (isLocalOcrSourceEntry(entry)) {
|
||||
ensureLocalOcrTaskDock();
|
||||
ensureLocalOcrTaskEvents(entry.rootUri);
|
||||
void loadLocalOcrJobs(entry.rootUri).catch(() => undefined);
|
||||
void maybeAutoCreateLocalOcrJob(entry).catch((error) => console.warn('mnote local OCR 自动任务失败', error));
|
||||
}
|
||||
installPassiveResourceWatch(entry);
|
||||
};
|
||||
|
||||
@@ -1610,6 +1935,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
syncOpenEditorsSnapshot();
|
||||
};
|
||||
|
||||
try {
|
||||
bindLocalOcrTopbarAction();
|
||||
} catch (_) {}
|
||||
|
||||
return {
|
||||
activateMainEditorTab,
|
||||
bindMainEditorPageTab,
|
||||
|
||||
@@ -51,11 +51,12 @@ export const legacyStylesToTiptapMarks = (styles) => {
|
||||
export const legacyMarkArrayToTiptapMarks = (inlineMarks) => {
|
||||
if (!Array.isArray(inlineMarks)) return [];
|
||||
return inlineMarks.flatMap((mark) => {
|
||||
if (!mark || typeof mark !== 'object') return [];
|
||||
if (mark.type === 'bold' || mark.type === 'italic' || mark.type === 'underline' || mark.type === 'strike' || mark.type === 'code') {
|
||||
return [{ type: mark.type }];
|
||||
const markType = typeof mark === 'string' ? mark : typeof mark?.type === 'string' ? mark.type : '';
|
||||
if (!markType) return [];
|
||||
if (markType === 'bold' || markType === 'italic' || markType === 'underline' || markType === 'strike' || markType === 'code') {
|
||||
return [{ type: markType }];
|
||||
}
|
||||
if (mark.type === 'link') {
|
||||
if (markType === 'link') {
|
||||
const href = typeof mark?.attrs?.href === 'string' ? mark.attrs.href.trim() : '';
|
||||
return href ? [{ type: 'link', attrs: { href } }] : [];
|
||||
}
|
||||
@@ -77,10 +78,19 @@ export const legacyInlineContentToTiptap = (value) => {
|
||||
if (typeof value === 'string') return value ? [{ type: 'text', text: value }] : [];
|
||||
if (Array.isArray(value)) return value.flatMap(legacyInlineContentToTiptap);
|
||||
if (value && typeof value === 'object') {
|
||||
const text = typeof value.text === 'string' ? value.text : '';
|
||||
const payload = value.payload && typeof value.payload === 'object' ? value.payload : null;
|
||||
if (payload?.type === 'hard_break') return [{ type: 'hardBreak' }];
|
||||
const text = typeof value.text === 'string'
|
||||
? value.text
|
||||
: typeof payload?.text === 'string'
|
||||
? payload.text
|
||||
: '';
|
||||
if (text) {
|
||||
const attrs = value.attrs && typeof value.attrs === 'object' ? value.attrs : {};
|
||||
const marks = mergeTiptapMarks(
|
||||
legacyStylesToTiptapMarks(attrs.styles),
|
||||
legacyStylesToTiptapMarks(value.styles),
|
||||
legacyMarkArrayToTiptapMarks(payload?.marks),
|
||||
legacyMarkArrayToTiptapMarks(value.marks)
|
||||
);
|
||||
return [{ type: 'text', text, ...(marks.length ? { marks } : {}) }];
|
||||
@@ -182,7 +192,7 @@ export const legacyBlockToTiptap = (block, index = 0) => {
|
||||
return { type: 'paragraph', attrs: withTextAlign({ blockId }), content: mediaContent };
|
||||
}
|
||||
if (type === 'table') {
|
||||
const tableSnapshot = block?.props?.tiptapTable;
|
||||
const tableSnapshot = block?.props?.tiptapTable || block?.attrs?.tiptapTable;
|
||||
if (tableSnapshot && typeof tableSnapshot === 'object' && tableSnapshot.type === 'table') return tableSnapshot;
|
||||
return {
|
||||
type: 'table',
|
||||
@@ -209,12 +219,13 @@ export const legacyBlockToTiptap = (block, index = 0) => {
|
||||
};
|
||||
}
|
||||
if (type === 'image') {
|
||||
const imageSnapshot = block?.props?.tiptapImage;
|
||||
const imageSnapshot = block?.props?.tiptapImage || block?.attrs?.tiptapImage;
|
||||
if (imageSnapshot && typeof imageSnapshot === 'object' && imageSnapshot.type === 'image') return imageSnapshot;
|
||||
const attrsSource = block?.attrs && typeof block.attrs === 'object' ? block.attrs : {};
|
||||
const attrs = {
|
||||
src: String(block?.props?.src || block?.src || ''),
|
||||
alt: block?.props?.alt || block?.alt || null,
|
||||
title: block?.props?.title || block?.title || null,
|
||||
src: String(block?.props?.src || attrsSource.src || block?.src || ''),
|
||||
alt: block?.props?.alt || attrsSource.alt || block?.alt || null,
|
||||
title: block?.props?.title || attrsSource.title || block?.title || null,
|
||||
};
|
||||
return attrs.src ? { type: 'image', attrs: { blockId, ...attrs } } : null;
|
||||
}
|
||||
@@ -318,7 +329,7 @@ export const localMarkdownDirectoryFromDocumentId = (documentId) => {
|
||||
};
|
||||
|
||||
export const isExternalOrSpecialUrl = (value) => {
|
||||
const text = String(value || '').trim();
|
||||
const text = unwrapMarkdownLinkTarget(value);
|
||||
return !text
|
||||
|| text.startsWith('#')
|
||||
|| text.startsWith('data:')
|
||||
@@ -329,8 +340,16 @@ export const isExternalOrSpecialUrl = (value) => {
|
||||
|| text.startsWith('/api/');
|
||||
};
|
||||
|
||||
export const normalizeLocalAssetRelativePath = (value, context) => {
|
||||
export const unwrapMarkdownLinkTarget = (value) => {
|
||||
const text = String(value || '').trim();
|
||||
if (text.length >= 2 && text.startsWith('<') && text.endsWith('>')) {
|
||||
return text.slice(1, -1).trim();
|
||||
}
|
||||
return text;
|
||||
};
|
||||
|
||||
export const normalizeLocalAssetRelativePath = (value, context) => {
|
||||
const text = unwrapMarkdownLinkTarget(value);
|
||||
if (!text || isExternalOrSpecialUrl(text)) return text;
|
||||
if (text.startsWith('/')) return text.replace(/^\/+/, '');
|
||||
const baseDir = localMarkdownDirectoryFromDocumentId(context?.documentId);
|
||||
|
||||
@@ -13,8 +13,9 @@ function visibleFileTreeRows(deps) {
|
||||
return Array.from(document.querySelectorAll('#sidebar-file-tree-root .tree-row[data-shell-mode="filetree"]'))
|
||||
.filter(function(row) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
if (row.closest('.tree-children--collapsed')) return false;
|
||||
return row.offsetParent !== null || row.getClientRects().length > 0;
|
||||
// 文件树展开恢复会在短时间内多次同步选择状态。这里不能读取
|
||||
// offsetParent/getClientRects,否则会和 DOM 写入交错触发强制布局。
|
||||
return !row.closest('.tree-children--collapsed');
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -448,13 +448,14 @@ async function insertUploadedAssetIntoEditor(asset, targetRoot, deps) {
|
||||
var markdownHref = typeof deps.uploadedAssetMarkdownHref === 'function' ? deps.uploadedAssetMarkdownHref(asset) : uploadedAssetMarkdownHref(asset);
|
||||
var localOpenUrl = typeof deps.localAssetOpenUrl === 'function' ? deps.localAssetOpenUrl(asset, false) : localAssetOpenUrl(asset, false);
|
||||
var fallbackUrl = typeof deps.uploadedAssetUrl === 'function' ? deps.uploadedAssetUrl(asset) : uploadedAssetUrl(asset);
|
||||
var imageUrl = localOpenUrl || fallbackUrl || markdownHref;
|
||||
var url = markdownHref || localOpenUrl || fallbackUrl;
|
||||
var type = typeof deps.uploadedAssetType === 'function' ? deps.uploadedAssetType(asset) : uploadedAssetType(asset);
|
||||
var assetId = String(asset && asset.id || '').trim();
|
||||
var sizeLabel = typeof deps.uploadedFileSize === 'function' ? deps.uploadedFileSize(asset) : uploadedFileSize(asset);
|
||||
try {
|
||||
if (type === 'image' && url) {
|
||||
var imageInserted = editor.chain().focus().setImage({ src: url, alt: title, title: title }).run() === true;
|
||||
if (type === 'image' && imageUrl) {
|
||||
var imageInserted = editor.chain().focus().setImage({ src: imageUrl, alt: title, title: title }).run() === true;
|
||||
if (imageInserted) {
|
||||
document.documentElement.setAttribute('data-mnote-last-upload-inserted', 'true');
|
||||
document.documentElement.setAttribute('data-mnote-last-upload-asset-id', assetId || '');
|
||||
|
||||
@@ -93,7 +93,7 @@ export function createSidebarPageAiProfileRuntime(context) {
|
||||
var baseProfile = String(profile && profile.baseProfile || '').trim();
|
||||
var isolatedProfile = String(profile && profile.isolatedProfile || '').trim();
|
||||
return PAGE_AI_CHAT_ONLY_PROFILE_REGISTRY.find(function(spec) {
|
||||
return spec.profileId === profileId || spec.baseProfile === baseProfile || isolatedProfile.indexOf(spec.baseProfile) >= 0;
|
||||
return spec.profileId === profileId || spec.baseProfile === profileId || spec.baseProfile === baseProfile || isolatedProfile.indexOf(spec.baseProfile) >= 0;
|
||||
}) || null;
|
||||
}
|
||||
|
||||
|
||||
@@ -265,7 +265,9 @@ export function createSidebarPageAiRenderRuntime(context) {
|
||||
var chatOnlyOptions = pageAiChatOnlyProfileEntries().map(function(profile) {
|
||||
var spec = pageAiChatOnlyProfileSpec(profile);
|
||||
var label = profile.menuLabel || (spec && spec.label) || pageAiProfileDisplayLabel(profile, '');
|
||||
return pageAiRenderAgentProfileOption('chat_only', profile, label, '网页问答 · 不申请文件写权限');
|
||||
var providerKind = String(profile.providerKind || (spec && spec.providerKind) || '').trim();
|
||||
var detail = providerKind === 'api-chat' ? 'API 聊天 · 不申请文件写权限' : '网页问答 · 不申请文件写权限';
|
||||
return pageAiRenderAgentProfileOption('chat_only', profile, label, detail);
|
||||
}).join('');
|
||||
var hermesOptions = pageAiHermesProfileEntries().map(function(profile) {
|
||||
var profileId = pageAiProfileValue(profile);
|
||||
|
||||
@@ -33,7 +33,13 @@ export function createSidebarPageAiRuntime(context) {
|
||||
var PAGE_AI_CHAT_ONLY_PROFILE_REGISTRY = [
|
||||
{ profileId: 'shared_deepseek_chat', baseProfile: 'deepseek-chat', label: 'DeepSeek' },
|
||||
{ profileId: 'shared_doubao_chat', baseProfile: 'doubao-chat', label: '豆包' },
|
||||
{ profileId: 'shared_gemini_chat', baseProfile: 'gemini-chat', label: 'Gemini' }
|
||||
{ profileId: 'shared_gemini_chat', baseProfile: 'gemini-chat', label: 'Gemini' },
|
||||
{ profileId: 'shared_api_deepseek_flash_chat', baseProfile: 'api-deepseek-flash-chat', label: 'DeepSeek Flash', providerKind: 'api-chat' },
|
||||
{ profileId: 'shared_api_deepseek_pro_chat', baseProfile: 'api-deepseek-pro-chat', label: 'DeepSeek Pro', providerKind: 'api-chat' },
|
||||
{ profileId: 'shared_api_gpt_chat', baseProfile: 'api-gpt-chat', label: 'GPT', providerKind: 'api-chat' },
|
||||
{ profileId: 'shared_api_kimi_chat', baseProfile: 'api-kimi-chat', label: 'Kimi', providerKind: 'api-chat' },
|
||||
{ profileId: 'shared_api_gemini_chat', baseProfile: 'api-gemini-chat', label: 'Gemini API', providerKind: 'api-chat' },
|
||||
{ profileId: 'shared_api_grok_chat', baseProfile: 'api-grok-chat', label: 'Grok API', providerKind: 'api-chat' }
|
||||
];
|
||||
var PAGE_AI_CONTEXT_REF_REGISTRY = [
|
||||
{ id: 'current_page', label: '当前页' },
|
||||
@@ -731,6 +737,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
alias: spec.label,
|
||||
kind: 'shared',
|
||||
baseProfile: spec.baseProfile,
|
||||
providerKind: spec.providerKind || '',
|
||||
readonly: true
|
||||
}, { menuLabel: spec.label });
|
||||
});
|
||||
@@ -763,6 +770,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
ownerUserId: String(profile && profile.ownerUserId || '').trim(),
|
||||
baseProfile: String(profile && profile.baseProfile || '').trim(),
|
||||
isolatedProfile: String(profile && profile.isolatedProfile || '').trim(),
|
||||
providerKind: String(profile && profile.providerKind || profile && profile.provider_kind || '').trim(),
|
||||
canRun: profile ? profile.canRun !== false : true,
|
||||
canManageSkills: canManageSkills,
|
||||
canManageConfig: profile ? profile.canManageConfig !== false : canManageSkills,
|
||||
|
||||
@@ -67,6 +67,10 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return Object.assign({}, DEFAULT_PAGE_WIDTH_PREFERENCES, pageUiState.pageWidthPreferences || {});
|
||||
}
|
||||
|
||||
function currentLocalOcrPreferences() {
|
||||
return Object.assign({ 'localOcr.autoEnabled': false }, pageUiState.localOcrPreferences || {});
|
||||
}
|
||||
|
||||
function pageWidthModeLabel(mode) {
|
||||
if (mode === 'inherit') return '继承默认';
|
||||
if (mode === 'readable') return '阅读';
|
||||
@@ -357,6 +361,17 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</label>';
|
||||
}
|
||||
|
||||
function createLocalOcrAutoRow() {
|
||||
return '' +
|
||||
'<label class="wolai-page-setting-row" data-page-setting-row="localOcrAutoEnabled">' +
|
||||
'<span class="wolai-page-setting-copy">' +
|
||||
'<span class="wolai-page-setting-label">自动 OCR</span>' +
|
||||
'<span class="wolai-page-setting-hint">默认关闭;仅作为搜索和 AI 索引补充处理图片与图片型 PDF</span>' +
|
||||
'</span>' +
|
||||
'<input type="checkbox" class="wolai-page-setting-checkbox" data-local-ocr-option-checkbox="autoEnabled" />' +
|
||||
'</label>';
|
||||
}
|
||||
|
||||
function createPageWidthSelectRow(type) {
|
||||
var options = type === 'default'
|
||||
? [
|
||||
@@ -397,6 +412,10 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
popover.querySelectorAll('[data-global-option-checkbox="showHeadingNumbers"]').forEach(function(input) {
|
||||
input.checked = globalHeadingNumbers;
|
||||
});
|
||||
var localOcrPreferences = currentLocalOcrPreferences();
|
||||
popover.querySelectorAll('[data-local-ocr-option-checkbox="autoEnabled"]').forEach(function(input) {
|
||||
input.checked = localOcrPreferences['localOcr.autoEnabled'] === true;
|
||||
});
|
||||
var preferences = currentPageWidthPreferences();
|
||||
popover.querySelectorAll('[data-page-width-select]').forEach(function(select) {
|
||||
var type = select.getAttribute('data-page-width-select') || '';
|
||||
@@ -579,6 +598,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
'</div>' +
|
||||
'<div class="wolai-page-settings-section" data-page-settings-panel="global" hidden>' +
|
||||
createGlobalHeadingNumbersRow() +
|
||||
createLocalOcrAutoRow() +
|
||||
createPageWidthRows() +
|
||||
'</div>' +
|
||||
'<div class="wolai-page-settings-actions">' +
|
||||
@@ -843,11 +863,47 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) return;
|
||||
pageUiState.pageWidthPreferences = normalizePageWidthPreferences(payload.result && payload.result.pageWidthPreferences);
|
||||
pageUiState.localOcrPreferences = Object.assign({ 'localOcr.autoEnabled': false }, payload.result && payload.result.localOcrPreferences || {});
|
||||
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, pageUiState.localOcrPreferences);
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-auto-enabled', String(pageUiState.localOcrPreferences['localOcr.autoEnabled'] === true));
|
||||
applyPageOptionsToShell();
|
||||
if (isPageSettingsOpen()) renderPageSettingsPopover();
|
||||
} catch (_) {}
|
||||
}
|
||||
|
||||
async function persistLocalOcrAutoPreference(enabled) {
|
||||
var previous = currentLocalOcrPreferences();
|
||||
var next = Object.assign({}, previous, { 'localOcr.autoEnabled': Boolean(enabled) });
|
||||
pageUiState.localOcrPreferences = next;
|
||||
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, next);
|
||||
renderPageSettingsPopover();
|
||||
try {
|
||||
var response = await fetch('/api/ui/preferences', {
|
||||
method: 'PUT',
|
||||
headers: { 'content-type': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
documentId: currentDocumentId(),
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
...currentWorkspaceSourcePayload(),
|
||||
updates: { 'localOcr.autoEnabled': Boolean(enabled) }
|
||||
})
|
||||
});
|
||||
var payload = await response.json().catch(function(){ return null; });
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(payload && payload.error && payload.error.message ? payload.error.message : 'local_ocr_preference_save_failed_' + response.status);
|
||||
}
|
||||
pageUiState.localOcrPreferences = Object.assign({ 'localOcr.autoEnabled': false }, payload.result && payload.result.localOcrPreferences || {});
|
||||
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, pageUiState.localOcrPreferences);
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-auto-enabled', String(pageUiState.localOcrPreferences['localOcr.autoEnabled'] === true));
|
||||
renderPageSettingsPopover();
|
||||
} catch (error) {
|
||||
pageUiState.localOcrPreferences = previous;
|
||||
window.__MNOTE_LOCAL_OCR_PREFERENCES = Object.assign({}, previous);
|
||||
renderPageSettingsPopover();
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-preference-error', error instanceof Error ? error.message : String(error));
|
||||
}
|
||||
}
|
||||
|
||||
async function persistPageWidthPreference(type, mode) {
|
||||
if (PAGE_WIDTH_TYPES.indexOf(type) < 0) return;
|
||||
var previous = pageUiState.pageWidthPreferences;
|
||||
@@ -928,6 +984,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
closePageHistoryDrawer,
|
||||
closePageSettingsPopover,
|
||||
closePageShareDialog,
|
||||
currentLocalOcrPreferences,
|
||||
currentPageOptions,
|
||||
ensureHistorySnapshotsSeeded,
|
||||
ensurePageHistoryDrawer,
|
||||
@@ -938,6 +995,7 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
openPageSettingsPopover,
|
||||
openPageShareDialog,
|
||||
pageOptionIsSupported,
|
||||
persistLocalOcrAutoPreference,
|
||||
persistPageOptionsPatch,
|
||||
persistPageWidthPreference,
|
||||
recordPageHistorySnapshot,
|
||||
|
||||
@@ -1440,12 +1440,9 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
}
|
||||
}
|
||||
if (fileTreeState.loadingParents.has(key)) {
|
||||
return fileTreeState.loadingParents.get(key).then(function(rows) {
|
||||
fileTreeState.dirtyParents.delete(key);
|
||||
return isFileTreeRootProjectionParent(parentRelativePath)
|
||||
? renderFileProjection({ parentRelativePath: parentRelativePath, items: rows })
|
||||
: patchFileTreeParentChildren(parentRelativePath, rows);
|
||||
});
|
||||
// 变更事件可能正好撞上同一 parent 的懒加载请求。
|
||||
// 旧请求结果不包含刚落盘的文件,不能直接拿它满足本次刷新。
|
||||
await fileTreeState.loadingParents.get(key).catch(function() { return []; });
|
||||
}
|
||||
var projection = await fetchFileTreeProjection(parentRelativePath, { childrenOnly: false });
|
||||
if (!projection) return false;
|
||||
@@ -1556,13 +1553,14 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return '';
|
||||
}
|
||||
|
||||
function setTreeRowExpanded(row, button, expanded) {
|
||||
function setTreeRowExpanded(row, button, expanded, options) {
|
||||
row.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
var shellMode = row.getAttribute('data-shell-mode') || '';
|
||||
if (button) {
|
||||
button.setAttribute('aria-expanded', expanded ? 'true' : 'false');
|
||||
if (shellMode === 'filetree') button.textContent = expanded ? '▾' : '▸';
|
||||
}
|
||||
var shouldPersist = !(options && options.persist === false);
|
||||
if (shellMode === 'page') {
|
||||
var nodeId = String(row.getAttribute('data-node-id') || '').trim();
|
||||
if (!nodeId) return;
|
||||
@@ -1570,18 +1568,20 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
if (expanded) pageState.expandedIds.add(nodeId);
|
||||
else pageState.expandedIds.delete(nodeId);
|
||||
pageState.hasUserState = true;
|
||||
persistSidebarTreeViewState('pagetree');
|
||||
if (shouldPersist) persistSidebarTreeViewState('pagetree');
|
||||
return;
|
||||
}
|
||||
var relativePath = localFileTreeRelativePathFromRow(row);
|
||||
if (!relativePath) return;
|
||||
if (expanded) fileTreeExpandedRelativePaths.add(relativePath);
|
||||
else fileTreeExpandedRelativePaths.delete(relativePath);
|
||||
persistFileTreeExpansionState();
|
||||
persistSidebarTreeViewState('filetree');
|
||||
if (shouldPersist) {
|
||||
persistFileTreeExpansionState();
|
||||
persistSidebarTreeViewState('filetree');
|
||||
}
|
||||
}
|
||||
|
||||
function markExistingFileTreeChildrenLoaded(row, button) {
|
||||
function markExistingFileTreeChildrenLoaded(row, button, options) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
var node = row.closest('.tree-node');
|
||||
if (!node) return false;
|
||||
@@ -1589,12 +1589,12 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
if (!(children instanceof HTMLElement)) return false;
|
||||
row.setAttribute('data-filetree-children-loaded', 'true');
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
setTreeRowExpanded(row, button, true, options);
|
||||
syncSidebarFileTreeSelection();
|
||||
return true;
|
||||
}
|
||||
|
||||
function renderCachedFileTreeChildren(row, button, relativePath) {
|
||||
function renderCachedFileTreeChildren(row, button, relativePath, options) {
|
||||
var key = currentFileTreeParentKey(relativePath);
|
||||
if (fileTreeState.staleParents.has(key) || fileTreeState.dirtyParents.has(key)) return false;
|
||||
var cachedRows = cachedFileTreeRows(relativePath);
|
||||
@@ -1612,7 +1612,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
children.replaceChildren(template.content.cloneNode(true));
|
||||
children.classList.remove('tree-children--collapsed');
|
||||
row.setAttribute('data-filetree-children-loaded', 'true');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
setTreeRowExpanded(row, button, true, options);
|
||||
syncSidebarFileTreeSelection();
|
||||
return true;
|
||||
}
|
||||
@@ -1641,7 +1641,7 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
return promise;
|
||||
}
|
||||
|
||||
async function loadFileTreeChildren(row, button) {
|
||||
async function loadFileTreeChildren(row, button, options) {
|
||||
if (!(row instanceof HTMLElement)) return false;
|
||||
if (row.getAttribute('data-shell-mode') !== 'filetree') return false;
|
||||
if (currentSourceKind() !== 'local_folder') return false;
|
||||
@@ -1649,21 +1649,21 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
var relativePath = localFileTreeRelativePathFromRow(row);
|
||||
var key = currentFileTreeParentKey(relativePath);
|
||||
var stale = fileTreeState.staleParents.has(key) || fileTreeState.dirtyParents.has(key);
|
||||
if (!stale && markExistingFileTreeChildrenLoaded(row, button)) return true;
|
||||
if (!stale && relativePath && renderCachedFileTreeChildren(row, button, relativePath)) return true;
|
||||
if (!stale && markExistingFileTreeChildrenLoaded(row, button, options)) return true;
|
||||
if (!stale && relativePath && renderCachedFileTreeChildren(row, button, relativePath, options)) return true;
|
||||
if (row.getAttribute('data-filetree-children-loaded') === 'true') return false;
|
||||
var rootUri = currentRootUri();
|
||||
if (!rootUri || !relativePath) return false;
|
||||
setTreeRowExpanded(row, button, true);
|
||||
setTreeRowExpanded(row, button, true, options);
|
||||
row.setAttribute('data-filetree-children-loading', 'true');
|
||||
try {
|
||||
var rows = await getFileTreeChildren(relativePath);
|
||||
if (!rows.length) {
|
||||
row.setAttribute('data-filetree-children-loaded', 'true');
|
||||
setTreeRowExpanded(row, button, true);
|
||||
setTreeRowExpanded(row, button, true, options);
|
||||
return true;
|
||||
}
|
||||
return renderCachedFileTreeChildren(row, button, relativePath);
|
||||
return renderCachedFileTreeChildren(row, button, relativePath, options);
|
||||
} catch (error) {
|
||||
setTreeLiveApplyError(error && error.message ? error.message : '文件树子目录加载失败');
|
||||
return false;
|
||||
@@ -1731,6 +1731,15 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
setTreeRowExpanded(row, button, !collapsed);
|
||||
}
|
||||
|
||||
function shouldAutoRestoreFileTreeExpansion(relativePath) {
|
||||
var normalized = normalizeFileTreeRelativePath(relativePath);
|
||||
if (!normalized) return false;
|
||||
var scope = normalizeFileTreeRelativePath(currentFileTreeScope());
|
||||
if (!scope) return normalized.indexOf('/') < 0;
|
||||
if (normalized.indexOf(scope + '/') !== 0) return false;
|
||||
return normalized.slice(scope.length + 1).indexOf('/') < 0;
|
||||
}
|
||||
|
||||
function restorePersistedFileTreeExpansionState() {
|
||||
if (currentSourceKind() !== 'local_folder') return false;
|
||||
ensureFileTreeLazyCacheScope();
|
||||
@@ -1741,17 +1750,20 @@ export const createSidebarTreeLiveApplyRuntime = (dependencies = {}) => {
|
||||
if (String(row.getAttribute('data-row-kind') || '').trim() !== 'folder') return;
|
||||
var relativePath = localFileTreeRelativePathFromRow(row);
|
||||
if (!relativePath || !fileTreeExpandedRelativePaths.has(relativePath)) return;
|
||||
// 首屏只自动恢复当前 scope 的直接子目录。历史 view-state 可能记录了
|
||||
// 多层 design 目录展开;一次性递归恢复会长时间占满浏览器主线程。
|
||||
if (!shouldAutoRestoreFileTreeExpansion(relativePath)) return;
|
||||
var button = row.querySelector('[data-rust-action="toggle"]');
|
||||
if (markExistingFileTreeChildrenLoaded(row, button)) {
|
||||
if (markExistingFileTreeChildrenLoaded(row, button, { persist: false })) {
|
||||
restored = true;
|
||||
return;
|
||||
}
|
||||
if (row.getAttribute('data-filetree-children-loaded') === 'true') {
|
||||
setTreeRowExpanded(row, button, true);
|
||||
setTreeRowExpanded(row, button, true, { persist: false });
|
||||
restored = true;
|
||||
return;
|
||||
}
|
||||
void loadFileTreeChildren(row, button).then(function(loaded) {
|
||||
void loadFileTreeChildren(row, button, { persist: false }).then(function(loaded) {
|
||||
if (loaded) restorePersistedFileTreeExpansionState();
|
||||
});
|
||||
restored = true;
|
||||
|
||||
@@ -208,6 +208,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
const openPageHistoryDrawer = (...args) => sidebarPageSettings.openPageHistoryDrawer(...args);
|
||||
const openPageShareDialog = (...args) => sidebarPageSettings.openPageShareDialog(...args);
|
||||
const pageOptionIsSupported = (...args) => sidebarPageSettings.pageOptionIsSupported(...args);
|
||||
const persistLocalOcrAutoPreference = (...args) => sidebarPageSettings.persistLocalOcrAutoPreference(...args);
|
||||
const persistPageOptionsPatch = (...args) => sidebarPageSettings.persistPageOptionsPatch(...args);
|
||||
const persistPageWidthPreference = (...args) => sidebarPageSettings.persistPageWidthPreference(...args);
|
||||
const recordPageHistorySnapshot = (...args) => sidebarPageSettings.recordPageHistorySnapshot(...args);
|
||||
@@ -441,6 +442,11 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
return title && title.textContent ? title.textContent.trim() : (fallback || '文件夹');
|
||||
}
|
||||
|
||||
function isLocalOcrMarkdownPath(relativePath) {
|
||||
var normalized = String(relativePath || '').trim().replace(/\\/g, '/').replace(/^\/+/, '');
|
||||
return /(^|\/)[^/]+\.ocr\/[^/]+\.ocr\.md$/i.test(normalized);
|
||||
}
|
||||
|
||||
function sidebarShortcutRows() {
|
||||
return Array.from(document.querySelectorAll('.wolai-starred-section [data-mnote-shortcut-kind]'));
|
||||
}
|
||||
@@ -1714,13 +1720,16 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
}
|
||||
var title = uploadedAssetTitle(asset);
|
||||
var markdownHref = uploadedAssetMarkdownHref(asset);
|
||||
var url = markdownHref || localAssetOpenUrl(asset, false) || uploadedAssetUrl(asset);
|
||||
var localOpenUrl = localAssetOpenUrl(asset, false);
|
||||
var fallbackUrl = uploadedAssetUrl(asset);
|
||||
var imageUrl = localOpenUrl || fallbackUrl || markdownHref;
|
||||
var url = markdownHref || localOpenUrl || fallbackUrl;
|
||||
var type = uploadedAssetType(asset);
|
||||
var assetId = String(asset && asset.id || '').trim();
|
||||
var sizeLabel = uploadedFileSize(asset);
|
||||
try {
|
||||
if (type === 'image' && url) {
|
||||
return editor.chain().focus().setImage({ src: url, alt: title, title: title }).run() === true;
|
||||
if (type === 'image' && imageUrl) {
|
||||
return editor.chain().focus().setImage({ src: imageUrl, alt: title, title: title }).run() === true;
|
||||
}
|
||||
var href = markdownHref || url;
|
||||
if (href) {
|
||||
@@ -2681,10 +2690,34 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
selectSidebarFileTreeRow(fileRow, { ctrlKey: e.ctrlKey, metaKey: e.metaKey, shiftKey: e.shiftKey });
|
||||
var objectIdentity = readFileTreeObjectIdentity(fileRow);
|
||||
var workspacePath = readWorkspacePathFromRow(fileRow);
|
||||
var localRelativePath = fileTreeRowLocalRelativePath(fileRow) || String(workspacePath && (workspacePath.relativePath || workspacePath.localRelativePath) || '').trim();
|
||||
dispatchSidebarEvent('tree.filetree.open', { rowId: rowId, rowKind: rowKind, documentId: ownerDocumentId || null, assetId: assetId || null, assetType: assetType || null, objectIdentity: objectIdentity, workspacePath: workspacePath, workspaceId: resolveWorkspaceId(fileRow) });
|
||||
if (e.shiftKey || e.ctrlKey || e.metaKey) {
|
||||
return;
|
||||
}
|
||||
if ((rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') && isLocalOcrMarkdownPath(localRelativePath)) {
|
||||
document.documentElement.setAttribute('data-mnote-local-ocr-filetree-open', 'resource-tab');
|
||||
var ocrResourceInput = {
|
||||
path: localRelativePath,
|
||||
title: fileTreeRowTitleForShortcut(fileRow, localRelativePath),
|
||||
kind: 'markdown',
|
||||
objectIdentity: 'local-ocr:' + localRelativePath,
|
||||
assetId: 'local-ocr:' + localRelativePath,
|
||||
documentId: documentId || ownerDocumentId || null,
|
||||
workspaceId: resolveWorkspaceId(fileRow),
|
||||
sourceKind: 'local_folder',
|
||||
rootUri: currentRootUri(),
|
||||
resourceKind: 'markdown',
|
||||
workspacePath: workspacePath,
|
||||
paneRole: 'primary'
|
||||
};
|
||||
if (typeof window.__mnoteDocumentPaneRuntime?.openResourceInActiveTab === 'function') {
|
||||
void window.__mnoteDocumentPaneRuntime.openResourceInActiveTab(ocrResourceInput);
|
||||
} else {
|
||||
void openLocalResourceInActiveTab(ocrResourceInput);
|
||||
}
|
||||
return;
|
||||
}
|
||||
if ((rowKind === 'document' || rowKind === 'index' || rowKind === 'markdown') && documentId) {
|
||||
void recordNavigationRecent({
|
||||
kind: 'page',
|
||||
@@ -2839,6 +2872,11 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
renderPageSettingsPopover();
|
||||
return;
|
||||
}
|
||||
var localOcrCheckbox = closestAction(event.target, '[data-local-ocr-option-checkbox="autoEnabled"]');
|
||||
if (localOcrCheckbox instanceof HTMLInputElement) {
|
||||
void persistLocalOcrAutoPreference(localOcrCheckbox.checked);
|
||||
return;
|
||||
}
|
||||
var checkbox = closestAction(event.target, '[data-page-option-checkbox]');
|
||||
if (checkbox instanceof HTMLInputElement) {
|
||||
var key = checkbox.getAttribute('data-page-option-checkbox') || '';
|
||||
|
||||
Reference in New Issue
Block a user