feat: align local-first workspace direction

Document the VSCode-like local-first product shape, demote Convex to a control-plane role, and retire stale architecture drafts.

Add local workspace migration/export references plus smoke coverage for no-Convex managed workspace startup, local markdown title/body/options persistence, asset upload behavior, and Convex fixture export.

Verification: git diff --cached --check; node scripts/check-local-first-convex-guard.js --staged; node scripts/task444-convex-workspace-export-local-fixture-smoke.js; node scripts/task166-local-first-managed-workspace-no-convex-smoke.js; node scripts/task167-local-markdown-title-body-options-no-convex-smoke.js
This commit is contained in:
lix-2026
2026-05-19 08:11:58 +08:00
parent 68d321e297
commit cdff672aa5
67 changed files with 5242 additions and 932 deletions
@@ -0,0 +1,47 @@
# 5-19 [done] Page Aggregate 块投影未优先消费 EditorBlockDocument
## 级别
P1 / 05-editor-mainline 主线迁移缺陷。
## 现象
`page.body.save` 已在 Rust runtime plan 与 artifact 中保留 `editorDocument`,但 `page.aggregate.get` 构建 `body.blockDocument` 时仍固定从 legacy `documents.content` 投影。
`editorDocument` 与旧 `content` 不一致时,AI 块读取、块定位与 Page Aggregate 回读会继续看到旧内容,导致页面域单一真源迁移停留在 legacy content 过渡层。
## 根因
`rust/crates/bridge-runtime/src/lib.rs``build_page_aggregate_projection_result` 只读取 `content_result.content`,随后调用 `project_legacy_content_to_block_document`,并把 `projectionSource` 固定为 `documents.content`
## 修复
- `page.aggregate.get` 的内容结果识别现在包含 `editorDocument``blockDocument`
- Page Aggregate `body.blockDocument` 来源顺序调整为:
- `editorDocument`:优先解析 `EditorBlockDocument`,由 Rust runtime 投影为 Page Aggregate 块文档。
- `blockDocument`:已有块投影直接作为次级来源。
- `documents.content`:仅在缺少原生块文档来源时回退。
- `body.projectionSource` 改为真实来源:`editorDocument` / `blockDocument` / `documents.content`
- 新增回归测试 `page_aggregate_get_prefers_editor_document_over_legacy_content`,覆盖 `editorDocument` 与旧 `content` 冲突时的优先级。
- 新增非法原生块文档防回退保护:
- `editorDocument` 存在但不可解析时返回 validation error,不再静默回退到旧 `content`
- `blockDocument` 存在但不是对象时返回 validation error,不再静默回退到旧 `content`
- Page Aggregate `body.content` 也同步按原生来源重建:
- `editorDocument` 来源下由 `EditorBlockDocument` 生成 legacy content。
- `blockDocument` 来源下由块投影生成 legacy content。
- 避免同一份 Page Aggregate 中 `body.blockDocument` 正确但 `body.content` 仍指向旧正文,导致编辑器/session 下游继续显示或保存旧真相。
## 验证
- RED`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate_get_prefers_editor_document_over_legacy_content -- --nocapture`
- 旧实现失败,返回 `legacy_1` 而不是 `editor_1`
- GREEN`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime page_aggregate -- --nocapture`
- 6 passed。
- Rust Web 回归:`cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate -- --nocapture`
- 7 passed。
- 格式:`cargo fmt --manifest-path rust/Cargo.toml --all -- --check`
- passed。
## 剩余边界
这次修的是 Page Aggregate 读侧优先级,不是 Convex 原生 `EditorBlockDocument` 落库 schema 的最终切换。`documents:updateContent` 兼容写入仍保留 legacy content substrate;后续主线应继续推进持久层原生块文档字段与命令写入闭环。
@@ -0,0 +1,39 @@
# 5-20 [done] page.body.save 发往 Convex 前剥离 EditorBlockDocument
## 级别
P1 / 05-editor-mainline 主线迁移缺陷。
## 现象
`page.body.save` 的 Rust runtime plan 已经生成 `editorDocument` / `tiptapDocument`,但 `mnote-web` transport 在真正调用 `documents:updateContent` 前会删除这两个字段。
这会导致原生块文档只存在于 plan / artifact 里,持久层仍只能保存 legacy `content`。刷新后 Page Aggregate 即使读侧已支持 `editorDocument -> blockDocument -> documents.content`,也会因为 substrate 没有原生字段而回到 `documents.content`
## 根因
`rust/crates/mnote-web/src/transport/convex.rs``documents.save | page.body.save` 视为 legacy validator,只保留 `id/content/expectedRevision/conflictDetectionKey`,显式删除 `editorDocument``tiptapDocument`
同时,当前可见的 Convex `documents:updateContent` 源码仍只声明 `content`,没有持久化原生编辑器快照字段。
## 修复
- `mnote-web` transport 不再剥离 `editorDocument` / `tiptapDocument`,只剥离 Rust artifact 事件字段。
- 本地可见 Convex substrate `recycle/wolai-frontend/convex/documents.ts` 已同步接受并写入:
- `editorDocument -> documents.editor_document`
- `tiptapDocument -> documents.tiptap_document`
- `getContent` / `getContentForIngest` 已同步返回 `editorDocument` / `tiptapDocument`,让 Page Aggregate 读侧能够优先消费原生块文档。
- `recycle/wolai-frontend/convex/schema.ts` 已补 `editor_document` / `tiptap_document` optional 字段。
说明:`recycle/` 当前被仓库 `.gitignore` 忽略,但这是本仓库可见的唯一 `documents:updateContent` Convex 源码位置;tracked Rust transport 已按该 validator 口径更新。
## 验证
- RED`cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_keeps_editor_runtime_fields_for_page_body_save -- --nocapture`
- 旧实现失败,实际 args 中 `editorDocument` / `tiptapDocument` 被删除。
- GREEN`cargo test --manifest-path rust/Cargo.toml -p mnote-web convex_command_args_keeps_editor_runtime_fields_for_page_body_save -- --nocapture`
- 1 passed。
## 剩余边界
这次修复让 `page.body.save` 不再在 Rust Web transport 边界丢弃原生块文档,但完整 EditorBlockDocument 原生落库仍需要后续真实 Convex 部署 schema 与迁移流程确认。历史页面已有 legacy `content`,不会自动反推补齐 `editor_document`
@@ -0,0 +1,31 @@
# 5-21 [done] page.body.save 对非法 editorDocument 不能静默回退
## 级别
P1 / 05-editor-mainline 主线迁移缺陷。
## 现象
`page.body.save` 在保存时会同时携带 `editorDocument``tiptapDocument` 和 legacy `content`
`editorDocument` 字段存在但结构非法时,原实现会静默回退到 `tiptapDocument``content`,把原生块快照损坏隐藏掉。
## 根因
`normalize_save_editor_document(...)` 之前只在 `serde_json::from_value::<EditorBlockDocument>` 成功时才使用 `editorDocument`,失败后会继续尝试 `tiptapDocument`,最后再回退 `legacy content`
## 修复
- `editorDocument` 一旦存在就必须可解析。
- `editorDocument` 无法反序列化时,直接返回 validation error,不再回退到 `tiptapDocument``content`
- 现有“合法 editorDocument 优先于其他来源”语义保持不变。
## 验证
- RED`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_save_command_plan_rejects_unparseable_editor_document_instead_of_falling_back -- --nocapture`
- 旧实现失败,证明原先会静默回退。
- GREEN`cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_save_command_plan -- --nocapture`
- 12 passed。
## 剩余边界
这次修复只收紧非法 `editorDocument` 的失败语义,不改变合法 `editorDocument` / `tiptapDocument` 的既有优先级顺序。
@@ -0,0 +1,62 @@
# 5-22 [done][bug] documents.content.get 在 Page Aggregate 前剥离原生块文档字段 v1
> 发现时间:2026-05-18
>
> 状态:`[done]`
>
> 关联主线:`05-editor-mainline`
## 1. 问题定义
`page.aggregate.get` 已支持按 `editorDocument -> blockDocument -> documents.content` 的顺序构建正文真相,但 `/api/page-aggregate/:id` 主链会先通过 `documents.content.get` 读取内容。
旧的 `DocumentContentResult` 只保留:
- `content`
- `revision`
- `conflictDetectionKey`
- `title`
- `pageSubtree`
上游返回的 `editorDocument / blockDocument / tiptapDocument` 被提前剥离,导致 Page Aggregate route 真实运行时仍回落到 `documents.content`
## 2. 根因
`bridge-runtime``build_document_content_result` 没有把原生块文档字段带入 `DocumentContentResult`
这使 5-19 的 Page Aggregate 优先级修复只在合成输入测试里成立,真实 `mnote-web` route 入口仍拿不到原生字段。
## 3. 修复
- `core-protocol::DocumentContentResult` 新增可选字段:
- `editorDocument`
- `tiptapDocument`
- `blockDocument`
- `blockProjectionVersion`
- `build_document_content_result` 保留这些上游字段。
- `mnote-web` Page Aggregate route 回归测试加入含 `editorDocument``documents:getContent` fixture,并断言:
- `body.projectionSource=editorDocument`
- `body.blockDocument.rootBlockIds[0]=editor_1`
- `body.blockDocument.blocks[0].text` 来自 `editorDocument`
## 4. 验证
RED
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate_endpoint_returns_snapshot_contract -- --nocapture
```
旧实现失败,`projectionSource` 仍为 `documents.content`
GREEN
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_aggregate_endpoint_returns_snapshot_contract -- --nocapture
```
结果:`1 passed`
## 5. 剩余边界
这修复的是 Rust route 读取链字段保留;底层 Convex schema/mutation 是否已完整原生落库仍按后续持久层迁移继续跟进。
@@ -0,0 +1,59 @@
# 5-23 [done][bug] editorDocument 缺少 blocks 或 root 引用损坏仍被当作合法真源 v1
> 发现时间:2026-05-18
>
> 状态:`[done]`
>
> 关联主线:`05-editor-mainline`
## 1. 问题定义
`EditorBlockDocument` 的 serde 结构对 `blocks/rootBlockIds` 使用默认值。旧实现只检查 JSON 是否能反序列化,导致这类输入被接受:
```json
{"documentId":"doc_1"}
```
或者:
```json
{"documentId":"doc_1","rootBlockIds":["missing"],"blocks":[]}
```
它们会优先于 legacy content 被当作原生块文档真相,可能把页面保存为空,或让 `body.content``body.blockDocument` 分裂。
## 2. 根因
`normalize_save_editor_document``page_aggregate_block_document_projection` 只做 serde 形状解析,没有做原始字段存在性和 root 引用完整性校验。
## 3. 修复
- 新增 `validate_editor_document_structure`
- 显式 `editorDocument` 必须带 `blocks` 数组。
- `rootBlockIds` 中的非空 id 必须能在 `blocks` 中找到。
- 保存链和 Page Aggregate 读链共用同一校验。
## 4. 验证
RED
```bash
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime rejects_editor_document -- --nocapture
```
旧实现中以下两个新增测试失败:
- `documents_save_command_plan_rejects_editor_document_missing_blocks`
- `page_aggregate_get_rejects_editor_document_root_ids_missing_blocks`
GREEN
```bash
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime rejects_editor_document -- --nocapture
```
结果:`2 passed`
## 5. 剩余边界
当前校验保证 `editorDocument` 不会因 serde default 被误判为合法真源;更深的 schema 版本、块类型特定必填字段仍应在 EditorBlockDocument 原生落库迁移中继续收紧。
@@ -0,0 +1,60 @@
# 5-24 [done][bug] page.body.save 上传图片与附件持久化字段丢失 v1
> 发现时间:2026-05-18
>
> 状态:`[done]`
>
> 关联主线:`05-editor-mainline`
## 1. 问题定义
主编辑区文本保存修复后,继续验证上传图片与附件发现两个持久化缺陷:
- 附件能插入到编辑器,并且 `/api/documents/save` 返回 200,但 `/api/documents/content` 回读时链接 mark 被降级成纯文本。
- 图片能上传并触发保存,但 `/api/documents/content` 回读时图片块只剩 `type: "image"``props.src / alt / title / tiptapImage` 为空。
这会导致刷新或跨浏览器打开后,附件不能作为 OnlyOffice 附件链接恢复,图片也缺少可渲染的资源地址。
## 2. 根因
保存链优先使用 `editorDocument` 生成 canonical legacy content 后再写入当前 Convex legacy substrate。
两个字段在这一步丢失:
- `legacy_content_from_editor_document` 只用 `legacy_text_from_editor_block()` 生成纯文本,未保留 `ContentNode.attrs.styles.link`
- `EditorBlockDocument` 反序列化时 `BlockProps.extra` 不是 flatten 字段,前端传来的 image `props.src / alt / title / tiptapImage` 会被 serde 忽略;`hydrate_editor_document_props_from_raw` 旧实现只回填 mindmap props,没有回填 image props。
## 3. 修复
- `legacy_content_from_editor_document` 对带结构化样式的 inline 节点输出 legacy inline content 数组,保留 `styles.link` 等样式;普通纯文本段落仍保持旧的字符串 content。
- `hydrate_editor_document_props_from_raw` 增加 image 分支,从原始 `editorDocument.blocks[].props` 回填 `src / alt / title / tiptapImage`
- `legacy_props_from_editor_block` 对 image 块输出回填后的直接 props 与 `tiptapImage`
## 4. 验证
单测:
```bash
cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime legacy_content_from_editor_document_preserves_inline_link_styles -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_save_hydrates_image_props_from_raw_editor_document -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p bridge-runtime documents_save_command_plan_prefers_valid_editor_document_over_other_sources -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web documents_save_route_executes_page_body_save_command -- --nocapture
```
浏览器验证:
- 以测试账号登录 `http://127.0.0.1:13000`
- 新建临时页面。
- 通过 slash 上传 `.docx` 附件和 `.png` 图片。
- 断言两次 `/api/documents/save` 均返回 200。
- 断言 `/api/documents/content` 回读包含:
- image `props.src`
- 附件 inline `styles.link`
- 刷新页面后,编辑器 DOM 中仍能找到附件链接与图片节点。
结果:通过;临时测试页面已 purge。
## 5. 剩余边界
当前仍是 legacy content substrate 下的过渡修复。完整 EditorBlockDocument 原生落库完成后,图片/附件 props 应由原生 schema 和迁移校验继续收紧。