# 5-6 [process] Page Aggregate 单一真源对齐执行清单 v1 > 更新时间:2026-05-09 > > 关联文档: > - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-5-page-aggregate-single-truth-alignment-v1.md` > - `/mnt/Data1T/mnote/design/05-editor-mainline/process/5-4-leptos-tiptap-mainline-correction-v1.md` > - `/mnt/Data1T/mnote/design/05-editor-mainline/done/5-5-1-page-aggregate-contract-v1.md` > - `/mnt/Data1T/mnote/design/04-tree-domain/done/4-4-tree-projection-protocol-contract-v1.md` ## 1. 文档目的 这份清单不是再讨论“问题是不是存在”,而是把: - `5-5` 里对当前混合态的判断 - 后续 `Page Aggregate` 主线 整理成一份能持续勾选、持续验收、持续防止跑偏的执行清单。 这份清单只围绕一个目标: > **让页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区 / AI 写入口,逐步收口到 Rust 主导的 page aggregate 单一真源。** --- ## 2. 当前阶段结论 当前可以确认的事实有两类。 ### 2.1 已经成立的事实 - [x] `leptos-tiptap` 已经成为页面内正式主编辑区,不再是 iframe bridge。 - [x] `/api/documents/page` 已优先消费 Rust `mnote.page_aggregate.v1` snapshot,TS builder 退为 fallback。 - [x] 正文保存已经能按 `workspaceId/documentId` 正确落到对应页面。 - [x] 当前主编辑区已具备可继续推进的基础交互能力。 - [x] 当前问题已经不再是“能不能接入主编辑器”,而是“接入后如何收口为单一真源”。 ### 2.2 还没有成立的事实 - [ ] 页面树 / 文件树 / 页面头部 / 页面设置 / 主编辑区还没有消费同一份 page aggregate projection。 - [x] 标题 / 正文 / 页面设置已经开始统一到同一组 page aggregate command family。 - [ ] `pageOptions` 还没有整体收口到 `leptos-tiptap` island 的正式运行时语义层。 - [ ] AI 写入口还没有完整对齐 page aggregate command family。 补充:本轮已新增前端统一 `page-command-client`,并把 `DocumentContent` 的标题 / 页面设置写入、AI 正文写回、以及 `BlockNote` / `leptos-tiptap` 各 host 的正文保存统一到 `page.head.updateTitle / page.layout.updateOptions / page.body.save`。同时,Next route 侧已新增统一 `page-write-command-adapter`,`/api/documents/title`、`/api/documents/options`、`/api/documents/save` 三条页面写链路已开始共用同一层执行器。这里勾选的是“命令执行面开始收口”,不等于页面域真相链已经完全统一。 补充:2026-04-28 树域剩余 runtime 收口中,`page.body.saved` 与 `document.snapshot.saved` 已完成职责拆分;`tree.node.embed` 的 `pageReference` block 结构与插入位置已由 Rust `pageAggregateEmbedPlan` 生成,3000 route 只保留源/目标内容读取作为 substrate preflight。这使正文保存快照语义和页面嵌入正文 patch 都开始落在 Page Aggregate / Rust artifact 边界内。 --- ## 3. 当前明确不做什么 为了避免再次回到“零散补 UI”的老路,下面这些项当前不进入主线前排: - [ ] 不把当前问题重新降级成“修两个样式 bug”。 - [ ] 不继续通过新增前端本地状态来掩盖 page aggregate 缺失。 - [ ] 不先重做一轮页面设置 UI。 - [ ] 不先扩一批和 page aggregate 无关的编辑器花活。 - [ ] 不先争论替换 `Tiptap`、替换 `leptos-tiptap`、移除 Convex。 --- ## 4. Phase F:冻结 Page Aggregate Contract 目标: > **先把页面聚合的 canonical contract 写清楚,阻止页面头部、正文、设置、子树继续各自长字段。** ### 4.1 设计层完成标准 - [x] 单独补一份 page aggregate contract 文档。 - [x] 文档中固定 `page_identity / page_head / page_layout / page_body / page_tree` 的最小字段。 - [x] 文档中固定哪些字段属于 page aggregate truth,哪些字段只是前端 UI state。 - [x] 文档中固定哪些字段必须由 Rust 主导,哪些字段允许作为前端临时态存在。 ### 4.2 代码层完成标准 - [x] 前端与 Rust 侧都出现统一命名的 page aggregate 类型或等价契约。 - [x] 不再继续给 `DocumentPageProps`、`DocumentContentProps` 零散加字段来扩页面真相。 - [x] 文档页加载入口能明确区分“聚合读取结果”和“局部 UI 临时态”。 补充:当前已新增 `page-aggregate-builder.ts`、`page-aggregate-loader.ts` 与 `/api/documents/page`,文档页 SSR 入口和 `DocumentContent` 的内容重试补拉都已改为消费同一份 `PageAggregateProjection`,不再由 `page.tsx` 手工拼 `meta + content`。Rust 侧当前已直接暴露 `/api/page-aggregate/:id` 与 `mnote.page_aggregate.v1` snapshot;`page-aggregate-loader.ts` 会先校验并消费这条 Rust 读链,只有 snapshot 不可用或不可信时才回退到 TS builder。与此同时,`storage-convex-bridge` 与 `bridge-runtime` 已开始接受 `page.head.updateTitle / page.layout.updateOptions / page.body.save` 这组 page command family 的命名口径,因此这里按“读取契约已进入 Rust-first,写入命令面已开始收口”勾选完成。 ### 4.3 退出标准 - [x] 后续再讨论标题、页面设置、正文保存时,能够直接定位到它属于 `page_head / page_layout / page_body / page_tree` 的哪一层。 --- ## 5. Phase G:主文档页改为消费统一聚合 目标: > **让 `/documents/[id]` 不再手工拼 `title + options + content + pageSubtree`,而是消费统一页面聚合。** ### 5.1 加载链路收口 - [x] 页面 SSR / route 入口改为优先读取统一 page aggregate,而不是分别读取 meta 和 content。 - [x] `DocumentShell` / `DocumentContent` 的入参改为围绕统一聚合对象组织。 - [x] 页面头部、页面设置初值、主编辑区 bootstrap、TOC/outline 都来自同一份聚合结果。 ### 5.2 当前散落 props 的收口 - [x] `title` 不再作为独立真相字段散落传递。 - [x] `initialOptions` 不再作为独立真相字段散落传递。 - [x] `initialContent`、`initialRevision`、`initialConflictDetectionKey` 归入统一 `page_body`。 - [x] `initialPageSubtree` 归入统一 `page_tree`,并明确其是正式 projection 还是仅兼容过渡项。 ### 5.3 退出标准 - [x] 页面加载时不再能明显看出“这是几份子结果拼出来的页面”。 - [ ] 后续页面新增字段时,不再需要继续向外层 props 链同时塞多种局部真相。 补充:当前首屏 SSR 与客户端内容重试补拉都已经走 `/api/documents/page -> page aggregate loader -> PageAggregateProjection`,因此“页面明显由 `meta + content` 两次查询拼起来”的入口级痕迹已经消失;但新增字段仍可能需要继续补 loader / route / island 消费链,所以第二项继续保留未完成。 补充:本轮已新增 `page-aggregate-client-state.ts`,并让 `DocumentContent` 把原先分散维护的 `options / content / serverContentSnapshot / serverPageSubtreeSnapshot / serverPageSubtreeTitle / contentRevision / conflictDetectionKey` 开始收口为同一份 client aggregate state reducer。这里代表“页面本地 `body/layout/tree` 真相已经开始统一”,不等于标题链、页面设置运行时分类、AI 对页面设置面的正式入口都已闭环,因此本阶段不提前宣称聚合完成。对应最小回归测试为 `page-aggregate-client-state.test.ts`。 --- ## 6. Phase H:让 `pageOptions` 真正进入 `leptos-tiptap` island 目标: > **把当前“能展示、能保存、但主编辑区不真正消费”的页面设置,按优先级接入 island。** ### 6.1 必须先分类 - [x] 把当前页面设置分成:页面壳布局类、阅读视图类、编辑器 runtime 语义类。 - [ ] 明确哪些项应继续保留在页面设置面板中,哪些项应降级或隐藏,哪些项必须进入 island。 补充:本轮已新增 `page-option-semantics.ts`,把 `pageOptions` 的运行时语义正式收口为代码契约,不再让这套规则继续散落在 `page-options-sidebar.tsx`、`editor-host-types.ts` 与 `leptos-tiptap-island-editor-host.tsx` 里各写一份。当前至少已经明确: - `wideLayout / smallText / layoutDensity` 属于 `page_shell_layout + editor_runtime` - `showHeadingNumbers` 属于 `read_view + editor_runtime` - `showToc` 属于 `read_view` - `protectEditing / showBlockRefCount` 属于 `editor_runtime`,但当前仍是 `planned` - `embedDefaultBlockId` 已进入 `editor_runtime` 对应最小回归测试为 `page-option-semantics.test.ts`、`leptos-tiptap-island-editor-host.test.tsx`、`page-options-sidebar.test.tsx`。第二项继续保留未完成,因为“哪些项应降级/隐藏/保留”的产品面纠偏还没完全落到 inspector 与 AI tool surface。 ### 6.2 最小接入优先级 - [x] `wideLayout` 真正进入 island 布局语义,而不是只改外层壳宽度。 - [x] `smallText` 真正进入主编辑区排版语义。 - [x] `layoutDensity` 真正进入块间距 / 正文密度语义。 - [x] `showHeadingNumbers` 真正进入 heading 展示语义,或明确标注暂不支持。 - [x] `embedDefaultBlockId` 真正进入主编辑器引用 / 嵌入默认位置逻辑,或明确标注暂不支持。 ### 6.3 页面设置面板纠偏 - [x] 没有真正接入 island 的编辑器语义项,不再继续以“已开启/已关闭”假装正式完成。 - [x] 对未支持项给出显式降级说明,而不是仅保存字段。 - [ ] 让“页面设置有值但编辑器内部没变化”这类状态在产品上消失。 ### 6.4 退出标准 - [ ] inspector 中至少最小优先级项修改后,主编辑区可见结果真实变化。 - [ ] 用户不再需要猜“这个设置到底有没有真正作用到编辑器”。 --- ## 7. Phase I:树域与页面域标题统一 目标: > **rename 后页头、sidebar row、page tree、file tree 使用同一份标题结果。** ### 7.1 标题真相纠偏 - [x] 页面头部标题不再长期由前端 `pageTitle` 本地状态冒充正式真相。 - [x] rename 命令在语义上进入统一 page aggregate command family。 - [x] 树域消费的标题与页面头部消费的标题来自同一份更新后的 projection。 ### 7.2 回归验证 - [x] 当前页重命名后,页头即时更新。 - [x] sidebar 对应节点标题同步更新。 - [x] page tree / file tree 对应节点标题同步更新。 - [x] 刷新后标题一致,不依赖额外手动刷新。 - [x] 切页往返后标题一致,不出现旧快照回闪。 注:当前已在标题持久化成功后广播 `emitDocumentsChanged(documentId)`,并补了 `sidebar-events.test.ts` 覆盖 `documents-changed -> sidebarRefetch` 的刷新链路。 补充:已新增 `usePreferredSidebarSnapshot`,当 `tree stream` 已连上但快照落后、`documents-changed` 触发的 query/refetch 先拿到新标题时,会优先消费更新后的 canonical sidebar snapshot;待 stream 追平后再回到 live stream。对应回归测试为 `use-preferred-sidebar-snapshot.test.tsx`。这修掉了“刷新链发出去了,但 stale stream 仍把 sidebar 标题压回旧值”的一类问题;浏览器层 `sidebar row / page tree / file tree` 真实渲染验收仍待补齐。 补充:已新增 `AppLayoutShell`,把 layout 顶栏 `Breadcrumb` 从 SSR 注入的静态 `documents` 挪到与 `Sidebar` 共享的同一条 live sidebar snapshot 管线;并且 layout shell 会把“已选中的 preferred snapshot”同一对象同时透传给 `Sidebar` 与 `Breadcrumb`,不再各自独立选择。对应回归测试为 `app-layout-shell.test.tsx`。这意味着 breadcrumb / sidebar 现在至少共享同一份工作区树 canonical snapshot,不再是 layout 一条静态链、sidebar 一条 live 链并行。页头 `page.head.title` 与这条工作区树链之间的最终统一验收仍待补齐,因此本阶段继续不提前打满。 补充:已新增 `PreferredSidebarSnapshotProvider` 与 `usePageHeadTitle`,把文档页头标题从 `DocumentContent` 内部长期持有的 `pageTitle` 本地真相,改为“同一份 preferred sidebar snapshot 的 committed title + 短暂 draft”。同时修正 `useSidebarData.refetch()`,在 Convex live 模式下收到 `documents-changed` 也会主动拉取一份新的 `/api/sidebar` snapshot,再与 tree stream 做 freshness 选择,避免“页头草稿是新的,但 breadcrumb / sidebar / page tree / file tree 还卡在旧快照”。对应单测为 `use-page-head-title.test.tsx`、`use-sidebar-data.test.tsx`。浏览器烟测 `scripts/task110-page-title-single-truth-smoke.js` 已验证:页头重命名后,breadcrumb、默认 sidebar、page tree、file tree、切页往返、刷新均保持一致,因此本阶段与“标题来自同一份更新后的 projection”相关的勾选正式保留。 ### 7.3 退出标准 - [x] 标题不同步问题不再以“前端本地状态漂移”的形式重复出现。 --- ## 8. Phase J:AI 写入口对齐 Page Aggregate 目标: > **AI 不再绕过 page aggregate,直接拼前端对象或编辑器内部格式。** ### 8.1 语义边界 - [x] 明确 AI 写页面时优先进入哪组 page aggregate command。 - [x] 明确 AI 写标题、写正文、改页面设置、插入引用时分别走哪些统一命令语义。 - [x] 明确 AI 不直接拼 DOM、不直接拼前端页面壳对象、不直接依赖浏览器临时状态。 补充:当前页面命令名已开始从 `documents.*` 收口到 `page.head.updateTitle / page.layout.updateOptions / page.body.save`,其中标题 route、页面设置 route、正文保存 route 都已切到这组 page command family,`storage-convex-bridge` 与 `bridge-runtime` 也已补齐对应 alias。 补充:本轮进一步把前端调用面与 Next route 执行面也开始统一到这组 `page.*` family:前端已新增 `page-command-client`,不再让 `DocumentContent`、AI 面板、各编辑器 host 各自维护一套页面写入口;服务端已新增 `page-write-command-adapter`,`title/options/save` 三条 route 不再分裂在 `metadata/save` 两套执行器里。对应最小回归测试为 `page-command-client.test.ts`、`page-write-command-adapter.test.ts`、`route-adapters.test.ts`、`DocumentAiAgentPanel.runtime.test.tsx`、`document-content.test.ts`。 补充:本轮继续把 AI 面板的读取边界开始收口到统一页面本地快照:`DocumentContent` 不再向 `DocumentAiAgentPanel` 透传 `getLatestBlocks / getLatestPageSubtree / getLatestPersistedMeta` 三组分散 getter,而是改为一份 `getLatestPageAggregateSnapshot`,由 `page-aggregate-client-state` 导出 `blocks / pageSubtree / persistedMeta / pageOptions`。这代表 AI 面板已开始消费同一份页面本地 aggregate state,而不是继续拼接独立局部真相;但页面设置写工具本身仍未进入 Hermes 正式 tool surface,因此这里仍然只算“开始收口”,不提前打满。 补充:本轮还把 `pageOptions` 与 `editorRuntimePageOptions` 一并带入 `/api/ai-agent/run -> buildHermesInstructions`,因此 AI 在服务端至少能看到“当前页面设置是什么”以及“哪些设置已经进入 island runtime payload”,不再只依赖正文块快照和树快照来猜测页面语义。对应回归测试为 `src/app/api/ai-agent/run/route.test.ts`。这推进的是 AI 读侧上下文,不等于页面设置写工具已经具备正式入口。 ### 8.2 与主编辑区的关系 - [x] 人类编辑与 AI 编辑共享同一块语义边界。 - [x] AI 改写结果能通过主编辑区 island 正式回显。 - [ ] AI 改写后树标题 / 页面头部 / 页面设置不再走各自独立副作用链。 ### 8.3 退出标准 - [x] AI 写入口已经可以被明确描述为“操作 page aggregate command family”,而不是“绕过系统写编辑器”。 注:当前 `/api/ai-agent/run` 在 `Hermes tool.completed` 后,会优先尝试把 `slash_run / doc_insert_blocks / doc_replace_range` 恢复成 `mnote-web bridge-runtime` 的结构化 `tool_result`,不再只把 Hermes 事件当作薄日志。其后: - `doc_insert_blocks / doc_replace_range` 继续按 `page.body.save` 语义落到 `/api/documents/save`,再正式回显主编辑区 island。 - `slash_run(rename current page)` 会把结构化结果回接到当前页 `DocumentContent` 的同一条标题提交链,并继续广播 `emitDocumentsChanged(documentId)`,因此页头标题与树标题不再靠 AI 面板内部本地状态各自漂移。 - 当前 `pageOptions` 仍没有进入 Hermes 正式 tool surface,因此“页面设置类 AI 命令”尚未收口;`8.2` 的最后一项继续保留未完成,避免误判为整条线已经闭环。 --- ## 9. 推荐实施顺序 当前推荐顺序固定为: 1. `Phase F` 2. `Phase G` 3. `Phase H` 4. `Phase I` 5. `Phase J` 约束如下: - [x] 没有完成 `Phase F` 之前,不再继续零散补标题 / 宽度 / 页面设置。 - [ ] 没有完成 `Phase G` 之前,不把当前文档页描述成“已经统一聚合完成”。 - [ ] 没有完成 `Phase H` 之前,不把页面设置大量打钩为“正式可用”。 - [ ] 没有完成 `Phase I` 之前,不把标题问题视为已从底层解决。 - [ ] 没有完成 `Phase J` 之前,不把 AI 直接写主编辑区描述成已经具备正式稳定入口。 --- ## 10. 当前阶段的总退出标准 只有当下面这些条件同时成立时,才可以把这条线描述成: > **“主编辑区与树域已经基本统一到 Rust 主导的 page aggregate 单一真源架构”。** - [x] 文档页消费统一 `page aggregate projection` - [ ] 标题 / 正文 / 页面设置不再是三条分裂真相链 - [x] `leptos-tiptap` island 真正消费 editor-related `page_layout` - [x] 树标题与页头标题来自同一份 projection - [x] AI 写入口开始围绕 page aggregate command family 实现 补充:当前未勾选“标题 / 正文 / 页面设置不再是三条分裂真相链”的原因,不再只是命令名或 route 分裂。虽然前端页面写入口、Next route 执行器、`DocumentContent` 内部的 `body/layout/tree` client state、以及 `pageOptions` 的代码级运行时分类都已经开始收口,但 projection 回流与 AI 对页面设置面的正式写入口仍未完全统一。也就是说,命令执行面、页面本地状态面、页面设置语义面都已开始统一,但页面域单一真源仍未闭环。 在此之前,正确口径都应保持为: > **`leptos-tiptap` 主编辑区已基本可用,但页面聚合仍未收口,当前仍处于从混合态向 Rust 单一真源过渡的过程中。**