feat: EditorRuntimeActor - 三层缓存/delta/事件架构
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
This commit is contained in:
@@ -13,6 +13,15 @@ pub fn manifest() -> Value {
|
||||
"writeOwner": "rust-runtime-kernel"
|
||||
},
|
||||
"tools": [
|
||||
doc_fetch_tool(),
|
||||
doc_find_tool(),
|
||||
block_fetch_tool(),
|
||||
doc_plan_update_tool(),
|
||||
block_replace_tool(),
|
||||
block_insert_after_tool(),
|
||||
block_delete_tool(),
|
||||
block_move_after_tool(),
|
||||
doc_apply_block_ops_tool(),
|
||||
page_get_tool(),
|
||||
page_save_tool(),
|
||||
available_tool("mnote.page.update_title", "更新当前页面标题", ["page.write"]),
|
||||
@@ -23,6 +32,306 @@ pub fn manifest() -> Value {
|
||||
})
|
||||
}
|
||||
|
||||
fn base_identity_properties() -> Value {
|
||||
json!({
|
||||
"workspaceId": { "type": "string" },
|
||||
"documentId": { "type": "string" },
|
||||
"sessionId": { "type": "string" },
|
||||
"runId": { "type": "string" },
|
||||
"toolCallId": { "type": "string" },
|
||||
"traceId": { "type": "string" },
|
||||
"actorId": { "type": "string" },
|
||||
"actorType": { "type": "string" }
|
||||
})
|
||||
}
|
||||
|
||||
fn doc_fetch_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert(
|
||||
"scope".into(),
|
||||
json!({ "type": "string", "default": "full" }),
|
||||
);
|
||||
map.insert(
|
||||
"detail".into(),
|
||||
json!({ "type": "string", "default": "with_ids" }),
|
||||
);
|
||||
map.insert(
|
||||
"format".into(),
|
||||
json!({ "type": "string", "enum": ["json", "markdown", "text", "page_xml"], "default": "json" }),
|
||||
);
|
||||
map.insert("blockId".into(), json!({ "type": "string" }));
|
||||
map.insert(
|
||||
"selectedBlockIds".into(),
|
||||
json!({ "type": "array", "items": { "type": "string" } }),
|
||||
);
|
||||
map.insert(
|
||||
"allowedTargetBlockIds".into(),
|
||||
json!({ "type": "array", "items": { "type": "string" } }),
|
||||
);
|
||||
map.insert("query".into(), json!({ "type": "string" }));
|
||||
map.insert(
|
||||
"maxBlocks".into(),
|
||||
json!({ "type": "integer", "default": 120 }),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.doc.fetch",
|
||||
"description": "读取当前页面的 canonical block projection,支持 full/outline/block/keyword 范围",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["page.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "sessionId", "runId", "toolCallId", "traceId", "actorId"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn doc_find_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("query".into(), json!({ "type": "string" }));
|
||||
map.insert(
|
||||
"match".into(),
|
||||
json!({ "type": "string", "default": "text" }),
|
||||
);
|
||||
map.insert("limit".into(), json!({ "type": "integer", "default": 20 }));
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.doc.find",
|
||||
"description": "在 Page Aggregate block projection 中按文本、类型或 blockId 查找块",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["page.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "sessionId", "runId", "toolCallId", "traceId", "actorId", "query"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn block_fetch_tool() -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("blockId".into(), json!({ "type": "string" }));
|
||||
map.insert(
|
||||
"includeChildren".into(),
|
||||
json!({ "type": "boolean", "default": true }),
|
||||
);
|
||||
map.insert(
|
||||
"contextBefore".into(),
|
||||
json!({ "type": "integer", "default": 1 }),
|
||||
);
|
||||
map.insert(
|
||||
"contextAfter".into(),
|
||||
json!({ "type": "integer", "default": 1 }),
|
||||
);
|
||||
map.insert(
|
||||
"format".into(),
|
||||
json!({ "type": "string", "enum": ["json", "markdown", "text", "page_xml"], "default": "json" }),
|
||||
);
|
||||
}
|
||||
json!({
|
||||
"name": "mnote.block.fetch",
|
||||
"description": "读取单个块及同父级上下文",
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["block.read", "page.read"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(true, false, true, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "sessionId", "runId", "toolCallId", "traceId", "actorId", "blockId"],
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn doc_plan_update_tool() -> Value {
|
||||
write_tool(
|
||||
"mnote.doc.plan_update",
|
||||
"生成页面块更新 dry-run 计划,不直接写入",
|
||||
["page.write", "block.write"],
|
||||
json!({
|
||||
"command": { "type": "string" },
|
||||
"blockId": { "type": "string" },
|
||||
"anchorBlockId": { "type": "string" },
|
||||
"content": { "type": ["string", "object", "array"] },
|
||||
"query": { "type": "string" },
|
||||
"revision": { "type": ["number", "string"] },
|
||||
"conflictDetectionKey": { "type": "string" }
|
||||
}),
|
||||
[],
|
||||
)
|
||||
}
|
||||
|
||||
fn block_replace_tool() -> Value {
|
||||
write_tool(
|
||||
"mnote.block.replace",
|
||||
"替换指定块内容;真实写入走 Rust page.body.save 链路",
|
||||
["block.write", "page.write"],
|
||||
json!({
|
||||
"blockId": { "type": "string" },
|
||||
"content": { "type": ["string", "object", "array"] },
|
||||
"revision": { "type": ["number", "string"] },
|
||||
"conflictDetectionKey": { "type": "string" },
|
||||
"blockRevisionRef": { "type": "string" }
|
||||
}),
|
||||
[
|
||||
"blockId",
|
||||
"content",
|
||||
"revision",
|
||||
"conflictDetectionKey",
|
||||
"blockRevisionRef",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn block_insert_after_tool() -> Value {
|
||||
write_tool(
|
||||
"mnote.block.insert_after",
|
||||
"在指定块后插入新块;真实写入走 Rust page.body.save 链路",
|
||||
["block.write", "page.write"],
|
||||
json!({
|
||||
"anchorBlockId": { "type": "string" },
|
||||
"content": { "type": ["string", "object", "array"] },
|
||||
"block": { "type": "object" },
|
||||
"revision": { "type": ["number", "string"] },
|
||||
"conflictDetectionKey": { "type": "string" },
|
||||
"anchorRevisionRef": { "type": "string" }
|
||||
}),
|
||||
[
|
||||
"anchorBlockId",
|
||||
"content",
|
||||
"revision",
|
||||
"conflictDetectionKey",
|
||||
"anchorRevisionRef",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn block_move_after_tool() -> Value {
|
||||
write_tool(
|
||||
"mnote.block.move_after",
|
||||
"受限同父级普通叶子块移动;真实写入走 Rust page.body.save 链路",
|
||||
["block.write", "page.write"],
|
||||
json!({
|
||||
"blockId": { "type": "string" },
|
||||
"anchorBlockId": { "type": "string" },
|
||||
"revision": { "type": ["number", "string"] },
|
||||
"conflictDetectionKey": { "type": "string" },
|
||||
"blockRevisionRef": { "type": "string" },
|
||||
"anchorRevisionRef": { "type": "string" }
|
||||
}),
|
||||
[
|
||||
"blockId",
|
||||
"anchorBlockId",
|
||||
"revision",
|
||||
"conflictDetectionKey",
|
||||
"blockRevisionRef",
|
||||
"anchorRevisionRef",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn block_delete_tool() -> Value {
|
||||
write_tool(
|
||||
"mnote.block.delete",
|
||||
"删除指定无子块普通块;真实写入走 Rust page.body.save 链路",
|
||||
["block.write", "page.write"],
|
||||
json!({
|
||||
"blockId": { "type": "string" },
|
||||
"revision": { "type": ["number", "string"] },
|
||||
"conflictDetectionKey": { "type": "string" },
|
||||
"blockRevisionRef": { "type": "string" }
|
||||
}),
|
||||
[
|
||||
"blockId",
|
||||
"revision",
|
||||
"conflictDetectionKey",
|
||||
"blockRevisionRef",
|
||||
],
|
||||
)
|
||||
}
|
||||
|
||||
fn doc_apply_block_ops_tool() -> Value {
|
||||
write_tool(
|
||||
"mnote.doc.apply_block_ops",
|
||||
"一次性应用多个块级操作;Rust 侧统一读取最新 projection、生成 canonical content 并一次保存,适合页面 AI 小段落增删改移动快路径",
|
||||
["block.write", "page.write"],
|
||||
json!({
|
||||
"operations": {
|
||||
"type": "array",
|
||||
"items": {
|
||||
"type": "object",
|
||||
"properties": {
|
||||
"op": { "type": "string" },
|
||||
"blockId": { "type": "string" },
|
||||
"anchorBlockId": { "type": "string" },
|
||||
"matchText": { "type": "string" },
|
||||
"anchorText": { "type": "string" },
|
||||
"afterText": { "type": "string" },
|
||||
"allowedTargetBlockIds": {
|
||||
"type": "array",
|
||||
"items": { "type": "string" }
|
||||
},
|
||||
"content": { "type": ["string", "object", "array"] }
|
||||
}
|
||||
}
|
||||
}
|
||||
}),
|
||||
["operations"],
|
||||
)
|
||||
}
|
||||
|
||||
fn write_tool(
|
||||
name: &str,
|
||||
description: &str,
|
||||
scope: impl IntoIterator<Item = &'static str>,
|
||||
extra_properties: Value,
|
||||
extra_required: impl IntoIterator<Item = &'static str>,
|
||||
) -> Value {
|
||||
let mut properties = base_identity_properties();
|
||||
if let Value::Object(map) = &mut properties {
|
||||
map.insert("dryRun".into(), json!({ "type": "boolean" }));
|
||||
map.insert("idempotencyKey".into(), json!({ "type": "string" }));
|
||||
if let Value::Object(extra) = extra_properties {
|
||||
for (key, value) in extra {
|
||||
map.insert(key, value);
|
||||
}
|
||||
}
|
||||
}
|
||||
let mut required = vec![
|
||||
"workspaceId",
|
||||
"documentId",
|
||||
"sessionId",
|
||||
"runId",
|
||||
"toolCallId",
|
||||
"traceId",
|
||||
"actorId",
|
||||
"dryRun",
|
||||
"idempotencyKey",
|
||||
];
|
||||
required.extend(extra_required);
|
||||
json!({
|
||||
"name": name,
|
||||
"description": description,
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": scope.into_iter().collect::<Vec<_>>(),
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(false, false, false, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": required,
|
||||
"properties": properties
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
fn page_get_tool() -> Value {
|
||||
json!({
|
||||
"name": "mnote.page.get",
|
||||
@@ -31,7 +340,7 @@ fn page_get_tool() -> Value {
|
||||
"capabilityScope": ["page.read"],
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "sessionId", "runId", "toolCallId", "traceId"],
|
||||
"required": ["workspaceId", "documentId", "sessionId", "runId", "toolCallId", "traceId", "actorId"],
|
||||
"properties": {
|
||||
"workspaceId": { "type": "string" },
|
||||
"documentId": { "type": "string" },
|
||||
@@ -39,6 +348,8 @@ fn page_get_tool() -> Value {
|
||||
"runId": { "type": "string" },
|
||||
"toolCallId": { "type": "string" },
|
||||
"traceId": { "type": "string" },
|
||||
"actorId": { "type": "string" },
|
||||
"actorType": { "type": "string" },
|
||||
"includeBody": { "type": "boolean", "default": true },
|
||||
"includeOptions": { "type": "boolean", "default": true },
|
||||
"includeBlocks": { "type": "boolean", "default": true }
|
||||
@@ -54,9 +365,10 @@ fn page_save_tool() -> Value {
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": ["page.write"],
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(false, true, false, false),
|
||||
"inputSchema": {
|
||||
"type": "object",
|
||||
"required": ["workspaceId", "documentId", "sessionId", "runId", "toolCallId", "traceId", "content", "dryRun", "idempotencyKey"],
|
||||
"required": ["workspaceId", "documentId", "sessionId", "runId", "toolCallId", "traceId", "actorId", "content", "dryRun", "idempotencyKey"],
|
||||
"properties": {
|
||||
"workspaceId": { "type": "string" },
|
||||
"documentId": { "type": "string" },
|
||||
@@ -64,6 +376,8 @@ fn page_save_tool() -> Value {
|
||||
"runId": { "type": "string" },
|
||||
"toolCallId": { "type": "string" },
|
||||
"traceId": { "type": "string" },
|
||||
"actorId": { "type": "string" },
|
||||
"actorType": { "type": "string" },
|
||||
"content": {
|
||||
"description": "要写入的正文块数组、{blocks:[...]}、TipTap content 数组或纯文本",
|
||||
"type": ["array", "object", "string"]
|
||||
@@ -90,6 +404,25 @@ fn available_tool(
|
||||
"description": description,
|
||||
"schemaVersion": TOOL_SCHEMA_VERSION,
|
||||
"capabilityScope": scope.into_iter().collect::<Vec<_>>(),
|
||||
"status": "available"
|
||||
"status": "available",
|
||||
"annotations": tool_annotations(false, false, false, false)
|
||||
})
|
||||
}
|
||||
|
||||
fn tool_annotations(
|
||||
readonly: bool,
|
||||
destructive: bool,
|
||||
idempotent: bool,
|
||||
requires_approval: bool,
|
||||
) -> Value {
|
||||
json!({
|
||||
"readonly": readonly,
|
||||
"destructive": destructive,
|
||||
"idempotent": idempotent,
|
||||
"requiresApproval": requires_approval,
|
||||
"approvalMode": if requires_approval { "review" } else { "yolo" },
|
||||
"runtimeOwner": "mnote-web",
|
||||
"writeOwner": "rust-runtime-kernel",
|
||||
"selectionEffect": if readonly { "preserve" } else { "may_change" }
|
||||
})
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user