Skip to content

节点文件名与可变 label 强绑定,导致高频 rename 在网络挂载盘(rclone/OneDrive)上产生残留重复文件 #159

Description

@mydmdm

问题描述

在使用 rclone FUSE 挂载的 OneDrive 作为 workspace(HUABU_WORKSPACE=/home/yuqyang/OneDrive/huabu)时,POST /:canvasId/execute 频繁报错:

Space write failed for node "<nodeId>": multiple persisted names claim this id: <oldName>.md, <newName>.md

例如:

  • node-6c0f7d49-...Huabu IPR Strategic Response Memo.md / Huabu Strategic IPR Response Memo.md
  • node-6399f579-...Question 1.md / 项目研究机会分析.md

后者复现时,两个文件的 frontmatter id 与正文内容完全一致,仅 label 不同(Question 1 → 自动生成标题 项目研究机会分析labelSource: auto)。旧文件 Question 1.mdunlinkSync 没有抛出任何异常、日志中也从未出现 "Failed to remove stale node sidecar" 警告的情况下,30+ 分钟后依然残留在磁盘上——即删除对调用方"假成功",但实际未在 rclone 后端真正落地/传播,这与 rclone --vfs-cache-mode full 配合 --dir-cache-time/--poll-interval 的异步、最终一致的删除语义吻合。

根因分析(两层)

表层(放大器):rclone FUSE 挂载的异步删除语义

--vfs-cache-mode full 下,删除操作先在本地 VFS 缓存生效并立即返回成功,实际到 OneDrive 后端的删除是异步/排队的。若该异步删除没有真正完成,--dir-cache-time 72h + --poll-interval 1m 之后的目录轮询可能让"已删除"的旧文件重新出现在本地视图中,而调用进程对此完全无感知(unlinkSync 早已返回成功)。

根本原因:文件名与可变 label 字段强绑定

apps/server/src/modules/storage/backends/disk/legacy/canvas-store.ts

// line 224
function nodeFilenameFor(nodeId: string, label: string | null): string {
  return `${toSafeFilename(label, nodeId)}.md`;
}

节点的磁盘文件名直接由 label 派生(为了让 .md 文件在文件系统里"见名知意")。writeNode()(同文件 ~1075-1245 行)区分两种写入路径:

  • label 不变atomicWriteText 直接覆盖同名文件——单步、原子(temp 写 + renameSync 到同一路径),即使在网络挂载盘上也基本安全。
  • label 变化isRename = true):先把新内容写到新文件名,成功后单独 unlinkSync 旧文件名——两步、非原子。这一步 unlink 是唯一的脆弱环节:任何失败(无论是显式抛错还是像本 issue 描述的"静默假成功")都会在磁盘上留下两个文件同时声称同一个 nodeId,被 nodeIndex() 重建时判定为 duplicate,此后该节点的所有写入都会被拒绝(nodeDuplicateIds 命中即直接短路返回 duplicate,见同文件 ~1128 行),需要人工介入清理。

而 label 变化在真实工作流中并不罕见:节点创建时先给默认标题(如 "Question 1"),agent 生成内容后自动打标题(labelSource: auto),用户手动改名等,都会触发这条"写新 + 删旧"的路径。label 越是被设计为"随内容自动演化",触发 rename 的频率就越高,风险敞口也越大——即使在本地磁盘上,这个设计仍然存在"任意一次 unlink 失败即产生残留重复文件"的结构性风险,只是本地磁盘上 unlink 失败概率极低,问题不易察觉;换到 rclone 这类网络挂载盘后,问题被放大到肉眼可见的频率。

建议修复方向

  1. 解耦文件名与可变 label:节点首次创建时基于标题生成一次性、稳定的文件名,之后 label 再变化只更新 frontmatter 里的 label 字段和文件内容(走 atomicWriteText 覆盖同名文件的安全路径),不再触发改名,除非用户显式执行"重命名文件"操作。
  2. 如果保留"文件名跟随 label"的产品特性,至少应该:
    • 让 rename 路径对"删除旧文件失败/未确认"更健壮,比如改名后不采用"先写新再删旧",而是引入短暂过渡态或延迟清理 + 定期对账,避免把用户可见的写入直接绑定到一次不可靠的 unlink
    • 增加针对本类"重复 id"的自动修复:检测到同一 nodeId 的多个文件时,依据内容 hash + mtime 自动判定并清理明显是"改名残留"的旧文件,而不是永久拒绝写入直到人工介入。

Activity

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Type

    No type

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions