fix: stabilize local folder AI document workflow
- scope local-folder PageTree revision and document sidebar rendering to fileTreeScope - preserve projected table/image attrs for local Markdown aggregate fallback - avoid FileTree restore forced layouts on cold design open - add API ChatOnly provider runtime and local OCR task handling regressions
This commit is contained in:
@@ -2838,7 +2838,11 @@ fn load_local_folder_page_tree_snapshot_for_scope(
|
||||
"PageTree scope parentRelativePath 必须指向目录",
|
||||
));
|
||||
}
|
||||
let watch_revision = local_folder_watch_revision_for_root(&canonical_root, &root_source_uri)?;
|
||||
let watch_revision = if parent_relative_path.is_empty() {
|
||||
local_folder_watch_revision_for_root(&canonical_root, &root_source_uri)?
|
||||
} else {
|
||||
local_folder_watch_revision_for_directory(&canonical_root, &scan_root, &root_source_uri)?
|
||||
};
|
||||
let cache_key = format!("{root_source_uri}\n{parent_relative_path}");
|
||||
if let Ok(cache) = local_page_tree_snapshot_cache().lock() {
|
||||
if let Some(entry) = cache.get(&cache_key) {
|
||||
@@ -7046,7 +7050,45 @@ fn should_ignore_entry(relative_path: &str, file_name: &str) -> bool {
|
||||
if matches!(file_name, ".git" | "node_modules" | ".mnote") {
|
||||
return true;
|
||||
}
|
||||
relative_path == ".mnote/trash" || relative_path.starts_with(".mnote/trash/")
|
||||
relative_path == ".mnote/trash"
|
||||
|| relative_path.starts_with(".mnote/trash/")
|
||||
|| is_local_ocr_intermediate_entry(relative_path, file_name)
|
||||
}
|
||||
|
||||
fn is_local_ocr_intermediate_entry(relative_path: &str, file_name: &str) -> bool {
|
||||
let normalized = relative_path.trim().trim_matches('/').replace('\\', "/");
|
||||
if normalized.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let segments = normalized
|
||||
.split('/')
|
||||
.filter(|segment| !segment.trim().is_empty())
|
||||
.collect::<Vec<_>>();
|
||||
if segments.is_empty() {
|
||||
return false;
|
||||
}
|
||||
let in_ocr_dir = segments
|
||||
.len()
|
||||
.checked_sub(2)
|
||||
.and_then(|index| segments.get(index))
|
||||
.map(|segment| segment.ends_with(".ocr"))
|
||||
.unwrap_or(false);
|
||||
let in_ocr_images_dir = segments
|
||||
.windows(2)
|
||||
.any(|window| window[0].ends_with(".ocr") && window[1] == "images");
|
||||
if in_ocr_images_dir {
|
||||
return true;
|
||||
}
|
||||
if !in_ocr_dir {
|
||||
return false;
|
||||
}
|
||||
let lower = file_name.to_ascii_lowercase();
|
||||
lower == "layout.json"
|
||||
|| lower == "images"
|
||||
|| lower.ends_with("_content_list.json")
|
||||
|| lower.ends_with("_content_list_v2.json")
|
||||
|| lower.ends_with("_model.json")
|
||||
|| lower.ends_with("_origin.pdf")
|
||||
}
|
||||
|
||||
fn compare_local_entries(a: &LocalFolderEntry, b: &LocalFolderEntry) -> Ordering {
|
||||
@@ -7723,6 +7765,14 @@ pub(crate) fn local_workspace_id(root: &Path) -> String {
|
||||
fn local_folder_watch_revision_for_root(
|
||||
root: &Path,
|
||||
root_source_uri: &str,
|
||||
) -> Result<LocalFolderWatchRevision, WebError> {
|
||||
local_folder_watch_revision_for_directory(root, root, root_source_uri)
|
||||
}
|
||||
|
||||
fn local_folder_watch_revision_for_directory(
|
||||
root: &Path,
|
||||
directory: &Path,
|
||||
root_source_uri: &str,
|
||||
) -> Result<LocalFolderWatchRevision, WebError> {
|
||||
let mut hasher = DefaultHasher::new();
|
||||
let mut entry_count = 0usize;
|
||||
@@ -7763,7 +7813,7 @@ fn local_folder_watch_revision_for_root(
|
||||
|
||||
visit(
|
||||
root,
|
||||
root,
|
||||
directory,
|
||||
&mut hasher,
|
||||
&mut entry_count,
|
||||
&mut latest_modified_ms,
|
||||
@@ -9282,9 +9332,10 @@ mod tests {
|
||||
get_share_links, get_user_access_policy, initialize_local_page_id,
|
||||
initialize_local_workspace_for_actor, load_local_folder_file_tree_children_snapshot,
|
||||
load_local_folder_file_tree_snapshot, load_local_folder_file_tree_snapshot_with_reveal,
|
||||
load_local_folder_page_tree_snapshot, local_folder_watch_revision,
|
||||
local_markdown_path_page_id, local_resource_write_editor_blocks, local_workspace_id,
|
||||
open_local_file, read_local_resource, record_shared_cache, record_sync_pending_change,
|
||||
load_local_folder_page_tree_scope_snapshot, load_local_folder_page_tree_snapshot,
|
||||
local_folder_watch_revision, local_markdown_path_page_id,
|
||||
local_resource_write_editor_blocks, local_workspace_id, open_local_file,
|
||||
read_local_resource, record_shared_cache, record_sync_pending_change,
|
||||
resolve_local_markdown_page_aggregate, save_local_markdown_page,
|
||||
update_local_markdown_title, validate_local_access_root, write_local_markdown_asset,
|
||||
write_local_markdown_page_body, write_local_mindmap_data, write_local_resource,
|
||||
@@ -9424,6 +9475,30 @@ mod tests {
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_folder_page_tree_scope_cache_ignores_outside_changes() {
|
||||
let root = temp_root("mnote-page-tree-scope-cache");
|
||||
init_workspace(&root);
|
||||
std::fs::create_dir_all(root.join("design")).expect("create design");
|
||||
std::fs::create_dir_all(root.join("notes")).expect("create notes");
|
||||
std::fs::write(root.join("design").join("page.md"), "# Design\n").expect("write design");
|
||||
std::fs::write(root.join("notes").join("outside.md"), "# Outside\n")
|
||||
.expect("write outside");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
super::reset_local_page_tree_snapshot_test_loads();
|
||||
|
||||
let first =
|
||||
load_local_folder_page_tree_scope_snapshot(&root_uri, "design").expect("first scope");
|
||||
std::fs::write(root.join("notes").join("next.md"), "# Next\n").expect("write next");
|
||||
let second =
|
||||
load_local_folder_page_tree_scope_snapshot(&root_uri, "design").expect("second scope");
|
||||
|
||||
assert_eq!(first.projection, second.projection);
|
||||
assert_eq!(super::local_page_tree_snapshot_scan_test_loads(), 1);
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_identity_uses_path_even_when_frontmatter_has_mnote_id() {
|
||||
let root = temp_root("mnote-local-frontmatter-path-id");
|
||||
@@ -13320,12 +13395,41 @@ fn main() {}
|
||||
init_workspace(&root);
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
std::fs::create_dir_all(root.join("docs").join("Page.ocr")).expect("ocr dir");
|
||||
std::fs::create_dir_all(root.join("docs").join("Page.ocr").join("images"))
|
||||
.expect("ocr images dir");
|
||||
std::fs::write(root.join("docs").join("Page.md"), "# Page\n").expect("page");
|
||||
std::fs::write(
|
||||
root.join("docs").join("Page.ocr").join("photo.png.ocr.md"),
|
||||
"---\nmnote_ocr_version: 1\nprovider: mock\nmodel_version: vlm\nowner_document: ../Page.md\nsource_path: ./Page.assets/photo.png\nsource_root_relative_path: docs/Page.assets/photo.png\nsource_size: 3\nsource_mtime_ms: 1\nstatus: done\ncreated_at: 1\nupdated_at: 1\n---\n\nOCR-only-token\n",
|
||||
)
|
||||
.expect("ocr markdown");
|
||||
std::fs::write(root.join("docs").join("Page.ocr").join("layout.json"), "{}")
|
||||
.expect("ocr layout");
|
||||
std::fs::write(
|
||||
root.join("docs")
|
||||
.join("Page.ocr")
|
||||
.join("abc_content_list.json"),
|
||||
"[]",
|
||||
)
|
||||
.expect("ocr content list");
|
||||
std::fs::write(
|
||||
root.join("docs").join("Page.ocr").join("abc_model.json"),
|
||||
"{}",
|
||||
)
|
||||
.expect("ocr model");
|
||||
std::fs::write(
|
||||
root.join("docs").join("Page.ocr").join("abc_origin.pdf"),
|
||||
b"%PDF-1.4\n",
|
||||
)
|
||||
.expect("ocr origin pdf");
|
||||
std::fs::write(
|
||||
root.join("docs")
|
||||
.join("Page.ocr")
|
||||
.join("images")
|
||||
.join("page_1.jpg"),
|
||||
b"jpg",
|
||||
)
|
||||
.expect("ocr image asset");
|
||||
|
||||
let file_tree = load_local_folder_file_tree_children_snapshot(&root_uri, "docs/Page.ocr")
|
||||
.expect("file tree");
|
||||
@@ -13335,6 +13439,20 @@ fn main() {}
|
||||
assert!(file_items
|
||||
.iter()
|
||||
.any(|item| item["title"].as_str() == Some("photo.png.ocr.md")));
|
||||
for hidden_title in [
|
||||
"layout.json",
|
||||
"abc_content_list.json",
|
||||
"abc_model.json",
|
||||
"abc_origin.pdf",
|
||||
"images",
|
||||
] {
|
||||
assert!(
|
||||
!file_items
|
||||
.iter()
|
||||
.any(|item| item["title"].as_str() == Some(hidden_title)),
|
||||
"OCR 中间产物不应出现在 FileTree: {hidden_title}"
|
||||
);
|
||||
}
|
||||
|
||||
let page_tree = load_local_folder_page_tree_snapshot(&root_uri).expect("page tree");
|
||||
let page_items = page_tree.projection["items"]
|
||||
|
||||
Reference in New Issue
Block a user