收口 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);
|
||||
};
|
||||
|
||||
|
||||
@@ -463,6 +463,7 @@ export function createSidebarPageAiRuntime(context) {
|
||||
const pageAiWorkspacePathForTarget = (...args) => pageAiTargetRuntime.pageAiWorkspacePathForTarget(...args);
|
||||
const pageAiBuildAllowedRoots = (...args) => pageAiTargetRuntime.pageAiBuildAllowedRoots(...args);
|
||||
const pageAiBuildContextRefs = (...args) => pageAiTargetRuntime.pageAiBuildContextRefs(...args);
|
||||
const pageAiEnrichOcrContextRefs = (...args) => pageAiTargetRuntime.pageAiEnrichOcrContextRefs(...args);
|
||||
const pageAiBuildRunTargetSnapshot = (...args) => pageAiTargetRuntime.pageAiBuildRunTargetSnapshot(...args);
|
||||
const pageAiPageContextForRefs = (...args) => pageAiTargetRuntime.pageAiPageContextForRefs(...args);
|
||||
const pageAiSetRunTargetSnapshot = (...args) => pageAiTargetRuntime.pageAiSetRunTargetSnapshot(...args);
|
||||
@@ -1828,6 +1829,11 @@ export function createSidebarPageAiRuntime(context) {
|
||||
var contextRefs = pageAiBuildContextRefs(scopedContext, runTargetSnapshot);
|
||||
var allowedRoots = pageAiBuildAllowedRoots();
|
||||
var agentTargetPackage = pageAiBuildAgentTargetPackage(scopedContext, runTargetSnapshot);
|
||||
if (typeof pageAiEnrichOcrContextRefs === 'function') {
|
||||
var ocrContext = await pageAiEnrichOcrContextRefs(contextRefs, agentTargetPackage, scopedContext.editorTarget);
|
||||
contextRefs = ocrContext.contextRefs || contextRefs;
|
||||
agentTargetPackage = ocrContext.agentTargetPackage || agentTargetPackage;
|
||||
}
|
||||
if (scopedContext.pageContext && scopedContext.pageContext.aiContext) {
|
||||
scopedContext.pageContext.aiContext.runTargetSnapshot = runTargetSnapshot;
|
||||
scopedContext.pageContext.aiContext.agentTargetPackage = agentTargetPackage;
|
||||
|
||||
@@ -412,6 +412,84 @@ export function createSidebarPageAiTargetRuntime(context) {
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiOcrEligibleResourceKind(value) {
|
||||
var normalized = String(value || '').trim().toLowerCase();
|
||||
return normalized === 'image' || normalized === 'pdf' || normalized === 'attachment' || normalized === 'resource';
|
||||
}
|
||||
|
||||
function pageAiOcrEligiblePath(value) {
|
||||
var path = String(value || '').trim().toLowerCase();
|
||||
return /\.(png|jpg|jpeg|webp|bmp|tif|tiff|pdf)$/.test(path);
|
||||
}
|
||||
|
||||
function pageAiOcrBodyPreview(markdown) {
|
||||
var body = String(markdown || '').replace(/^---\n[\s\S]*?\n---\n?/, '').trim();
|
||||
return body.slice(0, 1600);
|
||||
}
|
||||
|
||||
async function fetchPageAiOcrSidecarContext(editorTarget) {
|
||||
if (currentSourceKind() !== 'local_folder') return null;
|
||||
var target = editorTarget && typeof editorTarget === 'object' ? editorTarget : {};
|
||||
var workspacePath = target.workspacePath && typeof target.workspacePath === 'object' ? target.workspacePath : {};
|
||||
var relativePath = String(workspacePath.relativePath || target.path || '').trim();
|
||||
var resourceKind = String(target.resourceKind || target.editorKind || workspacePath.resourceKind || '').trim();
|
||||
if (!relativePath || !pageAiOcrEligiblePath(relativePath) || !pageAiOcrEligibleResourceKind(resourceKind)) return null;
|
||||
var rootUri = String(workspacePath.rootUri || currentRootUri() || '').trim();
|
||||
if (!rootUri) return null;
|
||||
try {
|
||||
var statusUrl = new URL('/api/local-folder/ocr/status', window.location.origin);
|
||||
statusUrl.searchParams.set('rootUri', rootUri);
|
||||
statusUrl.searchParams.set('sourceRootRelativePath', relativePath);
|
||||
var statusResponse = await fetch(statusUrl.toString(), { cache: 'no-store', headers: { accept: 'application/json' } });
|
||||
var statusPayload = await statusResponse.json().catch(function() { return null; });
|
||||
var job = statusResponse.ok && statusPayload && statusPayload.ok === true ? statusPayload.job : null;
|
||||
var ocrPath = String(job && job.ocrRootRelativePath || '').trim();
|
||||
if (!ocrPath || String(job.status || '') === 'failed') return null;
|
||||
var readUrl = new URL('/api/local-folder/ocr/read', window.location.origin);
|
||||
readUrl.searchParams.set('rootUri', rootUri);
|
||||
readUrl.searchParams.set('ocrRootRelativePath', ocrPath);
|
||||
var readResponse = await fetch(readUrl.toString(), { cache: 'no-store', headers: { accept: 'application/json' } });
|
||||
var readPayload = await readResponse.json().catch(function() { return null; });
|
||||
if (!readResponse.ok || !readPayload || readPayload.ok !== true) return null;
|
||||
return {
|
||||
schema: 'mnote.local_ocr_context.v1',
|
||||
source: 'local_ocr_sidecar',
|
||||
sourceRootRelativePath: relativePath,
|
||||
ocrRootRelativePath: ocrPath,
|
||||
status: String(job.status || ''),
|
||||
stale: job.stale === true,
|
||||
provider: String(job.provider || ''),
|
||||
modelVersion: String(job.modelVersion || ''),
|
||||
plainTextPreview: pageAiOcrBodyPreview(readPayload.markdown || ''),
|
||||
};
|
||||
} catch (_) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
async function pageAiEnrichOcrContextRefs(contextRefs, agentTargetPackage, editorTarget) {
|
||||
var ocrContext = await fetchPageAiOcrSidecarContext(editorTarget);
|
||||
if (!ocrContext) return { contextRefs, agentTargetPackage };
|
||||
var nextRefs = pageAiNormalizeArray(contextRefs).map(function(ref) {
|
||||
if (ref && ref.kind === 'active_editor') return Object.assign({}, ref, { ocrContext: ocrContext });
|
||||
return ref;
|
||||
});
|
||||
var nextPackage = agentTargetPackage && typeof agentTargetPackage === 'object'
|
||||
? Object.assign({}, agentTargetPackage, { ocrContext: ocrContext })
|
||||
: agentTargetPackage;
|
||||
if (nextPackage && nextPackage.currentFile && typeof nextPackage.currentFile === 'object') {
|
||||
nextPackage.currentFile = Object.assign({}, nextPackage.currentFile, { ocrRootRelativePath: ocrContext.ocrRootRelativePath });
|
||||
}
|
||||
if (nextPackage && Array.isArray(nextPackage.targets)) {
|
||||
nextPackage.targets = nextPackage.targets.map(function(target, index) {
|
||||
return index === 0 && target && typeof target === 'object'
|
||||
? Object.assign({}, target, { ocrContext: ocrContext })
|
||||
: target;
|
||||
});
|
||||
}
|
||||
return { contextRefs: nextRefs, agentTargetPackage: nextPackage };
|
||||
}
|
||||
|
||||
function pageAiBuildAllowedRoots() {
|
||||
return pageAiNormalizeArray(pageUiState.pageAiAllowedRoots).map(function(root) {
|
||||
return {
|
||||
@@ -733,6 +811,7 @@ export function createSidebarPageAiTargetRuntime(context) {
|
||||
pageAiBuildAgentTargetPackage,
|
||||
pageAiBuildAllowedRoots,
|
||||
pageAiBuildContextRefs,
|
||||
pageAiEnrichOcrContextRefs,
|
||||
pageAiBuildRunTargetSnapshot,
|
||||
pageAiCloneJson,
|
||||
pageAiContextKindsFromRefs,
|
||||
|
||||
Reference in New Issue
Block a user