chore: harden lightrag knowledge rag runtime
This commit is contained in:
@@ -30,6 +30,9 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
ppt: { mode: 'wide', resolvedMode: 'wide', cssMaxWidth: '1180px', source: 'system' },
|
||||
mindmap: { mode: 'full', resolvedMode: 'full', cssMaxWidth: 'none', source: 'system' }
|
||||
};
|
||||
var knowledgeRagPipelineEventSource = null;
|
||||
var knowledgeRagPipelineWatcherScopeKey = '';
|
||||
var knowledgeRagPipelineWatcherRefreshPending = false;
|
||||
|
||||
function defaultPageOptions() {
|
||||
return {
|
||||
@@ -71,11 +74,6 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return pageUiState.knowledgeRagSummary || {};
|
||||
}
|
||||
|
||||
var knowledgeRagBridgeTimer = 0;
|
||||
var knowledgeRagBridgeGeneration = 0;
|
||||
var knowledgeRagBridgeAttempt = 0;
|
||||
var knowledgeRagBridgeDelays = [1200, 2000, 3200, 5000, 8000, 13000, 21000, 34000];
|
||||
|
||||
function pageWidthModeLabel(mode) {
|
||||
if (mode === 'inherit') return '继承默认';
|
||||
if (mode === 'readable') return '阅读';
|
||||
@@ -1099,21 +1097,85 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}
|
||||
|
||||
function knowledgeRagStatusHasInFlight(status) {
|
||||
var pipeline = status && status.pipeline && typeof status.pipeline === 'object' ? status.pipeline : {};
|
||||
if (pipeline.busy === true || pipeline.scanning === true || pipeline.destructiveBusy === true || pipeline.requestPending === true || pipeline.pendingRequests === true || Number(pipeline.pendingEnqueues || 0) > 0) return true;
|
||||
var registry = status && status.registry && typeof status.registry === 'object' ? status.registry : {};
|
||||
var entries = Array.isArray(registry.entries) ? registry.entries : [];
|
||||
if (entries.some(function(entry) {
|
||||
var providerStatus = String(entry && entry.lightRagStatus || '').trim();
|
||||
return !entry?.deletedAtMs && !entry?.stale && !(entry?.indexedAtMs && entry?.lightRagDocId)
|
||||
&& ['submitted', 'pending', 'processing', 'parsed', ''].includes(providerStatus);
|
||||
&& ['submitted', 'pending', 'scanning', 'parsing', 'parse', 'parsed', 'analyzing', 'analyze', 'processing', 'process', ''].includes(providerStatus);
|
||||
})) return true;
|
||||
var groups = status && status.documents && status.documents.rawStatusGroups && typeof status.documents.rawStatusGroups === 'object'
|
||||
? status.documents.rawStatusGroups
|
||||
: {};
|
||||
return ['pending', 'parse', 'parsed', 'analyze', 'processing', 'process'].some(function(key) {
|
||||
return ['pending', 'scanning', 'parsing', 'parse', 'parsed', 'analyzing', 'analyze', 'processing', 'process'].some(function(key) {
|
||||
return Number(groups[key] || 0) > 0;
|
||||
});
|
||||
}
|
||||
|
||||
function knowledgeRagPipelineHasInFlight(pipeline) {
|
||||
pipeline = pipeline && typeof pipeline === 'object' ? pipeline : {};
|
||||
return pipeline.busy === true
|
||||
|| pipeline.scanning === true
|
||||
|| pipeline.destructiveBusy === true
|
||||
|| pipeline.requestPending === true
|
||||
|| pipeline.pendingRequests === true
|
||||
|| Number(pipeline.pendingEnqueues || 0) > 0;
|
||||
}
|
||||
|
||||
function parseKnowledgeRagChunkProgress(message) {
|
||||
var text = String(message || '');
|
||||
var match = text.match(/Chunk\s+(\d+)\s+of\s+(\d+).*?\b(doc-[A-Za-z0-9_-]+?)(?=-(?:chunk|mm)-|\s|$)/i);
|
||||
if (!match) return null;
|
||||
var current = Number(match[1]);
|
||||
var total = Number(match[2]);
|
||||
if (!Number.isFinite(current) || !Number.isFinite(total) || current <= 0 || total <= 0) return null;
|
||||
return {
|
||||
current: Math.min(current, total),
|
||||
total: total,
|
||||
docId: String(match[3] || ''),
|
||||
latestMessage: text
|
||||
};
|
||||
}
|
||||
|
||||
function normalizeKnowledgeRagPipelineEvent(payload) {
|
||||
var input = payload && typeof payload === 'object' ? payload : {};
|
||||
var latestMessage = String(input.latestMessage || input.latest_message || '');
|
||||
var historyMessages = Array.isArray(input.historyMessages)
|
||||
? input.historyMessages
|
||||
: (Array.isArray(input.history_messages) ? input.history_messages : []);
|
||||
var progress = input.progress && typeof input.progress === 'object'
|
||||
? input.progress
|
||||
: parseKnowledgeRagChunkProgress(latestMessage);
|
||||
if (!progress) {
|
||||
for (var index = historyMessages.length - 1; index >= 0; index -= 1) {
|
||||
progress = parseKnowledgeRagChunkProgress(historyMessages[index]);
|
||||
if (progress) break;
|
||||
}
|
||||
}
|
||||
return {
|
||||
ok: input.ok !== false,
|
||||
busy: input.busy === true,
|
||||
destructiveBusy: input.destructiveBusy === true || input.destructive_busy === true,
|
||||
scanning: input.scanning === true,
|
||||
scanningExclusive: input.scanningExclusive === true || input.scanning_exclusive === true,
|
||||
requestPending: input.requestPending === true || input.request_pending === true,
|
||||
pendingRequests: input.pendingRequests === true || input.pending_requests === true,
|
||||
pendingEnqueues: Number(input.pendingEnqueues ?? input.pending_enqueues ?? 0) || 0,
|
||||
docs: Number(input.docs || 0) || 0,
|
||||
batches: Number(input.batches ?? input.batchs ?? 0) || 0,
|
||||
currentBatch: Number(input.currentBatch ?? input.cur_batch ?? 0) || 0,
|
||||
jobName: String(input.jobName || input.job_name || ''),
|
||||
jobStart: input.jobStart || input.job_start || null,
|
||||
latestMessage: latestMessage,
|
||||
historyMessages: historyMessages.map(function(message) { return String(message || ''); }),
|
||||
cancellationRequested: input.cancellationRequested === true || input.cancellation_requested === true,
|
||||
cancellationReason: input.cancellationReason || input.cancellation_reason || null,
|
||||
progress: progress || null
|
||||
};
|
||||
}
|
||||
|
||||
function knowledgeRagPipelineProgress(status) {
|
||||
var pipeline = status && status.pipeline && typeof status.pipeline === 'object' ? status.pipeline : {};
|
||||
var progress = pipeline.progress && typeof pipeline.progress === 'object' ? pipeline.progress : null;
|
||||
@@ -1131,35 +1193,113 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
|
||||
function knowledgeRagSourceProgressHtml(progress) {
|
||||
if (!progress) return '';
|
||||
var percent = Math.max(0, Math.min(100, Math.round((progress.current / progress.total) * 100)));
|
||||
var label = progress.current + ' / ' + progress.total + ' · ' + percent + '%';
|
||||
var current = Number(progress.current || 0);
|
||||
var total = Number(progress.total || 0);
|
||||
var determinate = Number.isFinite(current) && Number.isFinite(total) && current > 0 && total > 0;
|
||||
var percent = determinate ? Math.max(0, Math.min(100, Math.round((current / total) * 100))) : 0;
|
||||
var label = determinate ? current + ' / ' + total + ' · ' + percent + '%' : String(progress.label || '处理中');
|
||||
var mode = determinate ? 'determinate' : 'indeterminate';
|
||||
var ariaValues = determinate
|
||||
? ' aria-valuemin="0" aria-valuemax="' + String(total) + '" aria-valuenow="' + String(current) + '"'
|
||||
: ' aria-valuetext="' + escapeHtml(label) + '"';
|
||||
return '' +
|
||||
'<div class="mnote-knowledge-rag-source-progress" role="progressbar" aria-valuemin="0" aria-valuemax="' + String(progress.total) + '" aria-valuenow="' + String(progress.current) + '" aria-label="' + escapeHtml('索引进度 ' + label) + '">' +
|
||||
'<div><i style="width:' + String(percent) + '%"></i></div>' +
|
||||
'<div class="mnote-knowledge-rag-source-progress" data-progress-mode="' + mode + '" role="progressbar"' + ariaValues + ' aria-label="' + escapeHtml('索引进度 ' + label) + '">' +
|
||||
'<div><i' + (determinate ? ' style="width:' + String(percent) + '%"' : '') + '></i></div>' +
|
||||
'<span>' + escapeHtml(label) + '</span>' +
|
||||
'</div>';
|
||||
}
|
||||
|
||||
function scheduleKnowledgeRagStatusBridge(reason) {
|
||||
if (!knowledgeRagIsAvailable()) return;
|
||||
if (knowledgeRagBridgeTimer) window.clearTimeout(knowledgeRagBridgeTimer);
|
||||
knowledgeRagBridgeGeneration += 1;
|
||||
knowledgeRagBridgeAttempt = 0;
|
||||
runKnowledgeRagStatusBridge(knowledgeRagBridgeGeneration, reason || 'source-updated');
|
||||
function knowledgeRagDocumentIsInFlight(doc) {
|
||||
var status = String(doc && (doc.statusGroup || doc.status) || '').toLowerCase();
|
||||
return ['pending', 'scanning', 'parsing', 'parse', 'parsed', 'analyzing', 'analyze', 'processing', 'process'].includes(status);
|
||||
}
|
||||
|
||||
function runKnowledgeRagStatusBridge(generation, reason) {
|
||||
if (generation !== knowledgeRagBridgeGeneration || !knowledgeRagIsAvailable()) return;
|
||||
var delay = knowledgeRagBridgeDelays[Math.min(knowledgeRagBridgeAttempt, knowledgeRagBridgeDelays.length - 1)];
|
||||
knowledgeRagBridgeTimer = window.setTimeout(async function() {
|
||||
if (generation !== knowledgeRagBridgeGeneration || !knowledgeRagIsAvailable()) return;
|
||||
knowledgeRagBridgeAttempt += 1;
|
||||
await loadKnowledgeRagStatus(true, { silentLoading: true, notifyUpdated: true, reason: reason || 'bridge' });
|
||||
var status = currentKnowledgeRagSummary().status || {};
|
||||
if (knowledgeRagStatusHasInFlight(status) && knowledgeRagBridgeAttempt < knowledgeRagBridgeDelays.length) {
|
||||
runKnowledgeRagStatusBridge(generation, reason || 'bridge');
|
||||
function knowledgeRagProgressLabel(status, doc, fallback) {
|
||||
var pipeline = status && status.pipeline && typeof status.pipeline === 'object' ? status.pipeline : {};
|
||||
var latest = String(pipeline.latestMessage || '').trim();
|
||||
if (latest) return '处理中';
|
||||
var providerStatus = String(doc && (doc.statusGroup || doc.status) || fallback || '').toLowerCase();
|
||||
if (providerStatus === 'pending') return '排队中';
|
||||
if (providerStatus === 'scanning') return '扫描中';
|
||||
if (providerStatus === 'parsing' || providerStatus === 'parse' || providerStatus === 'parsed') return '解析中';
|
||||
if (providerStatus === 'analyzing' || providerStatus === 'analyze') return '分析中';
|
||||
return '索引中';
|
||||
}
|
||||
|
||||
function knowledgeRagIndeterminateProgress(status, doc, fallback) {
|
||||
return { mode: 'indeterminate', label: knowledgeRagProgressLabel(status, doc, fallback) };
|
||||
}
|
||||
|
||||
function stopKnowledgeRagPipelineWatcher() {
|
||||
if (knowledgeRagPipelineEventSource && typeof knowledgeRagPipelineEventSource.close === 'function') {
|
||||
knowledgeRagPipelineEventSource.close();
|
||||
}
|
||||
knowledgeRagPipelineEventSource = null;
|
||||
knowledgeRagPipelineWatcherScopeKey = '';
|
||||
}
|
||||
|
||||
function handleKnowledgeRagPipelineEvent(payload) {
|
||||
var scopeKey = knowledgeRagScopeKey();
|
||||
if (knowledgeRagPipelineWatcherScopeKey && knowledgeRagPipelineWatcherScopeKey !== scopeKey) return;
|
||||
var current = currentKnowledgeRagSummary();
|
||||
var currentStatus = current.status && typeof current.status === 'object' ? current.status : {};
|
||||
var previousPipeline = currentStatus.pipeline && typeof currentStatus.pipeline === 'object' ? currentStatus.pipeline : {};
|
||||
var previousInFlight = knowledgeRagPipelineHasInFlight(previousPipeline);
|
||||
var pipeline = normalizeKnowledgeRagPipelineEvent(payload);
|
||||
var nextStatus = Object.assign({}, currentStatus, { pipeline: pipeline });
|
||||
pageUiState.knowledgeRagSummary = Object.assign({}, current, {
|
||||
scopeKey: scopeKey,
|
||||
loading: false,
|
||||
error: '',
|
||||
status: nextStatus
|
||||
});
|
||||
if (isKnowledgeRagSettingsOpen()) renderKnowledgeRagSettings();
|
||||
var currentInFlight = knowledgeRagPipelineHasInFlight(pipeline);
|
||||
if (currentInFlight) return;
|
||||
var latest = String(pipeline.latestMessage || '').toLowerCase();
|
||||
var finished = previousInFlight || latest.indexOf('completed') !== -1 || latest.indexOf('complete') !== -1;
|
||||
if (!finished || knowledgeRagPipelineWatcherRefreshPending) return;
|
||||
knowledgeRagPipelineWatcherRefreshPending = true;
|
||||
void loadKnowledgeRagStatus(true, {
|
||||
silentLoading: true,
|
||||
notifyUpdated: true,
|
||||
reason: 'pipeline-event-complete'
|
||||
}).finally(function() {
|
||||
knowledgeRagPipelineWatcherRefreshPending = false;
|
||||
if (!knowledgeRagStatusHasInFlight(currentKnowledgeRagSummary().status || {})) {
|
||||
stopKnowledgeRagPipelineWatcher();
|
||||
}
|
||||
}, delay);
|
||||
});
|
||||
}
|
||||
|
||||
function startKnowledgeRagPipelineWatcher(reason) {
|
||||
document.documentElement.setAttribute('data-mnote-knowledge-rag-watcher-required', String(reason || 'source-updated'));
|
||||
if (!knowledgeRagIsAvailable() || typeof window.EventSource !== 'function') return;
|
||||
var scopeKey = knowledgeRagScopeKey();
|
||||
if (knowledgeRagPipelineEventSource && knowledgeRagPipelineWatcherScopeKey === scopeKey) return;
|
||||
stopKnowledgeRagPipelineWatcher();
|
||||
var url = new URL('/api/knowledge-rag/pipeline-events', window.location.origin);
|
||||
url.searchParams.set('workspaceId', resolveWorkspaceId(document.body));
|
||||
url.searchParams.set('rootUri', currentRootUri());
|
||||
var source = new EventSource(url.toString());
|
||||
knowledgeRagPipelineEventSource = source;
|
||||
knowledgeRagPipelineWatcherScopeKey = scopeKey;
|
||||
source.addEventListener('pipeline_status', function(event) {
|
||||
var payload = null;
|
||||
try {
|
||||
payload = JSON.parse(event.data || '{}');
|
||||
} catch (_) {
|
||||
return;
|
||||
}
|
||||
handleKnowledgeRagPipelineEvent(payload);
|
||||
});
|
||||
source.onerror = function() {
|
||||
document.documentElement.setAttribute('data-mnote-knowledge-rag-watcher-error', 'true');
|
||||
};
|
||||
}
|
||||
|
||||
function scheduleKnowledgeRagStatusBridge(reason) {
|
||||
startKnowledgeRagPipelineWatcher(reason);
|
||||
}
|
||||
|
||||
function knowledgeRagUnmappedProviderRows(status, entries) {
|
||||
@@ -1245,6 +1385,15 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
return;
|
||||
}
|
||||
var pipelineProgress = knowledgeRagPipelineProgress(status || {});
|
||||
var providerDocs = knowledgeRagProviderDocuments(status || {});
|
||||
var providerDocsById = new Map();
|
||||
var providerDocsByPath = new Map();
|
||||
providerDocs.forEach(function(doc) {
|
||||
var docId = String(doc && doc.id || '').trim();
|
||||
var filePath = String(doc && doc.filePath || '').trim();
|
||||
if (docId) providerDocsById.set(docId, doc);
|
||||
if (filePath) providerDocsByPath.set(filePath, doc);
|
||||
});
|
||||
sourcesNode.innerHTML = items.map(function(row) {
|
||||
if (row.kind === 'provider') {
|
||||
var doc = row.doc || {};
|
||||
@@ -1252,6 +1401,9 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var docStatus = String(doc.status || doc.statusGroup || '');
|
||||
var failedDoc = row.category === 'unmapped_failed';
|
||||
var providerProgress = pipelineProgress && pipelineProgress.docId && pipelineProgress.docId === String(doc.id || '') ? pipelineProgress : null;
|
||||
if (!providerProgress && !failedDoc && knowledgeRagDocumentIsInFlight(doc)) {
|
||||
providerProgress = knowledgeRagIndeterminateProgress(status || {}, doc, docStatus);
|
||||
}
|
||||
return '' +
|
||||
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-source-kind="provider" data-knowledge-rag-source-path="' + escapeHtml(providerPath) + '">' +
|
||||
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + (failedDoc ? 'fault' : 'indexed') + '" title="' + escapeHtml(docStatus || 'LightRAG 文档') + '"></span>' +
|
||||
@@ -1286,6 +1438,10 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var canReindex = Boolean(sourcePath && !deleting);
|
||||
var canDelete = Boolean(sourcePath && !deleted && !deletedDone && !deleting);
|
||||
var entryProgress = pipelineProgress && pipelineProgress.docId && pipelineProgress.docId === docId && !indexed && !failed && !stale && !deleted && !deletedDone ? pipelineProgress : null;
|
||||
var providerDoc = (docId && providerDocsById.get(docId)) || providerDocsByPath.get(String(entry && entry.lightRagFilePath || '').trim()) || null;
|
||||
if (!entryProgress && !indexed && !failed && !stale && !deleted && !deletedDone && !deleting && (knowledgeRagDocumentIsInFlight(providerDoc) || docId || providerStatus)) {
|
||||
entryProgress = knowledgeRagIndeterminateProgress(status || {}, providerDoc, providerStatus);
|
||||
}
|
||||
return '' +
|
||||
'<div class="mnote-knowledge-rag-source-row" data-knowledge-rag-source-row="true" data-knowledge-rag-source-path="' + escapeHtml(sourcePath) + '">' +
|
||||
'<span class="wolai-page-settings-index-status-dot" data-index-status="' + kind + '" title="' + escapeHtml(statusLabel) + '"></span>' +
|
||||
@@ -1396,6 +1552,8 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}
|
||||
var scopeKey = knowledgeRagScopeKey();
|
||||
var current = currentKnowledgeRagSummary();
|
||||
var previousStatus = current.status || {};
|
||||
var previousDigest = knowledgeRagRegistryStatusDigest(previousStatus);
|
||||
if (!force && current.scopeKey === scopeKey && !current.error && !current.loading) {
|
||||
renderKnowledgeRagSettings();
|
||||
return;
|
||||
@@ -1419,14 +1577,20 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
if (!response.ok || !payload || payload.ok !== true) {
|
||||
throw new Error(payload && payload.message ? payload.message : 'knowledge_rag_status_' + response.status);
|
||||
}
|
||||
var nextDigest = knowledgeRagRegistryStatusDigest(payload);
|
||||
pageUiState.knowledgeRagSummary = {
|
||||
scopeKey: scopeKey,
|
||||
loading: false,
|
||||
error: '',
|
||||
status: payload
|
||||
};
|
||||
if (options.notifyUpdated) {
|
||||
notifyKnowledgeRagSourceUpdated({ action: 'status-sync', reason: options.reason || 'bridge', status: payload });
|
||||
if (knowledgeRagStatusHasInFlight(payload)) {
|
||||
startKnowledgeRagPipelineWatcher(options.reason || 'status-in-flight');
|
||||
} else if (!knowledgeRagPipelineWatcherRefreshPending) {
|
||||
stopKnowledgeRagPipelineWatcher();
|
||||
}
|
||||
if (options.notifyUpdated || previousDigest !== nextDigest) {
|
||||
notifyKnowledgeRagSourceUpdated({ action: 'status-sync', reason: options.reason || 'registry-status-changed', status: payload });
|
||||
}
|
||||
} catch (error) {
|
||||
pageUiState.knowledgeRagSummary = {
|
||||
@@ -1495,6 +1659,21 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
}));
|
||||
}
|
||||
|
||||
function knowledgeRagRegistryStatusDigest(status) {
|
||||
var registry = status && status.registry && typeof status.registry === 'object' ? status.registry : {};
|
||||
var entries = Array.isArray(registry.entries) ? registry.entries : [];
|
||||
return entries.map(function(entry) {
|
||||
return [
|
||||
String(entry && entry.sourceRootRelativePath || ''),
|
||||
String(entry && entry.lightRagDocId || ''),
|
||||
String(entry && entry.lightRagStatus || ''),
|
||||
String(entry && entry.indexedAtMs || ''),
|
||||
String(entry && entry.deletedAtMs || ''),
|
||||
entry && entry.stale ? 'stale' : 'fresh'
|
||||
].join('|');
|
||||
}).sort().join('\n');
|
||||
}
|
||||
|
||||
function removeKnowledgeRagSourceInput(button) {
|
||||
var row = button && button.closest ? button.closest('[data-knowledge-rag-source-input-row]') : null;
|
||||
if (row instanceof HTMLElement) row.remove();
|
||||
|
||||
Reference in New Issue
Block a user