feat: advance local-first workspace checklist

- add admin access-policy UI and local access control surfaces

- add local markdown conflict resolution UI and smoke coverage

- add ACP local agent changed-files audit scaffold and read-only write guard

- document current P0-P2 checklist progress and verification evidence
This commit is contained in:
lix-2026
2026-05-19 08:07:17 +08:00
parent a2cb1338c8
commit 68d321e297
36 changed files with 8643 additions and 235 deletions
+87
View File
@@ -76,6 +76,33 @@ pub struct WorkspaceSource {
pub capabilities: Vec<WorkspaceSourceCapability>,
}
fn default_page_body_content_format() -> String {
"editorBlocks".into()
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
#[serde(rename_all = "camelCase")]
pub struct PageBodyWriteRequest {
pub document_id: String,
pub workspace_id: String,
pub source_kind: WorkspaceSourceKind,
pub root_uri: String,
#[serde(
default,
alias = "expected_file_version",
alias = "conflictDetectionKey",
alias = "conflict_detection_key"
)]
pub expected_file_version: Option<String>,
#[serde(default)]
pub base_content_hash: Option<String>,
#[serde(default = "default_page_body_content_format")]
pub content_format: String,
pub content: Value,
#[serde(default)]
pub editor_source: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
#[serde(rename_all = "kebab-case")]
pub enum KernelProjectionCapability {
@@ -528,6 +555,14 @@ pub struct DocumentReadPageSubtree {
#[serde(rename_all = "camelCase")]
pub struct DocumentContentResult {
pub content: Value,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub editor_document: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tiptap_document: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_document: Option<Value>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub block_projection_version: Option<u32>,
pub revision: u64,
pub conflict_detection_key: String,
pub title: Option<String>,
@@ -730,4 +765,56 @@ mod tests {
serde_json::from_value(value).expect("workspace source 应可反序列化");
assert_eq!(decoded, source);
}
#[test]
fn page_body_write_request_uses_file_version_contract() {
let request: PageBodyWriteRequest = serde_json::from_value(json!({
"documentId": "local-md:README.md",
"workspaceId": "local-ws:user:my-space",
"sourceKind": "local_folder",
"rootUri": "file:///mnt/Data1T/Mnote_data/users/user/workspaces/my-space",
"expectedFileVersion": "local-md:local-md:README.md:1:2:hash",
"baseContentHash": "sha256:base",
"contentFormat": "editorBlocks",
"content": [{"type": "paragraph", "content": [{"type": "text", "text": "正文"}]}],
"editorSource": "tiptap"
}))
.expect("page.body.write request 应可反序列化");
assert_eq!(request.document_id, "local-md:README.md");
assert_eq!(request.source_kind, WorkspaceSourceKind::LocalFolder);
assert_eq!(
request.expected_file_version.as_deref(),
Some("local-md:local-md:README.md:1:2:hash")
);
assert_eq!(request.content_format, "editorBlocks");
assert_eq!(request.editor_source.as_deref(), Some("tiptap"));
let value = serde_json::to_value(&request).expect("request 应可序列化");
assert_eq!(
value["expectedFileVersion"],
json!("local-md:local-md:README.md:1:2:hash")
);
assert_eq!(value["baseContentHash"], json!("sha256:base"));
assert_eq!(value["contentFormat"], json!("editorBlocks"));
assert_eq!(value["editorSource"], json!("tiptap"));
}
#[test]
fn page_body_write_request_accepts_legacy_conflict_key_alias() {
let request: PageBodyWriteRequest = serde_json::from_value(json!({
"documentId": "local-md:README.md",
"workspaceId": "local-ws:user:my-space",
"sourceKind": "local_folder",
"rootUri": "file:///tmp/workspace",
"conflictDetectionKey": "legacy-key",
"content": []
}))
.expect("legacy compat request 应可反序列化");
assert_eq!(request.expected_file_version.as_deref(), Some("legacy-key"));
assert_eq!(request.content_format, "editorBlocks");
assert_eq!(request.base_content_hash, None);
assert_eq!(request.editor_source, None);
}
}