Files
mnote/bugs/04-tree-domain/process/4-49-filetree-drag-upload-target-parent-folder-v1.md
T

324 lines
17 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-49 [process][bug] 本地文件夹 Markdown 附件、打开目标与标题合同缺口 v1
> 发现时间:2026-05-23
>
> 状态:`[process]`
>
> 关联主线:`04-tree-domain`、`05-editor-mainline`
>
> 关联设计:`design/04-tree-domain/process/4-48-local-folder-markdown-resource-lifecycle-contract-v1.md`
## 1. 范围判断
这条记录不是单个拖拽 bug,而是本地文件夹 MVP 在 `Markdown 正文引用``真实文件资源``File Tree projection``主编辑区 object tab` 之间的合同缺口。
已按 Sidex / VSCode 行为确认的边界:
- 主编辑区删除 Markdown 附件块 / 链接,默认只删除正文里的引用,不删除真实附件文件。
- 文件树右键删除真实附件,默认只删除真实文件,不自动扫描并改写所有 Markdown 正文。
- 删除真实文件后,正文中的链接应保留为 broken reference;编辑区需要增强展示和点击失败处理,而不是静默清除链接。
- 只有显式危险动作才可以同时删除引用和真实文件,例如后续可设计“删除引用并移到回收站”,且必须做确认和反向引用检查。
## 2. 用户可见症状
### 2.1 主编辑区拖入 / 上传附件后落盘目录不符合预期
复现描述:
1. 在“我的空间”或本地文件夹中打开一个 Markdown 页面。
2. 向主编辑区拖入文件或上传文件。
3. 主编辑区能显示附件,点击也能打开。
4. 但文件落在主文件夹同级位置,预期应落在当前 Markdown 页面资源目录下。
截图:`/mnt/Data1T/mnote/tmp/image copy 65.png`
当前判断:
- 代码中已经有两条上传链路:
- 文件树外部 drop 到 folder row:通过 `targetRelativePath``write_local_folder_file_upload()`
- 主编辑区上传 / drop:通过 `documentId``write_local_markdown_asset()`,目标应为 `markdown_page_resource_directory()`
- 既有 `4-46` 已把“文件树拖到文件夹 row 上传到该文件夹”标记为完成,因此当前缺口更可能在“主编辑区 Markdown 上传目标目录”和“本地 page bundle 目录推导”上,而不是单纯 filetree folder drop。
建议修复:
- 明确区分两类 intent
- `filetree.folder.drop`:文件进入用户指定的文件树目录。
- `editor.markdown.attach`:文件进入当前 Markdown 页面的资源目录。
-`editor.markdown.attach`,后端只允许写入当前 Markdown 的 page resource directory
- bundle 形态:`Page/Page.md` 的附件写入 `Page/`
- flat 形态:`Page.md` 的附件写入 `Page/`,目录不存在时创建。
- 前端上传时不要把主编辑区 drop 误标成 folder target;有 `targetRelativePath` 时才走 folder upload。
- smoke 需要同时覆盖“拖到文件树文件夹”和“拖到主编辑区”两个入口,避免互相掩盖。
验收标准:
- 主编辑区上传附件后,文件树中该附件位于当前 Markdown 页面资源目录下。
- 文件树 folder row drop 仍写入目标 folder 下,不回退到当前页面资源目录。
- 新增或扩展 smoke 断言上传响应中的 `rootRelativePath`、正文中的相对链接、File Tree row 父子位置一致。
### 2.2 删除真实附件后,正文链接生命周期不稳定
复现描述:
1. 主编辑区中已有附件链接。
2. 在文件树附件 row 上右键真实删除该附件。
3. 主编辑区中的链接仍在,点击会显示附件不存在。
4. 刷新后,正文里的链接消失。
当前判断:
- 链接仍在是合理行为;真实文件删除不应自动改写 Markdown 正文。
- 刷新后链接消失是不合理行为,说明 Markdown 解析 / 回写 / 附件索引重建时把 broken local asset link 当成无效增强块过滤掉了。
- 点击显示附件不存在也可以保留,但应在 tab 内或编辑器内给出明确缺失状态,不应走新窗口兜底。
建议修复:
- Markdown parser / serializer 必须保留 broken link 的原始文本。
- 编辑器附件增强逻辑识别目标文件不存在时,保留链接并标记 `data-mnote-attachment-missing="true"` 或等价状态。
- 点击 missing 附件时打开资源 tab 的 missing state,显示原路径和“文件不存在/已删除”,不删除正文。
- 文件树真实删除后通过 watcher / event 让当前编辑器刷新附件存在性状态,但不改正文内容。
验收标准:
- 文件树删除附件后,不刷新页面时正文链接仍可见且标记为缺失。
- 刷新后正文链接仍存在,样式仍是 broken reference。
- 点击缺失附件不会在新窗口打开,也不会静默失败。
### 2.3 主编辑区删除附件引用的序列化错误
复现描述:
1. 主编辑区有两个附件。
2. 用 Backspace / Delete 删除后面的附件,前面的附件会变成普通文字。
3. 删除前面的附件,后面的附件不受影响。
4. 手柄中的删除看起来只是在 UI 上消失,刷新后又回来。
截图:`/mnt/Data1T/mnote/tmp/image copy 75.png`
当前判断:
- Backspace / Delete 默认只删除正文引用,这个产品语义正确。
- “前一个附件变成文字”是编辑器 transaction 或 Markdown serializer 范围错误。
- “手柄删除刷新后回来”是 UI 删除没有写回真实 `.md`,或写回后 parser 又按旧结构恢复。
建议修复:
- 把附件块视为一个可删除的 editor node / mark atom;删除一个附件时只移除对应链接范围,不影响相邻附件。
- 手柄删除复用同一条删除引用 command,不走纯 DOM 隐藏。
- 保存前后的 Markdown 文本做最小断言:删除第二个附件只删除第二条链接,第一条仍保持链接语法和增强属性来源。
验收标准:
- 删除前一个、后一个、中间一个附件,剩余附件都仍是可点击附件块。
- 手柄删除后刷新页面,被删引用不再回来。
- 真实附件文件仍保留在磁盘,除非用户选择未来的显式危险动作。
### 2.4 切回“云空间”进入空白 workspace
复现描述:
1. 用户打开本地文件夹。
2. 想回到“我的空间”,点击左上角下拉中的“云空间”(旧文案)。
3. 页面变成空白,整体效果是回不到我的空间。
当前判断:
- 该问题曾由 `bugs/04-tree-domain/done/4-36-local-folder-cloud-switch-empty-workspace-v1.md` 修复。
- 如果现在仍能复现,应按回归处理:重点检查 `lastCloudWorkspaceId` 是否又被写成 synthetic `default`,以及切回 cloud 时 URL 是否残留 `rootUri`
- 2026-05-24 复测发现新阶段根因不同:当前 local-first MVP 默认不启动 Convex,旧“云空间”按钮仍跳到 `sourceKind=convex_workspace` 会稳定进入 `convex_unavailable`;同时 `local-ws:*` 曾可能被当作 last cloud workspace 复用。
建议修复:
- 先补回归 smoke 或复用 `scripts/task441-local-folder-cloud-switch-smoke.js`
- `openLocalFolderRoot()` 进入本地前必须记住真实 cloud workspace id,不能从 `document.body` 粗粒度推导。
- `switchToCloudWorkspace()` 必须清理 `rootUri` / secondary local source 参数,只携带真实 cloud workspace id。
- 当前 MVP 修复口径:来源菜单入口文案改为“我的空间”,默认返回受管 `my-space` 本地根,而不是进入已退出默认 dev 主链的 Convex compat。
- 返回“我的空间”时使用 `/?treeView=filetree&mnoteHome=1`,显式绕过最近普通本地目录自动打开逻辑;`lastCloudWorkspaceId` 继续过滤 `default``local-ws:*`
验收标准:
- 我的空间有页面 -> 打开普通 local folder -> 点击“我的空间” -> 默认 `my-space` 文件树恢复。
- URL 不含普通 local folder 的 `rootUri``mnote.workspace.lastCloudWorkspaceId` 不得为 `default``local-ws:*`
### 2.5 刚打开本地文件夹时,直接打开附件只能新窗口打开
复现描述:
1. 用户刚打开本地文件夹。
2. 未先打开任何 Markdown 文件。
3. 直接打开任意附件,都会在新窗口打开。
4. 只有先打开一个 `.md` 文件后,附件才能在 new tab / 主编辑区 tab 中打开。
当前判断:
- 这是主编辑区 object tab host 的 landing state 缺口。
- 本地文件夹模式下,即使当前没有 active Markdown document,也应该有 workspace-level editor host 可承载 resource tab。
- 附件打开不应依赖 `currentDocumentId()` 是否存在;文件树 row 已经携带 `rootUri + rootRelativePath / assetId`,足够形成 `resource:file:{rootUri}:{path}` identity。
建议修复:
- 本地文件夹入口首屏初始化 editor host,即使没有当前文档,也显示空白 / welcome landing tab 容器。
- 文件树附件 open intent 使用 `objectIdentity=resource:file:{rootUri}:{rootRelativePath}`,不要回退成 `window.open()`
- `openConvexAssetFromFileTree()` / local file open 分支按 `sourceKind=local_folder` 分流到 active tab;只有 Shift/Ctrl/Meta 或显式“新窗口打开”才新窗口。
验收标准:
- 刚进入 local folder,直接点击 pdf / md / 图片 / 普通附件,均在主编辑区 tab 打开。
- 未打开任何 `.md` 时也不需要新窗口兜底。
- 文件树 row active / selected 保持在被打开附件,不跳到第一个文件。
### 2.6 文件树分隔线或滚动到底后出现底部空白
复现描述:
在文件树的左右拖动调整线处直接滚动页面,或文件树滚动到最下方后继续滚动,会导致下方出现空白。
截图:`/mnt/Data1T/mnote/tmp/image copy 66.png`
当前判断:
- 这更像 workbench layout / scroll container 边界问题,不是资源语义问题。
- 可能是 body / main workbench 和 sidebar tree 同时可滚动,滚轮事件穿透到页面根容器后把整个页面滚到工作台高度之外。
建议修复:
- Workbench 根容器固定 `height: 100vh`,主页面禁止 body 纵向滚动。
- Sidebar / File Tree 自己作为唯一滚动容器,`overscroll-behavior: contain`
- 分隔线 hover / drag 区域拦截 wheel,避免滚动事件穿透到 body。
验收标准:
- 文件树滚到最底部后继续滚动,工作台底部不出现额外空白。
- 在左右 resize handle 上滚轮,不移动页面根滚动位置。
- smoke 记录 `document.scrollingElement.scrollTop === 0` 或等价断言。
### 2.7 本地文件夹 PDF 只能新窗口打开
复现描述:
本地文件夹中的 PDF 文件只能在新窗口打开;我的空间中上传的 PDF 可以在 new tab 中打开。
当前判断:
- 与 2.5 同属本地资源 tab host 缺口。
- PDF 是普通 resource preview tab,不应因为来源是 local folder 就退化到浏览器新窗口。
建议修复:
- 统一本地文件打开目标:默认 active tab,显式 modifier / 菜单动作才 new window。
- PDF tab 使用与 cloud asset 相同的 preview host,只是数据源为 `/api/local-folder/files/open?rootUri=...&path=...`
- 缺失文件显示 missing state,不回退到新窗口。
验收标准:
- local folder PDF 默认在主编辑区 tab 预览。
- cloud PDF 与 local PDF 的 tab chrome、关闭、active row 行为一致。
### 2.8 本地 Markdown 标题来源与标题隐藏设置不清晰
复现描述:
本地文件夹 `.md` 标题解析有问题:第一行带 `#` 时,文件标题重复显示,截图见:
- `/mnt/Data1T/mnote/tmp/image copy 67.png`
- `/mnt/Data1T/mnote/tmp/image copy 69.png`
- `/mnt/Data1T/mnote/tmp/image copy 72.png`
- `/mnt/Data1T/mnote/tmp/image copy 73.png`
用户期望:
- 文件树和页面标题更接近文件名,不要把正文第一行 H1 当成文件标题造成重复。
- 页面设置新增全局设置:隐藏本地文件夹 Markdown 文件的文件标题。
当前判断:
- 当前代码中已有测试 `local_markdown_page_title_comes_from_file_name_not_body_heading`,但断言和注释实际倾向“有 H1 时标题来自 H1”。这与当前用户期望冲突,需要作为产品口径重新确认并修正测试命名 / 行为。
- 对普通本地 Markdown 来说,文件名是更稳定的资源 identity;正文 H1 应保留为正文内容,不建议继续作为文件树标题真源。
建议修复:
- 本地文件夹模式下,File Tree / Page Tree 默认标题来自文件名 stem。
- 本地 Markdown 页头标题也默认来自文件名,以统一“我的空间”和“本地文件夹”的行为。
- Frontmatter `title` 是 Markdown 文件开头 `--- title: ... ---` 这类元数据标题;它是否覆盖页头标题需要单独决定。建议 MVP 先不覆盖 File Tree 文件名,只作为正文 metadata 或页面内显示候选。
- 正文 H1 不参与文件名 / tree row title 推导。
- 页面设置新增 workspace-level 或 user-level 偏好:`隐藏本地 Markdown 文件标题`
- 默认开启:编辑器不额外显示 MNote 页头标题,只显示 Markdown 正文。
- 关闭:显示 MNote 页头标题,但该标题来自文件名,不能与正文 H1 混淆。
- 修改标题时应执行文件重命名;校验非法字符、同级重名和 bundle 目录同步。
验收标准:
- `File Name.md` 且正文第一行 `# Body Heading` 时,文件树显示 `File Name.md`,正文仍显示 `# Body Heading`
- 不出现页头标题和正文 H1 的重复标题误导。
- 页面设置切换“隐藏本地 Markdown 文件标题”后刷新仍生效。
- 重命名合法文件时,文件名和 File Tree row 同步更新;非法字符 / 同级重名在输入框内提示。
## 3. 建议实施顺序
1. 先补 smoke 红灯:
- 主编辑区上传落盘目录。
- 删除真实附件后 broken link 保留。
- 主编辑区删除附件引用不影响相邻附件。
- 刚进入 local folder 直接打开附件 / PDF 进入 tab。
- 文件树滚动到底无 body 空白。
- 本地 Markdown 文件名标题来源。
2. 修复资源生命周期合同:
- 上传 intent 分流。
- broken reference 保留和标记。
- editor attachment delete command 持久化。
3. 修复 local folder object tab host
- 没有 active document 时也能打开 resource tab。
- PDF / md / 普通附件默认 active tab。
4. 修复 workspace source switch 回归,如果 smoke 确认可复现。
5. 修复 layout scroll 边界。
6. 修正本地 Markdown 标题来源和隐藏标题设置。
## 4. 已确认与待确认
- 已确认:本地 Markdown 的 File Tree / 页头标题默认来自文件名,用来统一“我的空间”和“本地文件夹”的行为;正文 H1 永远只作为正文内容。
- 已确认:`隐藏本地 Markdown 文件标题` 默认开启,避免本地 Markdown 页头标题与正文 H1 重复。
- 已确认:Frontmatter `title` 暂不覆盖页头标题。Frontmatter `title` 指 Markdown 文件开头形如 `--- title: Some Title ---` 的元数据标题;MVP 阶段 File Tree / 页头标题继续以文件名为准。
## 5. 验证命令建议
```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/task441-local-folder-cloud-switch-smoke.js
```
后续应新增本 bug 专属 smoke,例如:
```bash
PLAYWRIGHT_CHROME_EXECUTABLE=/snap/bin/chromium node scripts/task479-local-folder-markdown-resource-lifecycle-smoke.js
```
## 6. 2026-05-24 执行记录
已修复 / 已验证:
- 2.1 主编辑区上传附件落盘到当前 Markdown 页面资源目录:`task479` 通过。
- 2.2 删除真实附件后刷新,正文 broken link 保留:`task479` 通过。
- 2.5 刚进入 local folder、无 active Markdown document 时,点击 PDF 附件进入主编辑区 resource tab`task479` 通过,`popupCount=0`
- 2.7 本地 Markdown 附件 / PDF 默认 active tab`task459``task479` 通过。
- 2.8 本地 Markdown 标题来源:File Tree / 页头标题来自文件名,正文 H1 保留在正文;`local_markdown` 单测与 `task479` 通过。
关键根因:
- local folder 根入口曾复用全局 `mnote_recent_page_id`。切换到另一个 root 时,旧 recent page 可能指向当前 root 不存在的 `.md`,导致 aggregate 构建失败后进入没有 resource tab host 的 workspace fallback。
- 无 active document 的 local folder 首屏缺少 workspace-level 主编辑区 resource tab host,导致附件 row 只能依赖新窗口兜底。
本轮修复:
- 显式 local folder 根入口不再使用跨 root 的 recent page cookie;无有效 page 时保持无 active page landing state。
- HomePage 无 active page 分支渲染主编辑区 tab strip、空 page panel 和 resource tab host。
- editor island adapter 允许 `panes=[]` 时仍注册 `__mnoteDocumentPaneRuntime.openResourceInActiveTab`,只是不挂载正文编辑器。
- `task479` Check 4 改为使用独立 asset-only local folder,确保真实覆盖“无 active Markdown document”。
仍在 process
- 2.3 Backspace / Delete 删除相邻附件已补浏览器级 smoke:`task479` Check 7 通过。Backspace 与手柄删除都只删除 Markdown 链接,不删除真实附件文件,相邻附件刷新后仍保持可点击附件块。
- 2.4 切回“我的空间”已按当前 local-first MVP 语义修复并复测:旧“云空间”入口不再进入 `convex_workspace`,改为“我的空间”,通过 `mnoteHome=1` 回默认 `my-space`,避免最近普通本地目录和旧 Convex compat 接管;`task441-local-folder-cloud-switch-smoke.js` 通过。
- 页面设置中“隐藏本地 Markdown 文件标题”的可切换 UI / 持久化尚未实现;当前只落地默认隐藏。