| #!/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." |
| } |
|
|
| |
| 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 |
| } |
|
|
| 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=<not set> (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..." |
| hf repos create "$space_repo_id" --repo-type space --space-sdk docker $space_private_flag --exist-ok |
| 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 |
| 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+)..." |
| |
| |
| |
| |
| |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_RESTORE_TIMEOUT" "5400" "$api_token" |
|
|
| info "Configuring backup frequency..." |
| |
| |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_CRON" "*/5 * * * *" "$api_token" |
|
|
| info "Configuring incremental backup..." |
| |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_INCREMENTAL_BACKUP" "true" "$api_token" |
| |
| |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_INCREMENTAL_INTERVAL_MINUTES" "5" "$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..." |
| |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_KEEP_COUNT" "48" "$api_token" |
| |
| info "Configuring compression level..." |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_COMPRESSION_LEVEL" "6" "$api_token" |
| |
| info "Configuring volume splitting for extra-large files..." |
| |
| |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_BACKUP_SPLIT_SIZE" "500M" "$api_token" |
| |
| info "Configuring large file warning threshold..." |
| |
| |
| 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" |
| |
| |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_DYNAMIC_SMALL_THRESHOLD_MB" "500" "$api_token" |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_DYNAMIC_MEDIUM_THRESHOLD_MB" "2000" "$api_token" |
| |
| |
| |
| 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" |
|
|
| |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_FULL_BACKUP_INTERVAL_HOURS" "1" "$api_token" |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_MAX_INCREMENTAL_BACKUPS" "10" "$api_token" |
|
|
| info "Configuring backup watchdog..." |
| |
| |
| |
| |
| |
| |
| set_space_variable "$space_repo_id" "WATCHDOG_INTERVAL" "300" "$api_token" |
| |
| |
| |
| set_space_variable "$space_repo_id" "MAX_BACKUP_AGE_MINUTES" "20" "$api_token" |
| |
| |
| |
| set_space_variable "$space_repo_id" "FORCE_BACKUP_INTERVAL" "3600" "$api_token" |
|
|
| info "Configuring BT Panel backup..." |
| |
| backup_dirs="bt-panel-data:/www/server/panel/data" |
| backup_dirs+=",bt-panel-config:/www/server/panel/config" |
| backup_dirs+=",bt-panel-plugin:/www/server/panel/plugin" |
| backup_dirs+=",bt-panel-ssl:/www/server/panel/ssl" |
| backup_dirs+=",bt-panel-host:/www/server/panel/vhost" |
| backup_dirs+=",bt-panel-nginx-conf:/www/server/nginx/conf" |
| backup_dirs+=",bt-panel-apache-conf:/www/server/apache/conf" |
| backup_dirs+=",bt-www-wwwroot:/www/wwwroot" |
| backup_dirs+=",bt-www-backup:/www/backup" |
| backup_dirs+=",bt-www-server-data:/www/server/data" |
| 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="bt-default:/www/server/panel/default.pl" |
| 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..." |
| |
| |
| |
| set_space_variable "$space_repo_id" "OPENCLAW_SSH_AGENT_AUTOSTART" "true" "$api_token" |
|
|
| info "Configuring SSH key auto-loading..." |
| |
| |
| 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 |
| ' |
| |
| 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 "$@" |
|
|