feat: land page aggregate and phase7 document ai mainline

- 收口 page aggregate 读取、本地状态与命令客户端\n- 接入 phase7 document ai sidecar 与前端编排入口\n- 更新 architecture 与 design 状态迁移
This commit is contained in:
lix-2026
2026-04-23 07:38:34 +08:00
parent 8353aea2f9
commit 41e958769e
93 changed files with 8778 additions and 2222 deletions
+276 -22
View File
@@ -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 {