feat(rag): align LightRAG native citations and MCP bridge
This commit is contained in:
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();
|
||||
|
||||
Reference in New Issue
Block a user