dsh-disk-doctor
Verifieddsh-disk-doctor · v0.1.1 · MIT · Web UI
Disk accounting for DeepSeek Harness with a reversible cleanup: where the gigabytes went, what is regenerable, and what is your content. It never deletes — a confirmed cleanup renames files into the plugin's own quarantine and can be undone with one click
Install
dsh plugin add dsh-disk-doctor Confirm the layer applied with dsh --profile default --dump-config — see the install guide.
Source
Tags
Creators
Readme
dsh-disk-doctor
DSH 从不删东西,所以这个数字只会涨。
dsh-disk-doctor把 DeepSeek Harness 的空间构成摊开给你看:三个区并排、每一类占多少、哪些是可再生的、哪些是你自己的会话内容。
v0.1 从不删除任何东西。 它只统计、只报告,唯一的「改动」是一次 rename(改名移动):当你在设置里打开清理、并二次确认之后,🟢「可以安全清理」的文件夹会被移进插件自己的隔离目录 ~/.dsh/disk-doctor/quarantine/ 下——不是复制,不是删除,一键就能撤回。除此之外它做的一切都是只读的。
能力声明
装之前先看这一段,它就是全部契约。
| 问题 | 回答 |
|---|---|
| 读什么? | 只读目录元数据(stat 大小):$DSH_HOME(默认 ~/.dsh)下、系统临时目录里以 dsh- 开头的目录,以及 DSH 的两份安装与两个系统缓存(见下)。不打开、不解析、不上传任何文件内容;唯一的例外是 storages/workspace.json,只用来给每个会话文件夹标上真实工作区标题。 |
| 写什么? | 每份报告正好两个文件:disk-report-<时间戳>.md 与 .json,都写在同一个目录(默认 <dshHome>/disk-doctor/reports)。被确认执行的清理会额外写出一个隔离批次:被移动的文件夹本身(一次 rename,字节没变,只是换了位置),以及一份 manifest.json——它在每移动一项之后都会重写,所以哪怕中途被打断,盘上的记录仍然和现实一致。全程不删除、不复制。 |
| 会删除或修改吗? | 它从不删除。 代码里没有任何删除路径:有一条测试禁止整个包出现 unlink、unlinkSync、rmdir、rmdirSync、rm(、rmSync、trashItem、Remove-Item 与 shutil.rmtree。被确认的清理做的事是把绿色档文件夹 rename 进插件自己的隔离目录——整个清理模块只有这一个改动调用(renameSync),并且有测试断言它只出现在一个文件里(lib/cleanup.js)。清空隔离目录始终是你自己的事,在本插件之外完成。 |
| 联网吗? | 不联网。 没有 fetch、没有 http/https、没有 socket;同样有测试断言。 |
| 会出界吗? | 会——为了那些它只统计、但并不拥有的目录:DSH 的两份安装(打包应用与 npm 全局那份)、桌面端用户数据目录、pnpm 内容仓库。这些都是探测出来的而不是猜的(见「三个区」一节),只读测量,并且在报告里写明位置。默认仍然不跟随符号链接与 junction,所以只有链接才能到达、且在树外的字节不会被计入。 |
| 有遥测吗? | 没有,代码里没有任何统计上报。 |
| 会打印我的真实路径吗? | 看这段文字去哪儿。面板上显示的是真实路径——因为你要在那里复制路径、在那里打开文件位置。凡是会离开这台机器的输出——工具返回值、Markdown 报告、JSON 报告——都会把主目录替换成 ~,于是 C:\Users\<你>\.dsh\… 变成 ~\.dsh\…。这个区分是刻意的:面板要给出可复制的路径,而离开本机的输出不应携带账户名。 |
| 会开端口或对外提供什么吗? | 它在 DSH 自己的本机 web 服务上注册了三条路由,自己不监听任何端口。GET /dsh-disk-doctor/report 返回的内容和工具返回的 JSON 完全一样,供右侧栏面板取数,且从不写文件;GET /dsh-disk-doctor/cleanup 返回清理预演清单以及已经在隔离目录里的批次;POST /dsh-disk-doctor/cleanup?action=move 执行移动、POST …?action=undo&id=<批次> 把一个批次移回去——两者在清理开关没打开时都会被 403 disabled 拒绝,而且都会重新读一遍当前设置,所以开关不需要重启。POST /dsh-disk-doctor/open 让宿主在系统文件管理器里打开一个文件夹——只接受 POST,并且只允许扫描真正覆盖到的根目录之内的路径。🟢「可以安全清理」的位置开箱即可打开;🟠「要谨慎处理」与 ⚪「不要动」两档需要另一个默认关闭的开关。没有 web 服务时三条路由都不注册。三条路由在做任何事之前都会先问宿主自己的浏览器信任栅栏——和官方路由同一套 Host/Origin 校验与浏览器身份认证——所以你只是访问过的一个网页无法对它们发起 POST 去移动文件或弹出文件管理器窗口。 |
这些声明同时是机器可读的:清单里 dshhub.permissions.network 声明为空数组(一个来源都不访问),另有 dshhub.surfaces: ["host", "web"] 与 schemaVersion: 1。测试会把该声明与代码绑定——两个半边里只要出现对外网络调用就失败;而浏览器半边对本机自己的相对路由做 fetch 是允许的。
你会得到什么
右侧栏的一个面板。 点开 磁盘空间,看到三个区上下排开——DSH 数据、DSH 程序、系统缓存,每个区有自己的数字、每个分类一条横条,点开某条能看到里面最大的几个文件夹,外加一句「删了会怎样」。最上面那个数字属于数据区,所以它写的是「DSH 的数据占了…」,而不是暗示它统计了整个 DSH。每行还有一个 打开 按钮,点了会在文件管理器里定位到那个文件夹:🟢「可以安全清理」的行直接就能点,🟠 和 ⚪ 的行在没打开「敏感位置」开关之前是灰的。面板底部有一份颜色说明,把三个小圆点的含义逐条写清楚。三个区下面还有两个方框:一个是清理方框(标题为「清理」),先列出「如果清理会移动什么」,然后——只有在你打开清理开关并二次确认之后——才把这些文件夹移进插件自己的隔离目录,并给出 撤销 按钮和仍在隔离目录里的其他批次(见下面的「清理:预演、移动与撤销」);另一个是「想省更多空间?这样做才对」的指路说明。链接安装的插件单独列一份——它们的真实文件在所有被扫描的文件夹之外。第一次会自动打开。要给不懂技术的人看,就是这个界面。
一个 disk_report 工具。 同样的数字,可以让模型按需跑一次,输出 Markdown 摘要并写出 .md / .json 报告文件。返回值里多了 areas 与 linkInstalls 两块,每个分类也会写明自己属于哪个区、有多少字节与别的区重叠。
数据区里的三个视图。 总览每个分类一片;按插件与按聊天记录则直接写出条目的名字。后两个视图的图例右上角有一个小小的 「显示完整路径 / 显示名称」 按钮,在名称与完整路径之间切换——它改的就是 ⚙ 里那个「显示完整路径」偏好,所以两个控件不可能各说各话。聊天记录用的是真实工作区标题加一小段会话编号(disk-doctor · 64875ca0,而不是 E\disk-doctor · session-),可展开的列表行用的是同一套名字。插件则保持包名(dsh-image-gen、@scope/pkg):更好看的 displayName 存在每个插件自己的 package.json 里,去读它就破坏了「只打开过 storages/workspace.json 一个文件」这个承诺。
插件页面上的 8 个设置项(插件 / Plugins → Installed 里那张卡片):三个明面字段、五个收在高级选项里,其中两个是开关。此外还有第 9 个字段 allowCleanup,它住在面板自己的 ⚙ 齿轮里——因为它管的就是面板上那个按钮。它同样是宿主设置,存进同一份 profile patch。每一项的大白话解释、以及怎么把插件交给别人,见 USAGE.md。
v0.1 从不删除。 被确认的清理只会把 🟢「可以安全清理」的文件夹移进插件自己的隔离目录,再点一次就能移回去,清空那个目录始终是你自己的事。插件与对话永远不会被碰——要卸插件请照常去插件页卸载。
三个区,以及为什么没有「总计」
报告把整台机器分成三个区,每个区各自给出总数,并排显示:
| 区 | 是什么 | 在这台机器上 |
|---|---|---|
data |
DSH 的数据文件夹——$DSH_HOME,默认 ~/.dsh:会话内容、存储与投影缓存、插件 node_modules、自带运行时,以及 ~/.dsh/electron 那份解包后的 Electron 运行时。 |
735 MB |
app |
DSH 程序,一台机器上它可能有两份:打包后的 Electron 应用本体(本机是 E:\dsh1),以及 npm 单独装的那份全局 DSH(本机是 %APPDATA%\npm\node_modules\@deepseek-ai\dsh,254.8 MB)——profile 里那个共享 node_modules 基本就是指向它的 junction 农场。只读:这些目录归卸载程序管,不归本插件管。 |
1270 MB(1.2 GB) |
cache |
DSH 使用但并不拥有的系统缓存:桌面端的 Chromium 用户数据目录(%APPDATA%\@deepseek-ai\dsh-desktop)与 pnpm 内容仓库(%LOCALAPPDATA%\pnpm)。 |
126 MB |
三个区永远不相加,也不打印任何「总计」。 pnpm 内容仓库里放的是插件 node_modules 硬链接过去的真实文件,把三个区加起来会把同一批物理字节算两遍。所以 pnpm-store 这个类别会报 overlapBytes——按 inode 量的——缓存区也会用自己的话把这件事说清楚。本机上 112 MB 的 pnpm 仓库里有约 109 MB 与数据区重叠。
面板与摘要最前面显示的数字是数据区——真正属于你的那部分 DSH——所以面板写的是「DSH 的数据占了…」,而不是暗示它统计了一切。
本机实测于 2026-10-07。
环境要求
- DeepSeek Harness
>=0.1.0-rc.2(清单里以engines.dsh声明)——0.2.0-rc.2与0.2.1-rc.2都实测过;超出声明范围的版本会被 DSH 自己拒绝加载,而不是悄悄跑起来 - Node.js
^22.19.0 || >=24.0.0 - 零运行时依赖、零构建,也不需要打包器——浏览器半边是用 web 客户端自己的模块格式手写的
安装
已发布到 npm,装完重启 DSH 即可。npm 不可用时也可以从本地文件夹安装——开发工作副本见 TRIAL-INSTALL.md,完全不打命令的路线见 USAGE.md(插件 → 添加插件)。
dsh plugin --profile <profile> add dsh-disk-doctor
dsh plugin 会在 profile 目录里转发给 pnpm;插件管理器默认会把新装的 bundle 选入,所以插件声明的 dsh.bundle.patch 会自动插入 profile 的分层栈。把包名换成本地绝对路径同样可用。
这条命令要求 pnpm 在 PATH 上;如果不在,见 TRIAL-INSTALL.md 里那行使用 DSH 运行时自带 pnpm 的转接写法。
装好并重启后:右侧栏出现 磁盘空间 面板,disk_report 工具可用,插件自己的页面上(插件 / Plugins → Installed 里那张卡片)出现 8 个配置项——第 9 个「允许清理(移动到回收站)」刻意放在面板自己的 ⚙ 齿轮里,紧挨着它管的那个按钮。
使用
直接用自然语言问即可——「DSH 占了多少磁盘?」——或者显式调用工具。
disk_report
| 参数 | 类型 | 默认 | 含义 |
|---|---|---|---|
scope |
"summary" | "full" |
"summary" |
summary 只看分类汇总;full 额外在每类下面列出 Top N 明细。 |
topN |
整数 1..100 |
取插件配置 | 每个类别列出的条目数。超范围会直接报错,不会静默截断。 |
write |
布尔 | true |
是否写入 Markdown 与 JSON 报告文件。 |
categories |
字符串数组 | 全部 | 只统计这些类别——合计也随之变化。未知 id 会报错。 |
工具返回:进入对话的 Markdown 摘要、写出的报告文件绝对路径、以及 JSON 结构。
输出示例
以下是一次真实运行的输出(路径与逐项数字做了缩写):
### DSH 空间体检 — DSH 数据 735 MB / 8,903 个文件
> 数据文件夹:~\.dsh
├─ 会话日志 2.73 MB (3 项,最大 1.15 MB)
├─ 存储与投影缓存 0.13 MB (5 项,最大 91.5 KB)
├─ 插件 node_modules 109 MB (511 个链接未跟随,23 项,最大 62.3 MB)
├─ 插件安装日志 0.00 MB (最大 3.70 KB)
├─ 市场数据 0.02 MB (最大 17.8 KB)
├─ 运行时依赖 272 MB (4 项,最大 165 MB)
├─ 临时目录与溢出 3.82 MB (10 项,最大 3.24 MB)
├─ 附件 0.27 MB (最大 279 KB)
├─ Electron 运行文件 347 MB (21 项,最大 215 MB)
└─ 其他 0.10 MB (19 项,最大 22.8 KB)
一共 **735 MB**:**3.84 MB** 可以安全清理(来自 临时目录与溢出 3.82 MB、市场数据 17.8 KB、插件安装日志 3.70 KB),**109 MB** 需要正确操作才能省下(例如卸载不用的插件),剩下 **622 MB** 请不要动。三部分相加就是总数。
**DSH 程序 — 1.24 GB**
> 安装位置:E:\dsh1(自动找到);npm 全局安装:~\AppData\Roaming\npm\node_modules\@deepseek-ai\dsh;只读展示,不参与清理
├─ 程序安装目录 0.99 GB (22 项,最大 649 MB)
└─ 全局安装的 DSH(npm) 0.25 GB (8 项,最大 255 MB)
**系统缓存 — 126 MB**
> 这些是 DSH 之外的位置,与上面的区不能相加:桌面端用户数据 ~\AppData\Roaming\@deepseek-ai\dsh-desktop、pnpm 内容仓库 ~\AppData\Local\pnpm
> 注意:其中约 109 MB 与「DSH 数据」里的插件文件是同一批物理文件(硬链接),所以两个区的数字不能相加,也不是删掉就能省下这么多。
├─ 桌面端用户数据 13.9 MB (24 项,最大 9.30 MB)
└─ pnpm 内容仓库 112 MB (最大 112 MB)
**链接安装(真实文件在扫描范围之外,不计入上面的数字)**
- ~\.dsh\profiles\desktop\node_modules\dsh-disk-doctor
→ 真实位置 `<你的插件目录>\dsh-disk-doctor`,420 KB
扫描用时 7.2 秒
报告已写入 `~\.dsh\disk-doctor\reports\disk-report-20261007-094500.md`
这份输出里有三点值得点出来:数据区的数字已经在最上面那行标题里了,所以紧跟其后的是它的位置说明,不再重复一个标题;另外两个区各自带自己的标题和自己的数字,而且谁都不与别人相加——缓存区会用文字说明为什么。被截断的测量会写成 至少 <size>:如果统计链接目标时撞到自己的上限,那一行会写「至少 12.4 MB」,而不是假装精确。链接列表在渲染文档里最多 10 行,超出时最后一行写「…另有 N 个未列出」。
遍历是并行的——同时走 16 个子目录、并发 64 个 lstat——所以哪怕 npm 那份安装有约 33,000 个文件,三个区完整跑一遍也只要几秒。
scope: "full" 时,数据区下面还会列出最大的若干条目,每条都带绝对路径——所以任何一个数字都能下钻到构成它的文件。
配置项
这 9 个字段里,8 个在插件自己的页面上改,第 9 个(allowCleanup)在面板的 ⚙ 齿轮里改;任何一个也都可以写进 profile 的 Cordis patch。
表单在哪里
侧栏 → 插件 / Plugins → Installed → dsh-disk-doctor 卡片 → 它自己的页面上就是配置表单。表单通过 plugins.bundle.config 插槽贡献(以 bundle 的包名为键),所以只有这个 bundle 自己的页面会渲染它。如果宿主还没提供该插件的设置命名空间,表单会如实说明,而不是整个消失。
如果你自己写 DSH 插件,这一点值得知道:光有 schema 不会产生页面。 dsh-settings 只从声明了 .volatile() 的字段投影表单(本插件 9 个字段都声明了),而没有任何已发布的客户端会按 schema 生成页面——autoGenerate 就是为此存在的,但没有东西消费它。页面必须由插件自己的浏览器半边提供,所以本包除了 lib/ 还带了一个 client/client.js。插件页面渲染其中 8 个字段——先是三个明面字段,另外五个收在高级选项里;第 9 个(allowCleanup)由面板的 ⚙ 齿轮渲染,走的是同一套共享配置表单,所以两个界面不可能各说各话,齿轮里的保存也和设置页保存一样会落到 profile patch。齿轮显示的是宿主生效值;如果这个值被 home patch 或命令行覆盖锁住,它会直接说明改不了。
同样这 9 项写在 YAML 里
- id: disk-doctor
name: 'dsh-disk-doctor'
config:
topN: 20
followSymlinks: true
allowOpenFolderSensitive: true
allowCleanup: true
两条路等价:表单写入的也是同一份 profile patch。无头部署、或需要在首次渲染前就定好的值,直接写 patch 更合适。表单里仍然文本编码的字段只剩两个,因为共享表单控件就是一个文本输入框:excludeGlobs 用英文逗号分隔,followSymlinks 填字面量 true 或 false。三个权限项都是**开关(switch)**而不是让你手打 true/false 的输入框——allowOpenFolder 与 allowOpenFolderSensitive 在插件页面上,allowCleanup 在面板的 ⚙ 齿轮里;它暂存的值走的仍是同一套文本编码,所以保存时写进 profile patch 的内容完全没变。任何字段清空即表示移除覆盖、回落到默认值。
| 字段 | 类型 | 默认 | 含义 |
|---|---|---|---|
dshHome |
字符串 | $DSH_HOME,否则 ~/.dsh |
要统计的数据文件夹。它完全跟着宿主走:设了 DSH_HOME 就用它,否则用 ~/.dsh——所以在别人机器上默认值就已经是对的。 |
topN |
整数 1..100 |
10 |
每个类别列出的条目数。 |
excludeGlobs |
字符串数组 | [] |
额外排除的绝对路径通配符,例如 C:\Users\me\.dsh\big-*。* 不跨路径分隔符,** 跨。 |
reportDir |
字符串 | <dshHome>/disk-doctor/reports |
报告输出目录,支持 ~;路径会用 realpath 规范化,因此符号链接无法悄悄把写入重定向到别处。这是唯一的可写位置;指到 $DSH_HOME 之外是允许的,但每份报告都会明确标注这一点。 |
followSymlinks |
布尔 | false |
是否跟随符号链接与 junction。开之前请先读下面的「符号链接」一节。 |
appDir |
字符串 | 留空 = 自动探测 | DSH 程序安装目录。留空就按运行中的程序位置探测;填了就以你填的为准,报告里那一区会标成「来自设置」而不是「自动找到」。只有探测不到时才需要填。 |
allowOpenFolder |
布尔 | true |
页面上是一个开/关开关。面板 打开 按钮的总开关,默认开着,所以 🟢「可以安全清理」的行开箱就能点开文件夹。把它关掉则会灰掉所有按钮,宿主也会直接拒绝。 |
allowOpenFolderSensitive |
布尔 | false |
页面上是一个开/关开关,默认关着。在总开关之外另一道独立的同意:打开后,🟠「要谨慎处理」与 ⚪「不要动」两类位置也能点开——那可能是你的对话目录,也可能是程序安装目录。 |
allowCleanup |
布尔 | false |
一个开/关开关,默认关着;它是唯一一个在面板的 ⚙ 齿轮里改的字段(齿轮里的第 4 个控件,在分隔线下面、三个外观偏好之后)。关着时移动按钮是灰的,宿主对 POST …?action=move 回 403 disabled。打开它本身不会移动任何东西:第一次点击只会把按钮改成「确定,移动」并弹出询问行,第二次点击才真的移动。设置改动在下一次请求就生效,不需要重启。 |
关于「打开」按钮
面板上的每一行都有一个 打开 / Open 按钮,由两个开关共同把关,而这两个开关故意不是同一个:
allowOpenFolder是总开关,而且默认开着:🟢「可以安全清理」的行直接就能点开文件夹,什么都不用配。它同时是「急停」开关——关掉后每个按钮都变灰,宿主也会以disabled直接拒绝。allowOpenFolderSensitive默认关着,是针对 🟠「要谨慎处理」与 ⚪「不要动」两行的第二道、独立的同意。在人家的对话目录或程序文件旁边打开文件夹,比打开一个临时目录要重得多,所以它不跟着总开关一起生效。
两者在设置页上都是开关行——一行标签、一行提示,外加一个只有被改过之后才出现的「恢复默认」小控件。它暂存的值走的仍是同一套文本编码,所以保存时写进 profile patch 的内容没变。
宿主会拿路径去跟扫描真正覆盖到的根目录做包含判断(取覆盖它的最具体的那个根),不在其中就拒绝;这条路由只接受 POST,所以误点一次导航或一次预取都不会弹出窗口;文件只会定位到它所在的文件夹,永远不会被当成文件交给别的程序打开。链接安装的插件不给按钮——它们的目标按定义就在所有被扫描的根之外——只给一个复制按钮。
按插件与按聊天记录两张图里的每一条图例行同样带「打开」按钮,而那些行是 🟠 与 ⚪——所以两张图一开始所有按钮都是灰的。这种情况下,图自己的右上角会写清楚原因和该去打开哪个开关:只差敏感开关时写「要在设置里打开『允许打开需谨慎处理的位置』才能点开」,总开关被关掉时写「要打开文件夹,得先去设置里把『允许打开文件夹』打开」;按钮一旦能用,这句话就消失。总览画的是分类而不是文件夹,所以它刻意不提供名称/路径切换。
颜色说明
同一份说明出现在两个地方:面板底部、范围说明的上方(紧凑模式下隐藏),以及设置页高级选项里两个开关的正下方——因为那里正是读者被要求判断「橙色和灰色那些要不要开」的地方。
| 圆点 | 档位 | 含义 |
|---|---|---|
| 🟢 | 可以安全清理 / Safe to clean up | DSH 自己能重新生成的,清理不影响使用。 |
| 🟠 | 要谨慎处理 / Handle with care | 能用正确方法省下的,比如去卸载不用的插件,而不是手动删文件。 |
| ⚪ | 不要动 / Leave alone | DSH 运行需要的,以及你自己的对话和附件,删了会出问题。 |
扫描范围
每个类别都会先探测存在性;路径不存在时跳过并在报告的 warnings 里记一行,而不是悄悄消失。每个类别都写明自己属于哪个区,所以三个区的合计不会互相串味:「其他」只是数据文件夹的兜底类别,永远不兜整台机器。
| 区 | id | 路径 | 可再生 | 处理建议 | 说明 |
|---|---|---|---|---|---|
| data | sessions |
<dshHome>/sessions/** |
否 | 不要动 | 会话内容。按会话排行,并标注真实工作区标题与日期。 |
| data | storages |
<dshHome>/storages/** |
否 | 要谨慎 | 存储与投影缓存;session_projcache/sessions/*.json 逐个列出。 |
| data | profile-deps |
<dshHome>/profiles/*/node_modules/** |
是 | 要谨慎 | 第三方插件依赖,按包排行。 |
| data | profile-logs |
<dshHome>/profiles/*/.plugin-manager/logs/** |
是 | 可以清理 | 插件安装日志。 |
| data | profile-market |
<dshHome>/profiles/*/.dsh-market/** |
是 | 可以清理 | 市场状态与日志。 |
| data | plugin-cache |
<dshHome>/plugin-packages/** |
是 | 可以清理 | 下载的插件包缓存。 |
| data | runtimes |
<dshHome>/dsh-runtimes/** |
否 | 不要动 | 内置 node/python/pnpm,按组件排行。 |
| data | temp-spill |
%TEMP%/dsh-* |
是 | 可以清理 | 溢出目录与会话私有临时目录。 |
| data | attachments |
<dshHome> 下任意 *attach* 目录 |
否 | 不要动 | 探测式;找不到会记 warning 并归入「其他」。 |
| data | runtime-app |
<dshHome>/electron/** |
否 | 不要动 | DSH 自己运行所依赖的那整套解包 Electron 运行时。它原来的名字是「DSH 程序本体」,现改名为 Electron 运行文件,免得与下面新的「程序安装目录」区撞名。 |
| data | other |
<dshHome> 的剩余部分 |
否 | 要谨慎 | 兜底类别,保证数据文件夹不漏账;排除以上所有 data 根。 |
| app | app-install |
DSH 程序安装目录(探测得出,或用 appDir) |
否 | 不要动 | 打包后的 Electron 应用本体。只读;这个目录归卸载程序管。探测不到时会用大白话写「没找到」。 |
| app | npm-global-dsh |
%APPDATA%/npm/node_modules/@deepseek-ai/dsh,或 npm prefix 下的位置 |
否 | 不要动 | 第二份安装:全局的 dsh CLI——桌面应用与它甚至读不到对方的 profile。只有该目录确实是个含 package.json 的目录时才会被采纳。 |
| cache | chromium-data |
%APPDATA%/@deepseek-ai/dsh-desktop |
否 | 要谨慎 | 桌面窗口的用户数据:登录状态与界面缓存。 |
| cache | pnpm-store |
%LOCALAPPDATA%/pnpm |
否 | 要谨慎 | pnpm 内容仓库。带重叠探针:它的 overlapBytes 是已经被数据区算过一遍的那部分字节,按 inode 量。 |
在数据区内,$DSH_HOME(加上临时目录)下的每一个字节都恰好落进一个类别,所以数据区各部分之和等于数据区的总数。程序区与缓存区各有各的总数;三个总数永远不相加(见「三个区,以及为什么没有『总计』」)。
「其他」的定义是数据区那些根的补集,而不是一串固定路径。所以用 categories 收窄范围时,合计会跟着变;只选「其他」时,它报告的是未被选中的那些类别没有覆盖到的部分。
数据区之外那些目录是怎么找到的
不猜,能算出来的也绝不问用户:
- 数据文件夹完全按宿主读取
DSH_HOME的方式跟随它,取不到时才回落到~/.dsh。所以有人把数据目录搬走了,这里也能自动统计对,不需要任何配置。 - 程序安装目录从正在运行的可执行文件推出来——
process.execPath,并以process.resourcesPath与DSH_DESKTOP_NODE_EXECUTABLE作为后备——而且只有那个目录里真的存在resources/app.asar才会被采纳。所以无头的node部署会老老实实报「没找到」,而不是随便挑一个旁边的目录。填了appDir就覆盖探测,此时该区的说明会写「来自设置」而不是「自动找到」。 - 第二份安装是 npm 那份。桌面应用与全局
dshCLI 在同一台机器上是两套安装(彼此连对方的 profile 都读不了),而 profile 里那个共享node_modules基本就是指向 npm 那份的 junction。它会从%APPDATA%\npm\node_modules、npm_config_prefix下的两个位置(lib/node_modules与node_modules)、~/.npm-global/lib/node_modules以及 Unix 上惯用的全局根目录去探测,并且只有候选目录里确实有package.json才算数。漏掉它,就会有四分之一 GB 藏在一份看起来很完整的报告旁边——这正是这个区要防的那种失败。 - 两个缓存目录来自
%APPDATA%与%LOCALAPPDATA%,在其他平台上回落到.config与Library/Application Support。
链接安装的插件
用链接方式装的插件——dsh plugin add <本地路径>,开发中的 dsh-disk-doctor 自己就是这么装的:它住在 <你的工作副本路径>,却出现在 ~/.dsh/profiles/<profile>/node_modules/ 下——它的字节没有一个真的落在被扫描的树里,所以本来会从报告里整个消失:装了个东西却什么也看不到的人,会开始怀疑其他每一个数字。因此它单列一节:链接本身、真实指向哪里、以及那个目标的真实大小。这些字节永远不会被加进任何一个区的总数。
判断的依据是目标,不是链接本身。如果链接的目标就在某个已覆盖的根里面,那它就不再单列——那些字节已经算在所在的区里了,再说它们「不在任何数字里」就是假话。本机上 512 个链接里有 510 个指向上面那份 npm 安装,报告会用一行说明这件事,只列出真正指向外面的那一个。
这项测量在单个根与整体两个层级上都有上限,渲染出来的文档最多列 10 行;撞到上限的那次测量会写成 至少 <size>(「至少」),而不是报一个它并没有量准的数字。
报告产物
disk-report-<YYYYMMDD-HHMMSS>.md—— 完整人类可读报告:每个区各一节,各自带构成树与自己的总数,然后是链接安装清单、注意事项、以及每个类别的 Top N 表格。disk-report-<YYYYMMDD-HHMMSS>.json—— 稳定的机器契约,"schema": "dsh-disk-doctor/report@1",本次是增量扩展(additive),没有破坏@1。它带totals(照旧是数据区)、新增的areas、linkInstalls与capabilities(openFolder、openFolderSensitive、cleanup),以及warnings;categories[]里的每一项仍带topItems,并新增了area与overlapBytes。
工具自身的返回值也多了同样的东西:顶层的 areas 与 linkInstalls,以及每个类别上的 area/overlapBytes。每个 topItems 条目现在还会带 mtimeMs——那一条里最新的文件修改时间——而且它是工具输出 schema 里的必填字段,因为清理判断「最近有没有在用」就靠它。
清理:预演、移动与撤销
清理方框分三个阶段,每一步都是一次独立的、需要你主动点的动作。
1. 预演:列出会移动什么
点 「看看会移动什么」,宿主会从 GET /dsh-disk-doctor/cleanup 画出一份清单("schema": "dsh-disk-doctor/cleanup@1",第一个字段是 dryRun: true);同一个响应还会带回隔离目录里已有的批次。清单背后的规则:
- 只有 🟢「可以安全清理」的类别参与——
profile-logs、profile-market、plugin-cache、temp-spill。其他每个类别都会出现在refused[]里并附上它的安全等级,所以「没参与」是明说的而不是悄悄漏掉:⚪ 里是程序本体、自带运行环境和你的对话;🟠 里是装好的插件,而插件「移走再重启看看」是必然失败的。 - 只动已经安静了一小时的东西。 最新文件在一小时内被碰过的条目不会进清单,而是记进
skipped[],reason: "recent"(RECENT_USE_MS = 60 * 60 * 1000)。判断用的是条目内部最新的 mtime,不是文件夹自身的时间戳——进程一直在往目录里写文件时,目录自己的 mtime 是不动的。一台在干活的机器上,绿色档里最大的那一项通常就是当前会话自己的溢出目录,把它从活着的进程脚下搬走不叫清理。完全无法检查的条目同样跳过。 - 是全部,不是 Top N。 报告每个类别只带 10 条——那是清单最不该有的形状:四十个文件夹里只列十五个,等于在「会移动什么」这件事上说谎。所以预演用
PREVIEW_ITEM_LIMIT = 500的上限把安全类别重新枚举一遍,某一类超过上限就置truncated: true并附一条提示。 - 落点只是算出来的。 清单会写出
<dshHome>/disk-doctor/quarantine/<YYYYMMDD-HHMMSS>,但不会创建它。
2. 移动:只 rename,不复制、不删除
allowCleanup(默认关着)才是让移动变成真的那个开关,而它由面板自己的 ⚙ 齿轮控制——齿轮里的第 4 个控件,在分隔线下面、三个外观偏好之后。关着时 移动到回收站 按钮是灰的并写明原因,宿主对移动请求直接回 403 disabled。打开之后,那个按钮上就是字面意义上的两次点击:第一次把按钮文字改成「确定,移动」并弹出询问行——「确定移动这 N 项(共 X)吗?」加「确定,移动」与「取消」——第二次点击(改过字的按钮,或询问行里的「确定,移动」)才真的移动。询问期间按钮保持可点:禁用按钮根本收不到点击,这正是这一版最初发出一个「看着能点、点了没反应」的按钮的原因。然后,逐项:
- 动手之前会把计划重新画一遍,所以永远不会按一份过期的清单行动:你在看预演之后才被写过的条目会直接掉出这次移动,并记进
skippedRecent。 - 每一项都会重新校验归属。
lib/targets.js的ownsCleanupPath()回答的是「绿色档清理是否有权拥有这个路径」,而且与它此刻是否存在无关——这份独立性正是关键:整目录条目(.dsh-market、某个dsh-spill-*目录)自己就是根,一移动它就不存在了。每个绿色类别各自声明规则:两种 profile 叶子形状(<profiles>/<profile>/.dsh-market与…/.plugin-manager/logs)、一个目录前缀(<dshHome>/plugin-packages),以及临时溢出的形状规则(<tempDir>/dsh-*)。改用扫描的根列表来判断,会把用户自己的文件判成「不能放回去」。 - 条目被 rename 到
<隔离目录>/<时间戳>/<分类 id>/<它在自己根下的原始布局>,所以打开隔离目录仍然看得出东西原来在哪。两个 profile 都有.dsh-market,因此名字已被占用时会加-2、-3……后缀。 - 每移动一项都会重写清单,所以中途被打断也留着与现实一致的记录。批次文档是
dsh-disk-doctor/quarantine@1,状态从moving走到moved/moved-with-failures;计划为空时一个文件夹都不会创建(nothing-to-move)。
移动失败时,它会说明三种原因之一,而不是让你看着什么都没发生:权限没开(disabled)、宿主还是旧版本(这个 POST 接口还不存在——重启 DSH 再试一次),或者根本连不上宿主。
renameSync 是清理模块里唯一的改动调用。清空隔离目录始终是你自己的事,在本插件之外完成。
3. 撤销:把每一项放回去,或者拒绝
方框会为刚移动的那个批次、以及仍在隔离目录里的每个批次提供 撤销(listQuarantine() 按新的在前列出)。在发生任何一次 rename 之前,undoCleanup() 会重新校验一切:批次 id 必须是 YYYYMMDD-HHMMSS 时间戳,记录里的每个 target 必须在这个批次内,每个 original 必须是清理有权拥有的路径。所以手改过的清单没法把文件挪到别处去。
原位置如果已经重新出现,会被拒绝、绝不覆盖,并记进 failures,reason: 'occupied';恢复按倒序进行,所以嵌套条目先回去,包含它们的文件夹后回去。批次会留在 status: 'undone'(或 partly-undone),面板会说明哪些没能回去、为什么。
另一个方框 「想省更多空间?这样做才对」 之所以存在,是因为在这里删文件夹几乎不省空间(pnpm 仓库与插件目录是同一批字节的硬链接),或者会把 DSH 弄坏(运行环境与程序本体):它会点出最大的那个插件并指向插件页、写出 pnpm 仓库及其测得的重叠量并给出 pnpm store prune,再列出绝对不能动的东西。
真机实测(2026-10-07)
预演耗时约 45 ms;02:21 那次列出 8 项 / 3,854.8 KB,同时把 466,243 字节因为「太新」而跳过。随后对本机已经死掉的临时溢出目录做了一次真实移动:6 项 / 3,833.3 KB,每一个原位置都空了,每一个目标都出现在 quarantine/<时间戳>/temp-spill/… 下,0 失败。撤销把 6 项全部移回,同样 0 失败,事后对所有「清理可涉及路径」做指纹比对逐字节一致,清单留在 status: 'undone'。隔离目录里剩下的批次(20261007-030514 及其后几个)只留着清单和一个空的分类目录,你自己删掉是安全的。
设计不变量
- 绝不破坏数据:v0.1 只写自己的报告,唯一的改动操作是把绿色档文件夹 rename 进插件自己的隔离目录——只限 🟢 档、只在开关打开时、只在二次确认之后,而且一路写清单,所以随时可撤。
- 可解释:每个数字都能下钻到构成它的文件,没有不可追溯的汇总值。
- 可回滚,而且是真的:每移动一项都会重写清单,撤销会先拿清单与磁盘重新校验:目标不在批次内、原位置不在安全根内、或原位置已经重新出现,一律拒绝而不是照做——绝不覆盖。这里不删任何东西。
- 不出界:默认只扫描数据文件夹与会话临时目录;DSH 的两份安装、桌面端用户数据目录与 pnpm 内容仓库是探测出来的、只读测量,并且在报告里写明位置。所有路径先经
realpath规范化再比对,报告目录仍是唯一的可写位置,而唯一能触达文件管理器的那条路由会拒绝扫描没覆盖到的任何路径。 - 默认保守:不跟随符号链接;扫描有上限(50 万文件或 30 秒),到顶就标注
truncated: true,而不是把宿主跑死。
符号链接与 junction
默认情况下,链接会被计数、记录,但不会被穿过。这是刻意的:在 pnpm 布局的 profile 里,profiles/node_modules/* 是指向全局 npm 目录的 junction,穿过去既出了 $DSH_HOME,又会把同一批字节算两遍(一遍经链接、一遍经真实目录)。开启 followSymlinks: true 后会跟随,但按真实路径去重,每个真实目录仍然只算一次。
准确度
大小全部来自 stat,同一个文件不会被算两次。准确度有两条互相独立的验证:
- 测试套件断言:扫描器的总计与对同一份 fixture 独立递归求和的结果逐字节相等,且数据区各类别之和等于数据区的总数。
- 在真实安装上与 PowerShell 自己的测量交叉比对——
Get-ChildItem -Recurse -File | Measure-Object -Sum Length(排除 reparse point)。扫描器的文件集合与字节总数完全一致:双方都是 8,903 个文件、767,049,761 字节。
v0.1 的边界
做:只读统计、三个区并排、分类构成、Top N 明细、链接安装插件单列、Markdown + JSON 报告、9 个配置项、带颜色说明与「省空间指路」的右侧栏面板、能在文件管理器里定位文件夹的打开按钮(🟢 安全档默认可用,🟠 与 ⚪ 两档另需一道独立开关),以及清理——逐条预演、把绿色档移进自己的隔离目录、再一键撤销。
不做:任何删除,任何网络请求。清理一个字节都不会删——它只 rename、只拒绝覆盖、只碰 🟢 档;清空隔离目录是你自己在插件之外做的事。破坏性删除(L3)属于 v0.2+,会作为独立且需显式确认的层引入。
开发
npm test # 四套一起跑:locate、scan、plugin、client
npm run test:locate # DSH 把东西放在哪:数据文件夹、安装目录、两个缓存
npm run test:scan # 只跑扫描器与报告器
npm run test:plugin # 入口模块,用桩替换 peer 依赖驱动
npm run test:client # 浏览器半边,同样用桩驱动
test/plugin.test.mjs 会把 @deepseek-ai/dsh-tools 与 @deepseek-ai/schemastery 换成 test/stubs/ 里的桩来加载 lib/index.js,因此不需要装好的 DSH 宿主就能跑。test/locate.test.mjs 则把环境变量、可执行文件路径与文件系统探针都做成可注入的,所以那些探测规则不依赖它恰好跑在哪台机器上。
除了这套测试,插件还对着真实实现验证过——先是 0.2.0-rc.2,随后在 0.2.1-rc.2 下重新验证了工具定义:定义能被真实的 defineTool 编译;真实参数校验器会拒绝非法输入,真实输出校验器接受返回值;并且在真实 cordis 上下文里用真实的 ToolRuntime 服务端到端派发过——注册、投影 schema、走完整条策略流水线执行、并写出报告文件。装入 profile 仍是在某台具体宿主上的最后一道确认。
目录结构
lib/index.js 插件入口:Config、apply、disk_report 工具
lib/locate.js 这台机器上 DSH 把东西放在哪:数据文件夹、安装目录、两个系统缓存
lib/targets.js 区与类别表、条目枚举、工作区索引、清理归属规则
lib/scan.js 遍历、聚合、上限保护、inode 重叠探针
lib/cleanup.js 预演、移入隔离目录、清单与撤销
lib/report.js Markdown 与 JSON 渲染
lib/serve.js 报告路由、清理路由与打开文件夹的路由
lib/targets.js里的projectKey()精确复刻了 DSH 0.2.0-rc.2 的会话目录命名算法,所以会话能显示出真实的工作区标题。
许可证
MIT —— 见 LICENSE。