#!/usr/bin/env bash set -euo pipefail SCRIPT_DIR="$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)" REPO_ROOT="$(cd -- "$SCRIPT_DIR/.." && pwd)" HF_TOKEN_FILE="${HF_TOKEN_FILE:-$HOME/.cache/huggingface/token}" RESTORE_HF_LOGIN_REQUIRED=0 RESTORE_HF_LOGIN_TOKEN="" RESTORE_HF_LOGIN_USERNAME="" trim() { printf '%s' "$1" | sed -e 's/^[[:space:]]*//' -e 's/[[:space:]]*$//' } info() { printf '%s\n' "$*" } warn() { printf 'warning: %s\n' "$*" >&2 } error() { printf 'error: %s\n' "$*" >&2 } die() { error "$1" exit 1 } prompt_line() { local prompt="$1" local default_value="${2:-}" local value="" if [[ -n "$default_value" ]]; then read -r -p "$prompt [$default_value]: " value || true else read -r -p "$prompt: " value || true fi value="$(trim "${value:-}")" if [[ -z "$value" ]]; then value="$default_value" fi printf '%s' "$value" } prompt_required() { local prompt="$1" local default_value="${2:-}" local value="" while [[ -z "$value" ]]; do value="$(prompt_line "$prompt" "$default_value")" value="$(trim "$value")" if [[ -z "$value" ]]; then printf 'This value is required.\n' >&2 fi done printf '%s' "$value" } prompt_secret_optional() { local prompt="$1" local value="" local char="" if [[ -t 0 && -t 1 ]]; then printf '%s: ' "$prompt" >&2 while IFS= read -r -s -n 1 char; do if [[ -z "$char" ]]; then break fi case "$char" in $'\177'|$'\b') if [[ -n "$value" ]]; then value="${value%?}" printf '\b \b' >&2 fi ;; *) value+="$char" printf '*' >&2 ;; esac done printf '\n' >&2 else read -r -p "$prompt: " value || true fi printf '%s' "$(trim "${value:-}")" } prompt_secret_required() { local prompt="$1" local value="" while [[ -z "$value" ]]; do value="$(prompt_secret_optional "$prompt")" value="$(trim "$value")" if [[ -z "$value" ]]; then printf 'This value is required.\n' >&2 fi done printf '%s' "$value" } prompt_yes_no() { local prompt="$1" local default_value="${2:-n}" local answer="" local hint="[y/N]" if [[ "$default_value" == "y" ]]; then hint="[Y/n]" fi while true; do read -r -p "$prompt $hint: " answer || true answer="$(trim "${answer:-}")" answer="$(printf '%s' "$answer" | tr '[:upper:]' '[:lower:]')" if [[ -z "$answer" ]]; then answer="$default_value" fi case "$answer" in y|yes) printf 'yes' return 0 ;; n|no) printf 'no' return 0 ;; *) printf 'Please enter y or n.\n' >&2 ;; esac done } login_with_hf_token() { local token="$1" hf auth login --token "$token" >/dev/null 2>&1 || die "hf auth login with token failed." } restore_previous_hf_login_if_needed() { if [[ "$RESTORE_HF_LOGIN_REQUIRED" -ne 1 ]]; then return 0 fi if [[ -z "$RESTORE_HF_LOGIN_TOKEN" ]]; then error "Skip restoring previous HF login because backup token is empty." RESTORE_HF_LOGIN_REQUIRED=0 return 0 fi local display_user="${RESTORE_HF_LOGIN_USERNAME:-unknown}" info "Restoring previous HF login for user: $display_user" if hf auth login --token "$RESTORE_HF_LOGIN_TOKEN" >/dev/null 2>&1; then info "Previous HF login restored." else error "Failed to restore previous HF login. Please run: hf auth login" fi RESTORE_HF_LOGIN_REQUIRED=0 } ensure_git() { command -v git >/dev/null 2>&1 || die "git is required. Please install git first." } ensure_hf_cli() { local os_name if command -v hf >/dev/null 2>&1; then return 0 fi os_name="$(uname -s)" case "$os_name" in Darwin|Linux) info "hf CLI not found. Installing via https://hf.co/cli/install.sh ..." curl -LsSf https://hf.co/cli/install.sh | bash ;; *) die "hf CLI not found. For Windows use: powershell -ExecutionPolicy ByPass -c \"irm https://hf.co/cli/install.ps1 | iex\"" ;; esac if [[ -d "$HOME/.local/bin" ]]; then PATH="$HOME/.local/bin:$PATH" fi command -v hf >/dev/null 2>&1 || die "hf CLI install failed. Please install manually and rerun." } # Uses uv run if available, falls back to python3 python_run() { if command -v uv >/dev/null 2>&1 && [[ -f "pyproject.toml" || -f "uv.lock" ]]; then uv run python "$@" else python3 "$@" fi } ensure_huggingface_hub_py() { python_run -c "import huggingface_hub" >/dev/null 2>&1 || die "python3 package 'huggingface_hub' is required. Install with: uv pip install 'huggingface_hub[cli]' or python3 -m pip install --user 'huggingface_hub[cli]'" } resolve_latest_openclaw_version() { local version version="$( python_run - <<'PY' 2>/dev/null || true import json import urllib.request url = "https://registry.npmjs.org/openclaw/latest" try: with urllib.request.urlopen(url, timeout=8) as response: payload = response.read().decode("utf-8", errors="replace") data = json.loads(payload) version = str(data.get("version", "")).strip() if version: print(version) except Exception: pass PY )" version="$(trim "$version")" if [[ -z "$version" ]]; then version="latest" fi printf '%s' "$version" } read_current_hf_token() { if [[ -n "${HUGGINGFACE_HUB_TOKEN:-}" ]]; then printf '%s' "${HUGGINGFACE_HUB_TOKEN}" | tr -d '\r\n' return 0 fi if [[ -n "${HF_TOKEN:-}" ]]; then printf '%s' "${HF_TOKEN}" | tr -d '\r\n' return 0 fi if [[ -f "$HF_TOKEN_FILE" ]]; then head -n 1 "$HF_TOKEN_FILE" | tr -d '\r\n' return 0 fi printf '' } get_hf_username() { local whoami_output local username local token_from_cache whoami_output="$(hf auth whoami 2>&1 || true)" username="$(printf '%s\n' "$whoami_output" | sed -nE 's/^[[:space:]]*user:[[:space:]]*([^[:space:]]+).*$/\1/p' | head -n 1)" if [[ -z "$username" ]]; then username="$(printf '%s\n' "$whoami_output" | sed -nE 's/.*[Ll]ogged in as[[:space:]]+([^[:space:]]+).*$/\1/p' | head -n 1)" fi if [[ -z "$username" ]]; then token_from_cache="$(read_current_hf_token)" if [[ -n "$token_from_cache" ]]; then username="$( HF_API_TOKEN="$token_from_cache" python_run - <<'PY' 2>/dev/null || true from huggingface_hub import HfApi import os token = (os.environ.get("HF_API_TOKEN") or "").strip() or None api = HfApi(token=token) data = api.whoami(token=token) name = data.get("name", "") if isinstance(data, dict) else "" print((name or "").strip()) PY )" fi fi username="$(printf '%s' "$username" | tr -d '\r\n')" printf '%s' "$username" } set_space_variable() { local space_repo_id="$1" local key="$2" local value="$3" local api_token="$4" SPACE_REPO_ID="$space_repo_id" \ SPACE_VARIABLE_KEY="$key" \ SPACE_VARIABLE_VALUE="$value" \ HF_API_TOKEN="$api_token" \ python_run - <<'PY' from huggingface_hub import HfApi import os token = (os.environ.get("HF_API_TOKEN") or "").strip() or None api = HfApi(token=token) try: api.add_space_variable( repo_id=os.environ["SPACE_REPO_ID"], key=os.environ["SPACE_VARIABLE_KEY"], value=os.environ["SPACE_VARIABLE_VALUE"], ) except Exception as exc: key = os.environ.get("SPACE_VARIABLE_KEY", "") repo_id = os.environ.get("SPACE_REPO_ID", "") raise SystemExit(f"failed to set space variable {key} on {repo_id}: {exc}") PY } set_space_secret() { local space_repo_id="$1" local key="$2" local value="$3" local api_token="$4" SPACE_REPO_ID="$space_repo_id" \ SPACE_SECRET_KEY="$key" \ SPACE_SECRET_VALUE="$value" \ HF_API_TOKEN="$api_token" \ python_run - <<'PY' from huggingface_hub import HfApi import os token = (os.environ.get("HF_API_TOKEN") or "").strip() or None api = HfApi(token=token) try: api.add_space_secret( repo_id=os.environ["SPACE_REPO_ID"], key=os.environ["SPACE_SECRET_KEY"], value=os.environ["SPACE_SECRET_VALUE"], ) except Exception as exc: key = os.environ.get("SPACE_SECRET_KEY", "") repo_id = os.environ.get("SPACE_REPO_ID", "") raise SystemExit(f"failed to set space secret {key} on {repo_id}: {exc}") PY } repo_exists() { local repo_id="$1" local repo_type="$2" local api_token="$3" REPO_CHECK_ID="$repo_id" \ REPO_CHECK_TYPE="$repo_type" \ HF_API_TOKEN="$api_token" \ python_run - <<'PY' 2>/dev/null from huggingface_hub import HfApi import os, sys token = (os.environ.get("HF_API_TOKEN") or "").strip() or None api = HfApi(token=token) try: api.repo_info(repo_id=os.environ["REPO_CHECK_ID"], repo_type=os.environ["REPO_CHECK_TYPE"]) sys.exit(0) except Exception: sys.exit(1) PY } delete_repo() { local repo_id="$1" local repo_type="$2" local api_token="$3" local display_name="$4" # e.g. "Space", "Dataset" info "Deleting existing $display_name: $repo_id..." REPO_DELETE_ID="$repo_id" \ REPO_DELETE_TYPE="$repo_type" \ HF_API_TOKEN="$api_token" \ python_run - <<'PY' from huggingface_hub import HfApi import os token = (os.environ.get("HF_API_TOKEN") or "").strip() or None api = HfApi(token=token) try: api.delete_repo(repo_id=os.environ["REPO_DELETE_ID"], repo_type=os.environ["REPO_DELETE_TYPE"]) print(f" Deleted successfully") except Exception as exc: print(f" Warning: could not delete: {exc}", file=sys.stderr) PY } restart_space() { local space_repo_id="$1" local api_token="$2" SPACE_RESTART_REPO_ID="$space_repo_id" \ HF_API_TOKEN="$api_token" \ python_run - <<'PY' from huggingface_hub import HfApi import os token = (os.environ.get("HF_API_TOKEN") or "").strip() or None api = HfApi(token=token) try: api.restart_space(repo_id=os.environ["SPACE_RESTART_REPO_ID"]) except Exception as exc: repo_id = os.environ.get("SPACE_RESTART_REPO_ID", "") raise SystemExit(f"failed to restart space {repo_id}: {exc}") PY } update_readme_title() { local new_title="$1" local readme_file="$REPO_ROOT/README.md" local temp_file if [[ ! -f "$readme_file" ]]; then return 0 fi temp_file="$(mktemp)" if sed "s/^title:.*/title: $new_title/" "$readme_file" > "$temp_file"; then mv "$temp_file" "$readme_file" else rm -f "$temp_file" fi } restore_readme_title() { local readme_file="$REPO_ROOT/README.md" local original_title="$1" local temp_file if [[ ! -f "$readme_file" ]]; then return 0 fi temp_file="$(mktemp)" if sed "s/^title:.*/title: $original_title/" "$readme_file" > "$temp_file"; then mv "$temp_file" "$readme_file" else rm -f "$temp_file" fi } main() { local hf_username local use_current_hf_user local current_hf_username local switch_hf_token local space_name local dataset_name local space_repo_id local dataset_repo_id local default_openclaw_version local openclaw_version local gateway_token local gateway_password local generated_gateway_token=0 local generated_gateway_password=0 local hf_token_for_backup local configure_llm local llm_base_url="" local llm_model="" local llm_api_key="" local enable_sshx="no" local sshx_auto_start_value="false" local bt_panel_port="7860" local bt_panel_username="btadmin" local bt_panel_password="" local generated_bt_panel_password=0 local bt_panel_safe_path="" local bt_panel_safe_path_type="default" local bt_panel_timezone="Asia/Shanghai" local proceed_with_deploy local api_token local space_page_url local space_host local app_url local health_url cd "$REPO_ROOT" info "OpenClaw Hugging Face bootstrap (interactive)" ensure_git ensure_hf_cli ensure_huggingface_hub_py git --version hf version python_run --version if ! hf auth whoami >/dev/null 2>&1; then info "HF CLI is not logged in." hf_token_for_backup="$(prompt_secret_required "HF_TOKEN (required for hf auth login)")" login_with_hf_token "$hf_token_for_backup" else current_hf_username="$(trim "$(get_hf_username)")" if [[ -n "$current_hf_username" ]]; then use_current_hf_user="$(prompt_yes_no "HF CLI is already logged in as '$current_hf_username'. Use this user?" "y")" else use_current_hf_user="$(prompt_yes_no "HF CLI is already logged in. Use current user?" "y")" fi if [[ "$use_current_hf_user" == "yes" ]]; then hf_token_for_backup="$(read_current_hf_token)" if [[ -z "$hf_token_for_backup" ]]; then hf_token_for_backup="$(prompt_secret_required "Cannot read current token. Enter HF_TOKEN")" login_with_hf_token "$hf_token_for_backup" fi else RESTORE_HF_LOGIN_TOKEN="$(read_current_hf_token)" [[ -n "$RESTORE_HF_LOGIN_TOKEN" ]] || die "Cannot backup current HF token. Ensure current token is readable before switching users." RESTORE_HF_LOGIN_REQUIRED=1 RESTORE_HF_LOGIN_USERNAME="$current_hf_username" switch_hf_token="$(prompt_secret_required "HF_TOKEN for switching HF user")" login_with_hf_token "$switch_hf_token" hf_token_for_backup="$switch_hf_token" fi fi [[ -n "$hf_token_for_backup" ]] || die "HF_TOKEN is required to configure Space secret HF_TOKEN." hf_username="$(get_hf_username)" hf_username="$(trim "$hf_username")" if [[ -z "$hf_username" ]]; then hf_username="$(prompt_required "HF username (cannot parse from hf auth whoami)")" fi info "HF user: $hf_username" space_name="$(prompt_required "Space name (without username)" "hi-ai")" space_private="$(prompt_yes_no "Make Space private?" "n")" if [[ "$space_private" == "yes" ]]; then space_private_flag="--private" else space_private_flag="" fi dataset_name="$(prompt_required "Dataset name (without username)" "${space_name}-backup")" space_repo_id="${hf_username}/${space_name}" dataset_repo_id="${hf_username}/${dataset_name}" storage_repo_id="${hf_username}/${space_name}-storage" info "" info "Configuring multi-dataset restore (optional)..." info " This allows restoring from a different dataset than the backup target." restore_dataset_choice="$(prompt_yes_no "Use a different dataset for restore than '$dataset_repo_id'?" "n")" if [[ "$restore_dataset_choice" == "yes" ]]; then restore_dataset_name="$(prompt_required "Restore dataset name (without username)" "${dataset_name}-restore")" restore_dataset_repo_id="${hf_username}/${restore_dataset_name}" else restore_dataset_repo_id="$dataset_repo_id" fi info " Restore dataset: $restore_dataset_repo_id" backup_enabled_choice="$(prompt_yes_no "Enable automatic backup?" "y")" if [[ "$backup_enabled_choice" == "yes" ]]; then backup_enabled_value="true" else backup_enabled_value="false" fi info " Automatic backup: $backup_enabled_value" if [[ "$backup_enabled_choice" == "yes" ]]; then backup_npm_choice="$(prompt_yes_no "Backup npm cache directory (/root/.npm)?" "y")" if [[ "$backup_npm_choice" == "yes" ]]; then backup_npm_value="true" else backup_npm_value="false" fi info " Backup npm directory: $backup_npm_value" else backup_npm_value="false" fi restore_npm_choice="$(prompt_yes_no "Restore npm cache directory (/root/.npm) during restore?" "y")" if [[ "$restore_npm_choice" == "yes" ]]; then restore_npm_value="true" else restore_npm_value="false" fi info " Restore npm directory: $restore_npm_value" if [[ "$backup_enabled_choice" == "yes" ]]; then backup_encryption_choice="$(prompt_yes_no "Enable backup encryption (AES-256-CBC)?" "y")" if [[ "$backup_encryption_choice" == "yes" ]]; then backup_encryption_value="true" info "NOTE: Encrypted backups require public datasets on HuggingFace." backup_encryption_password="$(prompt_secret_required "Encryption password (default: 123456789)")" backup_encryption_password="$(printf '%s' "$backup_encryption_password" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" if [[ -z "$backup_encryption_password" ]]; then backup_encryption_password="123456789" info " Using default password: 123456789" fi while [[ -z "$backup_encryption_password" ]]; do info " Password cannot be empty" backup_encryption_password="$(prompt_secret_required "Encryption password")" backup_encryption_password="$(printf '%s' "$backup_encryption_password" | sed 's/^[[:space:]]*//;s/[[:space:]]*$//')" if [[ -z "$backup_encryption_password" ]]; then backup_encryption_password="123456789" info " Using default password: 123456789" fi done else backup_encryption_value="false" backup_encryption_password="" fi else backup_encryption_value="false" backup_encryption_password="" fi info "Please specify OpenClaw version to install:" latest_openclaw_version="$(resolve_latest_openclaw_version)" default_openclaw_version="$(trim "${OPENCLAW_VERSION:-}")" if [[ -z "$default_openclaw_version" ]]; then default_openclaw_version="2026.4.15" if [[ -n "$latest_openclaw_version" && "$latest_openclaw_version" != "$default_openclaw_version" ]]; then info "Latest available version: $latest_openclaw_version" info "Default version: $default_openclaw_version" fi fi openclaw_version="$(prompt_required "OPENCLAW_VERSION" "$default_openclaw_version")" gateway_token="$(prompt_secret_optional "OPENCLAW_GATEWAY_TOKEN (optional, leave empty to auto-generate 32 chars)")" if [[ -z "$gateway_token" ]]; then gateway_token="$(openssl rand -hex 16)" generated_gateway_token=1 fi gateway_password="$(prompt_secret_optional "OPENCLAW_GATEWAY_PASSWORD (optional, leave empty to auto-generate 16 chars)")" if [[ -z "$gateway_password" ]]; then gateway_password="$(openssl rand -hex 8)" generated_gateway_password=1 fi configure_llm="$(prompt_yes_no "Configure custom LLM now?" "n")" if [[ "$configure_llm" == "yes" ]]; then llm_base_url="$(prompt_required "OPENCLAW_LLM_BASE_URL")" llm_model="$(prompt_required "OPENCLAW_LLM_MODEL")" llm_api_key="$(prompt_secret_optional "OPENCLAW_LLM_API_KEY")" [[ -n "$llm_api_key" ]] || die "OPENCLAW_LLM_API_KEY is required when enabling custom LLM config." else enable_sshx="$(prompt_yes_no "Set OPENCLAW_SSHX_AUTO_START=false for later sshx setup?" "n")" fi if [[ "$enable_sshx" == "yes" ]]; then sshx_auto_start_value="true" fi info "" info "Configuring BT Panel (宝塔面板)..." bt_panel_port="$(prompt_line "BT_PANEL_PORT (宝塔面板端口)" "$bt_panel_port")" while true; do bt_panel_username="$(prompt_line "BT_PANEL_USERNAME (宝塔面板用户名, 字母开头, 小写字母和数字)" "$bt_panel_username")" if [[ "$bt_panel_username" =~ ^[a-z][a-z0-9]{2,31}$ ]]; then break fi error "用户名必须以字母开头, 只能包含小写字母和数字, 长度3-32位" done # 密码选择 local default_bt_panel_password="openclaw@123" info "" info "选择宝塔面板密码设置方式:" info " 1) 使用默认密码 (openclaw@123)" info " 2) 自定义密码" info " 3) 自动生成随机密码" local password_choice password_choice="$(prompt_line "请选择 [1/2/3]" "1")" case "$password_choice" in 1|"default"|"") bt_panel_password="$default_bt_panel_password" generated_bt_panel_password=1 info "使用默认密码: $default_bt_panel_password" ;; 2|"custom") bt_panel_password="" while [[ -z "$bt_panel_password" ]]; do bt_panel_password="$(prompt_secret_required "请输入自定义密码")" if [[ -z "$bt_panel_password" ]]; then error "密码不能为空" fi done generated_bt_panel_password=0 ;; 3|"auto"|"random") bt_panel_password="$(openssl rand -base64 16 | tr -dc 'a-zA-Z0-9' | head -c 12)" generated_bt_panel_password=1 info "已自动生成随机密码" ;; *) warn "无效选择,使用默认密码" bt_panel_password="$default_bt_panel_password" generated_bt_panel_password=1 ;; esac info "" info "选择宝塔面板安全入口设置方式:" info " 1) 系统默认安全入口(默认值:lauer3912)" info " 2) 不设置安全入口(使用随机)" info " 3) 自定义安全入口路径" local default_bt_panel_safe_path="lauer3912" local safe_path_choice safe_path_choice="$(prompt_line "请选择 [1/2/3]" "1")" case "$safe_path_choice" in 1|""|"default") bt_panel_safe_path="$default_bt_panel_safe_path" bt_panel_safe_path_type="default" info "使用系统默认安全入口: /$bt_panel_safe_path" ;; 2|"random") bt_panel_safe_path="" bt_panel_safe_path_type="random" info "使用随机安全入口" ;; 3|"custom") bt_panel_safe_path="" bt_panel_safe_path_type="custom" while [[ -z "$bt_panel_safe_path" ]]; do bt_panel_safe_path="$(prompt_line "请输入自定义安全入口路径(字母和数字,不含特殊字符)")" if [[ -z "$bt_panel_safe_path" ]]; then error "安全入口路径不能为空" elif [[ ! "$bt_panel_safe_path" =~ ^[a-zA-Z0-9]+$ ]]; then error "安全入口路径只能包含字母和数字" bt_panel_safe_path="" fi done info "使用安全入口: /$bt_panel_safe_path" ;; *) bt_panel_safe_path="$default_bt_panel_safe_path" bt_panel_safe_path_type="default" info "无效选择,使用系统默认安全入口: /$bt_panel_safe_path" ;; esac info "" info "选择宝塔面板时区设置方式:" info " 1) 设置为 Asia/Shanghai(推荐)" info " 2) 使用系统默认时区" local default_bt_panel_timezone="Asia/Shanghai" local timezone_choice timezone_choice="$(prompt_line "请选择 [1/2]" "1")" case "$timezone_choice" in 1|""|"default") bt_panel_timezone="$default_bt_panel_timezone" info "使用 Asia/Shanghai 时区" ;; 2|"system") bt_panel_timezone="" info "使用系统默认时区" ;; *) bt_panel_timezone="$default_bt_panel_timezone" info "无效选择,使用 Asia/Shanghai 时区" ;; esac info "" info "Planned deployment configuration:" info "Space repo: $space_repo_id" info "Dataset repo: $dataset_repo_id" info "Restore dataset: $restore_dataset_repo_id" info "Storage repo: $storage_repo_id" info "OPENCLAW_VERSION: $openclaw_version" info "OPENCLAW_GATEWAY_CONTROLUI_ALLOW_INSECURE_AUTH=false" info "OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_DISABLE_DEVICE_AUTH=false" info "OPENCLAW_SSHX_AUTO_START=$sshx_auto_start_value" if [[ "$configure_llm" == "yes" ]]; then info "Custom LLM config: enabled" else info "Custom LLM config: disabled" fi info "BT Panel config:" info " BT_PANEL_PORT=$bt_panel_port" info " BT_PANEL_USERNAME=$bt_panel_username" if [[ "$generated_bt_panel_password" -eq 1 ]]; then info " BT_PANEL_PASSWORD=$bt_panel_password (generated/default)" else info " BT_PANEL_PASSWORD=**** (custom)" fi if [[ -n "$bt_panel_safe_path" ]]; then info " BT_PANEL_SAFE_PATH=$bt_panel_safe_path ($bt_panel_safe_path_type)" else info " BT_PANEL_SAFE_PATH= (random)" fi info " BT_PANEL_TIMEZONE=$bt_panel_timezone" proceed_with_deploy="$(prompt_yes_no "Proceed with these settings?" "y")" if [[ "$proceed_with_deploy" != "yes" ]]; then info "Cancelled by user before creating/updating Space or Dataset." return 0 fi info "Creating Space and Dataset..." # Check if Space already exists, prompt for recreation api_token="$(read_current_hf_token)" if repo_exists "$space_repo_id" "space" "$api_token"; then info "Space '$space_repo_id' already exists." recreate_choice="$(prompt_yes_no "Delete and recreate it? (WARNING: all existing files and settings will be lost)" "n")" if [[ "$recreate_choice" == "yes" ]]; then info "Recreating Space '$space_repo_id'..." delete_repo "$space_repo_id" "space" "$api_token" "Space" # Wait briefly for deletion to propagate sleep 5 hf repos create "$space_repo_id" --repo-type space --space-sdk docker $space_private_flag else info "Using existing Space '$space_repo_id' (updating files)..." hf repos create "$space_repo_id" --repo-type space --space-sdk docker $space_private_flag --exist-ok fi else hf repos create "$space_repo_id" --repo-type space --space-sdk docker $space_private_flag fi # Check if backup Dataset already exists, prompt for recreation if repo_exists "$dataset_repo_id" "dataset" "$api_token"; then info "Backup Dataset '$dataset_repo_id' already exists." recreate_dataset="$(prompt_yes_no "Delete and recreate it? (WARNING: all existing backup files will be lost)" "n")" if [[ "$recreate_dataset" == "yes" ]]; then info "Recreating Dataset '$dataset_repo_id'..." delete_repo "$dataset_repo_id" "dataset" "$api_token" "Dataset" sleep 3 if [[ "$backup_encryption_value" == "true" ]]; then hf repos create "$dataset_repo_id" --repo-type dataset else hf repos create "$dataset_repo_id" --repo-type dataset --private fi else if [[ "$backup_encryption_value" == "true" ]]; then hf repos create "$dataset_repo_id" --repo-type dataset --exist-ok else hf repos create "$dataset_repo_id" --repo-type dataset --private --exist-ok fi fi else if [[ "$backup_encryption_value" == "true" ]]; then hf repos create "$dataset_repo_id" --repo-type dataset else hf repos create "$dataset_repo_id" --repo-type dataset --private fi fi hf repos create "$storage_repo_id" --repo-type dataset --exist-ok if [[ "$restore_dataset_repo_id" != "$dataset_repo_id" ]]; then info "Creating restore dataset '$restore_dataset_repo_id'..." if [[ "$backup_encryption_value" == "true" ]]; then hf repos create "$restore_dataset_repo_id" --repo-type dataset --exist-ok else hf repos create "$restore_dataset_repo_id" --repo-type dataset --private --exist-ok fi fi info "Uploading repository to Space..." local original_readme_title original_readme_title="$(sed -n 's/^title: *//p' "$REPO_ROOT/README.md" | head -n 1)" update_readme_title "$space_name" restore_readme_on_exit() { restore_previous_hf_login_if_needed restore_readme_title "${original_readme_title:-HF Space}" } trap restore_readme_on_exit EXIT hf upload "$space_repo_id" . --repo-type space --exclude '.git/**' --exclude '.git' \ --commit-message "feat: deploy Gemma 4 to hf space" api_token="$(read_current_hf_token)" if [[ -z "$api_token" ]]; then api_token="$hf_token_for_backup" fi info "Configuring Space variables and secrets..." set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_ENABLED" "$backup_enabled_value" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_NPM_ENABLED" "$backup_npm_value" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_RESTORE_NPM_ENABLED" "$restore_npm_value" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_DATASET_REPO" "$dataset_repo_id" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_RESTORE_DATASET_REPO" "$restore_dataset_repo_id" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_REPO_TYPE" "dataset" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_PATH_PREFIX" "backups" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_HF_SPACE_ID" "$space_repo_id" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_VERSION" "$openclaw_version" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_GATEWAY_CONTROLUI_ALLOW_INSECURE_AUTH" "false" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_GATEWAY_CONTROLUI_DANGEROUSLY_DISABLE_DEVICE_AUTH" "false" "$api_token" set_space_secret "$space_repo_id" "OPENCLAW_GATEWAY_TOKEN" "$gateway_token" "$api_token" set_space_secret "$space_repo_id" "OPENCLAW_GATEWAY_PASSWORD" "$gateway_password" "$api_token" set_space_secret "$space_repo_id" "HF_TOKEN" "$hf_token_for_backup" "$api_token" if [[ "$backup_encryption_value" == "true" ]]; then set_space_secret "$space_repo_id" "OPENCLAW_BACKUP_ENCRYPTION_PASSWORD" "$backup_encryption_password" "$api_token" fi set_space_variable "$space_repo_id" "HF_STORAGE_REPO" "$storage_repo_id" "$api_token" if [[ "$configure_llm" == "yes" ]]; then set_space_variable "$space_repo_id" "OPENCLAW_LLM_BASE_URL" "$llm_base_url" "$api_token" set_space_variable "$space_repo_id" "OPENCLAW_LLM_MODEL" "$llm_model" "$api_token" set_space_secret "$space_repo_id" "OPENCLAW_LLM_API_KEY" "$llm_api_key" "$api_token" fi info "Configuring SSHX..." set_space_variable "$space_repo_id" "OPENCLAW_SSHX_AUTO_START" "$sshx_auto_start_value" "$api_token" info "Configuring BT Panel..." set_space_variable "$space_repo_id" "BT_PANEL_PORT" "$bt_panel_port" "$api_token" set_space_variable "$space_repo_id" "BT_PANEL_USERNAME" "$bt_panel_username" "$api_token" set_space_variable "$space_repo_id" "BT_PANEL_PASSWORD" "$bt_panel_password" "$api_token" if [[ -n "$bt_panel_safe_path" ]]; then set_space_variable "$space_repo_id" "BT_PANEL_SAFE_PATH" "$bt_panel_safe_path" "$api_token" fi set_space_variable "$space_repo_id" "BT_PANEL_TIMEZONE" "$bt_panel_timezone" "$api_token" info "Configuring restore timeout for extra-large backups (2GB+)..." # 【针对2GB+超大文件的恢复超时配置】 # 恢复2GB备份文件预计需要: # - 下载时间:20-40分钟(取决于网络) # - 解压时间:10-20分钟 # - 总时间:约30-60分钟 # 设置90分钟超时,确保充足时间 set_space_variable "$space_repo_id" "OPENCLAW_RESTORE_TIMEOUT" "5400" "$api_token" info "Configuring backup frequency..." # 【默认配置】每10分钟执行一次备份检查 # 使用增量备份策略,只有变化的数据会被备份 # Cron格式: */10 * * * * 表示每10分钟执行一次 set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_CRON" "*/10 * * * *" "$api_token" info "Configuring incremental backup..." # 默认开启增量备份 # 效果:完整备份后,只备份变化的文件,大幅减少备份时间和存储 set_space_variable "$space_repo_id" "OPENCLAW_INCREMENTAL_BACKUP" "true" "$api_token" # 【默认配置】增量备份间隔设置为15分钟 # 效果:距上次备份>=15分钟时,执行增量备份(只备份变化文件) set_space_variable "$space_repo_id" "OPENCLAW_INCREMENTAL_INTERVAL_MINUTES" "15" "$api_token" if [[ "$backup_encryption_value" == "true" ]]; then info "Configuring backup encryption..." set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_ENCRYPTION_ENABLED" "true" "$api_token" fi info "Configuring backup retention for extra-large datasets..." # 保留备份数量(默认24个) # 存储估算:单次1GB × 24个 ≈ 24GB(正常情况增量备份远小于1GB) set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_KEEP_COUNT" "24" "$api_token" info "Configuring compression level..." # 压缩级别:6(平衡压缩速度和大小,动态策略会根据文件大小调整) set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_COMPRESSION_LEVEL" "6" "$api_token" info "Configuring volume splitting for extra-large files..." # 启用分卷压缩(2GB+场景推荐) # 将大文件分割成多个500MB的小文件,便于上传和恢复 # 避免单文件过大导致的网络中断问题 set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_SPLIT_SIZE" "500M" "$api_token" info "Configuring large file warning threshold..." # 大文件警告阈值(MB) # 当备份大小超过此值时发出警告,提醒用户优化 set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_SIZE_WARNING_MB" "1500" "$api_token" info "Configuring dynamic backup strategy..." # 【动态备份策略】根据数据变化率自动调整备份参数 # 启用后,系统会根据以下因素自动优化: # - 文件大小:自动调整压缩级别和分卷大小 # - 变化率:根据文件修改频率调整备份间隔 # - 系统负载:高负载时跳过非紧急备份 # - 网络状况:慢网络时启用更强压缩 set_space_variable "$space_repo_id" "OPENCLAW_DYNAMIC_BACKUP" "true" "$api_token" # 动态策略参数 # 小文件阈值:<500MB使用快速策略 set_space_variable "$space_repo_id" "OPENCLAW_DYNAMIC_SMALL_THRESHOLD_MB" "500" "$api_token" # 中文件阈值:500MB-2GB使用平衡策略 set_space_variable "$space_repo_id" "OPENCLAW_DYNAMIC_MEDIUM_THRESHOLD_MB" "2000" "$api_token" # 大文件阈值:>2GB使用激进策略 # 高变化率阈值(每分钟变化的文件数) set_space_variable "$space_repo_id" "OPENCLAW_DYNAMIC_HIGH_CHANGE_RATE" "10" "$api_token" # 低变化率阈值 set_space_variable "$space_repo_id" "OPENCLAW_DYNAMIC_LOW_CHANGE_RATE" "2" "$api_token" # 最小变化文件数(低于此值且变化率低时跳过增量备份) set_space_variable "$space_repo_id" "OPENCLAW_DYNAMIC_MIN_CHANGED_FILES" "5" "$api_token" # 最小变化文件总大小(低于此值且变化率低时跳过增量备份) set_space_variable "$space_repo_id" "OPENCLAW_DYNAMIC_MIN_CHANGED_SIZE_KB" "100" "$api_token" # 全备份策略配置 # 每隔2小时强制执行一次全备份 set_space_variable "$space_repo_id" "OPENCLAW_FULL_BACKUP_INTERVAL_HOURS" "1" "$api_token" # 增量备份达到15次后强制执行全备份 set_space_variable "$space_repo_id" "OPENCLAW_MAX_INCREMENTAL_BACKUPS" "15" "$api_token" info "Configuring backup watchdog..." # 【备份看门狗配置】兜底保障机制 # 看门狗作为备份系统的最后一道防线,确保备份按时执行 # 即使 cron 失效,看门狗也会强制触发备份 # 每10分钟检查一次备份系统状态 set_space_variable "$space_repo_id" "WATCHDOG_INTERVAL" "600" "$api_token" # 最大备份间隔(分钟) # 如果超过30分钟没有成功备份,看门狗将触发强制备份 set_space_variable "$space_repo_id" "MAX_BACKUP_AGE_MINUTES" "30" "$api_token" # 强制备份间隔(秒) # 无论 cron 状态如何,每4小时至少执行一次强制备份 set_space_variable "$space_repo_id" "FORCE_BACKUP_INTERVAL" "14400" "$api_token" info "Configuring extra backup paths..." backup_dirs="supervisor-confd:/etc/supervisor/conf.d" backup_dirs+=",cron-spool:/var/spool/cron/crontabs" set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_EXTRA_DIRS" "$backup_dirs" "$api_token" backup_files="root-bashrc:/root/.bashrc" backup_files+=",root-bash_profile:/root/.bash_profile" backup_files+=",root-profile:/root/.profile" backup_files+=",root-gitconfig:/root/.gitconfig" backup_files+=",root-vimrc:/root/.vimrc" backup_files+=",root-tmux:/root/.tmux.conf" backup_files+=",ssh-agent-autostart:/usr/local/bin/ssh-agent-autostart.sh" backup_files+=",openclaw-backup-env:/root/.env.d/openclaw-backup.env" backup_files+=",openclaw-env-sh:/etc/profile.d/openclaw-env.sh" set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_EXTRA_FILES" "$backup_files" "$api_token" info "Configuring SSH agent auto-start..." # 【SSH密钥自动激活配置】 # 在容器启动时自动启动ssh-agent并加载/root/.ssh/下的私钥 # 用户只需将私钥放入/root/.ssh/目录,系统会自动处理 set_space_variable "$space_repo_id" "OPENCLAW_SSH_AGENT_AUTOSTART" "true" "$api_token" info "Configuring SSH key auto-loading..." # SSH自动激活脚本的bashrc钩子 # 这会在每次登录时自动执行ssh-agent-autostart.sh ssh_bashrc_hook=' # Auto-start SSH agent and load keys if [ -f /usr/local/bin/ssh-agent-autostart.sh ]; then source /usr/local/bin/ssh-agent-autostart.sh fi ' # 将钩子内容编码为base64以便传递 ssh_bashrc_hook_b64=$(echo "$ssh_bashrc_hook" | base64 -w0) set_space_variable "$space_repo_id" "OPENCLAW_SSH_BASHRC_HOOK" "$ssh_bashrc_hook_b64" "$api_token" info "Configuing Build Version for tracking..." build_version_for_space="$openclaw_version-$(date -u +%Y%m%d%H%M)" set_space_variable "$space_repo_id" "BUILD_VERSION" "$build_version_for_space" "$api_token" space_page_url="https://huggingface.co/spaces/${space_repo_id}" space_host="${space_repo_id/\//-}.hf.space" app_url="https://${space_host}" health_url="${app_url}/healthz" info "" info "Deployment complete." info "Space repo: $space_repo_id" info "Hugging Face Space: $space_page_url" info "Dataset repo: $dataset_repo_id" info "Restore dataset: $restore_dataset_repo_id" info "Storage repo: $storage_repo_id" info "OPENCLAW_VERSION: $openclaw_version" info "Space URL: $app_url" info "Health URL: $health_url" info "SSHX auto-start: $sshx_auto_start_value" info "BUILD_VERSION for tracking: $build_version_for_space" info "" info "BT Panel (宝塔面板) - 面板地址在容器启动后查看日志:" if [[ "$generated_bt_panel_password" -eq 1 ]]; then info " BT_PANEL_PASSWORD=$bt_panel_password (auto-generated)" fi if [[ -n "$bt_panel_safe_path" ]]; then info " BT_PANEL_SAFE_PATH=$bt_panel_safe_path ($bt_panel_safe_path_type)" fi info " BT_PANEL_TIMEZONE=$bt_panel_timezone" if [[ "$generated_gateway_token" -eq 1 ]]; then info "Generated OPENCLAW_GATEWAY_TOKEN=$gateway_token" fi if [[ "$generated_gateway_password" -eq 1 ]]; then info "Generated OPENCLAW_GATEWAY_PASSWORD=$gateway_password" fi } main "$@"