凌晨三点的沉默:OpenClaw 定时重启踩坑记
凌晨三点,一台 Mac mini 准时重启。然后,它沉默了。
SSH 不通,飞书消息石沉大海,OpenClaw Gateway 像从未存在过。直到第二天早上走到机器前,输入密码,一切才若无其事地恢复运转——仿佛那几个小时的失联只是一场梦。
这件事重复了好几天,才引起警觉。排查下来,发现是三个独立的疏忽,恰好叠加成了一场完美的故障。
背景
OpenClaw 是一个多通道 AI 助手平台,通过 WebSocket Gateway 将消息路由到飞书等通信渠道。Gateway 作为 macOS LaunchAgent 常驻运行。
长时间运行后 swap 膨胀是 macOS 的老问题,于是配置了凌晨 3:00 的定时重启任务,流程很简单:
定时调度 → night-cleanup.sh(关闭非白名单 GUI 应用)→ nightly-reboot.sh(等待 + 重启)
简单的东西,往往坏得最彻底。
故障现象
凌晨 3:00 定时重启执行后:
- SSH 远程登录完全不可用
- OpenClaw Gateway 未自动启动,飞书通道断连
- 必须物理接触机器、手动输入密码登录后,一切才恢复正常
排查:层层剥开的洋葱
第一层:$HOME 指向了虚空
定时重启任务最初以 LaunchDaemon(系统级)部署,由 root 身份执行脚本。
脚本中有这样一行:
| |
root 用户的 $HOME 是 /var/root(或 /),不是普通用户的主目录。于是 mkdir -p 和日志写入全部失败:
mkdir: /.openclaw: Read-only file system
所有日志丢失。但讽刺的是,这个 bug 不影响重启本身——shutdown -r now 照样执行了。机器忠实地重启了,只是没人知道发生了什么。
第二层:FileVault 把整台机器锁在了门外
这是最关键的一层。系统开启了 FileVault 全磁盘加密。
FileVault 的工作机制:磁盘在关机状态下是加密的,macOS 启动时需要在 PreBoot 阶段输入密码解锁磁盘,之后操作系统才能真正加载。
凌晨重启后,系统卡在 FileVault 解锁界面:
重启 → FileVault 等待密码 → 整个 macOS 未启动 → SSH 不存在 → LaunchAgent 不存在
不仅 OpenClaw Gateway 无法加载,连 SSH 都无法工作——操作系统本身尚未完成启动。机器在凌晨三点醒来,发现自己被锁在门外,于是安静地等在那里,等一个不会来的人。
第三层:即使没有 FileVault,手动登录也会阻塞 LaunchAgent
假设关闭 FileVault,macOS 能完成启动。但如果没有配置自动登录,系统停在登录界面时:
| 服务类型 | 是否可用 |
|---|---|
| LaunchDaemon(系统级,如 SSH) | 可用 |
| LaunchAgent(用户级,如 OpenClaw Gateway) | 不可用,需用户登录后才加载 |
这是 macOS launchd 的设计:LaunchAgent 绑定用户会话,登录界面没有用户会话。
第四层:shutdown -r now 的粗暴
shutdown -r now 虽然会发送 SIGTERM 通知进程退出,但不会走 macOS 的标准 GUI 关机流程(通知应用保存状态、等待应用响应等)。对于 GUI 密集的 macOS 环境,更稳妥的方式是通过 AppleScript 调用系统事件,等同于用户点击"苹果菜单 → 重新启动"。
修复方案
四项修复,协同生效。
1. 关闭 FileVault,开启自动登录
系统设置 → 隐私与安全性 → FileVault → 关闭
系统设置 → 用户与群组 → 自动登录 → 选择账户
关闭 FileVault 后磁盘无需解锁即可启动;自动登录确保用户会话立即建立,LaunchAgent 随之加载。
取舍:放弃磁盘加密和登录密码保护。适用于物理安全可控的环境(如家中服务器)。随身携带的笔记本不建议此方案。
2. LaunchDaemon 改为 LaunchAgent
将 plist 从 /Library/LaunchDaemons/(root 执行)迁移到 ~/Library/LaunchAgents/(用户执行),一举解决两个问题:
$HOME自然指向用户目录,日志路径正确osascript可以访问当前用户的 GUI 会话,AppleScript 重启命令能正常工作
迁移后的 plist:
| |
迁移命令:
| |
3. 优雅重启替代 shutdown -r now
将重启命令从:
| |
改为:
| |
sync 确保文件系统缓冲区写入磁盘;AppleScript 的 restart 走 macOS 标准关机流程,依次通知每个应用退出,等同于用户手动操作。
4. 修复日志文件权限
从 LaunchDaemon 切换到 LaunchAgent 后,原来由 root 创建的日志文件 owner 仍然是 root,用户身份的脚本无法写入。直接删除重建即可:
| |
修复后的完整流程
凌晨 3:00
└─ LaunchAgent 触发 nightly-reboot.sh(用户身份)
├─ night-cleanup.sh 关闭非白名单 GUI 应用
├─ sleep 10(等待进行中的请求完成)
├─ sync(数据落盘)
└─ osascript 优雅重启
└─ macOS 标准关机流程 → 重启
└─ 自动登录 → 用户会话建立
├─ LaunchAgent 加载 OpenClaw Gateway
└─ SSH 服务就绪
LaunchDaemon vs LaunchAgent 选型
| 维度 | LaunchDaemon | LaunchAgent |
|---|---|---|
| 执行身份 | root | 当前登录用户 |
$HOME | /var/root 或 / | 用户主目录 |
| GUI 访问 | 无(无法使用 osascript) | 有 |
| 生命周期 | 系统启动即加载 | 用户登录后加载 |
| 适用场景 | 网络服务、系统守护进程 | 用户级应用、需要 GUI 交互的任务 |
如果脚本需要访问用户目录、操作 GUI 应用、或使用 osascript,必须用 LaunchAgent。如果需要在无人登录时也运行,才用 LaunchDaemon——但要硬编码路径,不能依赖 $HOME。
三个独立的小疏忽——一个环境变量、一个加密开关、一条重启命令——叠加在一起,制造了一个每天准时上演的沉默事故。没有报错,没有告警,只是安静地不工作。这类故障最难发现,因为系统没有抱怨,它只是什么都没做。
调试的本质,大概就是学会倾听沉默。