| FROM node:20-slim |
|
|
| ENV NODE_ENV=production \ |
| PORT=7860 \ |
| HOSTNAME=0.0.0.0 \ |
| NEXT_TELEMETRY_DISABLED=1 |
|
|
| RUN apt-get update && apt-get install -y --no-install-recommends \ |
| python3 python3-pip curl procps build-essential ca-certificates \ |
| && rm -rf /var/lib/apt/lists/* |
|
|
| RUN pip3 install --break-system-packages --no-cache-dir huggingface_hub |
|
|
| RUN npm install -g 9router@0.4.52 --prefer-online --no-audit --no-fund \ |
| && mkdir -p /root/.9router/runtime \ |
| && cd /root/.9router/runtime \ |
| && npm install sql.js@1.13.0 better-sqlite3@12.6.2 --no-audit --no-fund --prefer-online |
|
|
| WORKDIR /app |
|
|
| RUN cat > /app/sync.py <<'PY' |
| import os |
| import tarfile |
| import time |
| import sys |
| from pathlib import Path |
| from huggingface_hub import HfApi, hf_hub_download |
|
|
| REPO_ID = os.environ.get("DATASET_REPO") |
| TOKEN = os.environ.get("HF_TOKEN") |
| BACKUP_FILE = "/tmp/9router_backup.tar.gz" |
| DATA_DIR = Path("/root/.9router") |
|
|
|
|
| def safe_extract(tar, path): |
| base = Path(path).resolve() |
| for member in tar.getmembers(): |
| target = (base / member.name).resolve() |
| if not str(target).startswith(str(base)): |
| raise RuntimeError(f"unsafe tar member: {member.name}") |
| tar.extractall(path) |
|
|
|
|
| def upload(): |
| if not REPO_ID or not TOKEN: |
| print(">>> [Sync] DATASET_REPO/HF_TOKEN 未配置,跳过备份") |
| return |
| if not DATA_DIR.exists(): |
| print(">>> [Sync] 数据目录不存在,跳过备份") |
| return |
| try: |
| with tarfile.open(BACKUP_FILE, "w:gz") as tar: |
| tar.add(str(DATA_DIR), arcname=".9router") |
| HfApi(token=TOKEN).upload_file( |
| path_or_fileobj=BACKUP_FILE, |
| path_in_repo="9router_backup.tar.gz", |
| repo_id=REPO_ID, |
| repo_type="dataset", |
| ) |
| print(">>> [Sync] 备份成功: " + time.strftime("%Y-%m-%d %H:%M:%S"), flush=True) |
| except Exception as e: |
| print(">>> [Sync] 备份失败: " + str(e), flush=True) |
|
|
|
|
| def download(): |
| if not REPO_ID or not TOKEN: |
| print(">>> [Sync] DATASET_REPO/HF_TOKEN 未配置,跳过恢复") |
| return |
| try: |
| print(">>> [Sync] 正在从 Dataset 下载数据...", flush=True) |
| path = hf_hub_download( |
| repo_id=REPO_ID, |
| filename="9router_backup.tar.gz", |
| repo_type="dataset", |
| token=TOKEN, |
| ) |
| with tarfile.open(path, "r:gz") as tar: |
| safe_extract(tar, "/root") |
| print(">>> [Sync] 数据恢复成功", flush=True) |
| except Exception as e: |
| print(">>> [Sync] 跳过恢复: " + str(e), flush=True) |
|
|
|
|
| if __name__ == "__main__": |
| if len(sys.argv) > 1 and sys.argv[1] == "upload": |
| upload() |
| elif len(sys.argv) > 1 and sys.argv[1] == "download": |
| download() |
| PY |
|
|
| RUN cat > /app/run.sh <<'SH' |
| #!/bin/bash |
| set -euo pipefail |
|
|
| mkdir -p /root/.9router/runtime |
|
|
| echo '=== 1. 数据恢复 ===' |
| python3 /app/sync.py download |
|
|
| echo '=== 2. 启动定时备份 (每10分钟一次) ===' |
| (while true; do sleep 600; python3 /app/sync.py upload; done) & |
|
|
| echo '=== 3. 启动 9router on 7860 ===' |
| export PORT=7860 |
| export HOSTNAME=0.0.0.0 |
| export NEXT_PUBLIC_BASE_URL="https://${SPACE_HOST:-jualhosting-9router.hf.space}" |
|
|
| # 关键修复:不能用 `printf '\n' | 9router`。 |
| # 9router 在 stdin 关闭后会触发 SIGINT/退出菜单,HF 看到 Exit code 0 后判定 RUNTIME_ERROR。 |
| # --tray + --skip-update 会跳过交互菜单,并让 Node 主进程常驻。 |
| exec 9router --tray --skip-update --no-browser --log --port 7860 --host 0.0.0.0 |
| SH |
|
|
| RUN chmod +x /app/run.sh |
|
|
| EXPOSE 7860 |
| CMD ["/bin/bash", "/app/run.sh"] |
|
|