Xinwei Xiong · 2026 年 7 月 18 日
12 分钟 · 5938 字 · | EN

你的 Mac 上有几套工具在打架?我设计了一个开发机体检 Skill

复盘 devbox-doctor 如何只读盘点 macOS 应用与常驻资源,用多源证据区分重复安装、配置冲突、卸载残留和不确定项,并限制模型只做提案。文章梳理 Spotlight 边界、Homebrew 语义、残留置信度、权限降级和误报评测,规定软件推荐必须附官方来源、版本与核验日期,适合想安全治理开发机的程序员。

你的 Mac 上有几套工具在打架?我设计了一个开发机体检 Skill

我决定做一个面向程序员的开发机体检 skill,代号 devbox-doctor。它只读扫描你的 Mac,把应用、包管理器、配置接管点、常驻资源和可能的卸载残留整理成事实;模型只负责解释证据并提出下一步,不能把“看起来像”变成删除命令。最终交付是一份能复核、能降级、能说明自己哪里不知道的体检报告。这篇文章记录它的设计,也记录几处最容易写得漂亮、做得危险的地方。

上一篇 我拆了一个敢删我文件的存储清理 skill,得到一套设计方法论。方法论不落地就是收藏夹,这篇是它的第一次完整应用——而且需求不是想出来的,是从我自己的机器上长出来的。

需求是从我的磁盘里挖出来的

起因还是那次磁盘清理。扫完 296 GB 已用空间,除了预期内的缓存,报告里躺着几个让我坐直了的条目:

  • Docker Desktop 和 OrbStack 并存,一个占着 4.4 GB 虚拟磁盘,一个占着 12 GB 数据——两个都是容器运行时,我日常只用其中一个,另一个纯粹是「换工具的时候忘了送走前任」;
  • Trae 编辑器的残留:应用本体早就卸了,Application Support 里躺着 2.5 GB 数据,~/.marscode 还有 1.2 GB——卸载动作只完成了一半;
  • pyenv 里囤着 3.6 GB 的 Python,而我今年的新项目已经全面转向 uv;
  • 还有一排「装过、试过、再没打开过」的工具:几个 AI 编辑器、几个终端、若干效率小工具。

把这几条放在一起看,一个模式浮出来了:程序员的电脑是工具的墓地。我们是全世界装软件最勤、卸软件最懒的人群。每一次技术选型、每一波工具热点、每一篇「XX 神器安利」都会在机器上留一具尸体:旧的运行时、被替代的 CLI、试用完忘掉的 App、迁移到一半的前任工具。单看每一次安装都有理由,日积月累就是几十 GB 的磁盘、一排开机自启动、和一个越来越说不清「我机器上到底装了什么」的你。

而且这个问题有个微妙的性质:它不是靠「清理」能解决的。清缓存是体力活,判断「这个工具还该不该活在我机器上」是脑力活——它需要知道这个工具是什么、生态位上有没有更好的、我到底还在不在用它。这正是上一篇里那个判断的完美测试场:确定性工作交代码,判断交模型。

过三关:这个需求配得上一个 Skill

按方法论,先过三关。

高频吗?装软件是程序员的日常动作,而审视存量几乎从不发生——不是不想,是成本太高:逐个 App 回忆「我上次打开它是什么时候」不现实。高频的债务积累 + 从不执行的盘点 = 典型的该被 skill 接管的流程。

有非模型不可的判断吗?这是 devbox-doctor 比存储清理更依赖模型的地方。「OrbStackDocker Desktop 属于相近生态位」「pyenv 与 uv 的能力有交集,但职责并不完全相同」——规则能发现共存,模型才能结合项目约束解释这究竟是刻意隔离、历史迁移,还是需要处理的冲突。模型适合做的是提出假设和补齐上下文,不是给软件贴“好坏”标签。

有明确交付物吗?一份体检报告:按证据置信度分级的处置提案、独立的升级观察区,以及一张脱敏统计卡。每个结论都能追溯到本机事实,每条易腐知识都能追溯到来源和核验日期。

三关全过。开工。

数据层设计:元数据不是使用日志

体检的第一原则和上一篇一致:扫描默认只读。但“macOS 一直在完整记账”是一个危险的说法:系统保留的是不同来源、覆盖范围各异的元数据,不是一份可审计的应用启动日志。devbox-doctor 只采集能解释其来源的线索:

scan.py 分层采集事实
├── 系统基线(始终可用)
│   ├── /Applications、~/Applications、应用 Info.plist
│   ├── mdls、du、ps、launchctl 的只读输出
│   └── LaunchAgents / LaunchDaemons 的文件清单
├── 可选探测(命令存在才运行,失败不影响基线报告)
│   ├── Homebrew:list、leaves、autoremove --dry-run
│   ├── npm / pipx / cargo 等全局工具清单
│   └── pyenv / nvm / rustup 等版本管理器状态
└── 关联证据
    ├── bundle ID、路径、安装 receipt、代码签名
    ├── Containers / Application Support / Caches 等目录
    └── 当前进程、登录项、守护进程与监听端口

这里有两个容易写错的 Homebrew 概念。按照 Homebrew 官方手册brew leaves 列出的是不再作为其他已安装 formula 或 cask 依赖的 formula;它们可能正是用户主动安装并直接使用的顶层工具,绝不能翻译成“孤儿依赖”。真正面向“曾作为依赖安装、如今不再需要”的候选项是 brew autoremove --dry-run。即便如此,扫描也只记录候选,正式执行前仍要展示列表并再次确认。

mdls 仍然有价值,但边界必须写清楚。Apple 对 kMDItemLastUsedDate 的定义是:文件经双击或请求 Launch Services 打开时自动更新。它没有承诺覆盖命令行启动、后台拉起、远程调用或所有应用内部活动。kMDItemUseCount 可以在部分机器上读到,却不应被描述成 Apple 保证准确的“累计启动次数”。它可能缺失,也可能因为索引重建、迁移、系统版本和打开路径而与人的真实使用不一致。

因此 null 只表达一件事:当前元数据源没有可用值。它不能单独证明“从未打开”。目录修改时间也不天然等于用户使用——自动更新和后台同步同样会改时间。报告只有在多个独立来源同向时才给出“疑似长期未使用”;单源、相互矛盾或无法解释的样本一律进入“不确定”,永不进入自动处置。

这条规则的本质,是把上一篇「模型只提议」的原则又往前推了一步:模型的提议本身也要有证据链,不能让它凭「这个 App 名字听起来过时了」来判死刑。

判断层设计:先把“打架”拆开

扫描 JSON 交给模型后,判断分四步走:

第一步,识别生态位,但允许多标签。每个 App、每个 CLI 可以同时承担编辑器、终端、容器运行时或版本管理等职责。已知 bundle ID 和包管理器元数据先走规则,模型只解释无法归类或职责重叠的项目,并为结论附来源。

第二步,把容易混淆的现象拆成四类

  • 重复安装:同一生态位存在多个实现,只陈述共存事实。Docker Desktop 与 OrbStack 同装,不等于二者正在冲突;
  • 配置冲突:多个工具同时竞争 PATH shim、shell 初始化、默认文件关联、端口、代理或同一运行时。必须展示冲突点;
  • 常驻资源:登录项、LaunchAgent、守护进程、菜单栏进程和后台服务单独统计 CPU、内存与唤醒证据;常驻不等于有害;
  • 卸载残留:应用本体已不在,但存在可关联的数据。关联本身也有置信度,不再用目录名差集直接定罪。

第三步,给残留关系打置信度。目录名相似只是弱证据;bundle ID 精确命中、安装 receipt 的 payload、代码签名 Team ID、应用内声明的容器标识,才是强证据。共享目录尤其危险:一个 vendor 目录可能同时服务多个应用,Homebrew 也明确提醒 cask uninstall --zap 可能删除应用间共享文件。报告按下表处理:

置信度证据示例报告动作
bundle ID 与容器标识精确匹配;receipt payload 指向该路径可进入待确认清单,仍不自动删除
签名 Team ID、vendor 与路径结构一致,且没有其他已安装应用引用展示证据,建议在访达核对
仅目录名、图标名或模糊字符串相似只展示,不给删除按钮
不确定共享目录、签名缺失、receipt 不完整或证据冲突明确标注“无法归属”,默认保留

第四步,升级建议保持独立。它回答的不是“谁更流行”,而是“当前工具是否已经形成可验证的约束,而候选工具能否满足这些约束”。没有项目配置、官方能力边界和迁移验证,就不产生推荐。

报告形态也不再把风险伪装成红绿灯:

区域语义动作
事实已安装、常驻、占用、配置接管点只读展示
提案高置信残留、可复现冲突、长期无使用证据展示证据与反证,等待用户确认
不确定单源或冲突证据、共享目录默认保留,提示如何补证
升级观察经官方资料核验的同生态位候选纯情报,不进入处置流程

升级观察区有一条铁的边界:它不是删除决策,永远不出现在处置清单里。它是一次可选的技术选型雷达扫描,看完可以完全无视。

推荐必须能回到官方来源

设计到这里,我停下来想了很久的不是技术,是一个产品伦理问题:AI 张口推荐软件,和营销号带节奏之间,隔着什么?

隔着可复核的来源和对“不推荐”的尊重。我给升级区定了五条规则:

  1. 来源只接受项目官网、官方文档和官方发布页。排行榜、营销稿和模型记忆只能提供检索线索,不能成为结论;
  2. 每张卡写清本机版本、候选版本与核验日期。版本拿不到就写“未知”,资料超过约定保鲜期就降级为“待复核”;
  3. 只比较可验证能力,不写“快一个量级”“更现代”这类没有统一基准的句子。性能主张必须附可复现实验环境,否则删掉;
  4. 必须写迁移成本和不换的理由。配置文件、插件、许可证、团队协作与 CI 都算迁移成本,不能只看安装命令;
  5. 重度使用、付费、受组织策略管理的工具默认不推荐替换。除非用户明确提出目标,使用事实和既有约束优先于流行度。

例如,一张关于 uv 的候选卡必须现场记录 uv self version 的本机输出,并从 uv 官方文档 核对能力、从 官方 Releases 核对候选版本,标注“核验日期:2026-07-31”。若当天无法确认最新稳定版本,这张卡就不生成。即使资料完整,也要用真实项目验证 lockfile、私有源和 CI;不能据此推导“uv 一个顶五个”,更不能把 pyenv、pipx、conda 全部判为冗余。工具边界取决于项目,不取决于口号。

一句话收束:推荐是有保质期的情报,不是钦定。拍板仍然在人。

威胁模型:只读也会泄露

“不会删文件”不等于“没有风险”。这类工具最值得防的并不是一个陌生黑客,而是四个更现实的失败面:

  • 扫描范围过宽,把用户名、客户项目名、私有仓库路径和安装软件清单送进远端模型;
  • 读取 shell history、进程参数或配置文件时碰到 token、数据库口令和内部域名;
  • 把共享目录误判成残留,确认界面又用绿色按钮诱导用户快速通过;
  • 本地报告服务监听所有网卡,或者把未脱敏报告写进会自动同步的目录。

所以默认权限应该小得近乎吝啬:不请求 Full Disk Access,不读取浏览器、钥匙串、邮件、聊天记录和文件正文;不采集完整 shell history 与进程参数;路径在进入模型前用稳定散列或占位符脱敏;原始 JSON 只保存在用户明确选择的本地目录,默认不上传。需要额外目录时,先解释“为了哪条判断、要读什么、保留多久”,按目录单独授权。

降级也必须是产品路径,而不是报错后的安慰:

  • Spotlight 不可用:保留安装时间、签名、receipt 与当前运行证据,使用状态统一标为未知;
  • Homebrew 不存在:跳过包管理器区,不建议安装 Homebrew;
  • 没有管理员权限:照常生成报告,隐藏需要提权的动作;
  • 网络不可用或用户选择离线:关闭升级推荐,只使用本地事实;
  • 权限被拒绝:记录未覆盖范围,健康分不因“没扫到”而变高。

真正的安全边界仍然是“代码—模型—代码”:采集器输出事实,模型只能生成结构化提案;执行器只接受固定动作和扫描时发现的精确路径,重新校验签名与 inode 后才移动到废纸篓。共享目录、系统目录和证据不确定项不进入执行白名单。

先测误报,再谈健康分

如果没有评测集,“至少两源同向”也只是听起来谨慎。我会准备一组人工标注机器快照,至少覆盖:正常单工具、刻意并存、多版本项目、迁移中机器、共享 vendor 目录、Spotlight 关闭、应用改名、远程开发和企业管理软件。标注者要为每个结论给出证据,而不只给“删/留”答案。

核心指标不是找出了多少垃圾,而是误报率

任务首要指标上线门槛
可处置残留精确率人工复核集达到 99%,系统与共享目录零自动处置
配置冲突精确率与证据可复现率每条都能用命令复现接管点
长期未使用误报率任何单源结论都不得进入处置区
升级推荐来源有效率、版本新鲜度官方来源 100%,过期卡片自动降级

每次规则或模型版本变化都重跑固定集,并保留错误案例。健康分只根据已覆盖且可解释的维度计算;扫描缺失越多,显示的是置信区间和覆盖率,而不是虚假的满分。

知识会过期,所以参考文档带日期戳

升级建议还有一个事实数据没有的难题:工具生态知识是易腐品。今天的候选两年后也可能停止维护。如果把“某工具优于另一工具”写死在 prompt 里,这个 skill 会以肉眼可见的速度烂掉。

解法是把易腐知识从 SKILL.md 里剥出去,放进独立的参考文档,并且每一条都带判断日期

devbox-doctor/
├── SKILL.md              # 流程与铁律(稳定,很少改)
├── references/
│   ├── macos.md          # 数据怎么读:mdls 的坑、残留匹配规则(稳定)
│   ├── stacks.md         # 生态位地图:能力边界、官方来源、版本与核验日期
│   │                     #(易腐!超过保鲜期自动降级)
│   └── judging.md        # 判定标准:未使用证据链、冲突条件、
│                         # 残留置信度与推荐铁律(稳定)
├── scripts/
│   ├── scan.py           # 分层只读扫描 → JSON
│   ├── build_report.py   # 分析 JSON → 静态报告
│   └── server.py         # 本地服务:展示确认、复核白名单动作
└── assets/
    └── report_template.html

SKILL.md 里的指令变成:「判断堆叠与替代时,读取 stacks.md;校验来源域名、当前版本和核验日期;超过保鲜期就停止生成推荐卡。」保鲜期不是全局一年:安全工具和快速迭代的运行时可以按 90 天,稳定的系统事实可以按一年。这个设计顺手回答了一个更大的问题——skill 和博客文章的根本区别是什么?文章发布即冻结,而 skill 的知识层是活的。上一篇说「Skill 是流程的固化」,这篇要补一句:固化的是流程,活着的是知识。

传播设计是一等公民,不是发布后的运营

上一篇分析过 storage-analyzer 的传播齿轮:结果可截图、人人相关、有惊讶感、低依赖。devbox-doctor 也把传播做进产品,但不能为了好看牺牲边界:

可截图:报告顶部是一张「体检卡」——重复安装 N 组、可复现冲突 N 个、疑似残留 N GB、覆盖率 N%,再附一个带置信区间的健康分。分享模式重新渲染卡片,不只是把路径藏起来:小样本计数会做区间化,时间、用户名、主机名、软件名和路径全部移除。即便如此,统计组合仍可能形成设备指纹,所以界面只说“已脱敏”,不承诺“隐私为零风险”。

有惊讶感:真正值得传播的不是“系统记录了每个 App 的使用次数”这种过度概括,而是“元数据也有观测边界”。一台机器上出现三套 Python 工具,并不自动说明它们在打架;把冲突点和不确定性一起展示,反而比耸动标题更经得住复查。

人人相关:圈层比全民清理窄,但密度高——每个程序员都有工具墓地,且程序员是最愿意晒终端截图的人群。

基线零安装mdlslaunchctlps 等随 macOS 提供;Homebrew、npm、pipx 与各版本管理器只是可选探测器。探测器不存在时,报告明确缩小覆盖范围,不为了体检再往机器里安装一套工具。

泼冷水:图纸不是产品

照例给自己泼两盆。

第一盆:这篇是设计文档,不是发布公告。我验证了若干采集点,却还没有足够样本证明判断层准确。下一步不是多找几台机器“感觉一下”,而是先冻结标注集与指标,再记录每次规则变更带来的误报和漏报。做完我会回来写实测篇,到时候这篇里被现实打脸的设计,会原样展示。

第二盆:升级区仍是整个设计里最没把握的部分。官方来源能挡住幻觉,却挡不住“正确但无用”。读取更多项目配置可以让建议更贴身,也会扩大隐私面。当前选择是保守的:默认不开升级区;用户主动开启后,只读取其明确选择的项目,并在报告结束时删除临时提取的数据。

我越来越不想把它叫“AI 清理器”。一个可靠的 devbox-doctor 更像医生:先承认观测有限,再区分症状与病因,最后把处方交给本人。工具的墓地人人都有,但守墓人不该拿着模型的猜测挥铲子。

常见问题

03
如何查看 Mac 上某个应用最后一次使用是什么时候?

可用系统自带的 mdls 读取 Spotlight 已索引的 kMDItemLastUsedDate 与 kMDItemUseCount。Apple 只明确说明前者会在通过 Launch Services 打开文件时更新;这两个值都不是完整、可靠的应用启动日志。返回 null 只代表该属性当前无值,可能与索引状态、启动路径或元数据来源有关,不能推导为从未使用。

什么是开发机上的僵尸应用和堆叠冲突?

僵尸应用是长期没有可验证使用证据、但仍占据空间或常驻资源的软件。重复安装只表示同一生态位存在多个工具;只有它们同时接管 PATH、端口、文件关联或运行时,才算配置冲突;登录项、守护进程与后台服务则单独归为常驻资源。三者证据和处置方式不同,不能混为一谈。

让 AI 推荐替代软件,怎么避免变成不负责任的带节奏?

每张推荐卡只接受项目官网、官方文档或官方发布页,并记录本机版本、候选版本与核验日期;无法确认版本就不生成卡片。卡片必须同时写迁移成本和不换的理由,重度使用、付费或受组织策略管理的工具默认不建议替换。

读者回响

加入讨论

新文章写好,先寄给你

每有新文章,寄一封信到你的邮箱。双重确认,随时退订。