Persist PageTree expand state via control-plane view-state and align chevron/DOM with restored expansion; keep Sidex-style shallow page-tree scan and drop the unused recursive scanner that only added cargo noise. Add password vault workbench routes/runtime/skill/CLI, split page_ai_pi into a module package, and retire Hermes/ACP/OpenHub recycle + root harness evidence from the index while gitignoring recycle and local diag dumps. Archive superseded design/bugs docs under old/, point architecture at ARCHITECTURE.md, and refresh smokes for Pi S1–S7, vault, and editor regressions so the working tree can stay clean.
218 lines
16 KiB
Markdown
218 lines
16 KiB
Markdown
# [recycle] Convex / Realtime / Storage 实现偏差审查
|
||
|
||
> 执行状态:已归档到 `design/10-review/done/`。本文保留为历史审查快照,当前活跃口径以 `./06-execution-checklist-and-acceptance.md` 为准。
|
||
|
||
## 范围
|
||
|
||
本报告审查 Convex / storage bridge / realtime / Page Aggregate 数据底座与当前设计主线的一致性,重点对照以下方向:
|
||
|
||
- Convex 保留为自托管 storage / realtime substrate,不作为树语义 owner。
|
||
- Rust kernel / bridge-runtime / mnote-web 持有 tree-first graph、projection、command、Page Aggregate 的语义主导权。
|
||
- `/api/page-aggregate/:id` 作为文档页 Rust-first 读取主链。
|
||
- `/api/tree/events` 作为 Rust Web tree realtime snapshot / delta / resync 主链。
|
||
- 前端只消费稳定 projection、Page Aggregate 与 tree stream,不重新持有第二套对象真相。
|
||
|
||
重点读取范围:
|
||
|
||
- `ARCHITECTURE.md`
|
||
- `design/01-05-current-priority-overview.md`
|
||
- `design/02-convex-rust-long-term-architecture/process/2-tree-first-graph-convex-rust-long-term-architecture-v1.md`
|
||
- `design/03-rust-web/process/3-3-rust-web-tree-realtime-event-stream-v1.md`
|
||
- `design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md`
|
||
- `design/05-editor-mainline/done/5-6-page-aggregate-alignment-checklist-v1.md`
|
||
- `design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md`
|
||
- `recycle/20260522-convex-runtime-retirement/infra/convex/README.md`
|
||
- `wolai-frontend/convex/`
|
||
- `rust/crates/storage-convex-bridge/`
|
||
- `rust/crates/mnote-web/src/transport/convex.rs`
|
||
- `rust/crates/mnote-web/src/routes/documents.rs`
|
||
- `rust/crates/mnote-web/src/routes/tree.rs`
|
||
- `rust/crates/mnote-web/src/routes/sse.rs`
|
||
- `rust/crates/mnote-web/src/routes/stream_support.rs`
|
||
- `wolai-frontend/src/lib/documents/`
|
||
- `wolai-frontend/src/lib/tree-stream/`
|
||
|
||
## 结论
|
||
|
||
整体方向与设计主线基本一致:Convex 没有被拆掉,仍承担本地自托管存储、文件和实时底座;Rust Web 已注册 `/api/page-aggregate/:document_id` 与 `/api/tree/events`;前端 tree stream 已直接使用 `EventSource("/api/tree/events")`;Next 的 `/api/documents/page` 与 `/api/mnote-web/stream` 已明确作为 410 compat 边界退场。
|
||
|
||
但当前实现仍是明显过渡态,主要问题集中在三处;本文里的 `infra/convex` 现在应按历史对照理解,因为自托管 compose 已软删除到 recycle,不再作为 active 部署入口:
|
||
|
||
1. Page Aggregate 读链虽然是 Rust endpoint,但实际数据仍由 `documents:getMeta` + `documents:getContent` 分别取回后在 bridge-runtime 中拼 projection,`source` 标识存在“看起来比真实实现更 canonical”的风险。
|
||
2. Rust Web 的 `/api/documents/title`、`/api/documents/options` 与 Next route 的 page write adapter 在 bridge artifact 记录上不一致;tree stream 依赖 bridgeLogs 时,Rust page write 路径可能漏掉 command/domain event。
|
||
3. `/api/tree/events` 是正式 SSE 入口,但当前 change detection 依赖周期性查询 `bridgeLogs:listWorkspaceOverview`,且 Convex query 内部会按 workspace collect 全量 command/domain rows 后内存过滤排序,实时性和规模风险较高。
|
||
|
||
## 关键发现表格
|
||
|
||
| 编号 | 分类 | 严重度 | 发现 | 证据 |
|
||
| --- | --- | --- | --- | --- |
|
||
| F1 | 实现偏差 | 高 | Rust Page Aggregate endpoint 对外是 `mnote.page_aggregate.v1`,但构建过程仍先分别读取 meta/content,再在 bridge-runtime 中拼 projection;`source=KernelProjection` 容易掩盖底层仍是兼容 join。 | `rust/crates/mnote-web/src/routes/web_shell.rs:2282`、`:2291`、`:2301`;`rust/crates/bridge-runtime/src/lib.rs:8246`、`:8252`、`:5291`、`:5303`、`:5386` |
|
||
| F2 | 风险 | 高 | Rust Web 的 title/options 写路由只执行 Convex mutation,不记录 bridge command/domain artifacts;而 tree stream 以 bridgeLogs 为事件来源,主路径 page 写入可能不进入实时流。 | `rust/crates/mnote-web/src/routes/documents.rs:714`、`:802`;`rust/crates/mnote-web/src/routes/command_support.rs:65`;对比 `rust/crates/mnote-web/src/routes/tree.rs:6529` 与 `wolai-frontend/src/lib/documents/page-write-command-adapter.ts:63` |
|
||
| F3 | 风险 | 高 | tree realtime 当前是 SSE transport,但后端通过 `sleep(poll_ms)` 周期性轮询 Convex bridgeLogs,而不是直接利用 Convex subscription/push;默认 2s,实时性和负载都需继续验证。 | `rust/crates/mnote-web/src/routes/sse.rs:25`、`:26`、`:51`、`:58`、`:60`;`rust/crates/mnote-web/src/routes/stream_support.rs:610` |
|
||
| F4 | 风险 | 中高 | `bridgeLogs:listWorkspaceOverview` 会按 workspace collect 全量 `command_logs` 与 `domain_events` 后在内存中过滤、排序、分页;SSE 轮询叠加后,历史日志增长会放大 Convex 读压力。 | `wolai-frontend/convex/bridgeLogs.ts:283`、`:287`、`:292`、`:301`、`:305` |
|
||
| F5 | 方向变化/文档滞后 | 中 | `storage-convex-bridge` 把 `page.aggregate.get` 映射到 `documents:getPageAggregate`,但 Convex `documents.ts` 只发现 `getMeta` / `getContent`,未发现 `getPageAggregate` export;当前真实 Page Aggregate 路线已绕到 Rust adapter。 | `rust/crates/storage-convex-bridge/src/mapping.rs:101`;`wolai-frontend/convex/documents.ts:684`、`:803`;全仓 `rg getPageAggregate` 未发现 Convex 实现 |
|
||
| F6 | 已完成/方向一致 | 中 | Next compat 读链和旧 stream alias 已显式退场,前端 tree stream 直接构造 `/api/tree/events`,符合当前主线口径。 | `wolai-frontend/src/app/api/documents/page/route.ts:17`;`wolai-frontend/src/app/api/mnote-web/stream/route.ts:9`;`wolai-frontend/src/lib/tree-stream/protocol.ts:66`、`:72` |
|
||
| F7 | 未完成 | 中 | Page Aggregate TS builder 已不在 runtime 主链中被引用,但文件仍保留;当前定位应继续写成 fallback / adapter / 测试材料,不能作为运行时主路径描述。 | `wolai-frontend/src/lib/documents/page-aggregate-builder.ts:51`;全仓非测试引用仅剩定义,runtime loader 只调用 Rust snapshot:`wolai-frontend/src/lib/documents/page-aggregate-loader.ts:225` |
|
||
|
||
## 证据
|
||
|
||
### 1. Page Aggregate 读链已收口到 Rust endpoint,但仍由 meta/content join 生成
|
||
|
||
`mnote-web` 注册了正式 endpoint:
|
||
|
||
- `rust/crates/mnote-web/src/routes/mod.rs:53` 到 `:56` 注册 `/api/page-aggregate/{document_id}`。
|
||
- `rust/crates/mnote-web/src/routes/web_shell.rs:2227` 到 `:2252` 返回 `schema: "mnote.page_aggregate.v1"` 和 `result: aggregate`。
|
||
|
||
但实际构建流程仍是:
|
||
|
||
- `rust/crates/mnote-web/src/routes/web_shell.rs:2282` 到 `:2290` 读取 `load_document_meta_result`。
|
||
- `rust/crates/mnote-web/src/routes/web_shell.rs:2291` 到 `:2299` 读取 `load_document_content_result`。
|
||
- `rust/crates/mnote-web/src/routes/web_shell.rs:2301` 到 `:2315` 把 `meta + content` 作为数据传给 `page.aggregate.get`。
|
||
- `rust/crates/bridge-runtime/src/lib.rs:5291` 到 `:5312` 的 `build_page_aggregate_projection_result` 明确从 `data.meta` 和 `data.content` 中拆字段。
|
||
|
||
这说明“Rust-first 读取主链”成立,但“Page Aggregate 已经由 kernel 原生投影独立产出”仍未成立。该点与 `5-5/5-6` 的过渡态判断一致,但需要在后续文档和汇报中保持精确。
|
||
|
||
需进一步验证:`PageAggregateSource::KernelProjection` 是否应在这种 `meta/content join` 场景下继续使用,还是应保留 `CompatMetaContentJoin` 以避免 provenance 误导。
|
||
|
||
### 2. Page write side effects 在 Rust route 与 Next adapter 之间不一致
|
||
|
||
Next route 侧:
|
||
|
||
- `wolai-frontend/src/app/api/documents/title/route.ts:58` 调用 `executePageWriteBridgeCommand`。
|
||
- `wolai-frontend/src/app/api/documents/options/route.ts:47` 调用 `executePageWriteBridgeCommand`。
|
||
- `wolai-frontend/src/app/api/documents/save/route.ts:43` 调用 `executePageWriteBridgeCommand`。
|
||
- `wolai-frontend/src/lib/documents/page-write-command-adapter.ts:63` 到 `:69` 成功后调用 `recordRustBridgeCommandArtifacts`。
|
||
|
||
Rust route 侧:
|
||
|
||
- `rust/crates/mnote-web/src/routes/documents.rs:579` 的正文保存使用 `execute_runtime_command_via_convex_with_artifacts`。
|
||
- `rust/crates/mnote-web/src/routes/documents.rs:714` 的标题更新使用 `execute_runtime_command_via_convex`。
|
||
- `rust/crates/mnote-web/src/routes/documents.rs:802` 的页面设置更新使用 `execute_runtime_command_via_convex`。
|
||
- `rust/crates/mnote-web/src/routes/command_support.rs:65` 到 `:73` 的 `execute_runtime_command_via_convex` 只执行 Convex command plan,不持久化 artifacts。
|
||
- `rust/crates/mnote-web/src/routes/command_support.rs:75` 到 `:93` 的 artifact 版本才会调用 `execute_convex_command_plan_with_artifacts`。
|
||
|
||
tree command route 是一致的:
|
||
|
||
- `rust/crates/mnote-web/src/routes/tree.rs:6529` 使用 `execute_runtime_command_via_convex_with_artifacts`。
|
||
|
||
风险是:如果当前 3000 主入口走 Rust Web `/api/documents/title` 或 `/api/documents/options`,这些 page 写入不会进入 `command_logs/domain_events`,而 `/api/tree/events` 正是通过 bridgeLogs 检测变化。标题类写入尤其可能影响 sidebar/breadcrumb/page tree 的实时一致性。
|
||
|
||
需进一步验证:当前文档页标题编辑在 3000 主壳中到底走 `/api/documents/title` 还是 `/api/tree/commands`;如果走前者,应补 Rust route artifact 记录或统一到 tree/page command route。
|
||
|
||
### 3. Tree realtime 是正式 SSE 入口,但实现仍是 polling-backed stream
|
||
|
||
正式入口成立:
|
||
|
||
- `rust/crates/mnote-web/src/routes/mod.rs:130` 注册 `/api/tree/events`。
|
||
- `rust/crates/mnote-web/src/routes/sse.rs:127` 到 `:146` 给 tree events 加 `x-mnote-tree-stream-owner: rust-web`。
|
||
- `wolai-frontend/src/lib/tree-stream/protocol.ts:66` 到 `:77` 构造 `/api/tree/events` URL。
|
||
- `wolai-frontend/src/lib/tree-stream/use-sidebar-tree-stream.ts:136` 到 `:143` 用 `EventSource` 监听 `snapshot/delta/resync`。
|
||
|
||
但服务端 change detection 是轮询:
|
||
|
||
- `rust/crates/mnote-web/src/routes/sse.rs:25` 到 `:26` 读取 `max_polls` 和 `poll_ms`,默认 `2000ms`,最低 `250ms`。
|
||
- `rust/crates/mnote-web/src/routes/sse.rs:51` 到 `:58` 循环中 `sleep(Duration::from_millis(poll_ms))`。
|
||
- `rust/crates/mnote-web/src/routes/sse.rs:60` 到 `:67` 每轮调用 `load_stream_overview` 再 `resolve_stream_change`。
|
||
- `rust/crates/mnote-web/src/routes/stream_support.rs:610` 到 `:625` 的 `load_stream_overview` 通过 `bridge.workspace.overview` 查询 Convex。
|
||
|
||
这与“Rust Web 提供正式实时 transport”一致,但还不是“基于 Convex realtime subscription 的 push stream”。短期可接受为过渡实现,长期如果继续承载主链,需要明确性能和延迟边界。
|
||
|
||
### 4. bridgeLogs overview 查询存在规模风险
|
||
|
||
`wolai-frontend/convex/bridgeLogs.ts` 的 `listWorkspaceOverview`:
|
||
|
||
- `:283` 到 `:286` 按 workspace 查询并 `collect()` 所有 `command_logs`。
|
||
- `:287` 到 `:290` 按 workspace 查询并 `collect()` 所有 `domain_events`。
|
||
- `:292` 到 `:301` 在内存中过滤、排序、切片 command logs。
|
||
- `:303` 到 `:312` 再用 commandId 集合过滤 domain events。
|
||
|
||
该实现作为调试/小数据过渡可以工作,但被 `/api/tree/events` 默认每 2 秒调用时,日志增长后会造成:
|
||
|
||
- Convex query 读放大。
|
||
- SSE 连接数量增加时成倍放大。
|
||
- cursor 语义依赖内存排序,历史数据规模变大后容易出现延迟或超时。
|
||
|
||
建议后续至少增加按 `workspace_id + created_at/id` 的索引分页,避免每次 stream poll 全量扫描。
|
||
|
||
### 5. storage-convex-bridge 中存在疑似过时的 `page.aggregate.get` 映射
|
||
|
||
`rust/crates/storage-convex-bridge/src/mapping.rs:101`:
|
||
|
||
```text
|
||
"page.aggregate.get" => "documents:getPageAggregate"
|
||
```
|
||
|
||
但当前 `wolai-frontend/convex/documents.ts` 中只发现:
|
||
|
||
- `getMeta`:`wolai-frontend/convex/documents.ts:684`
|
||
- `getContent`:`wolai-frontend/convex/documents.ts:803`
|
||
|
||
全仓搜索未发现 Convex `documents:getPageAggregate` 实现。当前真实 Page Aggregate 读取是 Rust Web 先读 meta/content,再由 bridge-runtime 生成 projection。因此该映射要么是未来目标,要么已经滞后;如果有代码路径直接通过 storage-convex-bridge 执行 `page.aggregate.get`,会存在运行时函数不存在风险。
|
||
|
||
需进一步验证:`storage-convex-bridge` 是否仍有生产路径直接执行 `page.aggregate.get -> documents:getPageAggregate`;如果没有,应把映射标注为 future/stale,或改为当前真实读链。
|
||
|
||
### 6. Convex substrate 定位总体一致
|
||
|
||
正向证据:
|
||
|
||
- `recycle/20260522-convex-runtime-retirement/infra/convex/README.md` 明确自托管 Convex backend/dashboard、HTTP Actions 和文件存储的历史对照口径。
|
||
- `wolai-frontend/convex/schema.ts:35` 到 `:91` 的 `documents` 表继续承接页面结构、正文、页面设置、统计字段。
|
||
- `wolai-frontend/convex/schema.ts:121` 到 `:162` 的 `media_assets` 与 `_storage` 字段保留文件底座。
|
||
- `rust/crates/storage-convex-bridge/README.md:5` 到 `:13` 明确 bridge 只做协议到 Convex 读写请求映射,不复制主事实层、不维护第二数据库。
|
||
|
||
这与“Convex 不拆,Rust 收口语义”的设计一致。
|
||
|
||
## 建议优先级
|
||
|
||
### P0:修正 Rust page write route 的 artifact 一致性
|
||
|
||
目标:让当前 3000 主入口的 `/api/documents/title`、`/api/documents/options` 至少在 side effect 上与 Next `page-write-command-adapter` 保持一致。
|
||
|
||
建议:
|
||
|
||
- 将 `rust/crates/mnote-web/src/routes/documents.rs` 中 title/options 的 `execute_runtime_command_via_convex` 改为 artifact 版本,或统一复用 tree/page command route。
|
||
- 对标题更新补最小回归:更新标题后 `bridgeLogs:listWorkspaceOverview` 能看到对应 command/domain event,`/api/tree/events` 能输出 delta 或 resync。
|
||
- 对 artifact 写失败的策略重新定级:当前 `rust/crates/mnote-web/src/transport/convex.rs:759` 到 `:770` 是主 mutation 成功、artifact 失败仍返回成功;对 tree-relevant command 至少应打可观测错误并触发保守 resync。
|
||
|
||
### P1:收口 Page Aggregate provenance
|
||
|
||
目标:避免把 `meta/content join` 误标为已完成的 kernel-native projection。
|
||
|
||
建议:
|
||
|
||
- 明确 `PageAggregateSource::KernelProjection` 与 `CompatMetaContentJoin` 的使用边界。
|
||
- 如果 `build_page_aggregate_snapshot` 仍通过 `load_document_meta_result + load_document_content_result` 构建,应在返回 source 或 header 中反映真实来源。
|
||
- 如果目标是 kernel-native projection,则补正式 kernel/query 数据路径,避免 route 层长期手工拼装。
|
||
|
||
### P1:优化 tree stream 的 Convex 查询模型
|
||
|
||
目标:让 `/api/tree/events` 可以长期承载主链,而不是随着日志增长退化。
|
||
|
||
建议:
|
||
|
||
- 为 `command_logs`、`domain_events` 增加按 `workspace_id + created_at/id` 的查询索引和 cursor 查询。
|
||
- `listWorkspaceOverview` 不再 `collect()` 全量 workspace 日志后内存分页。
|
||
- 明确 polling-backed SSE 是过渡实现,还是长期 realtime transport;若长期使用,应补连接数、日志量、延迟上限的 smoke/bench。
|
||
|
||
### P2:清理或标注 stale mapping
|
||
|
||
目标:降低后续 worker 误用 `page.aggregate.get -> documents:getPageAggregate` 的风险。
|
||
|
||
建议:
|
||
|
||
- 若 `documents:getPageAggregate` 不计划实现,移除或注释 `storage-convex-bridge` 中的映射。
|
||
- 若计划实现,补 Convex query 与最小测试,并让 mnote-web Page Aggregate route 直接消费它或说明为什么不消费。
|
||
|
||
### P2:保留 Next compat 退场边界,但避免双实现继续发散
|
||
|
||
目标:Next route 继续作为 legacy/adapter 参考时,不与 Rust Web 主入口形成不同 side effects。
|
||
|
||
建议:
|
||
|
||
- 对 `/api/documents/title/options/save` 明确 owner:Rust Web 主路径与 Next legacy 路径只能有一份 canonical side-effect 规则。
|
||
- 对 `wolai-frontend/src/lib/documents/page-aggregate-builder.ts` 保留测试/adapter 标签,避免被重新接回 runtime 主链。
|
||
|
||
## 修改的文件路径
|
||
|
||
- `design/10-review/done/03-convex-realtime-storage-review.md`
|