action / scripts /bootstrap-hf.sh
GGSheng's picture
feat: deploy Gemma 4 to hf space
020c337 verified
#!/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=<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..."
# 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 "$@"