feat: advance local-first conflict and search indexing
- 补齐本地 markdown 冲突处理与合并写回路径\n- 增加本地搜索索引路由、刷新与 browser smoke\n- 同步更新 current-priority checklist 的阶段进度
This commit is contained in:
@@ -7,6 +7,7 @@ use crate::page_aggregate::{
|
||||
use crate::routes::local_markdown_parser::{
|
||||
file_stem_title, parse_markdown_page, split_frontmatter,
|
||||
};
|
||||
use crate::routes::local_search_index;
|
||||
use crate::routes::snapshot_support::ProjectionSnapshot;
|
||||
use axum::extract::{Extension, Multipart, Path as AxumPath, Query};
|
||||
use axum::http::{header, HeaderMap, HeaderValue, StatusCode};
|
||||
@@ -1380,10 +1381,11 @@ pub fn save_local_markdown_page(
|
||||
.filter(|value| !value.is_empty())
|
||||
{
|
||||
if expected_key != current_conflict_key {
|
||||
return Err(WebError::new(
|
||||
StatusCode::CONFLICT,
|
||||
"local_markdown_external_change",
|
||||
"本地 Markdown 文件已被外部修改,请刷新后再保存",
|
||||
return Err(local_markdown_conflict_error(
|
||||
root_uri,
|
||||
document_id,
|
||||
expected_key,
|
||||
¤t_conflict_key,
|
||||
));
|
||||
}
|
||||
}
|
||||
@@ -1403,6 +1405,8 @@ pub fn save_local_markdown_page(
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let workspace_id = local_workspace_id(&canonical_root);
|
||||
refresh_local_search_index_best_effort(&canonical_root, root_uri, &workspace_id);
|
||||
let next_conflict_key =
|
||||
local_markdown_conflict_detection_key(document_id, &markdown_file.path)?;
|
||||
Ok(json!({
|
||||
@@ -1417,6 +1421,38 @@ pub fn save_local_markdown_page(
|
||||
}))
|
||||
}
|
||||
|
||||
fn refresh_local_search_index_best_effort(root: &Path, root_uri: &str, workspace_id: &str) {
|
||||
let _ = local_search_index::refresh_local_search_index(root, root_uri, workspace_id);
|
||||
}
|
||||
|
||||
fn local_markdown_conflict_error(
|
||||
root_uri: &str,
|
||||
document_id: &str,
|
||||
editor_base_version: &str,
|
||||
current_disk_version: &str,
|
||||
) -> WebError {
|
||||
WebError::new(
|
||||
StatusCode::CONFLICT,
|
||||
"local_markdown_external_change",
|
||||
"本地 Markdown 文件已被外部修改,请刷新后再保存",
|
||||
)
|
||||
.with_details(json!({
|
||||
"conflict": {
|
||||
"code": "local_markdown_external_change",
|
||||
"documentId": document_id,
|
||||
"rootUri": root_uri,
|
||||
"currentDiskVersion": current_disk_version,
|
||||
"editorBaseVersion": editor_base_version,
|
||||
"suggestedActions": [
|
||||
"accept_disk",
|
||||
"keep_editor",
|
||||
"open_diff",
|
||||
"merge"
|
||||
]
|
||||
}
|
||||
}))
|
||||
}
|
||||
|
||||
pub fn write_local_markdown_page_body(
|
||||
request: &core_protocol::PageBodyWriteRequest,
|
||||
) -> Result<Value, WebError> {
|
||||
@@ -1765,6 +1801,8 @@ pub fn update_local_markdown_title(
|
||||
),
|
||||
)
|
||||
})?;
|
||||
let workspace_id = local_workspace_id(&canonical_root);
|
||||
refresh_local_search_index_best_effort(&canonical_root, root_uri, &workspace_id);
|
||||
Ok(json!({
|
||||
"ok": true,
|
||||
"documentId": document_id,
|
||||
@@ -1814,7 +1852,7 @@ pub fn execute_local_tree_command(
|
||||
format!("无法访问本地文件夹: {error}"),
|
||||
)
|
||||
})?;
|
||||
match action {
|
||||
let result = match action {
|
||||
"create" => {
|
||||
create_local_markdown_page(&canonical_root, parent_id, title.unwrap_or("新页面"))
|
||||
}
|
||||
@@ -1838,7 +1876,10 @@ pub fn execute_local_tree_command(
|
||||
"local_tree_command_unsupported",
|
||||
format!("local_folder 暂不支持 tree action: {other}"),
|
||||
)),
|
||||
}
|
||||
}?;
|
||||
let workspace_id = local_workspace_id(&canonical_root);
|
||||
refresh_local_search_index_best_effort(&canonical_root, root_uri, &workspace_id);
|
||||
Ok(result)
|
||||
}
|
||||
|
||||
fn create_local_markdown_page(
|
||||
@@ -4412,12 +4453,12 @@ mod tests {
|
||||
add_local_access_grant_for_context, create_default_local_workspace_for_actor_at_base,
|
||||
create_local_access_grant, ensure_local_path_read_access,
|
||||
ensure_local_workspace_access_for_actor, ensure_local_workspace_read_access_for_actor,
|
||||
get_local_access_policy, initialize_local_page_id, initialize_local_workspace_for_actor,
|
||||
load_local_folder_page_tree_snapshot, local_folder_watch_revision, open_local_file,
|
||||
resolve_local_markdown_page_aggregate, save_local_markdown_page,
|
||||
validate_local_access_root, write_local_markdown_asset, write_local_markdown_page_body,
|
||||
LocalAccessGrantRequest, LocalAccessValidateRootRequest, LocalFileOpenQuery,
|
||||
LocalUploadFile,
|
||||
execute_local_tree_command, get_local_access_policy, initialize_local_page_id,
|
||||
initialize_local_workspace_for_actor, load_local_folder_page_tree_snapshot,
|
||||
local_folder_watch_revision, open_local_file, resolve_local_markdown_page_aggregate,
|
||||
save_local_markdown_page, validate_local_access_root, write_local_markdown_asset,
|
||||
write_local_markdown_page_body, LocalAccessGrantRequest, LocalAccessValidateRootRequest,
|
||||
LocalFileOpenQuery, LocalUploadFile,
|
||||
};
|
||||
use crate::context::RequestContext;
|
||||
use axum::extract::{Extension, Path as AxumPath, Query};
|
||||
@@ -5353,6 +5394,88 @@ fn main() {}
|
||||
let _ = std::fs::remove_dir_all(&base);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_tree_command_refreshes_search_index_after_rename() {
|
||||
let root = temp_root("mnote-local-tree-refresh-search-index");
|
||||
init_workspace(&root);
|
||||
std::fs::write(
|
||||
root.join("search-target.md"),
|
||||
"# Search Target\nrename-token\n",
|
||||
)
|
||||
.expect("write md");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
crate::routes::local_search_index::refresh_local_search_index(
|
||||
&root,
|
||||
&root_uri,
|
||||
"local-ws-search-index-test",
|
||||
)
|
||||
.expect("initial index");
|
||||
let before =
|
||||
std::fs::read_to_string(root.join(".mnote").join("index").join("search-index.json"))
|
||||
.expect("before index");
|
||||
assert!(before.contains("search-target.md"));
|
||||
|
||||
execute_local_tree_command(
|
||||
&root_uri,
|
||||
"rename",
|
||||
"local-md:search-target.md",
|
||||
None,
|
||||
Some("Renamed Target"),
|
||||
)
|
||||
.expect("rename");
|
||||
|
||||
let after =
|
||||
std::fs::read_to_string(root.join(".mnote").join("index").join("search-index.json"))
|
||||
.expect("after index");
|
||||
assert!(after.contains("Renamed Target.md"), "{after}");
|
||||
assert!(!after.contains("search-target.md"), "{after}");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_save_refreshes_search_index_after_write() {
|
||||
let root = temp_root("mnote-local-save-refresh-search-index");
|
||||
init_workspace(&root);
|
||||
std::fs::write(root.join("README.md"), "# Before\nold-token\n").expect("write md");
|
||||
let root_uri = format!("file://{}", root.display());
|
||||
|
||||
crate::routes::local_search_index::refresh_local_search_index(
|
||||
&root,
|
||||
&root_uri,
|
||||
"local-ws-search-index-test",
|
||||
)
|
||||
.expect("initial index");
|
||||
save_local_markdown_page(
|
||||
&root_uri,
|
||||
"local-md:README.md",
|
||||
None,
|
||||
&serde_json::json!([
|
||||
{
|
||||
"id": "heading_1",
|
||||
"type": "heading",
|
||||
"props": { "level": 1 },
|
||||
"content": [{ "type": "text", "text": "After" }]
|
||||
},
|
||||
{
|
||||
"id": "p_1",
|
||||
"type": "paragraph",
|
||||
"content": [{ "type": "text", "text": "new-token" }]
|
||||
}
|
||||
]),
|
||||
)
|
||||
.expect("save markdown");
|
||||
|
||||
let index =
|
||||
std::fs::read_to_string(root.join(".mnote").join("index").join("search-index.json"))
|
||||
.expect("index");
|
||||
assert!(index.contains("new-token"), "{index}");
|
||||
assert!(!index.contains("old-token"), "{index}");
|
||||
|
||||
let _ = std::fs::remove_dir_all(&root);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn local_markdown_asset_upload_copies_next_to_markdown_with_relative_path() {
|
||||
let root = temp_root("mnote-local-markdown-asset-upload");
|
||||
|
||||
Reference in New Issue
Block a user