- 归档 OnlyOffice live bridge、Page AI、mindmap、design governance 与相关 bug 条目 - 补齐 MinerU OCR 后端 runtime 合同与 smoke/test 基线 - 收口 ChatOnly/Doubao、ObjectIdentity、Page Aggregate compat 与 runtime owner 文档口径 验证: - cargo test --manifest-path rust/Cargo.toml -p mnote-web local_ocr -- --test-threads=1 - cargo test --manifest-path rust/Cargo.toml -p mnote-web onlyoffice_bridge -- --test-threads=1 - git diff --check - git diff --cached --check - codegraph index . --force && codegraph status . - codegraph sync . && codegraph status .
401 lines
11 KiB
Rust
401 lines
11 KiB
Rust
//! PageAggregate 构建器(Builder 模式)。
|
|
//!
|
|
//! 使用方法:
|
|
//! ```rust,ignore
|
|
//! let aggregate = PageAggregate::builder()
|
|
//! .document_id("doc_1")
|
|
//! .workspace_id("ws_demo")
|
|
//! .title("我的页面")
|
|
//! .build();
|
|
//! ```
|
|
|
|
use crate::page_aggregate::{
|
|
PageAggregate, PageAggregateSource, PageBody, PageHead, PageIdentity, PageLayout, PageOptions,
|
|
PagePermissions, PageStats, PageTree,
|
|
};
|
|
use bridge_runtime::project_legacy_content_to_block_document;
|
|
use serde_json::{to_value, Value};
|
|
|
|
/// PageAggregate 构建器。
|
|
///
|
|
/// 所有字段都有与当前 `build_page_aggregate_snapshot` 一致的默认值。
|
|
/// 使用 `build()` 方法消费 builder 并产出 `PageAggregate`。
|
|
#[derive(Debug)]
|
|
pub struct PageAggregateBuilder {
|
|
document_id: String,
|
|
workspace_id: String,
|
|
source: PageAggregateSource,
|
|
parent_id: Option<String>,
|
|
path: Vec<String>,
|
|
sidebar_tree_membership: Vec<String>,
|
|
body_ref: Option<String>,
|
|
title: String,
|
|
updated_at: Value,
|
|
read_only: bool,
|
|
disable_download: bool,
|
|
disable_copy: bool,
|
|
wide_layout: bool,
|
|
small_text: bool,
|
|
layout_density: String,
|
|
show_heading_numbers: bool,
|
|
show_toc: bool,
|
|
show_structure: bool,
|
|
protect_editing: bool,
|
|
show_word_count: bool,
|
|
collapse_backlinks: bool,
|
|
page_font: String,
|
|
hide_child_pages: bool,
|
|
show_block_ref_count: bool,
|
|
hide_title_header: bool,
|
|
embed_default_block_id: Value,
|
|
content: Value,
|
|
revision: Value,
|
|
conflict_detection_key: Value,
|
|
page_subtree: Value,
|
|
word_count: u64,
|
|
character_count: u64,
|
|
block_count: u64,
|
|
todo_total: u64,
|
|
todo_done: u64,
|
|
}
|
|
|
|
impl PageAggregateBuilder {
|
|
/// 创建新的构建器,所有字段使用与当前 `build_page_aggregate_snapshot` 一致的默认值。
|
|
pub fn new() -> Self {
|
|
Self {
|
|
document_id: String::new(),
|
|
workspace_id: "default".to_string(),
|
|
source: PageAggregateSource::KernelProjection,
|
|
parent_id: None,
|
|
path: Vec::new(),
|
|
sidebar_tree_membership: vec!["page-tree".into(), "file-tree".into()],
|
|
body_ref: None,
|
|
title: "无标题".to_string(),
|
|
updated_at: Value::Null,
|
|
read_only: false,
|
|
disable_download: false,
|
|
disable_copy: false,
|
|
wide_layout: false,
|
|
small_text: false,
|
|
layout_density: "normal".to_string(),
|
|
show_heading_numbers: false,
|
|
show_toc: false,
|
|
show_structure: false,
|
|
protect_editing: false,
|
|
show_word_count: true,
|
|
collapse_backlinks: false,
|
|
page_font: "default".to_string(),
|
|
hide_child_pages: false,
|
|
show_block_ref_count: false,
|
|
hide_title_header: false,
|
|
embed_default_block_id: Value::Null,
|
|
content: Value::Null,
|
|
revision: Value::Null,
|
|
conflict_detection_key: Value::Null,
|
|
page_subtree: Value::Null,
|
|
word_count: 0,
|
|
character_count: 0,
|
|
block_count: 0,
|
|
todo_total: 0,
|
|
todo_done: 0,
|
|
}
|
|
}
|
|
|
|
// ── Identity ──
|
|
|
|
pub fn document_id(mut self, value: impl Into<String>) -> Self {
|
|
self.document_id = value.into();
|
|
self
|
|
}
|
|
|
|
pub fn workspace_id(mut self, value: impl Into<String>) -> Self {
|
|
self.workspace_id = value.into();
|
|
self
|
|
}
|
|
|
|
pub fn source(mut self, value: PageAggregateSource) -> Self {
|
|
self.source = value;
|
|
self
|
|
}
|
|
|
|
pub fn parent_id(mut self, value: Option<String>) -> Self {
|
|
self.parent_id = value;
|
|
self
|
|
}
|
|
|
|
pub fn path(mut self, value: Vec<String>) -> Self {
|
|
self.path = value;
|
|
self
|
|
}
|
|
|
|
pub fn sidebar_tree_membership(mut self, value: Vec<String>) -> Self {
|
|
self.sidebar_tree_membership = value;
|
|
self
|
|
}
|
|
|
|
pub fn body_ref(mut self, value: Option<String>) -> Self {
|
|
self.body_ref = value;
|
|
self
|
|
}
|
|
|
|
// ── Head ──
|
|
|
|
pub fn title(mut self, value: impl Into<String>) -> Self {
|
|
self.title = value.into();
|
|
self
|
|
}
|
|
|
|
pub fn updated_at(mut self, value: Value) -> Self {
|
|
self.updated_at = value;
|
|
self
|
|
}
|
|
|
|
pub fn read_only(mut self, value: bool) -> Self {
|
|
self.read_only = value;
|
|
self
|
|
}
|
|
|
|
pub fn disable_download(mut self, value: bool) -> Self {
|
|
self.disable_download = value;
|
|
self
|
|
}
|
|
|
|
pub fn disable_copy(mut self, value: bool) -> Self {
|
|
self.disable_copy = value;
|
|
self
|
|
}
|
|
|
|
// ── Layout / PageOptions ──
|
|
|
|
pub fn wide_layout(mut self, value: bool) -> Self {
|
|
self.wide_layout = value;
|
|
self
|
|
}
|
|
|
|
pub fn small_text(mut self, value: bool) -> Self {
|
|
self.small_text = value;
|
|
self
|
|
}
|
|
|
|
pub fn layout_density(mut self, value: impl Into<String>) -> Self {
|
|
self.layout_density = value.into();
|
|
self
|
|
}
|
|
|
|
pub fn show_heading_numbers(mut self, value: bool) -> Self {
|
|
self.show_heading_numbers = value;
|
|
self
|
|
}
|
|
|
|
pub fn show_toc(mut self, value: bool) -> Self {
|
|
self.show_toc = value;
|
|
self
|
|
}
|
|
|
|
pub fn show_structure(mut self, value: bool) -> Self {
|
|
self.show_structure = value;
|
|
self
|
|
}
|
|
|
|
pub fn protect_editing(mut self, value: bool) -> Self {
|
|
self.protect_editing = value;
|
|
self
|
|
}
|
|
|
|
pub fn show_word_count(mut self, value: bool) -> Self {
|
|
self.show_word_count = value;
|
|
self
|
|
}
|
|
|
|
pub fn collapse_backlinks(mut self, value: bool) -> Self {
|
|
self.collapse_backlinks = value;
|
|
self
|
|
}
|
|
|
|
pub fn page_font(mut self, value: impl Into<String>) -> Self {
|
|
self.page_font = value.into();
|
|
self
|
|
}
|
|
|
|
pub fn hide_child_pages(mut self, value: bool) -> Self {
|
|
self.hide_child_pages = value;
|
|
self
|
|
}
|
|
|
|
pub fn show_block_ref_count(mut self, value: bool) -> Self {
|
|
self.show_block_ref_count = value;
|
|
self
|
|
}
|
|
|
|
pub fn embed_default_block_id(mut self, value: Value) -> Self {
|
|
self.embed_default_block_id = value;
|
|
self
|
|
}
|
|
|
|
// ── Body ──
|
|
|
|
pub fn content(mut self, value: Value) -> Self {
|
|
self.content = value;
|
|
self
|
|
}
|
|
|
|
pub fn revision(mut self, value: Value) -> Self {
|
|
self.revision = value;
|
|
self
|
|
}
|
|
|
|
pub fn conflict_detection_key(mut self, value: Value) -> Self {
|
|
self.conflict_detection_key = value;
|
|
self
|
|
}
|
|
|
|
// ── Tree ──
|
|
|
|
pub fn page_subtree(mut self, value: Value) -> Self {
|
|
self.page_subtree = value;
|
|
self
|
|
}
|
|
|
|
// ── Stats ──
|
|
|
|
pub fn word_count(mut self, value: u64) -> Self {
|
|
self.word_count = value;
|
|
self
|
|
}
|
|
|
|
pub fn character_count(mut self, value: u64) -> Self {
|
|
self.character_count = value;
|
|
self
|
|
}
|
|
|
|
pub fn block_count(mut self, value: u64) -> Self {
|
|
self.block_count = value;
|
|
self
|
|
}
|
|
|
|
pub fn todo_total(mut self, value: u64) -> Self {
|
|
self.todo_total = value;
|
|
self
|
|
}
|
|
|
|
pub fn todo_done(mut self, value: u64) -> Self {
|
|
self.todo_done = value;
|
|
self
|
|
}
|
|
|
|
// ── Build ──
|
|
|
|
/// 消费 builder 并产出 `PageAggregate`。
|
|
pub fn build(self) -> PageAggregate {
|
|
let block_document = project_legacy_content_to_block_document(
|
|
&self.document_id,
|
|
&self.content,
|
|
&self.revision,
|
|
)
|
|
.ok();
|
|
let block_projection_version = if block_document.is_some() { 1 } else { 0 };
|
|
let page_options = PageOptions {
|
|
wide_layout: self.wide_layout,
|
|
small_text: self.small_text,
|
|
layout_density: self.layout_density,
|
|
show_heading_numbers: self.show_heading_numbers,
|
|
show_toc: self.show_toc,
|
|
show_structure: self.show_structure,
|
|
protect_editing: self.protect_editing,
|
|
show_word_count: self.show_word_count,
|
|
collapse_backlinks: self.collapse_backlinks,
|
|
page_font: self.page_font,
|
|
hide_child_pages: self.hide_child_pages,
|
|
show_block_ref_count: self.show_block_ref_count,
|
|
hide_title_header: self.hide_title_header,
|
|
embed_default_block_id: self.embed_default_block_id,
|
|
};
|
|
let layout_options = to_value(&page_options).expect("PageOptions 序列化不应失败");
|
|
let updated_at_text = self.updated_at.as_str().map(ToOwned::to_owned);
|
|
let page_id = self.document_id.clone();
|
|
PageAggregate {
|
|
schema: "mnote.page_aggregate.v1".to_string(),
|
|
projection_version: PageAggregate::VERSION,
|
|
source: self.source,
|
|
page_id,
|
|
parent_id: self.parent_id,
|
|
title: self.title.clone(),
|
|
path: if self.path.is_empty() {
|
|
vec![self.document_id.clone()]
|
|
} else {
|
|
self.path
|
|
},
|
|
sidebar_tree_membership: self.sidebar_tree_membership,
|
|
body_ref: self.body_ref,
|
|
layout_options,
|
|
updated_at: updated_at_text,
|
|
identity: PageIdentity {
|
|
document_id: self.document_id,
|
|
workspace_id: self.workspace_id,
|
|
},
|
|
head: PageHead {
|
|
title: self.title,
|
|
updated_at: self.updated_at,
|
|
permissions: PagePermissions {
|
|
read_only: self.read_only,
|
|
disable_download: self.disable_download,
|
|
disable_copy: self.disable_copy,
|
|
},
|
|
},
|
|
layout: PageLayout { page_options },
|
|
body: PageBody {
|
|
content: self.content,
|
|
revision: self.revision,
|
|
conflict_detection_key: self.conflict_detection_key,
|
|
file_version: Value::Null,
|
|
block_document: block_document.unwrap_or(Value::Null),
|
|
block_projection_version,
|
|
projection_source: "builder.content".into(),
|
|
attachment_refs: Value::Array(Vec::new()),
|
|
},
|
|
tree: PageTree {
|
|
page_subtree: self.page_subtree,
|
|
},
|
|
stats: PageStats {
|
|
word_count: self.word_count,
|
|
character_count: self.character_count,
|
|
block_count: self.block_count,
|
|
todo_total: self.todo_total,
|
|
todo_done: self.todo_done,
|
|
},
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Default for PageAggregateBuilder {
|
|
fn default() -> Self {
|
|
Self::new()
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn new_defaults_to_kernel_projection() {
|
|
let aggregate = PageAggregateBuilder::new()
|
|
.document_id("doc_1")
|
|
.workspace_id("ws_demo")
|
|
.build();
|
|
|
|
assert_eq!(aggregate.source, PageAggregateSource::KernelProjection);
|
|
}
|
|
|
|
#[test]
|
|
fn explicit_compat_source_is_still_supported() {
|
|
let aggregate = PageAggregateBuilder::new()
|
|
.document_id("doc_1")
|
|
.workspace_id("ws_demo")
|
|
.source(PageAggregateSource::CompatMetaContentJoin)
|
|
.build();
|
|
|
|
assert_eq!(aggregate.source, PageAggregateSource::CompatMetaContentJoin);
|
|
}
|
|
}
|