凌晨三点的沉默: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 身份执行脚本。

脚本中有这样一行:

1
LOG="$HOME/.openclaw/logs/nightly-reboot.log"

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:

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
<?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.openclaw.nightly-reboot</string>
    <key>ProgramArguments</key>
    <array>
        <string>~/.openclaw/scripts/nightly-reboot.sh</string>
    </array>
    <key>StartCalendarInterval</key>
    <dict>
        <key>Hour</key>
        <integer>3</integer>
        <key>Minute</key>
        <integer>0</integer>
    </dict>
    <key>StandardOutPath</key>
    <string>~/.openclaw/logs/nightly-reboot.log</string>
    <key>StandardErrorPath</key>
    <string>~/.openclaw/logs/nightly-reboot.log</string>
</dict>
</plist>

迁移命令:

1
2
3
4
5
6
# 卸载旧的系统级任务
sudo launchctl bootout system/com.openclaw.nightly-reboot
sudo rm /Library/LaunchDaemons/com.openclaw.nightly-reboot.plist

# 加载新的用户级任务
launchctl bootstrap gui/$(id -u) ~/Library/LaunchAgents/com.openclaw.nightly-reboot.plist

3. 优雅重启替代 shutdown -r now

将重启命令从:

1
/sbin/shutdown -r now

改为:

1
2
sync
osascript -e 'tell application "System Events" to restart'

sync 确保文件系统缓冲区写入磁盘;AppleScript 的 restart 走 macOS 标准关机流程,依次通知每个应用退出,等同于用户手动操作。

4. 修复日志文件权限

从 LaunchDaemon 切换到 LaunchAgent 后,原来由 root 创建的日志文件 owner 仍然是 root,用户身份的脚本无法写入。直接删除重建即可:

1
2
rm ~/.openclaw/logs/nightly-reboot.log
touch ~/.openclaw/logs/nightly-reboot.log

修复后的完整流程

凌晨 3:00
  └─ LaunchAgent 触发 nightly-reboot.sh(用户身份)
       ├─ night-cleanup.sh 关闭非白名单 GUI 应用
       ├─ sleep 10(等待进行中的请求完成)
       ├─ sync(数据落盘)
       └─ osascript 优雅重启
            └─ macOS 标准关机流程 → 重启
                 └─ 自动登录 → 用户会话建立
                      ├─ LaunchAgent 加载 OpenClaw Gateway
                      └─ SSH 服务就绪

LaunchDaemon vs LaunchAgent 选型

维度LaunchDaemonLaunchAgent
执行身份root当前登录用户
$HOME/var/root/用户主目录
GUI 访问无(无法使用 osascript)
生命周期系统启动即加载用户登录后加载
适用场景网络服务、系统守护进程用户级应用、需要 GUI 交互的任务

如果脚本需要访问用户目录、操作 GUI 应用、或使用 osascript,必须用 LaunchAgent。如果需要在无人登录时也运行,才用 LaunchDaemon——但要硬编码路径,不能依赖 $HOME


三个独立的小疏忽——一个环境变量、一个加密开关、一条重启命令——叠加在一起,制造了一个每天准时上演的沉默事故。没有报错,没有告警,只是安静地不工作。这类故障最难发现,因为系统没有抱怨,它只是什么都没做。

调试的本质,大概就是学会倾听沉默。