Repository navigation
Replies: 3 comments 1 reply
|
Source check against Thank you for the evidence: the timeline, the alphabetical-order shape and the 1. §4.1 — confirmed, and the module is cleaner than you say
What I can add: the module performs no directory deletion at all. Its 2. §4.3 — confirmed
3. §4.2 — I read this the other way, and the falsifier is in this repositoryYou write that on Windows Node v24's symlinkSync(target, link, process.platform === 'win32' ? 'junction' : 'dir')
removeOwnedDirectory(link) // the junction IS the root path
expect(lstatSync(link, { throwIfNoEntry: false })).toBeUndefined()
expect(readFileSync(join(target, 'sentinel'), 'utf8')).toBe('preserved')The link is made with So one of the two is wrong on Windows: those assertions, or the One line separates them, printing the mode rather than the two booleans: const st = fs.lstatSync("D:/tmp/jprobe/link")
console.log(st.mode.toString(8), st.isSymbolicLink(), st.isDirectory())A link reports 4. The one command that decides the layerBy elimination the traversal is If 5. Your fixes (2), (3) and (4) survive whichever way it lands
|
|
我还发现了另外一个问题,link安装的插件在卸载时未移除node_modules下的link路径。 时间线
|
|
给插件作者一个可能的好消息:你的仓库大概率能完整救回。删除在遍历到
给 lovezi0 和后续所有人的警示:你实测的「npm 形式重装又一次击穿」说明安装和卸载两条路径都踩这个洞——在官方修掉(对 junction/reparse point 做 lstat 识别、只删链接本身不递归进入)之前:
另外一个跨帖观察:这是本讨论区第二起「工具跟随链接/名字误伤真实数据」的事故(上一起是升级助手在错误的 HOME 上重建、#7921),共同点是删除/重建前没有验证目标身份。给官方的修复优先级建议里,这一条值得排在很前面——它不是边角,是「开发者日常操作一次就掏空源码仓库」的等级。 |
Uh oh!
There was an error while loading. Please reload this page.
lstat能正确识别,推测不受影响,请官方确认dsh plugin --profile <p> remove—— 在**本地目录安装(link:/ 绝对路径)**这一常规开发工作流上触发1. 一句话摘要
在 Windows 上以
link:(本地目录)方式安装的插件,其在profile/node_modules/<pkg>处是 junction。执行卸载时,删除逻辑把 junction 当作普通目录递归进入它的目标目录(也就是开发者的源码仓库根),从仓库根开始按目录项顺序逐条删除。本次事故中,插件dsh-model-extension的源码仓库被删掉了.deepseek-harness/(agent 长期记忆全部丢失)与.git的头部文件(HEAD、config、index、logs、info、hooks、description、branches、COMMIT_EDITMSG),仓库直接报fatal: not a git repository。删除恰好在遍历到.git/objects之前中断,其余工作树内容幸免 —— 这是运气,不是设计。2. 环境
dsh-v0.2.1-alpha.4(commit86a402200d);desktop(Electron 应用内插件页)与web两侧装有同一插件link::desktop/package.json、web/package.json与两侧pnpm-lock.yaml均记录link:<repo>/dsh-model-extension,lockfile 解析为version: link:<repo>/dsh-model-extension3. 现场证据
3.1 link 形态(事故后仍在,可直接核验)
注意:链接本身还在,目标却已被掏空了一半。这与「删除器在删链接内容的阶段就异常退出、从未走到 unlink 链接」一致。
3.2 时间线(本地时间 = UTC+8,取自 NTFS 时间戳)
webprofile 一次插件操作profiles/web/.plugin-manager/logs/operation-w306W0;pnpm-lock.yamlmtime 09:24:27web/package.json重写.git最后一次条目增删(删除器正在.git内逐条删).git目录 mtime(重建前实测值)desktopprofile 插件操作记录生成profiles/desktop/.plugin-manager/logs/operation-5EHkrq(mtime = 2026-10-16T01:43:57Z)desktop/node_modules条目变动desktop/package.json+pnpm-lock.yaml重写、.plugin-manager更新3.3 被删内容严格呈「目录项字母序 + 中途停止」形态
.git内被删(全部字母序在objects之前).git内幸存(objects及之后)branches、COMMIT_EDITMSG、config、description、HEAD、hooks、index、info、logsobjects、ORIG_HEAD、packed-refs、refs仓库根同理:
.deepseek-harness(字母序在.git之前)被整体删除;.github、assets、docs、lib、plans、scripts、src、upgrade plan、package.json等排在.git之后,因中断而幸存。objects一个对象都没被删,用统计独立性排除「删了一部分」:4. 根因分析
4.1 卸载链路自身不做删除
packages/boot/plugin-manager/src/index.ts:686-717的removeBundle()只做runPnpm([+remove+, name, …])(见该文件 711 行),删除动作交给 pnpm 子进程完成。插件管理层这一层没有任何「链接目标不可触碰」的守卫。4.2 仓库里唯一的 junction 防呆,恰好在关键判定上失效
apps/desktop/src/owned-directory.ts的注释已经明确承认这个风险:它为此手写了
removeOwnedDirectory()。但该函数对根路径的判定用的是lstatSync:在 Windows 上,Node v24 的
lstat()不会把 junction 识别为符号链接。 我在本机同一 Node 运行时上实测(临时目录里创建 junction 后立刻探测):isSymbolicLink()isDirectory()fs.lstatSync(junction)fs.statSync(junction)fs.readdirSync(parent, { withFileTypes: true })得到的Dirent于是对
removeOwnedDirectory(<node_modules>/<link 包名>)而言:根级lstat判定「这是个普通目录」→ 保护短路 →readdirSync(path)穿过 junction 读到的是源仓库根的内容 → 进入逐条递归删除。函数自己的注释说要防的那件事,被它自己踩中了。矛盾点非常集中:同一个 junction,
Dirent认得、lstat认不得。该函数遍历子项时用了正确的Dirent语义,只在根路径上用错了lstat—— 这不是「作者没想到」,而是一个可以一行修掉的判定错误。4.3 这层防呆本来也覆盖不到卸载路径
grep -rn removeOwnedDirectory显示调用方只有:apps/desktop/scripts/test-host-updates.ts、test-workspace-updates.ts、tests/owned-directory.spec.ts、tests/fixtures/owned-directory-smoke.mjs—— 全部是 Desktop 更新自测脚本与测试夹具。插件卸载路径没有使用它,也没有任何等价保护。因此即使 §4.2 被修好,link:插件的卸载仍然没有防线。4.4 事故为什么「只删了一半」
删除按目录项顺序推进,在
.git中删完logs之后、处理objects时终止。可能原因无法从记录中确证(见 §6.1):或首个unlinkSync遭遇EPERM(对象文件被 git 进程 / 索引器 / 杀软占用)抛错冒泡中止,或 pnpm 子进程被超时/取消终止(github-connection.ts的注释表明 DSH 会 terminate its descendants)。无论哪一种,用户数据能否保住取决于删除何时失败。5. 复现步骤
5.1 最小语义复现(不需要安装 DSH,直接验证 §4.2 的判定缺陷)
5.2 端到端复现(请在隔离目录进行,切勿指向真实仓库)
6. 附带发现(同一次事故暴露的次要问题)
7. 期望行为与建议修复
期望:卸载 / 清理
link:(或file:)安装的包时,只移除 profile 里的链接条目并回写 manifest 与 lockfile,绝不触碰链接目标的内容。removeOwnedDirectory()的根级判定:与子项遍历统一改用readdir的Dirent(或对根路径先取父目录Dirent),使 junction / symlink 一律走unlinkSync分支、不进入目标。removeBundle()在删除node_modules/<name>前先判定 reparse point,命中即只删链接。realpath(link)与realpath(profile/node_modules),若目标的真实路径不在 profile 内,拒绝递归删除并明确报错。link:安装的包给出显式标记,并在卸载二次确认里写明「仅移除链接,源目录保持不变」,让语义对用户可见。本机实测可用于第 1/2 条的替代判据(供参考):
fs.readlinkSync(junction)在部分受限环境下也会失败,因此不建议用 readlink 做探测;Dirent.isSymbolicLink()是本次实测中唯一稳定正确的信号。All reactions