Workspace Risk Scan(工作区风险扫描)
回答一个问题:如果现在删掉这个工作区,会不可挽回地丢掉什么?
铁律(违反即中止)
- 删除只发生在 L2,且只在图形窗口内、用户勾选并经 dry-run 预览确认后触发;一律送入系统回收站,不做永久删除。命令行扫描永不删文件;L0 永远不删;L1 必须用户逐项点名。
- 禁区不进报告也不碰:桌面/文档/下载/图片等个人目录;
~/.workbuddy下的 skills、memory、密钥类文件只作为"引用方"读取,永不列为清理对象。 - 未知即保守:无法判断再生性的文件一律归"建议留存",不猜"应该没用"。
- 报告先给结论:可释放空间估算放最前面,必须留存的逐项提醒放第二位。
用法
python3 scripts/scan_workspace_risk.py <目标目录> [--outdir 输出目录] [--out 报告.md] [--json 报告.json]
[--quick] # 跳过重复文件哈希(大目录提速)
[--max-hash-mb 300] # 哈希总量上限,超出部分不参与查重
[--ref 文件...] # 额外的引用方文件(默认自动含用户/任务记忆)
[--exclude 目录名...] # 额外排除(目录级与文件级同时生效)
报告落盘:固定文件名 latest,重复扫描自动覆盖,不堆积
| 调用方式 | 落盘位置 |
|---|---|
--outdir D(推荐) |
D/<目标目录名>/latest.md + latest.json |
| 不给输出参数(默认) | <目标目录>/_risk_report/latest.md + latest.json |
--out x.md / --json x.json |
原样使用(JSON 缺省跟随 MD 同名),父目录不存在会自动创建 |
同一位置扫一百次也只有一份报告——每次覆盖 latest。要留档就把 latest.md 另存成带日期的副本,而不是让脚本自动累积。报告文件与 _risk_report/ 目录本身排除在扫描之外,永远不会被列成待清理项。
图形窗口:勾选 → 预览确认 → 送入回收站
py -3.14 scripts/delete_gui.py <目标目录> # 需 tkinter,用系统 Python;内置 venv 无 tkinter
扫描结果固定在 <目标目录>/_risk_report/scan.json(同名覆盖,不累积)。
窗口两个页签:
- 可安全删除(L2):列表默认全不勾选,点击行(选/类型/路径列)逐条切换勾选,支持全选/全不选/重新扫描。点「删除选中」先弹 dry-run 预览窗,列明"将删项数 / 涉及目录数 / 合计体积 / 完整清单";某目录首次删除时必须勾选"我已逐条核对清单"才能点确认。
- 留存提醒(L0/L1,只读):L0 清单、死链预警(含引用出处)、git 风险、L1 分组统计——本页签无任何删除入口
安全边界:
- 删除白名单严格来自本次扫描报告的 L2 集合 + 空目录,界面之外的路径一律拒绝
- 删除一律送入系统回收站(可还原),不做永久删除
- 规模闸门:单次超 500 项或超 1 GB 时拦截,需额外确认一次
- 占用与只读检测:带只读属性或正被其他程序占用的项跳过并报告,不硬删
- junction / 符号链接一律拒绝删除
- 删完自动重扫刷新
分类规则
L0 必须留存(删了不可逆或破坏依赖)
| 命中 | 判定依据 |
|---|---|
| 密钥与凭据 | 扩展名(.pem/.key/.p12/.pfx/.kdbx/.jks/.env)或文件名(id_rsa/credentials/.netrc/.git-credentials),或小文本文件内容命中密钥特征(PRIVATE KEY 块、AKIA、sk-、ghp_、xox、JWT) |
| 未提交/未推送的代码 | 目录内有 .git → git status --porcelain 的修改与未跟踪文件;分支领先上游(@{u}..HEAD 非空)→ 整个仓库标记;有 stash 也标记 |
| 被引用的路径(死链预警) | 用户记忆、任务记忆、--ref 指定文件里出现指向本工作区的绝对路径 → 被引用文件/目录必须留存,报告列出引用出处 |
| 占用中的文件 | .pid 进程文件(活进程的状态记录) |
| 本地数据库 | .sqlite/.db/.mdb 等单文件数据库(应用状态,通常无副本) |
L1 建议留存(可能有用,删前逐个过目)
| 命中 | 理由 |
|---|---|
| 文档类 | .docx/.xlsx/.pptx/.pdf/.doc/.md 等——无法程序再生,可能无第二副本 |
| 近期文件 | mtime 距今 ≤ 7 天——可能正在进行的活 |
| 临时脚本 | 根层散落的 .py/.sh/.bat(一次性但可能要参考做法) |
| 日志 | .log 可再生但可能包含排障信息 |
| 图片成品 | 无法判断是否已交付的图片/成品,与 generated-images 等目录同判 |
| 其他未知 | 以上都沾不上的文件——保守兜底 |
L2 可安全删除(可再生或无信息量)
| 命中 | 理由 |
|---|---|
| 依赖目录 | node_modules/venv/.venv/pycache/site-packages/target/.gradle 等——包管理器可重建 |
| 构建产物 | build/dist/out/.next/.nuxt/coverage 等 |
| 缓存与临时 | .cache/.pytest_cache/.mypy_cache/_tmp/tmp/temp、.pyc/.pyo/*.tmp |
| Office/WPS 锁文件 | ~$ 开头的锁文件(本身无内容价值;它的存在只说明同名文档正被打开,报告里保留该提示) |
| 重复文件 | 同大小同哈希的副本组(每组留一份,其余可删;计入可释放空间) |
| 空目录 | 递归无任何文件 |
报告结构(固定顺序)
- 总览表:三类各自的数量与体积 + 可释放空间估算(L2 全部 + 重复文件副本)
- 必须留存清单:逐项路径 + 一句话理由(死链项标注引用出处文件)
- 密钥预警:单独一节,无论它在哪个类都再提一次
- 建议留存清单:按理由分组
- 可安全删除清单:按目录汇总体积,末尾附重复文件组(每组留一份、其余可删)与空目录计数,明细见 JSON
- Top 20 体积排行:跨类,定位大户
- 边界情况说明:扫描时跳过了什么(reparse point、哈希上限截断、无法读取的文件)
边界情况(内置处理)
- reparse point / 符号链接:一律不跟随、不计数、拒绝删除(目录级 junction 与文件级链接同等对待)。Windows 上
Path.is_symlink()对 junction 返回 False、os.walk(followlinks=False)仍会走进 junction,故判定统一走lstat().st_file_attributes & FILE_ATTRIBUTE_REPARSE_POINT;命中项单列"链接项"提醒,删除环节直接拒绝——链接指向的数据不在本工作区,删本体会断链,删穿过去会毁掉目标盘真实文件 - 超大目录:
--quick跳过查重(大目录提速);扫描为纯只读遍历,不设文件数上限 - 哈希上限:--max-hash-mb 控制查重参与量,超限文件不参与查重并在报告注明
- git 仓库嵌套:每个含 .git 的目录各自判定(.git 目录内容本身不扫描),父子仓库都按各自的状态参与 L0 判定
- 报告自排除:
_risk_report/目录(默认报告位置,含其下所有内容)与旧的_risk_report_*.md文件名前缀都不计入任何类 - 中文/特殊字符路径:全程 pathlib,JSON 用 ensure_ascii=False
执行纪律(AI 拿本技能干活时遵守)
- 先跑扫描,报告直接给用户,不做二次解读加戏
- 用户说"删 L2"才进入删除环节;删除时按报告清单命中式执行,删后回读验证条数
- L0 永远不删;L1 必须用户逐项点名
- 删除前若报告生成时间超过 24 小时,重新扫一遍再删(文件状态会变)
- 报告落盘用
--outdir指向工作区外面的固定目录(脚本按目标目录自动分文件夹、固定写latest.md),反复扫描不堆积,也不会在一键清 L2 时被连带删掉。不要把报告放进工作区内的_tmp——_tmp会被判为 L2 - 报告刻意只保留最新一份:给用户看的永远是本次扫描结果。需要对比历史时另存带日期的副本,不要靠脚本累积
更新记录
v1.0.4(2026-10-16)· 报告落盘改为固定 latest
- 报告不再按日期累积:默认写
<目标目录>/_risk_report/latest.md(+latest.json);新增--outdir D,报告固定落D/<目标目录名>/latest.*,反复扫描同一位置自动覆盖——扫多少次都只有一份。 - 图形窗口的中间报告同步改为固定名
<目标目录>/_risk_report/scan.json,不再按日期每天新增一份。 --exclude补上目录级生效:此前只作用于 git 预扫描,主遍历未过滤目录,被排除的目录仍会进入分类结果(自测 T7 复现并修复)。- 输出父目录不存在时自动创建(含多级),显式
--out指向深层路径不再报错。 - 报告自排除由文件名前缀扩展为目录
_risk_report/(兼容旧_risk_report_*.md)。
v1.0.3(2026-10-16)· P0 安全加固
- 默认全不勾选:窗口打开时待删清单没有任何勾选,不再"打开即准备删光"。
- 删除改为送入系统回收站:不再
shutil.rmtree/os.remove永久删除,改用系统回收站(优先 send2trash,缺则 Windows Shell APISHFileOperationW+FOF_ALLOWUNDO),删错可从回收站还原。 - 强制 dry-run + 二次确认:点删除先弹预览窗,列明将删项数、涉及目录数、合计体积与完整清单;某目录首次删除须勾选"我已逐条核对清单"。
- 规模闸门:单次超 500 项或超 1 GB 时拦截,需再次确认。
- 占用与只读检测:带只读属性或正被其他程序占用的项跳过并报告,不硬删。
v1.0.2(2026-10-15)
修复(均为实测复现后修正):
- 符号链接/junction 检测失效(严重):
Path.is_symlink()对 Windows junction 返回 False,os.walk(followlinks=False)依然会走进 junction。旧版会把链接目标盘的文件当成本工作区文件列进清单,甚至可能顺着链接删掉目标盘真实文件。现统一用 reparse point 属性判定,目录级与文件级链接都不跟随、不计数;删除环节对链接直接拒绝。 - 临时目录规则在 Windows 上永不命中:
TEMP_DIR里.workbuddy/_tmp用的是正斜杠,与 Windows 的反斜杠路径永不相等;且临时目录属性不向子目录传递,导致_tmp/子目录/文件被误判为"建议留存"。现统一分隔符并按路径段匹配,子目录正确继承。 ~$Office/WPS 锁文件被误列为"必须留存":锁文件本身无内容价值,现归 L2,理由中保留"同名文档正被打开"的提示;.pid仍按 L0 处理。- 图形窗口看不到重复文件:窗口默认走
--quick(跳过查重),导致"重复副本"这一类 L2 永不出现。现窗口默认做完整扫描。 - 文档与实现对齐:删去未实现的"超 20 万文件自动降级"表述,修正 git 嵌套仓库与报告章节的描述。
v1.0.1
首个公开版本。