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
+24 -10
View File
@@ -129,6 +129,8 @@ fn build_authorization(config: &AppConfig, context: &RequestContext) -> Result<S
Ok(format!("Convex {admin_key}:{identity_encoded}"))
}
// Convex 授权在 local-first 下只作为控制面身份委托;
// 本地 workspace 的正文、附件和 AI 会话全文不依赖这里获得写入真相。
struct ActingIdentityUser {
user_id: String,
session_suffix: &'static str,
@@ -383,6 +385,8 @@ pub async fn execute_sidebar_dataset_query(
context: &RequestContext,
plan: &RuntimeQueryExecutionPlan,
) -> Result<Value, WebError> {
// sidebar:datasetList 只保留为 cloud source / compat projection source
// 默认 local-first 首屏应优先读取 LocalFS projection。
execute_convex_query_plan(config, context, plan).await
}
@@ -410,6 +414,8 @@ pub async fn execute_convex_query_by_name(
}
fn convex_command_args_for_plan(plan: &RuntimeCommandExecutionPlan) -> Value {
// 这里是 Rust command plan 到 legacy Convex validator 的兼容适配层。
// documents:* 属于 compat / cloud source / sync replicamediaAssets:* 属于 cloud resource replica。
let mut args = plan.args_json.clone();
if matches!(
plan.command_name.as_str(),
@@ -448,13 +454,11 @@ fn convex_command_args_for_plan(plan: &RuntimeCommandExecutionPlan) -> Value {
plan.command_name.as_str(),
"documents.save" | "page.body.save"
) {
if let Value::Object(map) = &mut args {
// 当前自托管 Convex 的 documents:updateContent 仍是 legacy validator。
// Rust plan 保留正式事件契约,但发送给 legacy mutation 时只传它实际接受的字段
map.remove("editorDocument");
map.remove("tiptapDocument");
}
// 当前 Convex documents:updateContent 仍是 legacy content substrate
// 真实可见保存必须只发送 validator 接受的字段;原生编辑器快照留在
// Rust plan / artifact 侧,待 Convex schema 正式迁移后再进入持久层
strip_tree_artifact_fields(&mut args);
strip_editor_snapshot_fields(&mut args);
}
if matches!(
plan.command_name.as_str(),
@@ -552,6 +556,15 @@ fn strip_tree_artifact_fields(args: &mut Value) {
}
}
fn strip_editor_snapshot_fields(args: &mut Value) {
if let Value::Object(map) = args {
map.remove("editorDocument");
map.remove("tiptapDocument");
map.remove("blockDocument");
map.remove("blockProjectionVersion");
}
}
pub async fn execute_convex_command_plan(
config: &AppConfig,
context: &RequestContext,
@@ -850,6 +863,7 @@ pub async fn persist_runtime_command_artifacts(
context: &RequestContext,
artifacts: &RuntimeCommandArtifactPlan,
) -> Result<(), WebError> {
// bridgeLogs 只作为控制面审计与事件副本,不承载 local-first 页面正文真相。
execute_convex_mutation_by_name(
config,
context,
@@ -958,9 +972,9 @@ mod tests {
}
#[test]
fn convex_command_args_strips_editor_runtime_fields_for_legacy_document_save() {
fn convex_command_args_strips_editor_runtime_fields_for_legacy_page_body_save() {
let plan = RuntimeCommandExecutionPlan {
command_name: "documents.save".into(),
command_name: "page.body.save".into(),
command_id: "cmd_1".into(),
function_name: "documents:updateContent".into(),
workspace_id: Some("ws_1".into()),
@@ -975,7 +989,7 @@ mod tests {
"content": [],
"expectedRevision": 0,
"conflictDetectionKey": "doc_1:0",
"editorDocument": {"rootBlockIds": []},
"editorDocument": {"documentId": "doc_1", "rootBlockIds": []},
"tiptapDocument": {"type": "doc", "content": []},
"streamDeltaHint": {"family": "tree"},
"domainEventHint": {"eventType": "page.body.saved"},
@@ -1133,7 +1147,7 @@ mod tests {
}
#[test]
fn convex_resource_lifecycle_args_keep_effective_user_id() {
fn convex_cloud_media_resource_lifecycle_args_keep_effective_user_id() {
let plan = RuntimeCommandExecutionPlan {
command_name: "tree.resource.archive".into(),
command_id: "cmd_resource_archive_1".into(),