手握256G的M1 MacBook Air,我决定自己造一个清理工具

  • King
  • 发布于 1天前
  • 阅读 28

AI时代,没有什么是一个提示词解决不了的。故事的开始事情是这样开始的。我的MacBookAirM1,256GB固态硬盘,8GB内存。Apple官网写着"轻、快、薄",我信了。五年后的今天,当我试图用Xcode更新一个项目时,系统弹出了这个提示:"您的启动磁盘几乎已满。"我陷入

AI时代,没有什么是一个提示词解决不了的。

故事的开始

事情是这样开始的。

我的MacBook Air M1,256GB固态硬盘,8GB内存。Apple官网写着"轻、快、薄",我信了。

五年后的今天,当我试图用Xcode更新一个项目时,系统弹出了这个提示:

"您的启动磁盘几乎已满。"

我陷入了沉默。

打开"关于本机"→"储存空间",那根橙色条几乎要吞噬整个进度条。我开始审视我的磁盘:

项目 占用空间 能删吗
Xcode 12GB 不敢删,删了等于重装系统
应用程序 60GB 删了会死
macOS 30GB 删了会死
系统数据 103GB 不敢删,鬼知道是什么
其他 40G 鬼知道是什么

40GB的"其他",像个黑洞。


市面上的选择,我挨个试了一遍

我决定花钱买解决方案。

打开App Store,搜索"清理""Clean",出来的结果让我眼花缭乱:

CleanMyMac X

  • 价格:$320/年(注意是刀)
  • 功能:全方位系统清理
  • 我的反应:太贵了,而且我只是想看看那40GB"其他"是什么

DaisyDisk

  • 价格:¥68一次性
  • 功能:可视化磁盘空间
  • 我的反应:看起来还行,但好像只是"看",不能"删"

Lemon(腾讯柠檬)

  • 价格:免费
  • 功能:国产清理工具
  • 我的反应:广告太多,界面像在逛菜市场

Onyx

  • 价格:免费
  • 功能:macOS优化工具
  • 我的反应:全英文,界面复杂,怕删坏系统

我陷入了一个困境:

  • 免费的功能不够
  • 付费的肉疼
  • 好像没有一个工具能告诉我:"这40GB'其他'到底是什么,能不能删"

一个大胆的想法

作为一个写了资深技术的开发者,那天晚上我失眠了。

我翻来覆去在想一件事:

我每天写代码、写脚本、做自动化,为什么要用商业软件来清理自己的电脑?

2026年了,AI都这么强了。

Cursor/Kiro/Copilot可以帮我写代码,ChatGPT可以帮我设计架构。

那为什么不能让AI帮我造一个清理工具?


AI时代:我的开发工作流

第一步:需求定义(10分钟)

我打开ChatGPT,给它投喂了一个提示词:

我想要一个macOS清理工具,需要:
1. 实时显示磁盘使用情况
2. 扫描大文件和占用空间最多的文件夹
3. 识别可清理的缓存、日志、重复文件
4. 安全删除功能(预览后确认)
5. 现代化UI,类似CleanMyMac X的界面风格
6. 技术栈:Tauri + React + Rust

请帮我设计架构,给出核心代码示例。

10分钟后,我得到了一份完整的技术方案。

第二步:项目初始化(5分钟)

AI告诉我,Tauri是最佳选择:

"相比Electron,Tauri打包体积小(<10MB vs >100MB),内存占用少,非常适合资源紧张的256GB用户。"

# 初始化Tauri项目
pnpm create tauri-app
# 选择:React + TypeScript + Rust

# 进入目录
cd bloat-sweep

# 安装依赖
pnpm install
cargo install tauri-cli

第三步:核心功能实现

3.1 获取磁盘使用情况

这个问题我扔给AI:

"如何在Rust中获取macOS的磁盘使用情况?需要获取总空间、已用空间、可用空间。"

AI给出了代码:

use std::mem;

pub struct DiskUsage {
    pub total: u64,
    pub available: u64,
    pub used: u64,
}

pub fn get_disk_usage(path: &str) -> Result&lt;DiskUsage, std::io::Error> {
    let mut fs: libc::statfs = unsafe { mem::zeroed() };
    let c_path = std::ffi::CString::new(path)?;

    unsafe {
        if libc::statfs(c_path.as_ptr(), &mut fs) == 0 {
            Ok(DiskUsage {
                total: (fs.f_blocks * fs.f_bsize) as u64,
                available: (fs.f_bavail * fs.f_bsize) as u64,
                used: ((fs.f_blocks - fs.f_bfree) * fs.f_bsize) as u64,
            })
        } else {
            Err(std::io::Error::last_os_error())
        }
    }
}

3.2 递归扫描目录文件

"那扫描大文件呢?我想找出超过100MB的文件。"

use std::path::Path;
use std::fs;

#[derive(Debug)]
pub struct FileInfo {
    pub path: std::path::PathBuf,
    pub size: u64,
    pub modified: std::time::SystemTime,
}

const SIZE_THRESHOLD: u64 = 100 * 1024 * 1024; // 100MB

pub fn scan_large_files(root: &Path) -> Result&lt;Vec&lt;FileInfo>, std::io::Error> {
    let mut large_files = Vec::new();

    walk_dir(root, &mut large_files)?;
    Ok(large_files)
}

fn walk_dir(dir: &Path, results: &mut Vec&lt;FileInfo>) -> Result&lt;(), std::io::Error> {
    if let Ok(entries) = fs::read_dir(dir) {
        for entry in entries.flatten() {
            let path = entry.path();

            if path.is_file() {
                if let Ok(metadata) = path.metadata() {
                    if metadata.len() > SIZE_THRESHOLD {
                        if let Ok(modified) = metadata.modified() {
                            results.push(FileInfo {
                                path: path.clone(),
                                size: metadata.len(),
                                modified,
                            });
                        }
                    }
                }
            } else if path.is_dir() {
                // 跳过系统目录和隐藏目录
                if !path.to_string_lossy().starts_with(".") 
                    && !is_system_directory(&path) {
                    walk_dir(&path, results)?;
                }
            }
        }
    }
    Ok(results)
}

fn is_system_directory(path: &Path) -> bool {
    let forbidden = ["/System", "/Library", "/private"];
    forbidden.iter().any(|p| path.to_string_lossy().starts_with(p))
}

3.3 前端UI:用React呈现数据

// src/components/DiskUsage.tsx
import { useEffect, useState } from 'react';
import { invoke } from '@tauri-apps/api/tauri';

interface DiskUsage {
  total: number;
  available: number;
  used: number;
}

export function DiskUsage() {
  const [usage, setUsage] = useState&lt;DiskUsage | null>(null);
  const [loading, setLoading] = useState(true);

  useEffect(() => {
    // 调用Rust后端获取磁盘数据
    invoke&lt;DiskUsage>('get_disk_usage', { path: '/' })
      .then(data => {
        setUsage(data);
        setLoading(false);
      })
      .catch(err => console.error('获取磁盘信息失败:', err));
  }, []);

  if (loading) return &lt;div className="animate-pulse">正在分析磁盘...&lt;/div>;

  const percentage = ((usage!.used / usage!.total) * 100).toFixed(1);

  return (
    &lt;div className="bg-white rounded-xl shadow-lg p-6">
      &lt;h2 className="text-xl font-bold mb-4">磁盘使用情况&lt;/h2>

      {/* 进度条 */}
      &lt;div className="w-full bg-gray-200 rounded-full h-4 mb-4">
        &lt;div 
          className="bg-blue-600 h-4 rounded-full transition-all duration-500"
          style={{ width: `${percentage}%` }}
        />
      &lt;/div>

      {/* 数值显示 */}
      &lt;div className="grid grid-cols-3 gap-4 text-center">
        &lt;div>
          &lt;p className="text-gray-500 text-sm">已用空间&lt;/p>
          &lt;p className="text-xl font-bold text-red-500">
            {formatBytes(usage!.used)}
          &lt;/p>
        &lt;/div>
        &lt;div>
          &lt;p className="text-gray-500 text-sm">可用空间&lt;/p>
          &lt;p className="text-xl font-bold text-green-500">
            {formatBytes(usage!.available)}
          &lt;/p>
        &lt;/div>
        &lt;div>
          &lt;p className="text-gray-500 text-sm">总容量&lt;/p>
          &lt;p className="text-xl font-bold">
            {formatBytes(usage!.total)}
          &lt;/p>
        &lt;/div>
      &lt;/div>
    &lt;/div>
  );
}

function formatBytes(bytes: number): string {
  if (bytes === 0) return '0 B';
  const k = 1024;
  const sizes = ['B', 'KB', 'MB', 'GB', 'TB'];
  const i = Math.floor(Math.log(bytes) / Math.log(k));
  return parseFloat((bytes / Math.pow(k, i)).toFixed(2)) + ' ' + sizes[i];
}

遇到的问题和解决方案

问题1:macOS权限告警

当我的程序试图扫描/Users以外的文件时,系统弹出了安全告警。

解决方案:在tauri.conf.json中配置权限:

{
  "allowlist": {
    "fs": {
      "all": true,
      "read": true,
      "write": false,
      "path": true,
      "recursive": true
    }
  }
}

对于某些受保护的系统目录,需要在应用首次运行时请求用户授权。

问题2:扫描太慢

全盘扫描256GB的文件,Rust代码跑一次要3分钟。

解决方案:优化策略

  1. 并行扫描:使用rayon库实现多线程并行遍历
  2. 增量扫描:缓存扫描结果,只扫描修改过的文件
  3. 优先级排序:先扫描用户目录(/Users),跳过系统目录
use rayon::prelude::*;

pub fn scan_with_parallelism(root: &Path) -> Result&lt;Vec&lt;FileInfo>, std::io::Error> {
    let entries: Vec&lt;_> = fs::read_dir(root)?
        .flatten()
        .filter(|e| {
            // 过滤隐藏目录和系统目录
            let path = e.path();
            !path.to_string_lossy().starts_with(".")
        })
        .collect();

    let files: Vec&lt;FileInfo> = entries
        .par_iter()
        .filter_map(|entry| {
            // 并行处理每个条目
            process_entry(entry).ok()
        })
        .collect();

    Ok(files)
}

优化后,扫描时间从3分钟降到了40秒。

问题3:文件删除的安全性

最关键的问题:万一用户删错了怎么办?

解决方案

  1. 删除前预览:显示文件路径、大小、修改时间
  2. 移动到废纸篓:而不是直接删除
  3. 回收站功能:提供恢复入口
use std::fs;

// 移动文件到废纸篓(macOS特有)
pub fn move_to_trash(file_path: &Path) -> Result&lt;(), std::io::Error> {
    let trash_path = dirs::home_dir()
        .unwrap()
        .join(".Trash")
        .join(file_path.file_name().unwrap());

    fs::rename(file_path, trash_path)
}

// 从废纸篓恢复
pub fn restore_from_trash(file_name: &str) -> Result&lt;(), std::io::Error> {
    let trash_path = dirs::home_dir()
        .unwrap()
        .join(".Trash")
        .join(file_name);

    // 记录删除时的原路径,这里简化处理
    // 实际应用中需要记录元数据

    Ok(())
}

最终成果

经过两天的开发,我的清理工具具备了以下功能:

✅ 已完成

功能 状态 说明
磁盘空间可视化 实时显示使用情况
大文件扫描 找出超过100MB的文件
文件夹排序 按占用空间排序
缓存清理 识别常见缓存位置
安全删除 预览后移动到废纸篓
现代化UI TailwindCSS + 深色模式

📋 待开发

功能 计划 说明
重复文件检测 v2.0 MD5比对查找重复文件
应用卸载 v2.0 完整卸载残留清理
智能建议 v2.0 AI推荐可清理项

成本核算

项目 成本
开发时间 约20小时(AI辅助下)
学习成本 约5小时(Tauri入门)
金钱成本 0元
心理成本

AI时代的软件开发新范式

这次开发经历让我深刻体会到:AI真的改变了软件开发的门槛。

传统开发 vs AI辅助开发

维度 传统开发 AI辅助开发
需求理解 读文档、搜Google 直接问AI
技术选型 逐一对比、踩坑 AI推荐最优解
代码编写 Google/CSDN/StackOverflow AI直接生成
Bug调试 print调试、阅读源码 AI分析错误栈
性能优化 经验积累 AI给出优化方案

我的AI开发提示词模板

【角色】
你是一个资深macOS开发者,精通Rust、Tauri、React。

【任务】
{描述你的需求}

【要求】
1. 给出完整的代码实现
2. 解释关键逻辑
3. 考虑边界情况和错误处理
4. 如果有性能考虑,请一并说明

【输出格式】
1. 方案概述
2. 核心代码(可直接复制使用)
3. 使用说明
4. 注意事项

写给同样256GB的你

如果你也是256GB受害者,我可以给你几条建议:

1. 先用这个思路做一个属于自己的工具

  • 不需要从零开始
  • 不需要精通Rust
  • 只需要一个好的提示词

2. 日常清理习惯

# 清理Homebrew缓存
brew cleanup

# 清理npm缓存
npm cache clean --force

# 清理Docker(如果你用Docker)
docker system prune -a

# 清理Xcode缓存
rm -rf ~/Library/Developer/Xcode/DerivedData

3. 如果你不想自己造

告诉我,我可以帮你做一个定制版。


结语

这篇文章的起因,是256GB磁盘空间不足的焦虑。

文章的结局,是我用AI工具在2天做出了一个自己的清理工具。

AI时代,没有什么是不可能的。

如果你也对这个项目感兴趣,或者有任何问题,欢迎在评论区交流。

另外,我打算把这个工具做得更完善,如果你希望用到这个工具,评论区告诉我,我会考虑分享出来。


欢迎关注我,后续会分享更多AI辅助开发的实战经验。

点赞 0
收藏 1
分享
本文参与登链社区写作激励计划 ,好文好收益,欢迎正在阅读的你也加入。

0 条评论

请先 登录 后评论
King
King
0x56af...a0dd
擅长Rust/Solidity/FunC/Move开发