收口 MinerU OCR 任务链与上下文
This commit is contained in:
@@ -1039,6 +1039,169 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
return payload.job || null;
|
||||
};
|
||||
|
||||
const localOcrTaskState = {
|
||||
rootUri: '',
|
||||
jobsBySource: new Map(),
|
||||
drawerOpen: false,
|
||||
eventSource: null,
|
||||
};
|
||||
|
||||
const localOcrJobKey = (job) => String(job?.sourceRootRelativePath || job?.jobId || '').trim();
|
||||
|
||||
const updateLocalOcrTaskState = (job) => {
|
||||
const key = localOcrJobKey(job);
|
||||
if (!key) return;
|
||||
localOcrTaskState.jobsBySource.set(key, job);
|
||||
renderLocalOcrTaskDock();
|
||||
};
|
||||
|
||||
const statusTextForLocalOcrJob = (job) => {
|
||||
const status = String(job?.status || '').trim();
|
||||
if (job?.stageLabel) return String(job.stageLabel);
|
||||
return status === 'done' ? '已识别'
|
||||
: status === 'failed' ? '识别失败'
|
||||
: status === 'stale' ? '来源已变化'
|
||||
: status === 'running' ? '处理中'
|
||||
: status || '未知';
|
||||
};
|
||||
|
||||
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();
|
||||
});
|
||||
}
|
||||
dock.addEventListener('click', (event) => {
|
||||
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') || '';
|
||||
const job = localOcrTaskState.jobsBySource.get(sourcePath) || null;
|
||||
if (job) {
|
||||
void openResourceInActiveTab({
|
||||
kind: 'markdown',
|
||||
title: String(job.ocrRootRelativePath || '').split('/').filter(Boolean).pop() || 'OCR',
|
||||
path: job.ocrRootRelativePath,
|
||||
objectIdentity: `local-ocr:${job.ocrRootRelativePath}`,
|
||||
assetId: `local-ocr:${job.ocrRootRelativePath}`,
|
||||
documentId: job.ownerDocumentId || currentWebShellDocumentId() || '',
|
||||
ownerDocumentId: job.ownerDocumentId || currentWebShellDocumentId() || '',
|
||||
workspaceId: currentWebShellWorkspaceId() || '',
|
||||
sourceKind: 'local_folder',
|
||||
rootUri: localOcrTaskState.rootUri || '',
|
||||
resourceKind: 'markdown',
|
||||
}).catch((error) => console.warn('mnote local OCR 任务打开失败', error));
|
||||
}
|
||||
}
|
||||
const retryButton = event.target instanceof Element ? event.target.closest('[data-mnote-local-ocr-task-retry]') : null;
|
||||
if (retryButton instanceof HTMLElement) {
|
||||
const sourcePath = retryButton.getAttribute('data-mnote-local-ocr-task-retry') || '';
|
||||
const job = localOcrTaskState.jobsBySource.get(sourcePath) || null;
|
||||
if (job) {
|
||||
const entry = {
|
||||
sourceKind: 'local_folder',
|
||||
rootUri: localOcrTaskState.rootUri,
|
||||
path: sourcePath,
|
||||
kind: sourcePath.toLowerCase().endsWith('.pdf') ? 'pdf' : 'image',
|
||||
title: sourcePath.split('/').filter(Boolean).pop() || sourcePath,
|
||||
documentId: job.ownerDocumentId || currentWebShellDocumentId() || '',
|
||||
ownerDocumentId: job.ownerDocumentId || currentWebShellDocumentId() || '',
|
||||
workspaceId: currentWebShellWorkspaceId() || '',
|
||||
localOcrJob: job,
|
||||
};
|
||||
void createLocalOcrJob(entry).catch((error) => console.warn('mnote local OCR 任务重试失败', error));
|
||||
}
|
||||
}
|
||||
});
|
||||
return dock;
|
||||
};
|
||||
|
||||
const renderLocalOcrTaskDock = () => {
|
||||
const dock = ensureLocalOcrTaskDock();
|
||||
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"]');
|
||||
if (toggle instanceof HTMLButtonElement) {
|
||||
toggle.textContent = runningCount > 0 ? `OCR ${runningCount} 处理中` : `OCR ${jobs.length}`;
|
||||
toggle.setAttribute('aria-expanded', localOcrTaskState.drawerOpen ? 'true' : 'false');
|
||||
}
|
||||
const drawer = dock.querySelector('[data-testid="mnote-local-ocr-task-drawer"]');
|
||||
if (drawer instanceof HTMLElement) drawer.hidden = !localOcrTaskState.drawerOpen;
|
||||
const list = dock.querySelector('[data-testid="mnote-local-ocr-task-list"]');
|
||||
if (!(list instanceof HTMLElement)) return;
|
||||
list.replaceChildren();
|
||||
if (!jobs.length) {
|
||||
const empty = document.createElement('div');
|
||||
empty.className = 'mnote-local-ocr-task-empty';
|
||||
empty.textContent = '暂无 OCR 任务';
|
||||
list.appendChild(empty);
|
||||
return;
|
||||
}
|
||||
jobs.forEach((job) => {
|
||||
const row = document.createElement('div');
|
||||
row.className = 'mnote-local-ocr-task-row';
|
||||
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.querySelector('strong').textContent = title;
|
||||
row.querySelector('span').textContent = statusTextForLocalOcrJob(job);
|
||||
const open = row.querySelector('[data-mnote-local-ocr-task-open]');
|
||||
if (open instanceof HTMLButtonElement) {
|
||||
open.setAttribute('data-mnote-local-ocr-task-open', String(job.sourceRootRelativePath || ''));
|
||||
open.disabled = !job.ocrRootRelativePath;
|
||||
}
|
||||
const retry = row.querySelector('[data-mnote-local-ocr-task-retry]');
|
||||
if (retry instanceof HTMLButtonElement) {
|
||||
retry.setAttribute('data-mnote-local-ocr-task-retry', String(job.sourceRootRelativePath || ''));
|
||||
retry.hidden = !['failed', 'stale'].includes(String(job.status || ''));
|
||||
}
|
||||
list.appendChild(row);
|
||||
});
|
||||
};
|
||||
|
||||
const loadLocalOcrJobs = async (rootUri) => {
|
||||
const normalizedRoot = String(rootUri || '').trim();
|
||||
if (!normalizedRoot) return;
|
||||
localOcrTaskState.rootUri = normalizedRoot;
|
||||
const url = new URL('/api/local-folder/ocr/jobs', window.location.origin);
|
||||
url.searchParams.set('rootUri', normalizedRoot);
|
||||
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);
|
||||
renderLocalOcrTaskDock();
|
||||
};
|
||||
|
||||
const ensureLocalOcrTaskEvents = (rootUri) => {
|
||||
const normalizedRoot = String(rootUri || '').trim();
|
||||
if (!normalizedRoot || typeof window.EventSource !== 'function') return;
|
||||
if (localOcrTaskState.eventSource && localOcrTaskState.rootUri === normalizedRoot) return;
|
||||
if (localOcrTaskState.eventSource) {
|
||||
try { localOcrTaskState.eventSource.close(); } catch (_) {}
|
||||
localOcrTaskState.eventSource = null;
|
||||
}
|
||||
localOcrTaskState.rootUri = normalizedRoot;
|
||||
const url = new URL('/api/local-folder/events', window.location.origin);
|
||||
url.searchParams.set('rootUri', normalizedRoot);
|
||||
const eventSource = new EventSource(url.toString());
|
||||
localOcrTaskState.eventSource = eventSource;
|
||||
eventSource.addEventListener('local_ocr.job.updated', (event) => {
|
||||
let payload = null;
|
||||
try { payload = JSON.parse(event.data || '{}'); } catch (_) {}
|
||||
if (payload?.job) updateLocalOcrTaskState(payload.job);
|
||||
});
|
||||
};
|
||||
|
||||
const openLocalOcrSidecar = async (entry, job) => {
|
||||
const target = job || entry?.localOcrJob || null;
|
||||
const ocrPath = String(target?.ocrRootRelativePath || '').trim();
|
||||
@@ -1108,6 +1271,7 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
}
|
||||
const job = payload.job || null;
|
||||
setLocalOcrStatus(entry, String(job?.status || 'done'), job?.stale ? 'OCR 需更新' : 'OCR 已完成', job);
|
||||
if (job) updateLocalOcrTaskState(job);
|
||||
return job;
|
||||
};
|
||||
|
||||
@@ -1146,9 +1310,13 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
});
|
||||
}
|
||||
setLocalOcrStatus(entry, 'idle', 'OCR 未生成', null);
|
||||
ensureLocalOcrTaskDock();
|
||||
ensureLocalOcrTaskEvents(entry.rootUri);
|
||||
void loadLocalOcrJobs(entry.rootUri).catch(() => undefined);
|
||||
void readLocalOcrStatus(entry).then((job) => {
|
||||
if (!job) return;
|
||||
setLocalOcrStatus(entry, job.stale ? 'stale' : String(job.status || 'done'), job.stale ? 'OCR 需更新' : 'OCR 已完成', job);
|
||||
updateLocalOcrTaskState(job);
|
||||
}).catch(() => undefined);
|
||||
};
|
||||
|
||||
|
||||
Reference in New Issue
Block a user