Skip to content
dsh.fish
Bundle

dsh-windows-c-cleanup

DSH 插件:Windows 系统盘清理能力 —— 扫描 → 五级分级(可安全删除/谨慎删除/建议迁移/保护名单/长期防护)→ 用户确认 → 分层执行与迁移

Source
runcat-tommy
License
MIT
Updated
Updated 3 days ago

Readme

# dsh-windows-c-cleanup

[English](README.en.md) | **中文**

仓库:<https://github.com/runcat-tommy/dsh-windows-c-cleanup>(npm:[`dsh-windows-c-cleanup`](https://www.npmjs.com/package/dsh-windows-c-cleanup))

给 [DeepSeek Harness](https://github.com/deepseek-ai/deepseek-harness)(DSH)加上** Windows 系统盘(C 盘)清理能力**的插件。

它不是「一键删缓存」脚本,而是把清理拆成一条可审计的流水线:

> **扫描 → 五级分级 → 生成可视化报告 → 用户选择 → 分层执行 / 迁移到其他盘**

## 为什么需要它

C 盘爆满通常不是「垃圾文件」造成的,而是**本该放在别的盘的东西放错了地方**:包管理器缓存、IDE 索引、浏览器端侧 AI 模型、升级包残留、虚拟磁盘。

直接删风险很高(删掉 IDE 索引要重建几小时,删掉虚拟磁盘等于毁掉整个 Linux 环境)。本插件的做法是:

1. **先量后删**:按规则库把每个候选项测出真实大小,产出可视化 Markdown 报告;
2. **五级分级**:每个候选项都带判定理由,用户看得懂再决定;
3. **保护名单是硬约束**:未收录规则的一律按保护处理(不明即不删);
4. **能迁移就不删除**:有多个盘时,优先用 junction / 改配置把缓存搬到别的盘,长期不再回涨。

## 五级分级

| 级别 | 含义 | 执行策略 |
| --- | --- | --- |
| 🟢 可安全删除 | 缓存 / 临时文件 / 升级包残留,无数据损失 | 确认一次后可批量执行 |
| 🟡 谨慎删除 | 可重建但代价高(IDE 索引、端侧 AI 模型、包管理器仓库),或需管理员权限(WinSxS 走 DISM) | 逐项确认 |
| 🟠 建议迁移 | 搬到其他盘后应用无感,可用 junction 或改配置 | 迁移 + 建链接 + 记台账,可回滚 |
| 🔴 保护名单 | 用户文档、凭据、聊天数据、虚拟磁盘、IDE 配置、未识别路径 | **永不自动删除**(硬约束,用户规则也不得覆盖) |
| 🔵 长期防护 | 配置类动作:缓存重定向、存储感知、虚拟磁盘位置核查 | 一次性做好,长期少清理 |

## 安装

`dsh plugin` 是 **pnpm 的转发壳**:它把插件装进指定 profile,再按**装载后的真实状态**把它登记进 `dsh.profile.bundles`(本包声明了 `dsh.bundle.patch`,所以会被加进层叠列表)。三条路任选其一,**装完都要重启 DSH** 才生效。

### 方式一:直接从 GitHub 安装

```powershell
dsh plugin --profile web add github:runcat-tommy/dsh-windows-c-cleanup
```

- **适合**:想直接装仓库里的最新提交,本机已装 git。
- **注意**:本仓库的 `lib/` 与 `client/client.js` 是构建产物、**没有提交进仓库**(靠 `prepare` 脚本在安装时现构建),所以这条路会**跑一次构建、需要你审批**,`dsh plugin` 会打印审批提示。
- **更新**:重跑同一条命令(pnpm 会重新拉取该 git 源的最新提交)。

### 方式二:先从 GitHub 下载到本地,再从本地目录安装

```powershell
# 第 1 步:把代码取到本地(git 克隆,或者在 GitHub 页面点 Code → Download ZIP 后解压)
git clone https://github.com/runcat-tommy/dsh-windows-c-cleanup.git D:\dsh-plugins\dsh-windows-c-cleanup

# 第 2 步:从本地目录安装
dsh plugin --profile web add D:\dsh-plugins\dsh-windows-c-cleanup
```

- **适合**:想装之前先看一眼代码、想自己改、或者网络不稳(可以先下载 ZIP 再解压,不必用 git)。
- **注意**:本地目录会以 `link:` 形式装进 profile,**你改的代码立即生效**——宿主侧改动重启 `dsh web`,只改客户端界面则 `npm run build:client` 后刷新页面即可。
- **更新**:在本地目录 `git pull`,再跑一次 `npm run build`,然后重启 `dsh web`。

### 方式三:从 npm 安装(推荐)

```powershell
dsh plugin --profile web add dsh-windows-c-cleanup

# 想钉住某个版本时
dsh plugin --profile web add dsh-windows-c-cleanup@0.5.2
```

- **适合**:日常使用,最省心。npm 包里**已经带了构建产物**,所以不需要本机编译、不需要 git、也没有构建审批。
- **更新**:`dsh plugin --profile web up dsh-windows-c-cleanup`
- **卸载**(三条路都一样):`dsh plugin --profile web remove dsh-windows-c-cleanup`
- **看装了哪些**:`dsh plugin --profile web list`

### 三条路怎么选

| | 命令 | 需要什么 | 装进来的东西 | 更新方式 |
| --- | --- | --- | --- | --- |
| 一、GitHub 直装 | `add github:runcat-tommy/dsh-windows-c-cleanup` | 本机有 git;要过一次构建审批 | 仓库最新提交(安装时构建) | 重跑同一命令 |
| 二、下载到本地 | `add D:\dsh-plugins\dsh-windows-c-cleanup` | 无(ZIP 也行) | 本地目录(`link:`,改代码即生效) | `git pull` + `npm run build` + 重启 |
| 三、npm | `add dsh-windows-c-cleanup` | 无 | npm 上的发布版(已带产物) | `up dsh-windows-c-cleanup` |

一句话:**npm 最省心,GitHub 直装最省事(但要构建审批),下载到本地最灵活(改动立即可见)**。

### 装完重启

三条路都一样,重启 DSH 后插件才会被加载:

```powershell
dsh web
```

重启后打开任意会话,就能在会话视图顶部看到「磁盘清理」tab。

开发时也可以不安装,直接挂 overlay(注意 Windows 必须用 `file://` URL):

```powershell
node -e "const{pathToFileURL}=require('node:url');console.log(pathToFileURL(process.argv[1]).href)" "$PWD\src\index.ts"
dsh web --patch .\dev.cordis.yml
```

## 使用

插件注册一个工具 `disk_cleanup`:

| 参数 | 取值 | 说明 |
| --- | --- | --- |
| `action` | `scan` \| `plan` \| `apply` \| `migrate` \| `rollback` \| `trash` | 必填。`scan`/`plan` 只读;`apply`/`trash` 执行清理(M2);`migrate`/`rollback` 迁移与回滚(M3) |
| `scope` | `hotspots` \| `full` | `hotspots` 只按规则库测热点(快);`full` 追加全盘 Top-N 大目录(默认) |
| `reportPath` | 路径 | 报告落盘位置,缺省 `工作目录/C盘清理报告-<时间戳>.md` |
| `format` | `markdown` \| `json` \| `both` | 报告格式,缺省取配置 `defaultReportFormat`;`json` 产出机器可读报告(传 `x.md` 时会同时写同名 `x.json`) |
| `items` | 路径数组 | 要清理的具体路径(**谨慎层必填**:只接受用户逐项确认过的路径) |
| `grade` | `safe` \| `caution` \| `migrate` | 按层级选范围:`safe` 可批量;`caution` 必须同时给出 `items`;`migrate` 配合 `action=migrate` 自动挑选迁移层 |
| `mode` | `permanent` \| `trash` | 删除模式:`trash` 移到其他盘暂存区(可恢复,**默认**);`permanent` 永久删除 |
| `trashPath` | 路径 | 暂存区位置,**必须位于其他盘**(同盘移动不释放空间);缺省 `<空闲最大的非系统盘>:\to_delete` |
| `dryRun` | 布尔 | **默认 `true`**:只列出将要执行的动作,不删任何文件;用户确认后才传 `false` |
| `elevation` | `none` \| `dism` \| `cleanmgr` \| `dism+cleanmgr` | 是否一并触发管理员级系统清理(会弹 UAC) |
| `targetDrive` | 如 `D:` | 迁移目标盘;缺省自动选空闲最大的非系统盘 |
| `extraRulesFile` | 路径 | 本次扫描叠加的用户规则文件 |

调用示例(自然语言即可,模型会映射到工具):

> 帮我扫一下 C 盘,看看哪里占地方最大

工具会产出:

- **工具返回值**:盘符、剩余空间、各层可释放字节数、大头 Top-N、迁移目标盘建议;
- **可视化报告**(Markdown):汇总 → 🟥 大头 → 🟢/🟡/🟠/🔴/🔵 五级清单 → 执行结果。

### 执行清理(M2)

执行是**两阶段**的,安全默认值不靠调用方自觉:

1. **预演**:`apply` 不传 `dryRun` 时默认 `dryRun: true`,只输出「将要删什么、多大、为什么」的逐项清单,并落盘一份 `C盘清理执行报告-<时间戳>.md`;
2. **执行**:用户确认后,才用 `dryRun: false` 真正执行。

```jsonc
// 1) 安全层批量预演(不删文件)
{ "action": "apply", "grade": "safe" }

// 2) 用户逐项确认后的谨慎层(必须列出具体路径)
{ "action": "apply", "items": ["C:\\Users\\<你>\\AppData\\Local\\Temp\\某缓存"], "mode": "trash", "dryRun": false }

// 3) 永久删除(需用户明确同意)
{ "action": "apply", "items": ["..."], "mode": "permanent", "dryRun": false }

// 4) 附带管理员级系统清理(会弹 UAC,用户拒绝则如实回报)
{ "action": "apply", "grade": "safe", "dryRun": false, "elevation": "dism+cleanmgr" }
```

执行报告与返回值会给出:逐项结果(已删除 / 已入暂存区 / 部分删除 / 已拒绝 / 需提权)、**逐项测量合计释放量**、盘符空闲净增、被拒绝项的完整理由、提权脚本路径与日志摘要。

### 迁移与回滚(M3)

「删掉」只是治标——企业微信、WPS、浏览器、包管理器的缓存删完会再长回来(实测一轮清理后约 11 GB 被应用自己重建)。迁移是治本:把目录搬到其他盘,**在原位置留一个目录联接(junction)**,应用完全无感。

```jsonc
// 1) 预演迁移(不动数据)
{ "action": "migrate", "items": ["C:\\Users\\<你>\\AppData\\Local\\npm-cache"], "targetDrive": "D" }

// 2) 用户确认后执行;原目录变成 junction,数据在 D:\dsh-cc-migrated\npm-cache
{ "action": "migrate", "items": ["..."], "targetDrive": "D", "dryRun": false }

// 3) 自动挑选规则库里的 🟠 迁移层
{ "action": "migrate", "grade": "migrate", "dryRun": false }

// 4) 后悔了:依据台账搬回并删除联接
{ "action": "rollback", "dryRun": false }
```

不可让步的执行顺序:**复制 → 校验(大小与文件数)→ 删除源 → 建立联接 → 校验联接可读**。任何一步失败都会清理副本并保持原状,绝不留下「半迁移」状态让用户自己收拾。具体保证:

| 情况 | 行为 |
| --- | --- |
| 目标与源在同一盘 | 拒绝(移动不释放空间) |
| 目标同名目录已存在 | 拒绝并提示,**绝不合并** |
| 目标盘空间不足 | 拒绝,并给出需要的空间 |
| 源目录被应用占用、删不掉 | 回滚已复制的副本,原状态不变,提示先关闭应用 |
| 复制成功但建联接失败 | 明确报告数据已在新位置、老路径不可用,绝不谎报成功 |
| 回滚时源位置不是联接或指向不一致 | **拒绝回滚**,避免覆盖用户后来放回的数据 |
| 迁移台账 | `<migrationRoot>\ledger.jsonl`,逐条记录源/目标/大小/时间/方法 |

`app-config` 类规则(如 npm 缓存)除搬数据外,还会返回建议命令(例如 `npm config set cache "D:\..."`),但**不自动修改应用配置**。目录联接在 Windows 上不需要管理员权限。

### 历史趋势与 JSON 报告(M4)

删掉的缓存会长回来——本机实测一轮清理后约 11 GB 被应用自己重建(企业微信升级 1.93 GB、`%TEMP%` 1.47 GB、WPS 插件 1.97 GB、Chrome ~0.8 GB、uv 327 MB)。所以每次扫描都会往 `<DSH_HOME>\windows-c-cleanup\history.jsonl` 追加一条记录,并与上一次对比:

```
📈 与上一次扫描(24 小时前):剩余空间 −1.20 GB | 长回来 3 项 | 被释放 1 项 | 增长最多:…\WXWork\upgrade +1.93 GB
```

Markdown 报告里渲染成「📈 历史趋势」区块(长回来的 / 被释放的 / 新出现的 / 本次未再测到的);机器可读版本用 `format`:

```jsonc
{ "action": "scan", "format": "both" }   // → C盘清理报告-<时间戳>.md + 同名 .json
```

JSON 报告带 `schema: "dsh-windows-c-cleanup/report@1"` 版本号,含五级分组、大头、趋势与告警,可直接喂给 GUI / 脚本 / 监控。**趋势只在路径交集上比较**:扫描被时间预算截断时,「上次有、这次没有」不等于「已被清理」,报告里用 `previousPartial` 标注。

### 定时扫描与告警(M4)

默认**关闭**——后台扫盘会占用你的磁盘 I/O,属于需要你点头的行为。打开后:

```yaml
- id: windows-c-cleanup
  name: dsh-windows-c-cleanup
  config:
    schedule:
      enabled: true
      intervalHours: 24
      alertFreePercent: 10
      scope: hotspots
```

- 低于 `alertFreePercent` 时写一条告警记录(下次扫描的工具输出 / 报告顶部会显示),并经 `ctx.logger` 输出 `warn` 日志;
- 宿主只提供 `ctx.logger` / `ctx.effect`,**没有定时器服务**,所以用 Node 定时器 + `unref()` + `ctx.effect` 托管释放;
- **四道保护**:单飞(上一轮没跑完就跳过本轮)、首次延迟 1 分钟(避开启动抢 I/O)、整轮 try/catch(失败只记日志)、`unref()`(不阻止宿主退出)。

### 清理面板(M5)

插件在 Web GUI 里注册一个**对话视图 tab**(`conversation.view`,additive list 插槽,不覆盖任何现有界面):打开任意会话,切到「磁盘清理」就能完成「看懂 → 勾选 → 预演 → 执行」。

实拍(中文界面,与本文档语言一致;英文界面见 [README.en.md](README.en.md)):

| 中文界面 |
| --- |
| ![磁盘清理面板中文界面](assets/preview-zh.jpg) |

面板是**薄的**:它不做任何业务判断,分级、安全闸、测量、释放量核算全部复用宿主侧既有模块;面板调用的「预演」和「真执行」走的是**同一个 `executeCleanup`**(只有 `dryRun` 不同),所以预演里出现的每一项、每个理由都与真执行一致。

#### 功能区对照表(每个区块左上角都有名字标签)

面板从上到下分成这些区块,每块左上角都有一个固定名字的小标签(中英各一套,测试 10.1–10.6 守着)。**要指哪一块,直接说名字就行**:

| 模块名 | 位置 | 里面有什么 |
| --- | --- | --- |
| **概览** | 最上方标题栏 | 盘符剩余/总量与已用比例;右侧两个状态片:迁移目标(盘符 + 可用空间)、定时扫描开关 |
| **操作区(扫描 → 选择 → 预演)** | 标题栏下方 | 三组相邻控件,组间有向右箭头:① 范围 + 扫描 C 盘 ② 已选 N 项 + 清空选择 ③ 删除方式 + 预演 + `?`。**这里没有「确认执行」**——执行入口只有一处,在「预演结果」里 |
| **预演结果** | **紧跟在操作区下方**(点「预演」后出现) | 每项会发生什么(移到暂存区 / 永久删除 / 需提权 / 被拒)、合计释放量、被拒清单;**面板唯一的「确认执行」按钮就在这里**,旁边还有「再预演一次」。标题右侧的动态文字才是"这次预演算出了什么" |
| **状态与提示** | 预演结果下方 | 进行中的操作(⏳)、上一次操作的结果提示、报错,以及扫描被时间预算截断时的告警 |
| **清理候选(🟢/🟡/🟠/🔴)** | 中部四列卡片 | 可安全删除 / 谨慎删除 / 可迁移 / 保护名单,逐项路径、大小、判定理由;保护层不可勾选 |
| **长期防护** | 候选下方(可折叠) | 改设置或使用习惯就能反复受益的 6 条长期措施 |
| **执行进度** | 点「确认执行」后出现(候选下方) | 任务号与状态、进度条(来自宿主逐项记账)、逐项明细、释放量、取消任务 |
| **迁移预览** | 点卡片里的「迁移预览」后出现 | 源 → 目标映射、文件数、目标盘是否够、需要你手动改的应用配置;底部是迁移按钮 |
| **记录与产物** | 面板最底部 | 历史台账与报告目录的绝对路径(报告落到磁盘、保持中文) |

有两点值得注意:

- **「预演结果」是唯一被上移到操作区正下方的动态区块**(其余动态区块仍在候选卡片下方)。这样点完「预演」,结果和它的执行按钮就在你刚点的那排按钮下面,不用翻过四列候选卡片;没有预演结果时,这个位置留给「状态与提示」,扫描完的提示同样紧贴操作区。
- **执行入口只有一处**:「确认执行」只存在于「预演结果」里。工具栏不再提供执行按钮,于是"没预演就执行"在界面上根本没有入口(宿主侧的 `guardTargets` 与 `dryRun` 契约不变,仍是最终防线)。预演后改动勾选或删除方式会让预演作废、整块结果消失、需要重新预演;永久删除未勾确认框时按钮不可点,**原因写在按钮旁边**(测试 8.1–8.9)。
- 按用户要求,原「历史对比」模块(与上次扫描的时间差、长回来的目录)已从界面移除;宿主的 `state.trend` 字段仍照常返回,需要恢复时接回来即可。


工具栏按「同组相邻」排成三组,组间各有一个向右箭头标明先后关系,从左到右:

| 组 | 控件 | 说明 |
| --- | --- | --- |
| ① 扫描 | `范围` 下拉 + `扫描 C 盘` | 选 hotspots/full,然后开扫(已扫过则显示「重新扫描」)→ |
| ② 选择 | `已选 N 项` + `清空选择` | 实时计数与一键清空 → |
| ③ 预演 | `删除方式` 下拉 + `预演` + `?` | 「预演」右边就是问号说明按钮;执行按钮不在这里(见「预演结果」) |

箭头是纯装饰(`aria-hidden`,读屏会跳过),左右各留 12px 再加组内 6px 间距,所以留白明显,读起来就是「先扫描 → 再选择 → 最后预演」。

**可辨识度**(专门调过,不是默认样式):

- 「扫描 C 盘」「清空选择」「预演」三个动作按钮在**可用**时是品牌色加粗描边 + 底色淡染 + 投影 + 加粗字,一眼就能看出是按钮;**不可用**时保持原来的浅底灰边半透明样式——不能点的按钮绝不能画得像能点;
- 「确认执行」(在「预演结果」里)是实心品牌色:实心=最后一步动作,描边=可点的普通动作,层级不混;
- 「预演」右边的 `?` 是 24px 圆形按钮(2px 品牌色描边、底色淡染、加粗问号、悬停放大)。点开就地说明,文案刻意写得直白:第一句就是「点「预演」= 先干跑一遍,什么都不删」,再列三条要点(真的检查能不能删 / 真的算权限与空间 / 每项写明会发生什么),最后才提两点注意(数字是估算、查不出文件被占用)。

- 五级卡片:🟢安全 / 🟡谨慎 / 🟠可迁移 / 🔴保护,逐项显示路径、大小、判定理由;保护层**不可勾选**;
- 两步执行:**执行入口只有一个**,在「预演结果」里,所以必须先「预演」才可能执行;勾选或模式一变,预演即作废、结果整块消失,得重新预演。唯一还能拦住执行按钮的是"永久删除未勾确认框",此时按钮不可点,**原因直接写在按钮旁边**(禁用按钮的 tooltip 在多数浏览器里根本弹不出来,只写 tooltip 等于没写)——由 `runGate()` 纯函数判定,测试 8.1–8.9 钉住;
- 真实进度:进度条来自宿主的**逐项记账回调**(不是猜日志文本),随时可「取消任务」;
- 迁移预览:先看「源 → 目标」映射、文件数、目标盘是否够,再决定是否迁移;需要你改的应用配置只提示、不代改。

#### 切到对话页再切回来,正在做的事不会丢

面板是插在会话视图里的,**切走会卸载组件**(本地 React 状态随之清空)。所以凡是"正在发生的事",面板都**以宿主为准**去认领,而不是自己记着:

| 你切走时 | 宿主的权威状态 | 切回来看到 |
| --- | --- | --- |
| 正在「扫描 C 盘」 | `panelState().scan = {running, scope, startedAt}`(扫描本来就跑在宿主侧,组件卸载不影响它) | 「正在扫描 C 盘」的 ⏳ 立刻回来,之后每 1.2 秒问一次宿主 |
| 扫描刚在你切走时跑完 | 结果在宿主缓存里(`scan-view`) | 候选卡片直接出现,并补一条「扫描完成」提示 —— 不用重新点一次扫描 |
| 正在真清理 / 迁移(有任务在跑) | `panelState().runningJobIds` | 拿 jobId 接上进度轮询,「执行进度」的进度条与逐项明细继续走 |

宿主**同一时刻只扫一次**:面板回来后你要是又点了「扫描」,会接上还在跑的那一次(省一次全盘遍历,也不会让两份结果互相覆盖;测试 12.3–12.5)。

有一点如实说明:**勾选与预演结果不跨卸载保留**(改动勾选或删除方式本来就会让预演作废,回到面板时按"重新勾选 → 重新预演"走)。`scan` 与 `runningJobIds` 这两个字段是 0.5.1 加的,老宿主(还没重启 `dsh web`)缺字段时面板退化成原样行为、不报错(测试 11.6)。

#### 中英双语

面板与宿主文案都是**中英两套**,跟随 DSH 的语言设置自动切换,不需要手动选语言:

- 界面文案由客户端字典提供(`client/src/i18n.ts`,中英各 120 条,键集合严格一致),tab 标题是**函数式标签**,语言一换标题即跟着换,无需重新注册;
- 面板每次调用宿主 RPC 都会带上当前 `locale`,所以**宿主生成的文案**也跟着切换:规则库的每一条判定理由、安全闸的 12 条拒绝理由、执行器的逐项计划动作、迁移配置提示、调度状态。英文文案放在 `src/rules/default-rules.en.json`,按规则 id 与中文严格对齐(101 条规则 + 6 条长期防护,有测试守着覆盖率和「英文里不得出现中文」);
- **切语言不会让你重扫一遍**(`scan-view` 端点):扫描结果里的规则说明、截断告警都是宿主**生成那一刻**渲染好的字符串,切语言本来不会变——所以宿主额外提供「用缓存里那次扫描按目标语言重新出视图」(不碰盘、不测量,实测 0ms),面板在语言变化时自动调它,并顺带重跑一次预演与迁移预览,让界面不再中英混杂(实测截图暴露过这个问题);
- **提示句存的是「键 + 参数」**(`Notice`,见 `client/src/panel.tsx`):渲染时才取当前语言的 `t`,所以「扫描完成…」「预演完成…」这类提示会跟着语言切换,而不是冻结在生成时的语言(类型上也堵住了:传字符串编译不过);
- **默认中文**:模型工具(`disk_cleanup`)那条路不传 `locale`,输出与历史完全一致;只有 Web 面板会传 `en`;
- 英文缺项一律回退中文原文,宁可显示中文也不显示空洞;
- **接线有时序要求**:字典必须在框架渲染带 `locale:` 的注册项**之前**登记好,所以客户端插件用 `ctx.inject(['locale'])` 等语言服务就绪再接线;相应地 `dsh.client.inject` 里也声明了 `@deepseek-ai/dsh-client-locale`(这属于包元数据变更 → 首次升级要**重启 `dsh web`**,之后改文案只刷新页面即可)。

浏览器与宿主之间走 Connection 的通用 RPC 通道 `/dsh-c-cleanup`(`authority: loopback`,只接受本机调用),端点包括 `state` / `scan` / `scan-view` / `preview` / `execute` / `migrate` / `progress` / `cancel` / `history` 等。任务表是宿主内存态,宿主重启即清空。

`state` 端点是面板的"重新挂载入口",除了盘符与调度状态,还如实报告**此刻在不在扫盘**(`scan`)与**有哪些任务在跑**(`runningJobIds`)——面板切走再回来靠这两个字段把界面接上(见上一节),扫描本身与任务本身都跑在宿主侧,不受组件卸载影响。

**落地条件**(平台机制决定,不是本插件的选择):

| 改动 | 需要做什么 |
| --- | --- |
| 新增/删除插件包、改 `dsh.client` 字段 | **重启 `dsh web`**(包元数据判定被宿主永久缓存) |
| 只改 `client/client.js` 内容 | **刷新页面**即可(bundle 带 `no-cache`;本 profile 的 HMR 是关闭的) |

## 配置

在 profile 的 `cordis.patch.yml` 中覆盖(patch 会**整体替换**该行 config,不做深合并):

```yaml
- id: windows-c-cleanup
  name: 'dsh-windows-c-cleanup'
  config:
    reportDir: 'D:\reports'
    defaultScope: full
    hotspotTimeBudgetMs: 70000
    topTreeTimeBudgetMs: 70000
    topTreeMaxDepth: 3
    bigItemThresholdBytes: 2147483648
    allowProtectedOverride: false
    allowExplicitUnmatched: false
    defaultDeleteMode: trash
    trashPath: 'D:\to_delete'
    migrationRoot: 'D:\dsh-cc-migrated'
    historyPath: 'C:\Users\<你>\.dsh\windows-c-cleanup\history.jsonl'
    defaultReportFormat: markdown
    schedule:
      enabled: true          # 默认 false:不主动占用你的磁盘 I/O
      intervalHours: 24
      alertFreePercent: 10
      initialDelayMinutes: 1
      scope: hotspots
    extraRulesFile: 'D:\my-rules.json'
```

| 配置项 | 默认 | 说明 |
| --- | --- | --- |
| `reportDir` | 当前工作目录 | 报告输出目录 |
| `defaultScope` | `full` | 默认扫描范围 |
| `hotspotTimeBudgetMs` | `70000` | 热点清单时间预算(超时截断并标记) |
| `topTreeTimeBudgetMs` | `70000` | 全盘 Top-N 时间预算(两个扫描并行,总时长约等于较大者) |
| `topTreeMaxDepth` | `3` | Top-N 遍历深度 |
| `bigItemThresholdBytes` | `2 GiB` | 「大头」判定阈值 |
| `allowProtectedOverride` | `false` | **是否允许用户规则覆盖保护名单**(默认禁止;硬约束项永不放行) |
| `allowExplicitUnmatched` | `false` | 是否允许清理未收录规则库的显式路径(默认「不明即不删」) |
| `defaultDeleteMode` | `trash` | 默认删除模式:`trash` 移到其他盘暂存区,`permanent` 直接删除 |
| `trashPath` | `<空闲最大的非系统盘>:\to_delete` | 暂存区位置(必须与其他盘同盘不同卷才释放空间) |
| `migrationRoot` | `<空闲最大的非系统盘>:\dsh-cc-migrated` | 迁移根目录;迁移台账 `ledger.jsonl` 与之同目录 |
| `historyPath` | `<DSH_HOME>\windows-c-cleanup\history.jsonl` | 扫描历史(趋势对比数据源);故意放在不会被清理的位置 |
| `defaultReportFormat` | `markdown` | 默认报告格式:`markdown` / `json` / `both` |
| `schedule.enabled` | `false` | 是否启用定时扫描(**默认关闭**,需你显式同意) |
| `schedule.intervalHours` | `24` | 定时扫描间隔(小时) |
| `schedule.alertFreePercent` | `10` | 剩余空间占比低于该值时写告警 |
| `schedule.initialDelayMinutes` | `1` | 首次执行延迟,避开宿主启动抢 I/O |
| `schedule.scope` | `hotspots` | 定时扫描范围(比 `full` 快且省 I/O) |
| `extraRulesFile` | 无 | 用户附加规则文件 |

## 规则库

内置 100+ 条规则位于 `src/rules/default-rules.json`,全部用占位符书写(`%LOCALAPPDATA%`、`%APPDATA%`、`%USERPROFILE%`、`%WINDIR%`…),**不含用户名硬编码**,可跨机器复用。

规则形态:

```json
{
  "id": "npm-cache-local",
  "path": "%LOCALAPPDATA%\\npm-cache",
  "grade": "migrate",
  "reason": "npm 包缓存,体积常达数 GB;建议迁移到其他盘并从 C 盘释放",
  "migrate": {
    "method": "app-config",
    "targetHint": "<其他盘>:\\npm-cache",
    "configHint": "npm config set cache \"<目标路径>\""
  }
}
```

匹配语义:候选路径等于规则路径或位于其下即命中;`*` 只匹配单段目录名(如 `%LOCALAPPDATA%\*-updater`)。
优先级:**最具体的路径胜出**,同长度时 `protected` 优先;未命中任何规则 → 按 `protected` 处理。

自定义规则文件只需同结构:

```json
{ "rules": [{ "id": "my-cache", "path": "%LOCALAPPDATA%\\my-app\\cache", "grade": "safe", "reason": "自建应用缓存" }] }
```

试图覆盖 `overridable: false` 的保护名单项会被拒绝并告警(除非显式开启 `allowProtectedOverride`)。

## 安全设计

- **保护名单硬约束**:用户文档、桌面、下载、SSH/云凭据、`.dsh` 配置、聊天数据(企业微信/飞书/微信)、IDE 配置、`pagefile.sys` / `hiberfil.sys` / 虚拟磁盘、`Program Files`、`Windows` —— 永不自动删除。
- **未识别即保护**:规则库没收录的目录不会被删(除非显式开启 `allowExplicitUnmatched`)。
- **分层确认**:安全层可一次确认;谨慎层逐项确认;保护层不出现执行入口。
- **默认预演**:`apply` / `trash` 的 `dryRun` 默认为 `true`,不显式传 `false` 就不会删任何文件。
- **结构性禁忌**:盘根、`C:\Users` 等关键目录、目录联接/符号链接、父目录含 junction 的路径、非系统盘路径,执行前一律拒绝。
- **同盘暂存被拒绝**:暂存区与源在同一卷时移动不释放任何空间,因此直接拒绝并说明原因。
- **如实汇报**:被占用/无权限的文件删不掉时返回「部分删除」与残留字节数;用户拒绝 UAC 时返回 `elevation-canceled`,绝不谎报成功。
- **两种释放量都给**:同时给出「逐项测量合计」与「盘符空闲净增」——清理量较小时后者会被其他进程的写入掩盖。
- **时间预算**:扫描超时会截断并在报告与返回值里明确标记 `partial`,不会静默给出残缺结论。
- **不跟随链接**:junction / 符号链接一律不跟随,避免重复计数与递归踩坑。
- **迁移可回滚**:迁移用 `junction` 或应用配置改路径,并记录台账(M3 起提供 `rollback`)。

## 当前状态(M1 + M2 + M3)

- [x] 规则库(100+ 条,占位符化)+ 长期防护清单
- [x] 扫描:盘符信息、热点清单、全盘 Top-N、junction 安全测量、时间预算
- [x] 五级分级 + 未识别即保护
- [x] 可视化 Markdown 报告
- [x] DSH 工具注册(`disk_cleanup`,参数/输出 schema 校验通过)
- [x] M2 执行层:删除(安全层批量 / 谨慎层逐项)、暂存区与台账、UAC 提权(Windows\Temp / WinSxS / DISM / cleanmgr)、执行报告与 dryRun 默认
- [x] M3 迁移层:目录联接迁移(应用无感)、迁移台账与 `rollback`、对同盘/同名冲突/空间不足/源被占用的拒绝与回滚、app-config 建议命令
- [x] M4 打磨:扫描历史与趋势对比、JSON 报告、定时扫描与告警(cleanmgr/DISM 提权已在 M2 落地)
- [x] M5 Client GUI 面板:`conversation.view` 五级卡片、勾选、两步执行(预演 → 确认)、逐项进度与取消、迁移预览
- [ ] M6 发布:npm + 社区插件市场(GitHub 已完成)

## 已知限制(M1 + M2 + M3 + M4 + M5)

- **执行需要明确授权**:`apply` / `trash` 默认预演;真正的执行路径必须先跑扫描并把报告交给用户确认。`migrate` / `rollback` 仍是 `not-implemented`(M3)。
- **管理员级清理依赖 UAC 弹窗**:DSH 的权限栈没有 UAC 原语,插件通过 `Start-Process -Verb RunAs` 触发系统弹窗(脚本落在 `%TEMP%\dsh-cc-elevated-*.ps1`);用户不点「是」就无法清理 `Windows\Temp`、`SoftwareDistribution`、WinSxS 等,此时结果里会明确标记为「用户取消」。
- **被占用的文件删不掉**:浏览器、IDE、企业微信正在运行时其缓存文件会被锁定,结果中记为「部分删除」并给出残留量——这是 Windows 的正常行为,不是插件故障。
- **全盘 Top-N 覆盖度受 I/O 上限约束**:Node 的文件操作走 libuv 线程池(默认 4 线程),目录测量成本基本由**文件数**决定,加并发也提不上去。因此在 70 秒预算内,153 GB 已用盘的热点规则可 100% 覆盖,但全盘 Top-N 只能遍历部分目录(实测约 160 个顶层/浅层目录后截断)。
  截断时报告与工具返回值都会标记 `partial` 并说明原因,不会伪装成完整结论;需要更高覆盖率可调大 `topTreeTimeBudgetMs`(代价是等待更久)。**可执行结论来自热点规则,Top-N 只作兜底**,因此截断不影响五级清单的可用性。
- **释放量核算的两种口径**:小体量清理(数十 MB 级)时「盘符空闲净增」可能为 0(被其他进程同时写入掩盖),此时以「逐项测量合计」为准,报告里会同时给出并注明。
- **迁移耗时与被迁移体积成正比**:迁移是「复制 → 校验 → 删源 → 建联接」,数十 GB 的目录会很慢(工具超时上限 15 分钟);被应用占用的目录会在删源阶段中止并回滚副本。
- **`app-config` 类迁移不自动改配置**:插件只搬数据(并建立目录联接保证应用仍可用)并给出建议命令;是否让应用改用新路径由用户确认后自己执行,避免静默改坏环境。
- **扫描可能被时间预算截断**:热点清单与全盘 Top-N 各有 70 秒预算(可配 `hotspotTimeBudgetMs` / `topTreeTimeBudgetMs`),超时即截断并在报告与工具输出里标注,绝不当成「扫全了」。
- **定时扫描默认关闭且不做系统级唤醒**:依赖宿主进程存活(DSH 没跑就不会扫);需要开机级定时请用 Windows 任务计划调用 `dsh` 或本插件的 `action=scan`。
- **趋势不跨机器迁移**:历史文件是本机的,换机或删掉历史后第一次扫描没有对比基准(不会报错,只是不显示趋势)。
- 尚未提供交互式确认界面:目前由模型把报告交给用户,用户选定范围后再进入执行链路(M4 提供 GUI 卡片)。
- **面板的落地条件由平台决定**:新增/删除插件包或改 `dsh.client` 字段必须重启 `dsh web`(包元数据判定被永久缓存);只改 bundle 内容刷新页面即可(本 profile 的 HMR 关闭)。
- **面板与工具共用同一套判断,但入口不同**:面板只能做「扫描 / 预演 / 执行 / 迁移 / 回滚」这些已在工具里实现的动作,配置类改动(如定时扫描开关、`reportDir`)仍走 `cordis.yml`,面板只显示状态、不做持久化设置。
- **面板任务表在内存里**:宿主重启后面板会显示「任务已随宿主重启消失」,正在跑的任务随之中止(已落盘的报告与台账不受影响)。

## 开发

```powershell
npm install --legacy-peer-deps   # DSH 类型包 peer 冲突,本地用 legacy 解析
npm run deps:link                # 把宿主自己的 @deepseek-ai/* 链接进 node_modules(见下方说明)
npm run typecheck                # 类型检查
npm run smoke                    # 快速自检:规则匹配 / 盘信息 / 限时测量
npm run m2                       # M2 执行层隔离用例(真实删除只发生在 %TEMP% 沙箱)
npm run m3                       # M3 迁移层隔离用例(真实迁移只发生在 %TEMP% 沙箱 + D:\dsh-cc-m3-test)
npm run m4                       # M4 历史/趋势/JSON/调度语义(假扫描,秒级;含一次真实热点扫描)
npm run m4:live                  # M4 定时扫描端到端(真扫盘,约 1 分钟;历史数字与 fs.statfs 实测对比)
npm run m5                       # M5 面板宿主侧:分级/预演=执行同一条路/真暂存区/真迁移/取消/RPC 端点(含一次真实热点扫描)
npm run m5:client                # M5 客户端 bundle 契约:重放浏览器的模块加载并真渲染一次面板(离线,秒级)
npm run m5:live                  # M5 活体验证:直接问运行中的 dsh web 要 boot manifest 与产物(含与本地构建的哈希比对)
npm run market:check             # 发布体检:把"能不能被 DSH 市场自动收录"的硬门槛钉成测试(离线,秒级)
npx tsx tests/tool-run.ts full    # 无头跑完整扫描,产出真实报告
npm run build                     # 编译到 lib/ 并打包 client/client.js(发布物)
npm run build:client              # 只重新打包客户端 bundle(改了 client/src 之后)
```

> ⚠️ 不要用 PowerShell 的 `Get-Content`/`Set-Content` 管道改写本仓库的 UTF-8 文本文件
> (默认编码会把中文写成乱码并使 `package.json` 变成非法 JSON);请用编辑工具直接改。
>
> ⚠️ 不要跑不带 `--legacy-peer-deps` 的 `npm install`:它会按 peer 解析 prune 掉
> `@deepseek-ai/dsh-tools` / `dsh-llm` 等提供类型的包,导致 `npm run build` 报 implicit any。装完请复验 `npm run build`。
>
> ⚠️ `@deepseek-ai/dsh-tools` 一族把自己的运行时依赖声明成 **peerDependencies**,所以
> `--legacy-peer-deps` 永远不会装它们(测试会以 `ERR_MODULE_NOT_FOUND: Cannot find package '@deepseek-ai/dsh-xxx'` 崩掉),
> 而普通 `npm install` 又会把它们 prune 掉。`npm run deps:link` 用目录联接把**宿主自己那份**副本挂进
> `node_modules`,于是测试跑的就是宿主真实加载的包,且与宿主版本严格一致。该命令幂等,装完依赖重跑一次即可。

## 许可

MIT

Install

dsh plugin --profile web add github:runcat-tommy/dsh-windows-c-cleanup

Profile: web

  • This package builds from source on install. pnpm will ask you to allow its build script — that is permission to run the package’s code on your machine, outside the agent sandbox. Only allow sources you trust.
  • This source has no pinned commit, so a later push upstream changes what installs. Prefer pinning a commit.
Source