收敛树命令与投影切换
This commit is contained in:
@@ -771,7 +771,7 @@ struct DocumentMoveSnapshotDocument {
|
||||
#[serde(default, alias = "parent_id")]
|
||||
parent_id: Option<String>,
|
||||
#[serde(default, alias = "sort_order")]
|
||||
sort_order: Option<i64>,
|
||||
sort_order: Option<f64>,
|
||||
#[serde(default, alias = "created_at")]
|
||||
created_at: Option<String>,
|
||||
}
|
||||
@@ -904,12 +904,18 @@ fn normalize_parent_id(value: &Option<String>) -> Option<String> {
|
||||
|
||||
fn document_move_sort_key(document: &DocumentMoveSnapshotDocument) -> (i64, String, String) {
|
||||
(
|
||||
document.sort_order.unwrap_or(i64::MAX),
|
||||
normalize_document_move_sort_order(document.sort_order).unwrap_or(i64::MAX),
|
||||
document.created_at.clone().unwrap_or_default(),
|
||||
document.id.clone(),
|
||||
)
|
||||
}
|
||||
|
||||
fn normalize_document_move_sort_order(value: Option<f64>) -> Option<i64> {
|
||||
value
|
||||
.filter(|number| number.is_finite())
|
||||
.map(|number| number.floor() as i64)
|
||||
}
|
||||
|
||||
fn clamp_document_move_index(sort_order: i64, max: usize) -> usize {
|
||||
if sort_order < 0 {
|
||||
return 0;
|
||||
@@ -927,7 +933,7 @@ fn append_document_move_order_patches(
|
||||
let sort_order = index as i64;
|
||||
let moved = document.id == moved_document_id;
|
||||
if normalize_parent_id(&document.parent_id) == parent_id
|
||||
&& document.sort_order == Some(sort_order)
|
||||
&& normalize_document_move_sort_order(document.sort_order) == Some(sort_order)
|
||||
&& !moved
|
||||
{
|
||||
continue;
|
||||
@@ -2286,11 +2292,15 @@ fn materialize_tree_domain_event_plans(
|
||||
.map(|event_plans| {
|
||||
event_plans
|
||||
.iter()
|
||||
.filter_map(|event_plan| materialize_tree_domain_event_value(event_plan, plan, result))
|
||||
.filter_map(|event_plan| {
|
||||
materialize_tree_domain_event_value(event_plan, plan, result)
|
||||
})
|
||||
.collect()
|
||||
})
|
||||
.filter(|event_plans: &Vec<(String, Value)>| !event_plans.is_empty())
|
||||
.or_else(|| materialize_tree_domain_event_plan(plan, result).map(|event_plan| vec![event_plan]))
|
||||
.or_else(|| {
|
||||
materialize_tree_domain_event_plan(plan, result).map(|event_plan| vec![event_plan])
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
@@ -2467,36 +2477,36 @@ pub fn build_runtime_command_artifact_plan(
|
||||
let domain_events: Vec<RuntimeDomainEventArtifactPlan> = materialized_event_plans
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(index, (event_type, domain_event_plan))| {
|
||||
RuntimeDomainEventArtifactPlan {
|
||||
workspace_id: workspace_id.into(),
|
||||
id: tree_domain_event_artifact_id(
|
||||
&command.command_id,
|
||||
event_type,
|
||||
index,
|
||||
materialized_event_plans.len(),
|
||||
),
|
||||
request_id: context.request_id.clone(),
|
||||
trace_id: context.trace_id.clone(),
|
||||
command_id: command.command_id.clone(),
|
||||
command_log_id: command_log_id.clone(),
|
||||
event_type: event_type.clone(),
|
||||
aggregate_type: aggregate_type.into(),
|
||||
aggregate_id: aggregate_id.into(),
|
||||
event_version: 1,
|
||||
status: "committed".into(),
|
||||
actor_type: context.actor.actor_type.clone(),
|
||||
payload: tree_artifact_payload(
|
||||
context,
|
||||
command,
|
||||
&event_type,
|
||||
aggregate_type,
|
||||
aggregate_id,
|
||||
&domain_event_plan,
|
||||
),
|
||||
created_at: now.into(),
|
||||
}
|
||||
})
|
||||
.map(
|
||||
|(index, (event_type, domain_event_plan))| RuntimeDomainEventArtifactPlan {
|
||||
workspace_id: workspace_id.into(),
|
||||
id: tree_domain_event_artifact_id(
|
||||
&command.command_id,
|
||||
event_type,
|
||||
index,
|
||||
materialized_event_plans.len(),
|
||||
),
|
||||
request_id: context.request_id.clone(),
|
||||
trace_id: context.trace_id.clone(),
|
||||
command_id: command.command_id.clone(),
|
||||
command_log_id: command_log_id.clone(),
|
||||
event_type: event_type.clone(),
|
||||
aggregate_type: aggregate_type.into(),
|
||||
aggregate_id: aggregate_id.into(),
|
||||
event_version: 1,
|
||||
status: "committed".into(),
|
||||
actor_type: context.actor.actor_type.clone(),
|
||||
payload: tree_artifact_payload(
|
||||
context,
|
||||
command,
|
||||
&event_type,
|
||||
aggregate_type,
|
||||
aggregate_id,
|
||||
&domain_event_plan,
|
||||
),
|
||||
created_at: now.into(),
|
||||
},
|
||||
)
|
||||
.collect();
|
||||
let domain_event = domain_events.first().cloned();
|
||||
|
||||
@@ -2507,7 +2517,12 @@ pub fn build_runtime_command_artifact_plan(
|
||||
})
|
||||
}
|
||||
|
||||
fn tree_domain_event_artifact_id(command_id: &str, event_type: &str, index: usize, total: usize) -> String {
|
||||
fn tree_domain_event_artifact_id(
|
||||
command_id: &str,
|
||||
event_type: &str,
|
||||
index: usize,
|
||||
total: usize,
|
||||
) -> String {
|
||||
if total <= 1 || index == 0 {
|
||||
return format!("evt_{command_id}");
|
||||
}
|
||||
@@ -2524,7 +2539,10 @@ fn tree_domain_event_artifact_id(command_id: &str, event_type: &str, index: usiz
|
||||
format!("evt_{command_id}_{:02}_{suffix}", index + 1)
|
||||
}
|
||||
|
||||
fn read_preflight_field<'a>(command: &'a RuntimeCommandEnvelopeWire, field: &str) -> Option<&'a Value> {
|
||||
fn read_preflight_field<'a>(
|
||||
command: &'a RuntimeCommandEnvelopeWire,
|
||||
field: &str,
|
||||
) -> Option<&'a Value> {
|
||||
command
|
||||
.preflight_data
|
||||
.as_ref()
|
||||
@@ -5168,7 +5186,9 @@ fn build_page_aggregate_projection_result(
|
||||
source: PageAggregateSource,
|
||||
) -> Result<PageAggregateProjection, BridgeError> {
|
||||
if data.is_null() {
|
||||
return Err(BridgeError::not_found("page.aggregate.get 未返回页面聚合数据"));
|
||||
return Err(BridgeError::not_found(
|
||||
"page.aggregate.get 未返回页面聚合数据",
|
||||
));
|
||||
}
|
||||
|
||||
let meta = data.get("meta").unwrap_or(data);
|
||||
@@ -5248,9 +5268,8 @@ fn build_page_aggregate_projection_result(
|
||||
.cloned()
|
||||
.unwrap_or(Value::Null),
|
||||
};
|
||||
let layout_options = serde_json::to_value(&page_options).map_err(|error| {
|
||||
BridgeError::transport(format!("PageOptions 序列化失败: {error}"))
|
||||
})?;
|
||||
let layout_options = serde_json::to_value(&page_options)
|
||||
.map_err(|error| BridgeError::transport(format!("PageOptions 序列化失败: {error}")))?;
|
||||
let revision_ref = revision
|
||||
.as_u64()
|
||||
.map(|value| format!("{resolved_document_id}:{value}"));
|
||||
@@ -5275,7 +5294,9 @@ fn build_page_aggregate_projection_result(
|
||||
title,
|
||||
updated_at: updated_at.map(Value::String).unwrap_or(Value::Null),
|
||||
permissions: PagePermissions {
|
||||
read_only: bool_field(meta, "can_edit").map(|can_edit| !can_edit).unwrap_or(false),
|
||||
read_only: bool_field(meta, "can_edit")
|
||||
.map(|can_edit| !can_edit)
|
||||
.unwrap_or(false),
|
||||
disable_download: bool_field(meta, "disable_download")
|
||||
.or_else(|| bool_field(meta, "disableDownload"))
|
||||
.unwrap_or(false),
|
||||
@@ -5353,11 +5374,7 @@ fn build_mindmap_projection_result(
|
||||
.filter(|value| value.get("data").is_some() || value.get("children").is_some())
|
||||
.unwrap_or(data);
|
||||
let tree = normalize_mindmap_from_value(tree_input)?;
|
||||
let root_node = tree
|
||||
.data
|
||||
.uid
|
||||
.clone()
|
||||
.unwrap_or_else(|| "root".into());
|
||||
let root_node = tree.data.uid.clone().unwrap_or_else(|| "root".into());
|
||||
let mut nodes = Vec::new();
|
||||
let mut edges = Vec::new();
|
||||
collect_mindmap_projection_rows(&tree, None, &mut nodes, &mut edges);
|
||||
@@ -5414,10 +5431,12 @@ fn collect_mindmap_projection_rows(
|
||||
}
|
||||
|
||||
fn search_documents_canonical_projection(evaluation: SearchDocumentsEvaluation) -> Value {
|
||||
let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| json!({
|
||||
"enqueueAssetIds": [],
|
||||
"results": []
|
||||
}));
|
||||
let mut payload = serde_json::to_value(evaluation).unwrap_or_else(|_| {
|
||||
json!({
|
||||
"enqueueAssetIds": [],
|
||||
"results": []
|
||||
})
|
||||
});
|
||||
if let Some(map) = payload.as_object_mut() {
|
||||
map.insert("projectionOwner".into(), json!("rust-kernel"));
|
||||
if let Some(results) = map.get_mut("results").and_then(Value::as_array_mut) {
|
||||
@@ -7483,9 +7502,7 @@ fn execute_query_result(
|
||||
let payload: MindmapGetQueryPayload = parse_payload(query_wire.payload)?;
|
||||
let result = build_mindmap_projection_result(&data, &payload.mindmap_id)?;
|
||||
serde_json::to_value(result).map_err(|error| {
|
||||
BridgeError::transport(format!(
|
||||
"mindmap.projection.get result 序列化失败: {error}"
|
||||
))
|
||||
BridgeError::transport(format!("mindmap.projection.get result 序列化失败: {error}"))
|
||||
})
|
||||
}
|
||||
"search.documents" | "search.documents.query" => {
|
||||
@@ -8167,14 +8184,10 @@ fn execute_command(
|
||||
"documentId": payload.document_id,
|
||||
}),
|
||||
);
|
||||
let page_body_event_payload = document_save_page_body_domain_event_payload(
|
||||
&payload,
|
||||
&editor_document,
|
||||
);
|
||||
let snapshot_event_payload = document_save_snapshot_domain_event_payload(
|
||||
&payload,
|
||||
&canonical_content,
|
||||
)?;
|
||||
let page_body_event_payload =
|
||||
document_save_page_body_domain_event_payload(&payload, &editor_document);
|
||||
let snapshot_event_payload =
|
||||
document_save_snapshot_domain_event_payload(&payload, &canonical_content)?;
|
||||
let page_body_event_plan = tree_domain_event_plan_with_payload(
|
||||
"page.body.saved",
|
||||
page_body_event_payload,
|
||||
@@ -8207,7 +8220,8 @@ fn execute_command(
|
||||
} else {
|
||||
"documents.embed"
|
||||
};
|
||||
let page_aggregate_embed_plan = build_page_aggregate_embed_plan(&command_wire, &payload)?;
|
||||
let page_aggregate_embed_plan =
|
||||
build_page_aggregate_embed_plan(&command_wire, &payload)?;
|
||||
let embed_content = page_aggregate_embed_plan
|
||||
.as_ref()
|
||||
.and_then(|plan| plan.get("content"))
|
||||
@@ -9575,47 +9589,47 @@ mod tests {
|
||||
"id": "block_1",
|
||||
"type": "paragraph",
|
||||
},
|
||||
"streamDeltaHint": {
|
||||
"family": "tree",
|
||||
"kind": "resync_required",
|
||||
"args": {
|
||||
"reason": "blocks.patch",
|
||||
"documentId": "doc_1",
|
||||
"blockId": "block_1"
|
||||
}
|
||||
},
|
||||
"domainEventHint": {
|
||||
"family": "tree",
|
||||
"eventType": "block.patched"
|
||||
},
|
||||
"domainEventPlan": {
|
||||
"family": "tree",
|
||||
"schema": "mnote.tree.domain_event",
|
||||
"schemaVersion": 1,
|
||||
"eventType": "block.patched",
|
||||
"payload": {
|
||||
"document": {
|
||||
"id": "doc_1",
|
||||
"workspaceId": "ws_1"
|
||||
},
|
||||
"block": {
|
||||
"id": "block_1"
|
||||
},
|
||||
"patch": {
|
||||
"summary": "replace_block",
|
||||
"nextType": "paragraph"
|
||||
}
|
||||
},
|
||||
"streamDeltaHint": {
|
||||
"family": "tree",
|
||||
"kind": "resync_required",
|
||||
"args": {
|
||||
"reason": "blocks.patch",
|
||||
"documentId": "doc_1",
|
||||
"blockId": "block_1"
|
||||
}
|
||||
}
|
||||
}
|
||||
"streamDeltaHint": {
|
||||
"family": "tree",
|
||||
"kind": "resync_required",
|
||||
"args": {
|
||||
"reason": "blocks.patch",
|
||||
"documentId": "doc_1",
|
||||
"blockId": "block_1"
|
||||
}
|
||||
},
|
||||
"domainEventHint": {
|
||||
"family": "tree",
|
||||
"eventType": "block.patched"
|
||||
},
|
||||
"domainEventPlan": {
|
||||
"family": "tree",
|
||||
"schema": "mnote.tree.domain_event",
|
||||
"schemaVersion": 1,
|
||||
"eventType": "block.patched",
|
||||
"payload": {
|
||||
"document": {
|
||||
"id": "doc_1",
|
||||
"workspaceId": "ws_1"
|
||||
},
|
||||
"block": {
|
||||
"id": "block_1"
|
||||
},
|
||||
"patch": {
|
||||
"summary": "replace_block",
|
||||
"nextType": "paragraph"
|
||||
}
|
||||
},
|
||||
"streamDeltaHint": {
|
||||
"family": "tree",
|
||||
"kind": "resync_required",
|
||||
"args": {
|
||||
"reason": "blocks.patch",
|
||||
"documentId": "doc_1",
|
||||
"blockId": "block_1"
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
);
|
||||
}
|
||||
@@ -9775,7 +9789,10 @@ mod tests {
|
||||
.expect("canonical search query result should build");
|
||||
|
||||
assert_eq!(result["projectionOwner"], json!("rust-kernel"));
|
||||
assert_eq!(result["results"][0]["projectionOwner"], json!("rust-kernel"));
|
||||
assert_eq!(
|
||||
result["results"][0]["projectionOwner"],
|
||||
json!("rust-kernel")
|
||||
);
|
||||
assert_eq!(result["results"][0]["id"], json!("page_1"));
|
||||
}
|
||||
|
||||
@@ -9848,7 +9865,10 @@ mod tests {
|
||||
RuntimeExecutionPlan::Command(plan) => {
|
||||
assert_eq!(plan.command_name, "mindmap.command.apply");
|
||||
assert_eq!(plan.function_name, "mindmaps:applyCommand");
|
||||
assert_eq!(plan.args_json["canonicalCommand"], json!("mindmap.command.apply"));
|
||||
assert_eq!(
|
||||
plan.args_json["canonicalCommand"],
|
||||
json!("mindmap.command.apply")
|
||||
);
|
||||
}
|
||||
RuntimeExecutionPlan::Query(_) | RuntimeExecutionPlan::Tool(_) => {
|
||||
panic!("expected command plan")
|
||||
@@ -11514,11 +11534,11 @@ mod tests {
|
||||
"content": [{ "id": "block_1", "type": "pageReference" }],
|
||||
"expectedRevision": 5,
|
||||
"conflictDetectionKey": "conflict_5",
|
||||
"sourceDocumentId": "doc_1",
|
||||
"targetDocumentId": "doc_2",
|
||||
"anchorBlockId": "anchor_1",
|
||||
"pageAggregateEmbedPlan": null,
|
||||
"streamDeltaHint": {
|
||||
"sourceDocumentId": "doc_1",
|
||||
"targetDocumentId": "doc_2",
|
||||
"anchorBlockId": "anchor_1",
|
||||
"pageAggregateEmbedPlan": null,
|
||||
"streamDeltaHint": {
|
||||
"family": "tree",
|
||||
"kind": "noop",
|
||||
"args": {}
|
||||
@@ -12009,6 +12029,31 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_move_order_plan_accepts_convex_float_sort_order_snapshot() {
|
||||
let payload = DocumentMoveCommandPayload {
|
||||
document_id: "doc_b".into(),
|
||||
parent_id: None,
|
||||
sort_order: 0,
|
||||
};
|
||||
let snapshot = serde_json::from_value::<DocumentMoveSnapshotPayload>(json!({
|
||||
"documents": [
|
||||
{ "id": "doc_a", "workspace_id": "ws_1", "parent_id": null, "sort_order": 0.0, "created_at": "2026-04-25T00:00:01Z" },
|
||||
{ "id": "doc_b", "workspace_id": "ws_1", "parent_id": null, "sort_order": 1.0, "created_at": "2026-04-25T00:00:02Z" }
|
||||
]
|
||||
}))
|
||||
.expect("snapshot with float sort_order");
|
||||
|
||||
let plan = build_document_move_order_plan_from_snapshot(&payload, &snapshot)
|
||||
.expect("move order plan");
|
||||
|
||||
assert_eq!(plan.normalized_sort_order, 0);
|
||||
assert!(plan
|
||||
.patches
|
||||
.iter()
|
||||
.any(|patch| patch.document_id == "doc_b" && patch.sort_order == 0 && patch.moved));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn document_move_order_plan_normalizes_cross_parent_and_clamps_index() {
|
||||
let payload = DocumentMoveCommandPayload {
|
||||
@@ -12598,33 +12643,33 @@ mod tests {
|
||||
page_id: Some("doc_2".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: json!({
|
||||
"documentId": "doc_2",
|
||||
"workspaceId": "ws_1",
|
||||
"revision": 5,
|
||||
"conflictDetectionKey": "conflict_5",
|
||||
"sourceDocumentId": "doc_1",
|
||||
"targetDocumentId": "doc_2",
|
||||
"anchorBlockId": "anchor_1",
|
||||
}),
|
||||
preflight_data: Some(json!({
|
||||
"pageAggregateEmbed": {
|
||||
"sourceDocumentId": "doc_1",
|
||||
"sourceTitle": "来源页面",
|
||||
"targetDocumentId": "doc_2",
|
||||
"targetContent": {
|
||||
"blocks": [
|
||||
{ "id": "anchor_1", "type": "paragraph" }
|
||||
],
|
||||
"format": "editor"
|
||||
},
|
||||
"anchorBlockId": "anchor_1",
|
||||
"blockId": "page_ref_doc_1"
|
||||
}
|
||||
})),
|
||||
reason: Some("树命令嵌入页面".into()),
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
payload: json!({
|
||||
"documentId": "doc_2",
|
||||
"workspaceId": "ws_1",
|
||||
"revision": 5,
|
||||
"conflictDetectionKey": "conflict_5",
|
||||
"sourceDocumentId": "doc_1",
|
||||
"targetDocumentId": "doc_2",
|
||||
"anchorBlockId": "anchor_1",
|
||||
}),
|
||||
preflight_data: Some(json!({
|
||||
"pageAggregateEmbed": {
|
||||
"sourceDocumentId": "doc_1",
|
||||
"sourceTitle": "来源页面",
|
||||
"targetDocumentId": "doc_2",
|
||||
"targetContent": {
|
||||
"blocks": [
|
||||
{ "id": "anchor_1", "type": "paragraph" }
|
||||
],
|
||||
"format": "editor"
|
||||
},
|
||||
"anchorBlockId": "anchor_1",
|
||||
"blockId": "page_ref_doc_1"
|
||||
}
|
||||
})),
|
||||
reason: Some("树命令嵌入页面".into()),
|
||||
refs: vec![],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
@@ -12636,59 +12681,59 @@ mod tests {
|
||||
assert_eq!(plan.command_name, "tree.node.embed");
|
||||
assert_eq!(
|
||||
plan.args_json,
|
||||
json!({
|
||||
"id": "doc_2",
|
||||
"content": {
|
||||
"blocks": [
|
||||
{ "id": "anchor_1", "type": "paragraph" },
|
||||
{
|
||||
"id": "page_ref_doc_1",
|
||||
"type": "pageReference",
|
||||
"props": {
|
||||
"pageId": "doc_1",
|
||||
"title": "来源页面"
|
||||
}
|
||||
}
|
||||
],
|
||||
"format": "editor"
|
||||
},
|
||||
"expectedRevision": 5,
|
||||
"conflictDetectionKey": "conflict_5",
|
||||
"sourceDocumentId": "doc_1",
|
||||
"targetDocumentId": "doc_2",
|
||||
"anchorBlockId": "anchor_1",
|
||||
"pageAggregateEmbedPlan": {
|
||||
"schema": "mnote.page_aggregate.embed_plan",
|
||||
"schemaVersion": 1,
|
||||
"sourceDocumentId": "doc_1",
|
||||
"targetDocumentId": "doc_2",
|
||||
"anchorBlockId": "anchor_1",
|
||||
"insertIndex": 1,
|
||||
"block": {
|
||||
"id": "page_ref_doc_1",
|
||||
"type": "pageReference",
|
||||
"props": {
|
||||
"pageId": "doc_1",
|
||||
"title": "来源页面"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"blocks": [
|
||||
{ "id": "anchor_1", "type": "paragraph" },
|
||||
{
|
||||
"id": "page_ref_doc_1",
|
||||
"type": "pageReference",
|
||||
"props": {
|
||||
"pageId": "doc_1",
|
||||
"title": "来源页面"
|
||||
}
|
||||
}
|
||||
],
|
||||
"format": "editor"
|
||||
},
|
||||
"blockCount": 2
|
||||
},
|
||||
"streamDeltaHint": {
|
||||
json!({
|
||||
"id": "doc_2",
|
||||
"content": {
|
||||
"blocks": [
|
||||
{ "id": "anchor_1", "type": "paragraph" },
|
||||
{
|
||||
"id": "page_ref_doc_1",
|
||||
"type": "pageReference",
|
||||
"props": {
|
||||
"pageId": "doc_1",
|
||||
"title": "来源页面"
|
||||
}
|
||||
}
|
||||
],
|
||||
"format": "editor"
|
||||
},
|
||||
"expectedRevision": 5,
|
||||
"conflictDetectionKey": "conflict_5",
|
||||
"sourceDocumentId": "doc_1",
|
||||
"targetDocumentId": "doc_2",
|
||||
"anchorBlockId": "anchor_1",
|
||||
"pageAggregateEmbedPlan": {
|
||||
"schema": "mnote.page_aggregate.embed_plan",
|
||||
"schemaVersion": 1,
|
||||
"sourceDocumentId": "doc_1",
|
||||
"targetDocumentId": "doc_2",
|
||||
"anchorBlockId": "anchor_1",
|
||||
"insertIndex": 1,
|
||||
"block": {
|
||||
"id": "page_ref_doc_1",
|
||||
"type": "pageReference",
|
||||
"props": {
|
||||
"pageId": "doc_1",
|
||||
"title": "来源页面"
|
||||
}
|
||||
},
|
||||
"content": {
|
||||
"blocks": [
|
||||
{ "id": "anchor_1", "type": "paragraph" },
|
||||
{
|
||||
"id": "page_ref_doc_1",
|
||||
"type": "pageReference",
|
||||
"props": {
|
||||
"pageId": "doc_1",
|
||||
"title": "来源页面"
|
||||
}
|
||||
}
|
||||
],
|
||||
"format": "editor"
|
||||
},
|
||||
"blockCount": 2
|
||||
},
|
||||
"streamDeltaHint": {
|
||||
"family": "tree",
|
||||
"kind": "noop",
|
||||
"args": {}
|
||||
|
||||
Reference in New Issue
Block a user