2026-05-24 21:03:33 +08:00
# 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
```
全部通过。
2026-05-24 21:35:21 +08:00
### 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`
2026-05-24 22:42:53 +08:00
### 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、文件树滚动、标题来源、单附件删除保留相邻附件。
2026-05-24 23:43:22 +08:00
### 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` 。
2026-05-25 00:03:12 +08:00
### 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 相关测试通过。
2026-05-25 00:08:23 +08:00
- `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 相关测试通过。
2026-05-25 00:03:12 +08:00
- `GET /api/mnote-browser-runtime/tree-shell-runtime.js`
2026-05-25 00:08:23 +08:00
- 结果:`200` , `content-type=application/javascript; charset=utf-8` ,浏览器读取内容包含 `startTreeShellRuntime` 、`tree.ready` 、`hydrateInitialFileTree` 、`reducePageActionWithRuntime` 、`buildFileTreeRuntimeEnvironment` 、`reconcilePageRuntimeResult` 。
2026-05-25 00:03:12 +08:00
- `codegraph_search reducePageActionWithRuntime` / `buildFileTreeRuntimeEnvironment` / `reconcilePageRuntimeResult`
- 结果:3 个函数均可定位到 `rust/crates/mnote-web/browser/tree-shell-runtime.js` 。
2026-05-25 00:08:23 +08:00
- 真实 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 仍待后续切片。
2026-05-25 00:03:12 +08:00
剩余风险:
2026-05-25 00:08:23 +08:00
- `type="module"` 自带作用域,`tree-shell-runtime.js` 已移除外层 IIFE,并只 hoist P3 关键函数;其他 DOM runtime 仍按原局部闭包组织,避免扩大改动。
2026-05-25 00:03:12 +08:00
- debug tree shell runtime 与主壳 `SIDEBAR_TREE_JS` 仍是两套 runtime,后续 P4/P5 继续拆主壳 filetree runtime 和 document pane host adapter。
2026-05-25 00:22:08 +08:00
### 9.7 Batch 6 执行记录(2026-05-25)
Reasonix read-only audit:
- run id: `reasonix-2026-05-24T16-11-49-842Z-94562749`
- worktree: `/mnt/Data1T/mnote-worktrees/p4-filetree-audit`
- handoff: `/home/lix/.codex/runtime/reasonix-coding-worker/reasonix-2026-05-24T16-11-49-842Z-94562749/process-handoff.md`
- 类型:只读审计,主控已读取 Hindsight recall、`process-handoff.md` 、工作区 diff 与验证结果。
- 采纳结论:主壳 filetree 第一批应优先迁纯 DOM/字符串读取 helper、path 解析和下载详情构造;DND、paste、菜单 DOM、上传执行链继续留在后续批次。
采纳内容:
- 新增 `rust/crates/mnote-web/browser/filetree-runtime.js` 。
- 新增固定 asset route: `GET /api/mnote-browser-runtime/filetree-runtime.js` 。
- `PageLayout` 在 `SIDEBAR_TREE_JS` 前注入 `type="module"` 外置脚本。
- `SIDEBAR_TREE_JS` 中以下 helper 先做 runtime 代理,保留原实现 fallback:
- `fileTreeRowDocumentId`
- `fileTreeRowAssetId`
- `decodeLocalEncodedPath`
- `fileTreeRowLocalRelativePath`
- `fileTreeRowLocalUploadTargetRelativePath`
- `fileTreeRowKind`
- `isFileTreeDownloadableAssetRow`
- `isFileTreeDownloadableRow`
- `fileTreeAssetDownloadDetail`
- `fileTreeRowsByRowIds`
- `fileTreeChildCount`
- 新增 `rust/crates/mnote-web/browser/filetree-context-menu-runtime.js` ,先承接 `buildSidebarFileTreeContext` 与 `evaluateSidebarFileTreeWhen` 的 CommandContext / when 表达式求值;菜单 DOM 构建仍留在 `SIDEBAR_TREE_JS` 。
采纳边界:
- 本批次不迁移 `selectFileTreeRow` 、`openFileTreeContextMenu` 、`pasteSidebarFileTreeClipboard` 、`downloadSelectedFileTreeAssetRows` 、内部/外部 DND、上传执行链。
- `fileTreeChildCount` 虽与 paste sortOrder 有关,但本批仅做同构 runtime 代理并保留 inline fallback,未改变 paste 命令语义。
- filetree context menu 本批只迁 CommandContext/when evaluator,不迁 menu item DOM、事件绑定或右键定位逻辑。
已通过验证:
- `node --check rust/crates/mnote-web/browser/filetree-runtime.js`
- `node --check rust/crates/mnote-web/browser/filetree-context-menu-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 filetree_runtime -- --test-threads=1`
- 结果:9 个相关测试通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web filetree_context_menu -- --test-threads=1`
- 结果:2 个相关测试通过。
- `cargo test --manifest-path rust/Cargo.toml -p mnote-web mnote_browser_runtime_assets_are_explicitly_mounted -- --test-threads=1`
- 结果:runtime asset route 测试通过。
- 真实浏览器轻量验收:
- dev server: `npm run dev:hot`
- 登录后访问 `http://127.0.0.1:3000/`
- `document.scripts` 包含 `/api/mnote-browser-runtime/filetree-runtime.js` 与 `/api/mnote-browser-runtime/filetree-context-menu-runtime.js`
- `window.__mnoteFileTreeRuntime.fileTreeRowLocalRelativePath` 和 `window.__mnoteFileTreeContextMenuRuntime.evaluateSidebarFileTreeWhen` 均为函数。
- 两个 runtime asset 均返回 `200` , `content-type=application/javascript; charset=utf-8` 。
2026-05-24 21:03:33 +08:00
## 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` 。
2026-05-24 22:42:53 +08:00
- [x] 新增 `rust/crates/mnote-web/browser/local-upload-runtime.js` ,或先与 resource open 合并为单个不超过 800 行的 `resource-open-runtime.js` 。
2026-05-24 21:03:33 +08:00
- [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 23:24:38 +08:00
- 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 23:43:22 +08:00
- 2026-05-24 / Batch 4 已迁:`resolveEditorUploadContext` 、`editorRootFromUploadOptions` 、`openEditorUploadFilePicker` ,并保持调用 `uploadFilesWithResolvedTarget` ,未同时迁上传主流程。
2026-05-24 23:24:38 +08:00
- 暂不迁:`uploadFileToMediaAsset` 、`uploadFilesWithResolvedTarget` 、`insertUploadedAssetIntoEditor` 。这些函数同时涉及 API POST、file tree refresh、Tiptap chain 插入、OnlyOffice/resource tab,需在后续批次继续拆边界。
2026-05-24 21:03:33 +08:00
- [ ] 保持现有 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`
2026-05-24 21:35:21 +08:00
- [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] 浏览器断言:
2026-05-24 21:03:33 +08:00
- `document.documentElement.dataset.mnoteTreeLiveTransport` 正确。
2026-05-24 21:35:21 +08:00
- 外置 module 实际执行并设置 `window.__mnoteTreeLiveControllerStarted` 。
- [ ] WS 推送仍更新 page/filetree。
- [ ] WS 不可用时仍 fallback 到 SSE。
2026-05-24 21:03:33 +08:00
### P3:外置 debug `/tree` shell runtime bridge
2026-05-25 00:03:12 +08:00
- [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` 等符号。
2026-05-24 21:03:33 +08:00
### P4:抽出主壳 filetree runtime
- [ ] 从 `SIDEBAR_TREE_JS` 中拆出文件树选择、keyboard、clipboard、DND、context menu。
2026-05-25 00:22:08 +08:00
- [x] 先拆纯函数和 reducer bridge,再拆 DOM 绑定。
- 2026-05-25 / Batch 6 已迁:filetree 行 accessor、local relative path、download detail、row lookup、child count wrapper,以及 context menu CommandContext / when evaluator。
- 暂不迁:selection、keyboard、clipboard、DND、菜单 DOM、上传执行链。
2026-05-24 21:03:33 +08:00
- [ ] 主壳保留 `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、资源 tab; editor 负责命令、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 runtime; Tiptap 命令、history、附件链接增强、编辑器 DOM overlay 由 `05-editor-mainline` 设计处理。
## 13. Done Gate
本设计移动到 `done/` 前必须满足:
2026-05-25 00:08:23 +08:00
- [x] `TREE_LIVE_CONTROLLER_JS` 已从 `layout.rs` 迁出。
- [x] debug `/tree` shell runtime 大块 JS 已从 `tree.rs` 迁出,或已有明确豁免说明。
2026-05-24 21:03:33 +08:00
- [ ] 主壳 filetree runtime 的核心 selection / DND / context menu 至少完成一个模块外置切片。
2026-05-25 00:08:23 +08:00
- [x] resource open / local upload adapter 至少完成一个高频 bug 模块外置切片。
- [x] 新增 JS runtime 文件能被 CodeGraph 索引。
2026-05-24 21:03:33 +08:00
- [ ] 至少一组 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。