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:
lix-2026
2026-06-02 17:17:49 +08:00
parent 610b23d3a5
commit 9f4b5c4d48
27 changed files with 3825 additions and 229 deletions
@@ -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,