feat: 收口文档桥接与 OnlyOffice/Sidebar 回归

- 为 documents.save/meta/content、blocks.patch 与 sidebar.dataset.list 补齐 Rust 协议映射、共享契约与桥接执行器

- 对齐 BlockNote、Mindmap、OnlyOffice 的保存/路由元信息,并补真实浏览器回归脚本与 OnlyOffice 部署基线

- 忽略 Rust 本地构建产物与 Harness 调试状态文件,避免临时产物进入仓库历史
This commit is contained in:
lix-2026
2026-04-15 03:06:29 +08:00
parent 84a8454fa9
commit b33ffb99e7
51 changed files with 3260 additions and 379 deletions
+19
View File
@@ -72,6 +72,25 @@ pub struct UpdatePageOptions {
pub embed_default_block_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SavePageContent {
pub page_id: String,
pub workspace_id: Option<String>,
pub revision: Option<u64>,
pub content_json: String,
pub conflict_detection_key: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct PatchPageBlock {
pub page_id: String,
pub block_id: String,
pub workspace_id: Option<String>,
pub revision: Option<u64>,
pub block_snapshot_json: String,
pub conflict_detection_key: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct InsertBlock {
pub page_id: String,
+6 -2
View File
@@ -6,13 +6,17 @@ pub mod tool;
pub use command::{
CommandEnvelope, CreatePage, CreateWorkspace, DeleteBlock, InsertBlock, MoveBlock,
UpdateBlock, UpdatePageStats, UpdatePageTitle,
PatchPageBlock, SavePageContent, UpdateBlock, UpdatePageOptions, UpdatePageStats,
UpdatePageTitle,
};
pub use common::{
ActorPayload, AffectedObject, ErrorDetail, ErrorPayload, OkPayload, RequestMeta, ResponseMeta,
SourcePayload, TargetRef,
};
pub use query::{GetPage, ListPageBlocks, QueryEnvelope, SearchBlocks, SearchPages};
pub use query::{
GetPage, GetPageContent, GetPageMeta, ListPageBlocks, ListSidebarDataset, QueryEnvelope,
SearchBlocks, SearchPages,
};
pub use tool::{InvocationKind, ToolInvocation};
#[cfg(test)]
+17
View File
@@ -15,6 +15,23 @@ pub struct GetPage {
pub page_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetPageMeta {
pub page_id: String,
pub workspace_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct GetPageContent {
pub page_id: String,
pub workspace_id: Option<String>,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListSidebarDataset {
pub workspace_id: String,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ListPageBlocks {
pub page_id: String,
+131 -2
View File
@@ -21,8 +21,11 @@ pub use write_path::{
mod tests {
use super::*;
use core_protocol::{
command::{CreatePage, CreateWorkspace, UpdatePageOptions, UpdatePageStats, UpdatePageTitle},
query::GetPage,
command::{
CreatePage, CreateWorkspace, PatchPageBlock, SavePageContent, UpdatePageOptions,
UpdatePageStats, UpdatePageTitle,
},
query::{GetPage, GetPageContent, GetPageMeta, ListSidebarDataset},
ActorPayload, CommandEnvelope, QueryEnvelope, SourcePayload, TargetRef,
};
@@ -176,6 +179,85 @@ mod tests {
assert!(request.payload_json.contains("\"name\":\"documents.title.update\""));
}
#[test]
fn document_save_command_maps_to_documents_update_content() {
let command = CommandEnvelope {
name: "documents.save".into(),
command_id: "cmd_save_1".into(),
idempotency_key: Some("idem_save".into()),
actor: ActorPayload {
actor_type: "human".into(),
actor_id: "user_1".into(),
session_id: Some("session_1".into()),
},
source: SourcePayload {
channel: "next-route".into(),
client: "wolai-frontend".into(),
},
target: Some(TargetRef {
workspace_id: Some("ws_1".into()),
page_id: Some("page_1".into()),
block_id: None,
}),
payload: SavePageContent {
page_id: "page_1".into(),
workspace_id: Some("ws_1".into()),
revision: Some(42),
content_json: "{\"type\":\"doc\"}".into(),
conflict_detection_key: Some("rev:42".into()),
},
reason: Some("保存正文".into()),
refs: vec!["checklist:c3".into()],
dry_run: false,
validate_only: false,
};
let request = build_write_request(&demo_context(), &command).expect("write request should build");
assert_eq!(request.function_name, "documents:updateContent");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert!(request.payload_json.contains("\"name\":\"documents.save\""));
}
#[test]
fn block_patch_command_maps_to_documents_update_content() {
let command = CommandEnvelope {
name: "blocks.patch".into(),
command_id: "cmd_patch_1".into(),
idempotency_key: Some("idem_patch".into()),
actor: ActorPayload {
actor_type: "human".into(),
actor_id: "user_1".into(),
session_id: Some("session_1".into()),
},
source: SourcePayload {
channel: "next-route".into(),
client: "wolai-frontend".into(),
},
target: Some(TargetRef {
workspace_id: Some("ws_1".into()),
page_id: Some("page_1".into()),
block_id: Some("block_1".into()),
}),
payload: PatchPageBlock {
page_id: "page_1".into(),
block_id: "block_1".into(),
workspace_id: Some("ws_1".into()),
revision: Some(7),
block_snapshot_json: "{\"id\":\"block_1\",\"type\":\"paragraph\"}".into(),
conflict_detection_key: Some("page:1:block:1".into()),
},
reason: Some("替换块快照".into()),
refs: vec!["checklist:c3".into()],
dry_run: false,
validate_only: false,
};
let request = build_write_request(&demo_context(), &command).expect("write request should build");
assert_eq!(request.function_name, "documents:updateContent");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert!(request.payload_json.contains("\"name\":\"blocks.patch\""));
}
#[test]
fn document_stats_command_maps_to_documents_update_stats() {
let command = CommandEnvelope {
@@ -263,4 +345,51 @@ mod tests {
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert!(request.payload_json.contains("\"name\":\"documents.options.update\""));
}
#[test]
fn document_meta_query_maps_to_documents_get_meta() {
let query = QueryEnvelope {
name: "documents.meta.get".into(),
payload: GetPageMeta {
page_id: "page_1".into(),
workspace_id: Some("ws_1".into()),
},
};
let request = build_query_request(&demo_context(), &query).expect("query request should build");
assert_eq!(request.function_name, "documents:getMeta");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert!(request.payload_json.contains("\"name\":\"documents.meta.get\""));
}
#[test]
fn document_content_query_maps_to_documents_get_content() {
let query = QueryEnvelope {
name: "documents.content.get".into(),
payload: GetPageContent {
page_id: "page_1".into(),
workspace_id: Some("ws_1".into()),
},
};
let request = build_query_request(&demo_context(), &query).expect("query request should build");
assert_eq!(request.function_name, "documents:getContent");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert!(request.payload_json.contains("\"name\":\"documents.content.get\""));
}
#[test]
fn sidebar_dataset_query_maps_to_sidebar_dataset_list() {
let query = QueryEnvelope {
name: "sidebar.dataset.list".into(),
payload: ListSidebarDataset {
workspace_id: "ws_1".into(),
},
};
let request = build_query_request(&demo_context(), &query).expect("query request should build");
assert_eq!(request.function_name, "sidebar:datasetList");
assert_eq!(request.workspace_id.as_deref(), Some("ws_1"));
assert!(request.payload_json.contains("\"name\":\"sidebar.dataset.list\""));
}
}
@@ -29,6 +29,8 @@ pub fn map_command_name_to_convex(command_name: &str) -> &'static str {
match command_name {
"create_workspace" => "workspaces:create",
"create_page" => "pages:create",
"blocks.patch" => "documents:updateContent",
"documents.save" => "documents:updateContent",
"documents.title.update" => "documents:updateTitle",
"documents.stats.update" => "documents:updateStats",
"documents.options.update" => "documents:updateOptions",
@@ -42,6 +44,9 @@ pub fn map_command_name_to_convex(command_name: &str) -> &'static str {
pub fn map_query_name_to_convex(query_name: &str) -> &'static str {
match query_name {
"documents.meta.get" => "documents:getMeta",
"documents.content.get" => "documents:getContent",
"sidebar.dataset.list" => "sidebar:datasetList",
"get_page" => "pages:get",
"list_page_blocks" => "blocks:list_by_page",
"search_pages" => "search:pages",