用Godot打造Windows C盘清理工具:安全扫描与回收站实现

发布时间:2026/9/4 18:37:39
用Godot打造Windows C盘清理工具:安全扫描与回收站实现 Windows 的 C 盘又红了剩余空间从 20 GB 变成 2 GB右下角的磁盘警告弹得比 IDE 报错还勤快。很多人的第一反应是装一个 CCleaner或者用各种“安全卫士”清一遍垃圾但我个人的建议是如果你的电脑需要频繁交付给客户、或者你在做系统运维类工作这类闭源清理工具反而值得警惕——它们为了商业目的会在规则和上报机制上做不少动作。那如果想要一个“看起来专业、逻辑完全可控、界面还不丑”的 C 盘清理工具该怎么做过去大家会想到 C# WinForms或 Python PyQt再或者是 Electron 套一层壳。这些方案都能做但各自都有很明显的痛点WinForms 界面老气PyQt 打包体积大、布局要写一堆样板代码Electron 更是动不动 200 MB。而 Godot 这个平时用来做游戏的开源引擎其实非常适合做这一类的系统工具型桌面应用。这篇文章不是标题党。我会围绕一个内部项目代号Godot-CleanScope讲清楚怎么用 Godot 4.x 的 UI 系统、DirectoryAccess 文件访问能力以及信号机制一步步搭建一个 Windows C 盘可清理目录扫描与清理工具。你会看到它绝不是“游戏引擎硬套系统工具”而是把 Godot 的节点体系和场景组织能力用在了正确的地方。如果你正准备做一款 Windows 效率工具或者对 Godot 的“非游戏用途”感兴趣这篇文章值得看到最后。我会把安全边界、线程模型、路径处理这些容易翻车的地方单独拿出来讲避免你照着网上一些游戏教程写出一个会在 C 盘里乱跑的“扫地机器人”。1. 这不是玩笑用 Godot 写 C 盘清理工具到底解决了什么问题先说重点Godot 做 C 盘清理工具核心价值不在“能不能读到文件”上——这一点几乎所有框架都能做到。真正带来改变的是它的 UI 生产效率和项目组织方式。一个传统的 Windows 清理工具需要处理这几层事情扫描目录、计算文件大小、按目录聚合展示、让用户勾选、再执行删除。放在 C# / WinForms 里你需要用 TreeView 和 ListView 去做很多细节适配放在 PyQt 里QTreeWidget 和信号槽虽然能写但额外还有打包、版本兼容、界面样式的问题放在 Electron 里Node.js 的 fs 模块本身很成熟可打包体积和内存占用会让用户质疑你“到底是在做清理工具还是制造更多垃圾”。Godot 的优势在于它把“界面”和“逻辑”放进了同一个运行时里。Control 节点的继承体系、自动布局容器、主题样式表是专门为高密度桌面界面设计的。你用四五个节点就能组合出一个干净的扫描结果页然后通过信号把按钮点击、勾选变化、进度更新连接起来。这比在传统 UI 框架里反复写控件事件绑定要高效得多。更关键的是Godot 导出的 Windows 程序基础体积只有几十到一百多 MB 量级比 Electron 小得多。对于 CleanScope 这种需要每天面对大量文件、可能要长期常驻后台的工具这点体积差异直接影响用户会不会愿意安装它。当然文章标题里的“C 盘清理工具”不能停留在概念层面。上帝视角看Windows 清理工具有一个隐藏问题它运行在操作系统之上却要删除操作系统认为“可以删除”的文件。这就涉及到非常多的权限边界、路径差异、文件占用情况。游戏引擎开发经验在这里反而是负资产因为游戏里访问资源通常用项目内 user:// 路径到了系统工具场景必须面对真实世界的 C:\Users、C:\Windows\Temp、C:\ProgramData 这些全局路径。所以我把整个项目的目标限定为一句话做一个“边界安全、表现清晰、用户可完全掌控”的 Windows C 盘可清理空间分析工具而不是一个默认勾选全盘、一键清理后系统崩溃的“暴力清理器”。理解了这一点后面的所有设计才不会跑偏。2. CleanScope 的设计目标与安全边界在写任何一行代码之前先讲 CleanScope 这个工具要遵守的底线。为什么要先讲安全因为网上能找到的很多 Godot 文件系统教程都是教你怎么读取项目文件、怎么做存档没有一篇会提醒你当你在 Windows 上递归遍历 C 盘时会遇到权限拒绝、符号链接、System Volume Information、页面文件等一大堆系统级对象。如果直接把“遍历目录”和“删除文件”粘在一起那不是在做一个清理工具而是在做一个“自己写的磁盘炸弹”。CleanScope 的安全边界我建议分成四条清理范围只覆盖用户级目录。优先处理%TEMP%、浏览器缓存目录、缩略图缓存、日志目录而不是一上来就扫C:\Windows。只扫描默认不删除。主界面第一个版本应该只呈现“发现哪些目录、占了多大空间”删除动作必须由用户手动勾选并二次确认。删除前先移入回收站。能使用系统回收站语义就不要直接物理删除文件。用户有后悔的机会代码也不需要处理复杂的数据恢复。不无限追深目录。扫描文件树务必有最大深度和超时机制避免遇到特殊目录时线程卡死或遍历时间过长。有读者可能会问那限制这么多这个工具有什么用答案是一个合格的清理工具一开始就应该做减法。Windows 自带的磁盘清理其实也并不是把所有可以删的东西都列给你它只列出安全分类项比如“临时文件”“缩略图”。CleanScope 定位是把它变成一个可深度可视化、可扩展自定义目录规则的现代化工具。下面我给出一个 CleanScope 的第一个可用版本扫描 Windows 用户目录中常见的可清理项、按目录展示大小、勾选后移入回收站。在这个版本里你完全可以做成“只读分析 确认后回收”的安全模式之后再加“一键清理”也不迟。3. 核心概念Godot 的路径体系与文件系统能力要用 Godot 写系统工具最需要先搞清楚的是 Godot 的路径体系。否则你很容易在res://、user://、C:\Users\...之间迷失。Godot 项目路径主要有三种路径前缀含义适合场景res://项目资源路径只读目录打包后对应游戏内部资源放代码、配置、静态资源user://用户数据路径Godot 自动映射到系统用户目录放配置、缓存、存档C:/、D:/等绝对路径操作系统真实路径系统工具必须使用的访问方式写 CleanScope 时我们大部分时间都在第三种路径上工作。Godot 的DirectoryAccess和FileAccess都支持绝对路径打开。也就是说如果你想扫描C:\Users\你的用户名\AppData\Local\Temp下面的内容并不需要借助 C# 或者外部命令行直接用 GDScript 就能访问。理解路径之后还要理解两个常用 API 的区别DirAccess.open(path)打开一个目录。如果路径不存在或没有权限返回null。DirAccess.remove_absolute(path)删除文件或空目录使用系统绝对路径。FileAccess.file_exists(path)判断文件是否存在。还有一个容易被忽略的关键点在 Windows 上路径分隔符不统一。Godot 内部推荐使用/作为路径分隔符Windows 也接受C:/Users/xxx这种写法。所以直接把C:\Users\xxx写进代码往往也能跑但如果你想减少转义和拼接问题最好在收到系统环境变量后统一把\替换成/。再讲一个很多人会踩的坑扫描目录时list_dir_begin()之后必须用list_dir_end()结束。如果漏掉了后续再次操作同一个DirAccess对象时可能得到错误结果。Godot 的底层实现里文件迭代器是带状态的一定要成对调用。最后像 CleanScope 这种工具Windows 上写绝对路径时要特别小心“当前工作目录”和“资源目录”的差异。直接用DirAccess.open(C:/Users/test/AppData/Local/Temp)最稳妥不要依赖OS.get_current_dir()去推算系统 Temp 位置更不要用相对路径猜测。4. 环境准备与项目初始化在开始写代码前先把环境准备好。我建议使用 Godot 4.x 的正式发布版本。虽然本文代码在 Godot 4.2/4.3 上都能工作但你下载时请以官网的正式版为准。安装完之后直接在 Windows 上打开 Godot新建一个空项目项目名称填写CleanScope。创建完成后修改项目下的project.godot。示例内容如下; 文件路径project.godot config_version5 [application] config/nameCleanScope run/main_sceneres://scenes/Main.tscn config/featuresPackedStringArray(4.2) config/iconres://icon.svg [display] window/size/viewport_width1024 window/size/viewport_height720 window/stretch/modecanvas_items window/stretch/aspectexpand [rendering] renderer/rendering_methodgl_compatibility renderer/rendering_method.mobilegl_compatibility这里有一个关键选项renderer/rendering_methodgl_compatibility。默认的 Forward Plus 渲染器对 3D 很好但做 2D 系统工具没必要承担额外开销而且 gl_compatibility 渲染器在低配电脑和虚拟机里兼容性更好。工具类应用应该追求启动速度和低资源占用而不是画面特效。然后创建目录结构CleanScope/ ├── project.godot ├── icon.svg ├── scenes/ │ └── Main.tscn ├── scripts/ │ ├── main.gd │ ├── scanner.gd │ └── trash_manager.gd └── data/ └── scan_targets.jsonscenes/Main.tscn是应用的主场景scripts/main.gd负责主逻辑scanner.gd负责扫描文件树trash_manager.gd负责回收站删除封装data/scan_targets.json用于配置扫描目录。这种分层的好处是即使后面要把扫描逻辑换成 C# 或者 Rust 动态库界面层也不需要大幅度改动。5. 扫描目标配置从 JSON 中读取要扫描的目录清理工具不能把目录写死在代码里。否则每次新增一个可清理项都要重新编译一次这不适合工具类项目的快速迭代。更好的方式是使用一个 JSON 配置把扫描目标和显示名称分开。在data/scan_targets.json中放入如下内容{ targets: [ { id: temp_dir, name: 系统临时目录, path_pattern: TEMP, max_depth: 6 }, { id: crash_dumps, name: Windows 错误转储, path_pattern: LOCALAPPDATA/CrashDumps, max_depth: 5 }, { id: thumbnail_cache, name: 缩略图缓存, path_pattern: LOCALAPPDATA/Microsoft/Windows/Explorer, max_depth: 3 } ] }这里的path_pattern是环境变量拼接的语法。使用系统环境变量而不是写死绝对路径能提高兼容性。不同 Windows 用户的用户名不一样写死C:\Users\zhangsan会让工具变成一次性脚本。读取该 JSON 并解析成目标列表的代码如下# 文件路径scripts/target_loader.gd class_name TargetLoader extends RefCounted static func load_targets() - Array[Dictionary]: var json_path : res://data/scan_targets.json if not FileAccess.file_exists(json_path): push_error(cannot find scan_targets.json) return [] var file : FileAccess.open(json_path, FileAccess.READ) if file null: push_error(cannot open scan_targets.json) return [] var text : file.get_as_text() file.close() var json : JSON.new() var error : json.parse(text) if error ! OK: push_error(parse scan_targets.json failed: , json.get_error_message()) return [] var data: Dictionary json.data var result: Array[Dictionary] [] for target in data.get(targets, []): var dict: Dictionary target # 将环境变量模板展开成真实路径 var real_path : expand_env(dict.get(path_pattern, )) dict[real_path] real_path result.append(dict) return result static func expand_env(pattern: String) - String: var result : pattern # 这里处理 LOCALAPPDATA/TEMP 这类通配符 if result.begins_with(TEMP): var temp : OS.get_environment(TEMP) result result.replace(TEMP, temp) elif result.begins_with(LOCALAPPDATA): var local : OS.get_environment(LOCALAPPDATA) result result.replace(LOCALAPPDATA, local) # 统一替换为 Godot 更友好的斜杠 return result.replace(\\, /)这段代码解决的问题是把“要清理什么目录”从代码里剥离出去。后续如果需要支持用户自定义目录只要修改 JSON 配置文件或者在界面里增加写入配置的能力即可。要提醒一下OS.get_environment(TEMP)在 Windows 下返回的路径经常带有末尾反斜杠比如C:\Users\xxx\AppData\Local\Temp\所以统一替换斜杠和去除末尾分隔符很重要。实际项目中我会再写一个normalize_path()函数用来去掉末尾/并处理重复斜杠避免后面拼接路径时出现//。6. 扫描器实现递归遍历目录并计算大小扫描器是 CleanScope 的核心模块。它的任务很简单给定一个根目录递归遍历其子目录和文件统计整个目录占用大小并把文件数量、最大文件路径等信息返回给上层。这里有一个工程上的选择要不要在 Godot 的Thread里执行扫描答案是必须。如果你在 UI 线程里递归遍历一个几百 GB 的目录界面会直接卡死用户会以为程序崩溃了。Godot 4 中创建一个线程并不复杂但要记住线程里不能直接操作场景树节点必须使用call_deferred把结果安全地传回主线程。下面是扫描器代码# 文件路径scripts/scanner.gd class_name Scanner extends RefCounted var _max_depth: int 6 var _cancel: bool false func setup(max_depth: int) - void: _max_depth max_depth func cancel() - void: _cancel true func scan_directory(root_path: String) - Dictionary: var result : { path: root_path, total_bytes: 0, file_count: 0, error_skipped: 0 } _scan_recursive(root_path, 0, result) return result func _scan_recursive(current_path: String, depth: int, result: Dictionary) - void: if _cancel: return if depth _max_depth: return var dir : DirAccess.open(current_path) if dir null: result[error_skipped] int(result[error_skipped]) 1 return var list_error : dir.list_dir_begin() if list_error ! OK: result[error_skipped] int(result[error_skipped]) 1 return while true: var entry_name : dir.get_next() if entry_name : break # 跳过当前目录、上级目录和隐藏的开头点文件 if entry_name . or entry_name ..: continue if entry_name.begins_with(.): continue var full_path : current_path.path_join(entry_name) if dir.current_is_dir(): _scan_recursive(full_path, depth 1, result) else: var file_size : dir.get_file_size() result[total_bytes] int(result[total_bytes]) file_size result[file_count] int(result[file_count]) 1 dir.list_dir_end()注意几个细节。第一dir.current_is_dir()判断的是刚才get_next()返回的那一项是不是目录。这一行必须放在get_next()之后立即使用不能在中间穿插其他目录操作否则状态会错乱。第二dir.get_file_size()只有在当前项是文件时才有意义。如果是目录得到的值不可信所以要先判断current_is_dir()。第三打开目录失败时不能直接抛弃要记录到error_skipped。Windows 的很多目录在普通用户权限下访问会返回无权限比如C:\System Volume Information。如果忘记记录错误用户很难发现扫描结果其实少了大量文件。第四path_join()是 Godot 4 中非常方便的路径拼接方法。它不会像手写path / filename那样因为多一个或少一个斜杠而出 bug。接下来提供一个配套的线程调度示例# 文件路径scripts/main.gd 中的线程调度片段 extends Control var _scanner: Scanner var _thread: Thread var _result_by_path: Dictionary {} func start_scan(root_paths: Array[String], max_depth: int 6) - void: _cancel_exist_thread_if_needed() _scanner Scanner.new() _scanner.setup(max_depth) _thread Thread.new() _thread.start(_thread_worker.bind(root_paths)) func _thread_worker(paths: Array[String]) - void: var results: Dictionary {} for path in paths: results[path] _scanner.scan_directory(path) call_deferred(_on_scan_thread_finished, results) func _on_scan_thread_finished(results: Dictionary) - void: _result_by_path results # 在这里刷新 UI print(scan finished, targets: , results.size()) func _exit_tree() - void: if _thread ! null: _thread.wait_to_finish()这里要注意的是Thread.wait_to_finish()。如果场景退出时线程还在跑直接关闭程序会导致崩溃。Godot 的线程不是“随进程自动清理”的托管线程你必须负责等待它结束。所以在_exit_tree()里写wait_to_finish()是个必要的兜底。7. 界面实现用 Godot 场景树搭建 CleanScope 主窗口扫描逻辑做完后开始搭界面。CleanScope 的界面布局如下顶部标题 扫描按钮 刷新按钮。中间区域左侧是一个扫描目标列表也就是我们在 JSON 里配置的那几个可清理项右侧是一个详情信息面板显示当前目录的文件数、占用大小、最近错误。底部一个“移入回收站”按钮和一个“重新扫描”按钮。用 Godot 的 Control 节点实现起来很直接。下面是scenes/Main.tscn的骨架你也可以在编辑器中手动搭建原理等价Main (Control) ├── VBoxContainer │ ├── HeaderContainer (HBoxContainer) │ │ ├── TitleLabel (Label) │ │ ├── ScanButton (Button) │ │ └── RefreshButton (Button) │ ├── ContentContainer (HSplitContainer) │ │ ├── TargetList (ItemList) │ │ └── DetailScroll (ScrollContainer) │ │ └── DetailLabel (RichTextLabel) │ └── FooterContainer (HBoxContainer) │ ├── ClearButton (Button) │ ├── ProgressBar │ └── StatusLabel (Label)为了让 GDScript 更易读我在主脚本中先把这些节点通过$路径拿下来。这里的关键是信号连接当用户点击ScanButton时触发扫描当扫描结束_on_scan_thread_finished会被call_deferred调用当用户点击ClearButton时需要先判断是否有选中目录再弹一个确认框。主逻辑代码片段如下# 文件路径scripts/main.gd节选 extends Control onready var target_list: ItemList $VBoxContainer/ContentContainer/TargetList onready var detail_label: RichTextLabel $VBoxContainer/ContentContainer/DetailScroll/DetailLabel onready var scan_button: Button $VBoxContainer/HeaderContainer/ScanButton onready var clear_button: Button $VBoxContainer/FooterContainer/ClearButton onready var progress_bar: ProgressBar $VBoxContainer/FooterContainer/ProgressBar onready var status_label: Label $VBoxContainer/FooterContainer/StatusLabel var _targets: Array[Dictionary] [] var _path_results: Dictionary {} func _ready() - void: scan_button.pressed.connect(_on_scan_pressed) clear_button.pressed.connect(_on_clear_pressed) target_list.item_selected.connect(_on_target_selected) _targets TargetLoader.load_targets() for target in _targets: target_list.add_item(target.get(name, 未知项)) func _on_scan_pressed() - void: scan_button.disabled true status_label.text 正在扫描... var paths : _targets.map(func(t): return String(t.get(real_path, ))) start_scan(paths) func _on_scan_thread_finished(results: Dictionary) - void: _path_results results scan_button.disabled false status_label.text 扫描完成 var total_bytes : 0 for key in results: var res: Dictionary results[key] total_bytes int(res.get(total_bytes, 0)) progress_bar.max_value 100 progress_bar.value 100 status_label.text 总共可清理 %s % _format_bytes(total_bytes) func _on_target_selected(index: int) - void: if index 0 or index _targets.size(): return var path: String _targets[index].get(real_path, ) var res: Dictionary _path_results.get(path, {}) detail_label.clear() detail_label.append_text(路径%s\n % path) detail_label.append_text(占用空间%s\n % _format_bytes(int(res.get(total_bytes, 0)))) detail_label.append_text(文件数%s\n % str(res.get(file_count, 0))) detail_label.append_text(跳过错误%s % str(res.get(error_skipped, 0))) func _format_bytes(size: int) - String: if size 1024 * 1024 * 1024: return %.2f GB % (float(size) / (1024 * 1024 * 1024)) if size 1024 * 1024: return %.2f MB % (float(size) / (1024 * 1024)) if size 1024: return %.2f KB % (float(size) / 1024) return %d B % size这个界面虽然简单但已经足够完成一次“选目标目录 - 扫描并汇总大小 - 点击查看详情”的完整流程。下一步我们要考虑真正把文件移入回收站时应该怎么处理。8. 清理动作与验证安全删除不能只调用一个函数比较谨慎的删除策略是不对整个目录递归删除而是先扫描出目录里的文件列表然后用户确认后再对文件列表逐条执行OS.move_to_trash()。Godot 4.x 在 Windows 平台提供了OS.move_to_trash(path)方法作用是把这个文件挪到回收站而不是永久删除。下面是一个回收站管理器的代码示例# 文件路径scripts/trash_manager.gd class_name TrashManager extends RefCounted # 返回四个值成功数量、失败数量、失败文件列表 func move_files_to_trash(file_paths: Array[String]) - Dictionary: var result : { succeed_count: 0, fail_count: 0, fail_paths: [] } for path in file_paths: var normalized : path.replace(\\, /) var err : OS.move_to_trash(normalized) if err OK: result[succeed_count] int(result[succeed_count]) 1 else: result[fail_count] int(result[fail_count]) 1 result[fail_paths].append(path) return result为什么我不直接递归删除整个目录原因有两个。第一目录里很可能有大量文件正被系统或其他进程占用。Windows 上如果一个文件被某个进程以独占方式打开你执行删除操作一定会得到“访问被拒绝”。如果直接对目录做递归删除很可能删到一半报错导致目录处于半删半不删的损坏状态。逐文件执行的好处是你能把失败文件记录下来让用户知道哪些文件暂时不能动。第二永久删除是不可恢复的。把文件先移入回收站即使误操作用户还能从回收站恢复这是清理工具的基本道德。在实现清理按钮时还需要确认用户已经勾选了要清理的目录。这里我给一个更严格的交互方式点击清理按钮后不要立即执行而是扫描一次当前选中目录生成文件清单然后弹出一个ConfirmationDialog显示“即将把目录下的 N 个文件移入回收站总计大小 XXX是否继续”。对应代码片段# 文件路径scripts/main.gd节选 func _on_clear_pressed() - void: var selected_indexes : target_list.get_selected_items() if selected_indexes.is_empty(): status_label.text 请先选择一个扫描项 return var path : String(_targets[selected_indexes[0]].get(real_path, )) var res: Dictionary _path_results.get(path, {}) var file_count : int(res.get(file_count, 0)) if file_count 0: status_label.text 该目录没有可清理文件 return var dialog : ConfirmationDialog.new() dialog.dialog_text 确定将目录中的 %d 个文件移入回收站吗 % file_count dialog.ok_button_text 移入回收站 dialog.cancel_button_text 取消 add_child(dialog) dialog.confirmed.connect(func(): _execute_clean_for_path(path) ) dialog.popup_centered()这里使用了一个ConfirmationDialog和confirmed信号。再次强调清理工具的用户交互要“宁可多一次确认也不能少”。清理结束后需要做结果验证。验证方式不是“弹个提示就完事”而是再次扫描该目录对比清理前后的文件数和占用大小。如果两次结果差异不大大概率说明文件仍然被占用或目录本身有大量系统文件需要在界面提示用户用管理员权限、重新启动或手动检查。9. 常见问题与排查思路在开发 CleanScope 时你会遇到不少和普通 Godot 游戏开发完全不同的问题。下面把最典型的整理成表格。问题现象可能原因排查方式解决方案扫描结果一直是 0目标目录路径解析错误TEMP环境变量没有展开打印real_path字段使用统一的expand_env函数并检查返回路径扫描速度极慢或界面卡死在主线程执行了递归扫描检查扫描工作是否在Thread中完成将所有文件遍历逻辑放入独立线程用call_deferred回传结果扫描到一半程序闪退线程结束时场景已经销毁或没有调用wait_to_finish查看 Godot 控制台崩溃栈在_exit_tree中等待线程结束删除文件返回失败报“访问被拒绝”文件被其他进程占用或权限不足检查路径文件是否正被打开跳过该文件并记录失败清单提示用户稍后重试遍历到某些目录后无法继续目录没有访问权限例如System Volume Information查看error_skipped统计跳过并增加错误计数不要中断整个扫描路径包含中文或空格导致解析异常拼接路径时用了错误的分隔符打印最终目录访问路径使用path_join()或统一规范化路径移入回收站失败路径含有 Windows 反斜杠或当前平台不支持检查OS.move_to_trash返回值先替换为/路径再调用对失败列表单独处理有人会在扫描 Windows 系统目录时发现很多“无法访问”的文件夹然后想着提升到管理员权限去访问。这里建议谨慎普通清理工具完全不需要访问C:\Windows\System32\config这类目录。如果某个目录总是报权限错误那么大概率它本来就不属于应该交给普通清理工具的范畴。用户自定义清理目录时也请默认用普通权限运行禁用管理员权限反而更容易防呆。10. 从原型到产品打包发布与后续迭代方向CleanScope 开发到当前阶段已经具备了“规则化扫描目标 后台线程遍历 结果列表展示 回收站式清理 失败清单”五大部分。如果想让这个工具真正进入实用阶段还有几个方向可以继续推进。首先是打包发布。Godot 的导出工具支持 Windows Desktop 平台。导出时建议选择“可执行文件 pck 包”模式并添加版本信息、产品名称、图标。不要把 pck 资源包和 exe 混在一起后不设签名——如果工具要公开分发最好做代码签名否则 Windows SmartScreen 会给出大段警告。其次是增加白名单与黑名单机制。在扫描结果中经常能看到用户自己不愿清理的目录比如某个项目里刚好有一个文件被 Watchdog 监听。CleanScope 需要在配置里加入以下两个字段{ id: my_custom, name: 自定义目录, path_pattern: USERPROFILE/Downloads/temp_download, max_depth: 3, exclude_patterns: [*.lock, node_modules] }第三是考虑使用 Godot 的 C# 版本来做更底层的 Windows API 调用。如果你想在工具里访问Windows.Storage.ApplicationData、查询卷影副本或者获取更精确的 NTFS 文件占用GDScript 可能不是最佳方案。Godot 4 支持 C# 脚本可以直接调用 .NET 的System.IO和Microsoft.Win32命名空间做一个负责平台能力的模块再让 GDScript 层负责 UI 展示两者之间通过信号通信是一种可扩展的架构。第四是引入错误日志和上报机制。CleanScope 在用户机器上扫描时经常会遇到各种想不到的目录、权限和文件占用问题。建议使用push_warning或简单文件日志把扫描失败路径、删除失败原因记录到user://logs/clean_scope.log。没有日志用户报 bug 时你根本没法排查。第五不要把“删除能力”做得太强。即使是在自己的工具里也最好默认使用回收站删除并把“永久物理删除”做成需要额外设置项并且默认关闭的高级功能。这个设计表面上限制了产品能力实际上是对用户负责。我见过不少开源清理工具就是因为作者贪图“彻底清理”而设计了递归物理删除最终把普通用户的重要文件删掉后口碑崩坏。最后建议先用一个最小版本跑通扫描和展示不要急着做一键清理。扫描能力本身已经能解决大量“C 盘空间去哪了”的问题。当你把“找到空间”这一步做得足够清晰、足够快、足够安全时清理动作自然会是水到渠成的下一步。