关于 macbook-desktop-mode
MacBook Desktop Mode 是一份将 MacBook 配置为长期接通电源桌面工作站的指南,面向处理 USB 设备休眠唤醒断连和电池循环问题的用户。内容涵盖电源设置、供电 USB 集线器及音频设备监控守护进程。
主要功能
- 01诊断 USB 设备休眠唤醒故障
- 02配置接通电源时的睡眠设置
- 03介绍 80% 充电上限
- 04通过 uhubctl 重置 USB 端口
- 05部署音频设备监控与恢复守护进程
适用场景
- 01将 MacBook 配置为常驻桌面工作站
- 02排查麦克风或音频设备断连
- 03减少接电使用时的电池循环
- 04设置 USB 端口自动恢复
安装
复制下面的命令,在终端中运行。
npx skills add https:/ / github.com/ terrylica/ cc-skills --skill macbook-desktop-mode安装后在 Claude Code 中输入/macbook-desktop-mode确认已加载。
附带文件references/evolution-log.md
详细介绍
MacBook 桌面模式
将 MacBook 配置为始终连接电源的桌面工作站的综合指南。解决两个相互关联的问题:USB 设备(尤其是 USB 1.1 音频设备)在睡眠/唤醒周期后消失,以及持续充电导致不必要的电池老化。
自我进化技能:此技能会在使用过程中不断改进。如果说明有误、参数发生偏移,或需要变通方案,请立即修复此文件,不要拖延。仅针对真实且可复现的问题进行更新。
何时使用此技能
- USB 麦克风或音频设备在睡眠/唤醒后消失
- 接通电源的 Mac 电池发生不必要的充放电循环
- 将 MacBook 设置为永久桌面工作站
- 配置
pmset,以便始终连接电源使用 - 使用
uhubctl设置带电源的 USB 集线器,以便通过软件控制 USB 重置 - 为 AudioDeviceMonitor Swift 守护进程增强自我修复能力
根因诊断框架
应用修复方案前,请先诊断具体的故障模式。此框架基于对一台始终连接电源的 Apple Silicon MacBook 的实证分析而开发。该设备不断从总线上掉线:一支无序列号的总线供电 USB 1.1 音频类麦克风。这是睡眠/唤醒过程中最脆弱的一类 USB 外设,因此也是这些修复方案针对的最坏情况。请使用下方的 ioreg 命令识别自己的设备,而不是通过供应商/产品 ID;此处无需使用这类 ID。
DarkWake 循环
macOS 会频繁执行 DarkWake(部分维护唤醒)循环,夜间通常每 15 分钟一次。在 DarkWake 期间:
- CPU 会为 Power Nap、网络保活和 Siri 唤醒
- USB 总线会部分供电,但设备不会被完整地重新枚举
- USB 1.1 全速设备不支持链路电源管理(LPM),无法协商平稳恢复
- 经过多次循环后,XHCI 控制器会将设备从 IO 注册表中移除
诊断命令——检查睡眠/唤醒历史记录:
pmset -g log | grep -E "Sleep |Wake |DarkWake" | tail -30
USB 1.1 设备的局限性
USB 1.1 设备(12 Mbps,USBSpeed = 1)在睡眠/唤醒过程中最为脆弱:
- 不支持链路电源管理协议
- 不支持 USB 选择性挂起协商
- 通常没有序列号(
iSerialNumber = 0),总线重置后难以可靠地重新识别设备
诊断命令——检查设备属性:
ioreg -r -c IOUSBHostDevice -l | grep -A 20 "DEVICE_NAME"
关键字段:USBSpeed(1=全速,2=高速,3=超高速)、iSerialNumber(0 = 无序列号)、IOPowerManagement.DriverPowerState。
USB 句柄冲突
Chrome 等应用会为 WebRTC/WebAudio 持有直接的 USB 用户端句柄(AppleUSBHostDeviceUserClient)。这些过期句柄会妨碍睡眠后正常重新初始化。
诊断命令——检查哪些进程持有 USB 句柄:
ioreg -r -c IOUSBHostDevice -l | grep -B5 "IOUserClientCreator"
电池微循环
未设置充电上限的常接电 Mac,其电池会在 ML 预测电量和实际电量之间循环。DarkWake 循环会消耗电力,导致反复出现充放电微循环。
诊断命令——检查每日充电范围:
ioreg -r -c AppleSmartBattery -l | grep -E "DailyMinSoc|DailyMaxSoc|Temperature|CycleCount"
Temperature的单位是百分之一度(除以 100 即为摄氏度)DailyMinSoc/DailyMaxSoc显示今天的电量波动范围
阶段 1:桌面模式的电源配置
1.1 将充电上限设为 80%
系统设置 → 电池 → 充电优化 →“限制为 80%”
在 Apple Silicon 设备上,达到上限后,Mac 会进入电源适配器旁路模式——电力会直接从充电器流向系统主板。电池在电气上保持断开,从而避免日历老化和循环老化。
1.2 在接通交流电时将 sleep 设为 0
对于始终接通交流电的桌面设备,系统睡眠弊大于利:
# Disable system sleep on AC only (battery settings unchanged)
sudo pmset -c sleep 0
# Verify
pmset -g custom | grep -A1 "AC Power" | grep sleep
这样可以保留:
- 显示器仍可睡眠(接通交流电时为
displaysleep=10) - 电池睡眠设置不变(外出时为
sleep=1) - 仍会遵循
ttyskeepawake=1
这样可以消除:
- DarkWake 循环(USB 断连的根本原因)
- 维护唤醒期间电池的微循环
- 热循环(反复经历冷却↔升温)
**代价:**Apple Silicon 空闲时约消耗 5-8W。风扇不会转动。每年电费约 5-8 美元。
1.3 验证电源配置
# Full power settings
pmset -g custom
# Battery health snapshot
ioreg -r -c AppleSmartBattery -l | grep -E "Temperature|CycleCount|DailyMinSoc|DailyMaxSoc|MaxCapacity|IsCharging"
# Active power assertions
pmset -g assertions
阶段 2:硬件层——带电源的 USB 集线器
2.1 为什么要使用带电源的集线器
带电源的 USB 集线器可以建立USB 会话边界。Mac 的 XHCI 控制器与集线器(带序列号的可靠 USB 2.0+ 设备)保持会话。集线器则独立与连接的设备保持会话。睡眠/唤醒只会对 Mac↔集线器链路造成压力。
此外,集线器还支持通过 uhubctl 以软件方式重置 USB 端口——相当于不接触硬件即可物理拔插设备。
2.2 集线器选择标准
要求:
- 外接电源供电(非总线供电)——必须有自己的电源
- 与 uhubctl 兼容的芯片组——支持逐端口电源切换
- 带序列号的 USB 2.0+ 集线器,以便可靠地重新识别
与 uhubctl 兼容的芯片组(来自 uhubctl 项目):
- VIA VL805/VL812/VL817
- Realtek RTS5411
- Genesys Logic GL850G/GL3510
2.3 设置 uhubctl
# Install
brew install uhubctl
# List compatible hubs (must have powered hub connected)
uhubctl
# Cycle all ports (2-second off period)
uhubctl -a cycle -d 2
# Cycle specific port on specific hub
uhubctl -a cycle -p 1 -l 1-1 -d 2
阶段 3:软件层——增强 AudioDeviceMonitor
参考实现是一个 Swift 守护进程(AudioDeviceMonitorRunner.swift),作为 macOS launchd KeepAlive Agent 部署。它结合了:
- 优先级强制执行(原始功能)——将默认输入/输出设备设为优先级最高的可用设备
- 设备守护程序(v2)——跟踪状态机、检测设备消失并执行恢复流程
- 唤醒检测——通过
IORegisterForSystemPower接收 IOKit 电源通知 - 心跳检测——每 60 秒定期检查,检测无提示的断连
- 恢复流程——uhubctl 端口循环 → 重试 → Telegram 通知
3.1 状态机
┌──────────┐
┌────────▶│ PRESENT │◀──────────────┐
│ └────┬─────┘ │
│ │ device gone │
│ ▼ │
│ ┌──────────────┐ │
│ │ DISAPPEARED │ │
│ │ (3s debounce)│ │
│ └──────┬───────┘ │
│ │ still missing │
│ ▼ │
│ ┌──────────────┐ found │
│ │ RECOVERING │───────────────┘
│ │ uhubctl cycle│
│ └──────┬───────┘
│ │ max attempts
│ ▼
│ ┌──────────────┐
│ │ DEAD │
└─────│ (Telegram) │
replug└──────────────┘
3.2 关键架构决策
- 使用 IOKit 电源回调而非
NSWorkspace——不依赖 AppKit,可用于无头 launchd 守护进程 - 手动定义 IOKit 消息常量——Swift 无法导入
iokit_common_msg()C 宏;值由sys_iokit (0xe0000000) | message_code计算得出 - 同步调用 uhubctl/curl——可以接受,因为阻塞期间主 RunLoop 会将 CoreAudio 回调排队;不会丢失事件,只会因恢复过程而延迟处理
- 通过 curl 子进程调用 Telegram——不依赖库;凭据在启动时从 dotenv 文件加载
- 通过主队列实现单线程运行——所有 CoreAudio 回调、心跳和电源通知都会分发到
.main;无需加锁
3.3 构建和部署
该守护进程是一个单文件 Swift 程序,可编译为独立二进制文件:
# Compile
swiftc -O -framework CoreAudio -framework IOKit \
-o /path/to/output/audio-device-monitor \
AudioDeviceMonitorRunner.swift
# Codesign for launchd (ad-hoc, no Apple Developer account needed)
codesign -s - -f -i com.yourorg.audio-device-monitor /path/to/output/audio-device-monitor
作为 launchd Agent 部署(用户级,KeepAlive):
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
<plist version="1.0">
<dict>
<key>Label</key>
<string>com.yourorg.audio-device-monitor</string>
<key>ProgramArguments</key>
<array>
<string>/path/to/output/audio-device-monitor</string>
</array>
<key>KeepAlive</key>
<true/>
<key>RunAtLoad</key>
<true/>
<key>ProcessType</key>
<string>Background</string>
<key>EnvironmentVariables</key>
<dict>
<key>HOME</key>
<string>/Users/yourusername</string>
</dict>
<key>StandardOutPath</key>
<string>/path/to/logs/audio-device-monitor-stdout.log</string>
<key>StandardErrorPath</key>
<string>/path/to/logs/audio-device-monitor-stderr.log</string>
</dict>
</plist>
加载:launchctl load ~/Library/LaunchAgents/com.yourorg.audio-device-monitor.plist
3.4 配置(在 Swift 源代码中)
优先级列表和受监控设备在 Swift 源代码顶部定义为常量:
inputPriorities/outputPriorities——按优先级从高到低排列的数组guardedDevices——用于监控设备消失情况的GuardedDevice(name:, isInput:)数组- 调优常量:
heartbeatInterval(60s)、disappearDebounce(3s)、maxRecoveryAttempts(3) - 通知凭据:从 dotenv 文件加载(路径在
loadCredentials()中配置)
更改后,重新编译并重启 launchd Agent。
阶段 4:验证和监控
4.1 电池健康监控
# Quick battery snapshot
ioreg -r -c AppleSmartBattery -l | grep -E "Temperature|CycleCount|DailyMinSoc|DailyMaxSoc|MaxCapacity"
# Full power state
pmset -g batt
system_profiler SPPowerDataType | grep -E "Cycle|Condition|Maximum|Charging"
在设置 80% 充电上限且接入电源时设置 sleep=0 后:
DailyMinSoc和DailyMaxSoc应趋于一致(不再波动)- 稳定状态下,
Temperature应低于 3500(35°C) CycleCount的累积速度应大幅减慢
4.2 USB 设备健康状况
# Current USB device tree
system_profiler SPUSBDataType
# IOKit details for a specific device
ioreg -r -c IOUSBHostDevice -l | grep -A 25 "DEVICE_NAME"
# Kernel USB assertions (shows which devices prevent sleep)
pmset -g assertions | grep USB
4.3 音频监控日志
# Tail live logs (adjust path to your log location)
tail -50 /path/to/logs/audio-device-monitor-stderr.log
# Key log patterns to watch for:
# "Starting v2 — device guardian mode" → v2 features active
# "System woke — checking devices" → wake detection working
# "ALERT: '<device>' disappeared" → device dropout detected
# "recovered via uhubctl!" → automated recovery succeeded
# "recovery FAILED" → manual replug needed
反模式
| 反模式 | 失败原因 | 正确做法 |
|---|---|---|
将 powernap=0 以减少 DarkWake | 以牺牲 USB 稳定性为代价,导致错过 iCloud 同步、「查找」等功能 | 接入电源时设置 sleep=0,彻底消除 DarkWake |
| 停用优化电池充电 | 允许电池充至 100%,不利于延长电池寿命 | 明确设置 80% 充电上限 |
| 结束 Chrome 进程以释放 USB 句柄 | 治标不治本;任何 WebRTC 应用都可以占用句柄 | 使用带电源的集线器创建会话边界 |
轮询 system_profiler 获取 USB 状态 | 子进程开销大,延迟为 2-3 秒 | CoreAudio 属性侦听器可即时响应 |
使用 NSWorkspace.didWakeNotification | 需要 AppKit/NSApplication,在 launchd 守护进程中无法工作 | 使用 IORegisterForSystemPower 回调 |
| 设备消失后通过 IOKit 重置 USB | 设备已从 IO 注册表中移除,无法重置 | uhubctl 对物理端口执行断电再上电 |
另请参阅
执行后反思
此技能完成后,请在结束任务前进行反思:
- 确认所在位置。 — 找到此 SKILL.md 的规范路径,然后再进行编辑。
- 哪些地方失败了? — 修复导致问题的指令。
- 哪些做法的效果超出预期? — 将其列为推荐实践。
- 哪些内容已偏离实际情况? — 修复不再符合实际情况的脚本、参考资料或依赖项。
- 记录下来。 — 在演进日志中记录触发原因、修复内容和证据。
不要延后处理。下一次调用将继承你留下的一切。