dsh-disk-cleanerv1.4.0

stallenlu/dsh-disk-cleaner

安全管理 MIT 最近更新 2026-11-05
0Stars
0Forks
0待解决 Issue
安装命令 dsh plugin --profile web add github:stallenlu/dsh-disk-cleaner#4dc59a2fee43fa06b6575b5ac5aba004ce8e211a

介绍

Disk Cleaner for DeepSeek Harness: scan known-safe caches, assess what cannot be cleaned, get a ranked plan

标签

deepseek-harness disk-cleaner dsh-plugin windows

概述

dsh-disk-cleaner

English | 中文

version1.4.0
DSH0.2.1-rc.5(开发与实测版本;用到 plugins.bundle.config、settings.section)
licenseMIT
Node>= 22(仅运行冒烟测试需要)
平台Windows(清理路径按 Windows 目录布局构造)

English

dsh-disk-cleaner is a disk-space plugin for DeepSeek Harness (DSH). It does two different jobs, and keeps them apart on purpose:

  1. Clean — twenty-eight 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.
  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.

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.

Both the panel and the agent reach the same operations: the cleaning logic lives once in lib/catalog.js, and the HTTP route and the disk_cleaner tool are two callers of it.

Features

FeatureDescription
ScanMeasures all twenty-eight targets concurrently and shows what each occupies
Per-drive filterChip row of mounted volumes; scanning or cleaning can be limited to one drive, and rows with nothing on that drive disappear
Risk labelsEvery target is safe or caution, with a one-line reason (in-use / rebuild / elevation / pending-update / rollback / superseded / history / duplicate / database-backup) and a permanent legend, so "caution" never means "unspecified"
Aged deletiontemp and windows-temp only remove files older than one day; files a running program still holds are skipped
Space assessmentWalks 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
Honest re-runRe-running the assessment re-measures; a plain call still answers from the ten-minute cache
PlanNine possible steps, ordered from zero-cost to your-decision-only; the steps the plugin can run have a button, the rest have a copyable command
Entry placementSidebar icon, settings section, or both (default) — switched from the plugin's own config card or the panel's ⚙ button
Behaviour settingsWhat is preselected after a scan, and whether deleting asks first
Panel memoryScan results, the selection and the assessment survive switching to another panel and back
Agent tooldisk_cleaner exposes the same five actions to the model, in readable prose
BilingualFollows the host locale service; zh and en dictionaries ship together

Cleanup targets

Every path is built from this table — a caller selects an id, never a path, so no user-supplied string can reach an unlink.

idWhat it isKindRiskWhy
temp%TEMP%, %TMP%, %LOCALAPPDATA%Tempaged, 1 daycautiona running program may hold some files
windows-tempC:WindowsTempaged, 1 daycautiona service may still hold files; only older ones go
npm-cache%LOCALAPPDATA%npm-cachepurgesafepure cache
pnpm-store%LOCALAPPDATA%pnpmstorepurgesafepure cache
pip-cache%LOCALAPPDATA%pipCachepurgesafepure cache
uv-cache%LOCALAPPDATA%uvcachepurgesafepure cache
crash-dumps%LOCALAPPDATA%CrashDumpspurgesafenothing to lose
chrome-cacheper-profile Cache, Code Cache, GPUCache, DawnCache, ShaderCache, GrShaderCachepurgecautionrebuilt; next open is slower
edge-cachesame set, Edge profilespurgecautionrebuilt; next open is slower
code-cache%APPDATA%Code{Cache,CachedData,CachedExtensionVSIXs}purgecautionrebuilt; next start is slower
windows-updateC:WindowsSoftwareDistributionDownloadpurgecautionneeds elevation
recycle-bin$Recycle.BinS-1-5-* on every mounted volumepurgesafeit is the Recycle Bin
inet-cache%LOCALAPPDATA%MicrosoftWindowsINetCachepurgecautionrebuilt
app-updaters*-updater folders under %LOCALAPPDATA% and %APPDATA% — the installers app updaters leave behindaged, 3 dayscautiona download from the last three days may still be a pending update
app-cachesnamed 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 ManagerpurgecautionCEF and Chromium caches rebuild on next start
app-logslog folders inside %APPDATA%: FTNN, WeChat, Qoder, Trae, DingTalk, PersonalCenter, Clash Vergepurgesafenothing reads an old log back at runtime
app-tempscratch folders apps keep inside their own %APPDATA% folder instead of %TEMP%: WorkDaddyclient-tmp, PersonalCenterTempaged, 2 dayscautionthe owning app may be running and holding files
plugin-old-versionsevery version but the newest of each family in the WPS plugin pool kingsoftwpsaddonspoolwin-i386purgecautionthe manager loads the newest; the rest are rollback copies
update-packagesupdate packages already applied: FTNN .spd patches whose build number is below the newest Program FilesFTNNapp[version], and WorkDaddyupdate installers below the installed versionpurgecautiondecided by version comparison, not by age
session-backupsmodify_backup folders under %USERPROFILE%.workbuddyworkspacesessions* — a whole-file snapshot of edits per agent sessionpurgecautionthe session itself lives beside the snapshot and does not read it back
agent-logsdate-named log folders under %USERPROFILE%.workbuddylogs and .workbuddy-ailogsaged, 3 dayssafenothing reads an old log back
toolchain-snapshotsduplicate snapshots under %USERPROFILE%.dshdsh-opsbash-ipc: folders whose .ops-ipc.json carries the same patchedSha256 collapse to the newestpurgecautiona folder without that metadata is never touched
editor-server-bundlesevery bundle but the newest under %USERPROFILE%vscode-remote-wslstablepurgecautionthe editor re-downloads the one it needs
database-backupszotero.sqlite*.bak under %USERPROFILE%Zoteropurgecautionthe live zotero.sqlite sits beside the copies
editor-buildscomplete build trees under %LOCALAPPDATA%ProgramsMicrosoft VS Code that bincode.cmd does not name, plus old_* files whose original still sits beside thempurgecautionno launcher, a launcher naming a missing build, or a lone build means nothing is dropped
agent-app-cachesCache / Code Cache / GPUCache under %USERPROFILE%.workbuddyapp and .workbuddy-aiapppurgecautionthe sibling cache folder holds spilled conversation text and is never a target
rewind-snapshotssession-* folders under %USERPROFILE%.dshrewind-snapshotsaged, 30 dayscautiona recent session stays restorable
rotated-logs*.old.log and *.log.N files in the logs folders of the agent appsaged, 3 dayssafenothing reads a rotated file back

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.

Space assessment areas

wsl (ext4.vhdx images), winsxs, system-files (hiberfil / pagefile / swapfile), programs (user scope), program-files (machine scope), old-versions (builds an installer kept for rollback), update-payloads (setup and installer files sitting in temp and app-data), downloads, documents, pictures, videos, desktop. An area that does not exist is dropped rather than reported as zero. The ten largest folders under %LOCALAPPDATA% and %APPDATA% (≥100 MB each) are listed separately, because "3.6 GB in Roamingkingsoft" points somewhere a Settings page does not.

Plan steps

codeWhen it appearsRunnable here
reclaim-safesafe targets ≥200 MByes, one button
reclaim-cautioncaution targets ≥200 MB (elevation excluded)yes, one button
wsl-compactWSL images ≥1 GBno — wsl --shutdown + diskpart compact vdisk
winsxs-cleanupWinSxS ≥1 GBno — Dism.exe /Online /Cleanup-Image /StartComponentCleanup
hibernate-offsystem-files ≥200 MBno — powercfg.exe /hibernate off
elevation-cleanelevation targets ≥200 MBno — run the plugin elevated
app-reviewan app-data folder ≥1 GB, top 3, not already coveredno — uninstall, never delete the folder
move-foldera personal folder ≥2 GB with another drive having roomno — Properties → Location
pressuresystem drive free < 12%no — context for the steps above

Bytes the plugin cannot reach from an unelevated process are never counted in reclaimNow: a delete that will fail must not appear as reclaimable.

Settings

SettingDefaultOptionsStored as
Panel entryEverywhereEverywhere / Sidebar only / Settings page onlydsc.placement.v2
Preselect after a scanSafe targets onlySafe targets only / Safe and caution / Nothingdsc.pick
Ask before deletingAlwaysAlways / Neverdsc.confirm

Settings live in the browser's localStorage and are reachable from three places that share one implementation: the plugin's own card on its page in the plugin manager, the settings.section page, and the panel's ⚙ button.

Panel state (drives, drive filter, scan rows, selection, notices, assessment) is kept in a module-scope Map, so switching panels does not throw it away. It does not survive a page reload — only the three settings do.

Agent tool

disk_cleaner, same logic as the panel:

ArgumentTypeNotes
actionoverviewscancleanauditrevealrequired
idsstring[]catalog ids; required for clean, max 32
drivestringC: / D: — limits the work to one volume
confirmbooleanwithout it, clean is a dry run
forcebooleanaudit re-measures instead of answering from the 10-minute cache
area / scope + name / indexreveal names what to open; never a path

clean without confirm: true deletes nothing and reports what it would free — that is the approval path. audit is read-only and returns the plan in prose, ending with "Report that plan to the user; never run those commands yourself."

HTTP surface

One route, POST /api/disk-cleaner/, used by the panel:

{"action":"overview"}                          // drives + target ids
{"action":"scan","drive":"C:"}                 // measured targets
{"action":"audit","force":true}                // areas + largest apps + plan
{"action":"reveal","area":"downloads"}         // opens it in Explorer
{"action":"clean","ids":["temp"],"confirm":true}

POST only (405 otherwise), same-origin only (403 otherwise), body ≤4096 bytes, at most 32 ids, unknown ids or actions rejected with 400. Handlers never receive a path: reveal names an assessment area (or an app folder by scope and name), the host rebuilds the path from the same table that measured it, and hands that to explorer.exe.

Safety rules

These are enforced in lib/catalog.js and must not be relaxed:

  • Only paths built by the catalog are touched. No API accepts a path.
  • A candidate must have at least three path segments and must not be a volume root (tooBroad).
  • After realpath, the candidate must still sit inside the environment root it was built from — a junction planted in a cache directory cannot widen the blast radius.
  • Symlinks and junctions met during a walk are skipped, never followed or deleted.
  • Locked or denied entries are counted as skipped and the walk continues.
  • The target directory itself is kept; only its contents go.
  • Rules that pick "the newest of a group" fail closed: a family with only one member, a folder whose metadata file is missing or whose hash is not a string, and a vscode-remote-wslstable that holds a single bundle all resolve to nothing to delete. The newest member is never returned.
  • editor-builds drops a build only when bincode.cmd exists and names a different build that is also on disk; old_x goes only while x is still beside it.
  • database-backups matches .sqlite*.bak by name only; the live zotero.sqlite is never a candidate.
  • The directory Windows refuses to let an unelevated process write (SoftwareDistributionDownload) is labelled elevation and excluded from "reclaimable now". C:WindowsTemp is not one of them on a default ACL: it grants BUILTINUsers create and delete, so only EBUSY on a live file stops a delete there, which the age limit covers.

Install

Published on npm as dsh-disk-cleaner:

npm i dsh-disk-cleaner

Then enable it in the profile — in $DSH_PROFILE_DIR/package.json:

{
  "dependencies": {
    "dsh-disk-cleaner": "^1.0.0"
  },
  "dsh": {
    "profile": {
      "bundles": ["dsh-disk-cleaner"]
    }
  }
}

Or point the plugin manager's install_bundle at a local checkout, using a link: dependency instead of a version range:

git clone https://github.com/stallenlu/dsh-disk-cleaner

The bundle's own cordis.patch.yml inserts the plugin row; no profile patch edit is needed. A link: dependency is a symlink, so client changes take effect on a page refresh, while host changes (lib/index.js) need the bundle re-enabled or DSH restarted. An installed npm copy is the opposite: bump the version to move it.

Development

node smoke.mjs                       # 68 assertions, zero dependencies
node --check lib/client.js           # and lib/index.js, lib/catalog.js, lib/audit.js, lib/plan.js

smoke.mjs executes the real client bundle in a throwaway DOM (fake window / document / require): the slot lifecycle runs for real, so switching the entry off and on again is proven by disposers being called, not by matching source text. The host half is captured through a throwaway ctx to prove the route and the tool are registered.

Negative control (proving an assertion bites): change reclaimable.filter((row) => row.note !== 'elevation') back to reclaimable in lib/plan.js — elevation steps are never counted as reclaimable now must fail. Restore afterwards.

What the smoke test deliberately does not cover: anything that walks a real volume (scan / clean / audit take tens of seconds) and anything visual. Those were verified by hand against a live host — see below.

Verification status

CheckedHow
Client bundle executes, slots register and disposenode smoke.mjs (real bundle in a fake DOM)
Settings reach the plugin page, the settings section and the panelnode smoke.mjs + cordis_inspect_query slot occupants
Route and tool registerednode smoke.mjs (throwaway ctx)
overview / scan / clean / audit / reveal round-tripReal POST /api/disk-cleaner/ against a live host
Re-run bypasses the cache, a plain call still uses itaudit twice: cached: false, then true
reveal refuses a path and a traversalname: "../..", and a request with neither area nor name → 400
Real deletion keeps the target directory, honours the age limitTemp directory seeded with old and new files
Dry run deletes nothingconfirm omitted, byte counts unchanged
Drive filter rejects unmounted and malformed volumesC:extra, Z:, abc → 400
Plan excludes elevation bytes from reclaimNowLive audit against this machine
"Keep only the newest of a group" fails closedThrowaway fixture tree: a same-hash duplicate collapses to the newest, a folder with no metadata is left alone, a different hash is kept, the live database survives (9/9)
Old editor builds need a launcher that proves which one runsThrowaway fixture tree: a build the launcher does not name goes, no launcher deletes nothing, a launcher naming a missing build deletes nothing, a lone build stays, old_x goes only while x exists (7/7)
A dry run reports what a real run freesThe 30-day rewind window reports 263 KB not 283 MB, and scan matches the dry run
Visual layoutChecked by hand in the running Web UI. No browser automation here, so this one is a human check, not a test.

Implementation notes

  • Client halves of static boot bundles receive only require in their factory — styles / host / harness do not exist there. Styles are installed as a <style data-plugin> element appended to document.head, and removed on dispose.
  • All colours go through --dsw-alias-* theme tokens; no literal colours in the CSS, and every class is prefixed dsc-.
  • A slot cannot be registered conditionally, so a setting that removes an entry is expressed by mounting and disposing the injection (bindWhile), deferred with queueMicrotask — the control that removes a section may itself live inside that section.
  • plugins.bundle.config returns null for view: 'summary', the same way other bundles do; the rows render only on the plugin page.
  • apply() registers everything through ctx.effect so the host can dispose it as a unit.

Known ceilings

  • Windows only. Paths are built from the Windows layout; there is no POSIX branch.
  • First assessment takes 30–60 s. Named areas and the per-app folder list walk overlapping trees. A result is cached for 10 minutes and invalidated by a real deletion, so only the first caller pays; the panel's re-run button passes force and always pays it. ponytail: comment in lib/audit.js records the shared-map alternative.
  • Leftover installers are cleaned by age, not by knowledge. *-updater folders are recognised by name; a download from the last three days is left alone because only the app knows whether it has been applied.
  • No elevation. The one Windows directory that refuses an unelevated write is reported, never attempted.
  • No uninstall, no move. Steps the plugin cannot perform carry a command instead of a button.
  • Traversal caps at 2,000,000 entries per walk, so an absurdly large tree is measured partially rather than hanging.

Architecture

├─ package.json           dsh.bundle.patch + dsh.client { platform: "web" }
├─ cordis.patch.yml       - insert:  id: dsh-disk-cleaner
├─ lib/index.js    (411)  Host: HTTP route + disk_cleaner tool, same run() for both; reveal
├─ lib/catalog.js  (969)  The 28 targets, measurePath / purgePath / resolveTargets / drives
├─ lib/audit.js    (365)  Read-only assessment, 10-minute cache, areaPaths, exposes no deletion
├─ lib/plan.js     (150)  Pure: measurements → ordered steps
├─ lib/client.js  (1379)  Client half: slots, panel, preferences, zh/en dictionaries, CSS
├─ locale/{en,zh}.json    Display metadata (meta.title / meta.description)
├─ icon.svg               Plugin icon
├─ README.md              This file
├─ LICENSE                MIT
└─ smoke.mjs              68 assertions, zero dependencies

Client-half slots: sidebar.panellist (order 50, disposed in settings-only mode), main (keyed by the package name), settings.section (order 30, disposed in sidebar-only mode), plugins.bundle.config (keyed by the package name).


中文

dsh-disk-cleaner 是 DeepSeek Harness(DSH)的磁盘空间插件。它做两件不同的事,并且刻意分开:

  1. 清理 —— 二十八个已知安全的位置(临时目录、包管理器存储、浏览器缓存、崩溃转储、回收站、更新安装包残留、各应用的缓存与日志、被淘汰的插件版本与已应用的更新包)。只有这些会被删除。
  2. 评估 —— 所有本插件不会删的东西(已装程序、WSL 虚拟磁盘、Windows 组件库、聊天软件数据、你自己的文件夹)。只测量、只列出,绝不删除。

评估的最后是一份处理方案:同一批数字按「你要付出多少代价」排序,本插件做不到的步骤附上确切的 Windows 命令。

面板和 agent 用的是同一套操作:清理逻辑只在 lib/catalog.js 里写一次,HTTP 路由和 disk_cleaner 工具都是它的调用方。

功能

功能说明
扫描并发测量全部二十八个目标,显示各自占用
按盘过滤已挂载卷的胶囊按钮;扫描与清理都可限定单盘,该盘上为空的项会隐藏
风险标注每个目标都标 安全 或 注意,并给一行理由(in-use / rebuild / elevation / pending-update / rollback / superseded / history / duplicate / database-backup),图例常驻,所以「注意」永远不是「说不清」
保留期删除temp 与 windows-temp 只删一天前的文件;被运行中程序占住的自动跳过
空间评估只读遍历磁盘,按 Windows 自身 / 已装软件 / 你自己的文件 分组,另列占用最大的十个应用数据目录。每一行都能打开所在目录——单击、右键或回车
重新评估真的重测「重新评估」会重新测量;普通调用仍然走十分钟缓存
处理方案九种可能步骤,从零风险到只能你决定;本插件能执行的带按钮,其余给可复制的命令
入口位置侧边栏图标、设置页分区、或两者都要(默认)——在插件自己的配置卡或面板 ⚙ 里切换
行为设置扫描后默认勾选什么、删除前是否确认
面板记忆扫描结果、勾选、评估结果在切走再回来时都还在
Agent 工具disk_cleaner 把同样的五个动作以可读文本交给模型
双语跟随宿主语言服务,zh / en 字典一起发布

清理目标

所有路径都由这张表构造——调用方只选 id,从不传路径,所以任何用户输入的字符串都到不了 unlink。

id是什么方式风险原因
temp%TEMP%、%TMP%、%LOCALAPPDATA%Temp保留期 1 天注意可能有程序正在使用其中的文件
windows-tempC:WindowsTemp保留期 1 天注意可能有服务正占着文件;只删一天前的
npm-cache%LOCALAPPDATA%npm-cache全清安全纯缓存
pnpm-store%LOCALAPPDATA%pnpmstore全清安全纯缓存
pip-cache%LOCALAPPDATA%pipCache全清安全纯缓存
uv-cache%LOCALAPPDATA%uvcache全清安全纯缓存
crash-dumps%LOCALAPPDATA%CrashDumps全清安全没有可丢失的数据
chrome-cache各配置目录下的 Cache、Code Cache、GPUCache、DawnCache、ShaderCache、GrShaderCache全清注意会重建,下次打开更慢
edge-cache同上,Edge 的配置目录全清注意会重建,下次打开更慢
code-cache%APPDATA%Code{Cache,CachedData,CachedExtensionVSIXs}全清注意会重建,下次启动更慢
windows-updateC:WindowsSoftwareDistributionDownload全清注意需要管理员权限
recycle-bin各已挂载卷的 $Recycle.BinS-1-5-*全清安全就是回收站
inet-cache%LOCALAPPDATA%MicrosoftWindowsINetCache全清注意会重建
app-updaters%LOCALAPPDATA% 与 %APPDATA% 下的 *-updater 目录——升级程序留下的安装包保留期 3 天注意三天内的下载可能还是待安装的更新
app-caches%APPDATA% 里点名列出的缓存目录:WPS 的 CEF 缓存、FTNN 的 cef_cache,以及 Qoder、Trae、DSH 桌面端、PersonalCenter、微信开发者工具、腾讯电脑管家的 Cache / Code Cache / CachedData / GPUCache全清注意CEF 与 Chromium 缓存会在下次启动时重建
app-logs%APPDATA% 里的日志目录:FTNN、微信、Qoder、Trae、钉钉、PersonalCenter、Clash Verge全清安全旧日志运行时不会被读回
app-temp应用自己放在 %APPDATA% 里的临时目录(不走 %TEMP%):WorkDaddyclient-tmp、PersonalCenterTemp保留期 2 天注意应用可能正在运行并占用其中的文件
plugin-old-versionsWPS 插件池 kingsoftwpsaddonspoolwin-i386 里,每个插件族除最高版本以外的全部版本全清注意管理器加载最高版本,其余是回滚副本
update-packages已应用的更新包:FTNN 的 .spd 补丁(build 号低于 Program FilesFTNNapp[版本] 中最新者),以及 WorkDaddyupdate 里低于已装版本的安装包全清注意按版本号比较判断,不按时间
session-backups%USERPROFILE%.workbuddyworkspacesessions* 下的 modify_backup 目录——每个 agent 会话的整文件修改快照全清注意会话本体在快照旁边,不会读回这些文件
agent-logs%USERPROFILE%.workbuddylogs 与 .workbuddy-ailogs 下按日期命名的日志目录保留期 3 天安全旧日志不会被读回
toolchain-snapshots%USERPROFILE%.dshdsh-opsbash-ipc 下重复的快照:.ops-ipc.json 里 patchedSha256 相同的目录只留最新一份全清注意没有该元数据的目录永不触碰
editor-server-bundles%USERPROFILE%vscode-remote-wslstable 下除最新以外的服务包全清注意编辑器需要时会重新下载
database-backups%USERPROFILE%Zotero 下的 zotero.sqlite*.bak全清注意正在用的 zotero.sqlite 就在副本旁边
editor-builds%LOCALAPPDATA%ProgramsMicrosoft VS Code 下 bincode.cmd 未指向的完整构建目录,以及原件仍在旁边的 old_* 文件全清注意没有启动器、启动器指向的构建不在磁盘上、或只有一份构建时,一律不删
agent-app-caches%USERPROFILE%.workbuddyapp 与 .workbuddy-aiapp 下的 Cache / Code Cache / GPUCache全清注意同级的 cache 目录存的是对话溢写内容,从不作为目标
rewind-snapshots%USERPROFILE%.dshrewind-snapshots 下的 session-* 目录保留期 30 天注意最近的会话仍然可以回滚
rotated-logs各 agent 应用 logs 目录下的 *.old.log 与 *.log.N 文件保留期 3 天安全轮转掉的日志不会被读回

保留期方式会跳过保留期内修改过的文件;全清方式清掉全部条目。两者都保留目标目录本身,并把锁定或无权限的条目跳过计数。

空间评估范围

wsl(ext4.vhdx 映像)、winsxs、system-files(hiberfil / pagefile / swapfile)、programs(用户级)、program-files(机器级)、old-versions(安装器留给回滚的旧版本目录)、update-payloads(临时目录与应用数据里躺着的 setup / installer 文件)、downloads、documents、pictures、videos、desktop。不存在的范围直接略去,不报成 0。%LOCALAPPDATA% 与 %APPDATA% 下超过 100 MB 的十个最大目录另外列出——因为「Roamingkingsoft 占 3.6 GB」指向的地方,设置页里根本找不到。

处理方案步骤

代码出现条件本插件可执行
reclaim-safe安全目标共 ≥200 MB是,一个按钮
reclaim-caution注意目标共 ≥200 MB(不含需提权的)是,一个按钮
wsl-compactWSL 映像 ≥1 GB否 —— wsl --shutdown + diskpart 的 compact vdisk
winsxs-cleanupWinSxS ≥1 GB否 —— Dism.exe /Online /Cleanup-Image /StartComponentCleanup
hibernate-offsystem-files ≥200 MB否 —— powercfg.exe /hibernate off
elevation-clean需提权目标 ≥200 MB否 —— 以管理员身份运行本插件
app-review单个应用数据目录 ≥1 GB,取前 3 个且未被其它步骤覆盖否 —— 走卸载,绝不删目录
move-folder个人文件夹 ≥2 GB,且另有盘有空间否 —— 属性 → 位置
pressure系统盘可用 < 12%否 —— 作为上述步骤的背景

普通权限够不到的字节绝不计入 reclaimNow:注定失败的删除不能被承诺成可回收空间。

设置项

设置默认选项存储键
面板入口全部全部 / 仅侧边栏 / 仅设置页dsc.placement.v2
扫描后默认勾选仅安全项仅安全项 / 安全 + 注意 / 都不勾选dsc.pick
删除前确认每次确认每次确认 / 不再确认dsc.confirm

设置在浏览器 localStorage 里,三个入口共用同一份实现:插件管理里该插件的配置卡、settings.section 设置页、面板的 ⚙ 按钮。

面板状态(盘列表、盘过滤、扫描结果、勾选、提示、评估结果)放在模块级 Map 里,所以切走面板不会丢;但它不跨刷新——只有那三个设置项会跨刷新保留。

Agent 工具

disk_cleaner,与面板同一套逻辑:

参数类型说明
actionoverviewscancleanauditreveal必填
idsstring[]catalog id;clean 必填,最多 32 个
drivestringC: / D:,把操作限定在一块盘
confirmboolean不传时 clean 只做试运行
forcebooleanaudit 跳过 10 分钟缓存重新测量
area / scope + name / indexreveal 用它说明要打开什么,从不传路径

不带 confirm: true 的 clean 一个字节都不删,只报告本来能释放多少——这就是审批路径。audit 只读,并以文本返回处理方案,结尾固定为「把方案报告给用户,不要自己执行这些命令」。

HTTP 接口

只有一个路由 POST /api/disk-cleaner/,面板使用:

{"action":"overview"}                          // 磁盘容量 + 目标 id
{"action":"scan","drive":"C:"}                 // 测量后的目标
{"action":"audit","force":true}                // 范围 + 最大应用目录 + 方案
{"action":"reveal","area":"downloads"}         // 在资源管理器中打开
{"action":"clean","ids":["temp"],"confirm":true}

只接受 POST(否则 405),只接受同源(否则 403),body ≤4096 字节,id 最多 32 个,未知 id 或动作返回 400。处理器永远收不到路径:reveal 只报一个评估范围(或 scope + name 表示的应用目录),宿主用当初测量它的那张表重建路径,再交给 explorer.exe。

安全规则

以下在 lib/catalog.js 中强制,不得放宽:

  • 只碰 catalog 构造出的路径,没有任何接口接受路径参数。
  • 候选路径至少三段,且不能是卷根(tooBroad)。
  • realpath 之后必须仍落在它所属的环境根内——在缓存目录里种一个 junction 也扩大不了波及面。
  • 遍历时遇到符号链接与 junction 一律跳过,不跟随也不删除。
  • 锁定或无权限的条目计为跳过,遍历继续。
  • 保留目标目录本身,只清内容。
  • 「只留最新一份」这类规则一律选保守的一侧:族内只有一个成员、元数据文件缺失或哈希不是字符串、 vscode-remote-wslstable 下只有一个服务包,结果都是没有可删项。最新那一份永远不会被返回。
  • editor-builds 只有在 bincode.cmd 存在且指向另一份确实在磁盘上的构建时才删;old_x 也只有在 x 仍在旁边时才删。
  • database-backups 只按名字匹配 .sqlite*.bak,正在用的 zotero.sqlite 永不成为候选。
  • 那个拒绝普通权限写入的 Windows 目录标为 elevation,并排除在「现在能回收」之外。

安装

已发布到 npm:dsh-disk-cleaner。

npm i dsh-disk-cleaner

然后在 profile 里启用 —— 在 $DSH_PROFILE_DIR/package.json:

{
  "dependencies": {
    "dsh-disk-cleaner": "^1.0.0"
  },
  "dsh": {
    "profile": {
      "bundles": ["dsh-disk-cleaner"]
    }
  }
}

或者把插件管理的 install_bundle 指向本地检出目录,依赖写成 link: 而不是版本范围:

git clone https://github.com/stallenlu/dsh-disk-cleaner

bundle 自带的 cordis.patch.yml 会插入插件行,无需改 profile 的 patch。link: 依赖是符号链接,所以改 client 刷新页面即生效,改 host(lib/index.js)需要重新启用该 bundle 或重启 DSH。从 npm 装则是相反的一面:要升级就改版本号。

git clone https://github.com/stallenlu/dsh-disk-cleaner

开发

node smoke.mjs                       # 68 条断言,零依赖
node --check lib/client.js           # 以及 lib/index.js、lib/catalog.js、lib/audit.js、lib/plan.js

smoke.mjs 在一次性假 DOM 里真的执行 client bundle(假 window / document / require):slot 生命周期是真的跑,所以「把入口关掉再打开」是靠 disposer 真被调用证明的,不是靠匹配源码文本。Host 半面用一次性 ctx 抓取,证明路由和工具确实注册了。

反向对照(证明断言有约束力):把 lib/plan.js 里的 reclaimable.filter((row) => row.note !== 'elevation') 改回 reclaimable,elevation steps are never counted as reclaimable now 必须失败。测完恢复。

冒烟测试刻意不覆盖:任何需要真实卷的操作(scan / clean / audit 都要几十秒)和任何视觉呈现。这两类是对着真实宿主手工验证的,见下表。

验证状态

已验证手段
client bundle 能执行、slot 能注册与拆除node smoke.mjs(假 DOM 里跑真 bundle)
设置能到达插件页、设置页、面板三处node smoke.mjs + cordis_inspect_query 查 slot 占用
路由与工具已注册node smoke.mjs(一次性 ctx)
五个动作端到端对真实宿主 POST /api/disk-cleaner/
「重新评估」绕过缓存、普通调用仍走缓存连调两次 audit:cached 先 false 后 true
reveal 拒绝路径与目录穿越name: "../..",以及既无 area 又无 name → 400
真删保留目标目录、遵守保留期在 Temp 里造一旧一新两个文件验证
试运行不删任何东西不传 confirm,字节数不变
盘过滤拒绝未挂载与畸形卷名C:extra、Z:、abc → 400
方案不把提权目标的字节算进「现在能回收」在本机跑真实 audit
「只留最新一份」一律选保守一侧一次性 fixture 目录:同哈希的重复项收敛到最新、无元数据的目录不动、不同哈希保留、正在用的数据库完好(9/9)
旧版编辑器构建要有启动器作证才删一次性 fixture 目录:启动器未指向的构建会删、没有启动器一律不删、启动器指向的构建不在磁盘上也不删、只有一份构建时不删、old_x 只在 x 仍在时删(7/7)
预演报告的字节数等于真删能释放的字节数30 天的回滚窗口报 263 KB 而不是 283 MB,且扫描结果与预演一致
视觉排版在运行中的 Web UI 里手工确认过。本环境没有浏览器自动化,所以这条是人眼检查,不是测试。

实现要点

  • 静态 boot bundle 的 client 半面工厂只收到 require——styles / host / harness 在这里不存在。样式自建 <style data-plugin> 插入 document.head,销毁时移除。
  • 颜色全部走 --dsw-alias-* 主题 token,CSS 不写死颜色,类名统一 dsc- 前缀。
  • slot 不能条件注册,所以「某个设置项让入口消失」是靠挂载与拆除(bindWhile)表达的,并用 queueMicrotask 延后——因为触发拆除的控件本身可能就长在要被拆掉的那个分区里。
  • plugins.bundle.config 在 view: 'summary' 时返回 null,与其它 bundle 一致;行只在插件页渲染。
  • apply() 内一切资源都通过 ctx.effect 注册,宿主可整体销毁。

已知上限

  • 仅 Windows。 路径按 Windows 目录布局构造,没有 POSIX 分支。
  • 首次评估 30–60 秒。 命名范围与「最大应用目录」会遍历重叠的树。结果缓存 10 分钟,并在真删后失效,所以只有第一个调用者付这笔钱;面板的「重新评估」会传 force,每次都要重付。lib/audit.js 里有 ponytail: 注释记录「共享 Map」这个替代方案。
  • 更新安装包按时间清理,不靠识别版本。 *-updater 目录靠名字认;三天内的下载一律不动,因为只有应用自己知道它装了没有。
  • 不提权。 那个拒绝普通权限写入的 Windows 目录只报告,不尝试删除。C:WindowsTemp 不是其中之一:默认 ACL 给 BUILTINUsers 创建与删除权限,挡住删除的只是被服务占用的 EBUSY,而保留期已经处理了这一类。
  • 不卸载、不迁移。 本插件做不到的步骤给命令,不给按钮。
  • 遍历上限每棵树 2,000,000 条目,极端大的树会被部分测量,而不是卡死。

架构

├─ package.json           dsh.bundle.patch + dsh.client { platform: "web" }
├─ cordis.patch.yml       - insert:  id: dsh-disk-cleaner
├─ lib/index.js    (411)  Host:HTTP 路由 + disk_cleaner 工具,共用同一个 run(),含 reveal
├─ lib/catalog.js  (969)  28 个目标,measurePath / purgePath / resolveTargets / drives
├─ lib/audit.js    (365)  只读评估,10 分钟缓存,带 areaPaths,不暴露任何删除
├─ lib/plan.js     (150)  纯函数:测量结果 → 有序步骤
├─ lib/client.js  (1379)  Client 半面:slot、面板、偏好、zh/en 字典、CSS
├─ locale/{en,zh}.json    展示元数据(meta.title / meta.description)
├─ icon.svg               插件图标
├─ README.md              本文件
├─ LICENSE                MIT
└─ smoke.mjs              68 条断言,零依赖

Client 半面注册的 slot:sidebar.panellist(order 50,仅设置页模式下拆除)、main(以包名为 key)、settings.section(order 30,仅侧边栏模式下拆除)、plugins.bundle.config(以包名为 key)。

License

MIT

星级评价

0.0 按 GitHub Stars 换算的社区热度,非用户打分
Stars0
Forks0
待解决 Issue0
最近更新2026-11-05

基本信息

作者stallenlu
版本1.4.0
开源许可MIT
默认分支main
入库时间2026-11-05

插件信息来自公开 GitHub 仓库与 SkillHub,版权归原作者所有;安装前请自行审查仓库代码。