apple / scripts /openclaw-post-restore.sh
GGSheng's picture
feat: deploy to hf space | model=claude-72604b
62f6d9a verified
Raw
History Blame Contribute Delete
23.2 kB
#!/usr/bin/env bash
#
# openclaw-post-restore.sh
#
# Container-startup hook invoked by openclaw-entrypoint.sh right
# after the backup restore + config-write steps. Anything that
# needs to run with a freshly-restored state should live here, not
# in openclaw-restore.sh (which is generic data) and not in
# openclaw-entrypoint.sh (which is process management).
#
# Why a dedicated script?
# - Keeps openclaw-restore.sh generic: the restore script only
# deals with data; it has no business knowing about openclaw.
# - Keeps openclaw-entrypoint.sh focused on orchestration: the
# entrypoint calls this hook, but the body of "what to do
# after restore" lives in its own file, easy to swap or
# extend without touching the entrypoint.
# - Lets operators customize per-deployment (different fleets
# can drop their own version in /usr/local/bin/) without
# forking the rest of the repo.
#
# Default behavior (good baseline, easy to override):
# 1. Verify openclaw CLI is reachable
# 2. Verify openclaw --version works
# 3. Verify `openclaw channels list --json` returns valid JSON
# 4. Log results
#
# Customize by editing the run_check_* functions below or appending
# your own. The entrypoint's run_step wrapper will pick up any new
# command you add to the do_all_checks chain.
#
# Idempotent — safe to run on every container start, even if no
# restore actually happened (the checks are read-only and cheap).
#
# Environment variables:
# OPENCLAW_POST_RESTORE_ENABLED default: false (opt-in)
# set to "true" to actually run
# any of the operations below.
# This script makes outbound
# calls (npm update, plugin
# install/uninstall, doctor
# --fix) and is intentionally
# NOT enabled by default —
# operators must turn it on
# per deployment.
# OPENCLAW_POST_RESTORE_FAIL_ON_ERROR default: false
# if "true", any check failure
# returns exit 1, which
# propagates through the
# entrypoint's run_step and
# aborts startup
# OPENCLAW_BIN default: /usr/local/bin/openclaw
#
# OpenClaw 2026.6.x compatibility notes (as of 2026.6.1):
# * `openclaw channels list --json` layout changed:
# - 2026.6.x: top-level key per channel (e.g. ".feishu",
# ".qqbot") with account-centric installed/configured/enabled.
# Auth providers + model usage were moved out of channels list
# (use `openclaw models auth list` / `openclaw status`).
# - <2026.6: nested under ".chat.<name>" with channel-level
# .installed + .accounts list.
# The health probes below accept ALL three known layouts
# (top-level / .chat.<name> / .channels[] array) so the script
# works on either generation.
# * `openclaw plugins install` source prefixes were introduced
# (clawhub:, npm:, git:, npm-pack:). Bare scoped specs like
# `@openclaw/qqbot@latest` still resolve to the bundled copy.
# The reinstall flow uses `@openclaw/qqbot@latest` for a
# deterministic latest-version pin.
# * `openclaw plugins update --all` is unchanged and still safe to
# invoke on every container start.
# * `openclaw doctor --fix` gained a disk space probe and now
# produces more stable JSON output post-upgrade.
set -uo pipefail
OPENCLAW_ENTRYPOINT_TAG="openclaw-post-restore"
timestamp_utc() {
date -u +"%Y-%m-%dT%H:%M:%SZ"
}
log_info() { printf '[%s] [INFO] [PID:%s] %s: %s\n' "$(timestamp_utc)" "$$" "$OPENCLAW_ENTRYPOINT_TAG" "$*"; }
log_warn() { printf '[%s] [WARN] [PID:%s] %s: %s\n' "$(timestamp_utc)" "$$" "$OPENCLAW_ENTRYPOINT_TAG" "$*" >&2; }
log_error() { printf '[%s] [ERROR] [PID:%s] %s: %s\n' "$(timestamp_utc)" "$$" "$OPENCLAW_ENTRYPOINT_TAG" "$*" >&2; }
is_true() {
local value
value="$(printf '%s' "${1:-}" | tr '[:upper:]' '[:lower:]')"
[[ "$value" == "1" || "$value" == "true" || "$value" == "yes" || "$value" == "on" ]]
}
# ---- OpenClaw 2026.6.x compatibility helpers ----
# OpenClaw 2026.6.1 changed the structure of `openclaw channels list --json`:
# - 2026.6.x: top-level key per channel (".feishu", ".qqbot"), with
# account-centric installed/configured/enabled status. The
# `installed` flag is now per-account, not per-channel.
# - <2026.6: nested under ".chat.<name>" with channel-level
# .installed + .accounts list.
# - Some builds also surface an array form ".channels[]".
# The helpers below probe the JSON once, cache it, and resolve the
# schema variant transparently so the rest of the script doesn't have
# to care which generation is installed.
OPENCLAW_DETECTED_VERSION=""
OPENCLAW_CHANNELS_JSON_CACHE=""
OPENCLAW_CHANNELS_JSON_LOADED=0
# Echo the OpenClaw version string (e.g. "2026.6.1") to stdout, or
# empty string if it cannot be determined. Result is cached in
# OPENCLAW_DETECTED_VERSION.
detect_openclaw_version() {
if [[ -n "$OPENCLAW_DETECTED_VERSION" ]]; then
printf '%s' "$OPENCLAW_DETECTED_VERSION"
return 0
fi
if ! command -v openclaw >/dev/null 2>&1; then
return 1
fi
local ver
ver=$(openclaw --version 2>/dev/null | head -1 | grep -oE '[0-9]+\.[0-9]+\.[0-9]+([._-][0-9A-Za-z.-]+)?' | head -1)
if [[ -z "$ver" ]]; then
return 1
fi
OPENCLAW_DETECTED_VERSION="$ver"
printf '%s' "$ver"
}
# True if the detected version is >= the given X.Y.Z threshold. Used
# to gate 2026.6.x-only behavior. Returns false when version can't
# be detected (be conservative).
openclaw_version_at_least() {
local threshold="$1"
local current
current=$(detect_openclaw_version 2>/dev/null) || return 1
# compare major.minor.patch numerically after stripping suffixes
local cur_major cur_minor cur_patch thr_major thr_minor thr_patch
cur_major=$(printf '%s' "$current" | cut -d. -f1)
cur_minor=$(printf '%s' "$current" | cut -d. -f2)
cur_patch=$(printf '%s' "$current" | cut -d. -f3 | grep -oE '^[0-9]+')
thr_major=$(printf '%s' "$threshold" | cut -d. -f1)
thr_minor=$(printf '%s' "$threshold" | cut -d. -f2)
thr_patch=$(printf '%s' "$threshold" | cut -d. -f3)
cur_major=${cur_major:-0}; cur_minor=${cur_minor:-0}; cur_patch=${cur_patch:-0}
thr_major=${thr_major:-0}; thr_minor=${thr_minor:-0}; thr_patch=${thr_patch:-0}
if (( cur_major > thr_major )); then return 0; fi
if (( cur_major < thr_major )); then return 1; fi
if (( cur_minor > thr_minor )); then return 0; fi
if (( cur_minor < thr_minor )); then return 1; fi
if (( cur_patch >= thr_patch )); then return 0; fi
return 1
}
# Load `openclaw channels list --json` once and cache it. On
# 2026.6.x the default view only shows configured accounts; pass
# --all=1 to also include bundled-but-unconfigured channels (needed
# for accurate "is the plugin on disk?" health checks).
fetch_channels_json() {
local with_all="${1:-}"
if [[ -n "$with_all" && -n "$OPENCLAW_CHANNELS_JSON_CACHE_ALL" ]]; then
printf '%s' "$OPENCLAW_CHANNELS_JSON_CACHE_ALL"
return 0
fi
if [[ -z "$with_all" && "$OPENCLAW_CHANNELS_JSON_LOADED" -eq 1 ]]; then
printf '%s' "$OPENCLAW_CHANNELS_JSON_CACHE"
return 0
fi
if ! command -v openclaw >/dev/null 2>&1; then
return 1
fi
local out=""
local args=(channels list --json)
if [[ -n "$with_all" ]] && openclaw_version_at_least 2026.6.0; then
args=(channels list --json --all)
fi
if ! out=$(openclaw "${args[@]}" 2>/dev/null); then
return 1
fi
if [[ -n "$with_all" ]]; then
OPENCLAW_CHANNELS_JSON_CACHE_ALL="$out"
else
OPENCLAW_CHANNELS_JSON_CACHE="$out"
OPENCLAW_CHANNELS_JSON_LOADED=1
fi
printf '%s' "$out"
}
# Resolve a single channel's entry from cached JSON, accepting all
# known layouts. Echoes the entry object (or empty string when
# absent). Recognized layouts:
# 1) 2026.6.x: { "<chan>": { installed, configured, enabled, accounts } }
# 2) legacy: { chat: { "<chan>": { installed, accounts } } }
# 3) array form: { channels: [ { name, installed, accounts } ] }
resolve_channel_entry() {
local channel_name="$1"
local json="$2"
if [[ -z "$json" ]] || ! command -v jq >/dev/null 2>&1; then
return 0
fi
printf '%s' "$json" | jq -c --arg chan "$channel_name" '
( .[$chan] // null ) as $top
| ( .chat[$chan] // null ) as $chatted
| ( (.channels // []) | map(select(.name == $chan)) | first // null ) as $listed
| ( $top // $chatted // $listed // null )
' 2>/dev/null
}
# Map a resolved channel entry to a health string. Echoes one of:
# missing - entry not present in channels list
# not_installed - entry present but no installed flag/account
# no_accounts - installed but no ready accounts
# healthy - installed and at least one ready account
#
# "Ready account" = an account whose per-account `installed` flag is
# true (2026.6.x) or, in legacy form, an entry in `.accounts`.
channel_health_from_entry() {
local entry_json="$1"
if [[ -z "$entry_json" || "$entry_json" == "null" ]]; then
printf 'missing'
return 0
fi
if ! command -v jq >/dev/null 2>&1; then
# Without jq we can't be precise; assume healthy to avoid churn.
printf 'healthy'
return 0
fi
printf '%s' "$entry_json" | jq -r '
( .installed // false ) as $chan_installed
| (
( .accounts // null ) as $acc
| if $acc == null then 0
elif ($acc | type) == "array" then
( [ $acc[] | select((.installed // true) == true) ] | length )
elif ($acc | type) == "object" then
( [ $acc
| to_entries[]
| select(.value != null and ((.value.installed // true) == true))
] | length )
else 0
end
) as $ready
| if ($chan_installed == true or $ready > 0) and $ready > 0 then "healthy"
elif $chan_installed == true or $ready > 0 then "no_accounts"
else "not_installed"
end
' 2>/dev/null
}
# One-shot helper: cache + resolve + classify. Echoes a health
# string. The second argument toggles --all (use 1 to include
# bundled channels, useful when "missing" should mean "not even
# bundled").
channel_health() {
local channel_name="$1"
local with_all="${2:-}"
local json entry
if ! json=$(fetch_channels_json "$with_all"); then
return 1
fi
entry=$(resolve_channel_entry "$channel_name" "$json")
channel_health_from_entry "$entry"
}
# ---- Guard rails ----
if ! is_true "${OPENCLAW_POST_RESTORE_ENABLED:-false}"; then
log_info "Skipped: OPENCLAW_POST_RESTORE_ENABLED is unset or false (opt-in; set OPENCLAW_POST_RESTORE_ENABLED=true to enable)"
exit 0
fi
OPENCLAW_BIN="${OPENCLAW_BIN:-/usr/local/bin/openclaw}"
FAIL_ON_ERROR="${OPENCLAW_POST_RESTORE_FAIL_ON_ERROR:-false}"
errors=0
record_error() {
log_error "$1"
errors=$((errors + 1))
}
# Surface the detected OpenClaw version up-front so the operator
# immediately sees which schema variant the rest of the script
# will operate against. A "?" means the CLI was not reachable at
# startup (we'll fail later in check_openclaw_cli_present if so).
DETECTED_VERSION="$(detect_openclaw_version 2>/dev/null || true)"
if [[ -n "$DETECTED_VERSION" ]]; then
log_info "OpenClaw target: ${DETECTED_VERSION} (supports unified channels list --all, top-level channel keys, per-account installed flag)"
else
log_info "OpenClaw target: unknown (CLI not yet reachable — will recheck in check_openclaw_cli_present)"
fi
# ---- Checks ----
# Each check_* function should be self-contained and call
# record_error on failure. Add new checks by writing a new
# check_* function and calling it from do_all_checks.
check_openclaw_cli_present() {
if command -v openclaw >/dev/null 2>&1; then
log_info "✓ openclaw CLI present: $(command -v openclaw)"
else
record_error "openclaw CLI not found in PATH (looked for: $OPENCLAW_BIN)"
fi
}
check_openclaw_version() {
if ! command -v openclaw >/dev/null 2>&1; then
return 0 # already reported by check_openclaw_cli_present
fi
if version_output=$(openclaw --version 2>&1); then
log_info "✓ openclaw version: $version_output"
else
record_error "openclaw --version failed: $version_output"
fi
}
check_channels_list_json() {
if ! command -v openclaw >/dev/null 2>&1; then
return 0
fi
if ! channels_json=$(openclaw channels list --json 2>&1); then
record_error "openclaw channels list --json failed: $channels_json"
return 0
fi
if ! command -v jq >/dev/null 2>&1; then
log_warn "jq not available, skipping JSON validation (channels list returned without error)"
return 0
fi
if ! echo "$channels_json" | jq -e . >/dev/null 2>&1; then
record_error "channels list --json returned invalid JSON"
return 0
fi
# Detect which schema layout we're looking at so the operator
# log line tells the truth on whatever version is installed.
# 2026.6.x: top-level keys (or .channels[]); <2026.6: nested
# under .chat. Both are valid.
local schema_hint
schema_hint=$(printf '%s' "$channels_json" | jq -r '
if (.channels | type) == "array" then "channels-array"
elif (.chat | type) == "object" then "chat-nested"
elif ([keys[] | select(. != "auth" and . != "usage" and . != "models" and . != "providers")] | length) > 0 then "top-level"
else "empty"
end
' 2>/dev/null)
log_info "✓ channels list --json returned valid JSON (schema: ${schema_hint:-unknown})"
}
do_all_checks() {
check_openclaw_cli_present
check_openclaw_version
check_channels_list_json
}
# ---- Operations ----
# Unlike check_* above, these have side effects (update, install,
# fix). Each is wrapped in run_step which:
# - logs the command before invoking it
# - captures stdout+stderr
# - measures wall time
# - reports success/failure with the appropriate level
# - re-emits the output line-by-line (info on success, warn on
# failure) so the operator can grep the log
# - counts failures in $errors (governed by FAIL_ON_ERROR)
run_step() {
local description="$1"
local cmd="$2"
local start_time end_time duration output rc=0
start_time=$(date +%s)
log_info "━━ STEP: ${description} [STARTING]"
log_info " Command: ${cmd}"
output=$(bash -c "$cmd" 2>&1) || rc=$?
end_time=$(date +%s)
duration=$((end_time - start_time))
if [[ $rc -eq 0 ]]; then
log_info "✓ STEP: ${description} [COMPLETED] (${duration}s)"
else
log_error "✗ STEP: ${description} [FAILED] (exit=$rc, ${duration}s)"
record_error "${description} failed (exit=$rc)"
fi
if [[ -n "$output" ]]; then
while IFS= read -r line; do
[[ -z "$line" ]] && continue
if [[ $rc -eq 0 ]]; then
log_info " | $line"
else
log_warn " | $line"
fi
done <<< "$output"
fi
}
# Each operation function pre-checks command availability so the
# log shows "skipped: CLI not present" instead of a confusing
# "command not found" coming out of run_step.
run_openclaw_plugins_update() {
if ! command -v openclaw >/dev/null 2>&1; then
log_warn "openclaw CLI not available, skipping plugins update"
return 0
fi
run_step "openclaw plugins update --all" "openclaw plugins update --all"
}
# Healthy = the feishu entry exists in `openclaw channels list --json`,
# is installed, AND has at least one ready account. "Ready" is
# detected via the per-account `installed` flag (2026.6.x) or by
# counting entries under `.accounts` (<2026.6). Anything else is
# considered "unhealthy" and gates run_lark_cli_update to fire.
check_feishu_plugin_healthy() {
if ! command -v openclaw >/dev/null 2>&1; then
log_warn "cannot check feishu health: openclaw CLI not available"
return 1
fi
if ! command -v jq >/dev/null 2>&1; then
log_warn "cannot check feishu health: jq not available"
return 1
fi
# On 2026.6.x, the default `channels list` view only surfaces
# configured accounts; pass --all so a missing-on-disk plugin
# still shows up as `not_installed` rather than `missing`. On
# older versions the --all flag is harmless (it was a no-op
# alias at worst) and is dropped by the helper when not
# supported.
local status
status=$(channel_health feishu 1)
local rc=$?
if (( rc != 0 )); then
log_warn "cannot check feishu health: channels list failed"
return 1
fi
if [[ "$status" == "healthy" ]]; then
log_info "✓ feishu plugin healthy (installed + has authenticated accounts)"
return 0
fi
log_info "feishu plugin status: $status (treats as unhealthy → will trigger lark-cli update)"
return 1
}
# Same shape as check_feishu_plugin_healthy, but for the qqbot
# channel. Health = installed + at least one ready account. Anything
# else is "unhealthy" and gates the reinstall step.
check_qqbot_plugin_healthy() {
if ! command -v openclaw >/dev/null 2>&1; then
log_warn "cannot check qqbot health: openclaw CLI not available"
return 1
fi
if ! command -v jq >/dev/null 2>&1; then
log_warn "cannot check qqbot health: jq not available"
return 1
fi
local status
status=$(channel_health qqbot 1)
local rc=$?
if (( rc != 0 )); then
log_warn "cannot check qqbot health: channels list failed"
return 1
fi
if [[ "$status" == "healthy" ]]; then
log_info "✓ qqbot plugin healthy (installed + has authenticated accounts)"
return 0
fi
log_info "qqbot plugin status: $status (treats as unhealthy → will trigger reinstall)"
return 1
}
run_lark_cli_update() {
if ! command -v npm >/dev/null 2>&1; then
log_warn "npm not available, skipping lark-cli update"
return 0
fi
# Gate: only update lark-cli if the feishu plugin is broken. A
# working feishu integration means the currently-installed lark-cli
# version is good enough — running `npm update` would just be churn
# (and might break the working setup if a major version lands).
if check_feishu_plugin_healthy; then
log_info "feishu plugin is healthy, skipping lark-cli update (don't touch what works)"
return 0
fi
log_info "feishu plugin is unhealthy or unconfigured, proceeding with lark-cli update"
# Operator visibility: log which OpenClaw version we're targeting
# and which command syntax we'll use. On 2026.6.x the recommended
# skills source is the unscoped `larksuite/cli` (matches the new
# skills hub manifest); on older versions some deployments still
# used `@larksuite/cli`. We always pin to `@larksuite/cli` for the
# binary (npm package, version-controlled) and pick the right
# `skills add` source per detected version.
local ver
ver=$(detect_openclaw_version 2>/dev/null || true)
log_info "Updating lark-cli for OpenClaw ${ver:-unknown}..."
local skills_source="larksuite/cli"
if [[ -n "$ver" ]] && ! openclaw_version_at_least 2026.6.0; then
# On <2026.6 the skills registry still expects the @-scoped form
skills_source="@larksuite/cli"
fi
local cmd="npm update -g @larksuite/cli && npx skills add ${skills_source} -g -y"
run_step "$cmd" "$cmd"
}
run_lark_cli_version() {
if ! command -v lark-cli >/dev/null 2>&1; then
log_warn "lark-cli not available, skipping version check"
return 0
fi
run_step "lark-cli --version" "lark-cli --version"
}
run_lark_cli_auth_status() {
if ! command -v lark-cli >/dev/null 2>&1; then
log_warn "lark-cli not available, skipping auth status"
return 0
fi
run_step "lark-cli auth status" "lark-cli auth status"
}
run_openclaw_doctor_fix() {
if ! command -v openclaw >/dev/null 2>&1; then
log_warn "openclaw CLI not available, skipping doctor --fix"
return 0
fi
run_step "openclaw doctor --fix" "openclaw doctor --fix"
}
run_qqbot_plugin_reinstall() {
if ! command -v openclaw >/dev/null 2>&1; then
log_warn "openclaw CLI not available, skipping qqbot reinstall"
return 0
fi
# Same fix-on-broken pattern as run_lark_cli_update: only reinstall
# the qqbot plugin if the channel is unhealthy. A working integration
# doesn't need a churn reinstall.
if check_qqbot_plugin_healthy; then
log_info "qqbot plugin is healthy, skipping reinstall (don't touch what works)"
return 0
fi
log_info "qqbot plugin is unhealthy or unconfigured, proceeding with uninstall+reinstall"
# 2026.6.1 plugin install syntax:
# - bare `@openclaw/qqbot` resolves to the bundled copy of the
# plugin that ships with this OpenClaw build (auto-detected
# during install). Pinning to `@latest` keeps the upgrade
# rolling even if the bundled copy was older.
# - explicit `npm:@openclaw/qqbot@latest` would force the npm
# registry path; not needed here, the bundled copy is the
# canonical source for an official channel plugin.
# - `uninstall || true` ensures we still reach the install step
# when the plugin was never installed to begin with (the
# "missing" health state). The original `&&` chain silently
# skipped install in that case, which defeated the fix.
run_step "openclaw plugins uninstall qqbot || true; openclaw plugins install @openclaw/qqbot@latest" \
"openclaw plugins uninstall qqbot || true; openclaw plugins install @openclaw/qqbot@latest"
}
run_feishu_doctor_check() {
# Targeted feishu diagnostic — richer than the openclaw doctor --fix
# catch-all, gives a per-section pass/warn/fail breakdown that
# operators can grep. Always runs (not gated on health) so the
# baseline check is captured on every container start.
if ! command -v feishu-doctor >/dev/null 2>&1; then
log_warn "feishu-doctor not on PATH, skipping"
return 0
fi
run_step "feishu-doctor check" "feishu-doctor check"
}
do_all_operations() {
log_info "=== Post-restore openclaw operations START ==="
run_openclaw_plugins_update
run_lark_cli_update
run_lark_cli_version
run_lark_cli_auth_status
run_qqbot_plugin_reinstall
run_feishu_doctor_check
run_openclaw_doctor_fix
log_info "=== Post-restore openclaw operations END ==="
}
# ---- Main ----
log_info "=== Post-restore openclaw hook START ==="
log_info "OPENCLAW_POST_RESTORE_ENABLED=${OPENCLAW_POST_RESTORE_ENABLED:-false}"
log_info "OPENCLAW_POST_RESTORE_FAIL_ON_ERROR=$FAIL_ON_ERROR"
log_info "OPENCLAW_BIN=$OPENCLAW_BIN"
log_info "OPENCLAW_VERSION=${OPENCLAW_VERSION:-<not set, will be probed>}"
log_info "Resolved target version: ${DETECTED_VERSION:-<not yet detectable>}"
do_all_checks
do_all_operations
log_info "=== Post-restore openclaw hook END (errors=$errors) ==="
if [[ "$errors" -gt 0 ]]; then
if is_true "$FAIL_ON_ERROR"; then
log_error "Aborting startup: $errors check(s) failed and OPENCLAW_POST_RESTORE_FAIL_ON_ERROR=true"
exit 1
fi
log_warn "Continuing startup with $errors check failure(s) (set OPENCLAW_POST_RESTORE_FAIL_ON_ERROR=true to fail-fast)"
fi
exit 0