- Главная
- Плагины
- Безопасность и права доступа
- dsh-recycle-bin
dsh-recycle-bin
yanli-nyan/dsh-recycle-bin
Sends file deletions to the Windows Recycle Bin, lists and restores them by id, and blocks permanent shell deletes.
Установка
dsh plugin --profile web add github:yanli-nyan/dsh-recycle-binREADME
@local/dsh-recycle-bin
把 DSH 的文件删除送进 Windows 回收站,并拦截 shell / 代码通道的永久删除。
DSH 不提供 delete 工具,ctx.fs 也没有删除方法,模型只能通过 pwsh、bash、
terminal_send 或 run_code 执行命令来删文件。本插件从四个层面接管这条路径:
trash工具 — 删除文件与目录,真正进入系统回收站,可从资源管理器还原;trash_list/trash_restore— 列出回收站内容,按 id 放回原位置;- 永久删除守卫 — 拦截上述四个入口上的永久删除命令;
- 系统提示 — 告知模型本环境没有永久删除,改用
trash。
零依赖:只 import Node 内置模块,任何 profile 下都能解析。
适用范围
本插件是 DSH 的工具层守卫,不是文件系统驱动。
| 场景 | 是否受保护 |
|---|---|
模型通过 pwsh / bash / terminal_send / run_code 删文件 | ✅ 拦截或引导到 trash |
模型调用 trash | ✅ 文件进回收站,可还原 |
| 你在资源管理器里按 Shift+Del | ❌ 与插件无关 |
| 其他程序(编辑器、git、npm、杀毒软件)删文件 | ❌ 与插件无关 |
| DSH 之外的任何进程 | ❌ 与插件无关 |
要覆盖整机删除,需要文件系统 ACL 或专门的文件监控,不属于插件能力范围。
安装
从 GitHub(推荐):
dsh plugin --profile desktop add https://github.com/yanli-nyan/dsh-recycle-bin
从 Release 的 tgz 包离线安装:
dsh plugin --profile desktop add ./local-dsh-recycle-bin-1.6.0.tgz
从本机目录(开发时用,改代码即时生效):
dsh plugin --profile desktop add <本目录绝对路径>
也可在插件管理中作为 bundle 安装。变更代码后必须重新加载(插件管理里禁用再启用,
或重启 DSH);只替换磁盘文件不会重载内存中的模块。用 tgz 或 git 方式安装的是
解包副本,升级需先 remove 再 add。
要求:Windows + powershell.exe(trash 依赖它)。没有 PowerShell 时工具不可用。
配置
在 profile 的 cordis.patch.yml 中覆盖,不需修改本包:
- id: recycle-bin
config:
mode: enforce # off:只提供 trash,不做拦截
enforceOnTerminal: true # 拦 terminal_send(持久终端绕过 pwsh)
enforceOnCode: true # 拦 run_code 的 Node fs 删除
enforceScriptFiles: true # 读命令行里点名的脚本文件内容再判断
listTool: true # 注册 trash_list
restoreTool: true # 注册 trash_restore
allowPaths: # 构建产物可直接删除,不打扰
- .dsh/tmp
- node_modules
allowPaths 显式留空会回落到默认值,而不是清空白名单。
拦截覆盖
| 通道 | 识别方式 |
|---|---|
| 直接删除命令 | Remove-Item / rm / del / rmdir / unlink / rimraf / git clean |
| 嵌套 shell | cmd /c、powershell -Command、Start-Process(含 -FilePath) |
| 解释器绝对路径 | C:\Windows\System32\cmd.exe /c del 等按 basename 识别 |
| 组合短开关 | bash -lc / -ic、sh -ec、zsh -c 等捆绑形式 |
| 编码命令 | powershell -EncodedCommand <base64>,静态解码后判定,不执行 |
| 删除 API | [System.IO.File]::Delete、.Delete()、.MoveTo()、shutil、robocopy /MIR |
| 内容破坏 | Clear-Content、Set-Content x ""、type nul > x、> 重定向、truncate |
| 脚本文件 | 读取 pwsh -File x.ps1 / bash x.sh / python x.py 的内容判断 |
| 内联代码 | node -e / python -c 中的 fs.rm、shutil.rmtree |
run_code | Node fs.rm/rmSync/unlink、spawn 子 shell 删除 |
| 变量间接 | $c = 'Remove-Item'; & $c 跨语句绑定 |
| 常量拼接 | fs["rm"+"Sync"]、[].join()、模板串、.concat() 折叠后识别 |
content destruction 类单独列出,因为它不删文件却让内容永久消失,后果与删除相同:
一个被 Clear-Content 清空的文件同样无法从回收站找回。
脚本文件的判定细节
- 相对路径按多个候选目录解析:调用时的
workdir、会话目录、进程目录。 守卫是同步的,看不到工具真正的工作目录,所以逐个尝试;任一处命中删除即拒绝。 - 读不到按不安全处理:路径存在但无法读取(权限不同)判为删除;确实不存在才放行, 因为不存在的脚本也无法执行。
- 超过 512 KB 不跳过:读首尾各 256 KB 再判断,长构建脚本的清理步骤照样能看到; 但如果删除命令恰好落在中段会漏(见「仍会漏的情况」)。
- 编码自适应:识别 UTF-16LE / UTF-16BE(含与不含 BOM)与 UTF-8 BOM。
PowerShell 存成 "Unicode" 的脚本按 UTF-8 读会变成乱码,真实的
Remove-Item就看不见了。 - 脚本内目标参与豁免判定:脚本删
.dsh/tmp下的产物与直接写命令结果一致,都放行。 - 注释与正则字面量不算删除:
# Remove-Item x、/shutil\.rmtree/不会误判。 插件自身源码满是删除词,不加这条node --check index.js都会被拦。
仍会漏的情况
以下情形仍会把文件彻底删除,实测确认,不做掩饰:
| 情形 | 例子 | 原因 |
|---|---|---|
| 变量参与拼名 | const a='rm'; fs[a+'Sync'](p) | 名字在执行时才存在 |
| 先解码再执行 | eval(Buffer.from(i,'base64').toString()) | 内容需解码才可见 |
| 方法引用转存 | const f = fs['rm'+'Sync']; f(p) | 删除动作不在调用点 |
| 删除藏进模块 | require('./helper').clean(p) | 只看到被调用的函数名 |
| 非删除语义工具 | dd、shred -u | 名字里没有删除语义 |
| 超大脚本中段 | 删除命令落在 512 KB 脚本正中间 | 只扫首尾各 256 KB |
| UNC 网络脚本 | \\server\share\k.ps1 | 不可达,读不到内容 |
| 配置关闭 | enforceScriptFiles: false / mode: off | 你主动关的 |
前四条属于原理性上限:它们要求执行才能知道结果,而守卫只能看文本。 第五条是黑名单的固有缺陷 —— 名单永远赶不上下一个工具。
因此不要把 guard 当作安全边界。 它降低误删概率,不提供保证。 要真正闭环,需把策略下沉到文件系统 ACL,撤销工作区的删除权限, 由操作系统拒绝,而不是用正则推测命令想做什么。
trash 返回状态
| status | 含义 |
|---|---|
recycled | 已进回收站,可还原 |
missing | 路径确实不存在 |
unavailable | 盘符不存在或卷未就绪 |
denied | 路径存在但无权限查看 |
refused | 网络路径或无回收站的卷(FAT32/exFAT/U 盘),主动拒绝 |
failed | 真失败,reason 说明原因 |
refused 是主动拒绝:这些位置删了无法还原,所以宁可失败。
被拒后若仍要删,只能改用 Remove-Item 之类,那就回到上面「仍会漏」的范畴。
列出与还原
删除之后可以用工具拿回来,不必打开资源管理器。
trash_list
trash_list({ limit: 20 })
返回 { entries, total },entries 按删除时间倒序:
| 字段 | 含义 |
|---|---|
id | 条目 id,传给 trash_restore |
path | 删除前的原始完整路径 |
size | 原始字节数 |
deletedAt | 删除时间(epoch 毫秒,0 表示未知) |
直接读取各卷 $Recycle.Bin 下的 $I 元数据文件,不走 Shell COM,
因此在多卷机器上也能列出所有盘的内容。
只列出 $R 数据文件仍存在的条目。系统在还原时只删 $R、留着 $I,
这些「有元数据没数据」的孤儿条目无法还原,列出它们只会误导。
trash_restore
trash_restore({ ids: ["KFV3TQ.txt"] })
对每个 id 返回 { id, status, path?, reason? }:
| status | 含义 |
|---|---|
restored | 已放回原位置,path 是还原后的路径 |
missing | 没有这个 id 的可还原条目 |
conflict | 原位置已存在同名文件,不覆盖 |
failed | 还原失败,reason 说明原因 |
还原调用 Shell 的 Undelete 动词,与资源管理器右键「还原」是同一操作。
$R 文件直接按普通文件打开时没有这个动词,所以脚本会先在回收站命名空间里
找条目,找不到再按卷打开 SID 目录。
原始路径来自 $I 解析,而不是 Shell 的 System.Recycle.DeletedFrom 属性 ——
后者在非系统卷上返回空值。
两个工具都可在配置里关闭:listTool: false / restoreTool: false。
设计内的拒绝
不是故障:
- 网络路径、非 NTFS 卷 →
refused。 - 裸
Remove-Item(无操作数)、管道喂路径 → 拒绝,因为无法证明目标是豁免目录。 node_modules2、.dsh/tmp-old→ 拒绝,按整段匹配不做前缀匹配。- 混合豁免与非豁免操作数 → 整条拒绝。
入参校验
trash 对 paths 的每种错误给出不同提示,便于模型自纠:
| 入参 | 提示 |
|---|---|
{} | 缺少 paths,并给出调用示例 |
{paths: "a.txt"} | 应为数组,提示包成 ["a.txt"] |
{paths: []} | 空数组,要求至少一个路径 |
{paths: [" "]} | 全为空白字符串 |
{paths: [1]} | 指出第几个元素类型不对 |
开发
node --check index.js
node test/guard-cases.mjs
test/guard-cases.mjs 是纯函数级用例集,不触碰真实文件。真实删除类用例需人工执行:
它们会把文件送进你的回收站。
版本历史
| 版本 | 内容 |
|---|---|
| v1.0.0 | trash 工具 + 永久删除守卫 + 系统提示 |
| v1.1.0 | 堵住嵌套 shell、.NET/Python 删除 API、变量拼接、run_code 通道;修复非 ASCII 路径误报、状态语义混淆、reason 乱码、绝对路径 .. 与 junction 穿越 |
| v1.2.0 | 预读脚本内容、Start-Process -FilePath、node -e、常量拼接折叠、入参错误分类 |
| v1.3.0 | 脚本相对路径按多目录解析、超限脚本读首尾、读不到改判为不安全、脚本内目标参与豁免判定、bash -lc 组合开关 |
| v1.4.0 | 绝对路径解释器、内容破坏类命令(Clear-Content 等)、-EncodedCommand 静态解码、脚本编码自适应 |
| v1.5.0 | 新增 trash_list / trash_restore:读取 $I 元数据列回收站、按 id 放回原位、过滤孤儿条目 |
| v1.6.0 | 插件图标改用 PNG(icon.png 256×256);Release 附带 pnpm pack 生成的 tgz 安装包 |
许可
Похожие плагины
dsh-infinite-gen-4
minglink/dsh-infinite-gen-4
dsh-purge
mmparko/dsh-purge
cc-safety-net
kenryu42/cc-safety-net
api-relay-audit
toby-bridges/api-relay-audit