Phase A — EditorRuntimeActor 内存缓存层 - 新增 editor_actor.rs: EditorBlockDocument 内存态 + apply_command + load_or_init - block.rs 四个写工具(replace/insert/delete/move)接入 actor 路径 - editor_actor feature flag(MNOTE_WEB_ENABLE_EDITOR_ACTOR=true 默认开启) - bridge-runtime 三个核心函数公开化 - rust-toolchain: 1.89 → stable(修复 spike WASM 编译阻塞) Phase B — 编辑器增量 delta channel - BlockDelta/DeltaOperation 类型 + actor.build_block_delta() - leptos-tiptap spike: mnote:editor:block-delta CustomEvent 监听 + JSON patch - DocumentAiAgentPanel: 拦截 blockDelta → window dispatchEvent - 工具响应含 blockDelta 字段供前端消费 Phase C — 事件 stream delta - broadcast channel 在 AppState/actor/SSE 三层贯通 - tree_events SSE 端点发 block.delta 事件 - 旧客户端降级兼容 环境修复 - rustc recursion_limit = 1024(修复 Leptos SSR 类型深度溢出) - run-convex-deploy.js(封装 Convex function 部署到本地后端 3210) ref: design/07-ai/process/7-13-page-block-editor-runtime-actor-v1.md
174 lines
4.8 KiB
Rust
174 lines
4.8 KiB
Rust
use serde::{Deserialize, Serialize};
|
|
use serde_json::Value;
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "PascalCase")]
|
|
pub enum PageAggregateSource {
|
|
KernelProjection,
|
|
CompatMetaContentJoin,
|
|
Fixture,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PageAggregateProjection {
|
|
pub schema: String,
|
|
pub projection_version: u32,
|
|
pub source: PageAggregateSource,
|
|
pub page_id: String,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub parent_id: Option<String>,
|
|
pub title: String,
|
|
#[serde(default)]
|
|
pub path: Vec<String>,
|
|
#[serde(default)]
|
|
pub sidebar_tree_membership: Vec<String>,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub body_ref: Option<String>,
|
|
pub layout_options: Value,
|
|
#[serde(skip_serializing_if = "Option::is_none")]
|
|
pub updated_at: Option<String>,
|
|
pub identity: PageIdentity,
|
|
pub head: PageHead,
|
|
pub layout: PageLayout,
|
|
pub body: PageBody,
|
|
pub tree: PageTree,
|
|
pub stats: PageStats,
|
|
}
|
|
|
|
impl PageAggregateProjection {
|
|
pub const SCHEMA: &'static str = "mnote.page_aggregate.v1";
|
|
pub const VERSION: u32 = 1;
|
|
|
|
pub fn source_label(&self) -> &'static str {
|
|
match self.source {
|
|
PageAggregateSource::KernelProjection => "rust-kernel",
|
|
PageAggregateSource::CompatMetaContentJoin => "compat-join",
|
|
PageAggregateSource::Fixture => "fixture",
|
|
}
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PageIdentity {
|
|
pub document_id: String,
|
|
pub workspace_id: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PageHead {
|
|
pub title: String,
|
|
pub updated_at: Value,
|
|
pub permissions: PagePermissions,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PagePermissions {
|
|
pub read_only: bool,
|
|
pub disable_download: bool,
|
|
pub disable_copy: bool,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PageLayout {
|
|
pub page_options: PageOptions,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PageOptions {
|
|
pub wide_layout: bool,
|
|
pub small_text: bool,
|
|
pub layout_density: String,
|
|
pub show_heading_numbers: bool,
|
|
pub show_toc: bool,
|
|
pub show_structure: bool,
|
|
pub protect_editing: bool,
|
|
pub show_word_count: bool,
|
|
pub collapse_backlinks: bool,
|
|
pub page_font: String,
|
|
pub hide_child_pages: bool,
|
|
pub show_block_ref_count: bool,
|
|
pub embed_default_block_id: Value,
|
|
}
|
|
|
|
impl Default for PageOptions {
|
|
fn default() -> Self {
|
|
Self {
|
|
wide_layout: false,
|
|
small_text: false,
|
|
layout_density: "normal".into(),
|
|
show_heading_numbers: false,
|
|
show_toc: false,
|
|
show_structure: false,
|
|
protect_editing: false,
|
|
show_word_count: true,
|
|
collapse_backlinks: false,
|
|
page_font: "default".into(),
|
|
hide_child_pages: false,
|
|
show_block_ref_count: false,
|
|
embed_default_block_id: Value::Null,
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::{PageBody, PageOptions};
|
|
use serde_json::json;
|
|
|
|
#[test]
|
|
fn page_options_default_disables_heading_numbers() {
|
|
assert!(!PageOptions::default().show_heading_numbers);
|
|
}
|
|
|
|
#[test]
|
|
fn page_body_accepts_legacy_payload_without_block_projection() {
|
|
let body: PageBody = serde_json::from_value(json!({
|
|
"content": [],
|
|
"revision": 7,
|
|
"conflictDetectionKey": "doc_1:7"
|
|
}))
|
|
.expect("legacy page body should deserialize");
|
|
|
|
assert_eq!(body.revision, json!(7));
|
|
assert_eq!(body.block_projection_version, 0);
|
|
assert_eq!(body.block_document, json!(null));
|
|
assert_eq!(body.projection_source, "");
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PageBody {
|
|
pub content: Value,
|
|
pub revision: Value,
|
|
pub conflict_detection_key: Value,
|
|
#[serde(default)]
|
|
pub block_document: Value,
|
|
#[serde(default)]
|
|
pub block_projection_version: u32,
|
|
#[serde(default)]
|
|
pub projection_source: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PageTree {
|
|
pub page_subtree: Value,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq, Eq, Default)]
|
|
#[serde(rename_all = "camelCase")]
|
|
pub struct PageStats {
|
|
pub word_count: u64,
|
|
pub character_count: u64,
|
|
pub block_count: u64,
|
|
pub todo_total: u64,
|
|
pub todo_done: u64,
|
|
}
|