Files
mnote/design/03-rust-web/process/3-19-rust-web-browser-runtime-module-extraction-v1.md
T

715 lines
38 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# 3-19 Rust Web 浏览器 Runtime 模块外置化重构 v1
> 创建时间:2026-05-24
> 状态:`process`
>
> 触发背景:
> - CodeGraph 对 `mnote-web` 前端内嵌 JS 的符号定位效果一般。
> - 字面搜索命中太多,尤其是 `layout.rs` / `tree.rs` 内的大型 raw string。
> - 需要判断是更换 graph 工具,还是调整源码组织方式。
>
> 参考实现:
> - `/mnt/Data1T/mnote/reference-code/sidex-main/src/vs/workbench/contrib/files/browser/views/explorerView.ts`
> - `/mnt/Data1T/mnote/reference-code/sidex-main/src/vs/workbench/contrib/files/browser/views/explorerViewer.ts`
> - `/mnt/Data1T/mnote/reference-code/sidex-main/src/vs/workbench/contrib/files/browser/explorerService.ts`
> - `/mnt/Data1T/mnote/reference-code/sidex-main/src/vs/workbench/contrib/files/browser/fileActions.contribution.ts`
## 1. 结论
当前不应优先更换 CodeGraph。主要问题不是 graph 项目选型,而是 `mnote-web` 的大量浏览器 runtime 仍作为 Rust raw string / HTML inline script 存在。对 CodeGraph、IDE、ast-grep、浏览器 stack trace 来说,这些逻辑首先是 Rust 字符串,不是一等 JavaScript / TypeScript 源文件。
短期可以做“内嵌 JS 提取索引”作为定位辅助,但它只解决查询体验,不改变维护边界。长期重构方向应是:保留 Rust SSR / projection / reducer 的语义主导权,把浏览器端 DOM 绑定、host bridge、transport controller、DND、context menu、resource open adapter 等 runtime 分阶段迁出 Rust raw string,变成可索引、可测试、可复用的 `.js``.ts` 模块。
补充结论(2026-05-24):最近 `bugs/0524.md` 的修复耗时说明,低效并不只发生在 `layout.rs` 的 tree/filetree runtime,也发生在 `web_shell.rs` 的文档页 host adapter 和 `leptos-tiptap-spike/src/lib.rs` 的编辑器 runtime 巨型文件。本设计只覆盖 `03-rust-web` 的浏览器 shell runtime 外置化;`leptos-tiptap` 编辑器命令、附件、history、DOM bridge 需要并行进入 `05-editor-mainline` 设计,不应塞进本设计里一起实施。
## 2. 当前代码基线
### 2.1 主壳内嵌 runtime
- `rust/crates/mnote-web/src/ssr/pages/layout.rs`
- `SIDEBAR_TREE_JS` 从文件顶部开始,是当前主壳最大浏览器 runtime。
- 职责混合了:侧栏宽度、工作区切换、page tree / file tree 事件、文件树选择、拖拽、资源打开、附件上传、Page AI panel、账号菜单、本地文件夹 watcher。
- `TREE_LIVE_CONTROLLER_JS` 负责 tree live bootstrap、WS / SSE / local-folder event transport、snapshot / delta / resync 分发。
- `PageLayout` 通过 `<script inner_html={...}>` 直接注入这些 runtime。
### 2.2 Debug tree shell 内嵌 runtime
- `rust/crates/mnote-web/src/routes/tree.rs`
- `build_tree_shell_html()` 内嵌整页 HTML、CSS、JS。
- JS 负责 page/filetree/picker 模式、DOM render、keyboard、DND、context menu、runtime reduce endpoint 调用、local fallback。
- `/api/tree/runtime/reduce` 已存在,入口为 `reduce_tree_shell_runtime()`
### 2.3 已经存在的 Rust 侧良好边界
- `rust/crates/mnote-web/src/tree_shell/runtime_api.rs`
- 已定义 `TreeShellRuntimeRequest``TreeShellRuntimeResult``TreeShellDomPatch``TreeShellHostEvent``TreeShellCommandEvent`
- `rust/crates/mnote-web/src/tree_shell/filetree_selection.rs`
- 已定义文件树选择 reducer contract。
- `rust/crates/mnote-web/src/tree_shell/filetree_renderer.rs` / `page_renderer.rs`
- 已有初始 HTML renderer 与测试。
这说明重构不需要推翻 Rust 主线。相反,应该把现有 Rust reducer / renderer 合同作为稳定边界,让浏览器 runtime 外置后继续消费这些合同。
### 2.4 需要另线处理的编辑器 runtime 巨型文件
- `rust/spikes/leptos-tiptap-spike/src/lib.rs`
- 约一万行,虽然是 Rust 源码而不是 raw string,但混合了 Tiptap bridge、编辑器命令、附件链接增强、上传、history、mindmap node view、DOM overlay、page option、AI surface。
- CodeGraph 能索引 Rust 符号,但单文件职责过大,worker 仍会反复字面搜索和误判边界。
- 该文件不属于 `03-rust-web` shell runtime 外置化范围,应由 `05-editor-mainline` 拆出编辑器命令和附件/history 相关模块。
## 3. Sidex / VSCode Explorer 对照结论
Sidex 参考实现的关键价值不是具体 UI,而是模块边界:
- `ExplorerView`:视图容器,持有 tree、renderer、context key、focus、layout。
- `ExplorerService`:模型、文件变更、refresh、cut/copy 状态、view 注册。
- `FileDragAndDrop`DND 独立类,包含 `onDragOver``drop``getDragURI``onDragStart`
- `fileActions.contribution.ts`:命令和快捷键独立注册,通过 context condition 控制可用性。
- `IExplorerView` / `IExplorerService`:视图与服务之间通过接口通信。
MNote 不应照搬 VSCode 的完整 DI / Workbench 架构;当前产品主线也不是 BlockNote-first 或 VSCode clone。但应采用同一类边界原则:
- 视图绑定与 DOM patch 是浏览器 runtime 职责。
- 树语义、资源归属、projection、command 语义仍由 Rust kernel / mnote-web reducer 主导。
- DND、context menu、keyboard、transport、resource open adapter 不应继续混在单个全局 IIFE 中。
## 4. 目标
- 让主壳浏览器 runtime 成为一等源文件,能被 CodeGraph / IDE / ast-grep / lint 正常解析。
-`layout.rs` 回到 SSR layout 与 bootstrap 注入职责,不再承载万行浏览器行为。
-`/tree` debug shell 与主壳 sidebar 复用同一套 tree/filetree runtime 模块,减少两套行为漂移。
- 保留 Rust `tree_shell` reducer / renderer / contract 作为语义边界,不把树真相迁到前端。
- 改善浏览器调试:错误堆栈能落到稳定 JS 模块文件和行号。
- 为后续 tree live cache、filetree DND、resource tab、local folder watcher 的继续收口降低定位成本。
## 5. 非目标
- 不更换 CodeGraph。
- 不引入完整 React / Next / BlockNote 主链。
- 不把 Rust kernel / Rust projection 的语义迁回前端。
- 不一次性重写整个 sidebar。
- 不在本阶段改变现有 UI 形态、Wolai 对齐目标、tree command 协议或 resource object identity 合同。
- 不把 debug `/tree` 重新提升为默认产品入口;它仍是显式 debug/internal 边界。
## 6. 方案选择
### 6.1 方案 A:只做内嵌 JS 提取索引
做一个脚本从 Rust raw string 中提取 `<script>` / `*_JS` 常量为临时 `.js` 文件,并带回源文件行号映射。
优点:
- 改动小。
- 对定位问题见效快。
- 不影响 runtime。
缺点:
- 只是辅助索引,真实维护边界仍然是 Rust 字符串。
- 浏览器 stack trace、代码 review、模块复用、单测边界不会根本改善。
- 提取器会成为额外维护成本。
### 6.2 方案 B:直接整体重写 sidebar runtime
一次性把 `SIDEBAR_TREE_JS` 全部迁出并重写成新模块体系。
优点:
- 最终形态干净。
- 可一次性消除万行 inline script。
缺点:
- 风险过大。
- 当前 sidebar 混合了 tree、资源、AI、账号、本地文件夹、多 pane 等多个活跃主线,整体重写容易引入回归。
- smoke 覆盖面需要非常大,不适合作为第一阶段。
### 6.3 推荐方案 C:分阶段外置化,先提取边界清楚的 runtime
先从职责边界最清晰、状态输入最明确、验证最容易的模块开始迁出。每个阶段都保持旧行为,完成后跑对应 smoke,再进入下一阶段。
推荐顺序:
1. resource open / local upload / attachment action adapter
2. `TREE_LIVE_CONTROLLER_JS`
3. debug `/tree` shell runtime 的 bridge 层
4. 主壳 filetree selection / keyboard / DND bridge
5. Page AI panel 等低相关大块 runtime 另立后续设计,不混入本次 tree runtime 重构
推荐理由:
- 每步可独立验收。
- 不破坏当前 Rust reducer / projection 主线。
- 能最快验证 CodeGraph 定位改善。
- 便于回滚单一模块,而不是回滚整个 sidebar。
- 最近缺陷集中在上传目标、附件打开、secondary pane、资源 tab、OnlyOffice/PDF/code 打开路径。先拆 resource/local-upload adapter,比先拆 tree live 更能直接降低后续 bug 修复成本。
- `TREE_LIVE_CONTROLLER_JS` 仍适合作为低风险模板切片:边界清楚、smoke 现成、能验证 asset route 和 ES module 注入方式。
## 7. 目标架构
### 7.1 文件布局建议
新增浏览器 runtime 源文件目录:
```text
rust/crates/mnote-web/browser/
resource-open-runtime.js
local-upload-runtime.js
tree-live-controller.js
tree-shell-runtime.js
sidebar-tree-runtime.js
filetree-selection-runtime.js
filetree-dnd-runtime.js
filetree-context-menu-runtime.js
local-folder-events-runtime.js
```
如后续决定引入 TypeScript,可迁移为:
```text
rust/crates/mnote-web/browser/src/
rust/crates/mnote-web/browser/dist/
```
第一阶段建议先用 plain ES module `.js`,避免为了重构引入额外 build chain。待模块边界稳定后,再评估是否升级 TypeScript。
### 7.2 Rust 注入边界
Rust 继续负责:
- SSR HTML layout。
- bootstrap JSON script,例如 `__MNOTE_TREE_LIVE_BOOTSTRAP__`
- initial page tree / file tree HTML。
- `/api/tree/runtime/reduce`
- `/api/tree/commands`
- `/api/realtime/ws``/api/tree/events``/api/local-folder/events`
- mnote-web 静态 runtime asset 路由。
Rust 不再负责:
- 在 raw string 中维护大块浏览器行为。
- 在 inline script 中拼出完整 DOM runtime。
-`layout.rs` 中混合 sidebar、AI、resource、tree live 的所有事件处理。
-`web_shell.rs` 中继续堆叠资源打开和 pane host adapter 的长期浏览器逻辑;文档页 host adapter 应逐步迁到外置模块,但编辑器内部 Tiptap 命令仍归 `05-editor-mainline`
### 7.3 浏览器模块职责
浏览器 runtime 负责:
- 读取 bootstrap JSON。
- 绑定 DOM event。
- 收集 DOM 当前可见状态,构造 reducer request。
- 调用 Rust runtime reduce endpoint。
- 应用 `domPatches`
- 执行 `hostEvents`,例如 open page、open resource、context menu、upload files。
- 连接 tree live transport 并向页面分发 `tree:snapshot` / `tree:delta` / `tree:resync`
浏览器 runtime 不负责:
- 决定页面/资源父子真相。
- 决定资源归属。
- 决定 tree command 语义。
- 维护第二套 file tree projection。
## 8. 合同草案
### 8.1 Runtime asset manifest
新增或复用类似 `leptos_tiptap_manifest()` 的 runtime manifest
```json
{
"schema": "mnote.browser_runtime_manifest.v1",
"assets": {
"treeLiveController": "/api/mnote-browser-runtime/tree-live-controller.js",
"sidebarTreeRuntime": "/api/mnote-browser-runtime/sidebar-tree-runtime.js",
"treeShellRuntime": "/api/mnote-browser-runtime/tree-shell-runtime.js"
}
}
```
### 8.2 Tree live bootstrap
沿用当前 `mnote.tree_live_bootstrap.v1`,但由外置模块读取:
```json
{
"schema": "mnote.tree_live_bootstrap.v1",
"transport": "convex-command-log-ws",
"endpoint": "/api/tree/events",
"wsEndpoint": "/api/realtime/ws",
"views": ["page-tree", "file-tree"]
}
```
### 8.3 Host bridge
主壳提供最小 host bridge
```js
window.__mnoteTreeHost = {
openPage(detail) {},
openResource(detail) {},
dispatchCommand(command) {},
applyTreeSnapshot(payload) {},
applyTreeDelta(payload) {},
reportRuntimeError(error, context) {}
};
```
该 bridge 只是浏览器适配层,不是树真相。
### 8.4 Worker 任务切片合同
外置模块后,ClaudeCode / Reasonix 任务书必须优先指向单一模块和单一 smoke,不再让 worker 直接搜索整个 `layout.rs` / `web_shell.rs`
```text
允许读取:
- rust/crates/mnote-web/browser/resource-open-runtime.js
- rust/crates/mnote-web/src/ssr/pages/layout.rs 中 module bootstrap 小段
- scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
禁止:
- 重写 sidebar 全局 IIFE
- 修改 Rust tree command 语义
- 修改不相关 Page AI panel
```
采纳 worker diff 的最低条件:
- diff 落在指定模块和测试内。
- handoff 说明调用链、未覆盖边界和验证命令。
- 主控能用 CodeGraph / `rg` 直接定位被修改符号。
- 浏览器 smoke 或截图证据可复核。
## 9. 执行策略
本设计按 Codex 主控、Reasonix 短跑 worker 协同执行:
- Codex 负责设计口径、模块边界、任务拆分、最终 diff 复核、测试和提交。
- Reasonix 只承接窄任务,每个 run 必须有任务书、允许文件、禁止事项、输出表格和 handoff。
- 同时最多启动 2 个 Reasonix worker;每批结束后 Codex 先读取 `process-handoff.md/json` 和 Hindsight `reasonix` recall,再决定是否采纳。
- 每个 worker 完成或超时后都应结束该 run,不在同一个长会话里连续追加不相关任务。
- Batch 0 只做只读审计,不修改文件、不跑 git 写操作,用来生成热点归属表和风险清单。
- Batch 1 起才允许实施单模块搬迁;每个实施 worker 只能修改一个 runtime 模块和对应 smoke。
当前建议批次:
- Batch 0 / R1:只读审计 `resource open / local upload / attachment action adapter`,输出函数归属表、目标模块、风险和 smoke 建议。
- Batch 0 / R2:只读审计 `leptos-tiptap` history / `set_content` 风险,结果写回 `05-editor-mainline` 设计,不混入本设计实现。
- Batch 1:外置 `resource-open-runtime.js` 的最小可验证切片,优先迁出纯 URL builder 和 layout bridge,不一次迁走 `web_shell.rs` 的完整 resource tab/session 生命周期。
- Batch 2:外置 `TREE_LIVE_CONTROLLER_JS`
- Batch 3:按审计结果选择 debug `/tree` bridge 或主壳 filetree runtime。
Batch 0 验收标准:
- R1 能列出 `layout.rs` / `web_shell.rs` 中资源打开、上传、附件动作相关入口和调用位置。
- R1 能给出“先拆哪个模块”的具体建议,并绑定现有 smoke。
- R2 的 editor 侧结论必须只作为跨设计参考;不得要求 3-19 修改 Tiptap 内部命令。
- Codex 必须把可采纳结论整合进本设计或 `5-28`,未采纳项记录原因。
### 9.1 Batch 0 执行记录(2026-05-24
Reasonix R1 run
- run id: `reasonix-2026-05-24T12-38-22-672Z-6ffcfa23`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T12-38-22-672Z-6ffcfa23/process-handoff.md`
- 类型:只读审计,无工作区改动。
- Codex 复核:已抽样用 `rg` 核对关键符号位置;Hindsight `reasonix` bank 可 recall 到该 handoff。
可采纳结论:
- `resource-open-runtime.js` 是 Batch 1 更合适的首个实施目标,但第一刀应只迁 `layout.rs` 中资源打开的纯函数和 bridge,例如:
- `buildOnlyOfficeOpenPath`
- `buildLocalFileOpenUrl`
- `openLocalOfficeFileInActiveTab`
- `openLocalResourceInActiveTab`
- `openConvexAssetFromFileTree`
- `openEditorAttachmentDetail`
- `openEditorAttachmentEditTab`
- `web_shell.rs``openResourceInActiveTab``createResourceTabDom``openTiptapResourceTab``replacePaneDocument``createEditorViewBinding` 等实际属于 resource tab / document pane host 生命周期,风险高,不应塞进 Batch 1 的同一个 worker。
- `local-upload-runtime.js` 是第二个高价值目标;真实入口是 `uploadFileToMediaAsset``insertUploadedAssetIntoEditor` 和 debug shell 的 `executeLocalFileTreeExternalDrop`,不是 `uploadFilesToLocalFolderTarget`
- `TREE_LIVE_CONTROLLER_JS` 仍适合作为 Batch 2:边界清楚,现有 smoke 对 WS/SSE fallback 覆盖更明确。
本轮发现的 worker 协同问题:
- Reasonix 能产出有用的只读审计表,但 runner 在产出 `completed` 结果后底层 `reasonix acp` 进程没有自动退出;主控必须显式检查并清理本轮启动的 worker 进程。
- Hindsight recall 可找到 handoff,但召回结果会混入同日其它 Reasonix 经验;主控仍必须读取本次 `process-handoff.md/json` 原文。
### 9.2 Batch 1 执行记录(2026-05-24
Reasonix implementation run
- run id: `reasonix-2026-05-24T12-49-55-633Z-be2844cb`
- worktree: `/mnt/Data1T/mnote-wt-3-19-resource-open-b1`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T12-49-55-633Z-be2844cb/process-handoff.md`
- Codex 采纳方式:从隔离 worktree 选择性合入主工作区;未提交 worker branch。
已落地的最小切片:
- 新增 `rust/crates/mnote-web/browser/resource-open-runtime.js`
- 新增固定 asset route`GET /api/mnote-browser-runtime/resource-open-runtime.js`
- `PageLayout``SIDEBAR_TREE_JS` 前注入 `type="module"` 外置脚本。
- `SIDEBAR_TREE_JS` 中以下 helper 先做 runtime 代理,保留原实现 fallback
- `buildOnlyOfficeOpenPath`
- `buildLocalFileOpenUrl`
- `localFilePathFromAssetId`
采纳边界:
- 本批次没有迁移 `openConvexAssetFromFileTree``openEditorAttachmentDetail``openEditorAttachmentEditTab` 等复杂行为函数。
- 本批次没有迁移 `web_shell.rs``openResourceInActiveTab`、resource tab DOM、session、pane host 生命周期。
- 固定 route 使用 `include_str!` 暴露单个 JS 文件,不提供动态 path,因此本批次不需要引入通用路径校验。
验证:
```bash
cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo test -p mnote-web sidebar_tree_runtime_opens_office_assets_through_resource_shell
cargo test -p mnote-web sidebar_tree_runtime_opens_pdf_and_code_assets_with_builtin_tools
node --check rust/crates/mnote-web/browser/resource-open-runtime.js
```
全部通过。
### 9.3 Batch 2 执行记录(2026-05-24
Reasonix implementation run
- run id: `reasonix-2026-05-24T13-14-25-162Z-f98927dc`
- worktree: `/mnt/Data1T/mnote-wt-3-19-tree-live-b2`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T13-14-25-162Z-f98927dc/process-handoff.md`
- Codex 采纳方式:读取 Hindsight recall 和 handoff 后,从隔离 worktree 选择性合入主工作区;未提交 worker branch。
已落地的切片:
- 新增 `rust/crates/mnote-web/browser/tree-live-controller.js`
- 新增固定 asset route`GET /api/mnote-browser-runtime/tree-live-controller.js`
- `PageLayout` 将原 inline `TREE_LIVE_CONTROLLER_JS` 替换为 `type="module"` 外置脚本。
- `layout.rs` 不再保留大块 `TREE_LIVE_CONTROLLER_JS` raw string;测试改为 `include_str!` 外置文件内容。
采纳边界:
- 本批次只做同构搬迁,不改 tree live 语义。
- WS 优先、SSE fallback、local-folder events、snapshot/delta/resync/block.delta 分发保持原逻辑。
- 后端 `/api/realtime/ws``/api/tree/events``/api/local-folder/events` 未修改。
验证:
```bash
cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo test -p mnote-web tree_live_controller_marks_transport_and_closes_source_on_pagehide
node --check rust/crates/mnote-web/browser/tree-live-controller.js
```
全部通过。
浏览器轻量验收:
- dev server: `npm run dev:hot`
- 页面:`http://127.0.0.1:3000/`,测试账号快速登录后进入主壳。
- `/api/mnote-browser-runtime/tree-live-controller.js` 返回 `200``content-type=application/javascript; charset=utf-8`
- `document.scripts` 中存在 1 个 tree live module script。
- `window.__mnoteTreeLiveControllerStarted === true`
- `<html>` 上出现 `data-mnote-tree-live-transport="disabled"``data-mnote-tree-live-status="disabled"`,证明外置 module 已执行。
- 控制台仍出现 `/api/local-folder/events` 500,这是既有 local folder event stream 问题,不属于本批次 route/module 加载回归。
CodeGraph 复核:
- 初始外置文件仍包在 IIFE 内,CodeGraph 只能识别外层匿名符号,不能定位 `startWithWebSocket`
- Codex 将外置 module 调整为模块级函数声明,并把启动 guard 放到底部。
- 复核结果:
- `codegraph_search startWithWebSocket` -> `rust/crates/mnote-web/browser/tree-live-controller.js`
- `codegraph_search startWithSseFallback` -> `rust/crates/mnote-web/browser/tree-live-controller.js`
### 9.4 Batch 3 执行记录(2026-05-24
Reasonix implementation run
- run id: `reasonix-2026-05-24T14-22-14-401Z-ea414358`
- worktree: `/mnt/Data1T/mnote-wt-3-19-local-upload-b3`
- 结果:未采纳。该 run 卡在提交 plan 后没有产生工作区 diff,也没有写出 handoff;主控终止残留 runner / acp 进程并删除临时 worktree。
Codex 主控直接完成的最小切片:
- 新增 `rust/crates/mnote-web/browser/local-upload-runtime.js`
- 新增固定 asset route`GET /api/mnote-browser-runtime/local-upload-runtime.js`
- `PageLayout``SIDEBAR_TREE_JS` 前注入 `type="module"` 外置脚本。
- `SIDEBAR_TREE_JS` 中以下上传/附件展示 helper 先做 runtime 代理,保留原实现 fallback
- `uploadedAssetTitle`
- `uploadedAssetUrl`
- `localAssetOpenUrl`
- `uploadedAssetType`
- `fileTreeIconKindForFileName`
- `isLocalUploadedAsset`
- `uploadedAssetExtension`
- `isNonOfficeAttachmentName`
- `attachmentExtensionFromFileName`
- `isPdfAttachmentFileName`
- `isCodeAttachmentFileName`
- `inferCodeAttachmentLanguage`
- `attachmentClassForFileName`
- `uploadedAttachmentClass`
- `uploadedFileSize`
采纳边界:
- 本批次不迁移 `uploadFileToMediaAsset``insertUploadedAssetIntoEditor``uploadFilesWithResolvedTarget` 主流程。
- 本批次不修改 `/api/local-folder/assets/upload` 请求字段、secondary pane 目标选择、resource tab、OnlyOffice、PDF/code 打开逻辑。
验证:
```bash
node --check rust/crates/mnote-web/browser/local-upload-runtime.js
cargo fmt --manifest-path rust/Cargo.toml --all --check
cargo test -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
```
结果:
- `local-upload-runtime.js` route 返回 `200``content-type=application/javascript; charset=utf-8`
- `task479` 最终 `ok=true`,覆盖主编辑区上传目录、filetree folder drop、broken link 保留、无 active document 附件 tab、文件树滚动、标题来源、单附件删除保留相邻附件。
### 9.5 Batch 4 执行记录(2026-05-24
Reasonix read-only audit
- run id: `reasonix-2026-05-24T15-27-18-395Z-12afda31`
- worker worktree: `/mnt/Data1T/mnote-wt-3-19-local-upload-context-b4`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-27-18-395Z-12afda31/process-handoff.md`
- 类型:只读审计,主控已读取 Hindsight recall、`process-handoff.md``process-handoff.json`
采纳内容:
- `local-upload-runtime.js` 新增 `resolveEditorUploadContext``editorRootFromUploadOptions``openEditorUploadFilePicker`
- `SIDEBAR_TREE_JS` 中保留同名 wrapper 和 inline fallback;优先委托 `window.__mnoteLocalUploadRuntime`,模块加载失败时保留旧行为。
- 本批不迁 `uploadFileToMediaAsset``uploadFilesWithResolvedTarget``insertUploadedAssetIntoEditor`。这些函数仍混合 API POST、file tree refresh、Tiptap chain 插入和 OnlyOffice/resource tab 逻辑,后续需要继续拆边界。
- `scripts/task472-side-target-secondary-pane-smoke.js` 补齐 `/api/local-folder/assets/upload` 当前必需的 `uploadIntent=editor.markdown.attach`,让 secondary pane smoke 恢复可运行。
已通过验证:
- `node --check rust/crates/mnote-web/browser/local-upload-runtime.js`
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web local_upload_runtime_contains_editor_upload_context_helpers`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web sidebar_upload_runtime_routes_local_markdown_assets_to_local_folder`
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
- 结果:`ok=true`
- 证据:命令 stdout JSON,覆盖 `editor-upload-to-page-resource-dir``filetree-folder-drop-to-target``broken-link-after-real-file-delete``direct-attach-open-without-active-md``editor-delete-one-attachment-preserves-adjacent` 等检查。
- `node scripts/task472-side-target-secondary-pane-smoke.js`
- 结果:`ok=true`
- 证据:命令 stdout JSON`assetId=local:asset:README/side-target-resource.md``officeAssetId=local:asset:README/side-target-office.docx`
### 9.6 Batch 5 执行记录(2026-05-24
Reasonix read-only audit
- run id: `reasonix-2026-05-24T15-57-06-903Z-51c4452a`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T15-57-06-903Z-51c4452a/process-handoff.md`
- 类型:只读审计,主控已读取 Hindsight recall、`process-handoff.md``process-handoff.json`
- 采纳结论:可采纳;本批是 debug `/tree` shell runtime 同构迁出,未改变 `/api/tree/runtime/reduce` 合同和 page/filetree/picker 行为。
采纳内容:
- 新增 `rust/crates/mnote-web/browser/tree-shell-runtime.js`,从 `tree.rs` 原 inline runtime 同构迁出。
- `build_tree_shell_html()` 保留 `tree-shell-state` bootstrap JSON,改为加载 `/api/mnote-browser-runtime/tree-shell-runtime.js` module script。
- 新增 `/api/mnote-browser-runtime/tree-shell-runtime.js` 路由,返回 `application/javascript; charset=utf-8`
- `tree.rs` 测试继续通过 `include_str!("../../browser/tree-shell-runtime.js")` 校验 tree shell runtime 的 page/filetree/picker 关键字符串。
- 主控复核后额外采纳一个小型结构化 hoist:`buildFileTreeRuntimeEnvironment``reducePageActionWithRuntime``reconcilePageRuntimeResult` 从 IIFE 内部移到文件顶层,通过 `runtimeContext` 显式接收依赖,使 CodeGraph 可以定位这些函数;未改变 runtime API 合同。
已通过验证:
- `node --check rust/crates/mnote-web/browser/tree-shell-runtime.js`
- `cargo fmt --manifest-path rust/Cargo.toml --all --check`
- `cargo check --manifest-path rust/Cargo.toml -p mnote-web`
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_shell`
- 结果:42 个 tree_shell 相关测试通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web runtime`
- 结果:57 个 runtime 相关测试通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree`
- 结果:28 个 filetree 相关测试通过。
- `GET /api/mnote-browser-runtime/tree-shell-runtime.js`
- 结果:`200``content-type=application/javascript; charset=utf-8`,浏览器读取内容包含 `startTreeShellRuntime``tree.ready``hydrateInitialFileTree``reducePageActionWithRuntime``buildFileTreeRuntimeEnvironment``reconcilePageRuntimeResult`
- `codegraph_search reducePageActionWithRuntime` / `buildFileTreeRuntimeEnvironment` / `reconcilePageRuntimeResult`
- 结果:3 个函数均可定位到 `rust/crates/mnote-web/browser/tree-shell-runtime.js`
- 真实 3000 服务运行态说明:
- 本轮直接访问 `/tree?...` 返回 `404`,说明 debug shell 入口当前不作为公开默认入口暴露;运行态 JS asset 加载由固定 asset route 浏览器验证,debug shell page/filetree/picker HTML 行为由 Rust route 单测覆盖。
- 主壳登录后确认 `tree-live-controller.js` 等已外置 runtime 正常挂载,P4 主壳 filetree runtime 仍待后续切片。
剩余风险:
- `type="module"` 自带作用域,`tree-shell-runtime.js` 已移除外层 IIFE,并只 hoist P3 关键函数;其他 DOM runtime 仍按原局部闭包组织,避免扩大改动。
- debug tree shell runtime 与主壳 `SIDEBAR_TREE_JS` 仍是两套 runtime,后续 P4/P5 继续拆主壳 filetree runtime 和 document pane host adapter。
## 10. 分阶段执行清单
### P0:定位辅助与基线保护
- [ ] 记录当前 CodeGraph 对内嵌 JS 的限制,确认不更换 graph 项目。
- [ ] 可选新增 `scripts/extract-embedded-js-for-index.js`,生成临时索引文件,不进入 runtime。
- [ ] 先生成一份“热点函数归属表”,把最近 bug 中高频搜索的函数归到待拆模块:
- `openResource` / `openLocalOfficeFileInActiveTab` / `openEditorAttachmentEditTab` -> `resource-open-runtime.js`
- `executeLocalFileTreeExternalDrop` / `uploadFileToMediaAsset` / `insertUploadedAssetIntoEditor` -> `local-upload-runtime.js`
- `startWithWebSocket` / `startWithSseFallback` -> `tree-live-controller.js`
- `selectFileTreeRow` / `openFileTreeContextMenu` -> `filetree-selection-runtime.js` / `filetree-context-menu-runtime.js`
- `createEditorViewBinding` / `replacePaneDocument` -> `document-pane-host-runtime.js`,归 `03-rust-web``05-editor-mainline` 交界,需单独切片。
- [ ]`layout.rs` / `tree.rs` 中计划迁出的 script 增加最小字符串快照测试,防止迁移时漏挂载。
- [ ] 建立 smoke 基线:
- `node scripts/task487-local-folder-tree-live-consumer-smoke.js`
- `node scripts/task446-tree-rename-dual-browser-live-smoke.js`
- `node scripts/task472-side-target-secondary-pane-smoke.js`
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
### P1:外置 resource open / local upload adapter
- [x] 新增 `rust/crates/mnote-web/browser/resource-open-runtime.js`
- [x] 新增 `rust/crates/mnote-web/browser/local-upload-runtime.js`,或先与 resource open 合并为单个不超过 800 行的 `resource-open-runtime.js`
- [x] 第一刀只迁 `layout.rs` 中 resource open 的纯函数和 bridge`web_shell.rs` 的 resource tab DOM、session、pane host 生命周期保持在后续 `document-pane-host-runtime.js` 批次。
- [ ]`SIDEBAR_TREE_JS` / `web_shell.rs` 中迁出资源打开路径适配,不改变现有 open intent
- page row -> 当前 pane page tab
- filetree doc row -> Markdown page tab
- local Markdown asset -> active resource tab / secondary pane
- pdf/code -> 对应 lightweight viewer
- office -> OnlyOffice path builder
- [ ]`SIDEBAR_TREE_JS` 中迁出本地文件夹上传目标计算和 adapter 调用;资源归属语义仍由 Rust API / kernel 合同决定。
- 2026-05-24 / `reasonix-2026-05-24T15-17-16-430Z-18054487` 只读审计:`leptos-tiptap` 只应发出上传 intent;本地 Markdown 路径、`rootUri``/api/local-folder/files/open` URL、`markdownRelativePath` 与真实上传落盘都归本设计的 shell host runtime。
- 2026-05-24 / Batch 4 已迁:`resolveEditorUploadContext``editorRootFromUploadOptions``openEditorUploadFilePicker`,并保持调用 `uploadFilesWithResolvedTarget`,未同时迁上传主流程。
- 暂不迁:`uploadFileToMediaAsset``uploadFilesWithResolvedTarget``insertUploadedAssetIntoEditor`。这些函数同时涉及 API POST、file tree refresh、Tiptap chain 插入、OnlyOffice/resource tab,需在后续批次继续拆边界。
- [ ] 保持现有 smoke
- `node scripts/task472-side-target-secondary-pane-smoke.js`
- `node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js`
- [ ] 验证 CodeGraph 或等价结构搜索能定位 `openResource``buildOnlyOfficeOpenPath``executeLocalFileTreeExternalDrop`
### P2:外置 `TREE_LIVE_CONTROLLER_JS`
- [x] 新增 `rust/crates/mnote-web/browser/tree-live-controller.js`
- [x] `layout.rs` 只注入 `__MNOTE_TREE_LIVE_BOOTSTRAP__``<script type="module" src="...">`
- [x] 新增 `/api/mnote-browser-runtime/tree-live-controller.js` 路由,返回 `application/javascript; charset=utf-8`
- [x] 保持现有 WS 优先、SSE fallback、local-folder event fallback 行为。
- [x] 浏览器断言:
- `document.documentElement.dataset.mnoteTreeLiveTransport` 正确。
- 外置 module 实际执行并设置 `window.__mnoteTreeLiveControllerStarted`
- [ ] WS 推送仍更新 page/filetree。
- [ ] WS 不可用时仍 fallback 到 SSE。
### P3:外置 debug `/tree` shell runtime bridge
- [x]`tree.rs``tree-shell-state` 后的大块 JS 迁到 `browser/tree-shell-runtime.js`
- [x] `build_tree_shell_html()` 只渲染 HTML、CSS、bootstrap JSON 和 module script。
- [x] 保持 `/api/tree/runtime/reduce` 合同不变。
- [x] debug shell page/filetree/picker 三个模式 smoke 保持通过。
- 2026-05-24`cargo test --manifest-path rust/Cargo.toml -p mnote-web tree_shell` 通过 42 个 tree shell 相关测试;运行态 `/tree` 需 debug route + auth/Convex 或 local workspace 登录态,不作为本批硬门槛。
- [x] 验证 CodeGraph 能定位 `reducePageActionWithRuntime``buildFileTreeRuntimeEnvironment``reconcilePageRuntimeResult` 等符号。
### P4:抽出主壳 filetree runtime
- [ ]`SIDEBAR_TREE_JS` 中拆出文件树选择、keyboard、clipboard、DND、context menu。
- [ ] 先拆纯函数和 reducer bridge,再拆 DOM 绑定。
- [ ] 主壳保留 `window.__mnoteSidebarTreeRuntimeStarted` 一类启动防重保护,但启动函数迁到外置模块。
- [ ] 与 Rust `FileTreeSelectionState` 合同对齐,不在前端新增第二套选择真相。
- [ ] 覆盖:
- 多选、shift range、ctrl/meta toggle。
- cut/copy/paste。
- 内部拖拽 move/copy。
- 外部文件拖入 local folder。
- readonly target 拒绝。
### P5:抽出 document pane host adapter
- [ ]`web_shell.rs` 的 inline module 中拆出文档 pane host adapter,例如:
- `document-pane-host-runtime.js`
- `document-session-runtime.js`
- `document-conflict-panel-runtime.js`
- [ ] 只迁 host/session/pane 生命周期,不迁 Tiptap 编辑器内部命令。
- [ ]`05-editor-mainline` 的编辑器 runtime 拆分保持边界:host 负责挂载、pane、session、资源 tabeditor 负责命令、history、附件链接、DOM overlay。
- [ ] 覆盖:
- primary / secondary pane 切换
- conflict panel unmount 清理
- attachment tab 打开
- resource tab error placeholder
### P6:收尾与旧 inline script 瘦身
- [ ] `layout.rs` 中只保留小型 bootstrap 和脚本引用。
- [ ] `tree.rs` 中不再内嵌大型 runtime JS。
- [ ] `web_shell.rs` 中只保留 page aggregate/bootstrap JSON 和 module script 引用。
- [ ] 更新 CodeGraph 索引,确认新增 JS 模块被索引。
- [ ] 删除或降级 P0 提取脚本:如果真实模块已覆盖主要 runtime,提取脚本只保留为历史/临时工具。
## 11. 测试与验收
### 11.1 Rust 测试
```bash
cd /mnt/Data1T/mnote/rust
cargo test -p mnote-web tree_shell
cargo test -p mnote-web filetree
cargo test -p mnote-web runtime
```
### 11.2 Browser smoke
```bash
node scripts/task446-tree-rename-dual-browser-live-smoke.js
node scripts/task447-tree-move-order-dual-browser-live-smoke.js
node scripts/task448-tree-resync-recovery-dual-browser-smoke.js
node scripts/task449-tree-sse-reconnect-snapshot-recovery-smoke.js
node scripts/task472-side-target-secondary-pane-smoke.js
node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
node scripts/task487-local-folder-tree-live-consumer-smoke.js
```
### 11.3 CodeGraph 验收
迁移后必须能通过 CodeGraph 或等价结构搜索定位以下浏览器符号:
- `startWithWebSocket`
- `startWithSseFallback`
- `buildOnlyOfficeOpenPath`
- `openEditorAttachmentEditTab`
- `reducePageActionWithRuntime`
- `buildFileTreeRuntimeEnvironment`
- `selectFileTreeRow`
- `executeLocalFileTreeExternalDrop`
- `openFileTreeContextMenu`
- `openResource`
验收标准:
- 这些符号不再只出现在 Rust raw string 中。
- `codegraph_status` 显示新增 JS 文件被索引。
- 字面搜索命中数量减少,定位结果能落到具体 runtime 模块。
- worker 任务书能以“单模块 + 单 smoke”派发,不再要求 worker 搜索整个 `layout.rs` / `web_shell.rs`
## 12. 风险与控制
- 风险:模块加载路径错误导致主壳首屏无交互。
- 控制:第一阶段沿用现有 no-store asset route;每次切片保留 smoke。
- 风险:外置模块改变执行时机。
- 控制:保留 `DOMContentLoaded` / immediate start 逻辑;迁移前后记录启动 DOM 标记。
- 风险:debug shell 与主壳复用时把 debug 行为带进主路径。
- 控制:host bridge 分为 `debugTreeShellHost``workspaceSidebarHost`,共享纯 runtime,不共享 debug UI。
- 风险:前端模块继续膨胀成新的巨文件。
- 控制:单文件超过约 800 行时必须拆职责;DND、selection、transport、resource open、context menu 分文件。
- 风险:外置 JS 无类型约束导致合同漂移。
- 控制:第一阶段用 Rust JSON contract 测试 + browser smoke;稳定后再评估 TypeScript 或 JSDoc typedef。
- 风险:resource open adapter 迁出时顺手改变 open target 行为。
- 控制:先建立 open intent 表,迁移只做同构搬迁;secondary pane、active tab、new window fallback 必须由 smoke 覆盖。
- 风险:`03-rust-web``05-editor-mainline` 边界混淆。
- 控制:本设计只处理 shell/browser host runtimeTiptap 命令、history、附件链接增强、编辑器 DOM overlay 由 `05-editor-mainline` 设计处理。
## 13. Done Gate
本设计移动到 `done/` 前必须满足:
- [x] `TREE_LIVE_CONTROLLER_JS` 已从 `layout.rs` 迁出。
- [x] debug `/tree` shell runtime 大块 JS 已从 `tree.rs` 迁出,或已有明确豁免说明。
- [ ] 主壳 filetree runtime 的核心 selection / DND / context menu 至少完成一个模块外置切片。
- [x] resource open / local upload adapter 至少完成一个高频 bug 模块外置切片。
- [x] 新增 JS runtime 文件能被 CodeGraph 索引。
- [ ] 至少一组 tree realtime smoke 和一组 local folder filetree/resource smoke 通过。
- [ ] `layout.rs` / `web_shell.rs` 不再继续新增大型 inline browser runtime。
## 14. 后续问题
- 是否引入 TypeScript:建议等 P1-P3 完成后再决定,不作为本设计前置。
- 是否引入 bundler:当前不需要;plain ES module 足够完成第一阶段。
- 是否把 Page AI panel 一并迁出:不建议混在本设计中。Page AI panel 应另起 `07-ai``05-editor-mainline` 设计,避免 tree runtime 重构扩大范围。
- 是否保留内嵌 JS 提取脚本:如果 P1-P3 推进顺利,可只作为临时诊断工具;如果短期没有资源迁出大块 runtime,则保留脚本辅助 CodeGraph。