dsh-disk-cleaner 1.1.0 → 1.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +23 -13
- package/lib/catalog.js +189 -1
- package/lib/client.js +14 -0
- package/lib/index.js +2 -0
- package/package.json +1 -1
- package/smoke.mjs +2 -2
package/README.md
CHANGED
|
@@ -5,7 +5,7 @@
|
|
|
5
5
|
<!-- badges -->
|
|
6
6
|
| | |
|
|
7
7
|
| --- | --- |
|
|
8
|
-
| **version** | `1.
|
|
8
|
+
| **version** | `1.2.0` |
|
|
9
9
|
| **DSH** | `0.2.1-rc.5`(开发与实测版本;用到 `plugins.bundle.config`、`settings.section`) |
|
|
10
10
|
| **license** | [MIT](LICENSE) |
|
|
11
11
|
| **Node** | `>= 22`(仅运行冒烟测试需要) |
|
|
@@ -16,7 +16,7 @@
|
|
|
16
16
|
|
|
17
17
|
**dsh-disk-cleaner** is a disk-space plugin for [DeepSeek Harness](https://github.com/deepseek-ai) (DSH). It does two different jobs, and keeps them apart on purpose:
|
|
18
18
|
|
|
19
|
-
1. **Clean** —
|
|
19
|
+
1. **Clean** — nineteen known-safe locations (temp folders, package manager stores, browser caches, crash dumps, the Recycle Bin, leftover update installers, per-app caches and logs, superseded plugin versions and applied update packages). Only these are ever deleted.
|
|
20
20
|
2. **Assess** — everything the plugin will *not* delete (installed programs, the WSL virtual disk, the Windows component store, chat-app data, your own folders). Measured and listed, never removed.
|
|
21
21
|
|
|
22
22
|
The assessment ends with a **plan**: the same numbers ordered by what it costs you to act, with the exact Windows command for the steps the plugin cannot perform itself.
|
|
@@ -27,9 +27,9 @@ Both the panel and the agent reach the *same* operations: the cleaning logic liv
|
|
|
27
27
|
|
|
28
28
|
| Feature | Description |
|
|
29
29
|
| --- | --- |
|
|
30
|
-
| Scan | Measures all
|
|
30
|
+
| Scan | Measures all nineteen targets concurrently and shows what each occupies |
|
|
31
31
|
| Per-drive filter | Chip row of mounted volumes; scanning or cleaning can be limited to one drive, and rows with nothing on that drive disappear |
|
|
32
|
-
| Risk labels | Every target is `safe` or `caution`, with a one-line reason (`in-use` / `rebuild` / `elevation` / `pending-update`) and a permanent legend, so "caution" never means "unspecified" |
|
|
32
|
+
| Risk labels | Every target is `safe` or `caution`, with a one-line reason (`in-use` / `rebuild` / `elevation` / `pending-update` / `rollback` / `superseded`) and a permanent legend, so "caution" never means "unspecified" |
|
|
33
33
|
| Aged deletion | `temp` and `windows-temp` only remove files older than one day; files a running program still holds are skipped |
|
|
34
34
|
| Space assessment | Walks the disk read-only and groups the result into Windows / installed software / your own files, plus the ten largest app-data folders. Any row opens its folder in Explorer — click, right click, or Enter |
|
|
35
35
|
| Honest re-run | Re-running the assessment re-measures; a plain call still answers from the ten-minute cache |
|
|
@@ -60,6 +60,11 @@ Every path is built from this table — a caller selects an **id**, never a path
|
|
|
60
60
|
| `recycle-bin` | `$Recycle.Bin\S-1-5-*` on every mounted volume | purge | safe | it is the Recycle Bin |
|
|
61
61
|
| `inet-cache` | `%LOCALAPPDATA%\Microsoft\Windows\INetCache` | purge | caution | rebuilt |
|
|
62
62
|
| `app-updaters` | `*-updater` folders under `%LOCALAPPDATA%` and `%APPDATA%` — the installers app updaters leave behind | aged, 3 days | caution | a download from the last three days may still be a pending update |
|
|
63
|
+
| `app-caches` | named cache folders inside `%APPDATA%`: the WPS CEF cache, FTNN `cef_cache`, and the `Cache` / `Code Cache` / `CachedData` / `GPUCache` of Qoder, Trae, the DSH desktop app, PersonalCenter, the WeChat devtools and QQ PC Manager | purge | caution | CEF and Chromium caches rebuild on next start |
|
|
64
|
+
| `app-logs` | log folders inside `%APPDATA%`: FTNN, WeChat, Qoder, Trae, DingTalk, PersonalCenter, Clash Verge | purge | safe | nothing reads an old log back at runtime |
|
|
65
|
+
| `app-temp` | scratch folders apps keep inside their own `%APPDATA%` folder instead of `%TEMP%`: `WorkDaddy\client-tmp`, `PersonalCenter\Temp` | aged, 2 days | caution | the owning app may be running and holding files |
|
|
66
|
+
| `plugin-old-versions` | every version but the newest of each family in the WPS plugin pool `kingsoft\wps\addons\pool\win-i386` | purge | caution | the manager loads the newest; the rest are rollback copies |
|
|
67
|
+
| `update-packages` | update packages already applied: FTNN `.spd` patches whose build number is below the newest `Program Files\FTNN\app\[version]`, and `WorkDaddy\update` installers below the installed version | purge | caution | decided by version comparison, not by age |
|
|
63
68
|
|
|
64
69
|
`aged` targets skip anything modified within the retention window; `purge` targets remove every entry. Both keep the target directory itself and leave locked entries alone, counting them as skipped.
|
|
65
70
|
|
|
@@ -218,11 +223,11 @@ What the smoke test deliberately does **not** cover: anything that walks a real
|
|
|
218
223
|
```
|
|
219
224
|
├─ package.json dsh.bundle.patch + dsh.client { platform: "web" }
|
|
220
225
|
├─ cordis.patch.yml - insert: id: dsh-disk-cleaner
|
|
221
|
-
├─ lib/index.js (
|
|
222
|
-
├─ lib/catalog.js (
|
|
226
|
+
├─ lib/index.js (405) Host: HTTP route + disk_cleaner tool, same run() for both; reveal
|
|
227
|
+
├─ lib/catalog.js (688) The 19 targets, measurePath / purgePath / resolveTargets / drives
|
|
223
228
|
├─ lib/audit.js (365) Read-only assessment, 10-minute cache, areaPaths, exposes no deletion
|
|
224
229
|
├─ lib/plan.js (150) Pure: measurements → ordered steps
|
|
225
|
-
├─ lib/client.js (
|
|
230
|
+
├─ lib/client.js (1355) Client half: slots, panel, preferences, zh/en dictionaries, CSS
|
|
226
231
|
├─ locale/{en,zh}.json Display metadata (meta.title / meta.description)
|
|
227
232
|
├─ icon.svg Plugin icon
|
|
228
233
|
├─ README.md This file
|
|
@@ -239,7 +244,7 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
239
244
|
|
|
240
245
|
**dsh-disk-cleaner** 是 [DeepSeek Harness](https://github.com/deepseek-ai)(DSH)的磁盘空间插件。它做两件不同的事,并且刻意分开:
|
|
241
246
|
|
|
242
|
-
1. **清理** ——
|
|
247
|
+
1. **清理** —— 十九个已知安全的位置(临时目录、包管理器存储、浏览器缓存、崩溃转储、回收站、更新安装包残留、各应用的缓存与日志、被淘汰的插件版本与已应用的更新包)。只有这些会被删除。
|
|
243
248
|
2. **评估** —— 所有本插件**不会**删的东西(已装程序、WSL 虚拟磁盘、Windows 组件库、聊天软件数据、你自己的文件夹)。只测量、只列出,绝不删除。
|
|
244
249
|
|
|
245
250
|
评估的最后是一份**处理方案**:同一批数字按「你要付出多少代价」排序,本插件做不到的步骤附上确切的 Windows 命令。
|
|
@@ -250,9 +255,9 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
250
255
|
|
|
251
256
|
| 功能 | 说明 |
|
|
252
257
|
| --- | --- |
|
|
253
|
-
| 扫描 |
|
|
258
|
+
| 扫描 | 并发测量全部十九个目标,显示各自占用 |
|
|
254
259
|
| 按盘过滤 | 已挂载卷的胶囊按钮;扫描与清理都可限定单盘,该盘上为空的项会隐藏 |
|
|
255
|
-
| 风险标注 | 每个目标都标 `安全` 或 `注意`,并给一行理由(`in-use` / `rebuild` / `elevation` / `pending-update`),图例常驻,所以「注意」永远不是「说不清」 |
|
|
260
|
+
| 风险标注 | 每个目标都标 `安全` 或 `注意`,并给一行理由(`in-use` / `rebuild` / `elevation` / `pending-update` / `rollback` / `superseded`),图例常驻,所以「注意」永远不是「说不清」 |
|
|
256
261
|
| 保留期删除 | `temp` 与 `windows-temp` 只删一天前的文件;被运行中程序占住的自动跳过 |
|
|
257
262
|
| 空间评估 | 只读遍历磁盘,按 Windows 自身 / 已装软件 / 你自己的文件 分组,另列占用最大的十个应用数据目录。每一行都能打开所在目录——单击、右键或回车 |
|
|
258
263
|
| 重新评估真的重测 | 「重新评估」会重新测量;普通调用仍然走十分钟缓存 |
|
|
@@ -283,6 +288,11 @@ Client-half slots: `sidebar.panellist` (order 50, disposed in settings-only mode
|
|
|
283
288
|
| `recycle-bin` | 各已挂载卷的 `$Recycle.Bin\S-1-5-*` | 全清 | 安全 | 就是回收站 |
|
|
284
289
|
| `inet-cache` | `%LOCALAPPDATA%\Microsoft\Windows\INetCache` | 全清 | 注意 | 会重建 |
|
|
285
290
|
| `app-updaters` | `%LOCALAPPDATA%` 与 `%APPDATA%` 下的 `*-updater` 目录——升级程序留下的安装包 | 保留期 3 天 | 注意 | 三天内的下载可能还是待安装的更新 |
|
|
291
|
+
| `app-caches` | `%APPDATA%` 里点名列出的缓存目录:WPS 的 CEF 缓存、FTNN 的 `cef_cache`,以及 Qoder、Trae、DSH 桌面端、PersonalCenter、微信开发者工具、腾讯电脑管家的 `Cache` / `Code Cache` / `CachedData` / `GPUCache` | 全清 | 注意 | CEF 与 Chromium 缓存会在下次启动时重建 |
|
|
292
|
+
| `app-logs` | `%APPDATA%` 里的日志目录:FTNN、微信、Qoder、Trae、钉钉、PersonalCenter、Clash Verge | 全清 | 安全 | 旧日志运行时不会被读回 |
|
|
293
|
+
| `app-temp` | 应用自己放在 `%APPDATA%` 里的临时目录(不走 `%TEMP%`):`WorkDaddy\client-tmp`、`PersonalCenter\Temp` | 保留期 2 天 | 注意 | 应用可能正在运行并占用其中的文件 |
|
|
294
|
+
| `plugin-old-versions` | WPS 插件池 `kingsoft\wps\addons\pool\win-i386` 里,每个插件族除最高版本以外的全部版本 | 全清 | 注意 | 管理器加载最高版本,其余是回滚副本 |
|
|
295
|
+
| `update-packages` | 已应用的更新包:FTNN 的 `.spd` 补丁(build 号低于 `Program Files\FTNN\app\[版本]` 中最新者),以及 `WorkDaddy\update` 里低于已装版本的安装包 | 全清 | 注意 | 按版本号比较判断,不按时间 |
|
|
286
296
|
|
|
287
297
|
保留期方式会跳过保留期内修改过的文件;全清方式清掉全部条目。两者都保留目标目录本身,并把锁定或无权限的条目跳过计数。
|
|
288
298
|
|
|
@@ -445,11 +455,11 @@ node --check lib/client.js # 以及 lib/index.js、lib/catalog.js、li
|
|
|
445
455
|
```
|
|
446
456
|
├─ package.json dsh.bundle.patch + dsh.client { platform: "web" }
|
|
447
457
|
├─ cordis.patch.yml - insert: id: dsh-disk-cleaner
|
|
448
|
-
├─ lib/index.js (
|
|
449
|
-
├─ lib/catalog.js (
|
|
458
|
+
├─ lib/index.js (405) Host:HTTP 路由 + disk_cleaner 工具,共用同一个 run(),含 reveal
|
|
459
|
+
├─ lib/catalog.js (688) 19 个目标,measurePath / purgePath / resolveTargets / drives
|
|
450
460
|
├─ lib/audit.js (365) 只读评估,10 分钟缓存,带 areaPaths,不暴露任何删除
|
|
451
461
|
├─ lib/plan.js (150) 纯函数:测量结果 → 有序步骤
|
|
452
|
-
├─ lib/client.js (
|
|
462
|
+
├─ lib/client.js (1355) Client 半面:slot、面板、偏好、zh/en 字典、CSS
|
|
453
463
|
├─ locale/{en,zh}.json 展示元数据(meta.title / meta.description)
|
|
454
464
|
├─ icon.svg 插件图标
|
|
455
465
|
├─ README.md 本文件
|
package/lib/catalog.js
CHANGED
|
@@ -158,6 +158,122 @@ const browserCaches = (userData) => {
|
|
|
158
158
|
return out;
|
|
159
159
|
};
|
|
160
160
|
|
|
161
|
+
/**
|
|
162
|
+
* Per-app cache folders inside `%APPDATA%`. Every one is spelled out instead of discovered by
|
|
163
|
+
* a `*cache*` sweep: a folder is only safe to delete when somebody has looked at what it
|
|
164
|
+
* holds, and the same sweep that finds this WPS cache also finds WeChat's profile store and
|
|
165
|
+
* VS Code's web storage, which cost a re-login to rebuild.
|
|
166
|
+
*/
|
|
167
|
+
const appCaches = () => {
|
|
168
|
+
const roaming = env('APPDATA') ?? '';
|
|
169
|
+
return [
|
|
170
|
+
path.join(roaming, 'kingsoft', 'wps', 'addons', 'data', 'win-i386', 'cef', '2', 'cache'),
|
|
171
|
+
path.join(roaming, 'FTNN', '2.0', 'Common', 'cef_cache'),
|
|
172
|
+
...subdirs(path.join(roaming, 'com.qoder.app.stable'), ['Cache', 'Code Cache']),
|
|
173
|
+
...subdirs(path.join(roaming, 'Trae CN'), ['Cache', 'CachedData']),
|
|
174
|
+
...subdirs(path.join(roaming, 'Trae'), ['Cache', 'CachedData']),
|
|
175
|
+
...subdirs(path.join(roaming, '@deepseek-ai', 'dsh-desktop'), ['Cache', 'Code Cache', 'GPUCache', 'GrShaderCache']),
|
|
176
|
+
...subdirs(path.join(roaming, 'PersonalCenter'), ['Cache']),
|
|
177
|
+
...subdirs(path.join(roaming, '微信开发者工具'), ['Cache']),
|
|
178
|
+
...subdirs(path.join(roaming, '腾讯电脑管家-全局搜'), ['Cache']),
|
|
179
|
+
...subdirs(path.join(roaming, 'appGallery'), ['Crashpad']),
|
|
180
|
+
];
|
|
181
|
+
};
|
|
182
|
+
|
|
183
|
+
/** Log folders that only ever hold history: nothing reads an old log back at runtime. */
|
|
184
|
+
const appLogs = () => {
|
|
185
|
+
const roaming = env('APPDATA') ?? '';
|
|
186
|
+
return [
|
|
187
|
+
path.join(roaming, 'FTNN', '2.0', 'Logs'),
|
|
188
|
+
path.join(roaming, 'Tencent', 'xwechat', 'log'),
|
|
189
|
+
...subdirs(path.join(roaming, 'com.qoder.app.stable'), ['logs']),
|
|
190
|
+
...subdirs(path.join(roaming, 'Trae CN'), ['logs']),
|
|
191
|
+
...subdirs(path.join(roaming, 'Trae'), ['logs']),
|
|
192
|
+
...subdirs(path.join(roaming, 'DingTalk'), ['log']),
|
|
193
|
+
...subdirs(path.join(roaming, 'PersonalCenter'), ['log']),
|
|
194
|
+
...subdirs(path.join(roaming, 'io.github.clash-verge-rev.clash-verge-rev'), ['logs']),
|
|
195
|
+
...subdirs(path.join(roaming, 'MAXHUBShare'), ['log']),
|
|
196
|
+
];
|
|
197
|
+
};
|
|
198
|
+
|
|
199
|
+
/**
|
|
200
|
+
* Scratch directories an app keeps inside its own roaming folder rather than in `%TEMP%`.
|
|
201
|
+
* These belong to an app that may be running right now, so the target keeps a two-day floor
|
|
202
|
+
* instead of being purged outright.
|
|
203
|
+
*/
|
|
204
|
+
const appTempDirs = () => [
|
|
205
|
+
path.join(env('APPDATA') ?? '', 'WorkDaddy', 'client-tmp'),
|
|
206
|
+
path.join(env('APPDATA') ?? '', 'PersonalCenter', 'Temp'),
|
|
207
|
+
];
|
|
208
|
+
|
|
209
|
+
/**
|
|
210
|
+
* A plugin pool keeps every version it ever installed, because the manager picks one by name
|
|
211
|
+
* instead of replacing it in place. WPS alone had 347 folders across 131 families here. The
|
|
212
|
+
* highest version of each family stays — it is what the manager loads — and the rest are dead
|
|
213
|
+
* weight it never reads again. Families with a single member are skipped: nothing then proves
|
|
214
|
+
* which one is live.
|
|
215
|
+
*/
|
|
216
|
+
const pooledOldVersions = (pool, pattern) => {
|
|
217
|
+
if (!isDir(pool)) return [];
|
|
218
|
+
const families = new Map();
|
|
219
|
+
for (const entry of listDir(pool)) {
|
|
220
|
+
if (entry.isSymbolicLink()) continue;
|
|
221
|
+
const match = pattern.exec(entry.name);
|
|
222
|
+
if (match === null) continue;
|
|
223
|
+
const [, base, version] = match;
|
|
224
|
+
if (!families.has(base)) families.set(base, []);
|
|
225
|
+
families.get(base).push({ name: entry.name, version });
|
|
226
|
+
}
|
|
227
|
+
const out = [];
|
|
228
|
+
for (const list of families.values()) {
|
|
229
|
+
if (list.length < 2) continue;
|
|
230
|
+
list.sort((a, b) => compareVersion(a.version, b.version));
|
|
231
|
+
for (const older of list.slice(0, -1)) out.push(path.join(pool, older.name));
|
|
232
|
+
}
|
|
233
|
+
return out;
|
|
234
|
+
};
|
|
235
|
+
|
|
236
|
+
const wpsPluginOldVersions = () =>
|
|
237
|
+
pooledOldVersions(path.join(env('APPDATA') ?? '', 'kingsoft', 'wps', 'addons', 'pool', 'win-i386'), /^(.+)_([0-9][0-9._]*)$/);
|
|
238
|
+
|
|
239
|
+
/** The version folders an installer left under its own `app` directory, such as FTNN's. */
|
|
240
|
+
const installedAppVersions = (appRoot) => {
|
|
241
|
+
if (!isDir(appRoot)) return [];
|
|
242
|
+
return listDir(appRoot)
|
|
243
|
+
.filter((entry) => entry.isDirectory() && !entry.isSymbolicLink())
|
|
244
|
+
.map((entry) => entry.name)
|
|
245
|
+
.filter((name) => /^[0-9][0-9._]*$/.test(name));
|
|
246
|
+
};
|
|
247
|
+
|
|
248
|
+
/**
|
|
249
|
+
* Update packages the vendor has already applied, decided by comparison rather than by age.
|
|
250
|
+
* FTNN names a patch `Download-<from>-<to>-<hash>.spd` with bare build numbers while it keeps
|
|
251
|
+
* full `16.35.27108` folders in Program Files, so the comparison is build number against the
|
|
252
|
+
* newest installed build. A patch at or above that build may still be pending, so it stays.
|
|
253
|
+
*/
|
|
254
|
+
const ftnnAppliedPatches = () => {
|
|
255
|
+
const programFiles = env('ProgramFiles');
|
|
256
|
+
if (programFiles === undefined) return [];
|
|
257
|
+
const installed = installedAppVersions(path.join(programFiles, 'FTNN', 'app')).sort(compareVersion);
|
|
258
|
+
if (installed.length === 0) return [];
|
|
259
|
+
const newestBuild = Number(installed[installed.length - 1].split('.').pop());
|
|
260
|
+
if (!Number.isFinite(newestBuild)) return [];
|
|
261
|
+
const liveUpdate = path.join(env('APPDATA') ?? '', 'FTNN', '2.0', 'LiveUpdate', 'win_qtnn');
|
|
262
|
+
const out = [];
|
|
263
|
+
for (const entry of listDir(liveUpdate)) {
|
|
264
|
+
if (!entry.isFile() || entry.isSymbolicLink()) continue;
|
|
265
|
+
const match = /^Download-[0-9]+-([0-9]+)-[0-9a-f]+\.spd$/i.exec(entry.name);
|
|
266
|
+
if (match === null) continue;
|
|
267
|
+
if (Number(match[1]) < newestBuild) out.push(path.join(liveUpdate, entry.name));
|
|
268
|
+
}
|
|
269
|
+
return out;
|
|
270
|
+
};
|
|
271
|
+
|
|
272
|
+
const supersededUpdatePackages = () => [
|
|
273
|
+
...ftnnAppliedPatches(),
|
|
274
|
+
...pooledOldVersions(path.join(env('APPDATA') ?? '', 'WorkDaddy', 'update'), /^(.+)-([0-9][0-9._]*)(\.[A-Za-z0-9]+)?$/),
|
|
275
|
+
];
|
|
276
|
+
|
|
161
277
|
/**
|
|
162
278
|
* `purge` targets drop every entry under the path. `aged` targets keep anything modified
|
|
163
279
|
* within `minAgeDays`, because those files may belong to a process that is still running.
|
|
@@ -281,6 +397,46 @@ const CATALOG = [
|
|
|
281
397
|
roots: ['LOCALAPPDATA', 'APPDATA'],
|
|
282
398
|
paths: () => appUpdaters(),
|
|
283
399
|
},
|
|
400
|
+
{
|
|
401
|
+
id: 'app-caches',
|
|
402
|
+
kind: 'purge',
|
|
403
|
+
risk: 'caution',
|
|
404
|
+
note: 'rebuild',
|
|
405
|
+
roots: ['APPDATA'],
|
|
406
|
+
paths: () => appCaches(),
|
|
407
|
+
},
|
|
408
|
+
{
|
|
409
|
+
id: 'app-logs',
|
|
410
|
+
kind: 'purge',
|
|
411
|
+
risk: 'safe',
|
|
412
|
+
roots: ['APPDATA'],
|
|
413
|
+
paths: () => appLogs(),
|
|
414
|
+
},
|
|
415
|
+
{
|
|
416
|
+
id: 'app-temp',
|
|
417
|
+
kind: 'aged',
|
|
418
|
+
minAgeDays: 2,
|
|
419
|
+
risk: 'caution',
|
|
420
|
+
note: 'in-use',
|
|
421
|
+
roots: ['APPDATA'],
|
|
422
|
+
paths: () => appTempDirs(),
|
|
423
|
+
},
|
|
424
|
+
{
|
|
425
|
+
id: 'plugin-old-versions',
|
|
426
|
+
kind: 'purge',
|
|
427
|
+
risk: 'caution',
|
|
428
|
+
note: 'rollback',
|
|
429
|
+
roots: ['APPDATA'],
|
|
430
|
+
paths: () => wpsPluginOldVersions(),
|
|
431
|
+
},
|
|
432
|
+
{
|
|
433
|
+
id: 'update-packages',
|
|
434
|
+
kind: 'purge',
|
|
435
|
+
risk: 'caution',
|
|
436
|
+
note: 'superseded',
|
|
437
|
+
roots: ['APPDATA'],
|
|
438
|
+
paths: () => supersededUpdatePackages(),
|
|
439
|
+
},
|
|
284
440
|
];
|
|
285
441
|
|
|
286
442
|
const byId = new Map(CATALOG.map((entry) => [entry.id, entry]));
|
|
@@ -309,6 +465,19 @@ const within = (abs, root) => {
|
|
|
309
465
|
/** A volume such as `C:\`; anything else (including undefined) means every volume. */
|
|
310
466
|
const onDrive = (abs, drive) => drive === undefined || path.parse(abs).root.toLowerCase() === drive.toLowerCase();
|
|
311
467
|
|
|
468
|
+
/**
|
|
469
|
+
* Some targets are single files (an already-applied update package) rather than directories,
|
|
470
|
+
* so every step below answers both. A file target is unlinked directly and never has its
|
|
471
|
+
* parent removed.
|
|
472
|
+
*/
|
|
473
|
+
const isFile = (p) => {
|
|
474
|
+
try {
|
|
475
|
+
return fs.statSync(p).isFile();
|
|
476
|
+
} catch {
|
|
477
|
+
return false;
|
|
478
|
+
}
|
|
479
|
+
};
|
|
480
|
+
|
|
312
481
|
/** Resolve catalog ids to the concrete directories that exist right now, optionally on one volume. */
|
|
313
482
|
export function resolveTargets(only, drive) {
|
|
314
483
|
const selected = only === undefined ? CATALOG : CATALOG.filter((entry) => only.includes(entry.id));
|
|
@@ -317,7 +486,7 @@ export function resolveTargets(only, drive) {
|
|
|
317
486
|
for (const candidate of entry.paths()) {
|
|
318
487
|
if (typeof candidate !== 'string' || candidate === '') continue;
|
|
319
488
|
if (tooBroad(candidate)) continue;
|
|
320
|
-
if (!isDir(candidate)) continue;
|
|
489
|
+
if (!isDir(candidate) && !isFile(candidate)) continue;
|
|
321
490
|
if (!onDrive(candidate, drive)) continue;
|
|
322
491
|
const roots = entry.roots.map((name) => env(name)).filter(Boolean);
|
|
323
492
|
if (roots.length > 0 && !roots.some((root) => within(candidate, root))) continue;
|
|
@@ -338,6 +507,13 @@ export function resolveTargets(only, drive) {
|
|
|
338
507
|
}
|
|
339
508
|
|
|
340
509
|
export async function measurePath(abs) {
|
|
510
|
+
if (isFile(abs)) {
|
|
511
|
+
try {
|
|
512
|
+
return { bytes: fs.statSync(abs).size, files: 1 };
|
|
513
|
+
} catch {
|
|
514
|
+
return { bytes: 0, files: 0 };
|
|
515
|
+
}
|
|
516
|
+
}
|
|
341
517
|
let bytes = 0;
|
|
342
518
|
let files = 0;
|
|
343
519
|
let seen = 0;
|
|
@@ -467,6 +643,18 @@ export async function clean(ids, options = {}) {
|
|
|
467
643
|
failed += 1;
|
|
468
644
|
continue;
|
|
469
645
|
}
|
|
646
|
+
if (isFile(abs)) {
|
|
647
|
+
try {
|
|
648
|
+
const stat = await fsp.stat(abs);
|
|
649
|
+
if (cutoff !== undefined && stat.mtimeMs > cutoff) continue;
|
|
650
|
+
await fsp.unlink(abs);
|
|
651
|
+
bytes += stat.size;
|
|
652
|
+
removed += 1;
|
|
653
|
+
} catch {
|
|
654
|
+
failed += 1;
|
|
655
|
+
}
|
|
656
|
+
continue;
|
|
657
|
+
}
|
|
470
658
|
const outcome = await purgePath(abs, cutoff);
|
|
471
659
|
bytes += outcome.bytes;
|
|
472
660
|
removed += outcome.removed;
|
package/lib/client.js
CHANGED
|
@@ -39,6 +39,8 @@ window.__ModuleLoader__.load({
|
|
|
39
39
|
why_rebuild: '缓存会自动重建,下次打开略慢',
|
|
40
40
|
why_elevation: '需要管理员权限;普通窗口下会被系统拒绝',
|
|
41
41
|
'why_pending-update': '只清 {n} 天前的下载,避免删掉还没装的更新',
|
|
42
|
+
why_rollback: '每个插件/程序只留版本号最高的一份,其余是回滚副本',
|
|
43
|
+
why_superseded: '版本号不高于已装版本的更新包,装着的那份已经生效',
|
|
42
44
|
pathCount: '{n} 处',
|
|
43
45
|
driveLabel: '清理范围',
|
|
44
46
|
allDrives: '全部磁盘',
|
|
@@ -59,6 +61,11 @@ window.__ModuleLoader__.load({
|
|
|
59
61
|
'target_windows-update': 'Windows 更新缓存',
|
|
60
62
|
'target_recycle-bin': '回收站',
|
|
61
63
|
'target_app-updaters': '更新安装包残留',
|
|
64
|
+
'target_app-caches': '各应用缓存',
|
|
65
|
+
'target_app-logs': '各应用日志',
|
|
66
|
+
'target_app-temp': '各应用临时目录',
|
|
67
|
+
'target_plugin-old-versions': '插件旧版本',
|
|
68
|
+
'target_update-packages': '已应用的更新包',
|
|
62
69
|
auditTitle: '空间评估(本插件不会删除)',
|
|
63
70
|
auditHint: '统计缓存之外真正占空间的东西:已装软件、WSL 虚拟磁盘、Windows 组件库,以及你自己的文件。这些一律不由本插件删除,只列出供你判断。',
|
|
64
71
|
auditRun: '开始评估',
|
|
@@ -176,6 +183,8 @@ window.__ModuleLoader__.load({
|
|
|
176
183
|
why_rebuild: 'rebuilt automatically; the next open is slower',
|
|
177
184
|
why_elevation: 'needs an elevated prompt; a normal window is refused by Windows',
|
|
178
185
|
'why_pending-update': 'only downloads older than {n} day(s), so a pending update survives',
|
|
186
|
+
why_rollback: 'only the highest version of each family stays; the rest are rollback copies',
|
|
187
|
+
why_superseded: 'the update package version is not above what is installed, so it is already applied',
|
|
179
188
|
pathCount: '{n} location(s)',
|
|
180
189
|
driveLabel: 'Limit to',
|
|
181
190
|
allDrives: 'All drives',
|
|
@@ -196,6 +205,11 @@ window.__ModuleLoader__.load({
|
|
|
196
205
|
'target_windows-update': 'Windows update cache',
|
|
197
206
|
'target_recycle-bin': 'Recycle Bin',
|
|
198
207
|
'target_app-updaters': 'Leftover update installers',
|
|
208
|
+
'target_app-caches': 'App caches',
|
|
209
|
+
'target_app-logs': 'App logs',
|
|
210
|
+
'target_app-temp': 'App scratch folders',
|
|
211
|
+
'target_plugin-old-versions': 'Old plugin versions',
|
|
212
|
+
'target_update-packages': 'Applied update packages',
|
|
199
213
|
auditTitle: 'Space assessment (this plugin deletes none of it)',
|
|
200
214
|
auditHint: 'What actually takes up room outside caches: installed programs, WSL disk images, the Windows component store, and your own files. None of it is removed here; it is listed so you can decide.',
|
|
201
215
|
auditRun: 'Run assessment',
|
package/lib/index.js
CHANGED
|
@@ -195,6 +195,8 @@ const NOTE_MEANING = {
|
|
|
195
195
|
'in-use': 'a running program may hold some of these files; only files older than the age limit go',
|
|
196
196
|
rebuild: 'the owner rebuilds these, so the next start or page load is slower',
|
|
197
197
|
elevation: 'Windows refuses an unelevated delete here; run the plugin elevated to reach these bytes',
|
|
198
|
+
rollback: 'an older version the app keeps for rollback; what it runs now is the newest of that family',
|
|
199
|
+
superseded: 'an update package whose version is at or below what is already installed',
|
|
198
200
|
};
|
|
199
201
|
|
|
200
202
|
const formatBytes = (bytes) => {
|
package/package.json
CHANGED
package/smoke.mjs
CHANGED
|
@@ -262,9 +262,9 @@ check('tool description warns audit is read-only', /read-only: it never removes
|
|
|
262
262
|
check('every effect is labelled', applied.every((label) => typeof label === 'string' && label.startsWith('dsh-disk-cleaner:')));
|
|
263
263
|
|
|
264
264
|
const ids = [...catalog.matchAll(/^\s{4}id: '([a-z-]+)',$/gm)].map((m) => m[1]);
|
|
265
|
-
check('catalog has the documented
|
|
265
|
+
check('catalog has the documented 19 targets', ids.length === 19, `got ${ids.length}`);
|
|
266
266
|
check('every catalog id has a client label', ids.every((id) => zhKeys.includes(`target_${id}`) && enKeys.includes(`target_${id}`)));
|
|
267
|
-
check('every note code has an explanation', ['why_safe', 'why_in-use', 'why_rebuild', 'why_elevation', 'why_pending-update'].every((k) => zhKeys.includes(k)));
|
|
267
|
+
check('every note code has an explanation', ['why_safe', 'why_in-use', 'why_rebuild', 'why_elevation', 'why_pending-update', 'why_rollback', 'why_superseded'].every((k) => zhKeys.includes(k) && enKeys.includes(k)));
|
|
268
268
|
const areaIds = [...auditSrc.matchAll(/^\s{6}id: '([a-z-]+)',$/gm)].map((m) => m[1]);
|
|
269
269
|
check('every assessment area has a translated label and reason', areaIds.length > 0 && areaIds.every((id) => zhKeys.includes(`area_${id}`) && zhKeys.includes(`area_${id}_why`) && enKeys.includes(`area_${id}`) && enKeys.includes(`area_${id}_why`)), `areas: ${areaIds.join(',')}`);
|
|
270
270
|
check('a reveal request names an area, never a path', index.includes("action === 'reveal'") && auditSrc.includes('export function areaPaths') && !/revealPath[\s\S]{0,200}payload\.path/.test(index));
|