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:
@@ -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;后续主线应继续推进持久层原生块文档字段与命令写入闭环。
|
||||
+39
@@ -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`。
|
||||
+31
@@ -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` 的既有优先级顺序。
|
||||
+62
@@ -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 原生落库迁移中继续收紧。
|
||||
+60
@@ -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 和迁移校验继续收紧。
|
||||
Reference in New Issue
Block a user