- CPU:温度 / 占用 / 负载三个圆环,负载历史面积图,核心按类型分色,各类核心平均占用与 1 / 5 / 15 分钟平均负载 - 内存:压力仪表与按 App / 联动 / 压缩分段的圆环,补充已用、可用与交换区 - 进程右键或进程页星形按钮调用系统自带的本机大模型解释进程,附带路径、所属应用与签名方,不联网 - 进程显示本地化应用名,详情弹窗进程列表 8 行;修复截图模式下短页面卡片被拉高 - 更新 README 截图与说明,更新日志同步到官网
187 lines
8.1 KiB
Swift
187 lines
8.1 KiB
Swift
import Foundation
|
||
import Metrics
|
||
import Observation
|
||
import Security
|
||
#if canImport(FoundationModels)
|
||
import FoundationModels
|
||
#endif
|
||
|
||
/// 用 Apple 智能(系统自带的本机大模型)解释一个进程是做什么的。
|
||
/// 全程在本机运行,不联网;需要 macOS 26 且开启了 Apple 智能。
|
||
@MainActor
|
||
@Observable
|
||
public final class ProcessExplainer {
|
||
public struct Subject: Equatable, Sendable {
|
||
let pid: Int32
|
||
let name: String
|
||
let displayName: String
|
||
let executablePath: String?
|
||
let appBundlePath: String?
|
||
let cpu: Double?
|
||
let memory: UInt64?
|
||
let networkRate: Double?
|
||
}
|
||
|
||
public enum Phase: Equatable {
|
||
case idle
|
||
case running
|
||
case done
|
||
case failed(String)
|
||
}
|
||
|
||
public private(set) var subject: Subject?
|
||
public private(set) var text = ""
|
||
public private(set) var phase: Phase = .idle
|
||
|
||
@ObservationIgnored private var task: Task<Void, Never>?
|
||
|
||
/// 系统版本与框架是否支持;是否真正可用(设备、开关、模型下载)在运行时再判断
|
||
static var isSupported: Bool {
|
||
#if canImport(FoundationModels)
|
||
if #available(macOS 26.0, *) { return true }
|
||
#endif
|
||
return false
|
||
}
|
||
|
||
func explain(_ subject: Subject) {
|
||
task?.cancel()
|
||
self.subject = subject
|
||
text = ""
|
||
phase = .running
|
||
task = Task { [weak self] in
|
||
let facts = await Task.detached { Self.facts(for: subject) }.value
|
||
guard let self, !Task.isCancelled else { return }
|
||
await self.run(facts: facts)
|
||
}
|
||
}
|
||
|
||
func dismiss() {
|
||
task?.cancel()
|
||
task = nil
|
||
subject = nil
|
||
text = ""
|
||
phase = .idle
|
||
}
|
||
|
||
private func run(facts: String) async {
|
||
#if canImport(FoundationModels)
|
||
if #available(macOS 26.0, *) {
|
||
let model = SystemLanguageModel.default
|
||
if case .unavailable(let reason) = model.availability {
|
||
phase = .failed(Self.message(for: reason))
|
||
return
|
||
}
|
||
let session = LanguageModelSession(instructions: Self.instructions)
|
||
do {
|
||
for try await snapshot in session.streamResponse(to: facts) {
|
||
guard !Task.isCancelled else { return }
|
||
text = snapshot.content
|
||
}
|
||
phase = .done
|
||
} catch {
|
||
guard !Task.isCancelled else { return }
|
||
phase = .failed("Apple 智能没有给出回答:\(error.localizedDescription)")
|
||
}
|
||
return
|
||
}
|
||
#endif
|
||
phase = .failed("需要 macOS 26 及以上,并在系统设置中开启 Apple 智能。")
|
||
}
|
||
|
||
private static let instructions = """
|
||
你是熟悉 macOS 的系统工程师。用户会给出 Mac 上一个进程的事实信息(名称、路径、所属应用、签名方、资源占用)。
|
||
请只根据这些事实和你确定知道的知识,用简体中文回答三点,每点一两句:
|
||
1. 它是什么:属于哪个应用或系统组件,负责什么。
|
||
2. 占用是否正常:结合给出的 CPU、内存数值判断。
|
||
3. 能否退出:直接退出会有什么影响。
|
||
不确定时明确说“不确定”,不要编造功能或来源。不使用 Markdown 标题,总共不超过 180 字。
|
||
"""
|
||
|
||
#if canImport(FoundationModels)
|
||
@available(macOS 26.0, *)
|
||
private static func message(for reason: SystemLanguageModel.Availability.UnavailableReason) -> String {
|
||
switch reason {
|
||
case .deviceNotEligible: "这台 Mac 不支持 Apple 智能。"
|
||
case .appleIntelligenceNotEnabled: "请先在“系统设置 → Apple 智能与 Siri”中开启 Apple 智能。"
|
||
case .modelNotReady: "Apple 智能模型还在下载或准备中,请稍后再试。"
|
||
@unknown default: "Apple 智能暂时不可用。"
|
||
}
|
||
}
|
||
#endif
|
||
|
||
// MARK: 事实
|
||
|
||
/// 交给模型的事实:路径位置、所属应用、签名方都能明显减少模型胡猜
|
||
nonisolated static func facts(for subject: Subject) -> String {
|
||
var lines = ["进程名:\(subject.name)"]
|
||
if subject.displayName != subject.name { lines.append("显示名称:\(subject.displayName)") }
|
||
let path = subject.executablePath ?? executablePath(pid: subject.pid)
|
||
if let path {
|
||
lines.append("可执行文件:\(path)")
|
||
lines.append("位置:\(location(of: path))")
|
||
if let signer = signer(of: path) { lines.append("签名方:\(signer)") }
|
||
}
|
||
if let bundlePath = subject.appBundlePath, let bundle = Bundle(path: bundlePath) {
|
||
let name = bundle.object(forInfoDictionaryKey: "CFBundleDisplayName") as? String
|
||
?? bundle.object(forInfoDictionaryKey: "CFBundleName") as? String
|
||
?? URL(fileURLWithPath: bundlePath).deletingPathExtension().lastPathComponent
|
||
lines.append("所属应用:\(name)(\(bundle.bundleIdentifier ?? "无 Bundle ID"))")
|
||
}
|
||
if let cpu = subject.cpu { lines.append("CPU:\(Format.percent(cpu))(以单核满载为 100%)") }
|
||
if let memory = subject.memory { lines.append("内存:\(Format.bytes(memory))") }
|
||
if let rate = subject.networkRate { lines.append("网络:\(Format.menuBarRate(rate))") }
|
||
return lines.joined(separator: "\n")
|
||
}
|
||
|
||
nonisolated private static func location(of path: String) -> String {
|
||
if path.hasPrefix("/System/") || path.hasPrefix("/usr/") || path.hasPrefix("/sbin/") || path.hasPrefix("/bin/") {
|
||
return "macOS 系统目录(系统组件)"
|
||
}
|
||
if path.hasPrefix("/Applications/") || path.contains("/Applications/") { return "应用程序文件夹" }
|
||
if path.hasPrefix("/Library/") { return "系统级资源库(第三方驱动、辅助工具或后台服务)" }
|
||
if path.contains("/Library/") { return "用户资源库" }
|
||
return "其他位置"
|
||
}
|
||
|
||
nonisolated private static func signer(of path: String) -> String? {
|
||
var code: SecStaticCode?
|
||
guard SecStaticCodeCreateWithPath(URL(fileURLWithPath: path) as CFURL, [], &code) == errSecSuccess, let code else { return nil }
|
||
var requirement: SecRequirement?
|
||
if SecRequirementCreateWithString("anchor apple" as CFString, [], &requirement) == errSecSuccess, let requirement,
|
||
SecStaticCodeCheckValidity(code, [], requirement) == errSecSuccess {
|
||
return "Apple(系统自带)"
|
||
}
|
||
var information: CFDictionary?
|
||
guard SecCodeCopySigningInformation(code, SecCSFlags(rawValue: kSecCSSigningInformation), &information) == errSecSuccess,
|
||
let dictionary = information as? [String: Any] else { return nil }
|
||
if let team = dictionary[kSecCodeInfoTeamIdentifier as String] as? String {
|
||
let certificates = dictionary[kSecCodeInfoCertificates as String] as? [SecCertificate]
|
||
let summary = certificates?.first.flatMap { SecCertificateCopySubjectSummary($0) as String? }
|
||
return [summary, "团队 \(team)"].compactMap { $0 }.joined(separator: ",")
|
||
}
|
||
return "未签名或临时签名"
|
||
}
|
||
|
||
nonisolated private static func executablePath(pid: Int32) -> String? {
|
||
var buffer = [CChar](repeating: 0, count: 4 * Int(MAXPATHLEN))
|
||
guard proc_pidpath(pid, &buffer, UInt32(buffer.count)) > 0 else { return nil }
|
||
return String(cString: buffer)
|
||
}
|
||
}
|
||
|
||
extension ProcessExplainer.Subject {
|
||
@MainActor
|
||
init(_ process: ProcessUsage) {
|
||
self.init(pid: process.pid, name: process.name, displayName: process.displayName,
|
||
executablePath: process.executablePath, appBundlePath: process.appBundlePath,
|
||
cpu: process.cpu, memory: process.memory, networkRate: nil)
|
||
}
|
||
|
||
@MainActor
|
||
init(_ process: NetworkProcessUsage) {
|
||
self.init(pid: process.pid, name: process.name, displayName: process.localizedName,
|
||
executablePath: nil, appBundlePath: process.appBundlePath,
|
||
cpu: nil, memory: nil, networkRate: process.download + process.upload)
|
||
}
|
||
}
|