推进本地优先迁移与资源闭环

- 补齐共享只读页面 AI 浏览器 smoke,并回填当前优先级 checklist 的 P4/P5/P6 证据。

- 为 OnlyOffice 资源增加 /office/{documentId}/{assetId} 对象壳,固定 resource identity 与侧边栏打开路径。

- 扩展 Convex 导出脚本,支持 dry-run、manifest、冲突报告、索引刷新与 rollback,并补充 smoke。

- 补充资源 AI 工具合同与 Convex 导出 Web 入口设计。
This commit is contained in:
lix-2026
2026-05-19 11:39:04 +08:00
parent a1b28a8e38
commit 94be1c927b
11 changed files with 1097 additions and 73 deletions
@@ -4163,12 +4163,6 @@ fn local_agent_audit_should_skip(path: &FsPath) -> bool {
)
}
fn local_agent_audit_hash(text: &str) -> u64 {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
text.hash(&mut hasher);
hasher.finish()
}
fn local_agent_audit_snapshot_entry(
path: &FsPath,
) -> Result<LocalAgentAuditFileSnapshot, WebError> {
@@ -4185,18 +4179,22 @@ fn local_agent_audit_snapshot_entry(
.map(|duration| duration.as_millis())
.unwrap_or_default();
let size = metadata.len();
let content = fs::read_to_string(path).ok();
let hash = content
.as_deref()
.map(local_agent_audit_hash)
.unwrap_or_default();
let markdown_content = content.and_then(|content| {
if path.extension().and_then(|value| value.to_str()) == Some("md") {
Some(content)
} else {
None
}
});
let bytes = fs::read(path).map_err(|error| {
WebError::bad_request_code(
"local_ai_audit_snapshot_failed",
format!("无法读取本地文件快照内容: {error}"),
)
})?;
let hash = {
let mut hasher = std::collections::hash_map::DefaultHasher::new();
bytes.hash(&mut hasher);
hasher.finish()
};
let markdown_content = if path.extension().and_then(|value| value.to_str()) == Some("md") {
String::from_utf8(bytes).ok()
} else {
None
};
Ok(LocalAgentAuditFileSnapshot {
size,
modified_ms,
@@ -5593,6 +5591,38 @@ mod tests {
let _ = std::fs::remove_dir_all(&root);
}
#[test]
fn local_agent_audit_snapshot_detects_resource_files() {
let root = std::env::temp_dir().join(format!(
"mnote-local-agent-audit-resources-{}",
std::process::id()
));
let _ = std::fs::remove_dir_all(&root);
std::fs::create_dir_all(root.join("maps")).expect("create maps dir");
std::fs::create_dir_all(root.join("office")).expect("create office dir");
std::fs::write(root.join("maps/a.mindmap.json"), r#"{"root":"old"}"#)
.expect("write mindmap");
std::fs::write(root.join("office/a.docx"), [0xff, 1, 2, 3]).expect("write office");
let root_uri = format!("file://{}", root.display());
let before = local_agent_audit_collect_snapshot(&root_uri).expect("before snapshot");
std::fs::write(root.join("maps/a.mindmap.json"), r#"{"root":"new"}"#)
.expect("modify mindmap");
std::fs::write(root.join("office/a.docx"), [0xfe, 1, 2, 3]).expect("modify office");
let after = local_agent_audit_collect_snapshot(&root_uri).expect("after snapshot");
let changed = local_agent_audit_change_files(&before, &after);
let files = changed.as_array().expect("changed files");
assert!(files.iter().any(|file| {
file["path"] == "maps/a.mindmap.json" && file["changeType"] == "modified"
}));
assert!(files
.iter()
.any(|file| { file["path"] == "office/a.docx" && file["changeType"] == "modified" }));
let _ = std::fs::remove_dir_all(&root);
}
fn app() -> axum::Router {
app_with_config(AppConfig {
service_name: "mnote-web".into(),