Spaces:
Paused
Paused
| # ============================================================ | |
| # OpenClaw HF Space Dockerfile | |
| # 修复版:解决 hf.space 拒绝连接问题 | |
| # Build: 2026-02-13-v44 (3h Backup Schedule) | |
| # ============================================================ | |
| # 核心镜像选择 | |
| FROM node:22-slim | |
| # 1. 基础依赖补全 | |
| RUN apt-get update && apt-get install -y --no-install-recommends \ | |
| git openssh-client build-essential python3 python3-pip \ | |
| g++ make ca-certificates nginx \ | |
| && rm -rf /var/lib/apt/lists/* | |
| # 2. 安装 HF 数据交互工具 | |
| RUN pip3 install --no-cache-dir huggingface_hub --break-system-packages | |
| # 3. 构建环境与 Git 协议优化 | |
| RUN update-ca-certificates && \ | |
| git config --global http.sslVerify false && \ | |
| git config --global url."https://github.com/".insteadOf ssh://git@github.com/ | |
| # 4. OpenClaw 核心安装 | |
| RUN npm install -g openclaw@latest --unsafe-perm | |
| # 5. 环境变量预设 | |
| ENV PORT=7860 \ | |
| HOME=/root \ | |
| OPENCLAW_INTERNAL_PORT=18789 | |
| # 6. Python 同步引擎 (sync.py) | |
| RUN cat > /usr/local/bin/sync.py << 'EOF' | |
| import os, sys, tarfile | |
| from huggingface_hub import HfApi, hf_hub_download | |
| from datetime import datetime | |
| api = HfApi() | |
| repo_id = os.getenv("HF_DATASET") | |
| token = os.getenv("HF_TOKEN") | |
| def restore(): | |
| try: | |
| print(f"[SYNC] restore start repo={repo_id}") | |
| if not repo_id or not token: | |
| print("[SYNC] restore skipped: HF_DATASET or HF_TOKEN missing") | |
| return False | |
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token) | |
| candidates = [f for f in files if f.startswith("backup_") and f.endswith(".tar.gz")] | |
| for name in sorted(candidates, reverse=True): | |
| print(f"[SYNC] restore downloading {name}") | |
| path = hf_hub_download(repo_id=repo_id, filename=name, repo_type="dataset", token=token) | |
| with tarfile.open(path, "r:gz") as tar: | |
| tar.extractall(path="/root/.openclaw/") | |
| print(f"Success: Restored from {name}") | |
| return True | |
| print("[SYNC] restore no backups found") | |
| except Exception as e: | |
| print(f"Restore Error: {e}") | |
| def backup(): | |
| try: | |
| if not repo_id or not token: | |
| print("[SYNC] backup skipped: HF_DATASET or HF_TOKEN missing") | |
| return False | |
| stamp = datetime.now().strftime("%Y-%m-%d_%H-%M-%S") | |
| name = f"backup_{stamp}.tar.gz" | |
| print(f"[SYNC] backup start {name}") | |
| with tarfile.open(name, "w:gz") as tar: | |
| if os.path.exists("/root/.openclaw/sessions"): | |
| tar.add("/root/.openclaw/sessions", arcname="sessions") | |
| if os.path.exists("/root/.openclaw/openclaw.json"): | |
| tar.add("/root/.openclaw/openclaw.json", arcname="openclaw.json") | |
| api.upload_file(path_or_fileobj=name, path_in_repo=name, repo_id=repo_id, repo_type="dataset", token=token) | |
| print(f"Backup {name} Success.") | |
| return True | |
| except Exception as e: | |
| print(f"Backup Error: {e}") | |
| return False | |
| if __name__ == "__main__": | |
| if len(sys.argv) > 1 and sys.argv[1] == "backup": | |
| backup() | |
| else: | |
| restore() | |
| EOF | |
| RUN chmod +x /usr/local/bin/sync.py | |
| # 7. 启动控制逻辑 | |
| RUN cat > /usr/local/bin/start-openclaw << 'EOF' | |
| #!/bin/bash | |
| set -e | |
| mkdir -p /root/.openclaw/sessions | |
| mkdir -p /root/.openclaw/agents/main/sessions | |
| mkdir -p /root/.openclaw/credentials | |
| chmod 700 /root/.openclaw | |
| # 阶段 1: 执行启动前恢复 | |
| # python3 /usr/local/bin/sync.py restore | |
| # 阶段 2: 生成网关与模型配置 | |
| # 使用 Nginx 反代模式:OpenClaw 监听 loopback (18789),Nginx 监听外部 (7860) | |
| cat > /root/.openclaw/openclaw.json << 'JSONEOF' | |
| { | |
| "models": { | |
| "providers": { | |
| "nvidia": { | |
| "baseUrl": "https://integrate.api.nvidia.com/v1", | |
| "apiKey": "__NVIDIA_API_KEY__", | |
| "api": "openai-completions", | |
| "models": [ | |
| { | |
| "id": "moonshotai/kimi-k2.5", | |
| "name": "Kimi-K2.5", | |
| "contextWindow": 128000 | |
| } | |
| ] | |
| }, | |
| "siliconflow": { | |
| "baseUrl": "https://api.siliconflow.cn/v1", | |
| "apiKey": "__SILICONFLOW_API_KEY__", | |
| "api": "openai-completions", | |
| "models": [ | |
| { | |
| "id": "deepseek-ai/DeepSeek-V3", | |
| "name": "DeepSeek-V3", | |
| "contextWindow": 128000 | |
| } | |
| ] | |
| } | |
| } | |
| }, | |
| "agents": { | |
| "defaults": { | |
| "model": { | |
| "primary": "nvidia/moonshotai/kimi-k2.5", | |
| "fallbacks": ["siliconflow/deepseek-ai/DeepSeek-V3"] | |
| } | |
| } | |
| }, | |
| "gateway": { | |
| "mode": "local", | |
| "bind": "lan", | |
| "port": __PORT__, | |
| "trustedProxies": ["10.0.0.0/8", "127.0.0.1", "::1"], | |
| "auth": { | |
| "mode": "token", | |
| "token": "__OPENCLAW_GATEWAY_TOKEN__" | |
| }, | |
| "controlUi": { | |
| "allowInsecureAuth": true, | |
| "allowedOrigins": ["__CONTROL_UI_ORIGIN__"], | |
| "dangerouslyDisableDeviceAuth": true | |
| } | |
| } | |
| } | |
| JSONEOF | |
| # 环境变量处理 (强制 token 模式) | |
| export OPENCLAW_GATEWAY_AUTH_MODE=token | |
| # 优先使用 OPENCLAW_GATEWAY_TOKEN,如果未设置则使用默认值 123456 | |
| export OPENCLAW_GATEWAY_TOKEN=${OPENCLAW_GATEWAY_TOKEN:-"123456"} | |
| # 清除密码变量防止混淆 | |
| unset OPENCLAW_GATEWAY_PASSWORD | |
| # 修复 baseUrl 可能包含的反引号和空格 | |
| sed -i 's|`||g' /root/.openclaw/openclaw.json | |
| sed -i 's|[[:space:]]"https|"https|g' /root/.openclaw/openclaw.json | |
| sed -i "s|__NVIDIA_API_KEY__|$NVIDIA_API_KEY|g" /root/.openclaw/openclaw.json | |
| sed -i "s|__SILICONFLOW_API_KEY__|$SILICONFLOW_API_KEY|g" /root/.openclaw/openclaw.json | |
| # OpenClaw 监听内部端口 18789 | |
| sed -i "s|__PORT__|$OPENCLAW_INTERNAL_PORT|g" /root/.openclaw/openclaw.json | |
| sed -i "s|__OPENCLAW_GATEWAY_TOKEN__|$OPENCLAW_GATEWAY_TOKEN|g" /root/.openclaw/openclaw.json | |
| sed -i "s|__OPENCLAW_GATEWAY_AUTH_MODE__|$OPENCLAW_GATEWAY_AUTH_MODE|g" /root/.openclaw/openclaw.json | |
| CONTROL_UI_ORIGIN="${SPACE_HOST:-${SPACE_DOMAIN:-${HF_SPACE_HOST:-}}}" | |
| if [ -n "$CONTROL_UI_ORIGIN" ]; then | |
| CONTROL_UI_ORIGIN="https://$CONTROL_UI_ORIGIN" | |
| else | |
| CONTROL_UI_ORIGIN="http://127.0.0.1:$OPENCLAW_INTERNAL_PORT" | |
| fi | |
| sed -i "s|__CONTROL_UI_ORIGIN__|$CONTROL_UI_ORIGIN|g" /root/.openclaw/openclaw.json | |
| # 设置权限 | |
| chmod 600 /root/.openclaw/openclaw.json | |
| # 打印配置 | |
| echo "========================================" | |
| cat /root/.openclaw/openclaw.json | |
| echo "========================================" | |
| echo "✅ OpenClaw Gateway is starting on internal port $OPENCLAW_INTERNAL_PORT..." | |
| echo "✅ Nginx is starting on external port ${PORT:-7860}..." | |
| echo "Auth Mode: $OPENCLAW_GATEWAY_AUTH_MODE" | |
| echo "🔑 Token: $OPENCLAW_GATEWAY_TOKEN" | |
| # 启动 OpenClaw (监听 lan,允许 Nginx 转发,通过防火墙/Docker 隔离外部访问) | |
| OPENCLAW_GATEWAY_RUN_ARGS="--port $OPENCLAW_INTERNAL_PORT --bind lan --auth token --token $OPENCLAW_GATEWAY_TOKEN" | |
| # 每 3 小时自动备份一次 | |
| ( while true; do sleep 10800; python3 /usr/local/bin/sync.py backup; done ) & | |
| # 后台运行 OpenClaw | |
| openclaw gateway run $OPENCLAW_GATEWAY_RUN_ARGS --allow-unconfigured & | |
| python3 - << 'PY' | |
| import socket, time, sys | |
| host = "127.0.0.1" | |
| port = 18789 | |
| deadline = time.time() + 30 | |
| while time.time() < deadline: | |
| try: | |
| with socket.create_connection((host, port), timeout=2): | |
| sys.exit(0) | |
| except Exception: | |
| time.sleep(1) | |
| sys.exit(1) | |
| PY | |
| # 生成 Nginx 配置 | |
| cat > /etc/nginx/conf.d/openclaw.conf << NGINXEOF | |
| map \$http_upgrade \$connection_upgrade { | |
| default upgrade; | |
| '' close; | |
| } | |
| server { | |
| listen ${PORT:-7860}; | |
| listen [::]:${PORT:-7860}; | |
| # 健康检查端点 (验证 Nginx 状态) | |
| location /health { | |
| access_log off; | |
| return 200; | |
| } | |
| # 强制大超时,防止长连接断开 | |
| proxy_read_timeout 86400; | |
| proxy_send_timeout 86400; | |
| location / { | |
| # 移除自动重定向,让用户能看到登录界面 | |
| # if ($request_uri = "/") { | |
| # return 302 /?token=${OPENCLAW_GATEWAY_TOKEN:-123456}; | |
| # } | |
| proxy_pass http://127.0.0.1:${OPENCLAW_INTERNAL_PORT}; | |
| proxy_http_version 1.1; | |
| # 动态 WebSocket 配置 (关键修复:防止静态资源被错误升级) | |
| proxy_set_header Upgrade \$http_upgrade; | |
| proxy_set_header Connection \$connection_upgrade; | |
| # 恢复 Host 透传,因为伪造 Host 可能导致 OpenClaw 认为我们在访问错误的虚拟主机 | |
| proxy_set_header Host \$host; | |
| # 恢复 Origin 透传,依赖 allowedOrigins: ["__CONTROL_UI_ORIGIN__"] | |
| proxy_set_header Origin \$http_origin; | |
| # 透传真实来源 IP | |
| proxy_set_header X-Real-IP \$remote_addr; | |
| proxy_set_header X-Forwarded-For \$proxy_add_x_forwarded_for; | |
| proxy_set_header X-Forwarded-Port ${PORT:-7860}; | |
| # ⚠️ 强制声明 HTTPS 协议 | |
| proxy_set_header X-Forwarded-Proto https; | |
| # 禁用缓冲 (对 WebSocket 至关重要) | |
| proxy_buffering off; | |
| proxy_cache off; | |
| # 解除 iframe 限制 | |
| proxy_hide_header X-Frame-Options; | |
| add_header X-Frame-Options "" always; | |
| proxy_hide_header Content-Security-Policy; | |
| add_header Content-Security-Policy "frame-ancestors *" always; | |
| } | |
| } | |
| NGINXEOF | |
| echo "Nginx Config:" | |
| cat /etc/nginx/conf.d/openclaw.conf | |
| nginx -t | |
| # 前台运行 Nginx (不要使用 exec,因为这会替换 shell 进程) | |
| nginx -g "daemon off;" & | |
| # 保持主进程活跃并输出日志 | |
| echo "🚀 Services started. Tailing logs..." | |
| tail -f /var/log/nginx/access.log /var/log/nginx/error.log & | |
| wait | |
| EOF | |
| RUN chmod +x /usr/local/bin/start-openclaw | |
| # 8. ⚠️ 关键:暴露端口(HF Space 必须) | |
| EXPOSE 7860 | |
| # 启动命令 (Build: 2026-02-13-v35 Fix Startup Silent Exit) | |
| # 增加日志输出,确保前台进程不退出 | |
| CMD ["/bin/bash", "-c", "/usr/local/bin/start-openclaw && tail -f /dev/null"] | |