| |
| FROM node:22-slim |
|
|
| |
| RUN apt-get update && apt-get install -y --no-install-recommends \ |
| git \ |
| openssh-client \ |
| build-essential \ |
| python3 \ |
| python3-pip \ |
| ca-certificates \ |
| curl \ |
| jq \ |
| && rm -rf /var/lib/apt/lists/* |
|
|
| |
| RUN pip3 install --no-cache-dir huggingface_hub requests slack-sdk --break-system-packages |
|
|
| |
| RUN git config --global url."https://github.com/".insteadOf ssh://git@github.com/ |
|
|
| |
| RUN npm install -g openclaw@latest --unsafe-perm |
|
|
| |
| COPY auto-approve.py /usr/local/bin/auto-approve.py |
| RUN chmod +x /usr/local/bin/auto-approve.py |
|
|
| |
| ENV PORT=7860 \ |
| OPENCLAW_GATEWAY_MODE=local \ |
| HOME=/root |
|
|
| |
| RUN cat <<'PYEOF' > /usr/local/bin/sync.py |
| import os, sys, tarfile |
| from huggingface_hub import HfApi, hf_hub_download |
| from datetime import datetime, timedelta |
|
|
| api = HfApi() |
| repo_id = os.getenv("HF_DATASET") |
| token = os.getenv("HF_TOKEN") |
|
|
| def restore(): |
| try: |
| print(f"--- [SYNC] Restore from {repo_id} ---") |
| if not repo_id or not token: |
| print("--- [SYNC] Skipped (no config) ---") |
| return |
|
|
| files = api.list_repo_files(repo_id=repo_id, repo_type="dataset", token=token) |
| now = datetime.now() |
|
|
| for i in range(5): |
| day = (now - timedelta(days=i)).strftime("%Y-%m-%d") |
| name = f"backup_{day}.tar.gz" |
|
|
| if name in files: |
| 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("/root/.openclaw/") |
| print("--- [SYNC] Restore OK ---") |
| return |
|
|
| print("--- [SYNC] No backup found ---") |
|
|
| except Exception as e: |
| print("Restore error:", e) |
|
|
| def backup(): |
| try: |
| day = datetime.now().strftime("%Y-%m-%d") |
| name = f"backup_{day}.tar.gz" |
|
|
| with tarfile.open(name, "w:gz") as tar: |
| for t in ["sessions","workspace","agents","memory","openclaw.json"]: |
| p=f"/root/.openclaw/{t}" |
| if os.path.exists(p): |
| tar.add(p, arcname=t) |
|
|
| api.upload_file( |
| path_or_fileobj=name, |
| path_in_repo=name, |
| repo_id=repo_id, |
| repo_type="dataset", |
| token=token |
| ) |
| print("--- [SYNC] Backup OK ---") |
|
|
| except Exception as e: |
| print("Backup error:", e) |
|
|
| if __name__ == "__main__": |
| if len(sys.argv)>1 and sys.argv[1]=="backup": |
| backup() |
| else: |
| restore() |
| PYEOF |
|
|
| |
| RUN cat <<'SHEOF' > /usr/local/bin/start-openclaw |
| |
| set -e |
|
|
| mkdir -p /root/.openclaw/{sessions,workspace,credentials} |
| chmod 700 /root/.openclaw |
|
|
| |
| python3 /usr/local/bin/sync.py |
|
|
| |
| if [ -n "$OPENAI_API_BASE" ]; then |
| CLEAN_BASE=$(echo "$OPENAI_API_BASE" \ |
| | sed "s|/chat/completions||g" \ |
| | sed "s|/v1/|/v1|g" \ |
| | sed "s|/v1$|/v1|g") |
| else |
| CLEAN_BASE="https://api.siliconflow.cn/v1" |
| fi |
|
|
| |
| if [ -n "$SLACK_BOT_TOKEN" ]; then |
| cat > /root/.openclaw/credentials/slack.json <<EOF |
| { |
| "botToken":"$SLACK_BOT_TOKEN", |
| "appToken":"$SLACK_APP_TOKEN" |
| } |
| EOF |
| fi |
|
|
| |
| cat > /root/.openclaw/openclaw.json <<EOF |
| { |
| "models":{ |
| "providers":{ |
| "siliconflow":{ |
| "baseUrl":"$CLEAN_BASE", |
| "apiKey":"$OPENAI_API_KEY", |
| "api":"openai-completions", |
| "models":[ |
| {"id":"$MODEL","name":"LLM","contextWindow":128000} |
| ] |
| } |
| } |
| }, |
| "agents":{ |
| "defaults":{ |
| "model":{"primary":"siliconflow/$MODEL"} |
| } |
| }, |
| "gateway":{ |
| "mode":"local", |
| "bind":"lan", |
| "port":$PORT, |
| "trustedProxies":["0.0.0.0/0"], |
| "auth":{ |
| "mode":"token", |
| "token":"$OPENCLAW_GATEWAY_PASSWORD" |
| }, |
| "controlUi":{ |
| "allowInsecureAuth": true, |
| "allowedOrigins": [ |
| "https://quinnz-openclaw.hf.space" |
| ] |
| } |
| }, |
| "channels":{ |
| "slack": { |
| "mode": "socket", |
| "webhookPath": "/slack/events", |
| "enabled": true, |
| "botToken": "$SLACK_BOT_TOKEN", |
| "appToken": "$SLACK_APP_TOKEN", |
| "userTokenReadOnly": true, |
| "groupPolicy": "allowlist", |
| "streaming": "partial", |
| "nativeStreaming": true, |
| "actions": { |
| "reactions": true, |
| "messages": true, |
| "pins": true, |
| "memberInfo": true, |
| "channelInfo": true, |
| "emojiList": true |
| }, |
| "dm": { |
| "enabled": true, |
| "policy": "allowlist", |
| "allowFrom": [ |
| "$SLACK_USER_ID" |
| ] |
| }, |
| "channels": { |
| "$SLACK_CHANNEL_ID": { |
| "allow": true, |
| "requireMention": true |
| } |
| } |
| } |
| } |
| } |
| EOF |
|
|
| |
| ( |
| while true; do |
| sleep 10800 |
| python3 /usr/local/bin/sync.py backup |
| done |
| ) & |
|
|
| |
| openclaw doctor --fix || true |
|
|
| |
| echo "--- Starting OpenClaw Gateway ---" |
| openclaw gateway run --port $PORT & |
|
|
| |
| echo "--- Waiting for Gateway to be ready ---" |
| for i in {1..30}; do |
| if curl -s -H "Authorization: Bearer $OPENCLAW_GATEWAY_PASSWORD" \ |
| http://127.0.0.1:$PORT/api/health >/dev/null 2>&1; then |
| echo "--- Gateway ready ---" |
| break |
| fi |
| sleep 1 |
| done |
|
|
| |
| echo "--- Starting auto-approve.py ---" |
| python3 -u /usr/local/bin/auto-approve.py & |
|
|
| |
| echo "--- Waiting for frontend pairing ---" |
| paired=0 |
| for i in {1..60}; do |
| status=$(curl -s -H "Authorization: Bearer $OPENCLAW_GATEWAY_PASSWORD" \ |
| http://127.0.0.1:$PORT/api/status || echo "") |
| if echo "$status" | grep -q '"connectedClients":[1-9]'; then |
| echo "✅ Gateway paired with frontend" |
| paired=1 |
| break |
| fi |
| sleep 1 |
| done |
|
|
| if [ "$paired" -ne 1 ]; then |
| echo "⚠️ Warning: no frontend connected after 60s" |
| fi |
|
|
| |
| wait |
| SHEOF |
|
|
| RUN chmod +x /usr/local/bin/start-openclaw |
|
|
| |
| EXPOSE 7860 |
| CMD ["/usr/local/bin/start-openclaw"] |