mcpField
M
macbook-desktop-mode

macbook-desktop-mode

github.com/terrylica其他164 次安装1 天前更新
代码仓库

关于 macbook-desktop-mode

MacBook Desktop Mode 是一份将 MacBook 配置为长期接通电源桌面工作站的指南,面向处理 USB 设备休眠唤醒断连和电池循环问题的用户。内容涵盖电源设置、供电 USB 集线器及音频设备监控守护进程。

主要功能

  1. 01诊断 USB 设备休眠唤醒故障
  2. 02配置接通电源时的睡眠设置
  3. 03介绍 80% 充电上限
  4. 04通过 uhubctl 重置 USB 端口
  5. 05部署音频设备监控与恢复守护进程

适用场景

  1. 01将 MacBook 配置为常驻桌面工作站
  2. 02排查麦克风或音频设备断连
  3. 03减少接电使用时的电池循环
  4. 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 部署。它结合了:

  1. 优先级强制执行(原始功能)——将默认输入/输出设备设为优先级最高的可用设备
  2. 设备守护程序(v2)——跟踪状态机、检测设备消失并执行恢复流程
  3. 唤醒检测——通过 IORegisterForSystemPower 接收 IOKit 电源通知
  4. 心跳检测——每 60 秒定期检查,检测无提示的断连
  5. 恢复流程——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 源代码顶部定义为常量:

  1. inputPriorities / outputPriorities——按优先级从高到低排列的数组
  2. guardedDevices——用于监控设备消失情况的 GuardedDevice(name:, isInput:) 数组
  3. 调优常量:heartbeatInterval(60s)、disappearDebounce(3s)、maxRecoveryAttempts(3)
  4. 通知凭据:从 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 对物理端口执行断电再上电

另请参阅

执行后反思

此技能完成后,请在结束任务前进行反思:

  1. 确认所在位置。 — 找到此 SKILL.md 的规范路径,然后再进行编辑。
  2. 哪些地方失败了? — 修复导致问题的指令。
  3. 哪些做法的效果超出预期? — 将其列为推荐实践。
  4. 哪些内容已偏离实际情况? — 修复不再符合实际情况的脚本、参考资料或依赖项。
  5. 记录下来。 — 在演进日志中记录触发原因、修复内容和证据。

不要延后处理。下一次调用将继承你留下的一切。

评论

支持 Markdown