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,53 @@
# 3-16 [done] 本地 Markdown 上传误走云端 media asset 修复 v1
> 创建时间:2026-05-18
>
> 状态:`[done]`
>
> 所属主线:`03-rust-web` / `local_folder` / `local markdown`
## 问题
本地文件夹模式下,`.md` 页面内上传图片或附件沿用了云端 `/api/media/upload` 链路,会生成 Convex media asset,并在正文中写入云端 URL 或 OnlyOffice assetId 链接。
这会破坏本地 Markdown 的可迁移性:离开 MNote 后文件引用不可读,也无法符合 local folder “文件系统即资源真源”的边界。
## 修复
- 新增 `POST /api/local-folder/assets/upload`
- 本地上传复制到当前 Markdown 同目录下的 `{mdBase}.assets/`
- 返回给编辑器的 `sourcePath` / `file_url` 使用相对当前 Markdown 目录的路径。
- 前端在 `sourceKind=local_folder` 时分流到本地上传入口,云端页面继续走 `/api/media/upload`
- 本地附件插入普通相对路径链接,不再生成 OnlyOffice assetId URL。
- 本地图片保存时写回标准 Markdown 图片语法:`![alt](relative/path)`
## 验证
```bash
cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown_ -- --nocapture
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_folder -- --nocapture
```
临时 HTTP smoke
```bash
MNOTE_WEB_BIND=127.0.0.1:13001 MNOTE_WEB_PUBLIC_BIND=127.0.0.1:13001 cargo run --manifest-path rust/Cargo.toml -p mnote-web --bin mnote-web
curl -sS -X POST \
-F "file=@/etc/hosts;filename=photo.png;type=image/png" \
-F "rootUri=file:///tmp/mnote-local-smoke-..." \
-F "documentId=local-md:README.md" \
-F "kind=image" \
http://127.0.0.1:13001/api/local-folder/assets/upload
```
结果:
- `local_markdown_`19 passed
- `local_folder`36 passed
- `cargo fmt --check`:通过
- HTTP smoke:返回 `sourcePath=README.assets/photo-1.png`,落盘文件存在且 size=188
## 剩余
- 尚未跑真实浏览器 smoke:本地 `.md` 页面上传图片/附件、刷新后恢复、文件树可见。
@@ -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 和迁移校验继续收紧。
@@ -0,0 +1,64 @@
# 7-31 [done][bug] 页面 AI block_edit_workflow 丢弃读取类回答摘要 v1
> 发现时间:2026-05-18
>
> 状态:`[done]`
>
> 关联主线:`07-ai`
## 1. 用户可见症状
在页面 AI 使用 `ACP · Reasonix` 输入:
```text
检查你是否能读取到本页第一段,同时请修改第二段为:测试123
```
`block_edit_workflow` 即使完成写入,前端也只显示固定文案:
```text
已通过页面 markdown 编辑快路径完成写入。
```
用户要求中的“是否能读取到第一段”没有被回答。
## 2. 根因
`page_ai_workflow.rs` 的模型提示已经要求模型输出:
```json
{"operations": [...], "summary": "..."}
```
但路由只提取 `operations`,丢弃了 `summary`,并在成功响应中固定返回“已通过页面 markdown 编辑快路径完成写入。”。
这会让读写混合请求退化为纯写入反馈。
## 3. 修复
- 新增 `MarkdownEditPlan`,同时解析模型输出中的 `operations``summary`
- `block_edit_workflow` 成功响应优先返回模型 `summary`,缺失时才使用固定 fallback。
- 系统提示明确要求:如果用户要求读取某段,`summary` 必须包含从 `page_text` 读取到的原文。
- 新增回归测试 `block_edit_workflow_surfaces_model_summary_for_read_and_edit_request`,覆盖“读取第一段 + 修改第二段”的真实快路径响应。
## 4. 验证
RED
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web block_edit_workflow_surfaces_model_summary_for_read_and_edit_request -- --nocapture
```
旧实现失败,`payload.message` 不包含 `已读取第一段:第一段`
GREEN
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_workflow -- --nocapture
```
结果:`5 passed`
## 5. 剩余边界
本修复只保证 fast workflow 成功响应不丢模型摘要;复杂多步推理、review session、流式 apply 仍属于 Phase C 冻结范围。
@@ -0,0 +1,50 @@
# 7-32 [done][bug] markdown_edit 直接 page.body.save 时丢失 revision / conflictDetectionKey v1
> 发现时间:2026-05-18
>
> 状态:`[done]`
>
> 关联主线:`07-ai`
## 1. 问题定义
7-27 后,在线 `mnote.doc.markdown_edit` 不再通过空 `block_ops` 写回,而是以最终 Markdown 生成 `content` 后直接执行 `page.body.save`
旧实现构造的写入 payload 只包含:
- `documentId`
- `workspaceId`
- `content`
- `mode`
没有携带当前 Page Aggregate 的 `revision``conflictDetectionKey`
## 2. 影响
AI markdown 主路径可能基于旧快照覆盖用户并发编辑,绕过 7-22 已补到 `apply_block_ops` 的写前置条件。
## 3. 修复
- 在线 `markdown_edit` 写回前读取当前 Page Aggregate。
- 构造 `page.body.save` payload 时带入:
- `/body/revision`
- `/body/conflictDetectionKey`
- 新增回归测试检查 command artifact payload 中真实携带这两个字段。
## 4. 验证
RED
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_online_page_body_save_carries_revision_conflict_key -- --nocapture
```
旧实现失败,artifact payload 中 `revision``Null`
GREEN
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_online_page_body_save_carries_revision_conflict_key -- --nocapture
```
结果:`1 passed`
@@ -0,0 +1,41 @@
# 7-33 [done][bug] markdown_edit 全部 search 失败仍返回成功 v1
> 发现时间:2026-05-18
>
> 状态:`[done]`
>
> 关联主线:`07-ai`
## 1. 问题定义
`mnote.doc.markdown_edit` 逐条执行 search/replace 时,如果所有 search 都失败,旧实现仍会继续进入写回阶段,并返回:
```json
{"ok": true, "operationsApplied": 0}
```
这会让页面 AI fast workflow 把“没有改动”误报为编辑成功。
## 2. 修复
-`applied == 0` 时立即返回 `mnote_markdown_edit_no_operations_applied`
- 不再执行本地文件写入或在线 `page.body.save`
- 新增回归测试 `hermes_tools_markdown_edit_rejects_no_applied_operations`
## 3. 验证
RED
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_rejects_no_applied_operations -- --nocapture
```
旧实现返回 `200`
GREEN
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web hermes_tools_markdown_edit_rejects_no_applied_operations -- --nocapture
```
结果:`1 passed`
@@ -0,0 +1,45 @@
# 7-34 [done][bug] 页面 AI ACP Runtime 默认值回跳 Hermes v1
> 发现时间:2026-05-18
>
> 状态:`[done]`
>
> 关联主线:`07-ai`
## 1. 用户可见症状
页面 AI 面板中选择 `ACP · Reasonix` 后,会自动回跳到 `ACP · Hermes`
用户期望:
- 默认 runtime 为 `ACP · Reasonix`
- 下拉仍可手动切换到 `ACP · Hermes`
## 2. 根因
页面 AI 的历史 runtime 语义仍把空 `acpRuntime` 当成旧 `Hermes HTTP` 默认值。当前主线已经退役 HTTP Hermes,但前端 session/localStorage 恢复、后端空 runtime 推导、以及 `/profiles` 尚未返回时的下拉渲染仍可能产生空 runtime。
空 runtime 进入旧逻辑后会被解释成 Hermes 路径,导致用户刚选择 Reasonix 后又被历史 session 或空 select value 覆盖。
## 3. 修复
- 前端 `pageAiAcpRuntime` 初始值改为 `reasonix`
- localStorage 版本提升到 `3`,避免旧缓存把 active runtime 覆盖回 Hermes。
- session 创建、持久化、停止 run、发送 run 默认都写入 `reasonix`
- 前端内置 `Reasonix / Hermes` 两个 ACP runtime fallback`/profiles` 慢或失败时也不会渲染空 select。
- 后端空 `acpRuntime` 默认归入 ACP 默认 runtime,默认值为 `reasonix`;旧 HTTP proxy 只保留在显式兼容开关后。
## 4. 验证
新增回归约束:
```bash
cargo test --manifest-path rust/Cargo.toml -p mnote-web page_ai_acp_runtime_defaults_to_reasonix_and_keeps_hermes_switch -- --nocapture
```
覆盖:
- `pageAiAcpRuntime` 默认是 `reasonix`
- 下拉 options 固定包含 `reasonix` / `hermes`
- 不再包含旧的 `默认 (Hermes HTTP)` 空选项
- change handler 空值 fallback 到 `reasonix`
@@ -59,6 +59,11 @@
- `bugs/05-editor-mainline/done/5-16-sidebar-preferred-snapshot-query-overrides-live-stream-v1.md`
- `bugs/05-editor-mainline/done/5-17-document-title-source-drift-live-sidebar-over-head-v1.md`
- `bugs/05-editor-mainline/done/5-18-ai-write-body-does-not-sync-page-aggregate-content-v1.md`
- `bugs/05-editor-mainline/done/5-19-page-aggregate-prefers-editor-document-v1.md`
- `bugs/05-editor-mainline/done/5-20-page-body-save-strips-editor-document-before-convex-v1.md`
- `bugs/05-editor-mainline/done/5-21-page-body-save-rejects-invalid-editor-document-v1.md`
- `bugs/05-editor-mainline/done/5-22-documents-content-strips-editor-document-before-page-aggregate-v1.md`
- `bugs/05-editor-mainline/done/5-23-editor-document-semantic-validation-v1.md`
- `bugs/05-editor-mainline/done/5-9-c12-topbar-sidebar-toggle-noop-v1.md`
收口结果:
@@ -66,6 +71,8 @@
- Page Aggregate 标题、正文、页面设置、page tree、AI fetch 回读已经形成同一组可验证真相。
- AI 外部写入后,文档 shell 会同步 Page Aggregate JSON script / session snapshot,避免下一次 AI context 继续读旧正文。
- Sidebar preferred snapshot 与 live stream 的优先级漂移已从当前 P0 blocker 中移除。
- Page Aggregate 读侧已从 route 主链保留 `editorDocument / blockDocument`,不再在 `documents.content.get` 结果中提前剥离原生块文档字段。
- `editorDocument` 显式输入已补语义校验:必须带 `blocks`,且 `rootBlockIds` 必须引用真实块。
关键证据:
@@ -92,6 +99,9 @@
- `bugs/07-ai/done/7-28-markdown-edit-selection-scope-not-enforced-v1.md`
- `bugs/07-ai/done/7-29-markdown-edit-flattens-inline-content-v1.md`
- `bugs/07-ai/done/7-30-block-tools-selection-scope-not-enforced-v1.md`
- `bugs/07-ai/done/7-31-page-ai-workflow-drops-read-summary-v1.md`
- `bugs/07-ai/done/7-32-markdown-edit-page-body-save-missing-preconditions-v1.md`
- `bugs/07-ai/done/7-33-markdown-edit-no-applied-operations-success-v1.md`
收口结果:
@@ -105,6 +115,9 @@
- `mnote.doc.markdown_edit` / `page_ai_workflow` 已补 selection scope 服务端兜底,`allowedTargetBlockIds` 外的写入会被拒绝。
- `mnote.doc.markdown_edit` 写回普通块时不再无条件扁平化 inline content;未变更块保持原结构,单 inline text 节点的已变更块保留 marks / attrs。
- 单个 `mnote.block.*` 写工具已补 selection scope 服务端兜底,越界块写入会返回 `mnote_block_target_out_of_scope`
- 页面 AI `block_edit_workflow` 现在会把模型 `summary` 返回给前端;“读取第一段 + 修改第二段”这类读写混合请求不再被固定写入文案吞掉读取结果。
- 在线 `mnote.doc.markdown_edit` 直接 `page.body.save` 写回时已携带当前 `revision/conflictDetectionKey`
- `mnote.doc.markdown_edit` 全部 search 失败时返回 `mnote_markdown_edit_no_operations_applied`,不再误报成功或触发无意义保存。
关键证据: