| #!/usr/bin/env bash |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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_DETECTED_VERSION="" |
| OPENCLAW_CHANNELS_JSON_CACHE="" |
| OPENCLAW_CHANNELS_JSON_LOADED=0 |
|
|
| |
| |
| |
| 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" |
| } |
|
|
| |
| |
| |
| openclaw_version_at_least() { |
| local threshold="$1" |
| local current |
| current=$(detect_openclaw_version 2>/dev/null) || return 1 |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| 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_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 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| 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 |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| 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" |
| } |
|
|
| |
| 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)) |
| } |
|
|
| |
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| |
|
|
| 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 |
| 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 |
|
|
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
|
|
| 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 |
| } |
|
|
| |
| |
| |
|
|
| 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" |
| } |
|
|
| |
| |
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| |
| |
| |
| 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 |
| } |
|
|
| |
| |
| |
| 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 |
|
|
| |
| |
| |
| |
| 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" |
|
|
| |
| |
| |
| |
| |
| |
| |
| 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 |
| |
| 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 |
|
|
| |
| |
| |
| 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" |
|
|
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| |
| 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() { |
| |
| |
| |
| |
| 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 ===" |
| } |
|
|
| |
| 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 |
|
|