Spaces:
Sleeping
Sleeping
ONEWECHAT
fix: use WEB_PASSWORD for admin API auth (admin_password takes priority over api_key)
6c0ebfd | # ============================================================ | |
| # Antigravity Manager - HF Spaces Entrypoint | |
| # Supabase Storage 数据持久化(变更检测 + 空文件保护) | |
| # ============================================================ | |
| set -e | |
| DATA_DIR="${ABV_DATA_DIR:-/home/user/.antigravity_tools}" | |
| SYNC_INTERVAL="${SUPABASE_SYNC_INTERVAL:-300}" | |
| SNAPSHOT_DIR="/tmp/.sync_snapshot" | |
| EXPORT_FILE="/tmp/.accounts_export.json" | |
| EXPORT_REMOTE="config/accounts_export.json" | |
| mkdir -p "$DATA_DIR/accounts" "$SNAPSHOT_DIR" | |
| # ============================================================ | |
| # Supabase Storage API | |
| # ============================================================ | |
| supabase_enabled() { | |
| [ -n "$SUPABASE_URL" ] && [ -n "$SUPABASE_KEY" ] && [ -n "$SUPABASE_BUCKET" ] | |
| } | |
| sb_download() { | |
| local remote="$1" local_path="$2" | |
| local code | |
| code=$(curl -sf -o "$local_path" -w "%{http_code}" \ | |
| "${SUPABASE_URL}/storage/v1/object/${SUPABASE_BUCKET}/${remote}" \ | |
| -H "Authorization: Bearer ${SUPABASE_KEY}" \ | |
| -H "apikey: ${SUPABASE_KEY}" 2>/dev/null) || true | |
| if [ "$code" = "200" ]; then | |
| echo "[sync] ✓ downloaded: $remote" | |
| return 0 | |
| else | |
| rm -f "$local_path"; return 1 | |
| fi | |
| } | |
| sb_upload() { | |
| local local_path="$1" remote="$2" | |
| [ -f "$local_path" ] || return 1 | |
| local code | |
| code=$(curl -sf -o /dev/null -w "%{http_code}" \ | |
| -X POST \ | |
| "${SUPABASE_URL}/storage/v1/object/${SUPABASE_BUCKET}/${remote}" \ | |
| -H "Authorization: Bearer ${SUPABASE_KEY}" \ | |
| -H "apikey: ${SUPABASE_KEY}" \ | |
| -H "x-upsert: true" \ | |
| -F "file=@${local_path}" 2>/dev/null) || true | |
| if [ "$code" = "200" ] || [ "$code" = "201" ]; then | |
| echo "[sync] ✓ uploaded: $remote"; return 0 | |
| else | |
| echo "[sync] ✗ upload failed ($code): $remote"; return 1 | |
| fi | |
| } | |
| sb_list() { | |
| local prefix="$1" | |
| curl -sf \ | |
| "${SUPABASE_URL}/storage/v1/object/list/${SUPABASE_BUCKET}" \ | |
| -H "Authorization: Bearer ${SUPABASE_KEY}" \ | |
| -H "apikey: ${SUPABASE_KEY}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"prefix\":\"${prefix}\",\"limit\":1000}" 2>/dev/null | |
| } | |
| # ============================================================ | |
| # 快照:下载后记录哈希,同步时只上传有变化的文件 | |
| # ============================================================ | |
| file_hash() { md5sum "$1" 2>/dev/null | cut -d' ' -f1; } | |
| save_snapshot() { | |
| local file="$1" key="$2" | |
| [ -f "$file" ] && file_hash "$file" > "$SNAPSHOT_DIR/$key" | |
| } | |
| file_changed() { | |
| local file="$1" key="$2" | |
| [ ! -f "$file" ] && return 1 | |
| local old new | |
| old=$(cat "$SNAPSHOT_DIR/$key" 2>/dev/null || echo "") | |
| new=$(file_hash "$file") | |
| [ "$old" != "$new" ] | |
| } | |
| upload_if_changed() { | |
| local file="$1" remote="$2" key="$3" | |
| if file_changed "$file" "$key"; then | |
| if sb_upload "$file" "$remote"; then | |
| save_snapshot "$file" "$key" | |
| fi | |
| fi | |
| } | |
| # ============================================================ | |
| # 导出文件导入:从 Supabase 下载 accounts_export.json 并通过 API 导入 | |
| # ============================================================ | |
| import_export_file() { | |
| supabase_enabled || return 0 | |
| local tmp_export="/tmp/.accounts_export_new.json" | |
| sb_download "$EXPORT_REMOTE" "$tmp_export" || { echo "[import] No export file on Supabase"; return 0; } | |
| # 检查文件是否有变化 | |
| local old_hash new_hash | |
| old_hash=$(file_hash "$EXPORT_FILE" 2>/dev/null || echo "none") | |
| new_hash=$(file_hash "$tmp_export") | |
| if [ "$old_hash" = "$new_hash" ]; then | |
| echo "[import] Export file unchanged, skipping" | |
| rm -f "$tmp_export" | |
| return 0 | |
| fi | |
| echo "[import] Export file updated, importing accounts..." | |
| cp "$tmp_export" "$EXPORT_FILE" | |
| rm -f "$tmp_export" | |
| # 读取导出文件中的每个账号,通过 API 导入 | |
| # 管理接口优先使用 admin_password (WEB_PASSWORD),没有则回退 api_key | |
| local port="${PORT:-7860}" | |
| local auth_token="${WEB_PASSWORD:-}" | |
| [ -z "$auth_token" ] && auth_token="${API_KEY:-}" | |
| [ -z "$auth_token" ] && auth_token=$(jq -r '.admin_password // .api_key // empty' "$DATA_DIR/gui_config.json" 2>/dev/null || echo "") | |
| local count=0 total | |
| total=$(jq 'length' "$EXPORT_FILE" 2>/dev/null || echo "0") | |
| for i in $(seq 0 $((total - 1))); do | |
| local email rt | |
| email=$(jq -r ".[$i].email // empty" "$EXPORT_FILE") | |
| rt=$(jq -r ".[$i].refresh_token // empty" "$EXPORT_FILE") | |
| [ -z "$rt" ] && continue | |
| # 调用 API 添加账号(已存在的会自动更新 token) | |
| local resp code | |
| code=$(curl -sf -o /tmp/.import_resp.json -w "%{http_code}" \ | |
| -X POST "http://127.0.0.1:${port}/api/accounts" \ | |
| -H "Authorization: Bearer ${auth_token}" \ | |
| -H "Content-Type: application/json" \ | |
| -d "{\"refreshToken\":\"${rt}\"}" 2>/dev/null) || true | |
| if [ "$code" = "200" ] || [ "$code" = "201" ]; then | |
| echo "[import] ✓ imported: ${email:-unknown}" | |
| count=$((count + 1)) | |
| else | |
| local err | |
| err=$(jq -r '.error // empty' /tmp/.import_resp.json 2>/dev/null || echo "HTTP $code") | |
| echo "[import] ✗ failed: ${email:-unknown} ($err)" | |
| fi | |
| done | |
| echo "[import] Done: $count/$total accounts imported" | |
| # 导入后重新保存快照(账号文件已被 app 更新) | |
| sleep 2 | |
| save_snapshot "$DATA_DIR/accounts.json" "accounts.json" | |
| for f in "$DATA_DIR/accounts"/*.json; do | |
| [ -f "$f" ] && save_snapshot "$f" "acc_$(basename "$f")" | |
| done | |
| } | |
| # ============================================================ | |
| # 恢复数据 | |
| # ============================================================ | |
| restore_data() { | |
| supabase_enabled || { echo "[sync] Supabase not configured, skipping"; return 0; } | |
| echo "[sync] Restoring data from Supabase..." | |
| sb_download "config/gui_config.json" "$DATA_DIR/gui_config.json" || true | |
| sb_download "config/accounts.json" "$DATA_DIR/accounts.json" || true | |
| local file_list | |
| file_list=$(sb_list "accounts/") | |
| if [ -n "$file_list" ] && [ "$file_list" != "[]" ]; then | |
| echo "$file_list" | jq -r '.[].name // empty' 2>/dev/null | while read -r fname; do | |
| [ -n "$fname" ] && [ "$fname" != ".emptyFolderPlaceholder" ] && \ | |
| sb_download "accounts/${fname}" "$DATA_DIR/accounts/${fname}" || true | |
| done | |
| fi | |
| sb_download "db/token_stats.db" "$DATA_DIR/token_stats.db" || true | |
| sb_download "db/security.db" "$DATA_DIR/security.db" || true | |
| sb_download "db/user_tokens.db" "$DATA_DIR/user_tokens.db" || true | |
| echo "[sync] Restore complete" | |
| # 验证 | |
| if [ -f "$DATA_DIR/accounts.json" ]; then | |
| local count | |
| count=$(jq '.accounts | length' "$DATA_DIR/accounts.json" 2>/dev/null || echo "?") | |
| echo "[sync] accounts.json contains $count account(s)" | |
| fi | |
| # 保存快照基准(下载后的状态) | |
| save_snapshot "$DATA_DIR/gui_config.json" "gui_config.json" | |
| save_snapshot "$DATA_DIR/accounts.json" "accounts.json" | |
| save_snapshot "$DATA_DIR/token_stats.db" "token_stats.db" | |
| save_snapshot "$DATA_DIR/security.db" "security.db" | |
| save_snapshot "$DATA_DIR/user_tokens.db" "user_tokens.db" | |
| for f in "$DATA_DIR/accounts"/*.json; do | |
| [ -f "$f" ] && save_snapshot "$f" "acc_$(basename "$f")" | |
| done | |
| } | |
| # ============================================================ | |
| # 同步数据(只上传变化的文件 + 空文件保护) | |
| # ============================================================ | |
| sync_data() { | |
| supabase_enabled || return 0 | |
| echo "[sync] Checking for changes... ($(date '+%H:%M:%S'))" | |
| upload_if_changed "$DATA_DIR/gui_config.json" "config/gui_config.json" "gui_config.json" | |
| # accounts.json 保护:0 accounts 绝不上传 | |
| if [ -f "$DATA_DIR/accounts.json" ]; then | |
| local acc_count | |
| acc_count=$(jq '.accounts | length' "$DATA_DIR/accounts.json" 2>/dev/null || echo "0") | |
| if [ "$acc_count" -gt 0 ]; then | |
| upload_if_changed "$DATA_DIR/accounts.json" "config/accounts.json" "accounts.json" | |
| else | |
| echo "[sync] ⚠ Skipping accounts.json (0 accounts, protecting remote)" | |
| fi | |
| fi | |
| for f in "$DATA_DIR/accounts"/*.json; do | |
| [ -f "$f" ] && upload_if_changed "$f" "accounts/$(basename "$f")" "acc_$(basename "$f")" | |
| done | |
| upload_if_changed "$DATA_DIR/token_stats.db" "db/token_stats.db" "token_stats.db" | |
| upload_if_changed "$DATA_DIR/security.db" "db/security.db" "security.db" | |
| upload_if_changed "$DATA_DIR/user_tokens.db" "db/user_tokens.db" "user_tokens.db" | |
| # 检查 Supabase 上的导出文件是否有更新,有则自动导入 | |
| import_export_file | |
| echo "[sync] Done" | |
| } | |
| background_sync() { | |
| while true; do sleep "$SYNC_INTERVAL"; sync_data; done | |
| } | |
| graceful_shutdown() { | |
| echo "[hf] Shutdown, final sync..." | |
| sync_data; exit 0 | |
| } | |
| trap graceful_shutdown SIGTERM SIGINT | |
| # ============================================================ | |
| # 主流程 | |
| # ============================================================ | |
| echo "============================================" | |
| echo " Antigravity Manager - HF Spaces" | |
| echo "============================================" | |
| echo " Data dir : $DATA_DIR" | |
| echo " Port : ${PORT:-7860}" | |
| echo " Supabase : $(supabase_enabled && echo 'enabled' || echo 'disabled')" | |
| echo "============================================" | |
| restore_data | |
| CONFIG_FILE="$DATA_DIR/gui_config.json" | |
| HF_PORT="${PORT:-7860}" | |
| if [ -f "$CONFIG_FILE" ]; then | |
| jq --argjson port "$HF_PORT" '.proxy.port = $port' "$CONFIG_FILE" > "${CONFIG_FILE}.tmp" && \ | |
| mv "${CONFIG_FILE}.tmp" "$CONFIG_FILE" | |
| echo "[hf] Port set to $HF_PORT" | |
| else | |
| echo "{\"proxy\":{\"port\":${HF_PORT}}}" > "$CONFIG_FILE" | |
| echo "[hf] Created config with port $HF_PORT" | |
| fi | |
| if supabase_enabled; then | |
| echo "[sync] Background sync every ${SYNC_INTERVAL}s (change-based)" | |
| background_sync & | |
| fi | |
| # 启动应用(后台),等待就绪后导入导出文件 | |
| /app/antigravity-tools --headless & | |
| APP_PID=$! | |
| # 等待服务就绪 | |
| echo "[hf] Waiting for app to start..." | |
| for i in $(seq 1 30); do | |
| if curl -sf "http://127.0.0.1:${HF_PORT}/health" >/dev/null 2>&1; then | |
| echo "[hf] App ready" | |
| break | |
| fi | |
| sleep 1 | |
| done | |
| # 首次导入导出文件 | |
| import_export_file | |
| # 等待应用进程 | |
| wait $APP_PID || true |