Improve LightRAG knowledge search locator alignment

This commit is contained in:
lix-2026
2026-06-08 20:35:49 +08:00
parent 9551d4c1dc
commit 0e8b03daf8
28 changed files with 5769 additions and 140 deletions
@@ -50,6 +50,10 @@ export function createSidebarPageAiRuntime(context) {
{ id: 'changed_files', label: '最近修改' }
];
var pageAiDelegatesInstalled = false;
var PAGE_AI_DRAWER_WIDTH_STORAGE_KEY = 'mnote.page_ai.drawer_width';
var PAGE_AI_DRAWER_DEFAULT_WIDTH = 440;
var PAGE_AI_DRAWER_MIN_WIDTH = 340;
var PAGE_AI_DRAWER_MAX_WIDTH = 760;
function clonePageAiDefaultValue(value) {
if (Array.isArray(value)) return value.slice();
@@ -57,6 +61,70 @@ export function createSidebarPageAiRuntime(context) {
return value;
}
function pageAiClampDrawerWidth(width) {
var viewportMax = Math.max(PAGE_AI_DRAWER_MIN_WIDTH, Math.min(PAGE_AI_DRAWER_MAX_WIDTH, window.innerWidth - 24));
var numeric = Number(width);
if (!Number.isFinite(numeric) || numeric <= 0) numeric = PAGE_AI_DRAWER_DEFAULT_WIDTH;
return Math.max(PAGE_AI_DRAWER_MIN_WIDTH, Math.min(viewportMax, Math.round(numeric)));
}
function pageAiStoredDrawerWidth() {
try {
return pageAiClampDrawerWidth(window.localStorage ? window.localStorage.getItem(PAGE_AI_DRAWER_WIDTH_STORAGE_KEY) : PAGE_AI_DRAWER_DEFAULT_WIDTH);
} catch (_error) {
return pageAiClampDrawerWidth(PAGE_AI_DRAWER_DEFAULT_WIDTH);
}
}
function pageAiApplyDrawerWidth(drawer, width) {
var target = drawer instanceof HTMLElement ? drawer : document.querySelector('[data-testid="wolai-page-ai-drawer"]');
if (!(target instanceof HTMLElement)) return;
var next = pageAiClampDrawerWidth(width == null ? pageAiStoredDrawerWidth() : width);
target.style.setProperty('--mnote-page-ai-width', next + 'px');
}
function handlePageAiPointerDown(event, helpers) {
var closestAction = helpers && helpers.closestAction;
if (typeof closestAction !== 'function') return false;
var handle = closestAction(event.target, '[data-page-ai-resize-handle]');
if (!(handle instanceof HTMLElement)) return false;
var drawer = handle.closest('[data-testid="wolai-page-ai-drawer"]');
var panel = drawer instanceof HTMLElement ? drawer.querySelector('.wolai-page-ai-panel') : null;
if (!(drawer instanceof HTMLElement) || !(panel instanceof HTMLElement)) return false;
event.preventDefault();
var pointerId = event.pointerId;
var startX = Number(event.clientX || 0);
var startWidth = panel.getBoundingClientRect().width || pageAiStoredDrawerWidth();
drawer.setAttribute('data-page-ai-resizing', 'true');
document.documentElement.setAttribute('data-mnote-page-ai-resizing', 'true');
try {
handle.setPointerCapture(pointerId);
} catch (_error) {}
function move(nextEvent) {
var nextWidth = startWidth + (startX - Number(nextEvent.clientX || 0));
pageAiApplyDrawerWidth(drawer, nextWidth);
}
function finish(nextEvent) {
move(nextEvent);
var value = drawer.style.getPropertyValue('--mnote-page-ai-width').replace('px', '').trim();
try {
if (window.localStorage) window.localStorage.setItem(PAGE_AI_DRAWER_WIDTH_STORAGE_KEY, String(pageAiClampDrawerWidth(value)));
} catch (_error) {}
drawer.removeAttribute('data-page-ai-resizing');
document.documentElement.removeAttribute('data-mnote-page-ai-resizing');
window.removeEventListener('pointermove', move, true);
window.removeEventListener('pointerup', finish, true);
window.removeEventListener('pointercancel', finish, true);
try {
handle.releasePointerCapture(pointerId);
} catch (_error) {}
}
window.addEventListener('pointermove', move, true);
window.addEventListener('pointerup', finish, true);
window.addEventListener('pointercancel', finish, true);
return true;
}
function ensurePageAiStateFacade(state) {
var defaults = {
pageAiOpen: false,
@@ -743,9 +811,12 @@ export function createSidebarPageAiRuntime(context) {
var prefix = '/documents/';
if (!url.pathname.startsWith(prefix)) return '';
try {
return decodeURIComponent(url.pathname.slice(prefix.length).split('/')[0] || '');
var raw = url.pathname.slice(prefix.length).split('/')[0] || '';
var decoded = decodeURIComponent(raw);
if (decoded.indexOf('%') >= 0 || decoded.indexOf('\uFFFD') >= 0) return '';
return decoded;
} catch (_error) {
return url.pathname.slice(prefix.length).split('/')[0] || '';
return '';
}
}
@@ -753,6 +824,14 @@ export function createSidebarPageAiRuntime(context) {
var explicitPath = String(url.searchParams.get('resourcePath') || '').trim();
if (explicitPath) return explicitPath;
var raw = String(url.searchParams.get('resourceTab') || '').trim();
if (!raw) {
try {
var decodedHash = decodeURIComponent(String(url.hash || ''));
var marker = '#resource-tab-';
var markerIndex = decodedHash.indexOf(marker);
if (markerIndex >= 0) raw = decodedHash.slice(markerIndex + marker.length).replace(/^#+/, '').trim();
} catch (_error) {}
}
if (raw.indexOf('::') >= 0) raw = raw.slice(raw.indexOf('::') + 2);
if (!raw.startsWith('resource:file:')) return '';
var rest = raw.slice('resource:file:'.length);
@@ -761,6 +840,40 @@ export function createSidebarPageAiRuntime(context) {
return rest.slice(prefix.length).replace(/^\/+/, '');
}
function pageAiSameMnoteOrigin(url) {
try {
var current = new URL(window.location.origin);
if (url.origin === current.origin) return true;
if (url.hostname === 'mnote.local') return true;
var localNames = ['localhost', '127.0.0.1', '::1', 'mnote.local'];
function defaultPort(protocol) {
return protocol === 'https:' ? '443' : protocol === 'http:' ? '80' : '';
}
return localNames.indexOf(url.hostname) >= 0 &&
localNames.indexOf(current.hostname) >= 0 &&
String(url.port || defaultPort(url.protocol)) === String(current.port || defaultPort(current.protocol));
} catch (_error) {
return false;
}
}
function pageAiPortableCitationUrl(url) {
try {
if (url.pathname === '/api/local-folder/files/open') {
return Boolean(String(url.searchParams.get('rootUri') || '').trim()) &&
Boolean(String(url.searchParams.get('path') || '').trim());
}
if (!url.pathname.startsWith('/documents/')) return false;
var resourceTab = String(url.searchParams.get('resourceTab') || '').trim();
return String(url.searchParams.get('sourceKind') || '').trim() === 'local_folder' ||
Boolean(String(url.searchParams.get('rootUri') || '').trim()) ||
Boolean(String(url.searchParams.get('resourcePath') || '').trim()) ||
resourceTab.startsWith('resource:file:');
} catch (_error) {
return false;
}
}
function pageAiOpenCitationUrl(href) {
var url;
try {
@@ -768,7 +881,34 @@ export function createSidebarPageAiRuntime(context) {
} catch (_error) {
return false;
}
if (url.origin !== window.location.origin || !url.pathname.startsWith('/documents/')) return false;
var unwrappedHref = pageAiUnwrapSearchCitationUrl(url);
if (unwrappedHref) return pageAiOpenCitationUrl(unwrappedHref);
if (!pageAiSameMnoteOrigin(url) && !pageAiPortableCitationUrl(url)) return false;
if (url.pathname === '/api/local-folder/files/open') {
var fileRootUri = String(url.searchParams.get('rootUri') || currentRootUri() || '').trim();
var filePath = String(url.searchParams.get('path') || '').trim();
if (!filePath) return false;
void openLocalResourceInActiveTab({
path: filePath,
rootUri: fileRootUri,
documentId: currentDocumentId() || '',
workspaceId: resolveWorkspaceId(document.body) || '',
sourceKind: currentSourceKind() || 'local_folder',
page: url.searchParams.get('page') || undefined,
bbox: pageAiEvidenceBboxFromParam(url.searchParams.get('bbox')),
sourceMapPath: String(url.searchParams.get('sourceMapPath') || '').trim(),
blockId: String(url.searchParams.get('blockId') || '').trim(),
evidenceText: String(url.searchParams.get('evidenceText') || '').trim(),
lineRange: pageAiEvidenceRangeFromParam(url.searchParams.get('lineRange')),
charRange: pageAiEvidenceRangeFromParam(url.searchParams.get('charRange')),
openTarget: 'active-tab',
paneRole: 'primary'
}).then(function(opened) {
if (!opened) window.open(url.toString(), '_blank', 'noopener,noreferrer');
});
return true;
}
if (!url.pathname.startsWith('/documents/')) return false;
var rootUri = String(url.searchParams.get('rootUri') || currentRootUri() || '').trim();
var resourcePath = pageAiCitationResourcePath(url, rootUri);
var documentId = pageAiDocumentIdFromUrl(url) || currentDocumentId() || '';
@@ -784,6 +924,7 @@ export function createSidebarPageAiRuntime(context) {
bbox: pageAiEvidenceBboxFromParam(url.searchParams.get('bbox')),
sourceMapPath: String(url.searchParams.get('sourceMapPath') || '').trim(),
blockId: String(url.searchParams.get('blockId') || '').trim(),
evidenceText: String(url.searchParams.get('evidenceText') || '').trim(),
lineRange: pageAiEvidenceRangeFromParam(url.searchParams.get('lineRange')),
charRange: pageAiEvidenceRangeFromParam(url.searchParams.get('charRange')),
openTarget: 'active-tab',
@@ -1035,6 +1176,30 @@ export function createSidebarPageAiRuntime(context) {
return true;
}
function pageAiUnwrapSearchCitationUrl(url) {
try {
var raw = '';
['wd', 'q', 'query'].some(function(key) {
raw = String(url.searchParams.get(key) || '').trim();
return Boolean(raw);
});
if (!raw) return '';
if (/^documents\//i.test(raw)) raw = '/' + raw;
if (!/^\/documents\//i.test(raw) && !/^https?:\/\/[^/]+\/documents\//i.test(raw) && !/^mnote:\/\/open/i.test(raw)) return '';
if (/^mnote:\/\/open/i.test(raw)) return raw;
var nested = new URL(raw, window.location.origin);
if (!nested.pathname.startsWith('/documents/')) return '';
['sourceKind', 'rootUri', 'workspaceId', 'resourceTab', 'resourcePath', 'page', 'bbox', 'sourceMapPath', 'blockId', 'evidenceText', 'lineRange', 'charRange'].forEach(function(key) {
if (nested.searchParams.get(key)) return;
var value = String(url.searchParams.get(key) || '').trim();
if (value) nested.searchParams.set(key, value);
});
return nested.pathname + nested.search;
} catch (_error) {
return '';
}
}
function pageAiPreviewValue(value) {
if (value == null || value === '') return '';
if (typeof value === 'string') return value.length > 120 ? value.slice(0, 120) + '…' : value;
@@ -1259,9 +1424,12 @@ export function createSidebarPageAiRuntime(context) {
if (argsSummary) existing.argsSummary = argsSummary;
if (resultSummary) existing.resultSummary = resultSummary;
if (locations.length) existing.locations = locations;
existing.rawOutput = toolEvent && toolEvent.output;
existing.rawResult = resultSource;
if (status === 'completed') {
pageAiNotifyToolWriteCompleted(toolName, toolEvent, runId, runTraceId);
}
return existing;
}
async function pageAiCancelQueuedRun(queueId) {
@@ -1735,6 +1903,7 @@ export function createSidebarPageAiRuntime(context) {
renderPageAiProviderButtons();
renderPageAiControls();
var drawer = ensurePageAiDrawer();
pageAiApplyDrawerWidth(drawer);
drawer.hidden = false;
pageUiState.pageAiOpen = true;
updatePageAiTriggerState();
@@ -1840,13 +2009,113 @@ export function createSidebarPageAiRuntime(context) {
return message.content;
}
function pageAiFinishStreamingAssistantMessage(runId, finalText, promptText) {
function pageAiAppendCitationSection(content, citations) {
var text = String(content || '');
var unique = [];
pageAiNormalizeArray(citations).forEach(function(value) {
var citation = String(value || '').trim();
if (!citation || unique.indexOf(citation) >= 0) return;
unique.push(citation);
});
var missing = unique.filter(function(citation) {
return text.indexOf(citation) < 0;
});
if (!missing.length) return text;
return text.replace(/\s+$/g, '') + '\n\n**引用**\n' + missing.map(function(citation) {
return '- ' + citation;
}).join('\n');
}
function pageAiPromptNeedsKnowledgeRagCitations(prompt) {
var text = searchText(prompt);
if (!text) return false;
var asksCitation = ['链接', '引用', '来源', '出处', '证据', '定位', 'link', 'citation', 'source'].some(function(word) {
return text.indexOf(word) >= 0;
});
if (!asksCitation) return false;
return ['lightrag', '资料库', '知识库', 'rag', '文献', '论文', '保护基'].some(function(word) {
return text.indexOf(word) >= 0;
});
}
function pageAiCleanDegradedCitationNotes(content) {
return String(content || '')
.split('\n')
.filter(function(line) {
var text = String(line || '');
return text.indexOf('来源定位降级') < 0 && text.toLowerCase().indexOf('locator degraded') < 0;
})
.join('\n')
.replace(/\n{3,}/g, '\n\n')
.trim();
}
function pageAiCollectKnowledgeRagApiCitations(payload) {
var references = pageAiNormalizeArray(payload && payload.references);
var citations = references.map(function(reference) {
return String(reference && reference.citationMarkdown || '').trim();
}).filter(Boolean);
var hasPreciseCitation = references.some(function(reference) {
return String(reference && reference.citationMarkdown || '').trim() && reference && reference.locatorDegraded !== true;
});
var seen = {};
return citations.filter(function(citation) {
if (seen[citation]) return false;
if (hasPreciseCitation && citation.indexOf('来源定位降级') >= 0) return false;
seen[citation] = true;
return true;
}).slice(0, 5);
}
async function pageAiAppendKnowledgeRagFallbackCitations(runId, promptText) {
if (!pageAiPromptNeedsKnowledgeRagCitations(promptText)) return;
var rootUri = currentRootUri();
if (!rootUri) return;
try {
var response = await fetch('/api/knowledge-rag/query', {
method: 'POST',
headers: { 'content-type': 'application/json', 'accept': 'application/json' },
body: JSON.stringify({
workspaceId: resolveWorkspaceId(document.body) || undefined,
rootUri: rootUri,
query: String(promptText || '').trim(),
mode: 'mix',
topK: 8,
chunkTopK: 8,
includeChunkContent: true
})
});
var payload = await response.json().catch(function() { return null; });
if (!response.ok) return;
var citations = pageAiCollectKnowledgeRagApiCitations(payload);
if (!citations.length) return;
var message = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.runId === runId;
}) || pageUiState.pageAiMessages.filter(function(item) { return item.role === 'assistant'; }).slice(-1)[0];
if (!message) return;
var hasPrecise = citations.some(function(citation) { return citation.indexOf('来源定位降级') < 0; });
var baseContent = hasPrecise ? pageAiCleanDegradedCitationNotes(message.content) : String(message.content || '');
message.content = pageAiAppendCitationSection(baseContent, citations);
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
} catch (error) {
console.warn('MNote Page AI 自动追加 LightRAG 引用失败', error);
}
}
function pageAiFinishStreamingAssistantMessage(runId, finalText, promptText, citations) {
var id = String(runId || pageUiState.pageAiCurrentRunId || '');
var message = pageUiState.pageAiMessages.find(function(item) {
return item.role === 'assistant' && item.streaming === true && item.runId === id;
});
var text = String(finalText || (message && message.content) || '');
var content = humanizePageAiResponse(text, promptText);
var hasPreciseCitation = pageAiNormalizeArray(citations).some(function(citation) {
return String(citation || '').indexOf('来源定位降级') < 0;
});
var contentBase = humanizePageAiResponse(text, promptText);
if (hasPreciseCitation) contentBase = pageAiCleanDegradedCitationNotes(contentBase);
var content = pageAiAppendCitationSection(contentBase, citations);
if (message) {
message.content = content;
message.streaming = false;
@@ -1859,6 +2128,123 @@ export function createSidebarPageAiRuntime(context) {
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
void pageAiAppendKnowledgeRagFallbackCitations(id, promptText);
}
function pageAiToolOutputText(value) {
var parts = [];
function visit(node) {
if (node == null) return;
if (typeof node === 'string') {
parts.push(node);
return;
}
if (typeof node === 'number' || typeof node === 'boolean') return;
if (Array.isArray(node)) {
node.forEach(visit);
return;
}
if (typeof node !== 'object') return;
if (typeof node.text === 'string') parts.push(node.text);
if (typeof node.content === 'string') parts.push(node.content);
if (node.content && typeof node.content === 'object') visit(node.content);
if (node.output && typeof node.output === 'object') visit(node.output);
if (node.result && typeof node.result === 'object') visit(node.result);
}
visit(value);
return parts.join('\n');
}
function pageAiParseJsonMaybe(value) {
if (value && typeof value === 'object') return value;
var text = String(value || '').trim();
if (!text || (text[0] !== '{' && text[0] !== '[')) return null;
try {
return JSON.parse(text);
} catch (_error) {
var firstLine = text.split('\n')[0].trim();
if (firstLine && firstLine !== text && (firstLine[0] === '{' || firstLine[0] === '[')) {
try {
return JSON.parse(firstLine);
} catch (_lineError) {}
}
return null;
}
}
function pageAiCollectCitationMarkdowns(value) {
var citations = [];
var seen = {};
function add(value) {
var citation = String(value || '').trim();
if (!citation || seen[citation]) return;
seen[citation] = true;
citations.push(citation);
}
function visit(node) {
if (node == null) return;
if (typeof node === 'string') {
var parsed = pageAiParseJsonMaybe(node);
if (parsed) visit(parsed);
return;
}
if (Array.isArray(node)) {
node.forEach(visit);
return;
}
if (typeof node !== 'object') return;
if (typeof node.citationMarkdown === 'string') add(node.citationMarkdown);
if (Array.isArray(node.citationMarkdowns)) {
node.citationMarkdowns.forEach(function(value) {
if (typeof value === 'string') add(value);
else visit(value);
});
}
Object.keys(node).forEach(function(key) {
visit(node[key]);
});
}
visit(value);
return citations;
}
function pageAiCollectKnowledgeRagCitations(toolItem, toolEvent) {
var toolName = String((toolItem && toolItem.toolName) || (toolEvent && (toolEvent.name || toolEvent.tool || toolEvent.toolName)) || '');
var sources = [
toolEvent,
toolItem,
toolEvent && toolEvent.output,
toolEvent && toolEvent.result,
toolEvent && toolEvent.summary,
toolItem && toolItem.rawOutput,
toolItem && toolItem.rawResult,
toolItem && toolItem.resultSummary
];
var citations = [];
var outputText = '';
sources.forEach(function(source) {
citations = citations.concat(pageAiCollectCitationMarkdowns(source));
var sourceText = pageAiToolOutputText(source);
if (sourceText) outputText += '\n' + sourceText;
var parsed = pageAiParseJsonMaybe(sourceText);
if (parsed) citations = citations.concat(pageAiCollectCitationMarkdowns(parsed));
});
var looksLikeKnowledgeRag = toolName.indexOf('knowledge_rag') >= 0 ||
toolName.indexOf('knowledge.rag') >= 0 ||
outputText.indexOf('mnote.knowledge_rag') >= 0 ||
outputText.indexOf('uiCitations') >= 0;
if (!looksLikeKnowledgeRag && citations.length === 0) return [];
var hasPreciseCitation = citations.some(function(value) {
return String(value || '').indexOf('来源定位降级') < 0;
});
var seen = {};
return citations.filter(function(value) {
var citation = String(value || '').trim();
if (!citation || seen[citation]) return false;
if (hasPreciseCitation && citation.indexOf('来源定位降级') >= 0) return false;
seen[citation] = true;
return true;
});
}
function pageAiLooksLikeBlockEdit(prompt) {
@@ -2078,6 +2464,7 @@ export function createSidebarPageAiRuntime(context) {
throw new Error(pageAiErrorMessage(eventError, 'hermes_events_failed_' + eventResponse.status));
}
var assistantText = '';
var autoCitations = [];
await streamPageAiResponse(eventResponse, function(eventName, payloadText) {
if (eventName === 'message.delta') {
if (pageUiState.pageAiStoppedRunIds[runId]) return;
@@ -2222,7 +2609,15 @@ export function createSidebarPageAiRuntime(context) {
} catch (_) {}
}
if (eventName === 'tool.started' || eventName === 'tool.completed' || eventName === 'tool.failed') {
pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId);
var toolEventPayload = null;
try {
toolEventPayload = JSON.parse(payloadText || 'null');
} catch (_) {}
var toolItem = pageAiApplyToolEvent(eventName, payloadText, runId, runTraceId);
if (eventName === 'tool.completed') {
var completedCitations = pageAiCollectKnowledgeRagCitations(toolItem, toolEventPayload);
if (completedCitations.length) autoCitations = completedCitations;
}
pageAiSyncCurrentSessionMessages();
pageAiPersistSessions();
renderPageAiConversation();
@@ -2231,7 +2626,7 @@ export function createSidebarPageAiRuntime(context) {
}
});
if (!pageUiState.pageAiStoppedRunIds[runId]) {
pageAiFinishStreamingAssistantMessage(runId, assistantText, prompt);
pageAiFinishStreamingAssistantMessage(runId, assistantText, prompt, autoCitations);
}
currentSession = pageAiCurrentSession();
if (currentSession) {
@@ -2596,6 +2991,9 @@ export function createSidebarPageAiRuntime(context) {
document.addEventListener('click', function(event) {
handlePageAiClick(event, helpers);
});
document.addEventListener('pointerdown', function(event) {
handlePageAiPointerDown(event, helpers);
});
document.addEventListener('keydown', function(event) {
handlePageAiKeyDown(event, helpers);
});