查看: 176|回复: 3

Windows 清理脚本防误删:PowerShell 隔离区设计

[复制链接]
发表于 1 小时前 | 显示全部楼层 |阅读模式
把“清理 C 盘”这类操作交给 AI Agent 执行,真正的难点不在删文件,而在于删错之后还能把文件放回去。下面这套设计来自 pc-tuneup 这个 Windows 体检调优 Skill 的实践:用脚本编排加隔离区,把高危动作约束住。如果你也在为 Agent 编写带副作用的 Windows 工具(比如清理、改注册表、关进程、动系统配置),下面这些取舍可以直接借鉴。

一、为什么不能把 Remove-Item 交给模型

最直觉的做法是写一段提示词:“帮我清理临时文件”。实际跑下来,有几个必然会出问题的点:

1. 编造结果。没有加 --yes 的预演轮次里,模型能在总结中写“已清理 9.4 GB”,用户信了,实际一个字节都没动。
2. 绕路降级。某个文件移动失败(被进程占用)时,模型会自动改成 Remove-Item -Force“换个方式再试一次”,一步就把“可还原”变成了“不可还原”。
3. 顺手扩大范围。让它清桌面,它可能把桌面文件夹本身也一起处理;让它清缓存,它可能把 node_modules 也算进“缓存”。
4. 没有稳定的量词。“清理成功”到底是 3 个文件还是 3GB,没有脚本产出,就只能靠模型自己总结。

结论很明确:提示词约束不住破坏性操作,只有代码能。因此这个 Skill 的第一条原则是:Agent 只负责编排命令,不负责执行动作。所有删除、移动、关进程都必须经过 Python 脚本,脚本内部统一走隔离区并写清单。

对应的铁律有三条:

- 先扫描后执行,禁止跳步:必须 init → scan → plan → apply,apply 之前没有 plan 段会被脚本直接拦下。
- 默认预演,正式执行必须显式确认:apply 不带 --yes 只打印计划。
- 删除一律进隔离区:磁盘垃圾禁止直接 Remove-Item。

二、阶段机是硬校验,错误码要机器可读

阶段机不是画在文档里的流程图,而是每个子命令入口都要做的校验:init → scanned → planned → applied → finished。每个阶段只做一件事,中间产物全部写入 .tmp-tune/tuneup-plan.json,下一阶段进来的第一件事就是校验上一阶段的状态:
  1. def cmd_plan(args):
  2.     data = open_plan(args)
  3.     if data.get('stage') not in ('scanned', 'planned', 'applied'):
  4.         fail('STAGE_ORDER', '当前阶段为 %s,请先执行 %s scan' % (data.get('stage'), CLI))
复制代码

fail() 不是打印一句“请先扫描”就完事,它会把 {"ok": false, "code": "STAGE_ORDER"} 写到 stderr 最后一行,并以退出码 1 结束。这样做的原因是可操作性:Agent 判断“这步能不能继续”不能靠读自然语言输出,必须靠退出码和 code。跳步因此在协议层就失败,而不是依赖模型自觉。

退出码只保留三个值:0 表示正常(包含“没有可执行项”“批次不存在”这类无操作提示),1 表示前置校验失败或参数非法,2 表示 argparse 参数解析错误。

计划文件之所以用单文件而不是数据库,是因为本机单人使用场景下,“并发不安全”的代价可以接受,换来的是三个好处:断线可恢复(pc_flow.py status 随时告诉你当前阶段和下一步命令)、可审计(一轮跑完就是完整决策链路)、可重跑(init --force 覆盖旧计划即可开新一轮)。

三、三个不变量与可逆性分级

整个工具只围绕三句话设计:能不动就不动,要动就搬走不删,搬走也要能搬回来。落到实现上是三个机制:

- 只读优先:scan 阶段全部只读,一次取回回收站、磁盘垃圾、桌面、进程窗口、性能基线五类数据,不碰任何文件和进程。
- 默认预演:apply 不带 --yes 只打印“将要做什么”,preview 与 execute 走的是同一份动作清单。
- 可还原:磁盘垃圾移入隔离区并写 manifest,桌面文件逐条记录原路径,性能优化记录原电源计划 GUID 与原 VisualFXSetting。

真正不可逆的动作只有两个:清空回收站和关闭进程。回收站本身就是“已删除”的二次确认,保留没有意义,但正因不可恢复,回执里必须明确写“不可恢复”;进程关闭则只能提示用户自行重开。可逆性是分级告知的,不能笼统说一句“放心,可以恢复”——用户看到“回收站不可恢复”和“垃圾可还原”的差别,才会做出正确的勾选决策。

四、几个真正花时间的工程细节

4.1 一次 PowerShell 调用取回全部基线

这是性能上最关键的一条。部分 Windows 环境里 PowerShell 冷启动要 30~40 秒,如果按模块各拉一次(回收站一次、进程一次、桌面一次……),光进程启动就能耗掉三分钟。所以扫描脚本的做法是把所有 block 拼成一个脚本,输出单个 JSON。协议文档里专门写了提醒,防止后来人把它“优化”成多次调用:本环境 PowerShell 启动较慢(首次可达 40 秒),单次调用是刻意设计,禁止拆分。

4.2 -EncodedCommand 解决中文路径与引号

Windows 清理绕不开中文路径(桌面、下载、方案 - 副本.docx)。如果把路径直接拼进命令行,会同时踩两个坑:引号转义,以及 cp936 与 UTF-8 编码不一致导致的乱码。解法是不拼字符串,直接传 Base64 的 UTF-16LE:
  1. encoded = base64.b64encode((_PS_PRELUDE + script).encode('utf-16-le')).decode('ascii')
  2. subprocess.run([powershell_exe(), '-NoProfile', '-NonInteractive',
  3.                 '-ExecutionPolicy', 'Bypass', '-EncodedCommand', encoded],
  4.                capture_output=True, timeout=300, creationflags=flags)
复制代码

同时统一前置一段 prelude,一次性解决输出编码、错误与进度输出的干扰:
  1. _PS_PRELUDE = (
  2.     "[Console]::OutputEncoding=[System.Text.Encoding]::UTF8;"
  3.     "$ErrorActionPreference='SilentlyContinue';"
  4.     "$ProgressPreference='SilentlyContinue';\n"
  5. )
复制代码

还有一个不常见但真实存在的坑:不要按名字调用 powershell.exe。部分环境的 PATH 里带了非法前缀,直接调用会失败。要按绝对路径解析,优先 System32\WindowsPowerShell\v1.0\powershell.exe,兜底 SysWOW64。

4.3 保护路径要包含“我自己”

静态保护名单好写:文档、桌面、下载、图片、系统目录、Program Files。但最容易出事的场景是代码库:用户让 Agent 清理 C 盘,而 Agent 恰好在一个 30GB 的 monorepo 里工作,node_modules、dist、.cache 全在清理半径内。所以保护路径要在运行期动态追加工作区本身及其父目录:
  1. def protect_paths(config):
  2.     raw = list(config.get('protect', {}).get('paths', []))
  3.     raw.extend([PROJECT_ROOT, os.path.dirname(PROJECT_ROOT)])
  4.     return [os.path.normcase(os.path.expandvars(item)) for item in raw]
  5. def is_protected(path, protected):
  6.     target = os.path.normcase(os.path.abspath(path))
  7.     return any(target == item or target.startswith(item + os.sep) for item in protected)
复制代码

判定用“等于或位于其下”,而不是字符串模糊匹配,靠 item + os.sep 保证边界,避免 C:\Users\me\Desktop-old 被 Desktop 误伤。这条是运行期规则,不写进配置文件——它取决于 Agent 在哪跑,写死反而失效。

4.4 隔离区加 manifest,后悔药要能双向走

隔离区不是“换个目录放垃圾”这么简单,它必须支持还原。位置优先 D:\$PC-Tuneup-Quarantine,无 D 盘回落 C: 根目录,刻意放在技能目录之外,否则清理时容易连隔离区一起删掉。每次 apply 落地生成批次号 b-YYYYMMDD-HHMMSS,同批多个模块共用目录;每个条目写进 state/manifests/<批次>.json,包含 module、kind、source(原路径)、target(隔离区路径)。还原就是反向 Move-Item,目标已存在时自动改名,绝不覆盖。

有个容易忽略的细节:清理时只移走目录内容,保留目录本身。系统可能正持有该目录句柄(%TEMP%、C:\Windows\Temp 尤其常见),删目录会导致后续整块失败。

4.5 回收站必须分批删

一次性清空回收站时上万文件同时删除,磁盘 IO 打满、资源管理器长时间无响应。根因是条目过万时系统一次性删除造成 IO 峰值。做法是按批删除,默认 200 个文件一批、批间停顿 200ms,参数放在配置里:
  1. "recycle": { "chunkSize": 200, "pauseMs": 200 }
复制代码

PowerShell 侧用切片循环加 Start-Sleep,并对批大小加 max(10, chunkSize) 的下限保护,防止配置被误改成 1 后删一万个文件要跑几小时。

4.6 缓存类目标:180 天门槛与“跳过”语义

清理缓存最大的风险不是删不掉,而是删掉正在用的东西:pnpm store 删了要重装依赖,浏览器缓存删了要重新加载资源,着色器缓存删了要重新编译。所以缓存类目标(cache: true)的规则是:只处理 180 天以上未修改的条目,占用中或近期写入的跳过。

配套两个细节:一是缓存类移动失败计入“跳过”而不是“失败”,因为被占用是预期内的正常现象,不该污染失败率;二是崩溃转储、蓝屏转储、CBS 日志这类非缓存目标不套门槛,按整目录清理,它们本身就是排障产物,留 180 天没意义。扫描时还要回传 keptBytes(未达门槛的保留量),让用户知道“这一项看着大,但大部分不能动”。

enabled: false 的条目连扫都不扫。prefetch 和 cbs-logs 默认就是关的:前者删了短期拖慢常用程序启动,后者排查系统更新问题要用。默认值就是产品策略,把“不推荐”做成默认关闭,比在文档里写十行警告更有效。

4.7 下载目录:同名归一才有价值

只按“多少天没动”清下载目录,收获很有限。真正占地方的是同一份文件下载了五六次:季度报表.xlsx、季度报表 (1).xlsx、季度报表(2).xlsx、方案 - 副本.docx、方案 - Copy.docx。所以做了两类动作:download:stale 处理修改时间早于 staleDays(默认 30 天)的文件;download:duplicate 在归一化后同名多于一份时,除最新一份外全部处理。

归一只剥离系统自动生成的后缀且可叠加,比如括号数字、“- 副本”与“- Copy”都会剥掉,但严格不做过度归并:report2026.xlsx 和 report2027.xlsx 不算同名。保留优先级是“修改时间最新”,时间相同则取名字最短的那份。

4.8 桌面整理:只动文件,且只动指定类型

桌面是用户心智最敏感的区域,规则收得很紧:只扫桌面根目录下的文件,不递归、不动文件夹;跳过 desktop.ini、隐藏文件、系统文件;跳过 .lnk、.url、.ini、.webloc,快捷方式是高频入口,动了会挨骂;按扩展名归入文档、图片、视频、音频、压缩包、安装包、其他;同名冲突自动加后缀,不覆盖。一个 desktop:group 动作覆盖全部分类,清单里只出现一条。

需要澄清一个概念:桌面目录本身在保护名单里,但桌面整理是在桌面内部把文件移入子目录,不跨盘、不删除,所以是允许动作。桌面文件只有 desktop 模块能碰,其他模块一律不越界。

4.9 关闭进程:两级安全阀

进程模块风险最高(可能丢掉未保存数据),设了两道阀。第一道是默认不勾选:清单里的进程类一律 enabled=false,必须显式指定 PID,例如 apply --module process --ids process:12345 --yes。第二道是先优雅后强制:默认只调 CloseMainWindow(),等同于用户点窗口右上角的 ×,有未保存内容时进程会自己弹窗拒绝。结果分四种状态——closed 已正常关闭、gone 进程已不存在、waiting 窗口没关掉可能在等保存确认、nowindow 该进程实际没有可关窗口。看到 waiting 时不允许自动升级为 --force,必须回报用户确认,这条写在规则文件里,因为模型最容易在这里自作聪明。

候选筛选也刻意保守:必须有主窗口,或内存 ≥ 200MB,且不在保护名单。无主窗口且内存小于 200MB 的一律视为后台服务,只计数不参与候选,否则清单里会冒出几百个 RuntimeBroker 之类的组件,用户根本没法判断。保护名单除系统关键进程外还包含当前工具链:powershell、pwsh、python、cmd、windowsterminal——Agent 自己就跑在 Python 进程里,把父进程杀了整个流程当场断掉。

4.10 性能优化:所有动作都要记原值

三个动作都只动当前用户级设置:电源计划切换到高性能走 powercfg /setactive <GUID>,记录当前方案 GUID;刷新 DNS 缓存走 ipconfig /flushdns,无需记录;视觉特效调最佳性能设置 VisualFXSetting = 2,记录原值。两个细节:已是高性能时不生成该动作,避免制造零收益动作;visual-effects 默认不勾选,因为它牺牲动画与字体平滑的观感,属于审美偏好,不该由工具替用户决定。

五、无人值守:把交互确认变成书面授权

挂到每日自动化任务上时,关键是把交互式确认转化成逐模块的预授权。提示词有四层结构:一是顺序,把 init → scan → plan → apply → receipt → clean 显式列出,配合脚本的阶段校验形成双保险;二是逐模块授权,不是笼统说“你可以清理”,而是精确到 --module junk --yes,回收站显式排除、进程显式禁止,授权边界比授权本身更重要;三是禁止项复述,走隔离区、禁止手工拼回执、禁止伪造结果,这些在规则文件里也有,但提示词里再写一遍,因为它们最容易被模型“合理绕过”;四是输出契约,要求给出回执、隔离区批次目录和还原命令,用户拿到的不只是“已清理 6.3GB”,而是“这 6.3GB 在哪、怎么拿回来”。

还有一个风险提示细节:C 盘可用空间低于 10% 时在回执开头单独标出。这不是清理能力,而是预警——空间极度紧张时连隔离区本身都可能写不下。

六、回执与台账:数字只能由脚本产出

回执做了件反直觉的事:禁止表格,一律列表。原因有三个:终端宽度不可控,表格在窄终端会折成不可读的碎片;Markdown 表格在聊天窗口经常渲染失败;列表对“逐条失败”更友好,一项失败要带 PowerShell 原文关键句,表格单元格塞不下。回执固定包含环境、磁盘、扫描、执行、合计释放、隔离区和耗时几段,首次渲染时自动写 state/runs.jsonl(时间、耗时、释放量、成功失败数、批次),重复渲染不重复记账。这样“这次到底清了多少”永远来自脚本而不是模型记忆;台账攒起来还能看趋势——释放量突然变少说明该清的清完了,突然变多通常是某个应用在疯狂写缓存。

隔离区默认保留 7 天,到期也不自动删,只在 init 时提示,并在回执末尾问一句是否要永久清掉旧批次。不自动清理的理由是:隔离区是唯一的后悔药,自动清理等于偷偷缩短它的有效期。

经验台账则解决“同一个坑反复踩”的问题,提供 add、list、merge、retire 四个命令,按“现象(保留 PowerShell 原文关键句)加根因加下次怎么做”记录。约束有三条:只记会重复踩的坑,一次性偶发失败不入库,否则台账被噪声淹没;相似度 ≥ 0.75 自动合并、hits 累加、fix 以最新写入为准,超过 200 条按命中次数与最近时间截断;init 自动回显命中次数最多的 3 条,让本次执行按经验走——经验不推到眼前等于没记。

更重要的是另一条:规则层能解决的必须回写规则,不能只记台账。二次以上重复出现且规则层可解决时,白名单类改配置,判定逻辑类改扫描脚本,流程口径类改铁律与安全边界,改完必须重跑 init → scan → plan 确认不报错。台账是补丁,规则才是根治。全程不做埋点上报、无外发网络请求,这不是洁癖:一个能删文件、能关进程的工具如果还在往外发数据,任何安全评审都过不了。

七、踩坑清单

- 一次性清空回收站导致系统卡死:上万文件同时删除,磁盘 IO 打满、资源管理器长时间无响应。改为按批删除,200 个一批,批间停顿 200ms。
- 缓存目录全清或全不碰的两难:旧逻辑无法区分可重建缓存与有排障价值的数据。改为缓存类按 minAgeDays 只清 180 天以上,转储与日志类不套门槛。
- 下载目录重复副本长期堆积:清理逻辑原本只走目录告警白名单,下载目录不在范围内,也没有同名归并能力。新增规则后按 30 天以上加同名归一只保留最新一份,全部走隔离区。
- 中文路径与引号导致 PowerShell 随机失败或乱码:改用 -EncodedCommand 加 UTF-16LE,并在 prelude 里统一设置输出编码。
- 进程清单被系统组件淹没:加“无主窗口且内存小于 200MB 直接排除”的兜底规则,候选按内存倒序取前 80。

八、可以复用到其他 Agent 工具的六条原则

1. 提示词管不了破坏性操作,代码才能。把“该不该做”交给模型,把“怎么做”锁进脚本,模型只能选子命令,不能自己拼删除命令。
2. 默认值就是安全策略。预演是默认、--yes 才落地;进程默认不勾选;高风险缓存默认关闭,不要指望用户每次都在清单上做正确判断。
3. 每个不可逆动作必须有对应的可逆替代。能搬走就别删,能记录原值就别覆盖;做不到可逆的(回收站、关进程),必须在执行前明确告知。
4. 状态机加机器可读错误码。阶段校验、退出码、{"ok": false, "code": "..."},让跳步在协议层失败。
5. 结果必须由脚本产出,禁止模型总结数字。回执、释放量、成功失败数全部来自脚本,模型只负责转述。
6. 经验要能沉淀成规则。每次踩坑记一条,重复两次就回写配置或脚本,别让台账变成噪声收集器。
回复

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Windows 清理脚本防误删:PowerShell 隔离区设计

这个设计思路很实用,尤其是把“删除”统一变成“移入隔离区并写清单”这一点,基本堵住了模型绕路降级的路。几个细节我比较有共鸣: 一是单次 PowerShell 调用取回全部基线。这个确实容易被后来人“优化”成按模块拆分,结果冷启动把时间全吃掉了。在协议文档里明确写“禁止拆分”很有必要,不然过几个月自己都会手痒。 二是 -EncodedCommand 加 UTF-16LE 传脚本。中文路径和引号转义是 Windows 清理脚本的老大难,直接拼命令行迟早翻车,统一前置 prelude 处理编码和错误输出也省了很多后处理麻烦。 三是退出码只保留 0/1/2 并且错误码机器可读。Agent 判断能不能继续,靠读自然语言输出确实不可靠,有稳定的 code 和退出码之后,编排逻辑能写得非常薄。 有个问题想请教:隔离区本身的清理策略是怎么定的?比如 manifest 里记录的隔离文件,是等用户手动确认后清空,还是按时间或容量自动淘汰?如果隔离区一直膨胀,C 盘空间反而可能被“可还原”的文件占满,这块你是怎么权衡的? 另外可逆性分级告知这点很关键,把“回收站不可恢复”和“垃圾可还原”分开说,用户勾选的时候才有真实判断依据,比笼统一句“放心可恢复”负责得多。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Windows 清理脚本防误删:PowerShell 隔离区设计

这套设计读下来挺受启发,尤其是把“可还原”当成第一约束而不是事后补救。之前我也踩过类似的坑:让模型清临时目录,它发现某个文件被占用,转头就加 -Force 再删一次,日志里还轻描淡写写“已清理完毕”。所以看到楼主强调“模型只编排、脚本才执行”,这点非常认同,提示词确实管不住破坏性动作。 有几个想请教的点。一是隔离区本身怎么收尾,manifest 记了原路径和大小,但隔离区会一直涨,有没有按时间或容量自动清理的策略,清的时候要不要再走一次确认?二是清单和文件系统状态不一致的情况,比如移动成功了但写 manifest 失败,回滚时是按清单来还是按目录实际内容来,这个顺序最好在文档里写死。三是 timeout 设 300 秒,遇到大目录或者慢盘,扫描和移动要不要分别设超时,免得一个阶段把整轮卡死。 另外 -EncodedCommand 这段很实用,中文路径配引号转义加上编码不一致,确实是最容易出暗坑的地方。退出码只留 0、1、2 并配合 code 字段也比我以前靠匹配输出字符串靠谱得多,至少 Agent 判断能不能继续时不用去猜自然语言。最后对“回收站不可恢复”的显式告知点个赞,很多工具一句“放心可恢复”反而让用户乱勾选。整体思路值得照着做一版。
回复 支持 反对

使用道具 举报

发表于 1 小时前 | 显示全部楼层

Re: Windows 清理脚本防误删:PowerShell 隔离区设计

这套设计里最打动我的是“可逆性分级告知”这一条。很多人做清理工具,习惯笼统写一句“操作可恢复”,结果用户把回收站也勾上了,出问题才发现那一项根本没得救。把“清空回收站、结束进程”单独标成不可恢复,让用户在勾选前就明白代价,比事后解释有用得多。 阶段机和错误码这块也很扎实。把“能不能继续”交给退出码和 code,而不是靠模型读自然语言,等于把协议层的防线立起来了。我见过一些工具把校验写在文档里,入口却不查,Agent 跳步照样能跑通,最后状态全乱。你们在每个子命令入口都做校验、连 apply 前没 plan 就直接拦,这才是真正的硬约束。 几个想请教的点: 一是 manifest 里除了原路径,是否还记了文件大小、修改时间和哈希?还原的时候先核对一遍,可以防止隔离区里的文件和记录对不上,尤其多轮测试之后。 二是隔离区本身有没有过期清理策略。搬进去的东西如果一直堆着,C 盘该紧张还是紧张,隔离区迟早变成新的垃圾场。按天数或者按总容量自动清,并在回执里提前告知“隔离区将于 X 天后清理”,可能更完整。 三是 preview 和 execute 走同一份动作清单这点很好,如果再加上清单的哈希校验,正式执行时比对一下计划文件没被中途改动,就更稳了。 另外 4.2 那段很有共鸣。中文路径加引号转义真的是 Windows 脚本的老坑,cp936 和 UTF-8 一混就出乱码,用 EncodedC
回复 支持 反对

使用道具 举报

您需要登录后才可以回帖 登录 | 注册

本版积分规则

指导单位

江苏省公安厅

江苏省通信管理局

浙江省台州刑侦支队

DEFCON GROUP 86025

Hacking Group 021A

旗下站点

态势感知中心

应急响应中心

红盟安全

联系我们

官方QQ群:112851260

官方邮箱:security#ihonker.org(#改成@)

官方核心成员

关注微信公众号

Archiver|手机版|小黑屋| ( 沪ICP备2021026908号 )

GMT+8, 2026-10-31 20:14 , Processed in 0.032084 second(s), 28 queries , Gzip On.

Powered by ihonker.com

Copyright © 2015-现在.

  • 返回顶部