feat: land page aggregate and phase7 document ai mainline
- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
This commit is contained in:
@@ -5468,12 +5468,12 @@ fn execute_command(
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"documents.title.update" | "tree.node.rename" => {
|
||||
"documents.title.update" | "tree.node.rename" | "page.head.updateTitle" => {
|
||||
let payload: DocumentTitleCommandPayload = parse_payload(command_wire.payload.clone())?;
|
||||
let command_name = if command_wire.name == "tree.node.rename" {
|
||||
"tree.node.rename"
|
||||
} else {
|
||||
"documents.title.update"
|
||||
let command_name = match command_wire.name.as_str() {
|
||||
"tree.node.rename" => "tree.node.rename",
|
||||
"page.head.updateTitle" => "page.head.updateTitle",
|
||||
_ => "documents.title.update",
|
||||
};
|
||||
let command = CommandEnvelope {
|
||||
name: command_name.into(),
|
||||
@@ -5508,12 +5508,12 @@ fn execute_command(
|
||||
}),
|
||||
}))
|
||||
}
|
||||
"documents.save" => {
|
||||
"documents.save" | "page.body.save" => {
|
||||
let payload: DocumentSaveCommandPayload = parse_payload(command_wire.payload.clone())?;
|
||||
let editor_document = normalize_save_editor_document(&payload)?;
|
||||
let canonical_content = legacy_content_from_editor_document(&editor_document);
|
||||
let command = CommandEnvelope {
|
||||
name: "documents.save".into(),
|
||||
name: command_wire.name.clone(),
|
||||
command_id: command_wire.command_id.clone(),
|
||||
idempotency_key: command_wire.idempotency_key.clone(),
|
||||
actor: to_actor_payload(&command_wire.actor),
|
||||
@@ -5525,7 +5525,8 @@ fn execute_command(
|
||||
revision: payload.revision,
|
||||
content_json: serde_json::to_string(&canonical_content).map_err(|error| {
|
||||
BridgeError::validation(format!(
|
||||
"documents.save content 序列化失败: {error}"
|
||||
"{} content 序列化失败: {error}",
|
||||
command_wire.name
|
||||
))
|
||||
})?,
|
||||
conflict_detection_key: payload.conflict_detection_key.clone(),
|
||||
@@ -6186,21 +6187,19 @@ fn normalize_save_editor_document(
|
||||
payload: &DocumentSaveCommandPayload,
|
||||
) -> Result<EditorBlockDocument, BridgeError> {
|
||||
if let Some(editor_document) = payload.editor_document.clone() {
|
||||
let mut parsed =
|
||||
serde_json::from_value::<EditorBlockDocument>(editor_document).map_err(|error| {
|
||||
BridgeError::validation(format!("documents.save editorDocument 非法: {error}"))
|
||||
})?;
|
||||
if parsed.document_id.trim().is_empty() {
|
||||
parsed.document_id = payload.document_id.clone();
|
||||
if let Ok(mut parsed) = serde_json::from_value::<EditorBlockDocument>(editor_document) {
|
||||
if parsed.document_id.trim().is_empty() {
|
||||
parsed.document_id = payload.document_id.clone();
|
||||
}
|
||||
if parsed.root_block_ids.is_empty() {
|
||||
parsed.root_block_ids = parsed
|
||||
.blocks
|
||||
.iter()
|
||||
.map(|block| block.block_id.clone())
|
||||
.collect();
|
||||
}
|
||||
return Ok(parsed);
|
||||
}
|
||||
if parsed.root_block_ids.is_empty() {
|
||||
parsed.root_block_ids = parsed
|
||||
.blocks
|
||||
.iter()
|
||||
.map(|block| block.block_id.clone())
|
||||
.collect();
|
||||
}
|
||||
return Ok(parsed);
|
||||
}
|
||||
if let Some(tiptap_document) = payload.tiptap_document.clone() {
|
||||
let parsed = serde_json::from_value::<TiptapNode>(tiptap_document).map_err(|error| {
|
||||
@@ -7259,6 +7258,261 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documents_save_command_plan_falls_back_from_invalid_editor_document_to_tiptap() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
context: demo_context(),
|
||||
command: RuntimeCommandEnvelopeWire {
|
||||
name: "documents.save".into(),
|
||||
command_id: "cmd_save_2".into(),
|
||||
idempotency_key: Some("idem_save_2".into()),
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: "user".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("sess_1".into()),
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: "next-route".into(),
|
||||
client: "wolai-frontend".into(),
|
||||
},
|
||||
target: Some(RuntimeTargetWire {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: Some("doc_1".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
"revision": 6,
|
||||
"editorDocument": {
|
||||
"documentId": "doc_1",
|
||||
"rootBlockIds": ["legacy_block_1"],
|
||||
"blocks": [
|
||||
{
|
||||
"blockId": "legacy_block_1",
|
||||
"blockType": "paragraph",
|
||||
"contentNodes": [
|
||||
{
|
||||
"attrs": {}
|
||||
}
|
||||
],
|
||||
"childBlockIds": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"tiptapDocument": {
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attrs": {
|
||||
"blockId": "p_1"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "来自 tiptap 回退"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"id": "content_block_1",
|
||||
"type": "paragraph",
|
||||
"content": "来自 content 回退"
|
||||
}
|
||||
],
|
||||
"conflictDetectionKey": "doc_1:6"
|
||||
}),
|
||||
reason: Some("保存正文".into()),
|
||||
refs: vec!["task-save-fallback".into()],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
.expect("documents.save plan should build");
|
||||
|
||||
let RuntimeExecutionPlan::Command(plan) = plan else {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(plan.args_json.pointer("/content/0/id"), Some(&json!("p_1")));
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/content"),
|
||||
Some(&json!("来自 tiptap 回退"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/editorDocument/rootBlockIds/0"),
|
||||
Some(&json!("p_1"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/editorDocument/blocks/0/blockId"),
|
||||
Some(&json!("p_1"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documents_save_command_plan_prefers_valid_editor_document_over_other_sources() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
context: demo_context(),
|
||||
command: RuntimeCommandEnvelopeWire {
|
||||
name: "documents.save".into(),
|
||||
command_id: "cmd_save_3".into(),
|
||||
idempotency_key: Some("idem_save_3".into()),
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: "user".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("sess_1".into()),
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: "next-route".into(),
|
||||
client: "wolai-frontend".into(),
|
||||
},
|
||||
target: Some(RuntimeTargetWire {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: Some("doc_1".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
"revision": 7,
|
||||
"editorDocument": {
|
||||
"documentId": "doc_1",
|
||||
"rootBlockIds": ["editor_block_1"],
|
||||
"blocks": [
|
||||
{
|
||||
"blockId": "editor_block_1",
|
||||
"blockType": "paragraph",
|
||||
"contentNodes": [
|
||||
{
|
||||
"payload": {
|
||||
"type": "text",
|
||||
"text": "来自 editorDocument",
|
||||
"marks": []
|
||||
},
|
||||
"attrs": {}
|
||||
}
|
||||
],
|
||||
"childBlockIds": []
|
||||
}
|
||||
]
|
||||
},
|
||||
"tiptapDocument": {
|
||||
"type": "doc",
|
||||
"content": [
|
||||
{
|
||||
"type": "paragraph",
|
||||
"attrs": {
|
||||
"blockId": "p_1"
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"type": "text",
|
||||
"text": "来自 tiptap"
|
||||
}
|
||||
]
|
||||
}
|
||||
]
|
||||
},
|
||||
"content": [
|
||||
{
|
||||
"id": "content_block_1",
|
||||
"type": "paragraph",
|
||||
"content": "来自 content"
|
||||
}
|
||||
],
|
||||
"conflictDetectionKey": "doc_1:7"
|
||||
}),
|
||||
reason: Some("保存正文".into()),
|
||||
refs: vec!["task-save-prefer-editor".into()],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
.expect("documents.save plan should build");
|
||||
|
||||
let RuntimeExecutionPlan::Command(plan) = plan else {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/content"),
|
||||
Some(&json!("来自 editorDocument"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/editorDocument/blocks/0/contentNodes/0/payload/text"),
|
||||
Some(&json!("来自 editorDocument"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/id"),
|
||||
Some(&json!("editor_block_1"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn documents_save_command_plan_supports_legacy_content_only_payloads() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
context: demo_context(),
|
||||
command: RuntimeCommandEnvelopeWire {
|
||||
name: "page.body.save".into(),
|
||||
command_id: "cmd_save_4".into(),
|
||||
idempotency_key: Some("idem_save_4".into()),
|
||||
actor: RuntimeActorWire {
|
||||
actor_type: "user".into(),
|
||||
actor_id: "user_1".into(),
|
||||
session_id: Some("sess_1".into()),
|
||||
},
|
||||
source: RuntimeSourceWire {
|
||||
channel: "next-route".into(),
|
||||
client: "wolai-frontend".into(),
|
||||
},
|
||||
target: Some(RuntimeTargetWire {
|
||||
workspace_id: Some("ws_1".into()),
|
||||
page_id: Some("doc_1".into()),
|
||||
block_id: None,
|
||||
}),
|
||||
payload: json!({
|
||||
"documentId": "doc_1",
|
||||
"workspaceId": "ws_1",
|
||||
"revision": 8,
|
||||
"content": [
|
||||
{
|
||||
"id": "legacy_content_1",
|
||||
"type": "paragraph",
|
||||
"content": "来自旧式 content-only 保存"
|
||||
}
|
||||
],
|
||||
"conflictDetectionKey": "doc_1:8"
|
||||
}),
|
||||
reason: Some("保存正文".into()),
|
||||
refs: vec!["task-save-content-only".into()],
|
||||
dry_run: false,
|
||||
validate_only: false,
|
||||
},
|
||||
})
|
||||
.expect("page.body.save plan should build");
|
||||
|
||||
let RuntimeExecutionPlan::Command(plan) = plan else {
|
||||
panic!("expected command plan");
|
||||
};
|
||||
|
||||
assert_eq!(plan.function_name, "documents:updateContent");
|
||||
assert_eq!(plan.args_json.pointer("/content/0/id"), Some(&json!("legacy_content_1")));
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/content/0/content"),
|
||||
Some(&json!("来自旧式 content-only 保存"))
|
||||
);
|
||||
assert_eq!(
|
||||
plan.args_json.pointer("/editorDocument/rootBlockIds/0"),
|
||||
Some(&json!("legacy_content_1"))
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn blocks_move_command_plan_maps_to_blocks_move() {
|
||||
let plan = execute_runtime_input(RuntimeInput::Command {
|
||||
|
||||
Reference in New Issue
Block a user