Files
gentpan 4741d40e1b OpenStats:macOS 菜单栏系统监控初始版本
- 菜单栏:CPU/GPU/内存多样式、网速两行、温度、风扇
- 面板:概览仪表盘、进程、散热、防休眠,液态玻璃与浅色/深色主题
- 采集层:CPU/内存/网络/GPU/磁盘/电池/进程/SMC 温度与风扇,按界面分级采样
- root 辅助工具:风扇调速、合盖不睡眠,断开时自动恢复
- XcodeGen 工程、单元测试、截图模式
2026-09-13 02:44:05 +05:00

405 lines
24 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# OpenStats 方案:macOS 菜单栏监控 + 系统清理
> 调研日期:2026-09-13 | 开发机:macOS 27.0 / Xcode 27 / Swift 6.4 / Apple Silicon
## ⚠️ 范围调整(2026-09-13,已实现)
以下原始方案中的**清理功能已取消**,第 5 节及相关里程碑仅作存档。当前实现的范围:
| 模块 | 状态 |
|---|---|
| 菜单栏:CPU / GPU / 内存(数字、柱状图、圆环、饼图、柱状+数字五种样式)、网速两行、温度、风扇 | ✅ |
| 面板:概览仪表盘(健康评分、三列指标卡、核心负载、电池、高占用进程、快捷开关)、进程、散热、防休眠 | ✅ |
| 液态玻璃面板(macOS 26+),浅色 / 深色 / 跟随系统 | ✅ |
| 风扇调速(自动 / 降温 / 强冷 / 自定义),CPU 过热自动交还系统 | ✅ 需安装辅助工具 |
| 防休眠:屏幕常亮、仅系统不休眠、合盖后继续运行(电量保护、定时) | ✅ 合盖模式需辅助工具 |
| 温度传感器:启动时枚举 SMC 键按前缀归类,不硬编码芯片型号 | ✅ |
| Developer ID 签名、公证、Sparkle 自动更新 | ⏳ 下一阶段 |
工程结构与运行方式见根目录 [README.md](../README.md)。
---
## 0. 结论
| 决策 | 选择 | 理由 |
|---|---|---|
| 语言 / UI | Swift 6 + AppKit(`NSStatusItem`)+ SwiftUI(面板、设置页) | 菜单栏图标自绘需要 AppKit,面板用 SwiftUI 开发效率高 |
| 最低系统 | macOS 14 Sonoma | 可用 `@Observable`、`SMAppService`、`Task.sleep(for:tolerance:)` |
| 监控层 | 参考并部分移植 **exelban/stats**(MIT) | 许可证允许,保留版权声明即可 |
| 清理层 | 参考 **tw93/Mole** 的**规则与安全模型**,用 Swift **自己重写** | Mole 是 **GPL-3.0**,不能直接复制代码进非 GPL 项目(详见 §1) |
| 沙盒 | **不沙盒**,不上 Mac App Store | 清理要访问 `~/Library` 各处,温度传感器要走 IOKit/SMC,沙盒内做不到 |
| 分发 | Developer ID 签名 + 公证(notarize)+ Sparkle 2 自动更新 + Homebrew Cask | Stats 与 Mole 都是这条路 |
| 工程组织 | Xcode App 工程 + 本地 Swift Package(多 target) | 采集、清理、UI 分离,便于单测 |
---
## 1. 参考项目核查(有几处需要纠正)
| 项目 | 核查结果 | 能怎么用 |
|---|---|---|
| [exelban/stats](https://github.com/exelban/stats) | MIT,Swift,约 4.2 万 Star,2026-09 仍在更新 | ✅ 可移植代码,需在 `LICENSE`/致谢页保留 `Copyright © Serhiy Mytrovtsiy`。注意作者声明“开源但不接受贡献” |
| [tw93/Mole](https://github.com/tw93/Mole) | **GPL-3.0**(不是 MIT),Shell + Go,约 6.7 万 Star;另有 `TRADEMARK.md` 商标声明;它的 GUI 版“Mole for Mac”是闭源商业产品 | ⚠️ 不能把它的脚本/Go 代码搬进 MIT 或闭源项目;可以参考思路、清理目录清单、安全设计(这些是事实和设计,不是代码表达)。不能用 “Mole” 名称 |
| mhaeuser/Battery-Toolkit | BSD-3-Clause,Swift | ✅ 参考特权辅助进程(Privileged Helper)+ XPC 的规范写法 |
| Sam-Spencer/bar-charts | **GitHub 上不存在**(API 返回 404) | ❌ 忽略。图表直接用系统自带 Swift Charts 即可 |
| 补充:vladkens/macmon | MIT,Rust | ✅ Apple Silicon 下无需 sudo 读功耗 / GPU 的 `IOReport` 用法参考 |
**关于 Mole 的两条路**
- **A(推荐):项目用 MIT,清理模块自己用 Swift 实现。** 只把 Mole 当“需求说明书”:看它清哪些目录、有哪些保护规则、踩过哪些坑(`SECURITY_AUDIT.md` 很有价值),然后自己写。
- B:项目整体采用 GPL-3.0,就可以直接移植 Mole 的逻辑,甚至打包调用 `mo` CLI。代价是整个 App 必须开源为 GPL,以后无法闭源或商业授权;而且调用 Shell 脚本在 GUI 里做进度、取消、错误处理都很别扭。
---
## 2. 功能范围
### MVP(第 1 阶段)
- 菜单栏:CPU 百分比 + 迷你折线(可选显示内存、网速)
- 弹出面板:CPU 总量 / 用户态 / 内核态、每核心(区分 P 核 / E 核)、Top 5 进程;内存(App / 联动 / 压缩 / 内存压力);实时网速
- 清理(仅用户态,无需管理员密码):扫描 → 分类预览 → 勾选 → **移到废纸篓** → 记录日志
- 设置:刷新频率、菜单栏显示项、开机自启(`SMAppService.mainApp`)
### V1(第 2 阶段)
- GPU 使用率、温度、功耗、风扇转速、电池健康度、磁盘读写与剩余空间
- 清理扩展:开发者缓存(Xcode / npm / pnpm / Homebrew…)、项目构建产物(`node_modules`、`target`)、安装包(`.dmg/.pkg`)、大文件查找
- 应用卸载(连带清理偏好设置、缓存、LaunchAgent)
- 白名单、历史记录、通知(CPU 持续过高、磁盘将满)
### V2(第 3 阶段)
- 需要 root 的操作:刷新 DNS、系统日志清理、Time Machine 本地快照 → 通过 `SMAppService.daemon` 安装特权辅助进程 + XPC
- 磁盘占用可视化(类似 `mo analyze`,树图)
- 桌面小组件(WidgetKit)
---
## 3. 总体架构
```
┌──────────────────────────── OpenStats.app(主进程,非沙盒)────────────────────────────┐
│ │
│ UI 层(@MainActor) │
│ ├─ StatusItemController NSStatusItem,自绘 NSImage(数字 + 迷你图),不用 SwiftUI 重建 │
│ ├─ PopoverPanel NSPanel/NSPopover 承载 SwiftUI:监控页 / 清理页 / 设置入口 │
│ └─ SettingsWindow SwiftUI Settings │
│ ▲ @Observable 状态(只存“最新值 + 环形缓冲历史”) │
│ │ │
│ 调度层(actor MetricsHub) │
│ ├─ 分级:menuBar 档(2s,只跑 CPU 总量/网速) popover 档(1s,全量 + 进程列表) │
│ ├─ 暂停:锁屏 / 屏幕休眠 / 系统睡眠时停止;低电量模式下降频 │
│ └─ Samplers:CPU / Memory / Network / GPU / Sensors / Battery / Disk / Process │
│ │
│ 清理层(actor CleanEngine) │
│ ├─ RuleCatalog 清理规则(数据化,Swift 静态表或 JSON) │
│ ├─ Scanner 并发扫描计算大小(只读,可取消) │
│ ├─ SafetyGuard 路径校验 / 保护名单 / 符号链接 / 运行中应用检测 / 白名单 │
│ ├─ Executor trashItem(默认)或 removeItem(用户显式选择) │
│ └─ OperationLog ~/Library/Logs/OpenStats/operations.jsonl │
│ │
└──────────────────────────────────────┬────────────────────────────────────────────────┘
│ XPC(NSXPCConnection,V2 才需要)
┌────────────▼─────────────┐
│ PrivilegedHelper(root) │ SMAppService.daemon 注册
│ 只接受白名单内的固定操作 │ 校验调用方签名(Team ID)
└──────────────────────────┘
```
### 工程目录
```
openstats/
├─ OpenStats.xcodeproj
├─ App/ # App target:AppDelegate、StatusItem、窗口管理
├─ Packages/OpenStatsKit/ # 本地 Swift Package
│ ├─ Sources/
│ │ ├─ Metrics/ # 各 Sampler + MetricsHub(纯逻辑,可单测)
│ │ ├─ SMCBridge/ # C/ObjC 桥接:SMC、IOHIDEventSystemClient、IOReport
│ │ ├─ Cleaner/ # RuleCatalog、Scanner、SafetyGuard、Executor
│ │ ├─ DesignSystem/ # 颜色 / 字号 / 间距 token
│ │ └─ Features/ # SwiftUI 页面:MonitorView、CleanView、SettingsView
│ └─ Tests/
├─ Helper/ # V2:特权辅助进程
├─ ThirdPartyNotices.md # Stats 的 MIT 版权声明
└─ docs/
```
---
## 4. 监控层:每个指标从哪里读
| 指标 | API | 要点 / 坑 |
|---|---|---|
| CPU 总量 & 每核 | `host_processor_info(PROCESSOR_CPU_LOAD_INFO)`,两次采样取差值 | 返回的数组是内核分配的,**每次必须 `vm_deallocate`**,否则持续泄漏;计数器用 `&-` 防回绕 |
| P 核 / E 核划分 | `sysctl hw.perflevel0.logicalcpu`(P)/ `hw.perflevel1.logicalcpu`(E) | 核心编号顺序:Apple Silicon 上 E 核通常在前,按 perflevel 数量切分 |
| 内存 | `host_statistics64(HOST_VM_INFO64)` | 与活动监视器同口径:App = `internal - purgeable`,已用 = App + wired + compressed;页大小用 `host_page_size()`(Apple Silicon 为 16K) |
| 内存压力 | `sysctl kern.memorystatus_vm_pressure_level`(1/2/4) | 需要事件时可叠加 `DispatchSource.makeMemoryPressureSource` |
| 网速 | `sysctl(NET_RT_IFLIST2)` → `if_msghdr2.ifm_data`(`if_data64`) | ⚠️ 纠正原文:`getifaddrs` 里的 `if_data.ifi_ibytes` 是 **32 位**,流量超过 4 GiB 会回绕,应改用 64 位计数器 |
| 进程列表 | `proc_listallpids` + `proc_pid_rusage(RUSAGE_INFO_V4)` / `proc_pidinfo(PROC_PIDTASKINFO)` | 只在面板展开时跑;CPU% 同样靠两次 `ri_user_time + ri_system_time` 差值 |
| GPU 使用率 | `IOServiceMatching("IOAccelerator")` → `PerformanceStatistics["Device Utilization %"]` | Stats 的 `Modules/GPU/reader.swift` 可直接参考 |
| 温度(Apple Silicon) | `IOHIDEventSystemClient` 私有 API(`kIOHIDEventTypeTemperature`)+ SMC 键 | 私有 API,需 ObjC/C 桥接头;参考 Stats `Modules/Sensors/reader.m` |
| 功耗 / GPU 频率 | `IOReport`(`libIOReport.dylib`,“Energy Model”、“GPU Stats” 通道) | 无需 sudo;参考 macmon |
| 风扇 | SMC 键 `FNum`、`F0Ac` | 读取无需 root;**写入(调速)需要 root helper**,建议不做或放到最后 |
| 电池 | `IOPSCopyPowerSourcesInfo` + `IOServiceMatching("AppleSmartBattery")` | 循环次数、设计容量、当前最大容量 |
| 磁盘空间 | `URLResourceValues.volumeAvailableCapacityForImportantUsage` | 比 `statfs` 更接近 Finder 显示(含可清除空间) |
| 磁盘读写 | `IOBlockStorageDriver` 的 `Statistics` 字典 | 两次采样取差值 |
### 功耗预算(硬指标)
| 场景 | 目标 |
|---|---|
| 面板关闭,只显示 CPU | 平均 CPU < 0.3%,常驻内存 < 40 MB |
| 面板展开,全量 | 平均 CPU < 2% |
| 锁屏 / 屏幕休眠 | 采样完全停止 |
实现手段:
1. **分级采样**:面板关闭时只跑 CPU 总量和网速;进程列表、温度、GPU 仅面板展开时启用(温度传感器是 Stats 里最耗电的模块)。
2. **定时器合并**:`Task.sleep(for:tolerance:)` 给 0.5s 容差,让系统合并唤醒。
3. **菜单栏自绘**:每次刷新只重画一张小 `NSImage` 赋给 `statusItem.button.image`,不在菜单栏里挂 SwiftUI 视图(后者每秒重建开销大)。
4. **历史数据用固定大小环形缓冲**(例如 60 个点),不做持久化数据库。
5. 监听 `NSWorkspace.screensDidSleepNotification` / `willSleepNotification` / `com.apple.screenIsLocked` 暂停;`ProcessInfo.isLowPowerModeEnabled` 时降到 5s。
6. 用 Instruments 的 **Energy Log / Time Profiler** 与活动监视器“能耗影响”列做回归测试。
---
## 5. 清理层设计(参考 Mole,Swift 重写)
### 5.1 流程
```
扫描(只读、并发、可取消)→ 分类展示大小 → 用户勾选 → 二次确认 → SafetyGuard 逐项再校验
→ 移到废纸篓(默认)/ 永久删除(需用户显式开启)→ 写操作日志 → 显示释放空间
```
所有删除只走 **一个出口函数**(对应 Mole 的 `safe_remove`),便于审计和测试。
### 5.2 规则数据化
```swift
struct CleanRule: Identifiable, Sendable {
enum Risk: Sendable { case safe, review } // safe 默认勾选,review 默认不勾选
enum Requirement: Sendable { case none, fullDiskAccess, admin }
let id: String // "user.caches"
let category: Category // .system / .browser / .developer / .apps / .downloads
let title: LocalizedStringResource
let paths: [PathPattern] // ~/Library/Caches/*、~/Downloads/*.crdownload
let minAge: Duration? // 如 7 天内修改过的跳过
let skipIfAppRunning: [String] // bundle id,运行中则跳过
let risk: Risk
let requirement: Requirement
let postAction: PostAction? // 例如调用 `xcrun simctl delete unavailable`
}
```
### 5.3 首批规则(参考 Mole 实际清理的目录整理)
| 分类 | 目录 / 操作 | 风险 | 备注 |
|---|---|---|---|
| 系统 | `~/Library/Caches/*` | safe | **所属应用正在运行则跳过**(Mole 踩过坑:删除正在使用的 SQLite 缓存会让进程写满磁盘) |
| 系统 | `~/Library/Logs/*`、`~/Library/Application Support/CrashReporter` | safe | |
| 系统 | `~/.Trash` | review | 调用 Finder 清空更稳妥 |
| 浏览器 | Chrome / Edge / Brave / Arc / Firefox 的 **Cache、Code Cache、GPUCache** 子目录 | safe | ❌ 绝不碰 Cookies、History、Login Data;浏览器运行中跳过 |
| 浏览器 | Chromium 系旧版本目录(`…/Versions/<旧版本>`) | safe | |
| 下载 | `~/Downloads/*.crdownload`、`*.part`、`*.download` | safe | 用 `lsof` 或文件修改时间确认未在下载 |
| 下载 | `~/Downloads/*.dmg / *.pkg / *.xip` | review | 列出让用户选 |
| 邮件 | `~/Library/Containers/com.apple.mail/Data/Library/Mail Downloads` | safe | 需要完全磁盘访问权限 |
| 开发 | `~/Library/Developer/Xcode/DerivedData` | safe | |
| 开发 | `~/Library/Developer/Xcode/Archives`、iOS DeviceSupport | review | |
| 开发 | 模拟器:`xcrun simctl delete unavailable` | safe | 调用命令而不是直接删目录 |
| 开发 | npm `~/.npm/_cacache`、pnpm `~/Library/pnpm/store`、yarn、pip `~/Library/Caches/pip`、Go `go clean -modcache`、Homebrew `brew cleanup` | safe | 优先调用各工具自带清理命令 |
| 开发 | Maven `~/.m2/repository`、Gradle `~/.gradle/caches`、NuGet、conda pkgs | review | 删除后需重新下载 |
| 项目产物 | 用户指定目录下的 `node_modules`、`target`、`build`、`dist`、`.next` | review | 7 天内修改过的标记为“最近使用”;避免父子目录重复删除 |
| 云盘 | Dropbox / Google Drive / OneDrive 缓存 | review | 客户端运行中跳过 |
| 备份 | `~/Library/Application Support/MobileSync/Backup` | review | **只提示,不默认勾选** |
### 5.4 SafetyGuard 规则(照搬 Mole 的安全边界思路)
1. 拒绝:空路径、相对路径、含 `..` 路径段、含控制字符的路径。
2. 保护前缀永不删除:`/`、`/System`、`/bin`、`/sbin`、`/usr`、`/etc`、`/var`、`/private`、`/Library/Extensions`;仅放行 `/private/tmp`、`/private/var/folders` 等少数子路径。
3. **符号链接**:解析后的真实路径也要过一遍拒绝规则,而且解析结果只能“更严格”不能“放行”;父目录是符号链接也要检查。
4. 保护类别:钥匙串、密码管理器、VPN/代理工具(Clash、Tailscale、WireGuard…)、AI 工具数据(Claude、ChatGPT、Cursor、Ollama)、浏览器 Cookie/历史、iCloud `Mobile Documents`、`~/Library/Messages`、Apple 自带 Group Containers、用户自己的 `~/Library/LaunchAgents/*.plist`。
5. 执行前再次校验(扫描和删除之间可能过了几分钟,应用可能已启动)。
6. 判断不了就**跳过**,不扩大删除范围。
### 5.5 权限
- **完全磁盘访问(FDA)**:尝试读取受 TCC 保护的文件(如 `~/Library/Safari/Bookmarks.plist`)判断是否已授权;未授权时引导打开 `x-apple.systempreferences:com.apple.preference.security?Privacy_AllFiles`。没有 FDA 时对应规则显示为“需要授权”,不报错。
- **管理员权限**(V2):`SMAppService.daemon(plistName:)` 注册 helper,XPC 协议只暴露固定的枚举操作(如 `.flushDNS`、`.cleanSystemLogs`),**绝不暴露“删除任意路径”接口**;helper 端校验调用方代码签名。
---
## 6. 技术选型清单
| 用途 | 选择 | 备注 |
|---|---|---|
| 菜单栏 | `NSStatusItem` + 自绘 `NSImage` | 不用 `MenuBarExtra`:label 不能自由绘图、点击行为难控制 |
| 弹出面板 | `NSPanel`(非激活、点击外部关闭)+ `NSHostingView` | 比 `NSPopover` 动画和定位更可控;想省事可先用 `NSPopover` |
| 状态管理 | Observation(`@Observable`) | |
| 并发 | Swift 6 严格并发,采样和清理放 `actor` | UI 更新 `@MainActor` |
| 图表 | Swift Charts | 面板内的历史曲线;菜单栏迷你图用 Core Graphics 自绘 |
| 设置存储 | `UserDefaults` + `@AppStorage` | |
| 开机自启 | `SMAppService.mainApp.register()` | 不需要单独的 LaunchAtLogin helper |
| 自动更新 | Sparkle 2 | 通过 SPM 引入 |
| 快捷键 | sindresorhus/KeyboardShortcuts(MIT) | 可选 |
| 图标 | SF Symbols(唯一图标集) | 16pt 行内 / 20pt 独立 |
| 日志 | `os.Logger` + 清理操作 JSONL 文件 | |
| 测试 | Swift Testing;清理用临时目录做沙箱测试 | SafetyGuard 必须覆盖符号链接、`..`、保护路径等用例 |
| CI | GitHub Actions `macos-latest`:构建、测试、签名、公证、生成 DMG | 用 `xcrun notarytool` |
### 界面设计 token(与你的全局设计规范一致)
- 颜色:`primary` / `secondary` / `neutral` 灰阶 / `success` / `warning` / `error`,定义在 Asset Catalog 并提供深色变体;CPU 负载颜色:< 60% 中性色,60–85% `warning`,> 85% `error`
- 字号:12 / 14 / 16 / 20 / 24 / 32;间距:4 / 8 / 12 / 16 / 24 / 32
- 圆角:统一 token(如 `radius.sm = 6`、`radius.md = 10`、`radius.lg = 14`)区分卡片 / 面板
- 卡片只用边框或阴影之一;不用渐变、不用毛玻璃(系统面板自带材质除外)
---
## 7. 关键代码骨架
完整可运行版本见 [prototype/Samplers.swift](prototype/Samplers.swift)(本机实测输出:`CPU total 25.7% … cores=18`、`MEM used 43775MB / 65536MB … pressure=1`、`NET rx 692486 B/s`,进程常驻内存约 6.6 MB)。
### 7.1 应用入口与菜单栏
```swift
import AppKit
import SwiftUI
@main
struct OpenStatsApp: App {
@NSApplicationDelegateAdaptor(AppDelegate.self) var delegate
var body: some Scene { Settings { SettingsView() } }
}
@MainActor
final class AppDelegate: NSObject, NSApplicationDelegate {
private let store = MetricsStore() // @Observable,UI 读取
private let hub = MetricsHub()
private var statusItem: NSStatusItem!
private var panel: StatusPanel!
func applicationDidFinishLaunching(_ note: Notification) {
NSApp.setActivationPolicy(.accessory) // 不显示 Dock 图标(也可在 Info.plist 设 LSUIElement)
statusItem = NSStatusBar.system.statusItem(withLength: NSStatusItem.variableLength)
statusItem.button?.target = self
statusItem.button?.action = #selector(togglePanel)
panel = StatusPanel(rootView: MonitorView().environment(store))
panel.onVisibilityChange = { [hub] visible in
Task { await hub.setTier(visible ? .popover : .menuBar) }
}
Task {
await hub.start { [weak self] cpu, mem, net in
guard let self else { return }
store.apply(cpu: cpu, memory: mem, network: net)
statusItem.button?.image = MenuBarRenderer.image(cpu: store.cpuHistory, network: net)
}
}
}
@objc private func togglePanel() {
guard let button = statusItem.button else { return }
panel.toggle(relativeTo: button)
}
}
```
### 7.2 CPU 采样(节选)
```swift
struct CPUSampler: Sampler {
private var prevTicks: [[UInt32]] = []
mutating func sample() -> CPULoad? {
var cpuCount: natural_t = 0
var info: processor_info_array_t?
var infoCount: mach_msg_type_number_t = 0
guard host_processor_info(mach_host_self(), PROCESSOR_CPU_LOAD_INFO,
&cpuCount, &info, &infoCount) == KERN_SUCCESS,
let info else { return nil }
defer { // 内核分配的内存必须归还
vm_deallocate(mach_task_self_, vm_address_t(bitPattern: info),
vm_size_t(Int(infoCount) * MemoryLayout<integer_t>.stride))
}
// …读取每核 user/system/nice/idle,与上一次做差值…
}
}
```
### 7.3 调度器(分级采样)
```swift
actor MetricsHub {
private var cpu = CPUSampler()
private var mem = MemorySampler()
private var net = NetworkSampler()
private var tier: SampleTier = .menuBar
private var loop: Task<Void, Never>?
func setTier(_ t: SampleTier) { tier = t }
func start(onUpdate: @escaping @MainActor @Sendable (CPULoad?, MemoryUsage?, NetRate?) -> Void) {
loop?.cancel()
loop = Task {
while !Task.isCancelled {
let c = cpu.sample(), n = net.sample()
let m = tier == .popover ? mem.sample() : nil // 面板关闭时不读细节
await onUpdate(c, m, n)
try? await Task.sleep(for: tier == .popover ? .seconds(1) : .seconds(2),
tolerance: .milliseconds(500))
}
}
}
}
```
### 7.4 清理执行出口
```swift
actor CleanEngine {
func execute(_ items: [CleanItem], mode: DeleteMode) async -> CleanReport {
var report = CleanReport()
for item in items {
do {
try SafetyGuard.validate(item.url) // 执行前再校验一次
if try await RunningAppProbe.isInUse(item) { report.skipped(item, .inUse); continue }
switch mode {
case .trash: try FileManager.default.trashItem(at: item.url, resultingItemURL: nil)
case .delete: try FileManager.default.removeItem(at: item.url)
}
report.cleaned(item)
} catch {
report.failed(item, error)
}
OperationLog.append(item, result: report.last)
}
return report
}
}
```
---
## 8. 里程碑
| 周 | 交付 |
|---|---|
| 第 1 周 | 工程搭建(App + 本地 Package)、`NSStatusItem` 自绘 CPU、`MetricsHub` 分级调度、锁屏暂停 |
| 第 2 周 | 弹出面板:CPU 每核(P/E)、内存、网速、Top 进程;Swift Charts 历史曲线;设置页、开机自启 |
| 第 3 周 | 清理引擎:`SafetyGuard` + 单测、规则表(系统 / 浏览器 / 下载 / Xcode)、扫描与预览 UI、废纸篓执行、操作日志 |
| 第 4 周 | FDA 权限引导、白名单、Energy Log 功耗回归、签名 + 公证 + Sparkle + DMG 打包,发布 0.1 |
| 第 5–6 周 | V1:GPU / 温度 / 功耗(IOHID + IOReport 桥接)、电池、磁盘、开发者缓存与项目产物清理、应用卸载 |
| 之后 | V2:特权 helper(DNS、系统日志)、磁盘占用可视化、小组件 |
---
## 9. 风险
| 风险 | 应对 |
|---|---|
| 误删用户数据 | 默认只移到废纸篓;review 类默认不勾选;单一删除出口 + 单测;操作日志可追溯 |
| 私有 API(IOHID 温度、IOReport)随系统更新失效 | 封装在 `SMCBridge` 内,读取失败时隐藏对应模块而不是崩溃;每个 macOS beta 回归 |
| 自身耗电过高 | §4 功耗预算作为发布门槛,CI 外手动跑 Energy Log |
| 许可证 | Stats 代码保留 MIT 声明并写入 `ThirdPartyNotices.md`;Mole 只参考不复制 |
| 公证被拒 | 开启 Hardened Runtime;私有框架用 `dlopen` 动态加载;不申请不必要的 entitlement |