@was35/dsh-reclaim
审计并安全回收磁盘空间,将删除内容移入可恢复垃圾箱。 用于清理和回收DSH占用的磁盘空间。适合需要释放存储空间且希望安全管理删除内容的用户。
安装
npx -p @deepseek-ai/dsh dsh plugin --profile web add github:was35/dsh-reclaim说明文档
阅读完整 README ↗dsh-reclaim
Audit and safely reclaim the disk space DeepSeek Harness keeps forever. 审计并安全回收 DeepSeek Harness 永久占用的磁盘空间。
DSH 插件。功能已完成,尚未发布到 npm——安装方式见下方「安装」。
为什么有这个项目
DSH 官方明确不回收附件。@deepseek-ai/dsh-attachment-local 的 README 原文:
Images are kept forever — stored images are never deleted automatically, and nothing collects unreferenced objects.
官方也说明了推迟的原因:
Retention and garbage collection are deferred because resumed and forked sessions may share immutable objects …
也就是说,这不是漏做,而是一个需要先解决正确性问题的功能。dsh-reclaim 把它做出来,并且尊重官方指出的那条约束。
它做什么
- 审计 会话、附件、可重建缓存各自占了多少、还有谁在用
- 按分档规则给风险分级:安全(可批量清理)/ 需确认(逐个查看)/ 危险(不可清理)/ 未分类(只报告)
- 可重建缓存是真的能清的:派生图片、原子写暂存残留这些族会被枚举出来、搬进隔离区,再逐字节还原回原处。附件对象不在此列——它们全都至少被一个会话引用,所以分级规则恒判「危险」;这不是漏做,见 docs/safety.md §8.8
- 清理不是删除:数据搬进插件自带的隔离区,可随时恢复。唯一不可恢复的动作,是你在界面上对某一批明确选择彻底清空——它只销毁隔离区里已经搬进去的批次,不碰活数据
- 只读的 Agent 审计工具(模型能查、不能删)
安装
装进一个带 web 层的 profile(也就是平时用来开界面的那个):
# 从 GitHub 安装(尚未发布到 npm)
dsh plugin --profile web add "git+https://github.com/was35/dsh-reclaim.git#main"
dsh web
本仓库尚未提供 Release 包,所以上面那条 git+ 是目前唯一可用的安装路径。将来附上预构建包后,也可以下载后用 file: 安装——省去 clone 与依赖安装。
装完打开 设置 → 空间回收。
⚠️ 不要装进一个全新的空 profile。
dsh plugin --profile add建出来的 profile 只带@deepseek-ai/dsh-base,里面没有任何东西提供 web 连接层,插件会一直停在pending (waiting for service: connection),宿主启动失败。要用自己的 profile 时,让它包含@deepseek-ai/dsh-web-app(例如以自带的web模板为底),再装本插件。
装好后想确认它在服务,可以直接问它:
curl -b http://127.0.0.1:/api/dsh-reclaim/health
已发布到 npm 时会多一条更短的路径:
dsh plugin --profile web add @was35/dsh-reclaim。尚未发布, 所以上面那条git+https://才是当前可用的方式。注意:上游占用的是无 scope 的包名
dsh-reclaim,本仓库改用@was35/dsh-reclaim,两者互不冲突。 所以add dsh-reclaim装到的是上游版本,不是本仓库的改动。
支持的 DSH 版本
DSH 处于 developer preview,官方 README 明确写着「THERE WILL BE COMPATIBILITY-BREAKING CHANGES」。 而且它不强制检查插件声明的兼容范围(
engines.dsh/dsh.manifestVersion官方原话是"仅作声明", 安装器与加载器都不校验)。所以:装得上不等于跑得对,声明范围只能当文档读。
| 支持的 DSH | 状态 | 说明 |
|---|---|---|
0.1.5-rc.1 … 0.1.5-rc.3 | 已验证 | DSH 0.1.5-rc.1 宿主 + 0.1.5-rc.2 依赖包,真实数据 + 真实浏览器 |
0.1.7-rc.1 | 已验证 | 独立 home + 0.1.7-rc.1 的完整依赖树(277 个包)。能力探测 7/7 通过;该版本导出 dshCachePath,插件据此自动改选了新布局 cache/attachments/request-images;真实浏览器面板正常 |
0.1.6-* | 推断,未实测 | 位于两个已验证版本之间,故一并纳入范围;真要依赖它之前建议先验一遍 |
0.2.0-rc.2 | 已验证 | 本仓库实测:隔离 home 里的 web profile 装上本插件,能力探测 7 present / 0 missing,request-images 解析到 cache/attachments/request-images,dshCachePathExported: true。npm run test:host 每次都会重跑并断言这三条(以前只是打印) |
0.2.0-rc.2 desktop 端 | 已验证到「不需要专门适配」 | 见下节。npm run test:desktop 把它做成了断言 |
desktop 端:结论是没有 desktop 专属的东西需要适配
这一节的每一条都由 npm run test:desktop 断言,不是推断(下面引的宿主源码都在 app.asar 里,不在本仓库):
desktopprofile 无法从任何命令行启动,这是设计而不是缺陷。@deepseek-ai/dsh/lib/bin.js:35-37的rejectElectronProfile对--profile desktop无条件报错,而它在 profile action(:112)里没有开关保护;manageDesktopProfile: true只解锁dsh plugin --profile desktop(:119),不解锁启动。真正启动它的是 carrier——dsh-desktop-host/lib/index.js:216的main()自己调runProfile,绕过参数解析,并把--no-open --port 19387写死。所以第二个 desktop 实例 必然和活着的那个抢端口,任何隔离验证都不可能在真的desktopprofile 上做。- carrier 不是 profile 成员,它从外面叠上去。所以 profile 的组成就是它自己声明的 bundle 列表——两个官方 bundle 打头,后面 13 个是第三方插件。
- 这个插件需要的每个服务都由那两个官方 bundle 提供:读活 desktop profile 合成出的
cordis.yml(330 条),sessions/attachments/sessionQuery/tools/connection.fetch的提供者分别是@deepseek-ai/dsh-session-persistence-jsonl、dsh-attachment-local、dsh-session-query-sqlite、dsh-tools、dsh-client-connection,全部来自dsh-base+dsh-web-app,那 13 个第三方 bundle 一个都没参与。 - 插件分不出两者:
lib/里DSH_PROFILE与profileContext零命中,唯一读的环境变量是DSH_HOME(lib/paths.js:260-262),而 desktop 与 web 用的是同一个~/.dsh。它的全部世界观就是 home 目录。
npm run test:desktop 因此做四件事:只读扫描活 desktop profile 的配置并断言上面第二、三条;断言 lib/*.js
里除 DSH_HOME 外不读任何环境变量、也不提 profile 名(那就是第四条,多读一个变量就 FAIL,因为那会是一个
可能在两端不同的新输入);在一个从 web 模板克隆出来的一次性 home 里跑完整闭环;前后比对活 desktop profile
三个文件的 sha256——最后这条是「没有污染全局环境」的断言,不是承诺。它默认保留那个 home
(/tmp/dsh-reclaim-desktop-verify),可以继续用来验别的插件,并打印一条 rm -rf 供你随时完全清理。
仍未做的:在 Electron 窗口里真的点一遍(见 docs/safety.md §9 第 10 条),以及那 13 个第三方 bundle 会不会影响本插件——它们不提供任何被依赖的服务,所以按当前证据不会,但那是从「提供者是谁」推出来的, 不是实测出来的。
版本范围的实现方式值得说明:必须逐 minor 串 OR 子句,
>=0.1.5-rc.1 =0.1.6-0 =0.1.7-0 =0.2.0-rc.1engines.dsh与peerDependencies` 里这段文本逐字相同,避免两者漂移。用
0.2.0-0 只因为宿主判定时带includePrerelease才侥幸放行0.2.0-rc.2(详见 docs/safety.md §7.4)。
为什么不写成"看起来更合理"的范围
顺手记在这里,免得后人(包括我自己)把它"简化"回去:
>=0.1.5-rc.1 =0.1.0-rc.8 =0.1.5-rc.1 =0.2.0-rc.1 =0.1.6-0 =0.1.7-0 =0.2.0-rc.1 =0.1.5-rc.1 =0.2.0-rc.1 ].layout.dshCachePathExported**——顶层并没有layout对象,照旧写法取值只会拿到undefined并误判成「版本不符」(见 docs/safety.md §8.1)。它在0.1.5上应为false、在0.1.6+上应为true——如果与目标版本不符,说明插件拿到的仍是旧依赖,这次验证不算数。
怎么自己验一遍
仓库自带两层闭环,都不需要额外依赖:
npm test # 单元:node:test,零依赖
npm run test:host # 真宿主在环:装一遍、启一遍、探 /health,再清一个缓存族并还原
npm run test:desktop # 上面那一整套,外加对活 desktop profile 的四条断言(只读)
npm run test:all # 先单元、后宿主
npm run test:host 默认在 /tmp/dsh-reclaim-verify 下建一个一次性 web profile,跑完删掉;
KEEP_HOME=1 npm run test:host 保留它,那个 profile 可以继续用来验别的插件。它不碰 desktop profile,
并且自带一道拒绝真实 home 的守卫——把 VERIFY_HOME 指向 $HOME 会被直接拒绝(退出码 2)。
npm run test:desktop 是宿主闭环的超集:它先只读扫描活 desktop profile 自己的配置并断言「插件要的五个服务
全由那两个官方 bundle 提供」,再在 /tmp/dsh-reclaim-desktop-verify 下从 web 模板克隆一个
desktop-clone profile 跑完整闭环(端口 19400,避开桌面端写死的 19387),最后比对活 desktop profile 三个
文件的 sha256 证明没碰过它。那台机器上没有 desktop profile 时它以退出码 3 跳过并说明原因。它不在
test:all 里——test:all 要在任何机器上都能跑,而这个脚本的前提是装了桌面端。
宿主那一层刻意不用测试替身:它证明的是宿主真的接受这份 manifest 与 patch。脚本还会显式断言 stderr 里
没有 did not activate——宿主带着一个挂掉的插件条目也能正常启动,不断言这一条就会假绿。
/health 里的路径解析结果也是断言出来的,不只是打印出来给人看:request-images 必须解析到 cache
候选、dshCachePathExported 必须为 true、anomaly 必须为 null。这三条只有这一层验得到——单元那层
把 @deepseek-ai/dsh-home-paths 换成了替身,真包的契约它证不了。加这三条之前它们只是三行 console.log,
而 ok 与 capabilities 由后端能力探测产生,路径解析出错不会让它们变红,所以布局回归能在这一层一路绿着过去。
宿主闭环的最后一步(scripts/verify-clear.mjs)是唯一能证明这个插件的核心承诺的地方:它在一次性 home 里
种两个夹具文件,断言缓存族被判 safe / batchClearable,POST /clear 把它搬进隔离区且原路径消失,
manifest 记下「哪一行 id 触发的」与 originalPath → relativePath 的映射,最后 POST /restore 逐字节放回原处。
最后一步是要害:全部承诺是「清理是搬运不是删除」,而关于还原的承诺只有在真的还原过一次之后才算数。
单元层证不了它——替身不枚举附件后端,所以那里造不出任何一行可清理的行。
顺带说明为什么以前没有测试:
lib/里唯一的裸 import 是@deepseek-ai/dsh-home-paths,它只存在于宿主的 asar 内,所以在宿主体外一个模块都加载不了。test/hooks/用一个解析钩子把这一个 specifier 映到测试 替身,生产代码一行不改;替身故意不导出dshCachePath,让默认布局是确定的。
canonicalizeWatchPath只在真的移动或还原时才被调用,/health走不到它——上面那最后一步正好是它的第一次 真实触发。仍未实测的是它的边界(不存在的路径、符号链接、权限错误),记在 docs/safety.md §9 第 9 条。
它凭什么判断
引用关系只认结构化字段,不在日志文本里搜哈希。 判定相对于"这一批保留下来的东西",并遵循一条硬规则:
只要有任何一个会话的引用关系读不出来,「孤儿附件」这一档就整体暂停。
宁可少删,不可删错。完整的分档判定规则、安全保证与已知边界见 docs/safety.md;该文档第八节逐条记录了代码与本文档已知的出入——其中 §8.5(「危险」档只在界面成立,且第一版修复在引用图只读出一半时失败开放)、§8.6(两条纵深防御守卫从未生效)与 §8.8(分级里标成「可批量清理」的缓存字节没有任何通路会真正释放)已在本仓库修复并留档。§8.8 里有一半是故意的:附件对象至今一个都清不掉,因为它们全都至少被一个会话引用,分级规则因此恒判「危险」——要让「零引用」的行存在,那是改判据而不是加通路,必须同时重过 §2 的硬规则,本轮没做。改动前先读那一节。
同一节还记下了修复之后的代价,因为它们是有意选择的:一条删除现在会多读一次引用关系(缓存命中时为零),被别的会话引用的会话会被服务端直接拒绝搬走,引用关系读不出来或没能证明读完时也拒绝——宁可少删,不可删错。
不做的事
不清理日志 / 备份目录 / 附件存储之外的临时文件(附件存储自己的原子写暂存区 attachments/v1/tmp 属于可重建残留,会清);不清理附件对象——它们全都被至少一个会话引用着,所以一个都到不了「安全」档,见 docs/safety.md §8.8;不管理 profiles/ 下的插件依赖;不做定时自动清理;不允许模型替用户删数据;不支持远端 / 共享存储后端。
上面这些不是设计目标,而是已经实现的行为:所有破坏性操作都要人在界面上明确确认,Agent 侧只有只读工具。