Passer au contenu principal
Y

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.

Installer

dsh plugin --profile web add github:yanli-nyan/dsh-recycle-bin

README

@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
嵌套 shellcmd /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_codeNode 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.0trash 工具 + 永久删除守卫 + 系统提示
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 安装包

许可

MIT

Plugins associés