feat(rag): align LightRAG native citations and MCP bridge
This commit is contained in:
@@ -352,6 +352,10 @@ import {
|
||||
bbox: url.searchParams.get('bbox') || undefined,
|
||||
sourceMapPath: String(url.searchParams.get('sourceMapPath') || '').trim(),
|
||||
blockId: String(url.searchParams.get('blockId') || '').trim(),
|
||||
paragraphOrdinal: String(url.searchParams.get('paragraphOrdinal') || '').trim(),
|
||||
paraIdStart: String(url.searchParams.get('paraIdStart') || '').trim(),
|
||||
paraIdEnd: String(url.searchParams.get('paraIdEnd') || '').trim(),
|
||||
textFingerprint: String(url.searchParams.get('textFingerprint') || '').trim(),
|
||||
evidenceText: String(url.searchParams.get('evidenceText') || '').trim(),
|
||||
lineRange: url.searchParams.get('lineRange') || null,
|
||||
charRange: url.searchParams.get('charRange') || null,
|
||||
|
||||
@@ -988,7 +988,11 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
const searchQuery = String(input.searchQuery || locator?.searchQuery || params.searchQuery || '').trim();
|
||||
const lineRange = input.lineRange || locator?.lineRange || params.lineRange || null;
|
||||
const charRange = input.charRange || locator?.charRange || params.charRange || null;
|
||||
if (!locator && !Number.isFinite(page) && !bbox && !sourceMapPath && !blockId && !evidenceText && !lineRange && !charRange) return null;
|
||||
const paragraphOrdinal = String(input.paragraphOrdinal ?? locator?.paragraphOrdinal ?? params.paragraphOrdinal ?? '').trim();
|
||||
const paraIdStart = String(input.paraIdStart || locator?.paraIdStart || params.paraIdStart || '').trim();
|
||||
const paraIdEnd = String(input.paraIdEnd || locator?.paraIdEnd || params.paraIdEnd || '').trim();
|
||||
const textFingerprint = String(input.textFingerprint || locator?.textFingerprint || params.textFingerprint || '').trim();
|
||||
if (!locator && !Number.isFinite(page) && !bbox && !sourceMapPath && !blockId && !evidenceText && !lineRange && !charRange && !paragraphOrdinal && !paraIdStart && !textFingerprint) return null;
|
||||
return {
|
||||
schema: 'mnote.evidence_locator.v1',
|
||||
...(locator || {}),
|
||||
@@ -1000,6 +1004,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
searchQuery,
|
||||
lineRange,
|
||||
charRange,
|
||||
paragraphOrdinal,
|
||||
paraIdStart,
|
||||
paraIdEnd,
|
||||
textFingerprint,
|
||||
};
|
||||
};
|
||||
|
||||
@@ -1017,6 +1025,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
if (bbox) url.searchParams.set('bbox', bbox);
|
||||
if (locator.sourceMapPath) url.searchParams.set('sourceMapPath', String(locator.sourceMapPath));
|
||||
if (locator.blockId) url.searchParams.set('blockId', String(locator.blockId));
|
||||
if (locator.paragraphOrdinal) url.searchParams.set('paragraphOrdinal', String(locator.paragraphOrdinal));
|
||||
if (locator.paraIdStart) url.searchParams.set('paraIdStart', String(locator.paraIdStart));
|
||||
if (locator.paraIdEnd) url.searchParams.set('paraIdEnd', String(locator.paraIdEnd));
|
||||
if (locator.textFingerprint) url.searchParams.set('textFingerprint', String(locator.textFingerprint));
|
||||
if (locator.evidenceText) url.searchParams.set('evidenceText', String(locator.evidenceText));
|
||||
if (locator.searchQuery) url.searchParams.set('searchQuery', String(locator.searchQuery));
|
||||
if (url.pathname === '/office-preview' && frame.contentWindow) {
|
||||
@@ -1038,6 +1050,10 @@ export const createResourceTabRuntime = (dependencies = {}) => {
|
||||
bbox,
|
||||
sourceMapPath: locator.sourceMapPath || '',
|
||||
blockId: locator.blockId || '',
|
||||
paragraphOrdinal: locator.paragraphOrdinal || '',
|
||||
paraIdStart: locator.paraIdStart || '',
|
||||
paraIdEnd: locator.paraIdEnd || '',
|
||||
textFingerprint: locator.textFingerprint || '',
|
||||
evidenceText: locator.evidenceText || '',
|
||||
searchQuery: locator.searchQuery || '',
|
||||
}, window.location.origin);
|
||||
|
||||
@@ -2038,6 +2038,22 @@ export function createSidebarPageAiRuntime(context) {
|
||||
});
|
||||
}
|
||||
|
||||
function pageAiKnowledgeRagFallbackQuery(prompt) {
|
||||
var text = searchText(prompt);
|
||||
if (!text) return '';
|
||||
var afterColon = text.split(/[::]/).slice(1).join(':').trim();
|
||||
var candidate = afterColon || text;
|
||||
candidate = candidate
|
||||
.replace(/^(请|请用|帮我|帮忙|用)?(资料库|知识库|lightrag|rag)?(搜索|检索|查找|查询|回答|说明|解释|总结)?/i, '')
|
||||
.replace(/(请)?(给出|给我|附上|提供)?(链接|引用|来源|出处|证据|定位).*$/i, '')
|
||||
.trim();
|
||||
var stopIndex = candidate.search(/(在|的|是|有哪些|有什么|如何|怎么|用于|用途|作用|资料|文献|论文)/);
|
||||
if (stopIndex > 0) candidate = candidate.slice(0, stopIndex).trim();
|
||||
var cjkMatch = candidate.match(/[A-Za-z0-9\u4e00-\u9fff·α-ωΑ-Ω\-]{2,24}/);
|
||||
if (cjkMatch && cjkMatch[0]) return cjkMatch[0];
|
||||
return text;
|
||||
}
|
||||
|
||||
function pageAiCleanDegradedCitationNotes(content) {
|
||||
return String(content || '')
|
||||
.split('\n')
|
||||
@@ -2050,38 +2066,92 @@ export function createSidebarPageAiRuntime(context) {
|
||||
.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;
|
||||
function pageAiCitationPrecisionLabel(value) {
|
||||
var precision = searchText(value || '').toLowerCase();
|
||||
if (precision === 'bbox') return '精确定位';
|
||||
if (precision === 'paragraph') return '段落级';
|
||||
if (precision === 'file') return '文件级';
|
||||
return '';
|
||||
}
|
||||
|
||||
function pageAiCitationSourceTitle(citation) {
|
||||
if (!citation || typeof citation !== 'object') return '';
|
||||
var path = searchText(citation.sourceRootRelativePath || citation.sourcePath || citation.filePath || citation.lightRagFilePath || '');
|
||||
if (path) {
|
||||
var parts = path.split('/').filter(Boolean);
|
||||
return parts[parts.length - 1] || path;
|
||||
}
|
||||
var markdown = searchText(citation.citationMarkdown || '');
|
||||
var labelMatch = markdown.match(/^\[([^\]]+)\]/);
|
||||
return labelMatch ? labelMatch[1] : '';
|
||||
}
|
||||
|
||||
function pageAiCitationDisplayMarkdown(value) {
|
||||
if (typeof value === 'string') return value.trim();
|
||||
if (!value || typeof value !== 'object') return '';
|
||||
var markdown = searchText(value.citationMarkdown || '');
|
||||
var citationId = searchText(value.citationLabel || (value.citationId ? '[' + value.citationId + ']' : ''));
|
||||
var title = pageAiCitationSourceTitle(value);
|
||||
var headingPath = pageAiNormalizeArray(value.headingPath).map(function(item) {
|
||||
return searchText(item);
|
||||
}).filter(Boolean).join(' > ');
|
||||
var precisionLabel = pageAiCitationPrecisionLabel(value.locatorPrecision);
|
||||
var quote = searchText(value.displayQuote || value.quote || '');
|
||||
if (quote.length > 120) quote = quote.slice(0, 117) + '...';
|
||||
var head = markdown || title;
|
||||
if (citationId && head.indexOf(citationId) < 0) head = citationId + ' ' + head;
|
||||
var details = [];
|
||||
if (precisionLabel) details.push(precisionLabel);
|
||||
if (headingPath) details.push(headingPath);
|
||||
if (quote) details.push(quote);
|
||||
return [head].concat(details).filter(Boolean).join(' · ').trim();
|
||||
}
|
||||
|
||||
function pageAiFilterCitationCards(values) {
|
||||
var cards = pageAiNormalizeArray(values);
|
||||
var hasPreciseCitation = cards.some(function(value) {
|
||||
if (typeof value === 'string') return value.indexOf('来源定位降级') < 0;
|
||||
return value && typeof value === 'object' && searchText(value.citationMarkdown || '') && value.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 cards.map(pageAiCitationDisplayMarkdown).filter(function(card, index) {
|
||||
var source = cards[index];
|
||||
if (!card || seen[card]) return false;
|
||||
if (hasPreciseCitation) {
|
||||
if (typeof source === 'string' && source.indexOf('来源定位降级') >= 0) return false;
|
||||
if (source && typeof source === 'object' && source.locatorDegraded === true) return false;
|
||||
}
|
||||
seen[card] = true;
|
||||
return true;
|
||||
}).slice(0, 5);
|
||||
}
|
||||
|
||||
function pageAiCollectKnowledgeRagApiCitations(payload) {
|
||||
var structuredCitations = pageAiNormalizeArray(payload && payload.citations);
|
||||
if (structuredCitations.length) {
|
||||
return pageAiFilterCitationCards(structuredCitations);
|
||||
}
|
||||
var references = pageAiNormalizeArray(payload && payload.references);
|
||||
return pageAiFilterCitationCards(references);
|
||||
}
|
||||
|
||||
async function pageAiAppendKnowledgeRagFallbackCitations(runId, promptText) {
|
||||
if (!pageAiPromptNeedsKnowledgeRagCitations(promptText)) return;
|
||||
var rootUri = currentRootUri();
|
||||
if (!rootUri) return;
|
||||
var fallbackQuery = pageAiKnowledgeRagFallbackQuery(promptText);
|
||||
if (!fallbackQuery) return;
|
||||
try {
|
||||
var response = await fetch('/api/knowledge-rag/query', {
|
||||
var response = await fetch('/api/knowledge-rag/search', {
|
||||
method: 'POST',
|
||||
headers: { 'content-type': 'application/json', 'accept': 'application/json' },
|
||||
body: JSON.stringify({
|
||||
workspaceId: resolveWorkspaceId(document.body) || undefined,
|
||||
rootUri: rootUri,
|
||||
query: String(promptText || '').trim(),
|
||||
query: fallbackQuery,
|
||||
mode: 'mix',
|
||||
topK: 8,
|
||||
chunkTopK: 8,
|
||||
topK: 12,
|
||||
chunkTopK: 24,
|
||||
includeChunkContent: true
|
||||
})
|
||||
});
|
||||
@@ -2193,10 +2263,11 @@ export function createSidebarPageAiRuntime(context) {
|
||||
return;
|
||||
}
|
||||
if (typeof node !== 'object') return;
|
||||
if (typeof node.citationMarkdown === 'string') add(node.citationMarkdown);
|
||||
if (typeof node.citationMarkdown === 'string') add(pageAiCitationDisplayMarkdown(node));
|
||||
if (Array.isArray(node.citationMarkdowns)) {
|
||||
node.citationMarkdowns.forEach(function(value) {
|
||||
if (typeof value === 'string') add(value);
|
||||
else if (value && typeof value === 'object') add(pageAiCitationDisplayMarkdown(value));
|
||||
else visit(value);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -1342,9 +1342,21 @@ export function createSidebarPageSettingsRuntime(context) {
|
||||
var config = healthBody.configuration && typeof healthBody.configuration === 'object' ? healthBody.configuration : {};
|
||||
var llmModel = String(config.llm_model || config.llmModel || '');
|
||||
var vlmModel = String(config.vlm_model || config.vlmModel || '');
|
||||
var rerank = status.rerank && typeof status.rerank === 'object' ? status.rerank : {};
|
||||
var rerankStatus = String(rerank.status || 'unknown');
|
||||
var rerankBinding = searchText(rerank.binding || '');
|
||||
var rerankModel = searchText(rerank.model || '');
|
||||
var rerankLabel = rerankStatus === 'disabled'
|
||||
? 'disabled'
|
||||
: rerankStatus === 'available'
|
||||
? 'available'
|
||||
: rerankStatus === 'unavailable'
|
||||
? 'unavailable'
|
||||
: 'unknown';
|
||||
metaNode.innerHTML = '' +
|
||||
'<div><span>Dashboard</span><code>' + escapeHtml(dashboardUrl || '未配置') + '</code></div>' +
|
||||
'<div><span>Model</span><code>' + escapeHtml(llmModel || '未上报') + (vlmModel ? ' / VLM ' + escapeHtml(vlmModel) : '') + '</code></div>' +
|
||||
'<div><span>Rerank</span><code>' + escapeHtml(rerankLabel + (rerankBinding ? ' · ' + rerankBinding : '') + (rerankModel ? ' · ' + rerankModel : '')) + '</code></div>' +
|
||||
'<div><span>Input</span><code>' + escapeHtml(inputDir || '未配置') + '</code></div>' +
|
||||
'<div><span>Storage</span><code>' + escapeHtml(workingDir || '未上报') + '</code></div>';
|
||||
}
|
||||
|
||||
@@ -2135,6 +2135,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
'<div class="wolai-search-options-left">' +
|
||||
'<span class="wolai-search-switch-control"><span>仅匹配标题</span><button type="button" class="wolai-search-switch" data-search-switch="title" role="switch" aria-checked="false" aria-label="仅匹配标题"></button></span>' +
|
||||
'<span class="wolai-search-switch-control"><span>精确匹配</span><button type="button" class="wolai-search-switch" data-search-switch="exact" role="switch" aria-checked="false" aria-label="精确匹配"></button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>资料库模式</span><select class="wolai-search-sort-value" data-search-knowledge-mode aria-label="资料库检索模式"><option value="mix">综合</option><option value="hybrid">图谱混合</option><option value="naive">向量</option><option value="local">实体</option><option value="global">关系</option><option value="exact">关键词</option></select></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按编辑时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="updated" aria-label="按编辑时间范围">所有</button></span>' +
|
||||
'<span class="wolai-search-sort-control"><span>按创建时间</span><button type="button" class="wolai-search-sort-value" data-search-sort="created" aria-label="按创建时间范围">所有</button></span>' +
|
||||
'</div>' +
|
||||
@@ -2170,6 +2171,13 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
scheduleSearchResultsRender();
|
||||
});
|
||||
});
|
||||
var knowledgeModeSelect = overlay.querySelector('[data-search-knowledge-mode]');
|
||||
if (knowledgeModeSelect) {
|
||||
knowledgeModeSelect.addEventListener('change', function() {
|
||||
searchUiState.hasRendered = false;
|
||||
scheduleSearchResultsRender();
|
||||
});
|
||||
}
|
||||
if (closeButton) closeButton.addEventListener('click', closeSearchModal);
|
||||
overlay.addEventListener('click', function(event) {
|
||||
if (event.target === overlay) closeSearchModal();
|
||||
@@ -2186,6 +2194,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
metaHtml: '',
|
||||
resultsHtml: '',
|
||||
items: [],
|
||||
knowledgeMode: 'mix',
|
||||
sourceCollapsed: {},
|
||||
scrollTop: 0,
|
||||
signature: '',
|
||||
@@ -2236,6 +2245,27 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
return button instanceof HTMLElement && button.getAttribute('aria-checked') === 'true';
|
||||
}
|
||||
|
||||
function searchKnowledgeModeValue(overlay) {
|
||||
var select = overlay.querySelector('[data-search-knowledge-mode]');
|
||||
var value = select && 'value' in select ? searchText(select.value).toLowerCase() : '';
|
||||
if (['mix', 'hybrid', 'naive', 'local', 'global', 'exact'].indexOf(value) >= 0) return value;
|
||||
return 'mix';
|
||||
}
|
||||
|
||||
function knowledgeSearchRequestMode(overlay) {
|
||||
if (searchSwitchValue(overlay, 'exact')) return 'exact';
|
||||
return searchKnowledgeModeValue(overlay);
|
||||
}
|
||||
|
||||
function knowledgeSearchModeLabel(mode) {
|
||||
if (mode === 'exact') return '关键词';
|
||||
if (mode === 'naive') return '向量';
|
||||
if (mode === 'local') return '实体';
|
||||
if (mode === 'global') return '关系';
|
||||
if (mode === 'hybrid') return '图谱混合';
|
||||
return '综合';
|
||||
}
|
||||
|
||||
function readSearchCollapseSourcesDefault() {
|
||||
try {
|
||||
var stored = window.localStorage && window.localStorage.getItem(SEARCH_COLLAPSE_SOURCES_KEY);
|
||||
@@ -2284,6 +2314,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
title: Boolean(switches.title),
|
||||
exact: Boolean(switches.exact),
|
||||
collapseSource: Boolean(switches.collapseSource),
|
||||
knowledgeMode: searchKnowledgeModeValue(overlay),
|
||||
workspaceId: resolveWorkspaceId(document.body) || '',
|
||||
rootUri: currentRootUri() || ''
|
||||
});
|
||||
@@ -2297,6 +2328,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
var query = input && 'value' in input ? searchText(input.value) : searchUiState.query;
|
||||
searchUiState.query = query;
|
||||
searchUiState.switches = collectSearchSwitchState(overlay);
|
||||
searchUiState.knowledgeMode = searchKnowledgeModeValue(overlay);
|
||||
searchUiState.metaHtml = meta ? meta.innerHTML : searchUiState.metaHtml;
|
||||
searchUiState.resultsHtml = results ? results.innerHTML : searchUiState.resultsHtml;
|
||||
searchUiState.items = Array.isArray(window.__mnoteSearchResults) ? window.__mnoteSearchResults.slice() : searchUiState.items;
|
||||
@@ -2312,6 +2344,8 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
var results = overlay.querySelector('[data-testid="wolai-search-results"]');
|
||||
if (input && 'value' in input) input.value = searchUiState.query || '';
|
||||
applySearchSwitchState(overlay, searchUiState.switches);
|
||||
var knowledgeModeSelect = overlay.querySelector('[data-search-knowledge-mode]');
|
||||
if (knowledgeModeSelect && 'value' in knowledgeModeSelect) knowledgeModeSelect.value = searchUiState.knowledgeMode || 'mix';
|
||||
if (options instanceof HTMLElement) options.hidden = !searchText(searchUiState.query);
|
||||
if (meta && searchUiState.metaHtml) meta.innerHTML = searchUiState.metaHtml;
|
||||
if (results && searchUiState.resultsHtml) {
|
||||
@@ -2421,7 +2455,8 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
|
||||
function renderSearchResultButton(item, index, query, exact, grouped) {
|
||||
var title = searchText(item.title || item.name || item.documentTitle || '无标题');
|
||||
var snippet = cleanSearchDisplayText(item.snippet || item.evidence && item.evidence.quote || (Array.isArray(item.evidence) && item.evidence[0] && (item.evidence[0].quote || item.evidence[0].snippet)) || '');
|
||||
var snippet = searchText(item.displayQuote || item.snippet || item.evidence && item.evidence.displayQuote || item.evidence && item.evidence.quote || (Array.isArray(item.evidence) && item.evidence[0] && (item.evidence[0].displayQuote || item.evidence[0].quote || item.evidence[0].snippet)) || '');
|
||||
if (!item.displayQuote) snippet = cleanSearchDisplayText(snippet);
|
||||
var path = searchText(item.path || item.parentTitle || item.workspaceName || currentWorkspaceName());
|
||||
var resourceType = searchText(item.resourceType || item.resourceKind || 'page');
|
||||
var locator = searchResultEvidenceLocator(item);
|
||||
@@ -2545,10 +2580,11 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
var resourceKind = evidenceLocatorResourceKind(locator, item && item.resourceType);
|
||||
var overlay = document.querySelector('[data-testid="wolai-search-modal"]');
|
||||
var searchInput = overlay instanceof HTMLElement ? overlay.querySelector('[data-testid="wolai-search-input"]') : null;
|
||||
var searchQueryText = searchText(item && (item.query || item.searchQuery))
|
||||
var searchQueryText = searchText(item && (item.searchQuery || item.query) || locator && locator.openAction && locator.openAction.params && locator.openAction.params.searchQuery)
|
||||
|| searchText(searchInput && 'value' in searchInput ? searchInput.value : '')
|
||||
|| searchText(searchUiState.query);
|
||||
var locatorEvidenceText = searchText(locator.evidenceText || locator.query || locator.openAction && locator.openAction.params && locator.openAction.params.query || '');
|
||||
var locatorEvidenceText = searchText(item && item.locatorEvidenceText || locator.evidenceText || locator.query || locator.openAction && locator.openAction.params && (locator.openAction.params.evidenceText || locator.openAction.params.query) || '');
|
||||
var locatorParams = locator && locator.openAction && locator.openAction.params && typeof locator.openAction.params === 'object' ? locator.openAction.params : {};
|
||||
var openTarget = event && event.altKey ? 'side' : 'active-tab';
|
||||
if (resourcePath && resourceKind) {
|
||||
var fileName = resourcePath.split('/').filter(Boolean).pop() || resourcePath;
|
||||
@@ -2573,6 +2609,10 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
bbox: locator.bbox,
|
||||
sourceMapPath: searchText(locator.sourceMapPath || locator.source_map_path),
|
||||
blockId: evidenceLocatorBlockId(locator),
|
||||
paragraphOrdinal: searchText(locator.paragraphOrdinal || locatorParams.paragraphOrdinal),
|
||||
paraIdStart: searchText(locator.paraIdStart || locatorParams.paraIdStart),
|
||||
paraIdEnd: searchText(locator.paraIdEnd || locatorParams.paraIdEnd),
|
||||
textFingerprint: searchText(locator.textFingerprint || locatorParams.textFingerprint),
|
||||
evidenceText: locatorEvidenceText,
|
||||
query: searchQueryText,
|
||||
searchQuery: searchQueryText,
|
||||
@@ -2593,6 +2633,10 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
var bbox = evidenceLocatorBbox(locator);
|
||||
if (bbox) target.searchParams.set('bbox', bbox);
|
||||
if (sourceMapPath) target.searchParams.set('sourceMapPath', sourceMapPath);
|
||||
if (locatorParams.paragraphOrdinal != null) target.searchParams.set('paragraphOrdinal', searchText(locatorParams.paragraphOrdinal));
|
||||
if (locatorParams.paraIdStart) target.searchParams.set('paraIdStart', searchText(locatorParams.paraIdStart));
|
||||
if (locatorParams.paraIdEnd) target.searchParams.set('paraIdEnd', searchText(locatorParams.paraIdEnd));
|
||||
if (locatorParams.textFingerprint) target.searchParams.set('textFingerprint', searchText(locatorParams.textFingerprint));
|
||||
window.location.assign(target.pathname + target.search + target.hash);
|
||||
} catch (_) {
|
||||
window.location.assign(url);
|
||||
@@ -2643,6 +2687,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
return;
|
||||
}
|
||||
var requestId = ++activeSearchRequestId;
|
||||
var knowledgeRequestMode = knowledgeMode ? knowledgeSearchRequestMode(overlay) : '';
|
||||
meta.innerHTML = '<span>搜索中…</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
results.innerHTML = '<div class="wolai-search-empty" data-search-loading="true">搜索中…</div>';
|
||||
try {
|
||||
@@ -2653,7 +2698,7 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
workspaceId: resolveWorkspaceId(document.body),
|
||||
rootUri: currentRootUri() || null,
|
||||
query: query,
|
||||
mode: 'hybrid',
|
||||
mode: knowledgeRequestMode,
|
||||
topK: 12,
|
||||
chunkTopK: 24,
|
||||
includeChunkContent: true
|
||||
@@ -2677,7 +2722,8 @@ import { createSidebarPageSettingsRuntime } from './sidebar-page-settings-runtim
|
||||
var payload = await response.json();
|
||||
if (requestId !== activeSearchRequestId) return;
|
||||
var items = Array.isArray(payload.results) ? payload.results : [];
|
||||
meta.innerHTML = '<span>' + (knowledgeMode ? '资料库检索' : '工作区搜索') + ' · 共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
var ownerLabel = knowledgeMode ? ('资料库检索 · ' + knowledgeSearchModeLabel(payload.retrievalMode || knowledgeRequestMode)) : '工作区搜索';
|
||||
meta.innerHTML = '<span>' + ownerLabel + ' · 共 ' + items.length + ' 条匹配结果</span><span>Ctrl + Enter 新窗口打开 / Alt + Enter 右侧边栏打开</span>';
|
||||
if (!items.length) {
|
||||
results.innerHTML = '<div class="wolai-search-empty" data-search-empty="true">没有搜索结果</div>';
|
||||
window.__mnoteSearchResults = [];
|
||||
|
||||
@@ -49,11 +49,29 @@ pub struct AcpRunBridge {
|
||||
}
|
||||
|
||||
fn collect_citation_markdowns_from_value(value: &Value) -> Vec<Value> {
|
||||
fn add_citation(text: &str, seen: &mut HashSet<String>, out: &mut Vec<Value>) {
|
||||
let citation = text.trim();
|
||||
if !citation.is_empty() && seen.insert(citation.to_string()) {
|
||||
out.push(json!({ "citationMarkdown": citation }));
|
||||
fn add_citation_value(value: &Value, seen: &mut HashSet<String>, out: &mut Vec<Value>) {
|
||||
let Some(citation) = value.get("citationMarkdown").and_then(Value::as_str) else {
|
||||
return;
|
||||
};
|
||||
let citation = citation.trim();
|
||||
if citation.is_empty() || !seen.insert(citation.to_string()) {
|
||||
return;
|
||||
}
|
||||
out.push(json!({
|
||||
"schema": value.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.citation.v1")),
|
||||
"citationMarkdown": citation,
|
||||
"citationId": value.get("citationId").cloned().unwrap_or(Value::Null),
|
||||
"citationLabel": value.get("citationLabel").cloned().unwrap_or(Value::Null),
|
||||
"sourceRootRelativePath": value.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
|
||||
"sourcePath": value.get("sourcePath").cloned().unwrap_or(Value::Null),
|
||||
"filePath": value.get("filePath").or_else(|| value.get("lightRagFilePath")).cloned().unwrap_or(Value::Null),
|
||||
"headingPath": value.get("headingPath").cloned().unwrap_or_else(|| json!([])),
|
||||
"displayQuote": value.get("displayQuote").or_else(|| value.get("quote")).cloned().unwrap_or(Value::Null),
|
||||
"locatorEvidenceText": value.get("locatorEvidenceText").cloned().unwrap_or(Value::Null),
|
||||
"locatorPrecision": value.get("locatorPrecision").cloned().unwrap_or(Value::Null),
|
||||
"locatorDegraded": value.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
|
||||
"citationUrl": value.get("citationUrl").cloned().unwrap_or(Value::Null),
|
||||
}));
|
||||
}
|
||||
|
||||
fn add_reference_citations(
|
||||
@@ -73,7 +91,7 @@ fn collect_citation_markdowns_from_value(value: &Value) -> Vec<Value> {
|
||||
if out.len() >= 8 {
|
||||
break;
|
||||
}
|
||||
let Some(citation) = reference.get("citationMarkdown").and_then(Value::as_str) else {
|
||||
let Some(_citation) = reference.get("citationMarkdown").and_then(Value::as_str) else {
|
||||
continue;
|
||||
};
|
||||
if has_precise
|
||||
@@ -82,7 +100,7 @@ fn collect_citation_markdowns_from_value(value: &Value) -> Vec<Value> {
|
||||
continue;
|
||||
}
|
||||
let before = out.len();
|
||||
add_citation(citation, seen, out);
|
||||
add_citation_value(reference, seen, out);
|
||||
added = added || out.len() > before;
|
||||
}
|
||||
added
|
||||
@@ -120,9 +138,9 @@ fn collect_citation_markdowns_from_value(value: &Value) -> Vec<Value> {
|
||||
.get("references")
|
||||
.and_then(Value::as_array)
|
||||
.is_some_and(|references| add_reference_citations(references, seen, out));
|
||||
if let Some(citation) = map.get("citationMarkdown").and_then(Value::as_str) {
|
||||
if let Some(_citation) = map.get("citationMarkdown").and_then(Value::as_str) {
|
||||
if !has_filtered_references {
|
||||
add_citation(citation, seen, out);
|
||||
add_citation_value(value, seen, out);
|
||||
}
|
||||
}
|
||||
for (key, item) in map {
|
||||
|
||||
@@ -1,245 +0,0 @@
|
||||
use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::{doc, ToolCallInput};
|
||||
use crate::routes;
|
||||
use axum::http::StatusCode;
|
||||
use core_protocol::{
|
||||
EvidenceLocator, EvidenceOpenAction, EvidenceOpenRequest, EvidenceReadRequest,
|
||||
EvidenceResourceKind, EvidenceSearchRequest, EVIDENCE_LOCATOR_SCHEMA,
|
||||
};
|
||||
use serde_json::{json, Value};
|
||||
|
||||
pub async fn evidence_search(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let body = evidence_search_request(input, context)?;
|
||||
routes::evidence::search_payload(state, context, body).await
|
||||
}
|
||||
|
||||
pub async fn evidence_read(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
let body = serde_json::from_value::<EvidenceReadRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_evidence_read_payload_invalid",
|
||||
format!("Evidence read 参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
routes::evidence::read_payload(state, context, body).await
|
||||
}
|
||||
|
||||
pub async fn evidence_open(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
let body = serde_json::from_value::<EvidenceOpenRequest>(args).map_err(|error| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_evidence_open_payload_invalid",
|
||||
format!("Evidence open 参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
routes::evidence::open_payload(state, context, body).await
|
||||
}
|
||||
|
||||
pub async fn legacy_docs_search(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
let payload = evidence_search(state, context, input).await?;
|
||||
Ok(json!({
|
||||
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
|
||||
"compatTool": "docs_search",
|
||||
"results": payload.get("results").cloned().unwrap_or_else(|| json!([])),
|
||||
"evidence": payload.get("results").cloned().unwrap_or_else(|| json!([])),
|
||||
"source": "mnote.evidence.search",
|
||||
}))
|
||||
}
|
||||
|
||||
pub async fn legacy_docs_read(
|
||||
state: &AppState,
|
||||
context: &RequestContext,
|
||||
input: &ToolCallInput,
|
||||
) -> Result<Value, WebError> {
|
||||
if input.arg_value("locator").is_some() {
|
||||
let payload = evidence_read(state, context, input).await?;
|
||||
return Ok(json!({
|
||||
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
|
||||
"compatTool": "docs_read",
|
||||
"result": payload,
|
||||
"source": "mnote.evidence.read",
|
||||
}));
|
||||
}
|
||||
|
||||
let document = doc::doc_fetch(state, context, input).await?;
|
||||
let locator = legacy_document_locator(input);
|
||||
Ok(json!({
|
||||
"ok": document.get("ok").cloned().unwrap_or_else(|| json!(true)),
|
||||
"compatTool": "docs_read",
|
||||
"documentId": input.effective_document_id(),
|
||||
"document": document,
|
||||
"evidence": locator.as_ref().map(|locator| json!({ "source": locator })),
|
||||
"source": {
|
||||
"tool": "mnote.doc.fetch",
|
||||
"locator": locator,
|
||||
},
|
||||
}))
|
||||
}
|
||||
|
||||
fn evidence_search_request(
|
||||
input: &ToolCallInput,
|
||||
context: &RequestContext,
|
||||
) -> Result<EvidenceSearchRequest, WebError> {
|
||||
let mut args = input.args.clone().unwrap_or_else(|| json!({}));
|
||||
if args.get("scope").is_none() {
|
||||
let query = input.arg_string("query").ok_or_else(|| {
|
||||
WebError::bad_request_code("mnote_evidence_query_required", "Evidence 搜索缺少 query")
|
||||
.with_context(context)
|
||||
})?;
|
||||
let workspace_id = input.effective_workspace_id().ok_or_else(|| {
|
||||
WebError::bad_request_code(
|
||||
"mnote_evidence_workspace_required",
|
||||
"Evidence 搜索缺少 workspaceId",
|
||||
)
|
||||
.with_context(context)
|
||||
})?;
|
||||
let root_uri = local_root_uri_for_evidence(input).ok_or_else(|| {
|
||||
WebError::bad_request_code("mnote_evidence_root_required", "Evidence 搜索缺少 rootUri")
|
||||
.with_context(context)
|
||||
})?;
|
||||
let include_resources = input
|
||||
.args
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("includeResources"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true);
|
||||
let include_ocr = input
|
||||
.args
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("includeOcr"))
|
||||
.and_then(Value::as_bool)
|
||||
.unwrap_or(true);
|
||||
let target_document_id = input
|
||||
.effective_document_id()
|
||||
.or_else(|| input.arg_string("pageId"))
|
||||
.or_else(|| input.arg_string("targetDocumentId"));
|
||||
args = json!({
|
||||
"query": query,
|
||||
"scope": {
|
||||
"workspaceId": workspace_id,
|
||||
"rootUri": root_uri,
|
||||
"targetDocumentId": target_document_id,
|
||||
"includeResources": include_resources,
|
||||
"includeOcr": include_ocr,
|
||||
},
|
||||
"mode": input.arg_string("mode").unwrap_or_else(|| "hybrid".into()),
|
||||
"topK": input
|
||||
.args
|
||||
.as_ref()
|
||||
.and_then(|value| value.get("topK").or_else(|| value.get("limit")))
|
||||
.and_then(Value::as_u64)
|
||||
.unwrap_or(8),
|
||||
});
|
||||
}
|
||||
serde_json::from_value::<EvidenceSearchRequest>(args).map_err(|error| {
|
||||
WebError::new(
|
||||
StatusCode::BAD_REQUEST,
|
||||
"mnote_evidence_search_payload_invalid",
|
||||
format!("Evidence search 参数无效: {error}"),
|
||||
)
|
||||
.with_context(context)
|
||||
})
|
||||
}
|
||||
|
||||
fn legacy_document_locator(input: &ToolCallInput) -> Option<EvidenceLocator> {
|
||||
let root_uri = local_root_uri_for_evidence(input)?;
|
||||
let document_id = input.effective_document_id()?;
|
||||
let owner_document_path =
|
||||
document_path_from_local_id(&document_id).unwrap_or_else(|| document_id.trim().to_string());
|
||||
Some(EvidenceLocator {
|
||||
schema: EVIDENCE_LOCATOR_SCHEMA.into(),
|
||||
root_uri: root_uri.clone(),
|
||||
owner_document_id: document_id,
|
||||
owner_document_path: owner_document_path.clone(),
|
||||
resource_path: Some(owner_document_path.clone()),
|
||||
resource_kind: EvidenceResourceKind::Markdown,
|
||||
page: None,
|
||||
bbox: None,
|
||||
section_path: Vec::new(),
|
||||
line_range: None,
|
||||
char_range: None,
|
||||
block_id: None,
|
||||
source_map_path: None,
|
||||
open_action: EvidenceOpenAction {
|
||||
action_type: "mnote.open_resource_locator".into(),
|
||||
url: "/".into(),
|
||||
params: json!({
|
||||
"rootUri": root_uri,
|
||||
"ownerDocumentPath": owner_document_path,
|
||||
}),
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
fn local_root_uri_for_evidence(input: &ToolCallInput) -> Option<String> {
|
||||
input.effective_root_uri().or_else(|| {
|
||||
input
|
||||
.arg_value("aiAccessScope")
|
||||
.and_then(|scope| {
|
||||
scope
|
||||
.get("allowedRoots")
|
||||
.or_else(|| scope.get("allowed_roots"))
|
||||
.cloned()
|
||||
})
|
||||
.and_then(|allowed_roots| {
|
||||
allowed_roots.as_array().and_then(|roots| {
|
||||
roots
|
||||
.iter()
|
||||
.filter_map(|root| {
|
||||
root.get("rootUri")
|
||||
.or_else(|| root.get("root_uri"))
|
||||
.and_then(Value::as_str)
|
||||
})
|
||||
.map(str::trim)
|
||||
.find(|root_uri| !root_uri.is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
fn document_path_from_local_id(document_id: &str) -> Option<String> {
|
||||
let encoded = document_id.trim().strip_prefix("local-md:")?;
|
||||
decode_local_id_segment(encoded)
|
||||
}
|
||||
|
||||
fn decode_local_id_segment(value: &str) -> Option<String> {
|
||||
let bytes = value.as_bytes();
|
||||
let mut decoded = Vec::with_capacity(bytes.len());
|
||||
let mut index = 0;
|
||||
while index < bytes.len() {
|
||||
if bytes[index] == b'~' {
|
||||
if index + 2 >= bytes.len() {
|
||||
return None;
|
||||
}
|
||||
let hex = &value[index + 1..index + 3];
|
||||
let byte = u8::from_str_radix(hex, 16).ok()?;
|
||||
decoded.push(byte);
|
||||
index += 3;
|
||||
} else {
|
||||
decoded.push(bytes[index]);
|
||||
index += 1;
|
||||
}
|
||||
}
|
||||
String::from_utf8(decoded).ok()
|
||||
}
|
||||
@@ -112,21 +112,33 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
|
||||
.collect::<Vec<_>>()
|
||||
})
|
||||
.unwrap_or_default();
|
||||
let citation_references = citation_references_for_ui(&references);
|
||||
let citations = citation_references
|
||||
let payload_citations = payload
|
||||
.get("citations")
|
||||
.and_then(Value::as_array)
|
||||
.cloned()
|
||||
.unwrap_or_default();
|
||||
let citations = if payload_citations.is_empty() {
|
||||
citation_references_for_ui(&references)
|
||||
.into_iter()
|
||||
.map(compact_citation_for_agent)
|
||||
.collect::<Vec<_>>()
|
||||
} else {
|
||||
citation_values_for_ui(&payload_citations)
|
||||
.into_iter()
|
||||
.map(compact_citation_for_agent)
|
||||
.collect::<Vec<_>>()
|
||||
};
|
||||
let citation_markdowns = citations
|
||||
.iter()
|
||||
.filter_map(|reference| {
|
||||
reference
|
||||
.filter_map(|citation| {
|
||||
citation
|
||||
.get("citationMarkdown")
|
||||
.and_then(Value::as_str)
|
||||
.filter(|value| !value.trim().is_empty())
|
||||
.map(ToOwned::to_owned)
|
||||
})
|
||||
.collect::<Vec<_>>();
|
||||
let ui_citations = citations
|
||||
.iter()
|
||||
.map(|citation| json!({ "citationMarkdown": citation }))
|
||||
.collect::<Vec<_>>();
|
||||
let ui_citations = citations.clone();
|
||||
json!({
|
||||
"ok": payload.get("ok").cloned().unwrap_or_else(|| json!(true)),
|
||||
"schema": "mnote.knowledge_rag.agent_query_result.v1",
|
||||
@@ -134,6 +146,7 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
|
||||
"answerGuidance": "Final answers should answer the user in plain text. Do not copy citationMarkdown, citationUrl, /documents, mnote://, or search-engine wrapped local links into the answer. MNote UI will render citations[] / references[].citationMarkdown after the answer as clickable source locators. Treat rawMetadata.keywords and entity/relation counts only as query analysis, not proof that a source contains those words. Use references[].quote/contentDiagnostics as evidence. If locatorDegraded is true, say the source location is degraded instead of inventing page or bbox. If contentDiagnostics.ocrTextExposed is false, say OCR text was not exposed in the returned reference instead of claiming OCR succeeded.",
|
||||
"references": references,
|
||||
"citations": citations,
|
||||
"citationMarkdowns": citation_markdowns,
|
||||
"uiCitations": ui_citations,
|
||||
"citationRendering": "MNote UI appends uiCitations/citations after the final answer; agent must not hand-write citation links.",
|
||||
"sourceScope": payload.get("sourceScope").cloned().unwrap_or(Value::Array(Vec::new())),
|
||||
@@ -148,7 +161,13 @@ fn compact_query_result_for_agent(payload: Value) -> Value {
|
||||
|
||||
fn compact_reference_for_agent(reference: &Value) -> Value {
|
||||
let quote = reference
|
||||
.get("quote")
|
||||
.get("displayQuote")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| reference.get("quote").and_then(Value::as_str))
|
||||
.map(|value| value.chars().take(700).collect::<String>())
|
||||
.unwrap_or_default();
|
||||
let locator_evidence_text = reference
|
||||
.get("locatorEvidenceText")
|
||||
.and_then(Value::as_str)
|
||||
.map(|value| value.chars().take(700).collect::<String>())
|
||||
.unwrap_or_default();
|
||||
@@ -159,28 +178,74 @@ fn compact_reference_for_agent(reference: &Value) -> Value {
|
||||
json!({
|
||||
"schema": reference.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.reference.v1")),
|
||||
"provider": reference.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
|
||||
"citationId": reference.get("citationId").cloned().unwrap_or(Value::Null),
|
||||
"citationLabel": reference.get("citationLabel").cloned().unwrap_or(Value::Null),
|
||||
"filePath": reference.get("filePath").cloned().unwrap_or(Value::Null),
|
||||
"sourceRootRelativePath": reference.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
|
||||
"chunkId": reference.get("chunkId").cloned().unwrap_or(Value::Null),
|
||||
"quote": quote,
|
||||
"displayQuote": reference.get("displayQuote").cloned().unwrap_or(Value::Null),
|
||||
"locatorEvidenceText": locator_evidence_text,
|
||||
"quoteSource": reference.get("quoteSource").cloned().unwrap_or(Value::Null),
|
||||
"locator": reference.get("locator").cloned().unwrap_or(Value::Null),
|
||||
"locatorPrecision": reference.get("locatorPrecision").cloned().unwrap_or(Value::Null),
|
||||
"locatorDegraded": reference.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
|
||||
"contentDiagnostics": quote_diagnostics,
|
||||
"citationDiagnostics": reference.get("citationDiagnostics").cloned().unwrap_or(Value::Null),
|
||||
"citationMarkdown": reference.get("citationMarkdown").cloned().unwrap_or(Value::Null),
|
||||
"citationUrl": reference.get("citationUrl").cloned().unwrap_or(Value::Null),
|
||||
})
|
||||
}
|
||||
|
||||
fn compact_citation_for_agent(citation: &Value) -> Value {
|
||||
let display_quote = citation
|
||||
.get("displayQuote")
|
||||
.and_then(Value::as_str)
|
||||
.or_else(|| citation.get("quote").and_then(Value::as_str))
|
||||
.map(|value| value.chars().take(420).collect::<String>())
|
||||
.unwrap_or_default();
|
||||
let locator_evidence_text = citation
|
||||
.get("locatorEvidenceText")
|
||||
.and_then(Value::as_str)
|
||||
.map(|value| value.chars().take(420).collect::<String>())
|
||||
.unwrap_or_default();
|
||||
json!({
|
||||
"schema": citation.get("schema").cloned().unwrap_or_else(|| json!("mnote.knowledge_rag.citation.v1")),
|
||||
"provider": citation.get("provider").cloned().unwrap_or_else(|| json!("lightrag")),
|
||||
"citationId": citation.get("citationId").cloned().unwrap_or(Value::Null),
|
||||
"citationLabel": citation.get("citationLabel").cloned().unwrap_or(Value::Null),
|
||||
"sourceId": citation.get("sourceId").cloned().unwrap_or(Value::Null),
|
||||
"sourcePath": citation.get("sourcePath").cloned().unwrap_or(Value::Null),
|
||||
"sourceRootRelativePath": citation.get("sourceRootRelativePath").cloned().unwrap_or(Value::Null),
|
||||
"filePath": citation.get("filePath").or_else(|| citation.get("lightRagFilePath")).cloned().unwrap_or(Value::Null),
|
||||
"chunkId": citation.get("chunkId").or_else(|| citation.get("lightRagChunkId")).cloned().unwrap_or(Value::Null),
|
||||
"blockId": citation.get("blockId").cloned().unwrap_or(Value::Null),
|
||||
"headingPath": citation.get("headingPath").cloned().unwrap_or_else(|| json!([])),
|
||||
"quote": display_quote,
|
||||
"displayQuote": citation.get("displayQuote").cloned().unwrap_or(Value::Null),
|
||||
"locatorEvidenceText": locator_evidence_text,
|
||||
"searchQuery": citation.get("searchQuery").cloned().unwrap_or(Value::Null),
|
||||
"locatorPrecision": citation.get("locatorPrecision").cloned().unwrap_or(Value::Null),
|
||||
"locatorDegraded": citation.get("locatorDegraded").cloned().unwrap_or(Value::Bool(true)),
|
||||
"citationMarkdown": citation.get("citationMarkdown").cloned().unwrap_or(Value::Null),
|
||||
"citationUrl": citation.get("citationUrl").cloned().unwrap_or(Value::Null),
|
||||
"diagnostics": citation.get("diagnostics").or_else(|| citation.get("citationDiagnostics")).cloned().unwrap_or(Value::Null),
|
||||
})
|
||||
}
|
||||
|
||||
fn citation_references_for_ui(references: &[Value]) -> Vec<&Value> {
|
||||
let has_precise = references.iter().any(|reference| {
|
||||
citation_values_for_ui(references)
|
||||
}
|
||||
|
||||
fn citation_values_for_ui(values: &[Value]) -> Vec<&Value> {
|
||||
let has_precise = values.iter().any(|reference| {
|
||||
reference
|
||||
.get("citationMarkdown")
|
||||
.and_then(Value::as_str)
|
||||
.is_some_and(|value| !value.trim().is_empty())
|
||||
&& reference.get("locatorDegraded").and_then(Value::as_bool) != Some(true)
|
||||
});
|
||||
references
|
||||
values
|
||||
.iter()
|
||||
.filter(|reference| {
|
||||
reference
|
||||
@@ -269,6 +334,41 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_query_result_prefers_structured_citations() {
|
||||
let payload = json!({
|
||||
"references": [{
|
||||
"sourceRootRelativePath": "docs/a.docx",
|
||||
"displayQuote": "raw reference should not be source card",
|
||||
"locatorDegraded": false,
|
||||
"citationMarkdown": "[docs/a.docx](/documents/a)"
|
||||
}],
|
||||
"citations": [{
|
||||
"schema": "mnote.knowledge_rag.citation.v1",
|
||||
"citationId": "c0de",
|
||||
"citationLabel": "[c0de]",
|
||||
"sourceRootRelativePath": "docs/a.docx",
|
||||
"headingPath": ["保护基", "硅基保护"],
|
||||
"displayQuote": "吡咯烷,5 h,90%",
|
||||
"locatorEvidenceText": "如果叔丁基用 TFA 裂解, 吡咯烷将不会去除 BOC 亚乙基。",
|
||||
"locatorPrecision": "paragraph",
|
||||
"locatorDegraded": false,
|
||||
"citationMarkdown": "[docs/a.docx](/documents/a)"
|
||||
}]
|
||||
});
|
||||
|
||||
let compact = compact_query_result_for_agent(payload);
|
||||
|
||||
assert_eq!(compact["citations"][0]["citationId"], "c0de");
|
||||
assert_eq!(compact["citations"][0]["headingPath"][0], "保护基");
|
||||
assert_eq!(compact["citations"][0]["displayQuote"], "吡咯烷,5 h,90%");
|
||||
assert!(compact["citations"][0].get("rawQuote").is_none());
|
||||
assert_eq!(
|
||||
compact["uiCitations"][0]["citationMarkdown"].as_str(),
|
||||
Some("[docs/a.docx](/documents/a)")
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn compact_reference_marks_image_placeholder_as_not_ocr_text() {
|
||||
let payload = json!({
|
||||
|
||||
@@ -290,108 +290,6 @@ fn doc_find_tool() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
// 旧 evidence 工具仅保留为历史对照;当前 manifest() 不注册这些工具,资料库问答走 mnote.knowledge_rag.*。
|
||||
#[allow(dead_code)]
|
||||
fn evidence_search_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("query".into(), json!({ "type": "string" }));
|
||||
map.insert("rootUri".into(), json!({ "type": "string" }));
|
||||
map.insert(
|
||||
"includeResources".into(),
|
||||
json!({ "type": "boolean", "default": true }),
|
||||
);
|
||||
map.insert(
|
||||
"includeOcr".into(),
|
||||
json!({ "type": "boolean", "default": true }),
|
||||
);
|
||||
map.insert(
|
||||
"mode".into(),
|
||||
json!({ "type": "string", "enum": ["keyword", "tree", "hybrid", "graph"], "default": "hybrid" }),
|
||||
);
|
||||
map.insert("topK".into(), json!({ "type": "integer", "default": 8 }));
|
||||
map.insert(
|
||||
"scope".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"workspaceId": { "type": "string" },
|
||||
"rootUri": { "type": "string" },
|
||||
"targetDocumentId": { "type": "string" },
|
||||
"includeResources": { "type": "boolean" },
|
||||
"includeOcr": { "type": "boolean" }
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.evidence.search",
|
||||
"description": "搜索可回跳原文的文档证据,返回 quote、EvidenceLocator、openAction 与可直接放进最终回答的 citationMarkdown 链接。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["evidence.read", "page.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "rootUri", "query"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn evidence_read_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("locator".into(), json!({ "type": "object" }));
|
||||
map.insert(
|
||||
"context".into(),
|
||||
json!({
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"beforeBlocks": { "type": "integer", "default": 3 },
|
||||
"afterBlocks": { "type": "integer", "default": 3 },
|
||||
"includeSectionSummary": { "type": "boolean", "default": true }
|
||||
}
|
||||
}),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.evidence.read",
|
||||
"description": "按 EvidenceLocator 读取原文证据及周边上下文,返回 quote/contextBlocks 与可点击引用链接供回答引用。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["evidence.read", "page.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["locator"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
#[allow(dead_code)]
|
||||
fn evidence_open_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("locator".into(), json!({ "type": "object" }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.evidence.open",
|
||||
"description": "把 EvidenceLocator 归一化为 MNote 可执行的打开/定位动作,并返回 citationUrl/citationMarkdown。",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["evidence.read", "page.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["locator"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn knowledge_rag_status_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
|
||||
@@ -2,7 +2,6 @@ pub mod artifact;
|
||||
pub mod block;
|
||||
pub mod context_tools;
|
||||
pub mod doc;
|
||||
pub mod evidence;
|
||||
pub mod index;
|
||||
pub mod knowledge_rag;
|
||||
pub mod manifest;
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -212,7 +212,7 @@ mod tests {
|
||||
"dryRun": false
|
||||
},
|
||||
"tool": {
|
||||
"tool": "docs_search",
|
||||
"tool": "mnote.knowledge_rag.query",
|
||||
"kind": "query",
|
||||
"mode": "plan",
|
||||
"argsJson": {"query": "Rust Web"},
|
||||
|
||||
@@ -3,8 +3,8 @@ use crate::app::AppState;
|
||||
use crate::context::RequestContext;
|
||||
use crate::error::WebError;
|
||||
use crate::hermes_tools::{
|
||||
artifact, block, context_tools, doc, evidence, knowledge_rag, manifest, onlyoffice_live, page,
|
||||
resource, skill, ToolCallInput,
|
||||
artifact, block, context_tools, doc, knowledge_rag, manifest, onlyoffice_live, page, resource,
|
||||
skill, ToolCallInput,
|
||||
};
|
||||
use axum::extract::{Extension, Query, State};
|
||||
use axum::http::{HeaderMap, HeaderName, HeaderValue, StatusCode};
|
||||
@@ -360,13 +360,15 @@ pub(crate) async fn execute_mnote_tool_call(
|
||||
}
|
||||
"mnote.doc.fetch" => doc::doc_fetch(&state, &context, &input).await,
|
||||
"mnote.doc.find" => doc::doc_find(&state, &context, &input).await,
|
||||
"docs_search" => evidence::legacy_docs_search(&state, &context, &input).await,
|
||||
"docs_read" => evidence::legacy_docs_read(&state, &context, &input).await,
|
||||
"mnote.evidence.search" | "mnote.evidence.read" | "mnote.evidence.open" => {
|
||||
"docs_search"
|
||||
| "docs_read"
|
||||
| "mnote.evidence.search"
|
||||
| "mnote.evidence.read"
|
||||
| "mnote.evidence.open" => {
|
||||
Err(WebError::new(
|
||||
StatusCode::GONE,
|
||||
"mnote_evidence_tools_retired",
|
||||
"旧 evidence / LiteParse tools 已退役;请使用 mnote.knowledge_rag.query/open_reference",
|
||||
"旧 docs/evidence/LiteParse tools 已退役;请使用 mnote.knowledge_rag.query/open_reference",
|
||||
)
|
||||
.with_context(&context))
|
||||
}
|
||||
@@ -5466,38 +5468,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_legacy_docs_search_forwards_to_evidence_search() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("mnote-docs-search-evidence-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join(".mnote")).expect("metadata");
|
||||
fs::write(
|
||||
root.join(".mnote").join("workspace.json"),
|
||||
r#"{"workspaceId":"local-ws-docs-search","ownerId":"user_1","createdAt":"2026-06-03T00:00:00Z","capabilities":["local_files"]}"#,
|
||||
)
|
||||
.expect("manifest");
|
||||
fs::write(
|
||||
root.join("README.md"),
|
||||
"# Evidence Home\n\ncompat-evidence-token 正文\n",
|
||||
)
|
||||
.expect("markdown");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
crate::routes::local_search_index::write_local_index_settings(
|
||||
&root,
|
||||
&[String::from(".")],
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
None,
|
||||
)
|
||||
.expect("settings");
|
||||
crate::routes::local_search_index::refresh_local_search_index(
|
||||
&root,
|
||||
&root_uri,
|
||||
"local-ws-docs-search",
|
||||
)
|
||||
.expect("refresh");
|
||||
|
||||
async fn hermes_tools_legacy_docs_search_is_retired() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -5511,7 +5482,7 @@ mod tests {
|
||||
"toolName": "docs_search",
|
||||
"workspaceId": "local-ws-docs-search",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"rootUri": "file:///tmp/mnote-retired-docs-search",
|
||||
"sessionId": "sess_docs_search",
|
||||
"runId": "run_docs_search",
|
||||
"toolCallId": "call_docs_search",
|
||||
@@ -5529,72 +5500,15 @@ mod tests {
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::GONE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["result"]["compatTool"], "docs_search");
|
||||
assert_eq!(payload["result"]["source"], "mnote.evidence.search");
|
||||
let result = payload["result"]["results"]
|
||||
.as_array()
|
||||
.and_then(|items| items.first())
|
||||
.expect("evidence result");
|
||||
assert_eq!(
|
||||
result["source"]["schema"].as_str(),
|
||||
Some("mnote.evidence_locator.v1")
|
||||
payload["code"].as_str(),
|
||||
Some("mnote_evidence_tools_retired")
|
||||
);
|
||||
assert_eq!(
|
||||
result["source"]["ownerDocumentPath"].as_str(),
|
||||
Some("README.md")
|
||||
);
|
||||
assert!(result["citationMarkdown"]
|
||||
.as_str()
|
||||
.is_some_and(|value| value.contains("](/documents/")));
|
||||
assert!(result["citationUrl"]
|
||||
.as_str()
|
||||
.is_some_and(|value| value.contains("resourceTab=")));
|
||||
let evidence_id = result["evidenceId"].as_str().expect("evidence id");
|
||||
assert_eq!(
|
||||
payload["result"]["evidenceIds"][0].as_str(),
|
||||
Some(evidence_id)
|
||||
);
|
||||
assert_eq!(
|
||||
payload["result"]["runReceipt"]["schema"].as_str(),
|
||||
Some("mnote.agent_run_receipt.evidence.v1")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["result"]["runReceipt"]["evidenceIds"][0].as_str(),
|
||||
Some(evidence_id)
|
||||
);
|
||||
assert_eq!(payload["evidenceIds"][0].as_str(), Some(evidence_id));
|
||||
assert_eq!(
|
||||
payload["runReceipt"]["toolCallId"].as_str(),
|
||||
Some("call_docs_search")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["audit"]["evidenceIds"][0].as_str(),
|
||||
Some(evidence_id)
|
||||
);
|
||||
let completed_audit =
|
||||
super::audit_events(Some("trace_docs_search"), Some("call_docs_search"))
|
||||
.into_iter()
|
||||
.find(|event| event["phase"] == "completed")
|
||||
.expect("completed audit");
|
||||
assert_eq!(
|
||||
completed_audit["audit"]["runReceipt"]["evidenceIds"][0].as_str(),
|
||||
Some(evidence_id)
|
||||
);
|
||||
assert!(payload["result"]["evidence"]
|
||||
.as_array()
|
||||
.expect("evidence")
|
||||
.iter()
|
||||
.any(|item| item["quote"]
|
||||
.as_str()
|
||||
.unwrap_or_default()
|
||||
.contains("compat-evidence-token")));
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
@@ -5697,19 +5611,7 @@ mod tests {
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn hermes_tools_legacy_docs_read_returns_document_with_locator() {
|
||||
let root =
|
||||
std::env::temp_dir().join(format!("mnote-docs-read-evidence-{}", std::process::id()));
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
fs::create_dir_all(root.join(".mnote")).expect("metadata");
|
||||
fs::write(
|
||||
root.join(".mnote").join("workspace.json"),
|
||||
r#"{"workspaceId":"local-ws-docs-read","ownerId":"user_1","createdAt":"2026-06-03T00:00:00Z","capabilities":["local_files"]}"#,
|
||||
)
|
||||
.expect("manifest");
|
||||
fs::write(root.join("README.md"), "# Docs Read\n\nlegacy docs read\n").expect("markdown");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
async fn hermes_tools_legacy_docs_read_is_retired() {
|
||||
let response = app()
|
||||
.oneshot(
|
||||
Request::builder()
|
||||
@@ -5724,7 +5626,7 @@ mod tests {
|
||||
"workspaceId": "local-ws-docs-read",
|
||||
"documentId": "local-md:README.md",
|
||||
"sourceKind": "local_folder",
|
||||
"rootUri": root_uri,
|
||||
"rootUri": "file:///tmp/mnote-retired-docs-read",
|
||||
"sessionId": "sess_docs_read",
|
||||
"runId": "run_docs_read",
|
||||
"toolCallId": "call_docs_read",
|
||||
@@ -5741,25 +5643,15 @@ mod tests {
|
||||
.await
|
||||
.expect("response");
|
||||
|
||||
assert_eq!(response.status(), StatusCode::OK);
|
||||
assert_eq!(response.status(), StatusCode::GONE);
|
||||
let body = to_bytes(response.into_body(), usize::MAX)
|
||||
.await
|
||||
.expect("body");
|
||||
let payload: Value = serde_json::from_slice(&body).expect("json");
|
||||
assert_eq!(payload["result"]["compatTool"], "docs_read");
|
||||
assert_eq!(
|
||||
payload["result"]["source"]["locator"]["schema"].as_str(),
|
||||
Some("mnote.evidence_locator.v1")
|
||||
payload["code"].as_str(),
|
||||
Some("mnote_evidence_tools_retired")
|
||||
);
|
||||
assert_eq!(
|
||||
payload["result"]["source"]["locator"]["ownerDocumentPath"].as_str(),
|
||||
Some("README.md")
|
||||
);
|
||||
assert!(payload["result"]["document"]
|
||||
.to_string()
|
||||
.contains("legacy docs read"));
|
||||
|
||||
let _ = fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -5,9 +5,12 @@ use crate::routes::local_markdown_parser::{
|
||||
};
|
||||
use crate::routes::local_ocr;
|
||||
use control_plane::{ControlPlaneStore, UpsertUserInput, UpsertUserUiPreferenceInput};
|
||||
#[cfg(test)]
|
||||
use core_protocol::EvidenceSearchMatchInfo;
|
||||
#[cfg(test)]
|
||||
use core_protocol::{EvidenceLocator, EvidenceSearchResult};
|
||||
use core_protocol::{
|
||||
EvidenceLocator, EvidenceSearchMatchInfo, EvidenceSearchResult, ParsedResourceArtifact,
|
||||
ResourceSourceMap, SourceMapBlock, PARSED_RESOURCE_ARTIFACT_SCHEMA,
|
||||
ParsedResourceArtifact, ResourceSourceMap, SourceMapBlock, PARSED_RESOURCE_ARTIFACT_SCHEMA,
|
||||
};
|
||||
use rusqlite::{params, Connection, OptionalExtension};
|
||||
use serde::{Deserialize, Serialize};
|
||||
@@ -84,6 +87,7 @@ struct LocalSearchResource {
|
||||
updated_at: u128,
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn query_local_search_index(
|
||||
root_path: &Path,
|
||||
root_uri: &str,
|
||||
@@ -636,6 +640,7 @@ pub(crate) fn query_evidence_sqlite_results(
|
||||
query_evidence_sqlite_results_with_mode(root_path, query, owner_document_id, limit, true)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn query_evidence_sqlite_results_with_mode(
|
||||
root_path: &Path,
|
||||
query: &str,
|
||||
@@ -703,6 +708,7 @@ pub(crate) fn query_evidence_sqlite_results_with_mode(
|
||||
Ok(Some(results))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn read_evidence_sqlite_context(
|
||||
root_path: &Path,
|
||||
locator: &EvidenceLocator,
|
||||
@@ -777,6 +783,7 @@ pub(crate) fn read_evidence_sqlite_context(
|
||||
Ok(Some(results))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
pub(crate) fn query_evidence_graph_results(
|
||||
root_path: &Path,
|
||||
query: &str,
|
||||
@@ -839,6 +846,7 @@ pub(crate) fn query_evidence_graph_results(
|
||||
Ok(Some(results))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn graph_edge_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<EvidenceSearchResult> {
|
||||
let edge_id: String = row.get(0)?;
|
||||
let edge_type: String = row.get(1)?;
|
||||
@@ -865,6 +873,7 @@ fn graph_edge_row(row: &rusqlite::Row<'_>) -> rusqlite::Result<EvidenceSearchRes
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_locator_matches(
|
||||
block_id: &str,
|
||||
source: &EvidenceLocator,
|
||||
@@ -877,6 +886,7 @@ fn evidence_locator_matches(
|
||||
&& source.source_map_path == locator.source_map_path
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn query_evidence_sqlite_fts(
|
||||
connection: &Connection,
|
||||
fts_query: &str,
|
||||
@@ -929,6 +939,7 @@ fn query_evidence_sqlite_fts(
|
||||
rows.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn query_evidence_sqlite_like(
|
||||
connection: &Connection,
|
||||
query: &str,
|
||||
@@ -980,6 +991,7 @@ fn query_evidence_sqlite_like(
|
||||
rows.map_err(sqlite_error)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn query_evidence_sqlite_fuzzy(
|
||||
connection: &Connection,
|
||||
query: &str,
|
||||
@@ -1075,6 +1087,7 @@ fn query_evidence_sqlite_fuzzy(
|
||||
.collect())
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_sqlite_row_parts(
|
||||
row: &rusqlite::Row<'_>,
|
||||
) -> rusqlite::Result<(String, String, EvidenceLocator)> {
|
||||
@@ -1087,6 +1100,7 @@ fn evidence_sqlite_row_parts(
|
||||
Ok((block_id, text, source))
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_result_from_sqlite_row(
|
||||
row: &rusqlite::Row<'_>,
|
||||
query: &str,
|
||||
@@ -1118,6 +1132,7 @@ fn evidence_result_from_sqlite_row(
|
||||
})
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn evidence_fts_phrase(query: &str) -> String {
|
||||
format!("\"{}\"", query.replace('"', "\"\""))
|
||||
}
|
||||
@@ -3631,6 +3646,7 @@ struct EvidenceQueryTerm {
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone)]
|
||||
#[cfg_attr(not(test), allow(dead_code))]
|
||||
struct EvidenceTextMatch {
|
||||
score: f64,
|
||||
matched_terms: Vec<String>,
|
||||
@@ -3640,6 +3656,7 @@ struct EvidenceTextMatch {
|
||||
}
|
||||
|
||||
impl EvidenceTextMatch {
|
||||
#[cfg(test)]
|
||||
fn into_match_info(self, rank: Option<u32>) -> EvidenceSearchMatchInfo {
|
||||
EvidenceSearchMatchInfo {
|
||||
rank,
|
||||
@@ -3821,6 +3838,7 @@ fn push_unique(values: &mut Vec<String>, value: String) {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn ocr_search_snippet(body: &str, query: &str) -> String {
|
||||
let normalized_body = body.replace('\n', " ");
|
||||
let normalized_query = query.trim();
|
||||
@@ -3850,6 +3868,7 @@ fn ocr_search_snippet(body: &str, query: &str) -> String {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn ocr_search_snippet_for_terms(body: &str, query: &str, terms: &[String]) -> String {
|
||||
let normalized_body = body.replace('\n', " ");
|
||||
let normalized_query = query.trim();
|
||||
@@ -3873,6 +3892,7 @@ fn ocr_search_snippet_for_terms(body: &str, query: &str, terms: &[String]) -> St
|
||||
ocr_search_snippet(body, query)
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn snippet_from_byte_index(body: &str, byte_index: usize, len: usize) -> String {
|
||||
let start = body[..byte_index]
|
||||
.char_indices()
|
||||
@@ -3883,6 +3903,7 @@ fn snippet_from_byte_index(body: &str, byte_index: usize, len: usize) -> String
|
||||
body[start..].chars().take(len).collect()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
fn fuzzy_search_start_byte(haystack: &str, query: &str) -> Option<usize> {
|
||||
if query.is_empty() {
|
||||
return None;
|
||||
|
||||
@@ -994,6 +994,14 @@ pub struct OfficePreviewQuery {
|
||||
source_map_path: Option<String>,
|
||||
#[serde(default, alias = "blockId")]
|
||||
block_id: Option<String>,
|
||||
#[serde(default, alias = "paragraphOrdinal")]
|
||||
paragraph_ordinal: Option<String>,
|
||||
#[serde(default, alias = "paraIdStart")]
|
||||
para_id_start: Option<String>,
|
||||
#[serde(default, alias = "paraIdEnd")]
|
||||
para_id_end: Option<String>,
|
||||
#[serde(default, alias = "textFingerprint")]
|
||||
text_fingerprint: Option<String>,
|
||||
#[serde(default, alias = "evidenceText")]
|
||||
evidence_text: Option<String>,
|
||||
#[serde(default, alias = "searchQuery")]
|
||||
@@ -1032,6 +1040,10 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
|
||||
let target_bbox = query.bbox.unwrap_or_default();
|
||||
let target_source_map_path = query.source_map_path.unwrap_or_default();
|
||||
let target_block_id = query.block_id.unwrap_or_default();
|
||||
let target_paragraph_ordinal = query.paragraph_ordinal.unwrap_or_default();
|
||||
let target_para_id_start = query.para_id_start.unwrap_or_default();
|
||||
let target_para_id_end = query.para_id_end.unwrap_or_default();
|
||||
let target_text_fingerprint = query.text_fingerprint.unwrap_or_default();
|
||||
let target_evidence_text = query.evidence_text.unwrap_or_default();
|
||||
let target_search_query = query.search_query.unwrap_or_default();
|
||||
let html = format!(
|
||||
@@ -1080,7 +1092,7 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
|
||||
}}
|
||||
</style>
|
||||
</head>
|
||||
<body data-file-url="{file_url}" data-file-name="{file_name}" data-file-type="{file_type}" data-page-width-content-type="{page_width_content_type}" data-workspace-id="{workspace_id}" data-mnote-source-kind="{source_kind}" data-mnote-root-uri="{root_uri}" data-document-id="{document_id}" data-evidence-page="{target_page}" data-evidence-bbox="{target_bbox}" data-evidence-source-map-path="{target_source_map_path}" data-evidence-block-id="{target_block_id}" data-evidence-text="{target_evidence_text}" data-evidence-search-query="{target_search_query}">
|
||||
<body data-file-url="{file_url}" data-file-name="{file_name}" data-file-type="{file_type}" data-page-width-content-type="{page_width_content_type}" data-workspace-id="{workspace_id}" data-mnote-source-kind="{source_kind}" data-mnote-root-uri="{root_uri}" data-document-id="{document_id}" data-evidence-page="{target_page}" data-evidence-bbox="{target_bbox}" data-evidence-source-map-path="{target_source_map_path}" data-evidence-block-id="{target_block_id}" data-evidence-paragraph-ordinal="{target_paragraph_ordinal}" data-evidence-para-id-start="{target_para_id_start}" data-evidence-para-id-end="{target_para_id_end}" data-evidence-text-fingerprint="{target_text_fingerprint}" data-evidence-text="{target_evidence_text}" data-evidence-search-query="{target_search_query}">
|
||||
<main class="mnote-office-preview">
|
||||
<section class="mnote-office-viewer" id="mnote-office-viewer"></section>
|
||||
</main>
|
||||
@@ -1099,6 +1111,10 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
|
||||
let evidenceBbox = body.dataset.evidenceBbox || '';
|
||||
let evidenceSourceMapPath = body.dataset.evidenceSourceMapPath || '';
|
||||
let evidenceBlockId = body.dataset.evidenceBlockId || '';
|
||||
let evidenceParagraphOrdinal = body.dataset.evidenceParagraphOrdinal || '';
|
||||
let evidenceParaIdStart = body.dataset.evidenceParaIdStart || '';
|
||||
let evidenceParaIdEnd = body.dataset.evidenceParaIdEnd || '';
|
||||
let evidenceTextFingerprint = body.dataset.evidenceTextFingerprint || '';
|
||||
let evidenceText = body.dataset.evidenceText || '';
|
||||
let evidenceSearchQuery = body.dataset.evidenceSearchQuery || '';
|
||||
let currentPptxBuffer = null;
|
||||
@@ -1484,6 +1500,17 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
|
||||
if (queryIndex >= 0 && normalized.length > 24) push(normalized.slice(0, 36), {{ allowShort: true }});
|
||||
}}
|
||||
const prefixWindow = cleaned.slice(0, 260);
|
||||
if (queryCompact.length >= 2) {{
|
||||
const prefixCompact = compactEvidenceText(prefixWindow);
|
||||
const compactIndex = prefixCompact.indexOf(queryCompact);
|
||||
if (compactIndex >= 0) {{
|
||||
const queryIndex = prefixWindow.indexOf(evidenceSearchQuery);
|
||||
const start = queryIndex >= 0 ? queryIndex : 0;
|
||||
const queryWindow = prefixWindow.slice(start, start + 96).split(/[。;;]/)[0];
|
||||
push(queryWindow, {{ allowShort: true }});
|
||||
queryWindow.split(/[,,]/).slice(0, 2).forEach(part => push(part, {{ allowShort: true }}));
|
||||
}}
|
||||
}}
|
||||
const catalogMatches = prefixWindow.match(/[^,,。;;#]{{2,56}}[,,]\s*[0-90-9]{{1,5}}/g) || [];
|
||||
catalogMatches.slice(0, 8).forEach(match => {{
|
||||
const value = normalizeEvidenceText(match);
|
||||
@@ -1711,6 +1738,42 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
|
||||
return markEvidenceTarget(marker);
|
||||
}}
|
||||
|
||||
function evidenceParagraphElements() {{
|
||||
if (!viewer) return [];
|
||||
const paragraphs = Array.from(viewer.querySelectorAll('p, li, td, th, blockquote'))
|
||||
.filter(node => node instanceof HTMLElement)
|
||||
.filter(node => normalizeEvidenceText(node.textContent || '').length > 0);
|
||||
if (paragraphs.length) return paragraphs;
|
||||
return Array.from(viewer.querySelectorAll('div, section.docx, section.mnote-docx'))
|
||||
.filter(node => node instanceof HTMLElement)
|
||||
.filter(node => normalizeEvidenceText(node.textContent || '').length > 0);
|
||||
}}
|
||||
|
||||
function scrollToEvidenceParagraphOrdinal() {{
|
||||
const ordinal = Number(evidenceParagraphOrdinal);
|
||||
if (!Number.isFinite(ordinal) || ordinal < 0) return false;
|
||||
const elements = evidenceParagraphElements();
|
||||
if (!elements.length) return false;
|
||||
const anchors = evidenceParagraphAnchors(evidenceText || evidenceSearchQuery);
|
||||
const indices = [];
|
||||
for (let offset = 0; offset <= 4; offset += 1) {{
|
||||
if (offset === 0) indices.push(ordinal);
|
||||
else {{
|
||||
indices.push(ordinal - offset);
|
||||
indices.push(ordinal + offset);
|
||||
}}
|
||||
}}
|
||||
let best = null;
|
||||
for (const index of indices) {{
|
||||
if (index < 0 || index >= elements.length) continue;
|
||||
const element = elements[index];
|
||||
const score = anchors.length ? scoreEvidenceParagraphElement(element, anchors) : 0;
|
||||
if (!best || score > best.score) best = {{ element, score }};
|
||||
if (score >= 2400 && evidenceElementMatchesSearchQuery(element)) break;
|
||||
}}
|
||||
return best ? markEvidenceTarget(best.element) : false;
|
||||
}}
|
||||
|
||||
function scrollToEvidencePageFallback() {{
|
||||
if (!viewer || !Number.isFinite(evidencePage) || evidencePage <= 0) return false;
|
||||
const pages = Array.from(viewer.querySelectorAll('section.docx, section.mnote-docx, .pptx-preview-slide-wrapper'));
|
||||
@@ -1719,16 +1782,18 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
|
||||
}}
|
||||
|
||||
async function applyEvidenceLocator() {{
|
||||
if (!viewer || (!evidencePage && !evidenceBlockId && !evidenceBbox && !evidenceText)) return;
|
||||
if (!viewer || (!evidencePage && !evidenceBlockId && !evidenceBbox && !evidenceText && !evidenceParagraphOrdinal && !evidenceTextFingerprint)) return;
|
||||
try {{
|
||||
const sourceMap = await fetchEvidenceSourceMap();
|
||||
const block = findEvidenceBlockInSourceMap(sourceMap);
|
||||
if (evidenceParagraphOrdinal && scrollToEvidenceParagraphOrdinal()) return;
|
||||
if (evidenceText && scrollToEvidenceParagraph(evidenceText)) return;
|
||||
if (block && scrollToEvidenceParagraph(block.text)) return;
|
||||
if (evidenceText && scrollToEvidenceTextCandidates(evidenceText)) return;
|
||||
if (block && scrollToEvidenceTextCandidates(block.text)) return;
|
||||
if (scrollToEvidenceCoordinate(sourceMap, block)) return;
|
||||
}} catch (_) {{}}
|
||||
if (evidenceParagraphOrdinal && scrollToEvidenceParagraphOrdinal()) return;
|
||||
if (evidenceText && scrollToEvidenceParagraph(evidenceText)) return;
|
||||
if (evidenceText && scrollToEvidenceTextCandidates(evidenceText)) return;
|
||||
scrollToEvidencePageFallback();
|
||||
@@ -1740,12 +1805,20 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
|
||||
evidenceBbox = String(next.bbox || '');
|
||||
evidenceSourceMapPath = String(next.sourceMapPath || '');
|
||||
evidenceBlockId = String(next.blockId || '');
|
||||
evidenceParagraphOrdinal = String(next.paragraphOrdinal || '');
|
||||
evidenceParaIdStart = String(next.paraIdStart || '');
|
||||
evidenceParaIdEnd = String(next.paraIdEnd || '');
|
||||
evidenceTextFingerprint = String(next.textFingerprint || '');
|
||||
evidenceText = String(next.evidenceText || next.query || '');
|
||||
evidenceSearchQuery = String(next.searchQuery || '');
|
||||
body.dataset.evidencePage = evidencePage ? String(evidencePage) : '';
|
||||
body.dataset.evidenceBbox = evidenceBbox;
|
||||
body.dataset.evidenceSourceMapPath = evidenceSourceMapPath;
|
||||
body.dataset.evidenceBlockId = evidenceBlockId;
|
||||
body.dataset.evidenceParagraphOrdinal = evidenceParagraphOrdinal;
|
||||
body.dataset.evidenceParaIdStart = evidenceParaIdStart;
|
||||
body.dataset.evidenceParaIdEnd = evidenceParaIdEnd;
|
||||
body.dataset.evidenceTextFingerprint = evidenceTextFingerprint;
|
||||
body.dataset.evidenceText = evidenceText;
|
||||
body.dataset.evidenceSearchQuery = evidenceSearchQuery;
|
||||
document.documentElement.removeAttribute('data-mnote-office-evidence-applied');
|
||||
@@ -1969,6 +2042,10 @@ pub async fn office_preview_page(Query(query): Query<OfficePreviewQuery>) -> Res
|
||||
target_bbox = escape_html(&target_bbox),
|
||||
target_source_map_path = escape_html(&target_source_map_path),
|
||||
target_block_id = escape_html(&target_block_id),
|
||||
target_paragraph_ordinal = escape_html(&target_paragraph_ordinal),
|
||||
target_para_id_start = escape_html(&target_para_id_start),
|
||||
target_para_id_end = escape_html(&target_para_id_end),
|
||||
target_text_fingerprint = escape_html(&target_text_fingerprint),
|
||||
target_evidence_text = escape_html(&target_evidence_text),
|
||||
);
|
||||
let mut response = Html(html).into_response();
|
||||
|
||||
@@ -548,6 +548,7 @@ mod tests {
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("mnote-knowledge-rag-source-filters"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("filter-sources"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("LightRAG 未映射"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("Rerank"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("scheduleKnowledgeRagStatusBridge"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("knowledgeRagStatusHasInFlight"));
|
||||
assert!(SIDEBAR_PAGE_SETTINGS_RUNTIME_JS.contains("delete_submitted"));
|
||||
|
||||
Reference in New Issue
Block a user