Files
mnote/design/04-tree-domain/process/4-48-local-folder-markdown-resource-lifecycle-contract-v1.md
T

190 lines
9.3 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.
# 4-48 [process] 本地文件夹 Markdown 资源生命周期合同 v1
> 日期:2026-05-24
>
> OwnerTree Domain / Local Folder / Editor Mainline
>
> 关联 bug`bugs/04-tree-domain/process/4-49-filetree-drag-upload-target-parent-folder-v1.md`
## 1. 背景
本地文件夹 MVP 已经具备 Markdown 打开、附件上传、File Tree projection、资源 tab 打开和回收站能力,但当前多个缺陷说明系统还缺少一份面向本地 Markdown 的资源生命周期合同。
核心问题不是“附件点击坏了”这类单点 UI,而是四类对象的边界没有在本地文件夹场景完全收口:
- Markdown 正文里的链接文本。
- 磁盘上的真实附件文件。
- File Tree 中的 resource row。
- 主编辑区中的 object tab / preview tab。
## 2. 目标
- 固定本地 Markdown 上传、删除、缺失、打开、标题来源的产品和技术合同。
- 保持 Markdown 纯文本语义:正文链接不隐式拥有真实文件。
- 让本地资源默认进入主编辑区 tab,而不是依赖新窗口兜底。
-`4-49` 的多症状 bug 提供实施顺序和验收边界。
## 3. 非目标
- 不实现完整跨文档反向引用索引。
- 不自动删除所有 broken link。
- 不把 File Tree UI 提升为资源真源。
- 不重写完整 workbench tab 系统。
- 不改变 cloud / Convex 资源合同,除非为了保持 open target 行为一致。
## 4. 合同定义
### 4.1 上传 intent
上传入口必须先归类为明确 intent:
- `editor.markdown.attach`:用户在主编辑区上传或拖入文件。目标是当前 Markdown 页面的 resource directory。
- `filetree.folder.drop`:用户把外部文件拖到 File Tree 的 folder row。目标是该 folder 的真实目录。
- `filetree.resource.import`:用户未来通过菜单导入资源。目标由菜单上下文决定。
规则:
- `editor.markdown.attach` 不应携带 `targetRelativePath`,除非产品明确支持“把正文引用指向任意文件树目录”。
- `filetree.folder.drop` 必须携带 `targetRelativePath`
- 后端根据 intent 选择 `write_local_markdown_asset()``write_local_folder_file_upload()`,不能只靠 `documentId` 是否为空隐式决定。
### 4.2 Markdown 引用与真实文件
Markdown 正文链接只是引用,不是所有权声明。
- 删除正文附件块:只删除链接文本。
- 删除文件树附件:只删除真实文件。
- 真实文件缺失:正文链接保留为 broken reference。
- 自动级联删除真实文件:禁止作为默认行为。
后续可选动作:
- “删除引用并移到回收站”:显式危险动作,需要确认。
- “清理所有失效引用”:批量工具,需要预览 diff。
- “重新定位文件”:把 broken link 指向新路径。
### 4.3 缺失资源展示
缺失资源不应静默消失。
- Parser 必须保留原始 Markdown 链接。
- Editor enhancer 标记缺失状态。
- 点击缺失资源打开 missing tab 或 inline missing panel。
- missing state 显示 root、原相对路径、最后一次检测状态。
### 4.4 Object tab 打开目标
本地资源默认打开到主编辑区 active tab。
- `resource:file:{rootUri}:{rootRelativePath}` 是本地普通文件的最小 object identity。
- 即使没有当前 Markdown documentlocal folder workspace 也必须有 editor host landing state。
- PDF、Markdown 附件、图片、普通文件都走同一套 resource tab host。
- Shift / Ctrl / Meta 或显式菜单才使用 new window。
### 4.5 本地 Markdown 标题来源
已确认 MVP 口径:
- File Tree / Page Tree 标题默认来自文件名 stem。
- 页头标题默认来自文件名 stem,用来统一“我的空间”和“本地文件夹”的行为。
- 正文 H1 只作为正文内容,不推导 tree row title。
- 用户可在页面设置中隐藏本地 Markdown 页头标题,默认开启,避免与正文 H1 重复。
已确认:
- Frontmatter `title` 暂不覆盖 File Tree / Page Tree / 页头标题。Frontmatter `title` 指 Markdown 文件开头形如 `--- title: Some Title ---` 的元数据标题;MVP 阶段它只作为文件元数据保留,不参与本地 Markdown 标题真源。
## 5. 执行清单
### 5.1 Smoke 红灯
- [x] 新增 `task479-local-folder-markdown-resource-lifecycle-smoke.js`
- [x] 覆盖主编辑区上传后落盘到 page resource directory。
- [x] 覆盖 filetree folder drop 后落盘到目标 folder。
- [x] 覆盖删除真实文件后 broken link 刷新仍保留。
- [x] 覆盖 Backspace / Delete 删除一个附件不破坏相邻附件。
- [x] 覆盖刚进入 local folder 直接打开 PDF / Markdown 附件进入 active tab。
- [x] 覆盖 body 不因文件树滚动到底产生额外空白。
- [x] 覆盖本地 Markdown 文件名标题来源。
### 5.2 上传链路
- [x] 前端上传 detail 增加明确 `uploadIntent`
- [x] 后端 `/api/local-folder/assets/upload` 解析并校验 intent。
- [x] `editor.markdown.attach` 写入 `markdown_page_resource_directory()`
- [x] `filetree.folder.drop` 写入 `targetRelativePath` 指向的目录。
- [x] 上传响应返回 intent、rootRelativePath、markdownRelativePath 和 ownerDocumentId,供 smoke 断言。
### 5.3 删除和缺失资源
- [x] 主编辑区附件删除 command 只删除 Markdown 链接并保存。
- [x] 手柄删除复用同一 command,不做纯 DOM 隐藏。
- [ ] Parser / serializer 保留 broken link。
- [ ] watcher 收到真实文件删除后刷新存在性状态,不改正文。
- [ ] 缺失资源点击进入 missing tab。
### 5.4 Resource tab host
- [x] local folder 首屏无 active document 时也渲染 editor host。
- [x] filetree resource open 默认 active tab。
- [x] PDF 使用与 cloud asset 一致的 preview chrome。
- [x] 本地 Markdown 附件作为 resource tab 打开,不抢占当前页面正文。
- [ ] active row / selected row 保持在被打开资源。
### 5.5 标题和页面设置
- [x] 修正本地 Markdown 标题解析测试命名和断言。
- [x] File Tree / Page Tree 标题默认来自文件名。
- [x] 正文 H1 保留在正文,不参与标题推导。
- [x] 本地 Markdown 页头标题默认隐藏。
- [ ] 页面设置新增“隐藏本地 Markdown 文件标题”的可切换 UI。
- [ ] 设置刷新后持久生效。
- [ ] 重命名标题时走文件重命名和 bundle 目录同步。
### 5.6 滚动边界
- [ ] Workbench 根容器固定视口高度。
- [ ] Sidebar / File Tree 是唯一纵向滚动容器。
- [ ] resize handle wheel 不穿透到 body。
- [ ] smoke 断言 `document.scrollingElement.scrollTop` 不变化。
## 6. 建议切片
1. Smoke + 合同观测字段。
2. 上传 intent 分流。
3. 删除引用和 broken link 保留。
4. 本地 resource tab host。
5. PDF / Markdown 附件 tab 一致性。
6. 标题来源和隐藏标题设置。
7. 滚动边界。
## 7. 验收命令
```bash
cargo check --manifest-path rust/Cargo.toml -p mnote-web
cargo test --manifest-path rust/Cargo.toml -p mnote-web local_markdown -- --test-threads=1
PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium node scripts/task459-local-markdown-attachment-tab-smoke.js
PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
codegraph sync .
```
## 8. Done Gate
- [ ] `4-49` 中的 6 组用户可见症状均有对应 smoke。
- [x] 默认删除行为不级联删除真实附件。
- [x] 文件树真实删除不再导致正文链接刷新后消失。
- [x] 本地资源默认在主编辑区 tab 打开。
- [x] 本地 Markdown 标题来源和隐藏默认值经过用户确认并落地。
- [ ] 相关 bug 记录移动到 `done/` 并补验证证据。
## 9. 2026-05-24 执行记录
- `task479-local-folder-markdown-resource-lifecycle-smoke.js` 已覆盖上传目录、filetree folder drop、broken link 保留、无 active document 附件 active tab、文件树滚动边界、文件名标题来源。
- 修复 local folder root entry 不再复用跨 root 的 `mnote_recent_page_id`,避免 asset-only 工作区误进入无效 active page fallback。
- 修复无 active document 的 local folder 首屏,补齐 workspace-level 主编辑区 resource tab host 和空 panes runtime。
- 本地 Markdown 标题已收口为文件名来源;Frontmatter title 暂不覆盖标题真源;本地 Markdown 页头默认隐藏。
- 验证:`task479` 全部通过,`task459-local-markdown-attachment-tab-smoke.js` 通过;Rust `root_entry_``local_markdown``document_shell_renders` 相关单测通过。
- 2026-05-24 追加:`task479` 新增 Check 7,覆盖 Backspace 与手柄删除单个附件引用;删除后只改 Markdown 链接,不删除真实附件文件,相邻附件刷新后仍保持可点击附件块。
- 2026-05-24 追加:来源菜单的旧“云空间”入口按当前 local-first MVP 语义改为“我的空间”,默认回到受管 `my-space` 本地根;新增 `mnoteHome=1` 防止被最近普通本地目录自动重定向接管。`task441-local-folder-cloud-switch-smoke.js` 已按当前语义通过。
- 2026-05-24 追加:上传链路已改为显式 `uploadIntent` 合同。主编辑区上传发送并返回 `editor.markdown.attach`,写入 page resource directoryFile Tree folder drop 发送并返回 `filetree.folder.drop`,写入 `targetRelativePath` 指向目录。`task479` 已断言响应中的 `uploadIntent``rootRelativePath``markdownRelativePath``ownerDocumentId`