对齐 Wolai 侧栏体验并收拢设计入库
This commit is contained in:
+328
@@ -0,0 +1,328 @@
|
||||
# 4-11 [done] 树域 Rust 家族 final renderer 与宿主收薄清单 v1
|
||||
|
||||
> 更新时间:2026-04-26
|
||||
>
|
||||
> 前置文档:
|
||||
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-10-tree-rust-family-cutover-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-9-tree-rust-family-cutover-remaining-architecture-and-capability-preservation-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||||
>
|
||||
> 口径覆盖(2026-04-28):此前 `rust_runtime_artifact_host`、Rust/WASM reducer runtime、Rust initial DOM 与 `task112/task113` smoke 通过,只能证明 reducer/runtime 阶段完成,不能证明 final Rust DOM renderer 完成。真正 final renderer 的硬门禁以 `/mnt/Data1T/mnote/design/04-tree-domain/done/4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准:默认真实流量必须不再依赖 `TreeShellIframeHost` 的 `iframe_srcdoc` DOM shell。
|
||||
>
|
||||
> 收口更新(2026-04-28):`task-010` 至 `task-013` 已完成上述 final DOM shell 硬门禁。`rust_family` 默认真实流量已切到 `rust_wasm_dom_shell_host` + `dom_wasm` bridge;`TreeShellIframeHost` 只在显式 `NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST=1` 下作为 legacy/debug host。
|
||||
|
||||
## 1. 当前已收口基线
|
||||
|
||||
- `3000` 仍是唯一浏览器公开入口。
|
||||
- `page tree / file tree / picker` 默认主路径已进入 `rust_family` 3000 `rust_wasm_dom_shell_host`。
|
||||
- `3104` tree shell proxy 已退到显式 debug/internal 边界;默认主站 iframe 不再请求 `/api/tree/shell -> mnote-web:3104`,避免 `desktop:hot` 下页面树/文件树显示 `{"error":"fetch failed"}`。
|
||||
- `file_tree` 主路径输入已收口到 `kernel file_tree items`。
|
||||
- `buildVisibleRows` 不再从 `pageRows + assets` fallback 重建文件树对象语义。
|
||||
- `streamDelta` 已可写入 `command_logs` 与 `domain_events`。
|
||||
- Rust SSE 与 3000 同源 SSE 都能从单条新 `domain_event.payload.streamDelta` 直接产出 `delta`。
|
||||
- Rust `bridge-runtime` 已具备 `tree.subtree.move` 的可测试 canonical move order plan。
|
||||
- TS transport 已把 Rust `normalizedMove` 透传到 Convex `documents.move` 的可选校验边界。
|
||||
- Convex `documents.move` 在收到 `normalizedMove` 时会先确认当前排序 patch plan 与 Rust plan 一致,再执行既有兼容写入。
|
||||
- Rust SSE 与 3000 同源 SSE 对同一 `command_id` 的 command log + domain event 双写做 delta 去重:delta 一致时发一次 `delta`,不一致继续 `resync`。
|
||||
- `tree.subtree.move` 已从 `replace_documents` 过渡 delta 推进到 `move_document` 细粒度 delta:
|
||||
- Rust `bridge-runtime` 已输出 `streamDeltaHint(kind=move_document)` 与 `domainEventHint(tree.subtree.moved)`
|
||||
- 3000 `/api/tree/commands` 按 Rust plan hint + mutation result 物化并写入 `move_document`
|
||||
- 3000 SSE 与 Rust SSE 均保留 `documentId / parentId / sortOrder / updatedAt`
|
||||
- 前端 `tree-delta` reducer 可用该 delta 重建 `sidebar_tree / page_tree / file_tree`
|
||||
- `tree.node.restore / tree.subtree.copy` 已从 `replace_documents` 过渡 delta 推进到细粒度 upsert:
|
||||
- `restore -> upsert_document`
|
||||
- `copy -> upsert_documents`
|
||||
- 前端 `tree-delta` reducer 可用 upsert delta 重建 `sidebar_tree / page_tree / file_tree`
|
||||
- Rust `bridge-runtime` 已输出正式 tree domain event plan:
|
||||
- `tree.node.created / renamed / archived / restored / purged / embedded`
|
||||
- `tree.subtree.moved / copied`
|
||||
- 每条正式 tree command plan 都带 `domainEventPlan(schema=mnote.tree.domain_event, schemaVersion=1, eventType, streamDeltaHint)`
|
||||
- `recordBridgeCommandArtifacts` 会优先使用 Rust plan 透传的 event type / materialized streamDelta,而不是 `${command}.requested`
|
||||
- `file_tree` 搜索过滤已接入 Rust projection query 主链:
|
||||
- `KernelProjectionFilter.query` 已进入协议
|
||||
- `KernelProjectionFilter.maxResults` 已进入协议
|
||||
- Rust `kernel.project_view(file_tree)` 已能输出命中项与必要祖先
|
||||
- Rust 搜索 projection 已按“直接命中项限流,必要祖先不计入上限”执行 `maxResults`
|
||||
- `mnote-web` `/api/tree/projections/file?...&query=` 与 3000 同源 `/api/tree/projections/file` route 已覆盖
|
||||
- `mnote-web` 与 3000 同源 route/client 已透传 `maxResults`
|
||||
- Sidebar 搜索态只消费 Rust 搜索 projection items,旧宿主裁剪 helper 已移除
|
||||
- tree realtime 主链已补强:
|
||||
- command log 与 domain event 双写时,即使上一帧 cursor 来自另一条流,也会按时间排除旧 artifact 后再做同 `command_id` delta 去重
|
||||
- 未识别 domain event payload 继续保守 `resync`
|
||||
- TS artifact 写入边界已固定 `mnote.tree.domain_event` payload schema v1,并保留历史 snake_case 字段兼容 SSE
|
||||
- resource command 主链已补强:
|
||||
- `tree.resource.copy / tree.resource.move` 已由 Rust `bridge-runtime` 生成 `resourceTransferPlan`
|
||||
- `tree.resource.upload` 已由 Rust `bridge-runtime` 生成 `resourceUploadPlan`
|
||||
- Convex `mediaAssets.batchCopy / batchMove / createWithStorage` 已按 Rust plan 做可选一致性校验
|
||||
- `tree.resource.copied / moved / uploaded` 已可生成 `asset_result -> upsert_assets` delta / domain event plan
|
||||
- 3000 media batch/upload route 已改为调用 Rust artifact writer,按 Rust artifact plan 持久化 `command_logs / domain_events`
|
||||
- Sidebar 对 copy / move / rename / delete / restore / upload 的资源请求已抽成 `file-tree/resource-command-client`
|
||||
- file tree 内部 drop 已新增 Rust command preflight:
|
||||
- `tree.filetree.drop.preflight` 由 Rust `bridge-runtime` 归一化 target、row 去重、doc/asset 分类、mindmap subPath、copy/move action
|
||||
- Rust preflight 会拒绝页面移动到自身/后代
|
||||
- preflight plan 已输出 `documentTransferPlan` 与 `resourceTransferPlan`,Sidebar 内部 drop 主路径改为先消费 Rust plan 再触发既有 transport
|
||||
- 宿主仍负责实际命令触发、乐观 UI、副作用通知和错误提示
|
||||
- file tree 删除目标已新增 Rust command preflight:
|
||||
- `tree.filetree.delete.preflight` 由 Rust `bridge-runtime` 归一化 row 去重、父页面覆盖过滤、doc/asset 删除目标
|
||||
- Rust delete plan 会在“选中父页面”时过滤其子页面附件,避免宿主重复构造 asset 删除列表
|
||||
- Sidebar 删除确认与执行主路径改为先消费 Rust delete plan,再触发既有 document/media transport
|
||||
- file tree 粘贴目标与复制分类已新增 Rust command preflight:
|
||||
- `tree.filetree.paste.preflight` 由 Rust `bridge-runtime` 归一化剪贴板 row、目标页面、focused row、`doc/index` 复制递归语义、真实 asset 过滤和 mindmap `targetSubPath`
|
||||
- 3000 同源 `/api/tree/filetree/paste-preflight` route 与 client 已覆盖
|
||||
- `Sidebar` 在 `rust_family` 粘贴主路径先消费 Rust paste plan,再触发既有 `copyTreeCommand` / `copyFileTreeResourceAssets`
|
||||
- file tree 外部上传目标已新增 Rust command preflight:
|
||||
- `tree.filetree.upload-target.preflight` 由 Rust `bridge-runtime` 归一化 drop target、focused row、active doc、目标 workspace 与 mindmap `targetSubPath`
|
||||
- 3000 同源 `/api/tree/filetree/upload-target-preflight` route 与 client 已覆盖
|
||||
- `Sidebar` 外部文件 drop 主路径先消费 Rust upload target plan,再执行既有文件字节读取与 upload transport
|
||||
- Rust tree shell 已新增可测试 renderer/state family 基础合同:
|
||||
- `renderer_input` 覆盖 `page / filetree / picker` 的 projection items、expanded ids、focused id、selected row ids、active picker item、excluded ids、command dispatcher
|
||||
- `mnote-web` tree shell HTML 已嵌入 Rust 构建的 `rendererInput` JSON contract,供 compat host 与后续 final renderer 共用;compat shell 初始 expanded / selection / picker active / exclude 已优先读取该合同
|
||||
- 3000 默认 `TreeShellRustDomShellHost` 已对齐同一 projection model:`page / filetree / picker` 的 projection items 进入 DOM host model;`TreeShellIframeHost` 的 appState JSON 只保留在 legacy/debug host。
|
||||
- `rendererInput` 已继续暴露三类 reducer contract:`rust_page_focus_keyboard_reducer_v1`、`rust_filetree_selection_reducer_v1`、`rust_picker_state_reducer_v1`
|
||||
- `mnote-web /tree` debug shell 与 3000 legacy inline compat host 均已消费同名 reducer contract;当前默认 DOM host 已切到 `TreeShellRustDomShellHost`,并通过 WASM artifact 或同源 Rust reduce seam 执行 runtime。
|
||||
- 前端 `TreeShellHost` 已暴露 `data-tree-renderer-contract=rust_renderer_input_v1`,主路径 `data-tree-host-implementation=rust_wasm_dom_shell_host`;`mnote_web_iframe_proxy`、`rust_inline_compat_host` 与 `rust_runtime_artifact_host` 不再代表默认主执行面
|
||||
- `filetree_selection` 覆盖单选、多选、Shift 范围选、右键选中、清空、visible rows 归一化、drag row ids
|
||||
- `picker_state` 覆盖高亮归一化、上下/Home/End、排除项、Enter pick
|
||||
- `focus_state` 覆盖 focused id 归一化与 next/previous/home/end 行走
|
||||
- `expansion_state` 覆盖默认展开、toggle、活动节点祖先展开
|
||||
- `keyboard_state` 覆盖导航、打开、展开/折叠、上下文菜单 intent
|
||||
- `drag_drop_state` 覆盖拖拽 payload 去重归一化与 copy/move effect
|
||||
- `action_registry` 覆盖 page/filetree/picker 的可用动作集合
|
||||
- `page_renderer` 已输出 `data-rust-page-renderer=initial_v1` 的 page tree 首屏嵌套 HTML,并由 compat runtime 优先 hydrate 该 Rust DOM;后续状态变化仍保留 JS 重绘路径
|
||||
- `filetree_renderer` 已输出 `data-rust-filetree-renderer=initial_v1` 的 file tree 首屏嵌套 HTML;历史 3000 inline `srcDoc` 曾优先 hydrate 该 DOM,当前默认主路径已由 `TreeShellRustDomShellHost` 承载,legacy iframe 与 `mnote-web /tree` 仅保留 debug/internal 验证边界。
|
||||
- `picker_renderer` 已输出 `data-rust-picker-renderer=initial_v1` 的 picker 首屏 HTML;历史 3000 inline `srcDoc` 曾优先 hydrate root/item DOM,当前默认主路径已由 `TreeShellRustDomShellHost` 承载,legacy iframe 与 `mnote-web /tree` 仅保留 debug/internal 验证边界。
|
||||
- Rust artifact 写路径已开始落地:
|
||||
- `bridge-runtime` 已新增 `RuntimeCommandArtifactPlan`,可从 Rust command plan + mutation result 物化 `commandLog` 与 `mnote.tree.domain_event` payload
|
||||
- `mnote-web /api/tree/commands` 已在 Rust transport 内持久化 `bridgeLogs.recordCommandLog / recordDomainEvent`
|
||||
- artifact 写入失败不会回滚已成功 mutation,响应会暴露 `artifactError` 供观测
|
||||
- 3000 `/api/tree/commands`、`/api/media/batch`、`/api/media/upload`、文档类 `page/save/lifecycle/metadata/page-write/documents-block` Rust transport adapter 已改为调用 Rust artifact writer
|
||||
- `documents.duplicate` 已补正式 `tree.node.duplicated` domain event plan,3000 duplicate 成功链不再回退 TS artifact helper
|
||||
- 当前显式 adapter 层已不再直接调用 TS `recordBridgeCommandArtifacts(...)`
|
||||
- file tree selection 的宿主职责已继续收薄:
|
||||
- `rust_family` 下 `Sidebar` 不再把 legacy React file tree selection reducer 作为选择真相
|
||||
- `Sidebar` 只从 `tree.filetree.selection.changed` 事件物化 renderer selection snapshot,删除 / 复制 / 粘贴 / 上传 / 内部 drop 只读消费该 snapshot
|
||||
- `SidebarTreeSurface` 不再向 Rust file tree host 传 `selectedRowIds` 控制 prop,避免宿主反向控制 renderer selection
|
||||
- compat runtime 在 file tree 重绘与可见行归一化后会继续回发 selection snapshot,避免宿主业务消费旧镜像
|
||||
- compat runtime 的 Shift 范围选择与右键选中语义已对齐 Rust `filetree_selection` 合同:Shift 默认覆盖旧选择,Ctrl/Cmd+Shift 才叠加;右键已选中行保留多选集合
|
||||
|
||||
## 2. 已完成的 final DOM shell 硬边界与仍未完成项
|
||||
|
||||
- 2026-04-28 final DOM shell 硬门禁已完成:`TreeShellIframeHost` 的 `iframe_srcdoc` browser bridge 不再是默认 page tree / file tree / picker DOM shell。
|
||||
- `page tree / file tree / picker` 3000 默认主路径已标识为 `rust_wasm_dom_shell_host`,默认浏览器 bridge 为 `data-tree-browser-bridge="dom_wasm"`。
|
||||
- `TreeShellIframeHost` 已降级为显式 legacy/debug host,只能通过 `NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST=1` 进入;旧 `LOCAL_TREE_SHELL_TEMPLATE` 与 `buildInline*Html` 不再参与默认路径。
|
||||
- 默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 WASM artifact 或同源 Rust HTTP seam 返回的 state、hostEvents、commandEvents;本地 JS fallback reducer 不再参与默认真实流量。
|
||||
- 仍未完成的是非 DOM shell 范围:`tree.subtree.move` 的 `normalizedMove` compat fallback 删除、`document.snapshot.saved` 独立事件、`tree.node.embed` Page Aggregate/Rust artifact 深层收口,以及后续删除 legacy iframe host 的清理。
|
||||
- 宿主仍承担文件字节读取、外部上传 transport、菜单状态、实际命令调度、乐观 UI 和副作用通知;资源 upload/copy/move、内部 drop preflight、粘贴 preflight、外部上传目标 preflight 的目标与 metadata plan 已进入 Rust command contract。
|
||||
- `tree.subtree.move` 的 canonical sort plan 已在 Rust 产出并进入 Convex 可选校验,但实际排序写入仍由 Convex `documents.move` 执行。
|
||||
- `file_tree` 搜索过滤主链已改为 Rust projection query;`maxResults / index.md / 附件 / 导图子附件 / book/pdf` 扩展 fixture 已补齐,后续仅保留更丰富搜索语义 hardening。
|
||||
- `replace_documents` 已退为 restore/copy 的 fallback;主路径 restore/copy 已是 `upsert_document(s)`。
|
||||
- Rust command 已生成 tree/resource delta / domain event plan;`mnote-web /api/tree/commands`、3000 tree/media 主写入口与文档类 Rust transport adapter 已可由 Rust artifact writer 落 `command_logs / domain_events`;block/save 复合命令已形成 `page.body.saved`、`block.patched`、`block.moved`、`block.embedded` formal domain-event contract。
|
||||
- block/save 复合命令已并入正式 Rust artifact 主链;OnlyOffice/media writeback 已切到 Rust artifact writer。`document.snapshot.saved` 是否作为独立事件继续留在 `4-16`,不阻塞本文件 final renderer / host thinning 收口。
|
||||
|
||||
## 3. Phase F:final Rust renderer 接入
|
||||
|
||||
### 目标
|
||||
|
||||
把 `page tree / file tree / picker` 从 `TreeShellIframeHost` 内联 DOM compat shell,迁到正式 Rust family renderer。
|
||||
|
||||
状态:默认主路径已完成迁移,legacy iframe host 仅作为显式排障开关保留。
|
||||
|
||||
### checklist
|
||||
|
||||
- [x] 固定 Rust renderer 输入合同:
|
||||
- `projection items`
|
||||
- `expanded ids`
|
||||
- `focused id`
|
||||
- `selected row ids`
|
||||
- `active picker item`
|
||||
- `command dispatcher`
|
||||
- 当前已作为 `rendererInput` 嵌入 `mnote-web` tree shell HTML;runtime DOM shell 已优先读该合同初始化局部状态,但尚未完全改为只消费该合同。
|
||||
- 历史 3000 inline host 已把 `rendererInput` 与 `items` 注入同一个 appState;当前默认 DOM host 直接消费 projection model,不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 主路径注入第二份 items。
|
||||
- 默认 DOM shell 已固定 `family=rust_family / version=1 / executionStrategy=dom_wasm`,并优先使用 3000 同源 `tree-shell-runtime` artifact;legacy iframe manifest 中的 `browserBridge=iframe_srcdoc` 仅保留在显式 legacy/debug host。
|
||||
- runtime artifact manifest 已新增 `runtimeApi`,显式暴露 `TreeShellRuntimeRequest / TreeShellRuntimeResult`、state snapshot、DOM patch、host event、command event kind。
|
||||
- runtime artifact manifest 已继续暴露 `reduceEndpoint=/api/tree/runtime/reduce`;3000 同源 thin proxy 与 mnote-web endpoint 保留为 fallback/debug。
|
||||
- 正式 Rust/WASM reducer runtime 已落地:`tree-shell-runtime-wasm` 导出 `reduceTreeShellRuntime`,3000 主路径默认优先加载 `mnote-tree-shell-runtime.js` 与 `mnote-tree-shell-runtime_bg.wasm` 执行 reduce。
|
||||
- [x] 拆出 renderer 内部模块:
|
||||
- [x] row model 雏形(page/filetree/picker renderer test id contract)
|
||||
- [x] page initial DOM renderer(首屏 page tree Rust HTML + hydrate contract)
|
||||
- [x] filetree initial DOM renderer(首屏 file tree Rust HTML contract)
|
||||
- [x] picker initial DOM renderer(首屏 picker Rust HTML contract)
|
||||
- [x] expansion model(默认展开 / toggle / 祖先展开 state family)
|
||||
- [x] focus model(focused id normalize / next / previous / home / end)
|
||||
- [x] selection model(filetree selection state family)
|
||||
- [x] picker state model(高亮 / Enter / exclude)
|
||||
- [x] keyboard model(导航 / 打开 / expand-collapse / context menu intent)
|
||||
- [x] drag/drop model(payload normalize / copy-move effect)
|
||||
- [x] action registry(page / filetree / picker action set)
|
||||
- [x] runtime facade(`TreeShellRuntimeRequest / TreeShellRuntimeResult` 可序列化 API,输出统一 DOM patch / host event / command event)
|
||||
- [x] `page tree` 先切 final renderer。
|
||||
- 当前状态:默认 page tree 由 `TreeShellRustDomShellHost` 渲染 DOM,focus / keyboard / expand / collapse / toggle / open / context menu / move command dispatch 均通过 `TreeShellRuntimeRequest/Result` 返回结果驱动。
|
||||
- [x] `picker` 以轻量模式复用同一 renderer state family。
|
||||
- 当前状态:默认 picker 由 `TreeShellRustDomShellHost` 渲染 DOM,keyboard command、hover focus、click pick 均通过 runtime state 与 hostEvents 驱动,测试已禁止默认 iframe postMessage 成功路径。
|
||||
- [x] `file tree` 后切 final renderer,并保留 `doc / index / asset-folder / asset` 能力。
|
||||
- 当前状态:默认 file tree 由 `TreeShellRustDomShellHost` 渲染 DOM,selection、context menu、open、internal/external drop 与 drop target 通过 runtime result/hostEvents 驱动,并保留 doc/index/asset-folder/asset row 口径。
|
||||
- [x] compat host 退为 debug/internal fallback(本批可验证范围)。
|
||||
- 3000 默认主路径不再请求 `/api/tree/shell -> mnote-web:3104`,也不再回退旧 React renderer;host implementation 已切到 `rust_wasm_dom_shell_host`;`mnote-web /tree` 与 `TreeShellIframeHost` 仅保留显式 debug/internal/legacy 验证边界。
|
||||
|
||||
### 验收
|
||||
|
||||
- 默认真实流量不再以 `rust_inline_compat_host`、`rust_runtime_artifact_host` 或 `TreeShellIframeHost` 作为主路径标识;page / picker / filetree 默认路径已不再依赖 `iframe_srcdoc` 内联 DOM renderer。
|
||||
- `data-tree-host-implementation` 不再以 `mnote_web_iframe_proxy` 代表主执行面。
|
||||
- `page tree / file tree / picker` smoke 仍覆盖打开、键盘、选择、拖放、菜单。
|
||||
|
||||
## 4. Phase G:宿主状态机收薄
|
||||
|
||||
### 目标
|
||||
|
||||
宿主只保留挂载、认证、文件读取、transport bridge,不再解释树域交互合法性。
|
||||
|
||||
### checklist
|
||||
|
||||
- [x] 上传执行从 sidebar 宿主抽成正式 resource command。
|
||||
- `tree.resource.upload` 已进入 Rust plan / domain event hint / `asset_result` delta
|
||||
- 浏览器文件字节读取与 Convex upload URL 仍由 3000 upload route 执行
|
||||
- [x] 内部 file tree drop payload 归一化进入 Rust command preflight。
|
||||
- `tree.filetree.drop.preflight` 已接 3000 同源 route `/api/tree/filetree/drop-preflight`
|
||||
- 前端 `file-tree/shell` 只负责从 projection row / parent snapshot 构造 preflight payload
|
||||
- Rust plan 输出 target、mindmap subPath、doc/asset 分类、top-level doc、source asset documents、`documentTransferPlan`、`resourceTransferPlan`
|
||||
- [x] copy/move 区分与非法投放校验进入 Rust command/projection contract。
|
||||
- `tree.filetree.drop.preflight` 已按 `copy` 输出文档/资源 transfer plan
|
||||
- 非 copy 的页面移动已在 Rust preflight 拒绝自身/后代投放
|
||||
- 更完整的跨工作空间/混合对象权限校验仍随正式 Rust 写路径继续下沉
|
||||
- [x] file tree 删除目标归一化进入 Rust command contract。
|
||||
- `tree.filetree.delete.preflight` 已接 3000 同源 route `/api/tree/filetree/delete-preflight`
|
||||
- `Sidebar` 删除链已先消费 Rust delete plan,再执行既有 `deleteDocumentCommand` / `deleteFileTreeResourceAssets`
|
||||
- legacy `computeFileTreeShellDeleteTargets` 已退出生产代码,只保留 Rust preflight payload builder
|
||||
- [x] file tree 粘贴目标推导与复制对象分类进入 Rust command contract。
|
||||
- `tree.filetree.paste.preflight` 已接 3000 同源 route `/api/tree/filetree/paste-preflight`
|
||||
- Rust paste plan 已输出 `docItems` 与 `resourceTransferPlan`,覆盖 `doc -> recursive true`、`index -> recursive false`、真实 asset 过滤和 mindmap `targetSubPath`
|
||||
- `Sidebar` 的 `rust_family` 粘贴链已不再本地推导 `docItemsMap / copyableAssetIds`,只消费 Rust paste plan 后触发既有 transport
|
||||
- [x] file tree 外部上传目标推导进入 Rust command contract。
|
||||
- `tree.filetree.upload-target.preflight` 已接 3000 同源 route `/api/tree/filetree/upload-target-preflight`
|
||||
- Rust upload target plan 已输出 `workspaceId / targetDocumentId / targetMindmapId / targetSubPath`
|
||||
- `Sidebar` 的外部文件 drop 链已不再本地推导 target row / focused row / workspace fallback,只消费 Rust upload target plan 后执行既有 upload transport
|
||||
- [x] 多选、范围选、右键选中状态进入 renderer state family。
|
||||
- Rust `filetree_selection` 已有纯状态测试;`mnote-web /tree` 与 3000 legacy inline compat runtime 均已通过 `rust_filetree_selection_reducer_v1` 合同入口执行选择、右键、可见行归一化与 drag rows 解析;当前默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 Rust/WASM runtime。
|
||||
- [x] picker 高亮、Enter 选中、排除项规则进入 renderer state family。
|
||||
- Rust `picker_state` 已有纯状态测试;`mnote-web /tree` 与 3000 legacy inline compat runtime 已通过 `rust_picker_state_reducer_v1` 合同入口执行 next/previous/home/end/pick/focus;当前默认 DOM host 已通过 `TreeShellRuntimeRequest/Result` 消费 Rust/WASM runtime。
|
||||
- [x] 宿主不再持有 file tree 选择真相,只订阅 renderer state event。
|
||||
- `Sidebar` 已拆分 legacy selection 与 renderer selection snapshot;`rust_family` 下只有 `handleFileTreeShellSelectionChange` 写入 renderer snapshot。
|
||||
- `SidebarTreeSurface` 已移除 `selectedRowIds` 控制输入,file tree 选择只能由 renderer event 回报给宿主业务。
|
||||
- 仍未完成:compat runtime 内部选择算法虽已统一为 `rust_filetree_selection_reducer_v1` 合同入口,但尚未替换为 Rust runtime/wasm 直接执行,继续归入 Phase F final renderer / runtime 收口。
|
||||
|
||||
### 验收
|
||||
|
||||
- sidebar 宿主不再包含主要 file tree selection truth 分支;drop legality 仍通过 Rust preflight + 宿主 transport 过渡执行。
|
||||
- 上传和资源移动失败能返回稳定 command error,不依赖前端临时判断。
|
||||
- renderer state 可独立测试,不需要挂载整个 sidebar。
|
||||
|
||||
## 5. Phase H:Rust 搜索 projection
|
||||
|
||||
### 目标
|
||||
|
||||
`file_tree` 搜索过滤不再由宿主裁剪 `kernel file_tree items`,而是由 Rust 输出搜索 projection。
|
||||
|
||||
### checklist
|
||||
|
||||
- [x] 定义 `file_tree.search` 基础输入:
|
||||
- query
|
||||
- root node
|
||||
- include ancestors
|
||||
- include assets
|
||||
- [x] 补齐 `file_tree.search` 扩展输入:
|
||||
- `index.md` 显式作为可搜索资源;仅自身命中时进入结果,祖先仅用于维持路径
|
||||
- `maxResults` 限制直接命中项,必要祖先不计入上限
|
||||
- [x] 定义搜索结果展开策略:
|
||||
- 命中页祖先展开
|
||||
- 命中 asset-folder 展开
|
||||
- 空结果稳定空态
|
||||
- [x] Rust projection fixture 覆盖:
|
||||
- mindmap folder
|
||||
- table
|
||||
- 命中项必要祖先
|
||||
- [x] 补齐搜索 fixture 扩展覆盖:
|
||||
- `index.md`
|
||||
- 附件
|
||||
- 导图子附件
|
||||
- book/pdf
|
||||
- [x] 前端只消费搜索 projection items,不再计算 visible document ids。
|
||||
|
||||
### 验收
|
||||
|
||||
- 搜索态、空态、正常态都由同一 Rust projection contract 驱动。
|
||||
- 前端不再持有搜索过滤的对象语义。
|
||||
- Phase H 当前验收已满足;后续 richer search semantics 作为 hardening,不再阻塞主链。
|
||||
|
||||
## 6. Phase I:move 排序执行下沉
|
||||
|
||||
### 目标
|
||||
|
||||
把 `tree.subtree.move` 的排序执行从 Convex compat mutation 迁到 Rust kernel command 主链。
|
||||
|
||||
### checklist
|
||||
|
||||
- [x] Rust 已有 canonical move order plan 纯函数。
|
||||
- [x] Rust command plan 已可输出 `normalizedMove`。
|
||||
- [x] Convex `documents.move` 增加可选校验:若传入 Rust `normalizedMove`,执行前确认 patch plan 一致。
|
||||
- [x] TS transport 对 `documents:move` 透传 `normalizedMove`,避免 Rust plan 停在不可观测状态。
|
||||
- [x] Rust command plan 已产出正式 `tree.subtree.moved` domain event hint。
|
||||
- [x] `mnote-web /api/tree/commands` Rust 写路径已可直接持久化正式 `tree.subtree.moved` domain event。
|
||||
- [x] 前端/stream 不再由 Next route 写 `replace_documents` 作为 move 主 delta。
|
||||
- [x] 细粒度 move delta reducer 同时更新 `sidebar_tree / page_tree / file_tree`。
|
||||
- [x] `move_document` 不再由 Next route 按 action 手写;当前由 Rust `streamDeltaHint` + mutation result 物化。
|
||||
- [x] `mnote-web /api/tree/commands` 的 `move_document` artifact 已由 Rust transport 直接生成/落库。
|
||||
- [x] 3000 Next 主写入口已切到 Rust artifact writer。
|
||||
|
||||
### 验收
|
||||
|
||||
- sort order clamp、同父移动、跨父移动、重复 sort_order、null sort_order、created_at tie-break 均由 Rust 测试锁定。
|
||||
- Convex 只作为存储执行层,不再持有唯一排序规则。
|
||||
|
||||
## 7. Phase J:正式 delta 主链
|
||||
|
||||
### 目标
|
||||
|
||||
树域 realtime 从 `replace_documents` 过渡 delta,推进到 Rust domain event 驱动的细粒度 delta。
|
||||
|
||||
### checklist
|
||||
|
||||
- [x] domain event 可携带并被 SSE 解释 `streamDelta`。
|
||||
- [x] command log 与 domain event 同一 `command_id` 且 `streamDelta` 一致时,SSE 发一次 `delta` 而不是退回 `resync`。
|
||||
- [x] 定义正式 domain event type hint:
|
||||
- `tree.node.created`
|
||||
- `tree.node.renamed`
|
||||
- `tree.node.archived`
|
||||
- `tree.node.restored`
|
||||
- `tree.node.purged`
|
||||
- `tree.node.duplicated`
|
||||
- `tree.subtree.moved`
|
||||
- `tree.subtree.copied`
|
||||
- `tree.resource.copied`
|
||||
- `tree.resource.moved`
|
||||
- `tree.resource.uploaded`
|
||||
- [x] 定义正式 domain event payload schema(TS artifact 写入边界与 Rust artifact writer 均固定 `mnote.tree.domain_event` v1)
|
||||
- [x] Rust 侧由 command plan 生成 delta / event hint,Next route 不再按 action 拼主 delta / event type。
|
||||
- [x] `mnote-web /api/tree/commands` Rust 写路径已由 command result 直接生成并持久化 domain event。
|
||||
- [x] 显式 compat adapter 写入口已迁出 TS artifact transport。
|
||||
- [x] block/save-snapshot 复合命令已补齐正式 Rust artifact / domain-event contract。
|
||||
- 当前完成口径:`page.body.saved` payload 已携带 snapshot 摘要,`block.patched / block.moved / block.embedded` 已有 formal domain-event contract。
|
||||
- `document.snapshot.saved` 是否拆成独立事件继续留在 `4-16`,不阻塞本阶段收口。
|
||||
- [x] 前端 reducer 支持细粒度 move。
|
||||
- [x] 前端 reducer 支持细粒度 restore/copy:
|
||||
- `upsert_document`
|
||||
- `upsert_documents`
|
||||
- [x] 前端 reducer 支持资源 upsert delta:
|
||||
- `upsert_assets`
|
||||
- [x] 未识别事件继续保守 `resync`。
|
||||
|
||||
### 验收
|
||||
|
||||
- 新树命令默认有 domain event delta。
|
||||
- `replace_documents` 仅作为 fallback,不再是 move / restore / copy 主路径。
|
||||
|
||||
## 8. 不做事项
|
||||
|
||||
- 不为了“看起来 Rust 化”把 React/DOM compat shell 原样翻译成另一层大壳。
|
||||
- 不在 sidebar 继续新增树对象真相。
|
||||
- 不把 `3104` 恢复成默认浏览器入口。
|
||||
- 不在 Next route 继续扩写长期树命令语义。
|
||||
+32
@@ -0,0 +1,32 @@
|
||||
# 4-12 [done] task-003 page tree final renderer 执行清单 v1
|
||||
|
||||
> 口径修正(2026-04-28):本文件的 `done` 状态只表示 page tree 的 Rust initial DOM、hydration patch 与 state-family 阶段完成;不表示 `4-11` 所定义的 final Rust DOM shell 已完成。默认主路径仍依赖 `TreeShellIframeHost` / `iframe_srcdoc` 时,不得引用本文宣称 page tree final renderer 已最终收口。
|
||||
|
||||
## 目标
|
||||
|
||||
让 `page tree` 默认运行时从 inline compat JS 整树重绘路径继续收口到 Rust family renderer contract:
|
||||
|
||||
- 首屏 DOM 继续由 `data-rust-page-renderer="initial_v1"` 输出。
|
||||
- 焦点、active 高亮与键盘移动不再触发 `renderTree()` 整树重绘,只 patch 已 hydrate 的 Rust DOM row 状态。
|
||||
- 展开/折叠不再把 Rust 初始 DOM 替换成 compat DOM;在已 hydrate Rust DOM 内 patch `aria-expanded`、toggle 文案与子树 fragment。
|
||||
- `mnote-web /tree` debug shell 与 3000 inline host 保持同一可测试合同,避免默认路径和 debug 路径继续漂移。
|
||||
|
||||
## 执行项
|
||||
|
||||
- [x] Rust `page_renderer` 初始 HTML 为有子节点的 page row 输出 `tree-node-toggle`,使 Rust DOM 自身具备展开/折叠交互锚点。
|
||||
- [x] 3000 inline `buildInlinePageTreeHtml` 对齐 Rust 初始 HTML,避免 `TreeShellIframeHost` 继续生成缺少 toggle 的第二份 page DOM 合同。
|
||||
- [x] 3000 inline runtime 增加 hydrated page DOM patch:
|
||||
- [x] `focusNode` 在 `usedRustInitialRenderer` 下只更新 `data-active / data-focused / tabIndex`。
|
||||
- [x] `toggleExpand` 在 `usedRustInitialRenderer` 下只 patch 当前节点 `aria-expanded`、toggle 文案与子树显示/插入。
|
||||
- [x] 新插入的 page 子树 row 复用同一 `bindPageRowEvents`,保留 open/create/rename/menu 事件。
|
||||
- [x] `mnote-web /tree` debug runtime 对齐同一 page DOM patch 合同,保留 direct debug 验证边界。
|
||||
- [x] 测试覆盖:
|
||||
- [x] `cargo test -p mnote-web page_renderer`
|
||||
- [x] `pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/components/sidebar/tree-shell-surface.test.tsx`
|
||||
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
|
||||
- [x] 本地 Convex 函数已通过 `npx convex dev --once --tail-logs disable --env-file ../.env.all --run ping:ping` 同步,避免 `documents.move` 运行旧 validator 拒绝 `normalizedMove`。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 本清单不把 file tree 与 picker 一并切 final renderer;它们对应 `task-004`、`task-005`。
|
||||
- 本清单不恢复 `3104` 默认入口;`/tree` 仍只作为显式 debug/internal 验证边界。
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# 4-13 [done] task-004 picker final renderer state family 执行清单 v1
|
||||
|
||||
> 口径修正(2026-04-28):本文件的 `done` 状态只表示 picker state-family / runtime reducer / hydrated DOM patch 阶段完成;不表示 picker 已脱离 `TreeShellIframeHost` 的 inline JS DOM shell。final DOM shell 收口以 `4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准;该硬门禁现已在同目录 `done/` 收口。
|
||||
|
||||
## 目标
|
||||
|
||||
让 `picker` 在 `rust_family` 默认路径下继续退出 compat JS 高亮与 pick 执行路径,收口到轻量 final renderer state family:
|
||||
|
||||
- 首屏 DOM 继续由 `data-rust-picker-renderer="initial_v1"` 输出。
|
||||
- 空查询态、搜索结果态、根目录、排除项使用同一 picker state family 口径。
|
||||
- 键盘高亮与 Enter pick 只走 `rust_picker_state_reducer_v1` 合同入口,并在 hydrated Rust DOM 上 patch `data-focused / tabIndex`。
|
||||
- 鼠标点击 picker row/root 不再绕过 state family;应先归一化当前 active item,再按同一 pick 结果回传宿主。
|
||||
- 3000 inline host 与 `mnote-web /tree` debug shell 保持同一可测试合同。
|
||||
|
||||
## 执行项
|
||||
|
||||
- [x] 补齐 3000 inline picker state family 口径:
|
||||
- [x] `getPickablePickerEntries` 区分 root 与 doc,并应用 `excludeIds` 后只返回可选项。
|
||||
- [x] `normalize`、`focus`、`next/previous/home/end`、`pick` 使用同一 pickable 列表。
|
||||
- [x] 鼠标点击 row/root 通过 state action + `postPickerPickResultToHost`,不直接调用 `handleNavigate` / `tree.pick.root`。
|
||||
- [x] 对齐 `mnote-web /tree` debug runtime:
|
||||
- [x] row/root 点击同样经 state action + pick result helper。
|
||||
- [x] hydrated Rust DOM 下高亮变化只 patch,不整树重绘;键盘 command 不把焦点抢入 iframe,搜索框焦点保持在宿主输入框。
|
||||
- [x] 测试覆盖:
|
||||
- [x] `cargo test -p mnote-web picker_renderer && cargo test -p mnote-web picker_state`
|
||||
- [x] `pnpm vitest run src/components/documents/move-embed-picker-dialog.test.tsx src/components/sidebar/tree-shell-iframe-host.test.tsx`
|
||||
- [x] `node scripts/task113-picker-keyboard-regression-smoke.js`
|
||||
|
||||
## 完成记录
|
||||
|
||||
- 2026-04-26:`task113` 的根因是键盘 command 后 hydrated picker row 主动 `focus()`,导致父级搜索输入框失焦;已改为键盘/state patch 只更新 `data-focused / tabIndex`,点击路径才允许 `focusDom: true`。
|
||||
- 2026-04-26:3000 inline host 与 `mnote-web /tree` debug shell 已同步 `postPickerPickResultToHost`、pickable state family、row/root click state action 路径。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 本清单不处理 file tree 的 final renderer;对应 `task-005`。
|
||||
- 本清单不拆除 compat host;对应 `task-006`。
|
||||
+37
@@ -0,0 +1,37 @@
|
||||
# 4-14 [done] task-005 file tree final renderer 能力保留执行清单 v1
|
||||
|
||||
> 口径修正(2026-04-28):本文件的 `done` 状态只表示 file tree 的 Rust initial DOM、row contract、selection state family 与能力保留阶段完成;不表示 file tree 已完成 final Rust DOM shell 切换。默认主路径仍依赖 `iframe_srcdoc` 时,不得用本文作为最终完成依据。
|
||||
|
||||
## 目标
|
||||
|
||||
让 `file tree` 在 `rust_family` 默认路径下继续从 3000 inline compat host 收口到 final Rust renderer state family,同时保留文件树关键能力:
|
||||
|
||||
- 首屏 DOM 由 `data-rust-filetree-renderer="initial_v1"` 输出,并覆盖 `doc / index / asset-folder / asset` 行语义。
|
||||
- 选择状态只通过 `rust_filetree_selection_reducer_v1` 合同入口执行与回报,不再由宿主反向控制。
|
||||
- 内部拖放、外部文件拖入、右键菜单、双击打开、空白区清空选择与 drop target dataset 不退化。
|
||||
- 3000 inline host 与 `mnote-web /tree` debug shell 保持同一可测试合同。
|
||||
|
||||
## 执行项
|
||||
|
||||
- [x] 补齐 3000 inline file tree hydrated runtime:
|
||||
- [x] Rust initial DOM 包含 `doc / index / asset-folder / asset` 行 test id、row kind、document id、asset id、icon hint 与 selected/active dataset。
|
||||
- [x] `asset-folder` 与 `asset` 行保留打开、右键、拖放目标与资源打开 bridge。
|
||||
- [x] 选择、右键选择、Shift/Ctrl/Cmd 多选、可见行归一化与 drag rows 解析统一走 `rust_filetree_selection_reducer_v1` 镜像入口。
|
||||
- [x] active/selection/drop feedback 在 hydrated Rust DOM 上 patch dataset,不因宿主 patch 触发整树重绘。
|
||||
- [x] 对齐 `mnote-web /tree` debug runtime:
|
||||
- [x] debug shell 的 file tree row 语义、selection contract、drag/drop bridge 与 3000 inline host 同步。
|
||||
- [x] fallback render path 仍可作为 debug/internal 边界,但主路径首屏不再清空 Rust initial DOM。
|
||||
- [x] 测试覆盖:
|
||||
- [x] `cargo test -p mnote-web filetree_renderer && cargo test -p mnote-web filetree_selection`
|
||||
- [x] `pnpm vitest run src/components/sidebar/tree-shell-iframe-host.test.tsx src/lib/file-tree/shell.test.ts src/lib/tree-stream/tree-delta.test.ts`
|
||||
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
|
||||
|
||||
## 完成记录
|
||||
|
||||
- 2026-04-26:代码对照确认 3000 inline host 与 `mnote-web /tree` debug shell 均已 hydrate `data-rust-filetree-renderer="initial_v1"`,并保留 `doc / index / asset-folder / asset` row contract、selection reducer contract、drag/drop bridge 与资源打开 bridge。
|
||||
- 2026-04-26:task-005 完整验证通过,未发现需要新增生产改动的缺口;本清单作为能力门槛记录。
|
||||
|
||||
## 非目标
|
||||
|
||||
- 本清单不拆除 compat host 的所有 debug fallback;对应 `task-006`。
|
||||
- 本清单不新增业务命令语义;file tree drop/delete/paste/upload preflight 已在前置任务中下沉 Rust。
|
||||
+31
@@ -0,0 +1,31 @@
|
||||
# 4-15 [done] task-006 compat host thinning 执行清单 v1
|
||||
|
||||
> 口径修正(2026-04-28):本文件的 `done` 状态只表示本批可验证范围内的 compat host 收薄完成;不表示 compat host 已退为纯 debug/internal,也不表示 `TreeShellIframeHost` 的 JS DOM/state machine 已从默认真实流量删除。最终退场门禁以 `4-18-tree-final-dom-shell-cutover-hard-gate-v1.md` 为准;该硬门禁现已在同目录 `done/` 收口。
|
||||
|
||||
## 目标
|
||||
|
||||
把本批可验证范围内的 compat host 继续收薄:默认 3000 主路径不再请求 `mnote-web:3104` proxy,不回退旧 React renderer,不再通过 `__MNOTE_TREE_SHELL_OVERRIDE__` 注入第二份主路径 items;page/filetree/picker 的首屏 Rust initial DOM 与 rendererInput/state family 成为主路径合同。
|
||||
|
||||
## 当前边界
|
||||
|
||||
- 本任务不声称已经完成完整 Rust runtime / wasm 替换。
|
||||
- `TreeShellIframeHost` 仍是 3000 主路径的 same-origin inline host;它的职责已收窄为挂载 srcDoc、postMessage transport、状态 patch 与 debug/internal fallback,而不是最终移除。
|
||||
- 3104 `/tree` 只保留显式 debug/internal 验证边界,`task112` 中 direct tree shell debug 已默认跳过。
|
||||
|
||||
## 执行项
|
||||
|
||||
- [x] 主路径宿主边界:
|
||||
- [x] page/filetree/picker 在 `rust_family` 下继续进入同源 iframe host,不回退旧 React renderer。
|
||||
- [x] 缺少 `workspaceId` 时显示 renderer removed 占位,不恢复旧 React renderer。
|
||||
- [x] 3000 inline `srcDoc` 主路径使用 `state.items + rendererInput`,不再依赖 `__MNOTE_TREE_SHELL_OVERRIDE__` 作为第二输入真相。
|
||||
- [x] 状态与事件收薄:
|
||||
- [x] sidebar refresh/sync 保持事件与语义 key,而不是引入新的树结构重建真相。
|
||||
- [x] file tree selection 只通过 renderer event snapshot 回宿主业务。
|
||||
- [x] page/filetree/picker smoke 继续覆盖主路径 iframe host 的导航、右键、双击打开、picker 搜索/选中与 stream fallback。
|
||||
- [x] 验证覆盖:
|
||||
- [x] `pnpm vitest run src/components/sidebar/tree-shell-surface.test.tsx src/components/sidebar/sidebar-events.test.ts src/components/sidebar/sidebar-sync.test.ts`
|
||||
- [x] `node scripts/task112-tree-rust-family-regression-smoke.js`
|
||||
|
||||
## 后续剩余
|
||||
|
||||
- 完整“compat host 退为 debug/internal fallback”仍需要真正 Rust runtime/wasm 或等价正式 renderer 承接运行时 DOM 与状态机;当前已把本批可验证主路径收薄记录为阶段完成。
|
||||
@@ -0,0 +1,172 @@
|
||||
# 4-18 [done] 树域 final DOM shell 切换硬门禁 v1
|
||||
|
||||
> 创建时间:2026-04-28
|
||||
>
|
||||
> 目的:纠正此前把 `Rust/WASM reducer runtime` 误当成 `final Rust renderer` 的完成口径,重新定义 page tree / file tree / picker 真正收口所需的不可绕过门禁。
|
||||
>
|
||||
> 前置文档:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-11-tree-rust-family-final-renderer-and-host-thinning-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-16-tree-rust-family-remaining-final-runtime-checklist-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/1.md`
|
||||
|
||||
## 1. 口径纠偏
|
||||
|
||||
### 2026-04-28 收口结果
|
||||
|
||||
本轮 `task-010` 至 `task-013` 已按本文硬门禁完成默认主路径切换:
|
||||
|
||||
- `TreeShellHost` 在 `rust_family + workspaceId` 下默认选择 `rust_wasm_dom_shell_host`。
|
||||
- `TreeShellIframeHost` 仅在显式 `NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST=1` 时进入,继续作为 legacy/debug 排障 host。
|
||||
- 默认 page tree / file tree / picker 主路径标注 `data-tree-browser-bridge="dom_wasm"`,负向测试禁止 `iframe_srcdoc` 被当成默认成功路径。
|
||||
- 默认 DOM host 不再调用 `LOCAL_TREE_SHELL_TEMPLATE`、`buildInlinePageTreeHtml`、`buildInlineFileTreeHtml`、`buildInlinePickerHtml`。
|
||||
- 默认 DOM host 的展开、选择、高亮、打开、右键、拖放、picker pick 由 `reduceTreeShellRuntime` WASM artifact 或同源 `/api/tree/runtime/reduce` Rust seam 返回的 `state / hostEvents / commandEvents` 驱动;本地 JS fallback reducer 只保留在 legacy iframe host 内。
|
||||
- `task112-tree-rust-family-regression-smoke.js` 与 `task113-picker-keyboard-regression-smoke.js` 已支持并验证默认 DOM host;legacy iframe 仅兼容显式 legacy 路径。
|
||||
|
||||
`task-010` 之前,过去几轮已经完成的是:
|
||||
|
||||
- Rust renderer input / state family / runtime facade。
|
||||
- `tree-shell-runtime-wasm` reducer artifact。
|
||||
- 3000 主路径 wasm-first `reduceTreeShellRuntime`。
|
||||
- page / filetree / picker 的若干 hostEvent / commandEvent / DOM patch 切片。
|
||||
- `rust_runtime_artifact_host` 主路径标识。
|
||||
|
||||
这些都只是 `Rust/WASM reducer runtime-first`,不是 `final Rust DOM renderer`。
|
||||
|
||||
`task-010` 之前真正没有完成的是:
|
||||
|
||||
- 当时 `TreeShellIframeHost` 仍在 3000 主路径生成 `srcDoc`。
|
||||
- 当时 `LOCAL_TREE_SHELL_TEMPLATE` 仍承载 DOM 壳、事件绑定、hydration、fallback renderer 与状态 patch。
|
||||
- 当时 `data-tree-browser-bridge="iframe_srcdoc"` 仍是默认真实流量的浏览器执行层。
|
||||
- 当时 page / filetree / picker 的 DOM shell 仍由 JS adapter 维护,Rust/WASM 只主导 reducer result。
|
||||
|
||||
因此,后续任何文档、harness、提交说明都不能再把 `task112/task113 通过`、`rust_runtime_artifact_host` 或 `wasmModuleUrl/jsGlueUrl 非空` 作为 final renderer 完成声明;必须同时检查默认 host implementation、浏览器 bridge 标记和 legacy flag 边界。
|
||||
|
||||
## 2. 不可绕过的完成定义
|
||||
|
||||
只有同时满足以下条件,才允许声明“页面树 / 文件树已切换为 Rust final renderer”:
|
||||
|
||||
- 3000 默认真实流量的 page tree / file tree / picker 不再通过 `TreeShellIframeHost` 的 `srcDoc` 内联模板承载 DOM shell。
|
||||
- 默认主路径不再标注 `data-tree-browser-bridge="iframe_srcdoc"`。
|
||||
- `LOCAL_TREE_SHELL_TEMPLATE`、`buildInlinePageTreeHtml`、`buildInlineFileTreeHtml`、`buildInlinePickerHtml` 不再参与默认 page / filetree / picker 渲染路径。
|
||||
- `TreeShellIframeHost` 只能作为显式 legacy/debug host 存在,必须由明确 debug flag 或 debug route 进入。
|
||||
- Rust/WASM 或等价 Rust family renderer 持有初始渲染、动态 DOM patch、事件绑定归一化、展开/选择/高亮/拖放反馈的运行时 DOM shell。
|
||||
- 3000 host 只保留挂载、认证、transport、文件字节读取、postMessage / command bridge、artifact 资源发布等浏览器能力边界。
|
||||
- page / filetree / picker 的主路径回归测试必须包含负向断言:默认路径不得出现 `iframe_srcdoc` DOM renderer。
|
||||
- `task112/task113` 仍通过,并且 smoke 需要验证的是新 Rust DOM shell 主路径,而不是旧 inline iframe host。
|
||||
|
||||
## 3. 第一优先级:先加负向门禁
|
||||
|
||||
下一轮实现前必须先补负向测试,防止继续小步绕开最终目标。
|
||||
|
||||
### 前端门禁
|
||||
|
||||
- `tree-shell-iframe-host.test.tsx` 或新测试必须断言默认主路径不再输出:
|
||||
- `srcDoc={renderedInlineSrcDoc}`
|
||||
- `data-tree-browser-bridge="iframe_srcdoc"`
|
||||
- `LOCAL_TREE_SHELL_TEMPLATE`
|
||||
- `buildInlineInitialTreeHtml`
|
||||
- `buildInlinePageTreeHtml`
|
||||
- `buildInlineFileTreeHtml`
|
||||
- `buildInlinePickerHtml`
|
||||
- 允许这些字符串只出现在显式 `legacy/debug` 测试或旧 host 文件中,但不能在默认 host implementation 测试里作为成功条件。
|
||||
|
||||
### smoke 门禁
|
||||
|
||||
- `task112-tree-rust-family-regression-smoke.js` 需要确认 page/filetree 默认 renderer host 不是 inline srcdoc host。
|
||||
- `task113-picker-keyboard-regression-smoke.js` 需要确认 picker 默认 renderer host 不是 inline srcdoc host。
|
||||
- direct `/api/tree/shell -> 3104` debug disabled 仍应保留,但它不能替代 final DOM shell 验收。
|
||||
|
||||
### 文档门禁
|
||||
|
||||
- `harness` 任务完成条件必须包含“不依赖 iframe srcdoc DOM shell”。
|
||||
- 任一阶段文档若只完成 reducer/state family,标题和完成记录必须写成 `runtime reducer/state-family stage`,不得写成 `final renderer completed`。
|
||||
|
||||
## 4. 实现路线
|
||||
|
||||
### Phase P:冻结旧 inline host 主路径
|
||||
|
||||
状态:已完成。
|
||||
|
||||
目标:把旧 `TreeShellIframeHost` 从“默认主路径”降级为“legacy/debug 候选”。
|
||||
|
||||
执行项:
|
||||
|
||||
- 新增主路径 host 名称,例如 `rust_dom_shell_host` 或 `rust_wasm_dom_shell_host`。
|
||||
- 保留旧 `TreeShellIframeHost`,但改名或包裹为 `LegacyTreeShellIframeHost`,只允许 debug/legacy flag 进入。
|
||||
- `TreeShellHost` 默认选择新 host;旧 host 不再作为 `rust_family` 默认实现。
|
||||
- 所有旧 `srcDoc` 合同测试迁到 legacy/debug 测试组。
|
||||
|
||||
验收:
|
||||
|
||||
- 默认 `TreeShellHost` 测试不再匹配 `iframe_srcdoc`。
|
||||
- legacy/debug 测试仍能证明旧 host 可用于回退排障。
|
||||
|
||||
### Phase Q:建立 Rust/WASM DOM shell host
|
||||
|
||||
状态:已完成默认主路径切换。
|
||||
|
||||
目标:给 page / filetree / picker 建立一个不依赖 inline JS template 的 DOM shell 承载。
|
||||
|
||||
执行项:
|
||||
|
||||
- 复用现有 `rendererInput` 与 `TreeShellRuntimeRequest/Result`。
|
||||
- WASM artifact 输出或驱动 DOM shell 初始化,不再由 `buildInline*Html` 生成主路径 HTML。
|
||||
- DOM patch、hostEvent、commandEvent 的应用入口统一放在新 host adapter,adapter 只负责浏览器边界,不维护树状态机。
|
||||
- page tree 先切,再切 picker,最后切 filetree。
|
||||
|
||||
验收:
|
||||
|
||||
- page / picker / filetree 默认主路径首屏可见。
|
||||
- 键盘、展开、选择、右键、打开、拖放反馈继续工作。
|
||||
- `task112/task113` 使用新 host 通过。
|
||||
|
||||
### Phase R:删除默认路径 JS renderer 状态机
|
||||
|
||||
状态:已完成默认路径隔离;旧 JS renderer/state machine 仅保留在 legacy iframe host。
|
||||
|
||||
目标:移除默认主路径对旧 JS DOM renderer/state machine 的依赖。
|
||||
|
||||
执行项:
|
||||
|
||||
- 默认路径删除对 `renderTree()`、`renderFileTree()`、`hydrateInitialPageTree()`、`hydrateInitialPickerTree()` 等旧 DOM shell 的调用。
|
||||
- 旧本地 fallback reducer 只保留在 legacy/debug host,不参与默认真实流量。
|
||||
- 删除或隔离 `buildInlinePageTreeHtml` / `buildInlineFileTreeHtml` / `buildInlinePickerHtml` 的默认路径调用。
|
||||
|
||||
验收:
|
||||
|
||||
- 默认 host implementation 中没有 `srcDoc` 主渲染链。
|
||||
- 默认主路径没有第二套 JS 展开、选择、高亮、拖放接受规则。
|
||||
- `TreeShellIframeHost` 可被删除,或只以 `LegacyTreeShellIframeHost` 形式留在 debug 目录。
|
||||
|
||||
### Phase S:最终文档收口
|
||||
|
||||
状态:已更新 4-11、4-16、4-18 的真实代码状态。
|
||||
|
||||
目标:只在真实代码满足门禁后移动文档状态。
|
||||
|
||||
执行项:
|
||||
|
||||
- `4-11`、`4-16`、`4-18` 中所有 final DOM shell gate 勾选。
|
||||
- 将真正完成后的最终清单移动到 `done/`。
|
||||
- 若 `4-12` 到 `4-15` 的旧标题继续误导,则移动到 `design/old/done` 或在标题中明确标记 `state-family stage`。
|
||||
|
||||
验收:
|
||||
|
||||
- 文档状态与默认真实流量一致。
|
||||
- 没有任何 `done` 文档暗示 `iframe_srcdoc` 主路径等于 final Rust renderer。
|
||||
|
||||
## 5. 非目标
|
||||
|
||||
- 不拆除 Convex substrate。
|
||||
- 不恢复 `3104` 为默认浏览器入口。
|
||||
- 不把文件字节读取、浏览器 DnD `File[]` 对象直接塞入 Rust/WASM。
|
||||
- 不因为切 DOM shell 而重写 tree command / projection / artifact contract。
|
||||
- 不把 `task112/task113` 的旧 inline host smoke 当作最终通过标准。
|
||||
|
||||
## 6. 当前下一步
|
||||
|
||||
本文定义的 final DOM shell 硬门禁已收口。后续继续推进时,不应再回到 `iframe_srcdoc` 默认路径,而应沿以下方向继续减少过渡面:
|
||||
|
||||
1. 保留 `NEXT_PUBLIC_TREE_SHELL_LEGACY_IFRAME_HOST=1` 作为短期排障开关,后续在 smoke 与线上观测稳定后删除旧 host。
|
||||
2. 继续把剩余 tree command、Page Aggregate、snapshot 独立事件等非 DOM shell 收口项按各自设计稿推进。
|
||||
3. 若新增 page / filetree / picker 交互,默认必须接入 `TreeShellRuntimeRequest/Result`,不得在 DOM host 增加第二套 JS reducer。
|
||||
+235
@@ -0,0 +1,235 @@
|
||||
# 4-2 [done] Sidebar / 页面树 / 文件树 产品交互合同 v1
|
||||
|
||||
> 更新时间:2026-04-17
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档不是继续讨论“是否要做 Rust tree shell”。
|
||||
|
||||
这份文档要冻结的是:
|
||||
|
||||
- 页面树对标 Wolai / Notion 的最小产品交互合同
|
||||
- 文件树对标 VS Code Explorer 的最小产品交互合同
|
||||
- 当前旧树已经具备的能力基线
|
||||
- 新 Rust tree shell 必须补齐的能力矩阵
|
||||
- 后续 `projection / command / row model / selection model / focus model / keyboard / DnD / context menu` 的最低验收口径
|
||||
|
||||
也就是说,这份文档是下一阶段多人并行推进时的共同合同,而不是描述性分析。
|
||||
|
||||
---
|
||||
|
||||
## 2. 基本原则
|
||||
|
||||
- 页面树 / 文件树都不是事实源,它们都只是 `tree-first graph kernel` 的 projection。
|
||||
- Sidebar 是壳,不是树真相。
|
||||
- 新实现不能以“能显示树结构”作为完成标准,而要以“不比旧交互与 UI 差”作为最低标准。
|
||||
- 文件树与页面树允许在 UI 上不同,但必须共享同一套 projection 与 command 主骨架。
|
||||
- 所有新增能力都应优先落到可测试的 `row model / selection model / focus model / keyboard / DnD` 层,而不是先堆散落 UI 事件。
|
||||
|
||||
---
|
||||
|
||||
## 3. 页面树合同
|
||||
|
||||
### 3.1 对标目标
|
||||
|
||||
- 功能对标:Wolai / Notion 页面树
|
||||
- 视觉与节奏对标:轻量、低干扰、hover 才显动作、不是调试面板
|
||||
|
||||
### 3.2 页面树必须具备的最低能力
|
||||
|
||||
- 稳定的页面层级展开 / 折叠
|
||||
- 当前页高亮与祖先自动展开
|
||||
- 行级 hover 动作区
|
||||
- 新建子页面
|
||||
- 重命名
|
||||
- 页面移动
|
||||
- 上下文菜单入口
|
||||
- 焦点与键盘导航
|
||||
- 基础拖拽排序
|
||||
- 搜索过滤后仍保持树层级可理解
|
||||
|
||||
### 3.3 页面树必须保留的旧能力基线
|
||||
|
||||
- 右键菜单不是只有重命名/删除,而应保留工作区级高频动作入口
|
||||
- 页面树不能退化成纯按钮列表
|
||||
- 大树场景不能因切流而失去稳定滚动体验
|
||||
- 主树 consumer 不能重新持有第二套结构真相
|
||||
|
||||
---
|
||||
|
||||
## 4. 文件树合同
|
||||
|
||||
### 4.1 对标目标
|
||||
|
||||
- 功能对标:VS Code Explorer
|
||||
- 视觉密度对标:资源管理器,而不是文档树换皮
|
||||
|
||||
### 4.2 文件树必须具备的最低能力
|
||||
|
||||
- 文件树专用 row model
|
||||
- 页面、`index.md`、附件、mindmap 文件夹、导图子附件的复合资源层级
|
||||
- 单选
|
||||
- 多选
|
||||
- Shift 范围选
|
||||
- 右键菜单入口
|
||||
- 双击打开资源
|
||||
- 基础键盘导航
|
||||
- 目录拖拽骨架
|
||||
- 外部文件拖入上传骨架
|
||||
- 资源图标语义
|
||||
- 资源类型菜单分支
|
||||
|
||||
### 4.3 文件树必须保留的旧能力基线
|
||||
|
||||
- 不能退化成“页面树加附件列表”
|
||||
- 不能丢掉多选与范围选
|
||||
- 不能丢掉资产级操作入口
|
||||
- 不能丢掉内部拖拽/复制与外部文件拖入的扩展空间
|
||||
- mindmap 相关资源不能被拍平成普通附件列表
|
||||
|
||||
---
|
||||
|
||||
## 5. 统一协议合同
|
||||
|
||||
### 5.1 Projection 合同
|
||||
|
||||
所有树 consumer 必须能明确声明自己消费哪一种 projection:
|
||||
|
||||
- `sidebar_tree`
|
||||
- `page_tree`
|
||||
- `file_tree`
|
||||
|
||||
最小字段基线:
|
||||
|
||||
- `row_id`
|
||||
- `node_id`
|
||||
- `parent_node_id`
|
||||
- `node_type`
|
||||
- `projection_kind`
|
||||
- `depth`
|
||||
- `position`
|
||||
- `title`
|
||||
- `capabilities`
|
||||
- `resource_meta`
|
||||
|
||||
文件树扩展字段:
|
||||
|
||||
- `resource_kind`
|
||||
- `asset_kind`
|
||||
- `icon_hint`
|
||||
- `expandable`
|
||||
- `expanded_by_default`
|
||||
|
||||
### 5.2 Command 合同
|
||||
|
||||
命令面至少要为后续产品交互预留稳定口径:
|
||||
|
||||
- `tree.node.create`
|
||||
- `tree.node.rename`
|
||||
- `tree.subtree.move`
|
||||
- `tree.node.archive`
|
||||
- `tree.node.restore`
|
||||
- `tree.asset.attach`
|
||||
- `tree.asset.detach`
|
||||
|
||||
### 5.3 本地 UI 状态合同
|
||||
|
||||
以下状态不应回流为结构真相,只能留在 UI 本地状态层:
|
||||
|
||||
- `expanded`
|
||||
- `selected`
|
||||
- `hover`
|
||||
- `focus`
|
||||
- `dragging`
|
||||
- `drop target`
|
||||
|
||||
---
|
||||
|
||||
## 6. 状态骨架合同
|
||||
|
||||
### 6.1 row model
|
||||
|
||||
必须单独存在,不能散落在 renderer 中。
|
||||
|
||||
最低要求:
|
||||
|
||||
- 页面树与文件树都能从 projection 映射到稳定 row model
|
||||
- row model 可以独立测试
|
||||
- row model 不再重新定义结构真相
|
||||
|
||||
### 6.2 selection model
|
||||
|
||||
最低要求:
|
||||
|
||||
- 单选
|
||||
- 多选
|
||||
- Shift 范围选
|
||||
- 右键选中
|
||||
- 可见行变化后的选择归一化
|
||||
|
||||
### 6.3 focus model
|
||||
|
||||
最低要求:
|
||||
|
||||
- 当前焦点行稳定可追踪
|
||||
- 焦点与选中不完全等价
|
||||
- 页面树与文件树都能共享焦点层定义
|
||||
|
||||
### 6.4 keyboard
|
||||
|
||||
最低要求:
|
||||
|
||||
- 上下导航
|
||||
- 左右展开/折叠
|
||||
- Enter 打开
|
||||
- 文件树预留 copy / paste / delete 的按键入口
|
||||
|
||||
### 6.5 DnD
|
||||
|
||||
最低要求:
|
||||
|
||||
- 页面树支持基础排序/移动
|
||||
- 文件树支持目录拖放骨架
|
||||
- 文件树支持外部文件拖入的扩展点
|
||||
- 非法投放校验必须可测试
|
||||
|
||||
### 6.6 context menu
|
||||
|
||||
最低要求:
|
||||
|
||||
- 页面树与文件树都有统一的 context menu 入口模型
|
||||
- 菜单本身可按资源类型分支
|
||||
- shell 与宿主之间要有稳定动作协议,而不是只靠壳内 `prompt/alert`
|
||||
|
||||
---
|
||||
|
||||
## 7. 当前判定
|
||||
|
||||
截至 2026-04-28 final DOM shell 收口后:
|
||||
|
||||
- `page_tree`:默认主路径已进入 `rust_wasm_dom_shell_host + dom_wasm`,展开/折叠、当前页高亮、祖先展开、行级动作、重命名、移动、上下文菜单、键盘与基础拖拽均由 Rust runtime contract 驱动或回放。
|
||||
- `picker`:轻量选择器已复用同一 renderer state family,键盘高亮、Enter 选中、根节点与排除项逻辑已进入默认 DOM host。
|
||||
- `file_tree`:默认主路径已进入 `rust_wasm_dom_shell_host + dom_wasm`,页面、`index.md`、附件、`mindmap` 文件夹、资源图标、单选/多选/范围选、右键、双击打开、内部拖放与外部文件拖入均保留。
|
||||
|
||||
本合同的产品交互最低门槛已由 `4-11` 与 `4-18` 收口;剩余 `normalizedMove` fallback、`document.snapshot.saved` 独立事件、`tree.node.embed pageReference` 等属于后续 Page Aggregate / runtime 深层收口,不再阻塞本文。
|
||||
|
||||
- `filetree` 模式闭环已由 Rust projection、runtime state family、preflight 与默认 DOM host 共同承接。
|
||||
- 页面树 / 文件树正式交互合同已固定,并由 `task112/task113` smoke 与组件/Rust 测试覆盖。
|
||||
- row model / selection model / focus model / keyboard / DnD / context menu 骨架已进入 renderer state family 与 runtime facade。
|
||||
|
||||
---
|
||||
|
||||
## 8. 完成判定
|
||||
|
||||
只有同时满足下面几条,才可以宣称“新树不比旧交互与 UI 差”:
|
||||
|
||||
- 页面树满足本合同第 3 节最低能力
|
||||
- 文件树满足本合同第 4 节最低能力
|
||||
- projection / command 合同不再漂移
|
||||
- `row model / selection model / focus model / keyboard / DnD / context menu` 有独立实现与测试
|
||||
- filetree 不再依赖协议裂缝或壳内临时拼装来维持主路径
|
||||
@@ -0,0 +1,218 @@
|
||||
# 4-4 [done] Tree Projection Protocol Contract v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 关联:
|
||||
> - `/mnt/Data1T/mnote/design/04-tree-domain/process/4-sidebar-pagetree-filetree-rust-web-rebuild-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 目的
|
||||
|
||||
这份文档用于冻结树域 projection contract,避免 `sidebar_tree`、`page_tree`、`file_tree` 在后续 Rust route、Leptos tree shell、Next 挂载切流阶段继续各自长字段。
|
||||
|
||||
这里固定三条原则:
|
||||
|
||||
- 树域只消费 projection,不消费前端自己拼出来的结构真相
|
||||
- Rust 与前端共享同一组 projection 字段语义
|
||||
- `sidebar_tree`、`page_tree`、`file_tree` 是同一协议家族,不是三套无关返回值
|
||||
|
||||
## 2. 协议分层
|
||||
|
||||
### 2.1 Rust canonical contract
|
||||
|
||||
Rust 侧的 canonical contract 统一表达为:
|
||||
|
||||
- `projection_id`
|
||||
- `projection`
|
||||
- `root_node_id`
|
||||
- `items`
|
||||
- `edges`
|
||||
|
||||
`items` 里的字段语义冻结如下。
|
||||
|
||||
### 2.2 TypeScript transport contract
|
||||
|
||||
前端 TypeScript 侧继续使用 camelCase 字段,但语义必须与 Rust 一致:
|
||||
|
||||
- `projectionId`
|
||||
- `projectionKind`
|
||||
- `rootNodeId`
|
||||
- `parentNodeId`
|
||||
- `resourceMeta`
|
||||
- `iconHint`
|
||||
- `expandedByDefault`
|
||||
|
||||
也就是说:
|
||||
|
||||
- Rust 是 canonical truth
|
||||
- TypeScript 只是命名风格映射,不允许再引入第二套语义
|
||||
|
||||
## 3. 共享字段
|
||||
|
||||
下面这些字段属于 `sidebar_tree`、`page_tree`、`file_tree` 的共享基线。
|
||||
|
||||
| canonical | TS | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `node_id` | `nodeId` | 当前 projection item 对应的 kernel node |
|
||||
| `parent_node_id` | `parentNodeId` | 上级 node,根节点为 `null` |
|
||||
| `node_type` | `nodeType` | `workspace/folder/page/section/asset/book/pdf/mindmap/table/index` 等语义类型 |
|
||||
| `projection_kind` | `projectionKind` | 当前 item 属于哪种 projection:`sidebar_tree/page_tree/file_tree` |
|
||||
| `title` | `title` | 展示标题,允许兜底为“无标题” |
|
||||
| `depth` | `depth` | 当前树深度 |
|
||||
| `position` | `position` | 同级排序位置 |
|
||||
| `child_count` | `childCount` | 子项数量 |
|
||||
| `expandable` | `expandable` | 当前 item 是否理论上可展开 |
|
||||
| `expanded_by_default` | `expandedByDefault` | 默认展开建议 |
|
||||
| `capabilities` | `capabilities` | 当前行允许的交互能力 |
|
||||
| `resource_meta` | `resourceMeta` | 绑定到业务资源的元信息 |
|
||||
| `icon_hint` | `iconHint` | 给 shell / renderer 的图标提示 |
|
||||
|
||||
## 4. `capabilities` 冻结口径
|
||||
|
||||
当前允许的共享 `capabilities` 为:
|
||||
|
||||
- `expand`
|
||||
- `open`
|
||||
- `drag`
|
||||
- `drop`
|
||||
- `select`
|
||||
- `create-child`
|
||||
- `rename`
|
||||
- `archive`
|
||||
- `restore`
|
||||
- `context-menu`
|
||||
- `reorder`
|
||||
- `open-asset`
|
||||
- `pick`
|
||||
|
||||
约束:
|
||||
|
||||
- `capabilities` 只表达“允许做什么”
|
||||
- 它不表达局部 UI 状态
|
||||
- 它不表达 hover、selected、dragging、drop target 这类临时态
|
||||
|
||||
## 5. `resource_meta` 冻结口径
|
||||
|
||||
`resource_meta` 统一用于表达 projection item 背后的真实资源。
|
||||
|
||||
共享字段:
|
||||
|
||||
- `resource_kind`
|
||||
- `document_id`
|
||||
- `asset_id`
|
||||
- `workspace_id`
|
||||
- `asset_kind`
|
||||
- `icon_hint`
|
||||
- `extra`
|
||||
|
||||
### 5.1 `resource_kind`
|
||||
|
||||
当前冻结为:
|
||||
|
||||
- `workspace`
|
||||
- `document`
|
||||
- `index`
|
||||
- `asset`
|
||||
- `asset_folder`
|
||||
- `mindmap`
|
||||
- `table`
|
||||
- `book`
|
||||
- `pdf`
|
||||
|
||||
### 5.2 `asset_kind`
|
||||
|
||||
当前冻结为:
|
||||
|
||||
- `file`
|
||||
- `mindmap`
|
||||
- `table`
|
||||
- `book`
|
||||
- `pdf`
|
||||
- `image`
|
||||
- `video`
|
||||
- `audio`
|
||||
- `unknown`
|
||||
|
||||
## 6. 三类 projection 的差异字段
|
||||
|
||||
### 6.1 `sidebar_tree`
|
||||
|
||||
`sidebar_tree` 是最轻的导航 projection。
|
||||
|
||||
要求:
|
||||
|
||||
- 保留共享字段
|
||||
- 不引入 `row_id`
|
||||
- 资源主语义通常落在 `resource_kind=document`
|
||||
- 允许后续继续作为 workspace 首屏 / stream snapshot 主链
|
||||
|
||||
### 6.2 `page_tree`
|
||||
|
||||
`page_tree` 是页面树 projection。
|
||||
|
||||
它在共享字段基础上新增:
|
||||
|
||||
- `row_id`
|
||||
|
||||
当前 `row_id` 命名约束:
|
||||
|
||||
- `page:<nodeId>`
|
||||
|
||||
要求:
|
||||
|
||||
- `page_tree` 必须可直接生成 picker 轻量列表
|
||||
- `page_tree` 必须可直接生成可见 rows
|
||||
- `page_tree` 不允许重新回退成前端自行 flatten 嵌套树
|
||||
|
||||
### 6.3 `file_tree`
|
||||
|
||||
`file_tree` 是页面树骨架上的更宽对象投影。
|
||||
|
||||
它在共享字段基础上强调这些字段必须稳定存在:
|
||||
|
||||
- `resource_kind`
|
||||
- `asset_kind`
|
||||
- `icon_hint`
|
||||
- `expandable`
|
||||
- `expanded_by_default`
|
||||
|
||||
要求:
|
||||
|
||||
- `file_tree` 不能继续由前端用 `asset-folder` / `asset` / `index` 临时猜语义作为长期真相
|
||||
- `file_tree` 必须由 Rust 直接输出 `document/index/asset/asset_folder/mindmap/table/book/pdf` 等对象投影
|
||||
|
||||
## 7. 当前稳定项与过渡项
|
||||
|
||||
### 7.1 已稳定
|
||||
|
||||
- `sidebar_tree` 基础 contract
|
||||
- `page_tree` 的 `row_id/node_id/parent_node_id/depth/position/capabilities/resource_meta`
|
||||
- `expanded_by_default`
|
||||
- `child_count`
|
||||
|
||||
### 7.2 仍处于过渡
|
||||
|
||||
- `file_tree` 里的 `asset_folder/asset/index` 仍有前端 adapter 残留
|
||||
- `icon_hint` 还没有完全由 Rust 主导
|
||||
- `asset_kind` 仍需要从更宽对象类型扩展到 `book/pdf`
|
||||
|
||||
## 8. Fixture 与 Contract Test 要求
|
||||
|
||||
后续所有树域 fixture / contract tests 至少覆盖:
|
||||
|
||||
- `sidebar_tree` 基础 fixture
|
||||
- `page_tree` fixture
|
||||
- `file_tree` fixture
|
||||
- `resource_meta` 差异字段
|
||||
- `capabilities`
|
||||
- `icon_hint`
|
||||
- `expanded_by_default`
|
||||
|
||||
## 9. 完成判定
|
||||
|
||||
当以下条件同时成立时,视为本 contract 冻结完成:
|
||||
|
||||
- `sidebar_tree`、`page_tree`、`file_tree` 的共享字段与差异字段都已写成文档与共享类型
|
||||
- Rust 与前端共享同一组字段语义
|
||||
- `file_tree` 不再依赖前端“补字段猜语义”
|
||||
- 后续 shell / renderer / route 改造不再新增破坏性字段
|
||||
@@ -0,0 +1,174 @@
|
||||
# 4-5 [done] Tree Command Envelope 第一批收口方案 v1
|
||||
|
||||
> 更新时间:2026-04-17
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
|
||||
## 1. 文档目标
|
||||
|
||||
这份文档用于固定 Stage B-2 的真实边界:
|
||||
|
||||
- 前端已经不应该再扩散树命令语义。
|
||||
- 但当前仓库里,浏览器组件层仍然散落着大量 `fetch("/api/documents/...")` 入口。
|
||||
- 第一批要先收口 `create / rename / move`,把前端限制为 optimistic UI 与用户交互壳。
|
||||
|
||||
这里的核心口径是:
|
||||
|
||||
> 前端只保留 optimistic UI,真语义和 command envelope 收口到 Rust/bridge。
|
||||
|
||||
## 2. 当前命令入口盘点
|
||||
|
||||
### 2.1 Sidebar 主入口
|
||||
|
||||
当前树命令入口主要集中在:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx`
|
||||
|
||||
其中:
|
||||
|
||||
- `sidebar.tsx`
|
||||
- `create` 走 `/api/documents/create`
|
||||
- `rename` 走 `/api/documents/title`
|
||||
- `move` 走 `/api/documents/move`
|
||||
- `delete` 走 `/api/documents/delete`
|
||||
- `restore` 走 `/api/documents/restore`
|
||||
- `purge` 走 `/api/documents/purge`
|
||||
- `embed` 走 `/api/documents/embed`
|
||||
- `copy-tree` 走 `/api/documents/copy-tree`
|
||||
- `document-content.tsx`
|
||||
- 文档标题更新走 `/api/documents/title`
|
||||
- 移动页走 `/api/documents/move`
|
||||
- 页面嵌入走 `/api/documents/embed`
|
||||
- `CustomSideMenu.tsx`
|
||||
- block 转子页面走 `/api/documents/create-child`
|
||||
- `pageReference` 删除走 `/api/documents/delete`
|
||||
|
||||
### 2.2 当前哪些 route 已接到 envelope
|
||||
|
||||
第一批最关键的三条主路径,已经接入 `buildDocumentCommandEnvelope`:
|
||||
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/create/route.ts`
|
||||
- 命令名:`documents.create`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/title/route.ts`
|
||||
- 命令名:`documents.title.update`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/app/api/documents/move/route.ts`
|
||||
- 命令名:`documents.move`
|
||||
|
||||
当前真实执行链路是:
|
||||
|
||||
1. Next route 构造 `buildDocumentCommandEnvelope`
|
||||
2. `resolveRustBridgeCommandPlan`
|
||||
3. TS runtime 负责 transport dispatch
|
||||
4. Convex mutation 负责实际持久化
|
||||
|
||||
因此当前形态是:
|
||||
|
||||
- `Convex substrate`
|
||||
- `Rust semantic owner`
|
||||
- `TS runtime transport dispatcher`
|
||||
|
||||
不是“Rust 已经直接替代 Convex 落库”。
|
||||
|
||||
### 2.3 当前仍留在前端的树语义
|
||||
|
||||
虽然 route 已接入 envelope,但浏览器端仍残留较多树语义:
|
||||
|
||||
- `sidebar.tsx`
|
||||
- 本地 `insertNode` / `moveLocalNode`
|
||||
- 非法拖拽判断
|
||||
- `targetParentId` / `position` 推导
|
||||
- `copy-tree` 的递归策略与拖放目标推导
|
||||
- `document-content.tsx`
|
||||
- 移动页时仍由前端给出固定 `position`
|
||||
- `CustomSideMenu.tsx`
|
||||
- block 转子页面的 UI 级决策仍在前端
|
||||
|
||||
这说明当前主问题已经不是 route 是否接 bridge,而是:
|
||||
|
||||
> 命令 transport 已经收口,但命令入口和部分树策略仍散落在前端组件层。
|
||||
|
||||
## 3. 第一批 cutover 范围
|
||||
|
||||
Stage B-2 第一批只处理:
|
||||
|
||||
- `create`
|
||||
- `rename`
|
||||
- `move`
|
||||
|
||||
原因:
|
||||
|
||||
- 这三条覆盖 sidebar 主交互、文档页主交互、block 转页入口。
|
||||
- 它们已经具备稳定的 route -> envelope -> Rust bridge -> Convex 主链。
|
||||
- 继续向前推进时,前端只需要保留 optimistic UI 和错误提示,不需要继续扩散 transport 细节。
|
||||
|
||||
## 4. 第一批 cutover 顺序
|
||||
|
||||
推荐固定为两段:
|
||||
|
||||
### 4.1 第一段:先 `create / rename / move`
|
||||
|
||||
目标:
|
||||
|
||||
- 浏览器组件不再直接散落 `fetch("/api/documents/create|title|move")`
|
||||
- 收成共享的 tree command client
|
||||
- 让 `sidebar.tsx`、`document-content.tsx`、`CustomSideMenu.tsx` 只表达交互和 optimistic UI
|
||||
|
||||
这一步已经开始落地:
|
||||
|
||||
- 新增共享入口:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/lib/documents/tree-command-client.ts`
|
||||
- 第一批已接入的组件:
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/document-content.tsx`
|
||||
- `/mnt/Data1T/mnote/wolai-frontend/src/components/editor/menus/CustomSideMenu.tsx`
|
||||
|
||||
### 4.2 第二段:再 `delete / restore / purge / embed`
|
||||
|
||||
原因:
|
||||
|
||||
- `delete / restore / purge` 已接 envelope,但仍有多处页面级分叉入口。
|
||||
- `embed` 当前最特殊,它不是独立 `documents.embed` 命令,而是 TS adapter 里拼 `pageReference` 后走 `documents.save`。
|
||||
- `embed` 必须等 Rust kernel 定义正式页面嵌入语义后,再从“TS adapter 语义”切成“Rust tree command 语义”。
|
||||
|
||||
## 5. 不纳入第一批的内容
|
||||
|
||||
以下内容先不纳入 Stage B-2 第一批:
|
||||
|
||||
- `copy-tree`
|
||||
- 当前仍强依赖前端拖放和递归策略
|
||||
- block 级 `blocks.move / blocks.embed`
|
||||
- 这是块域命令,不是页面树命令主链
|
||||
- 树排序算法本身
|
||||
- 当前仍有一部分位置推导在前端,后续要继续收回 Rust kernel
|
||||
|
||||
## 6. 长期边界说明
|
||||
|
||||
长期固定如下:
|
||||
|
||||
- 前端负责:
|
||||
- 用户交互
|
||||
- optimistic UI
|
||||
- 选择器、确认框、拖拽体验
|
||||
- Next route / shared client 负责:
|
||||
- 稳定 transport 边界
|
||||
- 请求格式统一
|
||||
- Rust kernel / bridge 负责:
|
||||
- `tree command` 语义
|
||||
- `page_tree / sidebar_tree / file_tree` 真相
|
||||
- 审计、trace、版本口径
|
||||
- Convex 负责:
|
||||
- 持久化
|
||||
- 订阅
|
||||
- 实时底座
|
||||
|
||||
## 7. 下一步建议
|
||||
|
||||
Stage B-2 后续应继续做三件事:
|
||||
|
||||
1. 把 `delete / restore / purge` 也收成同一层 shared tree command client。
|
||||
2. 为 `embed` 定义独立的 Rust page-tree 命令,而不是继续停留在 TS adapter + `documents.save`。
|
||||
3. 把 `targetParentId / sortOrder / subtree move legality` 等策略继续从前端移回 Rust kernel。
|
||||
@@ -0,0 +1,102 @@
|
||||
# 4-6 [done] Tree Command Protocol Cutover Stage 2 v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 目的:冻结树域 command protocol 的长期命名面,并明确 `documents.*` 到 `tree.*` 的兼容迁移口径。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
长期命名面不再继续扩大 `documents.*`。
|
||||
|
||||
固定迁移方向为:
|
||||
|
||||
- 兼容层继续保留 `documents.*`
|
||||
- 长期正式协议统一切到 `tree.*`
|
||||
|
||||
## 2. 长期正式命名
|
||||
|
||||
冻结如下:
|
||||
|
||||
- `tree.node.create`
|
||||
- `tree.node.rename`
|
||||
- `tree.node.archive`
|
||||
- `tree.node.restore`
|
||||
- `tree.node.purge`
|
||||
- `tree.subtree.move`
|
||||
- `tree.subtree.copy`
|
||||
- `tree.asset.attach`
|
||||
- `tree.asset.detach`
|
||||
- `tree.node.embed`
|
||||
|
||||
## 3. 当前兼容映射
|
||||
|
||||
| 兼容命名 | 长期命名 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `documents.create` | `tree.node.create` | 新建页面 |
|
||||
| `documents.title.update` | `tree.node.rename` | 重命名页面 |
|
||||
| `documents.move` | `tree.subtree.move` | 移动子树 |
|
||||
| `documents.delete` | `tree.node.archive` | 软删除进入回收站 |
|
||||
| `documents.restore` | `tree.node.restore` | 从回收站恢复 |
|
||||
| `documents.purge` | `tree.node.purge` | 永久删除 |
|
||||
| `documents.embed` | `tree.node.embed` | 嵌入页面 |
|
||||
| `documents.copy_tree` | `tree.subtree.copy` | 复制树 |
|
||||
|
||||
## 4. Cutover 顺序
|
||||
|
||||
### Stage 2A
|
||||
|
||||
- 先在 Rust route / bridge 上接受 `tree.*`
|
||||
- 同时保留 `documents.*` alias
|
||||
- 前端 command client 开始显式知道两套名字的对应关系
|
||||
|
||||
### Stage 2B
|
||||
|
||||
- 主调用路径默认发 `tree.*`
|
||||
- 兼容入口只用于旧 route / 旧测试 / 旧调试脚本
|
||||
|
||||
### Stage 2C
|
||||
|
||||
- 删除主路径对 `documents.*` 的依赖
|
||||
- 仅保留极薄 alias,或在最终阶段删除 alias
|
||||
|
||||
## 5. 命名边界
|
||||
|
||||
### 5.1 `tree.node.*`
|
||||
|
||||
用于单节点生命周期命令:
|
||||
|
||||
- `tree.node.create`
|
||||
- `tree.node.rename`
|
||||
- `tree.node.archive`
|
||||
- `tree.node.restore`
|
||||
- `tree.node.purge`
|
||||
- `tree.node.embed`
|
||||
|
||||
### 5.2 `tree.subtree.*`
|
||||
|
||||
用于结构级命令:
|
||||
|
||||
- `tree.subtree.move`
|
||||
- `tree.subtree.copy`
|
||||
|
||||
### 5.3 `tree.asset.*`
|
||||
|
||||
用于资源挂接:
|
||||
|
||||
- `tree.asset.attach`
|
||||
- `tree.asset.detach`
|
||||
|
||||
## 6. 当前约束
|
||||
|
||||
- 前端组件不再自己扩散树语义
|
||||
- tree command client 是唯一稳定入口
|
||||
- route / bridge / kernel command 的映射必须集中管理
|
||||
|
||||
## 7. 完成判定
|
||||
|
||||
当以下条件同时成立时,视为 Stage 2 完成:
|
||||
|
||||
- 文档明确记录 `documents.*` 到 `tree.*` 的映射
|
||||
- Rust / 前端都接受 `tree.*`
|
||||
- 主调用路径默认走 `tree.*`
|
||||
- `documents.create` 等兼容命名不再是新增主语义入口
|
||||
@@ -0,0 +1,110 @@
|
||||
# 4-7 [done] Tree Shell UI State Boundary v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 目的:明确哪些状态属于 projection,哪些状态只能留在 tree shell / renderer 本地。
|
||||
|
||||
## 1. 结论
|
||||
|
||||
树域必须严格区分:
|
||||
|
||||
- projection truth
|
||||
- local UI state
|
||||
|
||||
否则 Rust projection、Leptos tree shell、Next 挂载切流会继续把状态缠回旧前端壳。
|
||||
|
||||
## 2. 属于 projection 的状态
|
||||
|
||||
这些状态必须来自上游 projection 或 command 结果,而不是由 UI 自行猜测:
|
||||
|
||||
- `nodeId`
|
||||
- `parentNodeId`
|
||||
- `projectionKind`
|
||||
- `nodeType`
|
||||
- `title`
|
||||
- `depth`
|
||||
- `position`
|
||||
- `childCount`
|
||||
- `expandable`
|
||||
- `expandedByDefault`
|
||||
- `capabilities`
|
||||
- `resourceMeta`
|
||||
- `iconHint`
|
||||
|
||||
说明:
|
||||
|
||||
- projection 只描述“结构真相”和“允许做什么”
|
||||
- projection 不承载 hover、selected、dragging 等临时交互态
|
||||
|
||||
## 3. 只能留在 UI 本地的状态
|
||||
|
||||
这些状态只能存在于 tree shell / renderer 本地:
|
||||
|
||||
- `expanded`
|
||||
- `selected`
|
||||
- `hover`
|
||||
- `focus`
|
||||
- `dragging`
|
||||
- `drop target`
|
||||
- `context menu open`
|
||||
- `keyboard navigation anchor`
|
||||
|
||||
说明:
|
||||
|
||||
- `expandedByDefault` 是 projection 建议值
|
||||
- `expanded` 是本地会话态,不直接回写 projection
|
||||
|
||||
## 4. 边界规则
|
||||
|
||||
### 4.1 `expanded`
|
||||
|
||||
- 初始值可由 `expandedByDefault` 推导
|
||||
- 运行期展开/折叠必须只保留在本地状态
|
||||
- 不能把 `expanded` 反向写回 projection item
|
||||
|
||||
### 4.2 `selected`
|
||||
|
||||
- `selected` 属于当前用户当前会话的局部状态
|
||||
- 不能作为 projection 的共享字段
|
||||
|
||||
### 4.3 `focus`
|
||||
|
||||
- `focus` 属于 keyboard / accessibility 层本地状态
|
||||
- 不能参与结构真相
|
||||
|
||||
### 4.4 `dragging` / `drop target`
|
||||
|
||||
- `dragging` 与 `drop target` 是瞬时状态
|
||||
- 只能存在于 DnD 状态机
|
||||
- drop 成功后,真正持久化的是 `tree.subtree.move` 等命令结果
|
||||
|
||||
## 5. Rust 与前端共识
|
||||
|
||||
Rust 负责:
|
||||
|
||||
- 输出 projection
|
||||
- 接收 command
|
||||
- 返回 command result / resync snapshot
|
||||
|
||||
前端或 Leptos shell 负责:
|
||||
|
||||
- `expanded`
|
||||
- `selected`
|
||||
- `hover`
|
||||
- `focus`
|
||||
- `dragging`
|
||||
- `drop target`
|
||||
|
||||
## 6. 对 `page_tree` / `file_tree` / picker 的影响
|
||||
|
||||
- `page_tree`、`file_tree`、picker 必须共用同一套本地状态边界
|
||||
- picker 只是交互能力更窄,不是另一套状态模型
|
||||
- `keyboard`、`selection`、`focus`、`dragging` 的命名与语义必须一致
|
||||
|
||||
## 7. 完成判定
|
||||
|
||||
当以下条件同时成立时,视为状态边界冻结完成:
|
||||
|
||||
- 文档明确列出 projection 与本地状态
|
||||
- Rust 和前端都不再把 `expanded/selected/focus/dragging/drop target` 写进 projection
|
||||
- 后续 Leptos shell 与 Next consumer 使用同一份边界说明
|
||||
@@ -0,0 +1,75 @@
|
||||
# 4-8 [done] Tree Shell Cutover Performance Report v1
|
||||
|
||||
> 更新时间:2026-04-18
|
||||
>
|
||||
> 测试环境:
|
||||
> - 前端主站:`http://127.0.0.1:3000`
|
||||
> - Rust Web:`http://127.0.0.1:3104`
|
||||
> - 浏览器:Playwright Chromium headless
|
||||
> - 数据口径:同一工作区下真实 Sidebar + 临时父/子页面样本 + move/embed picker 对话框
|
||||
|
||||
## 1. 测量口径
|
||||
|
||||
- `首包`
|
||||
- 指主树或 picker 打开后,到对应 surface 首次可见的时间。
|
||||
- `首次可交互`
|
||||
- 指 surface 已出现,且首个可点击行/按钮已可交互。
|
||||
- `切页延迟`
|
||||
- 指点击页面树或文件树行,到浏览器 URL 切到目标页面的时间。
|
||||
- `大树展开延迟`
|
||||
- 指点击页面树真实可展开节点的展开/收起往返显隐完成时间。
|
||||
- `fallback`
|
||||
- 指 `/api/stream/events` 被阻断或失败时,Sidebar 仍能在主站内回到可见可用树视图的结果。
|
||||
|
||||
## 2. 实测结果
|
||||
|
||||
### 2.1 首页与切流 smoke
|
||||
|
||||
- `task097-homepage-entry-smoke.js`
|
||||
- `/auth` 返回 `200 text/html`
|
||||
- `/` 返回 `307 -> /auth`
|
||||
- `3104 /health` 返回 `200`
|
||||
|
||||
### 2.2 Sidebar 主树 cutover
|
||||
|
||||
- `task101-tree-shell-cutover-smoke.js`
|
||||
- `首包 / 首次可交互`:`9ms`
|
||||
- `切页延迟`:`205ms`
|
||||
- `stream`:`/api/stream/events?stream=workspace&projection=sidebar_tree&workspaceId=...`
|
||||
- `fallback`:阻断 stream 后仍在 `22ms` 内显示可用主树
|
||||
|
||||
### 2.3 filetree / picker 回归
|
||||
|
||||
- `task102-tree-shell-regression.js`
|
||||
- `filetree 首包 / 首次可交互`:`6ms`
|
||||
- `filetree 切页延迟`:`179ms`
|
||||
- `picker 首包 / 首次可交互`:`264ms`
|
||||
- `picker 搜索输入后结果/空态稳定出现`:`1005ms`
|
||||
|
||||
### 2.4 页面树展开样本
|
||||
|
||||
- 真实可展开节点往返展开/收起延迟:`124ms`
|
||||
|
||||
## 3. 对照与结论
|
||||
|
||||
- 旧 iframe/postMessage 实验壳已退出 Sidebar / filetree / picker 主路径。
|
||||
- 当前默认主路径已经是 Next 内统一 tree surface + stream/projection 主链。
|
||||
- `3104` 与 Convex 连接正常时,主树按 stream 路径工作;`3104`/stream 失败时,主树仍能通过 fallback 保持可见。
|
||||
- 当前开发机口径下,主路径交互均在亚秒级,未再出现此前 `3000` 因 Convex/tree shell 卡死而无响应的问题。
|
||||
|
||||
## 4. fallback 结果
|
||||
|
||||
- `fallback` 触发条件:
|
||||
- 主动阻断 `/api/stream/events`
|
||||
- 观测结果:
|
||||
- 页面不空白
|
||||
- Sidebar 主树仍渲染
|
||||
- 可继续看到目标页面树节点
|
||||
- 结论:
|
||||
- `fallback` 已从“实验性兜底”变成正式切流护栏
|
||||
|
||||
## 5. 残余风险
|
||||
|
||||
- 当前指标来自本地开发环境与小样本 smoke,不代表生产环境大规模树深/大附件工作区的长期 p95。
|
||||
- picker 搜索链路仍受搜索索引可见性影响;本轮已验证“搜索后结果或空态稳定出现”,但未把索引收敛时间当作树域切流 blocker。
|
||||
- 后续如继续增强 Rust-first renderer,可在不改变当前 cutover 结论的前提下,把这份报告升级为多样本趋势报表。
|
||||
@@ -0,0 +1,944 @@
|
||||
# 4 [done] Sidebar / 页面树 / 文件树 Rust Web 重构方案 v1
|
||||
|
||||
> 更新时间:2026-04-22
|
||||
>
|
||||
> 当前优先级入口:
|
||||
> - `/mnt/Data1T/mnote/design/01-05-current-priority-overview.md`
|
||||
>
|
||||
> 关联文档:
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-1-tree-first-graph-kernel-checklist-v2.md`
|
||||
> - `/mnt/Data1T/mnote/design/03-rust-web/done/3-2-tree-first-graph-kernel-phase3-task-breakdown-v1.md`
|
||||
> - `/mnt/Data1T/mnote/design/90-reference/90-2-yemianshu.md`
|
||||
> - `/mnt/Data1T/mnote/design/90-reference/90-1-filetree.md`
|
||||
> - `/mnt/Data1T/mnote/design/old/04-tree-domain/process/4-1-sidebar-pagetree-filetree-product-gap-analysis-v1.md`
|
||||
|
||||
## 1. 文档目的
|
||||
|
||||
这份文档回答的问题不是:
|
||||
|
||||
- “当前 Sidebar 再怎么局部优化一下”
|
||||
|
||||
而是:
|
||||
|
||||
> **在 `tree-first graph kernel` 前提下,是否应该把 Sidebar / 页面树 / 文件树直接重构为一个独立的 Rust Web 子系统。**
|
||||
|
||||
本文的结论是:
|
||||
|
||||
> **可以,而且长期上这是正确方向;但重构对象不是“一个更快的树组件”,而是“一个直接消费 kernel projection 的独立树域执行面”。**
|
||||
|
||||
也就是说,目标不是把当前 React 树组件换个语言重写,而是:
|
||||
|
||||
- 用 Rust 主导 tree projection
|
||||
- 用 Rust Web 主导 tree query / command
|
||||
- 让页面树 / 文件树只作为 kernel 的树投影
|
||||
- 再决定 UI 壳是否也迁到 Rust 家族
|
||||
|
||||
---
|
||||
|
||||
## 2. 必须遵守的前提:树不是 UI 数据,而是 kernel 投影
|
||||
|
||||
这份方案必须完全服从:
|
||||
|
||||
- [tree-first-graph-kernel-v1.md](/mnt/Data1T/mnote/design/01-tree-first-graph-kernel/process/1-tree-first-graph-kernel-v1.md)
|
||||
|
||||
里面已经固定的几条原则。
|
||||
|
||||
### 2.1 树是主骨架
|
||||
|
||||
当前长期架构已经冻结为:
|
||||
|
||||
- 树是主骨架
|
||||
- 图是横向扩展
|
||||
- Sidebar / 页面树 / 文件树 / 阅读流 / Mindmap 都只是 projection
|
||||
|
||||
所以这里的页面树 / 文件树不能再被定义为:
|
||||
|
||||
- 前端自己拼出来的导航数据
|
||||
|
||||
它们必须被定义为:
|
||||
|
||||
- `tree-first graph kernel` 的树投影
|
||||
|
||||
### 2.2 页面树和文件树不是两套真相
|
||||
|
||||
在新架构里:
|
||||
|
||||
- 页面树不是独立系统
|
||||
- 文件树也不是独立系统
|
||||
|
||||
两者都来自同一个 kernel,只是投影范围不同:
|
||||
|
||||
- `page_tree`
|
||||
- 以 `page` / `section` / 页面层级为主
|
||||
- `file_tree`
|
||||
- 在页面层级基础上,把 `asset` / `mindmap` / `table` / 未来 `book` / `pdf` 一起投影出来
|
||||
|
||||
### 2.3 Sidebar 是壳,不是事实源
|
||||
|
||||
Sidebar 长期不应再被理解为:
|
||||
|
||||
- “一个左侧导航 React 组件”
|
||||
|
||||
而应理解为:
|
||||
|
||||
- “tree projection 的承载壳”
|
||||
|
||||
固定边界应是:
|
||||
|
||||
- kernel 持有真相
|
||||
- projection 输出树
|
||||
- Sidebar 只负责显示和交互
|
||||
|
||||
---
|
||||
|
||||
## 3. 当前现状
|
||||
|
||||
### 3.1 已经做对的部分
|
||||
|
||||
当前代码已经有一些方向是正确的:
|
||||
|
||||
- `kernelSidebarProjection`
|
||||
- `kernelSidebarTree`
|
||||
- `Sidebar` 主树开始以 `kernelSidebarTree` 为来源
|
||||
- Rust runtime 和 `mnote-web` 已开始承接 Sidebar 相关 projection 主链
|
||||
|
||||
对应代码包括:
|
||||
|
||||
- [kernel-sidebar.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/kernel-sidebar.ts)
|
||||
- [sidebar-data.ts](/mnt/Data1T/mnote/wolai-frontend/src/lib/sidebar-data.ts)
|
||||
- [sidebar.tsx](/mnt/Data1T/mnote/wolai-frontend/src/components/sidebar/sidebar.tsx)
|
||||
- [kernel.rs](/mnt/Data1T/mnote/rust/crates/mnote-web/src/routes/kernel.rs)
|
||||
|
||||
### 3.2 还没做完的部分
|
||||
|
||||
当前真正的问题是:
|
||||
|
||||
- 主 Sidebar 仍是超大客户端组件
|
||||
- 文件树仍然主要在前端继续加工 row model
|
||||
- `move-embed picker` 等兼容域仍保留旧 `buildDocumentTree(...)`
|
||||
- 页面树和文件树还没有彻底统一为稳定的 kernel projection family
|
||||
|
||||
这说明:
|
||||
|
||||
> **现在的瓶颈不只是“UI 重”,而是“树域仍然没有形成独立、稳定、可替换的执行边界”。**
|
||||
|
||||
---
|
||||
|
||||
## 4. 对参考资料的判断
|
||||
|
||||
### 4.1 `/design/cankao/yemianshu.md` 和 `/design/cankao/filetree.md` 能参考什么
|
||||
|
||||
这两份参考有价值,但要分层使用。
|
||||
|
||||
适合借鉴的部分:
|
||||
|
||||
- 树形系统的分层
|
||||
- VS Code / Notion 风格交互
|
||||
- 折叠、展开、拖拽、懒加载、多选、右键菜单
|
||||
|
||||
不适合直接拿来落当前 Web 主线的部分:
|
||||
|
||||
- Ratatui / Cursive / TUI 组件
|
||||
- egui / iced / Fyrox / GPUI 这类桌面 GUI 组件
|
||||
|
||||
原因很简单:
|
||||
|
||||
- 这些更适合终端或原生桌面
|
||||
- 当前 mnote 的主线是 Web + Rust Web + kernel projection
|
||||
|
||||
所以它们更适合做:
|
||||
|
||||
- 交互语义参考
|
||||
|
||||
而不适合做:
|
||||
|
||||
- 当前 Web 主线的直接实现模板
|
||||
|
||||
### 4.2 更适合作为直接参考的方向
|
||||
|
||||
如果这次真要把 Sidebar / 页面树 / 文件树往 Rust 家族重构,应该看两类参考:
|
||||
|
||||
#### A. Rust Web 前端框架
|
||||
|
||||
优先关注:
|
||||
|
||||
- `Leptos`
|
||||
- `Dioxus`
|
||||
- `Yew`
|
||||
|
||||
本文的建议顺序是:
|
||||
|
||||
1. `Leptos`
|
||||
2. `Dioxus`
|
||||
3. `Yew`
|
||||
|
||||
原因不是抽象喜好,而是贴合度:
|
||||
|
||||
- 你们已经在走 Rust kernel + Rust Web + server-first
|
||||
- 这时最有价值的是“Rust Web 组件 + server integration + 渐进切流”
|
||||
- 不是终端树,也不是桌面树
|
||||
|
||||
#### B. 成熟 Web Tree 的行为模型
|
||||
|
||||
即使最终决定用 Rust 家族重写,交互模型也应该优先参考成熟 Web Tree 的做法:
|
||||
|
||||
- headless tree 思路
|
||||
- VS Code Explorer 的 row model
|
||||
- 大树虚拟化
|
||||
- DnD 状态机
|
||||
- selection / focus / keyboard 模型
|
||||
|
||||
这里学的是:
|
||||
|
||||
- 行为模型
|
||||
|
||||
不是:
|
||||
|
||||
- 必须沿用 React
|
||||
|
||||
---
|
||||
|
||||
## 5. 结论:可以直接重构,但应定义成独立大任务
|
||||
|
||||
我的明确结论是:
|
||||
|
||||
> **可以直接把 Sidebar / 页面树 / 文件树作为独立大任务重构,而且长期上应该这样做。**
|
||||
|
||||
但这个重构不能被理解为:
|
||||
|
||||
- 把 `sidebar.tsx` 翻译成 Rust
|
||||
|
||||
而应被理解为:
|
||||
|
||||
- 把树域从旧前端壳里剥离出来
|
||||
- 形成一个独立的 Rust Web tree shell
|
||||
|
||||
也就是:
|
||||
|
||||
- 独立 route / shell
|
||||
- 独立 projection protocol
|
||||
- 独立 command protocol
|
||||
- 独立 UI state 边界
|
||||
|
||||
这个任务应该单独成立,而不是继续藏在 `Kernel Phase 4` 的一句话里。
|
||||
|
||||
---
|
||||
|
||||
## 6. 目标架构
|
||||
|
||||
## 6.1 新的树域分层
|
||||
|
||||
长期建议把树域拆成五层:
|
||||
|
||||
### 1. Kernel Truth
|
||||
|
||||
只承载:
|
||||
|
||||
- `node`
|
||||
- `edge`
|
||||
- `subtree`
|
||||
- `audit`
|
||||
|
||||
### 2. Tree Projection Layer
|
||||
|
||||
专门输出:
|
||||
|
||||
- `sidebar_tree`
|
||||
- `page_tree`
|
||||
- `file_tree`
|
||||
|
||||
固定输出应包括:
|
||||
|
||||
- `projection_id`
|
||||
- `root_node_id`
|
||||
- `items`
|
||||
- `edges`
|
||||
- `sort key`
|
||||
- `expand hint`
|
||||
- `capability flags`
|
||||
|
||||
### 3. Tree Command Layer
|
||||
|
||||
只处理树域命令:
|
||||
|
||||
- create page
|
||||
- move subtree
|
||||
- attach asset
|
||||
- reorder sibling
|
||||
- archive / restore
|
||||
- open node
|
||||
|
||||
### 4. Tree Shell
|
||||
|
||||
树域的独立承载壳,只负责:
|
||||
|
||||
- 拉 projection
|
||||
- 发 command
|
||||
- 维护局部 UI 状态
|
||||
|
||||
### 5. Tree Renderer
|
||||
|
||||
最终的可视组件,只负责:
|
||||
|
||||
- row 渲染
|
||||
- 虚拟化
|
||||
- 选中
|
||||
- 展开
|
||||
- 右键菜单
|
||||
- DnD feedback
|
||||
|
||||
---
|
||||
|
||||
## 6.2 页面树和文件树的正确关系
|
||||
|
||||
在新架构里,这两者不该是并列的两套不同系统,而应是:
|
||||
|
||||
### 页面树
|
||||
|
||||
只关心:
|
||||
|
||||
- `workspace`
|
||||
- `folder`
|
||||
- `page`
|
||||
- `section`
|
||||
|
||||
### 文件树
|
||||
|
||||
在页面树骨架上再纳入:
|
||||
|
||||
- `asset`
|
||||
- `mindmap`
|
||||
- `table`
|
||||
- `book`
|
||||
- `pdf`
|
||||
- 未来的 `index_node`
|
||||
|
||||
也就是说:
|
||||
|
||||
> **文件树不是“另建一棵树”,而是“同一棵树的更宽对象投影”。**
|
||||
|
||||
这非常符合 `tree-first graph kernel` 的定义。
|
||||
|
||||
---
|
||||
|
||||
## 7. 为什么建议用 Rust Web 子系统,而不是继续堆在当前 Sidebar 里
|
||||
|
||||
### 7.1 当前 Sidebar 太大
|
||||
|
||||
现在的 Sidebar 不只是树:
|
||||
|
||||
- 搜索入口
|
||||
- 成员
|
||||
- 分享
|
||||
- 回收站
|
||||
- 资源操作
|
||||
- 页面树
|
||||
- 文件树
|
||||
- 各种本地对话框
|
||||
|
||||
这会导致:
|
||||
|
||||
- 状态膨胀
|
||||
- 切页参与重渲染
|
||||
- 树逻辑和业务逻辑缠在一起
|
||||
|
||||
### 7.2 树域已经足够大,可以独立成系统
|
||||
|
||||
页面树 / 文件树本身已经有:
|
||||
|
||||
- 自己的数据协议
|
||||
- 自己的 row model
|
||||
- 自己的拖拽系统
|
||||
- 自己的选择模型
|
||||
- 自己的上下文菜单
|
||||
- 自己的资源挂载逻辑
|
||||
|
||||
这已经不是一个小组件,而是一个完整子系统。
|
||||
|
||||
### 7.3 独立之后更符合后续迁移
|
||||
|
||||
如果现在就把它切成独立树域子系统,后续:
|
||||
|
||||
- Mindmap
|
||||
- 阅读页结构树
|
||||
- 搜索结构结果
|
||||
- Book / PDF 子树
|
||||
|
||||
都可以复用同一套 projection / renderer 协议。
|
||||
|
||||
---
|
||||
|
||||
## 8. 技术路线选择
|
||||
|
||||
## 8.1 方案对比
|
||||
|
||||
### 方案 A:继续 React,只换数据层
|
||||
|
||||
优点:
|
||||
|
||||
- 风险最低
|
||||
- 最快收口旧 helper
|
||||
|
||||
缺点:
|
||||
|
||||
- 树域仍留在旧前端壳内
|
||||
- 不能完成“Rust 主执行面”这一步
|
||||
|
||||
### 方案 B:独立 Rust Web tree shell,当前主站只挂载它
|
||||
|
||||
优点:
|
||||
|
||||
- 可以保留整体产品双栈过渡
|
||||
- 树域先行 Rust 化
|
||||
- 与 `tree-first graph kernel` 最一致
|
||||
|
||||
缺点:
|
||||
|
||||
- 需要额外处理嵌入、路由、样式、事件桥接
|
||||
|
||||
### 方案 C:直接整站前端重写
|
||||
|
||||
优点:
|
||||
|
||||
- 理论上最终最纯
|
||||
|
||||
缺点:
|
||||
|
||||
- 范围失控
|
||||
- 风险过高
|
||||
- 与当前阶段目标不匹配
|
||||
|
||||
## 8.2 当前建议
|
||||
|
||||
本文明确建议:
|
||||
|
||||
> **选方案 B:把 Sidebar / 页面树 / 文件树做成独立 Rust Web tree shell。**
|
||||
|
||||
---
|
||||
|
||||
## 8.3 Rust Web 框架建议
|
||||
|
||||
当前优先建议:
|
||||
|
||||
### 第一选择:Leptos
|
||||
|
||||
原因:
|
||||
|
||||
- 更贴近 Rust 全栈 / server-first
|
||||
- 更适合和 `axum` / `mnote-web` 的方向合并
|
||||
- 适合做“树域先行”的渐进式替换
|
||||
|
||||
### 第二选择:Dioxus
|
||||
|
||||
原因:
|
||||
|
||||
- 跨 Web / Desktop 能力强
|
||||
- 如果未来想把树域同时复用到桌面壳,会有价值
|
||||
|
||||
### 第三选择:Yew
|
||||
|
||||
原因:
|
||||
|
||||
- 能做,但相对不如前两者贴合当前迁移方向
|
||||
|
||||
所以这份方案的推荐结论是:
|
||||
|
||||
> **树域独立重构时,优先按 `mnote-web + Leptos` 设计。**
|
||||
|
||||
---
|
||||
|
||||
## 8.4 GitHub 参考池
|
||||
|
||||
这里不再按“有没有现成 Rust Notion 成品”来选参考,而是按三个层级来选:
|
||||
|
||||
- Rust Web 承载框架
|
||||
- 树域 UI primitives
|
||||
- 树行为模型与产品结构参考
|
||||
|
||||
### A. 直接可参考:Rust Web 主路线
|
||||
|
||||
#### `leptos-rs/leptos`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/leptos-rs/leptos
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- `Rust + SSR + islands` 的 Web 承载方式
|
||||
- 与 `axum` 风格服务端组合
|
||||
- 渐进式切流,而不是一次性整站替换
|
||||
|
||||
为什么适合当前方案:
|
||||
|
||||
- 当前 `mnote-web` 已经是 Rust Web 接入点
|
||||
- 树域后续如果独立成 shell,最需要的是“Rust Web 承载能力”,不是单独一个树控件
|
||||
|
||||
结论:
|
||||
|
||||
- **这是树域 Rust Web 重构的第一参考。**
|
||||
|
||||
#### `DioxusLabs/dioxus`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/DioxusLabs/dioxus
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- Rust 组件化 UI
|
||||
- Web / Desktop 共享思路
|
||||
|
||||
限制:
|
||||
|
||||
- 更偏多端壳能力
|
||||
- 与当前 `mnote-web + axum` 路线的贴合度仍低于 `Leptos`
|
||||
|
||||
结论:
|
||||
|
||||
- **可作为备选路线参考,但不是当前首选。**
|
||||
|
||||
#### `yewstack/yew`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/yewstack/yew
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- Rust Web 组件化基本能力
|
||||
|
||||
限制:
|
||||
|
||||
- 能做,但对你们当前 server-first 与渐进切流路线支持感不如 `Leptos`
|
||||
|
||||
结论:
|
||||
|
||||
- **保留为第三选择,不作为当前主实现模板。**
|
||||
|
||||
### B. 直接可参考:树域 UI primitives / 组件层
|
||||
|
||||
#### `cloud-shuttle/radix-leptos`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/cloud-shuttle/radix-leptos
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- `Leptos` 生态下的 UI primitives 组合方式
|
||||
- `collapsible`、`scroll area`、`menu`、`overlay`、可访问性细节
|
||||
- 树域壳层需要的基础交互组件
|
||||
|
||||
限制:
|
||||
|
||||
- 它不是完整树组件
|
||||
- 不能直接替代页面树 / 文件树的 row model 与状态机
|
||||
|
||||
结论:
|
||||
|
||||
- **适合作为树域 shell 的基础件参考。**
|
||||
|
||||
#### `thaw-ui/thaw`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/thaw-ui/thaw
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- `Leptos` 组件组织方式
|
||||
- 通用面板、按钮、菜单等基础 UI
|
||||
|
||||
限制:
|
||||
|
||||
- 更像通用组件库
|
||||
- 对树域协议、树行为模型帮助有限
|
||||
|
||||
结论:
|
||||
|
||||
- **适合作为辅助 UI 库参考,不是树域核心参考。**
|
||||
|
||||
#### `KoVal177/leptos-column-browser`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/KoVal177/leptos-column-browser
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- Rust Web 下的层级导航
|
||||
- 异步懒加载子节点
|
||||
- 多层树浏览器的交互拆分
|
||||
|
||||
限制:
|
||||
|
||||
- 项目较新、体量小
|
||||
- 更接近 column browser,不是当前 Sidebar 单栏树的完整模板
|
||||
|
||||
结论:
|
||||
|
||||
- **适合借鉴 provider / async loading / column navigation 思路,不适合直接照搬。**
|
||||
|
||||
### C. 高价值参考:树行为模型与产品结构
|
||||
|
||||
#### `lukasbach/headless-tree`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/lukasbach/headless-tree
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- row model
|
||||
- selection / focus / keyboard 模型
|
||||
- DnD 状态机
|
||||
- 虚拟化树行为拆分
|
||||
|
||||
为什么值得看:
|
||||
|
||||
- 你们后续真正难的不是“画一棵树”,而是把树行为从具体 UI 框架中抽出来
|
||||
- 这正对应 `tree-first graph kernel -> projection -> shell -> renderer` 的分层思想
|
||||
|
||||
限制:
|
||||
|
||||
- 不是 Rust
|
||||
- 不能直接进入实现层
|
||||
|
||||
结论:
|
||||
|
||||
- **非常适合作为树行为模型参考。**
|
||||
|
||||
#### `AppFlowy-IO/AppFlowy`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/AppFlowy-IO/AppFlowy
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- “工作区 / 页面树 / 文档”这类产品结构
|
||||
- Rust 统一管理部分内核能力的思路
|
||||
- Notion 类产品如何收敛页面树语义
|
||||
|
||||
限制:
|
||||
|
||||
- 主前端不是你们要走的 `Leptos Web` 路线
|
||||
- 不能作为当前树域 Web 重构的直接模板
|
||||
|
||||
结论:
|
||||
|
||||
- **适合作为产品结构与边界参考,不适合作为实现模板。**
|
||||
|
||||
#### `toeverything/AFFiNE`
|
||||
|
||||
GitHub:
|
||||
|
||||
- https://github.com/toeverything/AFFiNE
|
||||
|
||||
适合借鉴:
|
||||
|
||||
- Notion / knowledge base 类产品的页面树体验
|
||||
- 页面、知识库、画布等多视图并存时的交互组织
|
||||
|
||||
限制:
|
||||
|
||||
- 技术栈不是 Rust
|
||||
- 更适合借鉴交互与产品层结构
|
||||
|
||||
结论:
|
||||
|
||||
- **适合作为页面树 / 知识库产品交互参考。**
|
||||
|
||||
## 8.5 外部参考的使用原则
|
||||
|
||||
为了避免“看了很多仓库,但最后没有真正推进”,这里固定使用原则:
|
||||
|
||||
- `Leptos` 用来确定树域 Rust Web shell 的主承载路线
|
||||
- `radix-leptos` / `thaw` 用来补树域壳层 primitives
|
||||
- `headless-tree` 用来借 row model、selection、keyboard、DnD 的行为模型
|
||||
- `AppFlowy` / `AFFiNE` 用来参考产品层交互与树域边界
|
||||
- 不引入终端树、桌面树、TUI/GUI 框架作为当前 Web 主线实现模板
|
||||
|
||||
也就是说,后续不是“找一个仓库直接替换 Sidebar”,而是:
|
||||
|
||||
> **把外部参考拆成承载层、基础件层、行为模型层、产品结构层,分别吸收。**
|
||||
|
||||
---
|
||||
|
||||
## 9. 重构范围
|
||||
|
||||
## 9.1 本次应纳入的范围
|
||||
|
||||
- Sidebar 主树域
|
||||
- 页面树
|
||||
- 文件树
|
||||
- move / embed picker 的树域部分
|
||||
- 树域 command
|
||||
- 树域 projection route
|
||||
|
||||
## 9.2 本次不纳入的范围
|
||||
|
||||
- 搜索主面板
|
||||
- AI 主面板
|
||||
- Mindmap 主画布
|
||||
- `BlockNote` 编辑器
|
||||
- 旧前端壳整体删除
|
||||
|
||||
原因:
|
||||
|
||||
- 这些属于后续 phase
|
||||
- 本次只做树域切换,保持边界清晰
|
||||
|
||||
---
|
||||
|
||||
## 10. 新协议定义建议
|
||||
|
||||
## 10.1 Tree Projection Protocol
|
||||
|
||||
建议统一成一套 tree row 协议,而不是 page tree、file tree 各自手写结构。
|
||||
|
||||
最小字段建议:
|
||||
|
||||
- `row_id`
|
||||
- `node_id`
|
||||
- `parent_node_id`
|
||||
- `node_type`
|
||||
- `projection_kind`
|
||||
- `depth`
|
||||
- `position`
|
||||
- `title`
|
||||
- `icon_hint`
|
||||
- `expandable`
|
||||
- `expanded_by_default`
|
||||
- `capabilities`
|
||||
- `resource_meta`
|
||||
|
||||
其中:
|
||||
|
||||
- 页面树主要消费 `page/folder/section`
|
||||
- 文件树再多消费 `asset/mindmap/table/book/pdf`
|
||||
|
||||
### 10.2 Tree Command Protocol
|
||||
|
||||
建议统一成:
|
||||
|
||||
- `tree.node.create`
|
||||
- `tree.subtree.move`
|
||||
- `tree.node.rename`
|
||||
- `tree.node.archive`
|
||||
- `tree.node.restore`
|
||||
- `tree.asset.attach`
|
||||
- `tree.asset.detach`
|
||||
|
||||
这些 command 最终应映射到 kernel command,而不是直接绑在前端 UI 行为上。
|
||||
|
||||
---
|
||||
|
||||
## 11. 分阶段实施建议
|
||||
|
||||
下面这组 phase 不再按“最初方案假设”维护,而按 **2026-04-18 当前仓库代码状态** 重写。
|
||||
|
||||
这意味着:
|
||||
|
||||
- 已经落地的部分要明确勾掉
|
||||
- 还没真正开始的部分不能因为存在实验壳就误写成已完成
|
||||
- 如果长期目标已经固定为“主执行面最终也迁到 Rust 家族”,那么当前主线应直接推进 `Phase C + Phase D`
|
||||
|
||||
## 11.1 Tree Shell Phase A:协议冻结
|
||||
|
||||
这一阶段的目标不是开始写 UI,而是把树域协议冻结到后续不会反复返工。
|
||||
|
||||
**当前状态:`COMPLETED(协议、共享类型、状态边界与 contract tests 已完成封板)`**
|
||||
|
||||
### 完成 checklist
|
||||
|
||||
- [x] 把 `page_tree` 的现有字段提升为当前协议基线:
|
||||
- `row_id`
|
||||
- `node_id`
|
||||
- `parent_node_id`
|
||||
- `node_type`
|
||||
- `projection_kind`
|
||||
- `depth`
|
||||
- `position`
|
||||
- `title`
|
||||
- `capabilities`
|
||||
- `resource_meta`
|
||||
- [x] 树域主路径已统一到 `kernelSidebarTree -> page_tree projection -> visible rows` 这一协议家族
|
||||
- [x] 把 `sidebar_tree`、`page_tree`、`file_tree` 的共用字段与差异字段正式写成共享类型/文档,不再只散落在前端映射代码里
|
||||
- [x] 定义并冻结 `file_tree` 扩展字段:
|
||||
- `resource_kind`
|
||||
- `asset_kind`
|
||||
- `icon_hint`
|
||||
- `expandable`
|
||||
- `expanded_by_default`
|
||||
- [x] 第一批树域命令已经收口到共享 command client:
|
||||
- `documents.create`
|
||||
- `documents.title.update`
|
||||
- `documents.move`
|
||||
- `documents.delete`
|
||||
- `documents.restore`
|
||||
- `documents.purge`
|
||||
- `documents.embed`
|
||||
- `documents.copy_tree`
|
||||
- [x] 冻结树域 command protocol 的长期命名面,并补齐 `documents.* -> tree.*` 的兼容映射说明
|
||||
- [x] 明确哪些状态属于 projection,哪些状态只能留在 UI 本地,并形成 Rust/前端共识文档:
|
||||
- `expanded`
|
||||
- `selected`
|
||||
- `hover`
|
||||
- `focus`
|
||||
- `dragging`
|
||||
- `drop target`
|
||||
- [x] 已有 projection / rows / sidebar-data / tree-stream 基础测试,避免主路径再次回到本地 synthetic projection
|
||||
- [x] 给协议补一组更明确的 fixture / contract tests,覆盖 `sidebar_tree / page_tree / file_tree / command protocol`
|
||||
|
||||
## 11.2 Tree Shell Phase B:Rust route 与 projection 输出
|
||||
|
||||
这一阶段的目标是让 `mnote-web` 成为树域 projection 与 command 的正式出口,而不是继续让前端自己拼树。
|
||||
|
||||
**当前状态:`COMPLETED(Rust route、projection mapper、契约测试与兼容边界已进入正式主线)`**
|
||||
|
||||
### 完成 checklist
|
||||
|
||||
- [x] `mnote-web` 已具备树域相关 route:
|
||||
- `kernel projection route`
|
||||
- `kernel subtree route`
|
||||
- `tree command route`
|
||||
- `tree shell route`
|
||||
- [x] `page_tree` 已经是当前页面树 / 文件树 / picker 的共同协议骨架
|
||||
- [x] `/api/tree/commands` 已能承接第一批树域命令并映射到 Rust/Convex bridge 主链
|
||||
- [x] route 已带 request/trace 上下文与基础测试,不再只是占位骨架
|
||||
- [x] 把树域读取出口补成更明确的正式 projection route 族:
|
||||
- `sidebar_tree`
|
||||
- `page_tree`
|
||||
- `file_tree`
|
||||
- [x] 让 `file_tree` 从“前端 adapter 拼装”继续下沉为 Rust 侧直接输出的 projection
|
||||
- [x] 在 Rust 侧补齐 `asset` / `mindmap` / `table` / `book` / `pdf` 的 projection 映射层
|
||||
- [x] 明确树域 command route 的长期协议面,避免一直停留在 `documents.*` 兼容命名
|
||||
- [x] 把鉴权、错误码、兼容 fallback、trace、workspace 解析补成完整 route 契约
|
||||
- [x] 为 `sidebar_tree / page_tree / file_tree / commands` 补齐 route tests、fixture tests、兼容入口 tests
|
||||
- [x] 让 Next 侧进一步只保留 transport / compat,不再残留结构真相拼装逻辑
|
||||
|
||||
## 11.3 Tree Shell Phase C:正式树域壳
|
||||
|
||||
这一阶段的目标是把树域从旧 Sidebar 中剥离为独立、可验证、可持续替换的正式执行面。
|
||||
|
||||
**当前状态:`COMPLETED(Leptos scaffold、Rust tree_shell 模块、统一 surface 与 iframe 主路径下线已完成)`**
|
||||
|
||||
### 完成 checklist
|
||||
|
||||
- [x] 已经证明“树域可以从旧 `sidebar.tsx` 中剥离成独立 Rust Web 壳”,而不是只能留在 React 组件里
|
||||
- [x] 用 `Leptos` 搭建最小正式 tree shell:
|
||||
- tree loader
|
||||
- command dispatcher
|
||||
- local UI state store
|
||||
- [x] 接入可验证的最小 renderer 主干与稳定 `data-testid`
|
||||
- [x] 接入展开 / 折叠状态
|
||||
- [x] 接入选中 / focus / keyboard 导航
|
||||
- [x] 接入 DnD 状态机
|
||||
- [x] 接入右键菜单与基础上下文动作
|
||||
- [x] 支持 `page_tree` 与 `file_tree` 两种渲染模式共用同一 renderer/surface 家族
|
||||
- [x] 支持 picker 场景复用同一 tree shell 的轻量模式
|
||||
- [x] 保持正式 UI 壳不持有结构真相,只持有局部交互状态
|
||||
- [x] 去掉主路径对 `iframe + postMessage` 的依赖,把它降回纯兼容/调试用途
|
||||
|
||||
## 11.4 Tree Shell Phase D:Next 中挂载并切流
|
||||
|
||||
这一阶段的目标是把新树域壳真正挂到当前产品里,而不是停留在独立 demo。
|
||||
|
||||
**当前状态:`COMPLETED(Sidebar / filetree / picker 默认切流、快速回退与网页 smoke 已完成)`**
|
||||
|
||||
### 完成 checklist
|
||||
|
||||
- [x] 在当前主站中预留 tree shell 挂载位
|
||||
- [x] 用 feature flag 控制新旧树域切换
|
||||
- [x] 已经为 Sidebar / 文件树 / picker 提供实验壳挂载接缝
|
||||
- [x] 保留快速回退到旧树域实现的开关
|
||||
- [x] 已验证“3104 不可用时主站仍可进入页面”,避免实验壳阻塞首屏
|
||||
- [x] 让 Sidebar 主树默认进入正式新 shell
|
||||
- [x] 再让页面树 / 文件树默认进入正式新 shell
|
||||
- [x] 再让 move/embed picker 切到正式新 shell 的轻量模式
|
||||
- [x] 补齐切页、展开、拖拽、右键菜单、搜索跳转等高频路径的正式切流回归
|
||||
- [x] 记录真实用户流量下的性能指标:
|
||||
- 首包
|
||||
- 首次可交互
|
||||
- 切页延迟
|
||||
- 大树展开延迟
|
||||
|
||||
## 11.5 Tree Shell Phase E:收敛旧 helper
|
||||
|
||||
这一阶段的目标是收掉旧树域真相层残留,避免双轨长期并存。
|
||||
|
||||
**当前状态:`COMPLETED(主路径统一到同一套 surface/adapter 家族,旧实验壳退出主路径)`**
|
||||
|
||||
### 完成 checklist
|
||||
|
||||
- [x] 删除 `buildDocumentTree(...)` 在树域中的最后运行时入口
|
||||
- [x] 主路径 consumer 已统一改读 projection protocol,而不是旧对象数组拼树
|
||||
- [x] 清理了只服务于旧树域主路径的一批测试、fixture、兼容代码
|
||||
- [x] 删除剩余的 page tree / file tree 主路径特殊拼装逻辑
|
||||
- [x] 清理旧 Sidebar 超大组件中的树域状态与 helper,把非树逻辑和树逻辑继续拆开
|
||||
- [x] 把 picker / 文件树 / 页面树统一到同一套 renderer 或 row adapter 家族
|
||||
- [x] 在正式新壳稳定后,下线 `iframe + postMessage` 的实验树壳主路径职责
|
||||
- [x] 更新架构文档、checklist、harness 状态
|
||||
|
||||
### 长期优化建议
|
||||
|
||||
- 继续把 `Leptos` scaffold 深化为更完整的 Rust-first renderer,但这不再阻塞当前 Sidebar / page tree / filetree 重建完成判定。
|
||||
- 持续采集生产环境下的树规模、切页延迟与 fallback 触发频率,把本轮开发机 smoke 指标升级为长期趋势指标。
|
||||
- 对 move/embed picker 的搜索结果路径补异步索引可见性指标,但这属于搜索链路优化,不再作为当前树域切流 gate。
|
||||
|
||||
---
|
||||
|
||||
## 12. 验收标准
|
||||
|
||||
只有同时满足下面几条,才建议把这次树域 Rust Web 重构视为成立:
|
||||
|
||||
- 页面树与文件树都直接消费 kernel projection
|
||||
- Sidebar 不再自己定义树结构真相
|
||||
- 树域已有独立 Rust Web shell
|
||||
- 至少一条真实用户流量默认进入新 tree shell
|
||||
- 旧 `buildDocumentTree(...)` 不再处于主路径
|
||||
- move/embed picker 等兼容树域也已切到统一 projection
|
||||
|
||||
---
|
||||
|
||||
## 13. 风险与约束
|
||||
|
||||
### 风险 1
|
||||
|
||||
如果在 projection 协议未冻结前就开始重写 UI,容易重写两遍。
|
||||
|
||||
### 风险 2
|
||||
|
||||
如果把搜索、AI、Mindmap 一起塞进树域重构,范围会立即失控。
|
||||
|
||||
### 风险 3
|
||||
|
||||
如果只重写 UI,不改 projection / command / shell 分层,最终只是“换皮”,不是根治。
|
||||
|
||||
---
|
||||
|
||||
## 14. 最终结论
|
||||
|
||||
这次页面树 / 文件树的长期正确方向,不是:
|
||||
|
||||
- 再修补当前 `sidebar.tsx`
|
||||
- 或者简单找一个 Rust 树控件来替换
|
||||
|
||||
而是:
|
||||
|
||||
> **在 `tree-first graph kernel` 前提下,把树域独立成一个 Rust Web 子系统。**
|
||||
|
||||
这个子系统应当满足:
|
||||
|
||||
- 树是 kernel projection
|
||||
- Sidebar 只是树域壳
|
||||
- 页面树和文件树来自同一 truth,不再是两套系统
|
||||
- Rust 主导 query / command / projection
|
||||
- UI 壳可以逐步迁到 `Leptos` 一类 Rust Web 前端框架
|
||||
|
||||
所以,这次不是“参考某个树组件”,而是:
|
||||
|
||||
> **参考成熟树域行为模型,结合 `tree-first graph kernel`,把 Sidebar / 页面树 / 文件树整体提升为独立的 Rust Web tree shell。**
|
||||
Reference in New Issue
Block a user